From 8f7ebfdbfe191348cfe0e8b042bd15d2e8abeb5c Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Fri, 13 Sep 2019 12:23:34 +0100 Subject: [PATCH 1/6] Replace numeric error codes with strings Fixes https://github.com/share/sharedb/issues/287 Fixes https://github.com/share/sharedb/issues/198 This is a breaking change that moves us from using numeric error codes to string-based codes [similar to Node][1]. The motivation for this is: - more descriptive error codes - stop implying that errors can be broadly classified together (in the same way that HTTP errors might) This change also exposes these codes on `ShareDBError.code`, so that they are easily discoverable, and can be referenced by consumers for programmatic error handling. [1]: https://nodejs.org/api/errors.html#nodejs-error-codes --- README.md | 59 +------------------------ lib/agent.js | 15 ++++--- lib/backend.js | 17 +++++-- lib/client/connection.js | 13 ++++-- lib/client/doc.js | 34 ++++++++------ lib/db/index.js | 12 ++--- lib/error.js | 40 ++++++++++++++++- lib/milestone-db/index.js | 19 ++++++-- lib/milestone-db/memory.js | 18 +++++--- lib/ot.js | 51 +++++++++++---------- lib/projections.js | 5 ++- lib/pubsub/index.js | 8 ++-- lib/query-emitter.js | 4 +- lib/submit-request.js | 30 +++++++++---- test/backend.js | 2 +- test/client/snapshot-version-request.js | 2 +- test/client/submit.js | 4 +- test/milestone-db.js | 12 ++--- test/pubsub-memory.js | 10 ++--- 19 files changed, 201 insertions(+), 154 deletions(-) diff --git a/README.md b/README.md index 034ab59fb..7dc3aed3c 100644 --- a/README.md +++ b/README.md @@ -651,69 +651,14 @@ ShareDB only supports the following logger methods: - `error` -## Error codes +## Errors ShareDB returns errors as plain JavaScript objects with the format: ``` { - code: 5000, + code: 'ERR_UNKNOWN_ERROR', message: 'ShareDB internal error' } ``` Additional fields may be added to the error object for debugging context depending on the error. Common additional fields include `collection`, `id`, and `op`. - -### 4000 - Bad request - -* 4001 - Unknown error type -* 4002 - Database adapter does not support subscribe -* 4003 - Database adapter not found -* 4004 - Missing op -* 4005 - Op must be an array -* 4006 - Create data in op must be an object -* 4007 - Create op missing type -* 4008 - Unknown type -* 4009 - del value must be true -* 4010 - Missing op, create or del -* 4011 - Invalid src -* 4012 - Invalid seq -* 4013 - Found seq but not src -* 4014 - op.m invalid -* 4015 - Document does not exist -* 4016 - Document already exists -* 4017 - Document was deleted -* 4018 - Document was created remotely -* 4019 - Invalid protocol version -* 4020 - Invalid default type -* 4021 - Invalid client id -* 4022 - Database adapter does not support queries -* 4023 - Cannot project snapshots of this type -* 4024 - Invalid version -* 4025 - Passing options to subscribe has not been implemented - -### 5000 - Internal error - -The `41xx` and `51xx` codes are reserved for use by ShareDB DB adapters, and the `42xx` and `52xx` codes are reserved for use by ShareDB PubSub adapters. - -* 5001 - No new ops returned when retrying unsuccessful submit -* 5002 - Missing snapshot -* 5003 - Snapshot and op version don't match -* 5004 - Missing op -* 5005 - Missing document -* 5006 - Version mismatch -* 5007 - Invalid state transition -* 5008 - Missing version in snapshot -* 5009 - Cannot ingest snapshot with null version -* 5010 - No op to send -* 5011 - Commit DB method unimplemented -* 5012 - getSnapshot DB method unimplemented -* 5013 - getOps DB method unimplemented -* 5014 - queryPollDoc DB method unimplemented -* 5015 - _subscribe PubSub method unimplemented -* 5016 - _unsubscribe PubSub method unimplemented -* 5017 - _publish PubSub method unimplemented -* 5018 - Required QueryEmitter listener not assigned -* 5019 - getMilestoneSnapshot MilestoneDB method unimplemented -* 5020 - saveMilestoneSnapshot MilestoneDB method unimplemented -* 5021 - getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented -* 5022 - getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented diff --git a/lib/agent.js b/lib/agent.js index 1cc9d264f..3f6ad74e4 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -2,6 +2,9 @@ var hat = require('hat'); var types = require('./types'); var util = require('./util'); var logger = require('./logger'); +var ShareDBError = require('./error'); + +var ERROR_CODE = ShareDBError.code; /** * Agent deserializes the wire protocol messages received from the stream and @@ -232,7 +235,7 @@ Agent.prototype._sendOpsBulk = function(collection, opsMap) { function getReplyErrorObject(err) { if (typeof err === 'string') { return { - code: 4001, + code: ERROR_CODE.UNKNOWN_ERROR, message: err }; } else { @@ -287,7 +290,7 @@ Agent.prototype._open = function() { if (agent.closed) return; if (typeof chunk !== 'object') { - var err = {code: 4000, message: 'Received non-object message'}; + var err = {code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: 'Received non-object message'}; return agent.close(err); } @@ -330,7 +333,7 @@ Agent.prototype._checkRequest = function(request) { Agent.prototype._handleMessage = function(request, callback) { try { var errMessage = this._checkRequest(request); - if (errMessage) return callback({code: 4000, message: errMessage}); + if (errMessage) return callback({code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: errMessage}); switch (request.a) { case 'qf': @@ -354,14 +357,14 @@ Agent.prototype._handleMessage = function(request, callback) { case 'op': // Normalize the properties submitted var op = createClientOp(request, this.clientId); - if (!op) return callback({code: 4000, message: 'Invalid op message'}); + if (!op) return callback({code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: 'Invalid op message'}); return this._submit(request.c, request.d, op, callback); case 'nf': return this._fetchSnapshot(request.c, request.d, request.v, callback); case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: - callback({code: 4000, message: 'Invalid or unknown message'}); + callback({code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: 'Invalid or unknown message'}); } } catch (err) { callback(err); @@ -610,7 +613,7 @@ Agent.prototype._submit = function(collection, id, op, callback) { // Occassional 'Op already submitted' errors are expected to happen as // part of normal operation, since inflight ops need to be resent after // disconnect. In this case, ack the op so the client can proceed - if (err.code === 4001) return callback(null, ack); + if (err.code === ERROR_CODE.OP_ALREADY_SUBMITTED) return callback(null, ack); return callback(err); } diff --git a/lib/backend.js b/lib/backend.js index a8bcbd732..af9ee7b8d 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -8,6 +8,7 @@ var MemoryPubSub = require('./pubsub/memory'); var ot = require('./ot'); var projections = require('./projections'); var QueryEmitter = require('./query-emitter'); +var ShareDBError = require('./error'); var Snapshot = require('./snapshot'); var StreamSocket = require('./stream-socket'); var SubmitRequest = require('./submit-request'); @@ -22,6 +23,8 @@ var AFTER_SUBMIT_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "after submit" and "a 'Use "afterWrite" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to ' + 'disable the "after submit" and "afterSubmit" actions and this warning.'; +var ERROR_CODE = ShareDBError.code; + function Backend(options) { if (!(this instanceof Backend)) return new Backend(options); emitter.EventEmitter.call(this); @@ -482,7 +485,10 @@ Backend.prototype.subscribe = function(agent, index, id, version, options, callb // add the ability to SubmitRequest.commit to optionally pass the metadata to other clients on // PubSub. This behaviour is not needed right now, but we have added an options object to the // subscribe() signature so that it remains consistent with getOps() and fetch(). - return callback({code: 4025, message: 'Passing options to subscribe has not been implemented'}); + return callback({ + code: ERROR_CODE.NOT_IMPLEMENTED, + message: 'Passing options to subscribe has not been implemented' + }); } var start = Date.now(); var projection = this.projections[index]; @@ -604,7 +610,7 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba backend._triggerQuery(agent, index, query, options, function(err, request) { if (err) return callback(err); if (request.db.disableSubscribe) { - return callback({code: 4002, message: 'DB does not support subscribe'}); + return callback({code: ERROR_CODE.SUBSCRIBE_UNSUPPORTED, message: 'DB does not support subscribe'}); } backend.pubsub.subscribe(request.channel, function(err, stream) { if (err) return callback(err); @@ -650,7 +656,7 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac // Set the DB reference for the request after the middleware trigger so // that the db option can be changed in middleware request.db = (options.db) ? backend.extraDbs[options.db] : backend.db; - if (!request.db) return callback({code: 4003, message: 'DB not found'}); + if (!request.db) return callback({code: ERROR_CODE.DATABASE_ADAPTER_NOT_FOUND, message: 'DB not found'}); request.snapshotProjection = backend._getSnapshotProjection(request.db, projection); callback(null, request); }); @@ -731,7 +737,10 @@ Backend.prototype._fetchSnapshot = function(collection, id, version, callback) { if (error) return callback(error); if (version > snapshot.v) { - return callback({code: 4024, message: 'Requested version exceeds latest snapshot version'}); + return callback({ + code: ERROR_CODE.INVALID_VERSION, + message: 'Requested version exceeds latest snapshot version' + }); } callback(null, snapshot); diff --git a/lib/client/connection.js b/lib/client/connection.js index 521a8ceb6..22e188b64 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -8,6 +8,8 @@ var types = require('../types'); var util = require('../util'); var logger = require('../logger'); +var ERROR_CODE = ShareDBError.code; + function connectionState(socket) { if (socket.readyState === 0 || socket.readyState === 1) return 'connecting'; return 'disconnected'; @@ -186,15 +188,15 @@ Connection.prototype.handleMessage = function(message) { case 'init': // Client initialization packet if (message.protocol !== 1) { - err = new ShareDBError(4019, 'Invalid protocol version'); + err = new ShareDBError(ERROR_CODE.INVALID_PROTOCOL_VERSION, 'Invalid protocol version'); return this.emit('error', err); } if (types.map[message.type] !== types.defaultType) { - err = new ShareDBError(4020, 'Invalid default type'); + err = new ShareDBError(ERROR_CODE.INVALID_DEFAULT_TYPE, 'Invalid default type'); return this.emit('error', err); } if (typeof message.id !== 'string') { - err = new ShareDBError(4021, 'Invalid client id'); + err = new ShareDBError(ERROR_CODE.INVALID_CLIENT_ID, 'Invalid client id'); return this.emit('error', err); } this.id = message.id; @@ -303,7 +305,10 @@ Connection.prototype._setState = function(newState, reason) { this.state !== 'connecting' ) ) { - var err = new ShareDBError(5007, 'Cannot transition directly from ' + this.state + ' to ' + newState); + var err = new ShareDBError( + ERROR_CODE.INVALID_STATE_TRANSITION, + 'Cannot transition directly from ' + this.state + ' to ' + newState + ); return this.emit('error', err); } diff --git a/lib/client/doc.js b/lib/client/doc.js index d9c6f19fb..05374fc5e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -3,6 +3,8 @@ var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); +var ERROR_CODE = ShareDBError.code; + /** * A Doc is a client's view on a sharejs document. * @@ -141,7 +143,7 @@ Doc.prototype._setType = function(newType) { // If we removed the type from the object, also remove its data this.data = undefined; } else { - var err = new ShareDBError(4008, 'Missing type ' + newType); + var err = new ShareDBError(ERROR_CODE.UNKNOWN_TYPE, 'Missing type ' + newType); return this.emit('error', err); } }; @@ -158,7 +160,10 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (!snapshot) return callback && callback(); if (typeof snapshot.v !== 'number') { - var err = new ShareDBError(5008, 'Missing version in ingested snapshot. ' + this.collection + '.' + this.id); + var err = new ShareDBError( + ERROR_CODE.MISSING_SNAPSHOT_VERSION, + 'Missing version in ingested snapshot. ' + this.collection + '.' + this.id + ); if (callback) return callback(err); return this.emit('error', err); } @@ -179,7 +184,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { } // Otherwise, we've encounted an error state var err = new ShareDBError( - 5009, + ERROR_CODE.INVALID_VERSION, 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id ); if (callback) return callback(err); @@ -283,9 +288,9 @@ Doc.prototype._handleOp = function(err, message) { if (err) { if (this.inflightOp) { // The server has rejected submission of the current operation. If we get - // an error code 4002 "Op submit rejected", this was done intentionally + // an "Op submit rejected" error, this was done intentionally // and we should roll back but not return an error to the user. - if (err.code === 4002) err = null; + if (err.code === ERROR_CODE.OP_SUBMIT_REJECTED) err = null; return this._rollback(err); } return this.emit('error', err); @@ -471,10 +476,10 @@ function transformX(client, server) { if (client.del) return setNoOp(server); if (server.del) { - return new ShareDBError(4017, 'Document was deleted'); + return new ShareDBError(ERROR_CODE.DOCUMENT_WAS_DELETED, 'Document was deleted'); } if (server.create) { - return new ShareDBError(4018, 'Document alredy created'); + return new ShareDBError(ERROR_CODE.DOCUMENT_CREATED_REMOTELY, 'Document already created'); } // Ignore no-op coming from server @@ -482,7 +487,7 @@ function transformX(client, server) { // I believe that this should not occur, but check just in case if (client.create) { - return new ShareDBError(4018, 'Document already created'); + return new ShareDBError(ERROR_CODE.DOCUMENT_CREATED_REMOTELY, 'Document already created'); } // They both edited the document. This is the normal case for this function - @@ -520,7 +525,10 @@ Doc.prototype._otApply = function(op, source) { if (op.op) { if (!this.type) { // Throw here, because all usage of _otApply should be wrapped with a try/catch - throw new ShareDBError(4015, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id); + throw new ShareDBError( + ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, + 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id + ); } // Iteratively apply multi-component remote operations and rollback ops @@ -611,7 +619,7 @@ Doc.prototype._sendOp = function() { } var op = this.inflightOp; if (!op) { - var err = new ShareDBError(5010, 'No op to send on call to _sendOp'); + var err = new ShareDBError(ERROR_CODE.NO_OP_TO_SEND, 'No op to send on call to _sendOp'); return this.emit('error', err); } @@ -655,7 +663,7 @@ Doc.prototype._submit = function(op, source, callback) { if (op.op) { if (!this.type) { var err = new ShareDBError( - 4015, + ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id ); if (callback) return callback(err); @@ -793,7 +801,7 @@ Doc.prototype.create = function(data, type, options, callback) { type = types.defaultType.uri; } if (this.type) { - var err = new ShareDBError(4016, 'Document already exists'); + var err = new ShareDBError(ERROR_CODE.DOCUMENT_ALREADY_CREATED, 'Document already exists'); if (callback) return callback(err); return this.emit('error', err); } @@ -815,7 +823,7 @@ Doc.prototype.del = function(options, callback) { options = null; } if (!this.type) { - var err = new ShareDBError(4015, 'Document does not exist'); + var err = new ShareDBError(ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, 'Document does not exist'); if (callback) return callback(err); return this.emit('error', err); } diff --git a/lib/db/index.js b/lib/db/index.js index c5adf8123..de11eed0e 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -1,6 +1,8 @@ var async = require('async'); var ShareDBError = require('../error'); +var ERROR_CODE = ShareDBError.code; + function DB(options) { // pollDebounce is the minimum time in ms between query polls this.pollDebounce = options && options.pollDebounce; @@ -16,11 +18,11 @@ DB.prototype.close = function(callback) { }; DB.prototype.commit = function(collection, id, op, snapshot, options, callback) { - callback(new ShareDBError(5011, 'commit DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'commit DB method unimplemented')); }; DB.prototype.getSnapshot = function(collection, id, fields, options, callback) { - callback(new ShareDBError(5012, 'getSnapshot DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'getSnapshot DB method unimplemented')); }; DB.prototype.getSnapshotBulk = function(collection, ids, fields, options, callback) { @@ -39,7 +41,7 @@ DB.prototype.getSnapshotBulk = function(collection, ids, fields, options, callba }; DB.prototype.getOps = function(collection, id, from, to, options, callback) { - callback(new ShareDBError(5013, 'getOps DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'getOps DB method unimplemented')); }; DB.prototype.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) { @@ -77,7 +79,7 @@ DB.prototype.getCommittedOpVersion = function(collection, id, snapshot, op, opti }; DB.prototype.query = function(collection, query, fields, options, callback) { - callback(new ShareDBError(4022, 'query DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'query DB method unimplemented')); }; DB.prototype.queryPoll = function(collection, query, options, callback) { @@ -93,7 +95,7 @@ DB.prototype.queryPoll = function(collection, query, options, callback) { }; DB.prototype.queryPollDoc = function(collection, id, query, options, callback) { - callback(new ShareDBError(5014, 'queryPollDoc DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'queryPollDoc DB method unimplemented')); }; DB.prototype.canPollDoc = function() { diff --git a/lib/error.js b/lib/error.js index 5da9cb995..db2181775 100644 --- a/lib/error.js +++ b/lib/error.js @@ -2,9 +2,47 @@ var makeError = require('make-error'); function ShareDBError(code, message) { ShareDBError.super.call(this, message); - this.code = code; + this.code = code || ShareDBError.code.UNKNOWN_ERROR; } makeError(ShareDBError); +ShareDBError.code = { + BADLY_FORMED_MESSAGE: 'ERR_BADLY_FORMED_MESSAGE', + CANNOT_PROJECT_TYPE: 'ERR_CANNOT_PROJECT_TYPE', + CREATE_DATA_MUST_BE_AN_OBJECT: 'ERR_CREATE_DATA_MUST_BE_AN_OBJECT', + DATABASE_ADAPTER_NOT_FOUND: 'ERR_DATABASE_ADAPTER_NOT_FOUND', + DEL_MUST_BE_TRUE: 'ERR_DEL_MUST_BE_TRUE', + DOCUMENT_ALREADY_CREATED: 'ERR_DOCUMENT_ALREADY_CREATED', + DOCUMENT_CREATED_REMOTELY: 'ERR_DOCUMENT_CREATED_REMOTELY', + DOCUMENT_DOES_NOT_EXIST: 'ERR_DOCUMENT_DOES_NOT_EXIST', + DOCUMENT_WAS_DELETED: 'ERR_DOCUMENT_WAS_DELETED', + INVALID_CLIENT_ID: 'ERR_INVALID_CLIENT_ID', + INVALID_DEFAULT_TYPE: 'ERR_INVALID_DEFAULT_TYPE', + INVALID_METADATA: 'ERR_INVALID_METADATA', + INVALID_SRC: 'ERR_INVALID_SRC', + INVALID_SEQ: 'ERR_INVALID_SEQ', + INVALID_STATE_TRANSITION: 'ERR_INVALID_STATE_TRANSITION', + INVALID_PROTOCOL_VERSION: 'ERR_INVALID_PROTOCOL_VERSION', + INVALID_VERSION: 'ERR_INVALID_VERSION', + LISTENER_NOT_ASSIGNED: 'ERR_LISTENER_NOT_ASSIGNED', + MAX_RETRIES: 'ERR_MAX_RETRIES', + MISSING_CREATE_TYPE: 'ERR_MISSING_CREATE_TYPE', + MISSING_OP: 'ERR_MISSING_OP', + MISSING_TRANSFORM_OPS: 'ERR_MISSING_TRANSFORM_OPS', + MISSING_SNAPSHOT: 'ERR_MISSING_SNAPSHOT', + MISSING_SNAPSHOT_VERSION: 'ERR_MISSING_SNAPSHOT_VERSION', + NO_OP_TO_SEND: 'ERR_NO_OP_TO_SEND', + NOT_IMPLEMENTED: 'ERR_NOT_IMPLEMENTED', + OP_ALREADY_SUBMITTED: 'ERR_OP_ALREADY_SUBMITTED', + OP_INVALID_IN_PROJECTION: 'ERR_OP_INVALID_IN_PROJECTION', + OP_SUBMIT_REJECTED: 'ERR_OP_SUBMIT_REJECTED', + OP_VERSION_NEWER_THAN_SNAPSHOT: 'ERR_OP_VERSION_NEWER_THAN_SNAPSHOT', + SRC_AND_SEQ_MUST_BE_SET_TOGETHER: 'ERR_SRC_AND_SEQ_MUST_BE_SET_TOGETHER', + SUBSCRIBE_UNSUPPORTED: 'ERR_SUBSCRIBE_UNSUPPORTED', + UNKNOWN_ERROR: 'ERR_UNKNOWN_ERROR', + UNKNOWN_TYPE: 'ERR_UNKNOWN_TYPE', + VERSION_MISMATCH: 'ERR_VERSION_MISMATCH' +}; + module.exports = ShareDBError; diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index 3726b2ca8..c489c4720 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -2,6 +2,8 @@ var emitter = require('../emitter'); var ShareDBError = require('../error'); var util = require('../util'); +var ERROR_CODE = ShareDBError.code; + module.exports = MilestoneDB; function MilestoneDB(options) { emitter.EventEmitter.call(this); @@ -25,7 +27,7 @@ MilestoneDB.prototype.close = function(callback) { * the signature (error, snapshot) => void; */ MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { - var error = new ShareDBError(5019, 'getMilestoneSnapshot MilestoneDB method unimplemented'); + var error = new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'getMilestoneSnapshot MilestoneDB method unimplemented'); this._callBackOrEmitError(error, callback); }; @@ -36,17 +38,26 @@ MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, c * Should have the signature (error) => void; */ MilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) { - var error = new ShareDBError(5020, 'saveMilestoneSnapshot MilestoneDB method unimplemented'); + var error = new ShareDBError( + ERROR_CODE.NOT_IMPLEMENTED, + 'saveMilestoneSnapshot MilestoneDB method unimplemented' + ); this._callBackOrEmitError(error, callback); }; MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { - var error = new ShareDBError(5021, 'getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented'); + var error = new ShareDBError( + ERROR_CODE.NOT_IMPLEMENTED, + 'getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented' + ); this._callBackOrEmitError(error, callback); }; MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { - var error = new ShareDBError(5022, 'getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented'); + var error = new ShareDBError( + ERROR_CODE.NOT_IMPLEMENTED, + 'getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented' + ); this._callBackOrEmitError(error, callback); }; diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index 7b64dee36..2deadad37 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -1,6 +1,8 @@ var MilestoneDB = require('./index'); var ShareDBError = require('../error'); +var ERROR_CODE = ShareDBError.code; + /** * In-memory ShareDB milestone database * @@ -23,7 +25,9 @@ function MemoryMilestoneDB(options) { MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); MemoryMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { - if (!this._isValidVersion(version)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid version')); + if (!this._isValidVersion(version)) { + return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Invalid version')); + } var predicate = versionLessThanOrEqualTo(version); this._findMilestoneSnapshot(collection, id, predicate, callback); @@ -35,8 +39,8 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapsho this.emit('save', collection, snapshot); }.bind(this); - if (!collection) return callback(new ShareDBError(4001, 'Missing collection')); - if (!snapshot) return callback(new ShareDBError(4001, 'Missing snapshot')); + if (!collection) return callback(new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing collection')); + if (!snapshot) return callback(new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing snapshot')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); milestoneSnapshots.push(snapshot); @@ -49,7 +53,7 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapsho MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { if (!this._isValidTimestamp(timestamp)) { - return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Invalid timestamp')); } var filter = timestampLessThanOrEqualTo(timestamp); @@ -58,7 +62,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collec MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { if (!this._isValidTimestamp(timestamp)) { - return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Invalid timestamp')); } var filter = timestampGreaterThanOrEqualTo(timestamp); @@ -75,8 +79,8 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collect }; MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, breakCondition, callback) { - if (!collection) return process.nextTick(callback, new ShareDBError(4001, 'Missing collection')); - if (!id) return process.nextTick(callback, new ShareDBError(4001, 'Missing ID')); + if (!collection) return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing collection')); + if (!id) return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing ID')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); diff --git a/lib/ot.js b/lib/ot.js index f04da0145..99c1c547f 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -4,46 +4,49 @@ // delete operations. var types = require('./types').map; +var ShareDBError = require('./error'); + +var ERROR_CODE = ShareDBError.code; // Returns an error string on failure. Rockin' it C style. exports.checkOp = function(op) { if (op == null || typeof op !== 'object') { - return {code: 4004, message: 'Missing op'}; + return {code: ERROR_CODE.MISSING_OP, message: 'Missing op'}; } if (op.create != null) { if (typeof op.create !== 'object') { - return {code: 4006, message: 'create data must be an object'}; + return {code: ERROR_CODE.CREATE_DATA_MUST_BE_AN_OBJECT, message: 'create data must be an object'}; } var typeName = op.create.type; if (typeof typeName !== 'string') { - return {code: 4007, message: 'Missing create type'}; + return {code: ERROR_CODE.MISSING_CREATE_TYPE, message: 'Missing create type'}; } var type = types[typeName]; if (type == null || typeof type !== 'object') { - return {code: 4008, message: 'Unknown type'}; + return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; } } else if (op.del != null) { - if (op.del !== true) return {code: 4009, message: 'del value must be true'}; + if (op.del !== true) return {code: ERROR_CODE.DEL_MUST_BE_TRUE, message: 'del value must be true'}; } else if (op.op == null) { - return {code: 4010, message: 'Missing op, create, or del'}; + return {code: ERROR_CODE.MISSING_OP, message: 'Missing op, create, or del'}; } if (op.src != null && typeof op.src !== 'string') { - return {code: 4011, message: 'Invalid src'}; + return {code: ERROR_CODE.INVALID_SRC, message: 'Invalid src'}; } if (op.seq != null && typeof op.seq !== 'number') { - return {code: 4012, message: 'Invalid seq'}; + return {code: ERROR_CODE.INVALID_SEQ, message: 'Invalid seq'}; } if ( (op.src == null && op.seq != null) || (op.src != null && op.seq == null) ) { - return {code: 4013, message: 'Both src and seq must be set together'}; + return {code: ERROR_CODE.SRC_AND_SEQ_MUST_BE_SET_TOGETHER, message: 'Both src and seq must be set together'}; } if (op.m != null && typeof op.m !== 'object') { - return {code: 4014, message: 'op.m invalid'}; + return {code: ERROR_CODE.INVALID_METADATA, message: 'op.m invalid'}; } }; @@ -56,20 +59,20 @@ exports.normalizeType = function(typeName) { // type) and edits it in-place. Returns an error or null for success. exports.apply = function(snapshot, op) { if (typeof snapshot !== 'object') { - return {code: 5002, message: 'Missing snapshot'}; + return {code: ERROR_CODE.MISSING_SNAPSHOT, message: 'Missing snapshot'}; } if (snapshot.v != null && op.v != null && snapshot.v !== op.v) { - return {code: 5003, message: 'Version mismatch'}; + return {code: ERROR_CODE.VERSION_MISMATCH, message: 'Version mismatch'}; } // Create operation if (op.create) { - if (snapshot.type) return {code: 4016, message: 'Document already exists'}; + if (snapshot.type) return {code: ERROR_CODE.DOCUMENT_ALREADY_CREATED, message: 'Document already exists'}; // The document doesn't exist, although it might have once existed var create = op.create; var type = types[create.type]; - if (!type) return {code: 4008, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; try { snapshot.data = type.create(create.data); @@ -98,11 +101,11 @@ exports.apply = function(snapshot, op) { }; function applyOpEdit(snapshot, edit) { - if (!snapshot.type) return {code: 4015, message: 'Document does not exist'}; + if (!snapshot.type) return {code: ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, message: 'Document does not exist'}; - if (edit == null) return {code: 5004, message: 'Missing op'}; + if (edit == null) return {code: ERROR_CODE.MISSING_OP, message: 'Missing op'}; var type = types[snapshot.type]; - if (!type) return {code: 4008, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; try { snapshot.data = type.apply(snapshot.data, edit); @@ -115,12 +118,12 @@ exports.transform = function(type, op, appliedOp) { // There are 16 cases this function needs to deal with - which are all the // combinations of create/delete/op/noop from both op and appliedOp if (op.v != null && op.v !== appliedOp.v) { - return {code: 5006, message: 'Version mismatch'}; + return {code: ERROR_CODE.VERSION_MISMATCH, message: 'Version mismatch'}; } if (appliedOp.del) { if (op.create || op.op) { - return {code: 4017, message: 'Document was deleted'}; + return {code: ERROR_CODE.DOCUMENT_WAS_DELETED, message: 'Document was deleted'}; } } else if ( (appliedOp.create && (op.op || op.create || op.del)) || @@ -128,14 +131,14 @@ exports.transform = function(type, op, appliedOp) { ) { // If appliedOp.create is not true, appliedOp contains an op - which // also means the document exists remotely. - return {code: 4018, message: 'Document was created remotely'}; + return {code: ERROR_CODE.DOCUMENT_CREATED_REMOTELY, message: 'Document was created remotely'}; } else if (appliedOp.op && op.op) { // If we reach here, they both have a .op property. - if (!type) return {code: 5005, message: 'Document does not exist'}; + if (!type) return {code: ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, message: 'Document does not exist'}; if (typeof type === 'string') { type = types[type]; - if (!type) return {code: 4008, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; } try { @@ -160,7 +163,7 @@ exports.applyOps = function(snapshot, ops) { if (snapshot.type) { type = types[snapshot.type]; - if (!type) return {code: 4008, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; } for (var index = 0; index < ops.length; index++) { @@ -170,7 +173,7 @@ exports.applyOps = function(snapshot, ops) { if (op.create) { type = types[op.create.type]; - if (!type) return {code: 4008, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; snapshot.data = type.create(op.create.data); snapshot.type = type.uri; } else if (op.del) { diff --git a/lib/projections.js b/lib/projections.js index d9a48ea03..b6a3fd656 100644 --- a/lib/projections.js +++ b/lib/projections.js @@ -1,4 +1,7 @@ var json0 = require('ot-json0').type; +var ShareDBError = require('./error'); + +var ERROR_CODE = ShareDBError.code; exports.projectSnapshot = projectSnapshot; exports.projectSnapshots = projectSnapshots; @@ -11,7 +14,7 @@ exports.isOpAllowed = isOpAllowed; function projectSnapshot(fields, snapshot) { // Only json0 supported right now if (snapshot.type && snapshot.type !== json0.uri) { - throw new Error(4023, 'Cannot project snapshots of type ' + snapshot.type); + throw new Error(ERROR_CODE.CANNOT_PROJECT_TYPE, 'Cannot project snapshots of type ' + snapshot.type); } snapshot.data = projectData(fields, snapshot.data); } diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js index e454a694c..cd3e7e7d4 100644 --- a/lib/pubsub/index.js +++ b/lib/pubsub/index.js @@ -3,6 +3,8 @@ var OpStream = require('../op-stream'); var ShareDBError = require('../error'); var util = require('../util'); +var ERROR_CODE = ShareDBError.code; + function PubSub(options) { if (!(this instanceof PubSub)) return new PubSub(options); emitter.EventEmitter.call(this); @@ -38,19 +40,19 @@ PubSub.prototype.close = function(callback) { PubSub.prototype._subscribe = function(channel, callback) { process.nextTick(function() { - callback(new ShareDBError(5015, '_subscribe PubSub method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, '_subscribe PubSub method unimplemented')); }); }; PubSub.prototype._unsubscribe = function(channel, callback) { process.nextTick(function() { - callback(new ShareDBError(5016, '_unsubscribe PubSub method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, '_unsubscribe PubSub method unimplemented')); }); }; PubSub.prototype._publish = function(channels, data, callback) { process.nextTick(function() { - callback(new ShareDBError(5017, '_publish PubSub method unimplemented')); + callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, '_publish PubSub method unimplemented')); }); }; diff --git a/lib/query-emitter.js b/lib/query-emitter.js index cfa338845..f863b9e34 100644 --- a/lib/query-emitter.js +++ b/lib/query-emitter.js @@ -3,6 +3,8 @@ var deepEqual = require('fast-deep-equal'); var ShareDBError = require('./error'); var util = require('./util'); +var ERROR_CODE = ShareDBError.code; + function QueryEmitter(request, stream, ids, extra) { this.backend = request.backend; this.agent = request.agent; @@ -293,7 +295,7 @@ QueryEmitter.prototype.onError = QueryEmitter.prototype.onDiff = QueryEmitter.prototype.onExtra = QueryEmitter.prototype.onOp = function() { - throw new ShareDBError(5018, 'Required QueryEmitter listener not assigned'); + throw new ShareDBError(ERROR_CODE.LISTENER_NOT_ASSIGNED, 'Required QueryEmitter listener not assigned'); }; function getInserted(diff) { diff --git a/lib/submit-request.js b/lib/submit-request.js index 068be3123..b33f6ae94 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -1,5 +1,8 @@ var ot = require('./ot'); var projections = require('./projections'); +var ShareDBError = require('./error'); + +var ERROR_CODE = ShareDBError.code; function SubmitRequest(backend, agent, index, id, op, options) { this.backend = backend; @@ -240,34 +243,43 @@ SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) { // Non-fatal client errors: SubmitRequest.prototype.alreadySubmittedError = function() { - return {code: 4001, message: 'Op already submitted'}; + return {code: ERROR_CODE.OP_ALREADY_SUBMITTED, message: 'Op already submitted'}; }; SubmitRequest.prototype.rejectedError = function() { - return {code: 4002, message: 'Op submit rejected'}; + return {code: ERROR_CODE.OP_SUBMIT_REJECTED, message: 'Op submit rejected'}; }; // Fatal client errors: SubmitRequest.prototype.alreadyCreatedError = function() { - return {code: 4010, message: 'Invalid op submitted. Document already created'}; + return {code: ERROR_CODE.DOCUMENT_ALREADY_CREATED, message: 'Invalid op submitted. Document already created'}; }; SubmitRequest.prototype.newerVersionError = function() { - return {code: 4011, message: 'Invalid op submitted. Op version newer than current snapshot'}; + return { + code: ERROR_CODE.OP_VERSION_NEWER_THAN_SNAPSHOT, + message: 'Invalid op submitted. Op version newer than current snapshot' + }; }; SubmitRequest.prototype.projectionError = function() { - return {code: 4012, message: 'Invalid op submitted. Operation invalid in projected collection'}; + return { + code: ERROR_CODE.OP_INVALID_IN_PROJECTION, + message: 'Invalid op submitted. Operation invalid in projected collection' + }; }; // Fatal internal errors: SubmitRequest.prototype.missingOpsError = function() { return { - code: 5001, + code: ERROR_CODE.MISSING_TRANSFORM_OPS, message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version' }; }; SubmitRequest.prototype.versionDuringTransformError = function() { - return {code: 5002, message: 'Op submit failed. Versions mismatched during op transform'}; + return {code: ERROR_CODE.VERSION_MISMATCH, message: 'Op submit failed. Versions mismatched during op transform'}; }; SubmitRequest.prototype.versionAfterTransformError = function() { - return {code: 5003, message: 'Op submit failed. Op version mismatches snapshot after op transform'}; + return { + code: ERROR_CODE.VERSION_MISMATCH, + message: 'Op submit failed. Op version mismatches snapshot after op transform' + }; }; SubmitRequest.prototype.maxRetriesError = function() { - return {code: 5004, message: 'Op submit failed. Maximum submit retries exceeded'}; + return {code: ERROR_CODE.MAX_RETRIES, message: 'Op submit failed. Maximum submit retries exceeded'}; }; diff --git a/test/backend.js b/test/backend.js index c6177d05f..e6a87f806 100644 --- a/test/backend.js +++ b/test/backend.js @@ -95,7 +95,7 @@ describe('Backend', function() { opsOptions: {metadata: true} }; backend.subscribe(null, 'books', '1984', null, options, function(error) { - expect(error.code).to.equal(4025); + expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); }); diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index e2155d45b..4ce6ed890 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -142,7 +142,7 @@ describe('SnapshotVersionRequest', function() { it('errors if asking for a version that does not exist', function(done) { backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) { - expect(error.code).to.equal(4024); + expect(error.code).to.equal('ERR_INVALID_VERSION'); expect(snapshot).to.equal(undefined); done(); }); diff --git a/test/client/submit.js b/test/client/submit.js index 4f43cbe4b..5d622e5af 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -637,7 +637,7 @@ module.exports = function() { if (err) return done(err); doc.pause(); doc.submitOp({p: ['age'], na: 1}, function(err) { - expect(err.code).to.equal(4017); + expect(err.code).to.equal('ERR_DOCUMENT_WAS_DELETED'); expect(doc.version).equal(2); expect(doc.data).eql(undefined); done(); @@ -661,7 +661,7 @@ module.exports = function() { if (err) return done(err); doc.pause(); doc.create({age: 9}, function(err) { - expect(err.code).to.equal(4018); + expect(err.code).to.equal('ERR_DOCUMENT_CREATED_REMOTELY'); expect(doc.version).equal(3); expect(doc.data).eql({age: 5}); done(); diff --git a/test/milestone-db.js b/test/milestone-db.js index b75216360..c79c9ce53 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -14,14 +14,14 @@ describe('Base class', function() { it('calls back with an error when trying to get a snapshot', function(done) { db.getMilestoneSnapshot('books', '123', 1, function(error) { - expect(error.code).to.equal(5019); + expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); }); it('emits an error when trying to get a snapshot', function(done) { db.on('error', function(error) { - expect(error.code).to.equal(5019); + expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); @@ -30,14 +30,14 @@ describe('Base class', function() { it('calls back with an error when trying to save a snapshot', function(done) { db.saveMilestoneSnapshot('books', {}, function(error) { - expect(error.code).to.equal(5020); + expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); }); it('emits an error when trying to save a snapshot', function(done) { db.on('error', function(error) { - expect(error.code).to.equal(5020); + expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); @@ -46,14 +46,14 @@ describe('Base class', function() { it('calls back with an error when trying to get a snapshot before a time', function(done) { db.getMilestoneSnapshotAtOrBeforeTime('books', '123', 1000, function(error) { - expect(error.code).to.equal(5021); + expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); }); it('calls back with an error when trying to get a snapshot after a time', function(done) { db.getMilestoneSnapshotAtOrAfterTime('books', '123', 1000, function(error) { - expect(error.code).to.equal(5022); + expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); }); diff --git a/test/pubsub-memory.js b/test/pubsub-memory.js index 371d982e6..a6ac9e7f4 100644 --- a/test/pubsub-memory.js +++ b/test/pubsub-memory.js @@ -14,7 +14,7 @@ describe('PubSub base class', function() { var pubsub = new PubSub(); pubsub.subscribe('x', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal(5015); + expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); }); @@ -23,7 +23,7 @@ describe('PubSub base class', function() { var pubsub = new PubSub(); pubsub.on('error', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal(5015); + expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); pubsub.subscribe('x'); @@ -38,7 +38,7 @@ describe('PubSub base class', function() { if (err) return done(err); pubsub.on('error', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal(5016); + expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); stream.destroy(); @@ -50,7 +50,7 @@ describe('PubSub base class', function() { pubsub.on('error', done); pubsub.publish(['x', 'y'], {test: true}, function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal(5017); + expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); }); @@ -59,7 +59,7 @@ describe('PubSub base class', function() { var pubsub = new PubSub(); pubsub.on('error', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal(5017); + expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); done(); }); pubsub.publish(['x', 'y'], {test: true}); From 3039a6bacbf73117f4999511c173eea1babc883f Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 16 Oct 2019 15:44:53 +0100 Subject: [PATCH 2/6] Reword error codes This change aims to reword our new error codes according to review comments. The main changes are: - Prefix all code keys with `ERR_` - Rename to follow the _ format - Tweak some error code specificity - Provide a bit more context with some error messages --- lib/agent.js | 16 +++--- lib/backend.js | 15 +++--- lib/client/connection.js | 16 ++++-- lib/client/doc.js | 26 +++++----- lib/db/index.js | 12 ++--- lib/error.js | 69 ++++++++++++------------- lib/milestone-db/index.js | 13 +++-- lib/milestone-db/memory.js | 20 ++++--- lib/ot.js | 50 +++++++++--------- lib/projections.js | 4 +- lib/pubsub/index.js | 16 ++++-- lib/query-emitter.js | 11 ++-- lib/submit-request.js | 26 ++++++---- test/backend.js | 2 +- test/client/snapshot-version-request.js | 2 +- test/client/submit.js | 4 +- test/milestone-db.js | 12 ++--- test/pubsub-memory.js | 10 ++-- 18 files changed, 175 insertions(+), 149 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 3f6ad74e4..87570ab10 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -4,7 +4,7 @@ var util = require('./util'); var logger = require('./logger'); var ShareDBError = require('./error'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; /** * Agent deserializes the wire protocol messages received from the stream and @@ -235,7 +235,7 @@ Agent.prototype._sendOpsBulk = function(collection, opsMap) { function getReplyErrorObject(err) { if (typeof err === 'string') { return { - code: ERROR_CODE.UNKNOWN_ERROR, + code: ERROR_CODE.ERR_UNKNOWN_ERROR, message: err }; } else { @@ -290,7 +290,7 @@ Agent.prototype._open = function() { if (agent.closed) return; if (typeof chunk !== 'object') { - var err = {code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: 'Received non-object message'}; + var err = {code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: 'Received non-object message'}; return agent.close(err); } @@ -333,7 +333,7 @@ Agent.prototype._checkRequest = function(request) { Agent.prototype._handleMessage = function(request, callback) { try { var errMessage = this._checkRequest(request); - if (errMessage) return callback({code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: errMessage}); + if (errMessage) return callback({code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: errMessage}); switch (request.a) { case 'qf': @@ -357,14 +357,14 @@ Agent.prototype._handleMessage = function(request, callback) { case 'op': // Normalize the properties submitted var op = createClientOp(request, this.clientId); - if (!op) return callback({code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: 'Invalid op message'}); + if (!op) return callback({code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: 'Invalid op message'}); return this._submit(request.c, request.d, op, callback); case 'nf': return this._fetchSnapshot(request.c, request.d, request.v, callback); case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: - callback({code: ERROR_CODE.BADLY_FORMED_MESSAGE, message: 'Invalid or unknown message'}); + callback({code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: 'Invalid or unknown message'}); } } catch (err) { callback(err); @@ -610,10 +610,10 @@ Agent.prototype._submit = function(collection, id, op, callback) { // Message to acknowledge the op was successfully submitted var ack = {src: op.src, seq: op.seq, v: op.v}; if (err) { - // Occassional 'Op already submitted' errors are expected to happen as + // Occasional 'Op already submitted' errors are expected to happen as // part of normal operation, since inflight ops need to be resent after // disconnect. In this case, ack the op so the client can proceed - if (err.code === ERROR_CODE.OP_ALREADY_SUBMITTED) return callback(null, ack); + if (err.code === ERROR_CODE.ERR_OP_ALREADY_SUBMITTED) return callback(null, ack); return callback(err); } diff --git a/lib/backend.js b/lib/backend.js index af9ee7b8d..ee1a710a6 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -23,7 +23,7 @@ var AFTER_SUBMIT_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "after submit" and "a 'Use "afterWrite" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to ' + 'disable the "after submit" and "afterSubmit" actions and this warning.'; -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; function Backend(options) { if (!(this instanceof Backend)) return new Backend(options); @@ -486,7 +486,7 @@ Backend.prototype.subscribe = function(agent, index, id, version, options, callb // PubSub. This behaviour is not needed right now, but we have added an options object to the // subscribe() signature so that it remains consistent with getOps() and fetch(). return callback({ - code: ERROR_CODE.NOT_IMPLEMENTED, + code: ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, message: 'Passing options to subscribe has not been implemented' }); } @@ -599,7 +599,7 @@ Backend.prototype.queryFetch = function(agent, index, query, options, callback) // Options can contain: // db: The name of the DB (if the DB is specified in the otherDbs when the backend instance is created) // skipPoll: function(collection, id, op, query) {return true or false; } -// this is a syncronous function which can be used as an early filter for +// this is a synchronous function which can be used as an early filter for // operations going through the system to reduce the load on the DB. // pollDebounce: Minimum delay between subsequent database polls. This is // used to batch updates to reduce load on the database at the expense of @@ -610,7 +610,10 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba backend._triggerQuery(agent, index, query, options, function(err, request) { if (err) return callback(err); if (request.db.disableSubscribe) { - return callback({code: ERROR_CODE.SUBSCRIBE_UNSUPPORTED, message: 'DB does not support subscribe'}); + return callback({ + code: ERROR_CODE.ERR_DATABASE_DOES_NOT_SUPPORT_SUBSCRIBE, + message: 'DB does not support subscribe' + }); } backend.pubsub.subscribe(request.channel, function(err, stream) { if (err) return callback(err); @@ -656,7 +659,7 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac // Set the DB reference for the request after the middleware trigger so // that the db option can be changed in middleware request.db = (options.db) ? backend.extraDbs[options.db] : backend.db; - if (!request.db) return callback({code: ERROR_CODE.DATABASE_ADAPTER_NOT_FOUND, message: 'DB not found'}); + if (!request.db) return callback({code: ERROR_CODE.ERR_DATABASE_ADAPTER_NOT_FOUND, message: 'DB not found'}); request.snapshotProjection = backend._getSnapshotProjection(request.db, projection); callback(null, request); }); @@ -738,7 +741,7 @@ Backend.prototype._fetchSnapshot = function(collection, id, version, callback) { if (version > snapshot.v) { return callback({ - code: ERROR_CODE.INVALID_VERSION, + code: ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT, message: 'Requested version exceeds latest snapshot version' }); } diff --git a/lib/client/connection.js b/lib/client/connection.js index 22e188b64..0cd4d79a7 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -8,7 +8,7 @@ var types = require('../types'); var util = require('../util'); var logger = require('../logger'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; function connectionState(socket) { if (socket.readyState === 0 || socket.readyState === 1) return 'connecting'; @@ -188,15 +188,21 @@ Connection.prototype.handleMessage = function(message) { case 'init': // Client initialization packet if (message.protocol !== 1) { - err = new ShareDBError(ERROR_CODE.INVALID_PROTOCOL_VERSION, 'Invalid protocol version'); + err = new ShareDBError( + ERROR_CODE.ERR_PROTOCOL_VERSION_NOT_SUPPORTED, + 'Unsupported protocol version: ' + message.protocol + ); return this.emit('error', err); } if (types.map[message.type] !== types.defaultType) { - err = new ShareDBError(ERROR_CODE.INVALID_DEFAULT_TYPE, 'Invalid default type'); + err = new ShareDBError( + ERROR_CODE.ERR_DEFAULT_TYPE_MISMATCH, + message.type + ' does not match the server default type' + ); return this.emit('error', err); } if (typeof message.id !== 'string') { - err = new ShareDBError(ERROR_CODE.INVALID_CLIENT_ID, 'Invalid client id'); + err = new ShareDBError(ERROR_CODE.ERR_CLIENT_ID_BADLY_FORMED, 'Client id must be a string'); return this.emit('error', err); } this.id = message.id; @@ -306,7 +312,7 @@ Connection.prototype._setState = function(newState, reason) { ) ) { var err = new ShareDBError( - ERROR_CODE.INVALID_STATE_TRANSITION, + ERROR_CODE.ERR_CONNECTION_STATE_TRANSITION_INVALID, 'Cannot transition directly from ' + this.state + ' to ' + newState ); return this.emit('error', err); diff --git a/lib/client/doc.js b/lib/client/doc.js index 05374fc5e..cd3ddaf91 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -3,7 +3,7 @@ var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; /** * A Doc is a client's view on a sharejs document. @@ -143,7 +143,7 @@ Doc.prototype._setType = function(newType) { // If we removed the type from the object, also remove its data this.data = undefined; } else { - var err = new ShareDBError(ERROR_CODE.UNKNOWN_TYPE, 'Missing type ' + newType); + var err = new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, 'Missing type ' + newType); return this.emit('error', err); } }; @@ -161,7 +161,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (typeof snapshot.v !== 'number') { var err = new ShareDBError( - ERROR_CODE.MISSING_SNAPSHOT_VERSION, + ERROR_CODE.ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION, 'Missing version in ingested snapshot. ' + this.collection + '.' + this.id ); if (callback) return callback(err); @@ -184,7 +184,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { } // Otherwise, we've encounted an error state var err = new ShareDBError( - ERROR_CODE.INVALID_VERSION, + ERROR_CODE.ERR_DOC_MISSING_VERSION, 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id ); if (callback) return callback(err); @@ -290,7 +290,7 @@ Doc.prototype._handleOp = function(err, message) { // The server has rejected submission of the current operation. If we get // an "Op submit rejected" error, this was done intentionally // and we should roll back but not return an error to the user. - if (err.code === ERROR_CODE.OP_SUBMIT_REJECTED) err = null; + if (err.code === ERROR_CODE.ERR_OP_SUBMIT_REJECTED) err = null; return this._rollback(err); } return this.emit('error', err); @@ -476,10 +476,10 @@ function transformX(client, server) { if (client.del) return setNoOp(server); if (server.del) { - return new ShareDBError(ERROR_CODE.DOCUMENT_WAS_DELETED, 'Document was deleted'); + return new ShareDBError(ERROR_CODE.ERR_DOC_WAS_DELETED, 'Document was deleted'); } if (server.create) { - return new ShareDBError(ERROR_CODE.DOCUMENT_CREATED_REMOTELY, 'Document already created'); + return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document already created'); } // Ignore no-op coming from server @@ -487,7 +487,7 @@ function transformX(client, server) { // I believe that this should not occur, but check just in case if (client.create) { - return new ShareDBError(ERROR_CODE.DOCUMENT_CREATED_REMOTELY, 'Document already created'); + return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document already created'); } // They both edited the document. This is the normal case for this function - @@ -526,7 +526,7 @@ Doc.prototype._otApply = function(op, source) { if (!this.type) { // Throw here, because all usage of _otApply should be wrapped with a try/catch throw new ShareDBError( - ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, + ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id ); } @@ -619,7 +619,7 @@ Doc.prototype._sendOp = function() { } var op = this.inflightOp; if (!op) { - var err = new ShareDBError(ERROR_CODE.NO_OP_TO_SEND, 'No op to send on call to _sendOp'); + var err = new ShareDBError(ERROR_CODE.ERR_INFLIGHT_OP_MISSING, 'No op to send on call to _sendOp'); return this.emit('error', err); } @@ -663,7 +663,7 @@ Doc.prototype._submit = function(op, source, callback) { if (op.op) { if (!this.type) { var err = new ShareDBError( - ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, + ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id ); if (callback) return callback(err); @@ -801,7 +801,7 @@ Doc.prototype.create = function(data, type, options, callback) { type = types.defaultType.uri; } if (this.type) { - var err = new ShareDBError(ERROR_CODE.DOCUMENT_ALREADY_CREATED, 'Document already exists'); + var err = new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document already exists'); if (callback) return callback(err); return this.emit('error', err); } @@ -823,7 +823,7 @@ Doc.prototype.del = function(options, callback) { options = null; } if (!this.type) { - var err = new ShareDBError(ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, 'Document does not exist'); + var err = new ShareDBError(ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Document does not exist'); if (callback) return callback(err); return this.emit('error', err); } diff --git a/lib/db/index.js b/lib/db/index.js index de11eed0e..d7d47ce7d 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -1,7 +1,7 @@ var async = require('async'); var ShareDBError = require('../error'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; function DB(options) { // pollDebounce is the minimum time in ms between query polls @@ -18,11 +18,11 @@ DB.prototype.close = function(callback) { }; DB.prototype.commit = function(collection, id, op, snapshot, options, callback) { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'commit DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'commit DB method unimplemented')); }; DB.prototype.getSnapshot = function(collection, id, fields, options, callback) { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'getSnapshot DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'getSnapshot DB method unimplemented')); }; DB.prototype.getSnapshotBulk = function(collection, ids, fields, options, callback) { @@ -41,7 +41,7 @@ DB.prototype.getSnapshotBulk = function(collection, ids, fields, options, callba }; DB.prototype.getOps = function(collection, id, from, to, options, callback) { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'getOps DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'getOps DB method unimplemented')); }; DB.prototype.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) { @@ -79,7 +79,7 @@ DB.prototype.getCommittedOpVersion = function(collection, id, snapshot, op, opti }; DB.prototype.query = function(collection, query, fields, options, callback) { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'query DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'query DB method unimplemented')); }; DB.prototype.queryPoll = function(collection, query, options, callback) { @@ -95,7 +95,7 @@ DB.prototype.queryPoll = function(collection, query, options, callback) { }; DB.prototype.queryPollDoc = function(collection, id, query, options, callback) { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'queryPollDoc DB method unimplemented')); + callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'queryPollDoc DB method unimplemented')); }; DB.prototype.canPollDoc = function() { diff --git a/lib/error.js b/lib/error.js index db2181775..158ce4e06 100644 --- a/lib/error.js +++ b/lib/error.js @@ -2,47 +2,42 @@ var makeError = require('make-error'); function ShareDBError(code, message) { ShareDBError.super.call(this, message); - this.code = code || ShareDBError.code.UNKNOWN_ERROR; + this.code = code || ShareDBError.CODES.ERR_UNKNOWN_ERROR; } makeError(ShareDBError); -ShareDBError.code = { - BADLY_FORMED_MESSAGE: 'ERR_BADLY_FORMED_MESSAGE', - CANNOT_PROJECT_TYPE: 'ERR_CANNOT_PROJECT_TYPE', - CREATE_DATA_MUST_BE_AN_OBJECT: 'ERR_CREATE_DATA_MUST_BE_AN_OBJECT', - DATABASE_ADAPTER_NOT_FOUND: 'ERR_DATABASE_ADAPTER_NOT_FOUND', - DEL_MUST_BE_TRUE: 'ERR_DEL_MUST_BE_TRUE', - DOCUMENT_ALREADY_CREATED: 'ERR_DOCUMENT_ALREADY_CREATED', - DOCUMENT_CREATED_REMOTELY: 'ERR_DOCUMENT_CREATED_REMOTELY', - DOCUMENT_DOES_NOT_EXIST: 'ERR_DOCUMENT_DOES_NOT_EXIST', - DOCUMENT_WAS_DELETED: 'ERR_DOCUMENT_WAS_DELETED', - INVALID_CLIENT_ID: 'ERR_INVALID_CLIENT_ID', - INVALID_DEFAULT_TYPE: 'ERR_INVALID_DEFAULT_TYPE', - INVALID_METADATA: 'ERR_INVALID_METADATA', - INVALID_SRC: 'ERR_INVALID_SRC', - INVALID_SEQ: 'ERR_INVALID_SEQ', - INVALID_STATE_TRANSITION: 'ERR_INVALID_STATE_TRANSITION', - INVALID_PROTOCOL_VERSION: 'ERR_INVALID_PROTOCOL_VERSION', - INVALID_VERSION: 'ERR_INVALID_VERSION', - LISTENER_NOT_ASSIGNED: 'ERR_LISTENER_NOT_ASSIGNED', - MAX_RETRIES: 'ERR_MAX_RETRIES', - MISSING_CREATE_TYPE: 'ERR_MISSING_CREATE_TYPE', - MISSING_OP: 'ERR_MISSING_OP', - MISSING_TRANSFORM_OPS: 'ERR_MISSING_TRANSFORM_OPS', - MISSING_SNAPSHOT: 'ERR_MISSING_SNAPSHOT', - MISSING_SNAPSHOT_VERSION: 'ERR_MISSING_SNAPSHOT_VERSION', - NO_OP_TO_SEND: 'ERR_NO_OP_TO_SEND', - NOT_IMPLEMENTED: 'ERR_NOT_IMPLEMENTED', - OP_ALREADY_SUBMITTED: 'ERR_OP_ALREADY_SUBMITTED', - OP_INVALID_IN_PROJECTION: 'ERR_OP_INVALID_IN_PROJECTION', - OP_SUBMIT_REJECTED: 'ERR_OP_SUBMIT_REJECTED', - OP_VERSION_NEWER_THAN_SNAPSHOT: 'ERR_OP_VERSION_NEWER_THAN_SNAPSHOT', - SRC_AND_SEQ_MUST_BE_SET_TOGETHER: 'ERR_SRC_AND_SEQ_MUST_BE_SET_TOGETHER', - SUBSCRIBE_UNSUPPORTED: 'ERR_SUBSCRIBE_UNSUPPORTED', - UNKNOWN_ERROR: 'ERR_UNKNOWN_ERROR', - UNKNOWN_TYPE: 'ERR_UNKNOWN_TYPE', - VERSION_MISMATCH: 'ERR_VERSION_MISMATCH' +ShareDBError.CODES = { + ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT: 'ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT', + ERR_APPLY_SNAPSHOT_NOT_PROVIDED: 'ERR_APPLY_SNAPSHOT_NOT_PROVIDED', + ERR_CLIENT_ID_BADLY_FORMED: 'ERR_CLIENT_ID_BADLY_FORMED', + ERR_CONNECTION_STATE_TRANSITION_INVALID: 'ERR_CONNECTION_STATE_TRANSITION_INVALID', + ERR_DATABASE_ADAPTER_NOT_FOUND: 'ERR_DATABASE_ADAPTER_NOT_FOUND', + ERR_DATABASE_DOES_NOT_SUPPORT_SUBSCRIBE: 'ERR_DATABASE_DOES_NOT_SUPPORT_SUBSCRIBE', + ERR_DATABASE_METHOD_NOT_IMPLEMENTED: 'ERR_DATABASE_METHOD_NOT_IMPLEMENTED', + ERR_DEFAULT_TYPE_MISMATCH: 'ERR_DEFAULT_TYPE_MISMATCH', + ERR_DOC_MISSING_VERSION: 'ERR_DOC_MISSING_VERSION', + ERR_DOC_ALREADY_CREATED: 'ERR_DOC_ALREADY_CREATED', + ERR_DOC_DOES_NOT_EXIST: 'ERR_DOC_DOES_NOT_EXIST', + ERR_DOC_TYPE_NOT_RECOGNISED: 'ERR_DOC_TYPE_NOT_RECOGNISED', + ERR_DOC_WAS_DELETED: 'ERR_DOC_WAS_DELETED', + ERR_INFLIGHT_OP_MISSING: 'ERR_INFLIGHT_OP_MISSING', + ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION: 'ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION', + ERR_MAX_SUBMIT_RETRIES_EXCEEDED: 'ERR_MAX_SUBMIT_RETRIES_EXCEEDED', + ERR_MESSAGE_BADLY_FORMED: 'ERR_MESSAGE_BADLY_FORMED', + ERR_OP_ALREADY_SUBMITTED: 'ERR_OP_ALREADY_SUBMITTED', + ERR_OP_NOT_ALLOWED_IN_PROJECTION: 'ERR_OP_NOT_ALLOWED_IN_PROJECTION', + ERR_OP_SUBMIT_REJECTED: 'ERR_OP_SUBMIT_REJECTED', + ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM: 'ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM', + ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM: 'ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM', + ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT: 'ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT', + ERR_OT_OP_BADLY_FORMED: 'ERR_OT_OP_BADLY_FORMED', + ERR_OT_OP_NOT_PROVIDED: 'ERR_OT_OP_NOT_PROVIDED', + ERR_PROTOCOL_VERSION_NOT_SUPPORTED: 'ERR_PROTOCOL_VERSION_NOT_SUPPORTED', + ERR_QUERY_EMITTER_LISTENER_NOT_ASSIGNED: 'ERR_QUERY_EMITTER_LISTENER_NOT_ASSIGNED', + ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND: 'ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND', + ERR_TYPE_CANNOT_BE_PROJECTED: 'ERR_TYPE_CANNOT_BE_PROJECTED', + ERR_UNKNOWN_ERROR: 'ERR_UNKNOWN_ERROR' }; module.exports = ShareDBError; diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index c489c4720..a0f46ccc3 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -2,7 +2,7 @@ var emitter = require('../emitter'); var ShareDBError = require('../error'); var util = require('../util'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; module.exports = MilestoneDB; function MilestoneDB(options) { @@ -27,7 +27,10 @@ MilestoneDB.prototype.close = function(callback) { * the signature (error, snapshot) => void; */ MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { - var error = new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, 'getMilestoneSnapshot MilestoneDB method unimplemented'); + var error = new ShareDBError( + ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, + 'getMilestoneSnapshot MilestoneDB method unimplemented' + ); this._callBackOrEmitError(error, callback); }; @@ -39,7 +42,7 @@ MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, c */ MilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) { var error = new ShareDBError( - ERROR_CODE.NOT_IMPLEMENTED, + ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'saveMilestoneSnapshot MilestoneDB method unimplemented' ); this._callBackOrEmitError(error, callback); @@ -47,7 +50,7 @@ MilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, cal MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { var error = new ShareDBError( - ERROR_CODE.NOT_IMPLEMENTED, + ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented' ); this._callBackOrEmitError(error, callback); @@ -55,7 +58,7 @@ MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { var error = new ShareDBError( - ERROR_CODE.NOT_IMPLEMENTED, + ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, 'getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented' ); this._callBackOrEmitError(error, callback); diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index 2deadad37..278a1788d 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -1,7 +1,7 @@ var MilestoneDB = require('./index'); var ShareDBError = require('../error'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; /** * In-memory ShareDB milestone database @@ -26,7 +26,7 @@ MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); MemoryMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { if (!this._isValidVersion(version)) { - return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Invalid version')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Invalid version')); } var predicate = versionLessThanOrEqualTo(version); @@ -39,8 +39,8 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapsho this.emit('save', collection, snapshot); }.bind(this); - if (!collection) return callback(new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing collection')); - if (!snapshot) return callback(new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing snapshot')); + if (!collection) return callback(new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing collection')); + if (!snapshot) return callback(new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing snapshot')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); milestoneSnapshots.push(snapshot); @@ -53,7 +53,7 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapsho MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { if (!this._isValidTimestamp(timestamp)) { - return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Invalid timestamp')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Invalid timestamp')); } var filter = timestampLessThanOrEqualTo(timestamp); @@ -62,7 +62,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collec MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { if (!this._isValidTimestamp(timestamp)) { - return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Invalid timestamp')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Invalid timestamp')); } var filter = timestampGreaterThanOrEqualTo(timestamp); @@ -79,8 +79,12 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collect }; MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, breakCondition, callback) { - if (!collection) return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing collection')); - if (!id) return process.nextTick(callback, new ShareDBError(ERROR_CODE.UNKNOWN_ERROR, 'Missing ID')); + if (!collection) { + return process.nextTick( + callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing collection') + ); + } + if (!id) return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing ID')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); diff --git a/lib/ot.js b/lib/ot.js index 99c1c547f..494b2ef29 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -6,47 +6,47 @@ var types = require('./types').map; var ShareDBError = require('./error'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; // Returns an error string on failure. Rockin' it C style. exports.checkOp = function(op) { if (op == null || typeof op !== 'object') { - return {code: ERROR_CODE.MISSING_OP, message: 'Missing op'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Op must be an object'}; } if (op.create != null) { if (typeof op.create !== 'object') { - return {code: ERROR_CODE.CREATE_DATA_MUST_BE_AN_OBJECT, message: 'create data must be an object'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Create data must be an object'}; } var typeName = op.create.type; if (typeof typeName !== 'string') { - return {code: ERROR_CODE.MISSING_CREATE_TYPE, message: 'Missing create type'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Missing create type'}; } var type = types[typeName]; if (type == null || typeof type !== 'object') { - return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; + return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; } } else if (op.del != null) { - if (op.del !== true) return {code: ERROR_CODE.DEL_MUST_BE_TRUE, message: 'del value must be true'}; + if (op.del !== true) return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'del value must be true'}; } else if (op.op == null) { - return {code: ERROR_CODE.MISSING_OP, message: 'Missing op, create, or del'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Missing op, create, or del'}; } if (op.src != null && typeof op.src !== 'string') { - return {code: ERROR_CODE.INVALID_SRC, message: 'Invalid src'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'src must be a string'}; } if (op.seq != null && typeof op.seq !== 'number') { - return {code: ERROR_CODE.INVALID_SEQ, message: 'Invalid seq'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'seq must be a string'}; } if ( (op.src == null && op.seq != null) || (op.src != null && op.seq == null) ) { - return {code: ERROR_CODE.SRC_AND_SEQ_MUST_BE_SET_TOGETHER, message: 'Both src and seq must be set together'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Both src and seq must be set together'}; } if (op.m != null && typeof op.m !== 'object') { - return {code: ERROR_CODE.INVALID_METADATA, message: 'op.m invalid'}; + return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'op.m must be an object or null'}; } }; @@ -59,20 +59,20 @@ exports.normalizeType = function(typeName) { // type) and edits it in-place. Returns an error or null for success. exports.apply = function(snapshot, op) { if (typeof snapshot !== 'object') { - return {code: ERROR_CODE.MISSING_SNAPSHOT, message: 'Missing snapshot'}; + return {code: ERROR_CODE.ERR_APPLY_SNAPSHOT_NOT_PROVIDED, message: 'Missing snapshot'}; } if (snapshot.v != null && op.v != null && snapshot.v !== op.v) { - return {code: ERROR_CODE.VERSION_MISMATCH, message: 'Version mismatch'}; + return {code: ERROR_CODE.ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT, message: 'Version mismatch'}; } // Create operation if (op.create) { - if (snapshot.type) return {code: ERROR_CODE.DOCUMENT_ALREADY_CREATED, message: 'Document already exists'}; + if (snapshot.type) return {code: ERROR_CODE.ERR_DOC_ALREADY_CREATED, message: 'Document already exists'}; // The document doesn't exist, although it might have once existed var create = op.create; var type = types[create.type]; - if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; try { snapshot.data = type.create(create.data); @@ -101,11 +101,11 @@ exports.apply = function(snapshot, op) { }; function applyOpEdit(snapshot, edit) { - if (!snapshot.type) return {code: ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, message: 'Document does not exist'}; + if (!snapshot.type) return {code: ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, message: 'Document does not exist'}; - if (edit == null) return {code: ERROR_CODE.MISSING_OP, message: 'Missing op'}; + if (edit == null) return {code: ERROR_CODE.ERR_OT_OP_NOT_PROVIDED, message: 'Missing op'}; var type = types[snapshot.type]; - if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; try { snapshot.data = type.apply(snapshot.data, edit); @@ -118,12 +118,12 @@ exports.transform = function(type, op, appliedOp) { // There are 16 cases this function needs to deal with - which are all the // combinations of create/delete/op/noop from both op and appliedOp if (op.v != null && op.v !== appliedOp.v) { - return {code: ERROR_CODE.VERSION_MISMATCH, message: 'Version mismatch'}; + return {code: ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, message: 'Version mismatch'}; } if (appliedOp.del) { if (op.create || op.op) { - return {code: ERROR_CODE.DOCUMENT_WAS_DELETED, message: 'Document was deleted'}; + return {code: ERROR_CODE.ERR_DOC_WAS_DELETED, message: 'Document was deleted'}; } } else if ( (appliedOp.create && (op.op || op.create || op.del)) || @@ -131,14 +131,14 @@ exports.transform = function(type, op, appliedOp) { ) { // If appliedOp.create is not true, appliedOp contains an op - which // also means the document exists remotely. - return {code: ERROR_CODE.DOCUMENT_CREATED_REMOTELY, message: 'Document was created remotely'}; + return {code: ERROR_CODE.ERR_DOC_ALREADY_CREATED, message: 'Document was created remotely'}; } else if (appliedOp.op && op.op) { // If we reach here, they both have a .op property. - if (!type) return {code: ERROR_CODE.DOCUMENT_DOES_NOT_EXIST, message: 'Document does not exist'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, message: 'Document does not exist'}; if (typeof type === 'string') { type = types[type]; - if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; } try { @@ -163,7 +163,7 @@ exports.applyOps = function(snapshot, ops) { if (snapshot.type) { type = types[snapshot.type]; - if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; } for (var index = 0; index < ops.length; index++) { @@ -173,7 +173,7 @@ exports.applyOps = function(snapshot, ops) { if (op.create) { type = types[op.create.type]; - if (!type) return {code: ERROR_CODE.UNKNOWN_TYPE, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; snapshot.data = type.create(op.create.data); snapshot.type = type.uri; } else if (op.del) { diff --git a/lib/projections.js b/lib/projections.js index b6a3fd656..40151374a 100644 --- a/lib/projections.js +++ b/lib/projections.js @@ -1,7 +1,7 @@ var json0 = require('ot-json0').type; var ShareDBError = require('./error'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; exports.projectSnapshot = projectSnapshot; exports.projectSnapshots = projectSnapshots; @@ -14,7 +14,7 @@ exports.isOpAllowed = isOpAllowed; function projectSnapshot(fields, snapshot) { // Only json0 supported right now if (snapshot.type && snapshot.type !== json0.uri) { - throw new Error(ERROR_CODE.CANNOT_PROJECT_TYPE, 'Cannot project snapshots of type ' + snapshot.type); + throw new Error(ERROR_CODE.ERR_TYPE_CANNOT_BE_PROJECTED, 'Cannot project snapshots of type ' + snapshot.type); } snapshot.data = projectData(fields, snapshot.data); } diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js index cd3e7e7d4..57c9640d8 100644 --- a/lib/pubsub/index.js +++ b/lib/pubsub/index.js @@ -3,7 +3,7 @@ var OpStream = require('../op-stream'); var ShareDBError = require('../error'); var util = require('../util'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; function PubSub(options) { if (!(this instanceof PubSub)) return new PubSub(options); @@ -40,19 +40,25 @@ PubSub.prototype.close = function(callback) { PubSub.prototype._subscribe = function(channel, callback) { process.nextTick(function() { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, '_subscribe PubSub method unimplemented')); + callback(new ShareDBError( + ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, + '_subscribe PubSub method unimplemented' + )); }); }; PubSub.prototype._unsubscribe = function(channel, callback) { process.nextTick(function() { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, '_unsubscribe PubSub method unimplemented')); + callback(new ShareDBError( + ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, + '_unsubscribe PubSub method unimplemented' + )); }); }; PubSub.prototype._publish = function(channels, data, callback) { process.nextTick(function() { - callback(new ShareDBError(ERROR_CODE.NOT_IMPLEMENTED, '_publish PubSub method unimplemented')); + callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, '_publish PubSub method unimplemented')); }); }; @@ -123,7 +129,7 @@ PubSub.prototype._removeStream = function(channel, stream) { if (util.hasKeys(map)) return; delete this.streams[channel]; // Synchronously clear subscribed state. We won't actually be unsubscribed - // until some unkown time in the future. If subscribe is called in this + // until some unknown time in the future. If subscribe is called in this // period, we want to send a subscription message and wait for it to // complete before we can count on being subscribed again delete this.subscribed[channel]; diff --git a/lib/query-emitter.js b/lib/query-emitter.js index f863b9e34..11525dada 100644 --- a/lib/query-emitter.js +++ b/lib/query-emitter.js @@ -3,7 +3,7 @@ var deepEqual = require('fast-deep-equal'); var ShareDBError = require('./error'); var util = require('./util'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; function QueryEmitter(request, stream, ids, extra) { this.backend = request.backend; @@ -162,7 +162,7 @@ QueryEmitter.prototype.queryPoll = function(callback) { // simultaneously per emitter will act as a natural adaptive rate limiting // in case the db is under load. // - // This isn't neccessary for the document polling case, since they operate + // This isn't necessary for the document polling case, since they operate // on a given id and won't accidentally modify the wrong doc. Also, those // queries should be faster and are less likely to be the same, so there is // less benefit to possible load reduction. @@ -286,7 +286,7 @@ QueryEmitter.prototype.queryPollDoc = function(id, callback) { }); }; -// Clients must assign each of these functions syncronously after constructing +// Clients must assign each of these functions synchronously after constructing // an instance of QueryEmitter. The instance is subscribed to an op stream at // construction time, and does not buffer emitted events. Diff events assume // all messages are received and applied in order, so it is critical that none @@ -295,7 +295,10 @@ QueryEmitter.prototype.onError = QueryEmitter.prototype.onDiff = QueryEmitter.prototype.onExtra = QueryEmitter.prototype.onOp = function() { - throw new ShareDBError(ERROR_CODE.LISTENER_NOT_ASSIGNED, 'Required QueryEmitter listener not assigned'); + throw new ShareDBError( + ERROR_CODE.ERR_QUERY_EMITTER_LISTENER_NOT_ASSIGNED, + 'Required QueryEmitter listener not assigned' + ); }; function getInserted(diff) { diff --git a/lib/submit-request.js b/lib/submit-request.js index b33f6ae94..04357a0f3 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -2,7 +2,7 @@ var ot = require('./ot'); var projections = require('./projections'); var ShareDBError = require('./error'); -var ERROR_CODE = ShareDBError.code; +var ERROR_CODE = ShareDBError.CODES; function SubmitRequest(backend, agent, index, id, op, options) { this.backend = backend; @@ -243,43 +243,49 @@ SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) { // Non-fatal client errors: SubmitRequest.prototype.alreadySubmittedError = function() { - return {code: ERROR_CODE.OP_ALREADY_SUBMITTED, message: 'Op already submitted'}; + return {code: ERROR_CODE.ERR_OP_ALREADY_SUBMITTED, message: 'Op already submitted'}; }; SubmitRequest.prototype.rejectedError = function() { - return {code: ERROR_CODE.OP_SUBMIT_REJECTED, message: 'Op submit rejected'}; + return {code: ERROR_CODE.ERR_OP_SUBMIT_REJECTED, message: 'Op submit rejected'}; }; // Fatal client errors: SubmitRequest.prototype.alreadyCreatedError = function() { - return {code: ERROR_CODE.DOCUMENT_ALREADY_CREATED, message: 'Invalid op submitted. Document already created'}; + return {code: ERROR_CODE.ERR_DOC_ALREADY_CREATED, message: 'Invalid op submitted. Document already created'}; }; SubmitRequest.prototype.newerVersionError = function() { return { - code: ERROR_CODE.OP_VERSION_NEWER_THAN_SNAPSHOT, + code: ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT, message: 'Invalid op submitted. Op version newer than current snapshot' }; }; SubmitRequest.prototype.projectionError = function() { return { - code: ERROR_CODE.OP_INVALID_IN_PROJECTION, + code: ERROR_CODE.ERR_OP_NOT_ALLOWED_IN_PROJECTION, message: 'Invalid op submitted. Operation invalid in projected collection' }; }; // Fatal internal errors: SubmitRequest.prototype.missingOpsError = function() { return { - code: ERROR_CODE.MISSING_TRANSFORM_OPS, + code: ERROR_CODE.ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND, message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version' }; }; SubmitRequest.prototype.versionDuringTransformError = function() { - return {code: ERROR_CODE.VERSION_MISMATCH, message: 'Op submit failed. Versions mismatched during op transform'}; + return { + code: ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, + message: 'Op submit failed. Versions mismatched during op transform' + }; }; SubmitRequest.prototype.versionAfterTransformError = function() { return { - code: ERROR_CODE.VERSION_MISMATCH, + code: ERROR_CODE.ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM, message: 'Op submit failed. Op version mismatches snapshot after op transform' }; }; SubmitRequest.prototype.maxRetriesError = function() { - return {code: ERROR_CODE.MAX_RETRIES, message: 'Op submit failed. Maximum submit retries exceeded'}; + return { + code: ERROR_CODE.ERR_MAX_SUBMIT_RETRIES_EXCEEDED, + message: 'Op submit failed. Exceeded max submit retries of ' + this.maxRetries + }; }; diff --git a/test/backend.js b/test/backend.js index e6a87f806..e48b19122 100644 --- a/test/backend.js +++ b/test/backend.js @@ -95,7 +95,7 @@ describe('Backend', function() { opsOptions: {metadata: true} }; backend.subscribe(null, 'books', '1984', null, options, function(error) { - expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(error.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); }); diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index 4ce6ed890..2fff43afe 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -142,7 +142,7 @@ describe('SnapshotVersionRequest', function() { it('errors if asking for a version that does not exist', function(done) { backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) { - expect(error.code).to.equal('ERR_INVALID_VERSION'); + expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT'); expect(snapshot).to.equal(undefined); done(); }); diff --git a/test/client/submit.js b/test/client/submit.js index 5d622e5af..8041d997d 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -637,7 +637,7 @@ module.exports = function() { if (err) return done(err); doc.pause(); doc.submitOp({p: ['age'], na: 1}, function(err) { - expect(err.code).to.equal('ERR_DOCUMENT_WAS_DELETED'); + expect(err.code).to.equal('ERR_DOC_WAS_DELETED'); expect(doc.version).equal(2); expect(doc.data).eql(undefined); done(); @@ -661,7 +661,7 @@ module.exports = function() { if (err) return done(err); doc.pause(); doc.create({age: 9}, function(err) { - expect(err.code).to.equal('ERR_DOCUMENT_CREATED_REMOTELY'); + expect(err.code).to.equal('ERR_DOC_ALREADY_CREATED'); expect(doc.version).equal(3); expect(doc.data).eql({age: 5}); done(); diff --git a/test/milestone-db.js b/test/milestone-db.js index c79c9ce53..f2472bbbf 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -14,14 +14,14 @@ describe('Base class', function() { it('calls back with an error when trying to get a snapshot', function(done) { db.getMilestoneSnapshot('books', '123', 1, function(error) { - expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(error.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); }); it('emits an error when trying to get a snapshot', function(done) { db.on('error', function(error) { - expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(error.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); @@ -30,14 +30,14 @@ describe('Base class', function() { it('calls back with an error when trying to save a snapshot', function(done) { db.saveMilestoneSnapshot('books', {}, function(error) { - expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(error.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); }); it('emits an error when trying to save a snapshot', function(done) { db.on('error', function(error) { - expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(error.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); @@ -46,14 +46,14 @@ describe('Base class', function() { it('calls back with an error when trying to get a snapshot before a time', function(done) { db.getMilestoneSnapshotAtOrBeforeTime('books', '123', 1000, function(error) { - expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(error.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); }); it('calls back with an error when trying to get a snapshot after a time', function(done) { db.getMilestoneSnapshotAtOrAfterTime('books', '123', 1000, function(error) { - expect(error.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(error.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); }); diff --git a/test/pubsub-memory.js b/test/pubsub-memory.js index a6ac9e7f4..804fd3ca1 100644 --- a/test/pubsub-memory.js +++ b/test/pubsub-memory.js @@ -14,7 +14,7 @@ describe('PubSub base class', function() { var pubsub = new PubSub(); pubsub.subscribe('x', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(err.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); }); @@ -23,7 +23,7 @@ describe('PubSub base class', function() { var pubsub = new PubSub(); pubsub.on('error', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(err.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); pubsub.subscribe('x'); @@ -38,7 +38,7 @@ describe('PubSub base class', function() { if (err) return done(err); pubsub.on('error', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(err.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); stream.destroy(); @@ -50,7 +50,7 @@ describe('PubSub base class', function() { pubsub.on('error', done); pubsub.publish(['x', 'y'], {test: true}, function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(err.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); }); @@ -59,7 +59,7 @@ describe('PubSub base class', function() { var pubsub = new PubSub(); pubsub.on('error', function(err) { expect(err).instanceOf(Error); - expect(err.code).to.equal('ERR_NOT_IMPLEMENTED'); + expect(err.code).to.equal('ERR_DATABASE_METHOD_NOT_IMPLEMENTED'); done(); }); pubsub.publish(['x', 'y'], {test: true}); From 6283301b1a719b267d7a1f627ce19beb9f3213ca Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Fri, 18 Oct 2019 10:49:16 +0100 Subject: [PATCH 3/6] Review markups - Add documentation for common errors - Americanize spelling - Add a special error for invalid Milestone DB arguments --- README.md | 79 ++++++++++++++++++++++++++++++++++++++ lib/client/doc.js | 2 +- lib/error.js | 3 +- lib/milestone-db/memory.js | 12 +++--- lib/ot.js | 12 +++--- 5 files changed, 94 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 7dc3aed3c..e7cf16039 100644 --- a/README.md +++ b/README.md @@ -662,3 +662,82 @@ ShareDB returns errors as plain JavaScript objects with the format: ``` Additional fields may be added to the error object for debugging context depending on the error. Common additional fields include `collection`, `id`, and `op`. + +### Common error codes + +#### `ERR_OP_SUBMIT_REJECTED` + +The op submitted by the client has been rejected by the server for a non-critical reason. + +When the client receives this code, it will attempt to roll back the rejected op, leaving the client in a usable state. + +This error might be used as part of standard control flow. For example, consumers may define a middleware that validates document structure, and rejects operations that do not conform to this schema using this error code to reset the client to a valid state. + +#### `ERR_OP_ALREADY_SUBMITTED` + +The same op has been received by the server twice. + +This is non-critical, and part of normal control flow, and is sent as an error in order to short-circuit the op processing. It is eventually swallowed by the server, and shouldn't need further handling. + +#### `ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND` + +The ops needed to transform the submitted op up to the current version of the snapshot could not be found. + +If a client on an old version of a document submits an op, that op needs to be transformed by all the ops that have been applied to the document in the meantime. If the server cannot fetch these ops from the database, then this error is returned. + +The most common case of this would be ops being deleted from the database. For example, let's assume we have a TTL set up on the ops in our database. Let's also say we have a client that is so old that the op corresponding to its version has been deleted by the TTL policy. If this client then attempts to submit an op, the server will not be able to find the ops required to transform the op to apply to the current version of the snapshot. + +Other causes of this error may be dropping the ops collection all together, or having the database corrupted in some other way. + +#### `ERR_MAX_SUBMIT_RETRIES_EXCEEDED` + +The number of retries defined by the `maxSubmitRetries` option has been exceeded by a submission. + +#### `ERR_DOC_ALREADY_CREATED` + +The creation request has failed, because the document was already created by another client. + +This can happen when two clients happen to simultaneously try to create the same document, and is potentially recoverable by simply fetching the already-created document. + +#### `ERR_DOC_ALREADY_DELETED` + +The deletion request has failed, because the document was already deleted by another client. + +This can happen when two clients happen to simultaneously try to delete the same document. Given that the end result is the same, this error can potentially just be ignored. + +#### `ERR_DOC_TYPE_NOT_RECOGNIZED` + +The specified document type has not been registered with ShareDB. + +This error can usually be remedied by remembering to register any types you need: + +```javascript +var ShareDB = require('sharedb'); +var richText = require('rich-text'); + +ShareDB.types.register(richText.type); +``` + +#### `ERR_DEFAULT_TYPE_MISMATCH` + +The default type being used by the client does not match the default type expected by the server. + +This will typically only happen when using a different default type to the built-in `json0` used by ShareDB by default (eg if using a fork). The exact same type must be used by both the client and the server, and should be registered as the default type: + +```javascript +var ShareDB = require('sharedb'); +var forkedJson0 = require('forked-json0'); + +// Make sure to also do this on your client +ShareDB.types.defaultType = forkedJson0.type; +``` + +#### `ERR_OP_NOT_ALLOWED_IN_PROJECTION` + +The submitted op is not valid when applied to the projection. + +This may happen if the op targets some property that is not included in the projection. + +#### `ERR_TYPE_CANNOT_BE_PROJECTED` + +The document's type cannot be projected. `json0` is currently the only type that supports projections. diff --git a/lib/client/doc.js b/lib/client/doc.js index cd3ddaf91..85e7c15ac 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -143,7 +143,7 @@ Doc.prototype._setType = function(newType) { // If we removed the type from the object, also remove its data this.data = undefined; } else { - var err = new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, 'Missing type ' + newType); + var err = new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Missing type ' + newType); return this.emit('error', err); } }; diff --git a/lib/error.js b/lib/error.js index 158ce4e06..4bf5fe171 100644 --- a/lib/error.js +++ b/lib/error.js @@ -19,12 +19,13 @@ ShareDBError.CODES = { ERR_DOC_MISSING_VERSION: 'ERR_DOC_MISSING_VERSION', ERR_DOC_ALREADY_CREATED: 'ERR_DOC_ALREADY_CREATED', ERR_DOC_DOES_NOT_EXIST: 'ERR_DOC_DOES_NOT_EXIST', - ERR_DOC_TYPE_NOT_RECOGNISED: 'ERR_DOC_TYPE_NOT_RECOGNISED', + ERR_DOC_TYPE_NOT_RECOGNIZED: 'ERR_DOC_TYPE_NOT_RECOGNIZED', ERR_DOC_WAS_DELETED: 'ERR_DOC_WAS_DELETED', ERR_INFLIGHT_OP_MISSING: 'ERR_INFLIGHT_OP_MISSING', ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION: 'ERR_INGESTED_SNAPSHOT_HAS_NO_VERSION', ERR_MAX_SUBMIT_RETRIES_EXCEEDED: 'ERR_MAX_SUBMIT_RETRIES_EXCEEDED', ERR_MESSAGE_BADLY_FORMED: 'ERR_MESSAGE_BADLY_FORMED', + ERR_MILESTONE_ARGUMENT_INVALID: 'ERR_MILESTONE_ARGUMENT_INVALID', ERR_OP_ALREADY_SUBMITTED: 'ERR_OP_ALREADY_SUBMITTED', ERR_OP_NOT_ALLOWED_IN_PROJECTION: 'ERR_OP_NOT_ALLOWED_IN_PROJECTION', ERR_OP_SUBMIT_REJECTED: 'ERR_OP_SUBMIT_REJECTED', diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index 278a1788d..75cf52e1c 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -26,7 +26,7 @@ MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); MemoryMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { if (!this._isValidVersion(version)) { - return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Invalid version')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Invalid version')); } var predicate = versionLessThanOrEqualTo(version); @@ -39,8 +39,8 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapsho this.emit('save', collection, snapshot); }.bind(this); - if (!collection) return callback(new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing collection')); - if (!snapshot) return callback(new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing snapshot')); + if (!collection) return callback(new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Missing collection')); + if (!snapshot) return callback(new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Missing snapshot')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); milestoneSnapshots.push(snapshot); @@ -53,7 +53,7 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapsho MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { if (!this._isValidTimestamp(timestamp)) { - return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Invalid timestamp')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Invalid timestamp')); } var filter = timestampLessThanOrEqualTo(timestamp); @@ -62,7 +62,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collec MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) { if (!this._isValidTimestamp(timestamp)) { - return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Invalid timestamp')); + return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Invalid timestamp')); } var filter = timestampGreaterThanOrEqualTo(timestamp); @@ -84,7 +84,7 @@ MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, br callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing collection') ); } - if (!id) return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing ID')); + if (!id) return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Missing ID')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); diff --git a/lib/ot.js b/lib/ot.js index 494b2ef29..d80fd5650 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -24,7 +24,7 @@ exports.checkOp = function(op) { } var type = types[typeName]; if (type == null || typeof type !== 'object') { - return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; + return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; } } else if (op.del != null) { if (op.del !== true) return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'del value must be true'}; @@ -72,7 +72,7 @@ exports.apply = function(snapshot, op) { // The document doesn't exist, although it might have once existed var create = op.create; var type = types[create.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; try { snapshot.data = type.create(create.data); @@ -105,7 +105,7 @@ function applyOpEdit(snapshot, edit) { if (edit == null) return {code: ERROR_CODE.ERR_OT_OP_NOT_PROVIDED, message: 'Missing op'}; var type = types[snapshot.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; try { snapshot.data = type.apply(snapshot.data, edit); @@ -138,7 +138,7 @@ exports.transform = function(type, op, appliedOp) { if (typeof type === 'string') { type = types[type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; } try { @@ -163,7 +163,7 @@ exports.applyOps = function(snapshot, ops) { if (snapshot.type) { type = types[snapshot.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; } for (var index = 0; index < ops.length; index++) { @@ -173,7 +173,7 @@ exports.applyOps = function(snapshot, ops) { if (op.create) { type = types[op.create.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNISED, message: 'Unknown type'}; + if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; snapshot.data = type.create(op.create.data); snapshot.type = type.uri; } else if (op.del) { From a80e79bcdf15a377bf26691122924f44c6bbb6b6 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 14 Nov 2019 09:31:36 +1300 Subject: [PATCH 4/6] Review markups --- README.md | 2 +- lib/milestone-db/memory.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index e7cf16039..76348bfb1 100644 --- a/README.md +++ b/README.md @@ -699,7 +699,7 @@ The creation request has failed, because the document was already created by ano This can happen when two clients happen to simultaneously try to create the same document, and is potentially recoverable by simply fetching the already-created document. -#### `ERR_DOC_ALREADY_DELETED` +#### `ERR_DOC_WAS_DELETED` The deletion request has failed, because the document was already deleted by another client. diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index 75cf52e1c..8ccc1ae81 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -81,7 +81,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collect MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, breakCondition, callback) { if (!collection) { return process.nextTick( - callback, new ShareDBError(ERROR_CODE.ERR_UNKNOWN_ERROR, 'Missing collection') + callback, new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Missing collection') ); } if (!id) return process.nextTick(callback, new ShareDBError(ERROR_CODE.ERR_MILESTONE_ARGUMENT_INVALID, 'Missing ID')); From 751728ca4f33b7f9b123d556c4107d9f0c286b1d Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 14 Nov 2019 09:57:41 +1300 Subject: [PATCH 5/6] Wrap all errors in `ShareDBError` This change drops our dependence on `make-error`, and instead implements our own simple `ShareDBError` class, which extends `Error`. We also wrap all anonymous error objects in this `ShareDBError`, so that its type can be checked by consumers. --- README.md | 10 +------- lib/agent.js | 8 +++---- lib/backend.js | 26 ++++++++++----------- lib/error.js | 16 +++++++++---- lib/ot.js | 48 +++++++++++++++++++------------------- lib/submit-request.js | 54 +++++++++++++++++++++---------------------- package.json | 1 - test/error.js | 49 +++++++++++++++++++++++++++++++++++++++ 8 files changed, 130 insertions(+), 82 deletions(-) create mode 100644 test/error.js diff --git a/README.md b/README.md index 76348bfb1..178a4719b 100644 --- a/README.md +++ b/README.md @@ -653,15 +653,7 @@ ShareDB only supports the following logger methods: ## Errors -ShareDB returns errors as plain JavaScript objects with the format: -``` -{ - code: 'ERR_UNKNOWN_ERROR', - message: 'ShareDB internal error' -} -``` - -Additional fields may be added to the error object for debugging context depending on the error. Common additional fields include `collection`, `id`, and `op`. +ShareDB returns errors as an instance of `ShareDBError`, with a machine-parsable `code`, as well as more details in the human-readable `message`. ### Common error codes diff --git a/lib/agent.js b/lib/agent.js index 87570ab10..a332fb89e 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -290,7 +290,7 @@ Agent.prototype._open = function() { if (agent.closed) return; if (typeof chunk !== 'object') { - var err = {code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: 'Received non-object message'}; + var err = new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Received non-object message'); return agent.close(err); } @@ -333,7 +333,7 @@ Agent.prototype._checkRequest = function(request) { Agent.prototype._handleMessage = function(request, callback) { try { var errMessage = this._checkRequest(request); - if (errMessage) return callback({code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: errMessage}); + if (errMessage) return callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, errMessage)); switch (request.a) { case 'qf': @@ -357,14 +357,14 @@ Agent.prototype._handleMessage = function(request, callback) { case 'op': // Normalize the properties submitted var op = createClientOp(request, this.clientId); - if (!op) return callback({code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: 'Invalid op message'}); + if (!op) return callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Invalid op message')); return this._submit(request.c, request.d, op, callback); case 'nf': return this._fetchSnapshot(request.c, request.d, request.v, callback); case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: - callback({code: ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, message: 'Invalid or unknown message'}); + callback(new ShareDBError(ERROR_CODE.ERR_MESSAGE_BADLY_FORMED, 'Invalid or unknown message')); } } catch (err) { callback(err); diff --git a/lib/backend.js b/lib/backend.js index ee1a710a6..3fc0aa655 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -485,10 +485,10 @@ Backend.prototype.subscribe = function(agent, index, id, version, options, callb // add the ability to SubmitRequest.commit to optionally pass the metadata to other clients on // PubSub. This behaviour is not needed right now, but we have added an options object to the // subscribe() signature so that it remains consistent with getOps() and fetch(). - return callback({ - code: ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, - message: 'Passing options to subscribe has not been implemented' - }); + return callback(new ShareDBError( + ERROR_CODE.ERR_DATABASE_METHOD_NOT_IMPLEMENTED, + 'Passing options to subscribe has not been implemented' + )); } var start = Date.now(); var projection = this.projections[index]; @@ -610,10 +610,10 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba backend._triggerQuery(agent, index, query, options, function(err, request) { if (err) return callback(err); if (request.db.disableSubscribe) { - return callback({ - code: ERROR_CODE.ERR_DATABASE_DOES_NOT_SUPPORT_SUBSCRIBE, - message: 'DB does not support subscribe' - }); + return callback(new ShareDBError( + ERROR_CODE.ERR_DATABASE_DOES_NOT_SUPPORT_SUBSCRIBE, + 'DB does not support subscribe' + )); } backend.pubsub.subscribe(request.channel, function(err, stream) { if (err) return callback(err); @@ -659,7 +659,7 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac // Set the DB reference for the request after the middleware trigger so // that the db option can be changed in middleware request.db = (options.db) ? backend.extraDbs[options.db] : backend.db; - if (!request.db) return callback({code: ERROR_CODE.ERR_DATABASE_ADAPTER_NOT_FOUND, message: 'DB not found'}); + if (!request.db) return callback(new ShareDBError(ERROR_CODE.ERR_DATABASE_ADAPTER_NOT_FOUND, 'DB not found')); request.snapshotProjection = backend._getSnapshotProjection(request.db, projection); callback(null, request); }); @@ -740,10 +740,10 @@ Backend.prototype._fetchSnapshot = function(collection, id, version, callback) { if (error) return callback(error); if (version > snapshot.v) { - return callback({ - code: ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT, - message: 'Requested version exceeds latest snapshot version' - }); + return callback(new ShareDBError( + ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT, + 'Requested version exceeds latest snapshot version' + )); } callback(null, snapshot); diff --git a/lib/error.js b/lib/error.js index 4bf5fe171..efb49adb9 100644 --- a/lib/error.js +++ b/lib/error.js @@ -1,11 +1,19 @@ -var makeError = require('make-error'); - function ShareDBError(code, message) { - ShareDBError.super.call(this, message); + this.name = 'ShareDBError'; this.code = code || ShareDBError.CODES.ERR_UNKNOWN_ERROR; + this.message = message; + if (this.canCaptureStackTrace()) { + Error.captureStackTrace(this, ShareDBError); + } else { + this.stack = new Error().stack; + } } -makeError(ShareDBError); +ShareDBError.prototype = Object.create(Error.prototype); + +ShareDBError.prototype.canCaptureStackTrace = function() { + return !!Error.captureStackTrace; +}; ShareDBError.CODES = { ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT: 'ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT', diff --git a/lib/ot.js b/lib/ot.js index d80fd5650..1de04ef64 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -11,42 +11,42 @@ var ERROR_CODE = ShareDBError.CODES; // Returns an error string on failure. Rockin' it C style. exports.checkOp = function(op) { if (op == null || typeof op !== 'object') { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Op must be an object'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Op must be an object'); } if (op.create != null) { if (typeof op.create !== 'object') { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Create data must be an object'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Create data must be an object'); } var typeName = op.create.type; if (typeof typeName !== 'string') { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Missing create type'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Missing create type'); } var type = types[typeName]; if (type == null || typeof type !== 'object') { - return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; + return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); } } else if (op.del != null) { - if (op.del !== true) return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'del value must be true'}; + if (op.del !== true) return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'del value must be true'); } else if (op.op == null) { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Missing op, create, or del'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Missing op, create, or del'); } if (op.src != null && typeof op.src !== 'string') { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'src must be a string'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'src must be a string'); } if (op.seq != null && typeof op.seq !== 'number') { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'seq must be a string'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'seq must be a string'); } if ( (op.src == null && op.seq != null) || (op.src != null && op.seq == null) ) { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'Both src and seq must be set together'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Both src and seq must be set together'); } if (op.m != null && typeof op.m !== 'object') { - return {code: ERROR_CODE.ERR_OT_OP_BADLY_FORMED, message: 'op.m must be an object or null'}; + return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'op.m must be an object or null'); } }; @@ -59,20 +59,20 @@ exports.normalizeType = function(typeName) { // type) and edits it in-place. Returns an error or null for success. exports.apply = function(snapshot, op) { if (typeof snapshot !== 'object') { - return {code: ERROR_CODE.ERR_APPLY_SNAPSHOT_NOT_PROVIDED, message: 'Missing snapshot'}; + return new ShareDBError(ERROR_CODE.ERR_APPLY_SNAPSHOT_NOT_PROVIDED, 'Missing snapshot'); } if (snapshot.v != null && op.v != null && snapshot.v !== op.v) { - return {code: ERROR_CODE.ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT, message: 'Version mismatch'}; + return new ShareDBError(ERROR_CODE.ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT, 'Version mismatch'); } // Create operation if (op.create) { - if (snapshot.type) return {code: ERROR_CODE.ERR_DOC_ALREADY_CREATED, message: 'Document already exists'}; + if (snapshot.type) return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document already exists'); // The document doesn't exist, although it might have once existed var create = op.create; var type = types[create.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; + if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); try { snapshot.data = type.create(create.data); @@ -101,11 +101,11 @@ exports.apply = function(snapshot, op) { }; function applyOpEdit(snapshot, edit) { - if (!snapshot.type) return {code: ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, message: 'Document does not exist'}; + if (!snapshot.type) return new ShareDBError(ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Document does not exist'); - if (edit == null) return {code: ERROR_CODE.ERR_OT_OP_NOT_PROVIDED, message: 'Missing op'}; + if (edit == null) return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_PROVIDED, 'Missing op'); var type = types[snapshot.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; + if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); try { snapshot.data = type.apply(snapshot.data, edit); @@ -118,12 +118,12 @@ exports.transform = function(type, op, appliedOp) { // There are 16 cases this function needs to deal with - which are all the // combinations of create/delete/op/noop from both op and appliedOp if (op.v != null && op.v !== appliedOp.v) { - return {code: ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, message: 'Version mismatch'}; + return new ShareDBError(ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, 'Version mismatch'); } if (appliedOp.del) { if (op.create || op.op) { - return {code: ERROR_CODE.ERR_DOC_WAS_DELETED, message: 'Document was deleted'}; + return new ShareDBError(ERROR_CODE.ERR_DOC_WAS_DELETED, 'Document was deleted'); } } else if ( (appliedOp.create && (op.op || op.create || op.del)) || @@ -131,14 +131,14 @@ exports.transform = function(type, op, appliedOp) { ) { // If appliedOp.create is not true, appliedOp contains an op - which // also means the document exists remotely. - return {code: ERROR_CODE.ERR_DOC_ALREADY_CREATED, message: 'Document was created remotely'}; + return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document was created remotely'); } else if (appliedOp.op && op.op) { // If we reach here, they both have a .op property. - if (!type) return {code: ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, message: 'Document does not exist'}; + if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Document does not exist'); if (typeof type === 'string') { type = types[type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; + if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); } try { @@ -163,7 +163,7 @@ exports.applyOps = function(snapshot, ops) { if (snapshot.type) { type = types[snapshot.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; + if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); } for (var index = 0; index < ops.length; index++) { @@ -173,7 +173,7 @@ exports.applyOps = function(snapshot, ops) { if (op.create) { type = types[op.create.type]; - if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; + if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); snapshot.data = type.create(op.create.data); snapshot.type = type.uri; } else if (op.del) { diff --git a/lib/submit-request.js b/lib/submit-request.js index 04357a0f3..092f5261a 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -243,49 +243,49 @@ SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) { // Non-fatal client errors: SubmitRequest.prototype.alreadySubmittedError = function() { - return {code: ERROR_CODE.ERR_OP_ALREADY_SUBMITTED, message: 'Op already submitted'}; + return new ShareDBError(ERROR_CODE.ERR_OP_ALREADY_SUBMITTED, 'Op already submitted'); }; SubmitRequest.prototype.rejectedError = function() { - return {code: ERROR_CODE.ERR_OP_SUBMIT_REJECTED, message: 'Op submit rejected'}; + return new ShareDBError(ERROR_CODE.ERR_OP_SUBMIT_REJECTED, 'Op submit rejected'); }; // Fatal client errors: SubmitRequest.prototype.alreadyCreatedError = function() { - return {code: ERROR_CODE.ERR_DOC_ALREADY_CREATED, message: 'Invalid op submitted. Document already created'}; + return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Invalid op submitted. Document already created'); }; SubmitRequest.prototype.newerVersionError = function() { - return { - code: ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT, - message: 'Invalid op submitted. Op version newer than current snapshot' - }; + return new ShareDBError( + ERROR_CODE.ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT, + 'Invalid op submitted. Op version newer than current snapshot' + ); }; SubmitRequest.prototype.projectionError = function() { - return { - code: ERROR_CODE.ERR_OP_NOT_ALLOWED_IN_PROJECTION, - message: 'Invalid op submitted. Operation invalid in projected collection' - }; + return new ShareDBError( + ERROR_CODE.ERR_OP_NOT_ALLOWED_IN_PROJECTION, + 'Invalid op submitted. Operation invalid in projected collection' + ); }; // Fatal internal errors: SubmitRequest.prototype.missingOpsError = function() { - return { - code: ERROR_CODE.ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND, - message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version' - }; + return new ShareDBError( + ERROR_CODE.ERR_SUBMIT_TRANSFORM_OPS_NOT_FOUND, + 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version' + ); }; SubmitRequest.prototype.versionDuringTransformError = function() { - return { - code: ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, - message: 'Op submit failed. Versions mismatched during op transform' - }; + return new ShareDBError( + ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, + 'Op submit failed. Versions mismatched during op transform' + ); }; SubmitRequest.prototype.versionAfterTransformError = function() { - return { - code: ERROR_CODE.ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM, - message: 'Op submit failed. Op version mismatches snapshot after op transform' - }; + return new ShareDBError( + ERROR_CODE.ERR_OP_VERSION_MISMATCH_AFTER_TRANSFORM, + 'Op submit failed. Op version mismatches snapshot after op transform' + ); }; SubmitRequest.prototype.maxRetriesError = function() { - return { - code: ERROR_CODE.ERR_MAX_SUBMIT_RETRIES_EXCEEDED, - message: 'Op submit failed. Exceeded max submit retries of ' + this.maxRetries - }; + return new ShareDBError( + ERROR_CODE.ERR_MAX_SUBMIT_RETRIES_EXCEEDED, + 'Op submit failed. Exceeded max submit retries of ' + this.maxRetries + ); }; diff --git a/package.json b/package.json index a8bec97d0..c635c77db 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "async": "^1.4.2", "fast-deep-equal": "^2.0.1", "hat": "0.0.3", - "make-error": "^1.1.1", "ot-json0": "^1.0.1" }, "devDependencies": { diff --git a/test/error.js b/test/error.js new file mode 100644 index 000000000..64782c01b --- /dev/null +++ b/test/error.js @@ -0,0 +1,49 @@ +var ShareDBError = require('../lib/error'); +var expect = require('chai').expect; +var sinon = require('sinon'); + +describe('ShareDBError', function() { + afterEach(function() { + sinon.restore(); + }); + + it('can be identified by instanceof', function() { + var error = new ShareDBError(); + expect(error).to.be.instanceof(ShareDBError); + }); + + it('has a code', function() { + var error = new ShareDBError('ERROR_CODE'); + expect(error.code).to.equal('ERROR_CODE'); + }); + + it('has a message', function() { + var error = new ShareDBError(null, 'Detailed message'); + expect(error.message).to.equal('Detailed message'); + }); + + it('has a stack trace', function() { + function badFunction() { + throw new ShareDBError(); + } + + try { + badFunction(); + } catch (error) { + expect(error.stack).to.contain('badFunction'); + } + }); + + it('has a stack trace when Error.captureStackTrace is not implemented', function() { + sinon.stub(ShareDBError.prototype, 'canCaptureStackTrace').returns(false); + function badFunction() { + throw new ShareDBError(); + } + + try { + badFunction(); + } catch (error) { + expect(error.stack).to.contain('badFunction'); + } + }); +}); From c0e9e0b0101a2cc5336d5947684d066291bab24e Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 21 Nov 2019 06:30:42 +1300 Subject: [PATCH 6/6] Review markups --- lib/error.js | 13 +++++-------- test/error.js | 18 ------------------ 2 files changed, 5 insertions(+), 26 deletions(-) diff --git a/lib/error.js b/lib/error.js index efb49adb9..e169f3cdc 100644 --- a/lib/error.js +++ b/lib/error.js @@ -1,8 +1,7 @@ function ShareDBError(code, message) { - this.name = 'ShareDBError'; - this.code = code || ShareDBError.CODES.ERR_UNKNOWN_ERROR; - this.message = message; - if (this.canCaptureStackTrace()) { + this.code = code; + this.message = message || ''; + if (Error.captureStackTrace) { Error.captureStackTrace(this, ShareDBError); } else { this.stack = new Error().stack; @@ -10,10 +9,8 @@ function ShareDBError(code, message) { } ShareDBError.prototype = Object.create(Error.prototype); - -ShareDBError.prototype.canCaptureStackTrace = function() { - return !!Error.captureStackTrace; -}; +ShareDBError.prototype.constructor = ShareDBError; +ShareDBError.prototype.name = 'ShareDBError'; ShareDBError.CODES = { ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT: 'ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT', diff --git a/test/error.js b/test/error.js index 64782c01b..2a2938704 100644 --- a/test/error.js +++ b/test/error.js @@ -1,12 +1,7 @@ var ShareDBError = require('../lib/error'); var expect = require('chai').expect; -var sinon = require('sinon'); describe('ShareDBError', function() { - afterEach(function() { - sinon.restore(); - }); - it('can be identified by instanceof', function() { var error = new ShareDBError(); expect(error).to.be.instanceof(ShareDBError); @@ -33,17 +28,4 @@ describe('ShareDBError', function() { expect(error.stack).to.contain('badFunction'); } }); - - it('has a stack trace when Error.captureStackTrace is not implemented', function() { - sinon.stub(ShareDBError.prototype, 'canCaptureStackTrace').returns(false); - function badFunction() { - throw new ShareDBError(); - } - - try { - badFunction(); - } catch (error) { - expect(error.stack).to.contain('badFunction'); - } - }); });