diff --git a/.editorconfig b/.editorconfig
deleted file mode 100644
index e29f5e504..000000000
--- a/.editorconfig
+++ /dev/null
@@ -1,9 +0,0 @@
-root = true
-
-[*]
-indent_style = space
-indent_size = 2
-end_of_line = LF
-charset = utf-8
-trim_trailing_whitespace = true
-insert_final_newline = true
diff --git a/.eslintrc.js b/.eslintrc.js
new file mode 100644
index 000000000..ab65eaee8
--- /dev/null
+++ b/.eslintrc.js
@@ -0,0 +1,47 @@
+// The ESLint ecmaVersion argument is inconsistently used. Some rules will ignore it entirely, so if the rule has
+// been set, it will still error even if it's not applicable to that version number. Since Google sets these
+// rules, we have to turn them off ourselves.
+const DISABLED_ES6_OPTIONS = {
+ 'no-var': 'off',
+ 'prefer-rest-params': 'off'
+};
+
+const SHAREDB_RULES = {
+ // Comma dangle is not supported in ES3
+ 'comma-dangle': ['error', 'never'],
+ // We control our own objects and prototypes, so no need for this check
+ 'guard-for-in': 'off',
+ // Google prescribes different indents for different cases. Let's just use 2 spaces everywhere. Note that we have
+ // to override ESLint's default of 0 indents for this.
+ 'indent': ['error', 2, {
+ 'SwitchCase': 1
+ }],
+ // Less aggressive line length than Google, which is especially useful when we have a lot of callbacks in our code
+ 'max-len': ['error',
+ {
+ code: 120,
+ tabWidth: 2,
+ ignoreUrls: true,
+ }
+ ],
+ // Google overrides the default ESLint behaviour here, which is slightly better for catching erroneously unused variables
+ 'no-unused-vars': ['error', {vars: 'all', args: 'after-used'}],
+ // It's more readable to ensure we only have one statement per line
+ 'max-statements-per-line': ['error', {max: 1}],
+ // as-needed quote props are easier to write
+ 'quote-props': ['error', 'as-needed'],
+ 'require-jsdoc': 'off',
+ 'valid-jsdoc': 'off'
+};
+
+module.exports = {
+ extends: 'google',
+ parserOptions: {
+ ecmaVersion: 3
+ },
+ rules: Object.assign(
+ {},
+ DISABLED_ES6_OPTIONS,
+ SHAREDB_RULES
+ ),
+};
diff --git a/.gitignore b/.gitignore
index cd1d217b2..abd0d58e5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,9 @@
# Emacs
\#*\#
+# VS Code
+.vscode/
+
# Logs
logs
*.log
@@ -30,4 +33,6 @@ coverage
# Dependency directories
node_modules
+package-lock.json
+yarn.lock
jspm_packages
diff --git a/.jshintrc b/.jshintrc
deleted file mode 100644
index cf514a151..000000000
--- a/.jshintrc
+++ /dev/null
@@ -1,18 +0,0 @@
-{
- "node": true,
- "laxcomma": true,
- "eqnull": true,
- "eqeqeq": true,
- "indent": 2,
- "newcap": true,
- "quotmark": "single",
- "undef": true,
- "trailing": true,
- "shadow": true,
- "expr": true,
- "boss": true,
- "globals": {
- "window": false,
- "document": false
- }
-}
diff --git a/.travis.yml b/.travis.yml
index 736e5fe78..5dc6cbaa0 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,6 +3,6 @@ node_js:
- "10"
- "8"
- "6"
-script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover"
+script: "npm run lint && npm run test-cover"
# Send coverage data to Coveralls
after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js"
diff --git a/README.md b/README.md
index c627942ca..b2d1f3473 100644
--- a/README.md
+++ b/README.md
@@ -27,10 +27,26 @@ tracker](https://github.com/share/sharedb/issues).
- Projections to select desired fields from documents and operations
- Middleware for implementing access control and custom extensions
- Ideal for use in browsers or on the server
-- Reconnection of document and query subscriptions
- Offline change syncing upon reconnection
- In-memory implementations of database and pub/sub for unit testing
+### Reconnection
+
+**TLDR**
+```javascript
+const WebSocket = require('reconnecting-websocket');
+var socket = new WebSocket('ws://' + window.location.host);
+var connection = new sharedb.Connection(socket);
+```
+
+The native Websocket object that you feed to ShareDB's `Connection` constructor **does not** handle reconnections.
+
+The easiest way is to give it a WebSocket object that does reconnect. There are plenty of example on the web. The most important thing is that the custom reconnecting websocket, must have the same API as the native rfc6455 version.
+
+In the "textarea" example we show this off using a Reconnecting Websocket implementation from [reconnecting-websocket](https://github.com/pladaria/reconnecting-websocket).
+
+
+
## Example apps
[
@@ -58,9 +74,21 @@ initial data. Then you can submit editing operations on the document (using
OT). Finally you can delete the document with a delete operation. By
default, ShareDB stores all operations forever - nothing is truly deleted.
-## User presence synchronization
+## User Presence Synchronization
+
+ShareDB supports synchronization of user presence data such as cursor positions and text selections. This feature is opt-in, not enabled by default. To enable this feature, pass a presence implementation as the `presence` option to the ShareDB constructor.
-Presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs), for example it may contain a user ID and a cursor position in a text document. All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document.
+ShareDB includes an implementation of presence called `StatelessPresence`. This provides an implementation of presence that works out of the box, but it has some scalability problems. Each time a client joins a document, this implementation requests current presence information from all other clients, via the server. This approach may be problematic in terms of performance when a large number of users are present on the same document simultaneously. If you don't expect too many simultaneous users per document, `StatelessPresence` should work well. The server does not store any state at all regarding presence (it exists only in clients), hence the name "Stateless Presence".
+
+In `StatelessPresence`, presence data represents a user and is automatically synchronized between all clients subscribed to the same document. Its format is defined by the document's [OT Type](https://github.com/ottypes/docs) (specifically, by [`transformPresence`, `createPresence`, and `comparePresence`](https://github.com/teamwork/ot-docs#optional-properties)). All clients can modify their own presence data and receive a read-only version of other client's data. Presence data is automatically cleared when a client unsubscribes from the document or disconnects. It is also automatically transformed against applied operations, so that it still makes sense in the context of a modified document, for example a cursor position may be automatically advanced when a user types at the beginning of a text document.
+
+To use `StatelessPresence`, pass it into the ShareDB constructor like this:
+
+```js
+var ShareDB = require('sharedb');
+var statelessPresence = require('sharedb/lib/presence/stateless');
+var share = new ShareDB({ presence: statelessPresence })`).
+```
## Server API
@@ -80,6 +108,8 @@ __Options__
* `options.pubsub` _(instance of `ShareDB.PubSub`)_
Notify other ShareDB processes when data changes
through this pub/sub adapter. Defaults to `ShareDB.MemoryPubSub()`.
+* `options.presence` _(implementation of presence classes)_
+ Enable user presence synchronization. The value of `options.presence` option is expected to contain implementations of the classes `DocPresence`, `ConnectionPresence`, `AgentPresence`, and `BackendPresence`. Logic related to presence is encapsulated within these classes, so it is possible develop additional third party presence implementations external to ShareDB.
#### Database Adapters
* `ShareDB.MemoryDB`, backed by a non-persistent database with no queries
@@ -101,7 +131,7 @@ Community Provided Pub/Sub Adapters
### Listening to WebSocket connections
```js
-var WebSocketJSONStream = require('websocket-json-stream');
+var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
// 'ws' is a websocket server connection, as passed into
// new (require('ws').Server).on('connection', ...)
@@ -128,7 +158,6 @@ Register a new middleware.
One of:
* `'connect'`: A new client connected to the server.
* `'op'`: An operation was loaded from the database.
- * `'doc'`: DEPRECATED: A snapshot was loaded from the database. Please use 'readSnapshots'
* `'readSnapshots'`: Snapshot(s) were loaded from the database for a fetch or subscribe of a query or document
* `'query'`: A query is about to be sent to the database
* `'submit'`: An operation is about to be submitted to the database
@@ -139,17 +168,24 @@ Register a new middleware.
* `'afterSubmit'`: An operation was successfully submitted to
the database.
* `'receive'`: Received a message from a client
-* `fn` _(Function(request, callback))_
+ * `'reply'`: About to send a non-error reply to a client message
+* `fn` _(Function(context, callback))_
Call this function at the time specified by `action`.
- `request` contains a subset of the following properties, as relevant for the action:
- * `action`: The action this middleware is handing
- * `agent`: An object corresponding to the server agent handing this client
- * `req`: The HTTP request being handled
- * `collection`: The collection name being handled
- * `id`: The document id being handled
- * `snapshots`: The retrieved snapshots for the `readSnapshots` action
- * `query`: The query object being handled
- * `op`: The op being handled
+ * `context` will always have the following properties:
+ * `action`: The action this middleware is hanlding
+ * `agent`: A reference to the server agent handling this client
+ * `backend`: A reference to this ShareDB backend instance
+ * `context` can also have additional properties, as relevant for the action:
+ * `collection`: The collection name being handled
+ * `id`: The document id being handled
+ * `op`: The op being handled
+ * `req`: HTTP request being handled, if provided to `share.listen` (for 'connect')
+ * `stream`: The duplex Stream provided to `share.listen` (for 'connect')
+ * `query`: The query object being handled (for 'query')
+ * `snapshots`: Array of retrieved snapshots (for 'readSnapshots')
+ * `data`: Received client message (for 'receive')
+ * `request`: Client message being replied to (for 'reply')
+ * `reply`: Reply to be sent to the client (for 'reply')
### Projections
@@ -172,6 +208,27 @@ share.addProjection('users_limited', 'users', { name:true, profileUrl:true });
Note that only the [JSON0 OT type](https://github.com/ottypes/json0) is supported for projections.
+### Logging
+
+By default, ShareDB logs to `console`. This can be overridden if you wish to silence logs, or to log to your own logging driver or alert service.
+
+Methods can be overridden by passing a [`console`-like object](https://developer.mozilla.org/en-US/docs/Web/API/console) to `logger.setMethods`:
+
+```javascript
+var ShareDB = require('sharedb');
+ShareDB.logger.setMethods({
+ info: () => {}, // Silence info
+ warn: () => alerts.warn(arguments), // Forward warnings to alerting service
+ error: () => alerts.critical(arguments) // Remap errors to critical alerts
+});
+```
+
+ShareDB only supports the following logger methods:
+
+ - `info`
+ - `warn`
+ - `error`
+
### Shutdown
`share.close(callback)`
@@ -217,6 +274,48 @@ changes. Returns a [`ShareDB.Query`](#class-sharedbquery) instance.
* `options.*`
All other options are passed through to the database adapter.
+`connection.fetchSnapshot(collection, id, version, callback): void;`
+Get a read-only snapshot of a document at the requested version.
+
+* `collection` _(String)_
+ Collection name of the snapshot
+* `id` _(String)_
+ ID of the snapshot
+* `version` _(number) [optional]_
+ The version number of the desired snapshot. If `null`, the latest version is fetched.
+* `callback` _(Function)_
+ Called with `(error, snapshot)`, where `snapshot` takes the following form:
+
+ ```javascript
+ {
+ id: string; // ID of the snapshot
+ v: number; // version number of the snapshot
+ type: string; // the OT type of the snapshot, or null if it doesn't exist or is deleted
+ data: any; // the snapshot
+ }
+ ```
+
+`connection.fetchSnapshotByTimestamp(collection, id, timestamp, callback): void;`
+Get a read-only snapshot of a document at the requested version.
+
+* `collection` _(String)_
+ Collection name of the snapshot
+* `id` _(String)_
+ ID of the snapshot
+* `timestamp` _(number) [optional]_
+ The timestamp of the desired snapshot. The returned snapshot will be the latest snapshot before the provided timestamp. If `null`, the latest version is fetched.
+* `callback` _(Function)_
+ Called with `(error, snapshot)`, where `snapshot` takes the following form:
+
+ ```javascript
+ {
+ id: string; // ID of the snapshot
+ v: number; // version number of the snapshot
+ type: string; // the OT type of the snapshot, or null if it doesn't exist or is deleted
+ data: any; // the snapshot
+ }
+ ```
+
### Class: `ShareDB.Doc`
`doc.type` _(String_)
@@ -229,7 +328,7 @@ Unique document ID
Document contents. Available after document is fetched or subscribed to.
`doc.presence` _(Object)_
-Each property under `doc.presence` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data.
+Each property under `doc.presence` contains presence data shared by a client subscribed to this document. The property name is an empty string for this client's data and connection IDs for other clients' data. The structure of the presence object is defined by the OT type of the document (for example, in [ot-rich-text](https://github.com/Teamwork/ot-rich-text#presence) and [@datavis-tech/json0](https://github.com/datavis-tech/json0#presence)).
`doc.fetch(function(err) {...})`
Populate the fields on `doc` with a snapshot of the document from the server.
@@ -239,7 +338,7 @@ Populate the fields on `doc` with a snapshot of the document from the server, an
fire events on subsequent changes.
`doc.ingestSnapshot(snapshot, callback)`
-Ingest snapshot data. This data must include a version, snapshot and type. This method is generally called interally as a result of fetch or subscribe and not directly. However, it may be called directly to pass data that was transferred to the client external to the client's ShareDB connection, such as snapshot data sent along with server rendering of a webpage.
+Ingest snapshot data. The `snapshot` param must include the fields `v` (doc version), `data`, and `type` (OT type). This method is generally called interally as a result of fetch or subscribe and not directly from user code. However, it may still be called directly from user code to pass data that was transferred to the client external to the client's ShareDB connection, such as snapshot data sent along with server rendering of a webpage.
`doc.destroy()`
Unsubscribe and stop firing events.
@@ -338,6 +437,27 @@ after a sequence of diffs are handled.
`query.on('extra', function() {...}))`
(Only fires on subscription queries) `query.extra` changed.
+### Logging
+
+By default, ShareDB logs to `console`. This can be overridden if you wish to silence logs, or to log to your own logging driver or alert service.
+
+Methods can be overridden by passing a [`console`-like object](https://developer.mozilla.org/en-US/docs/Web/API/console) to `logger.setMethods`
+
+```javascript
+var ShareDB = require('sharedb/lib/client');
+ShareDB.logger.setMethods({
+ info: () => {}, // Silence info
+ warn: () => alerts.warn(arguments), // Forward warnings to alerting service
+ error: () => alerts.critical(arguments) // Remap errors to critical alerts
+});
+```
+
+ShareDB only supports the following logger methods:
+
+ - `info`
+ - `warn`
+ - `error`
+
## Error codes
@@ -376,9 +496,11 @@ Additional fields may be added to the error object for debugging context dependi
* 4021 - Invalid client id
* 4022 - Database adapter does not support queries
* 4023 - Cannot project snapshots of this type
-* 4024 - OT Type does not support presence
-* 4025 - Not subscribed to document
-* 4026 - Presence data superseded
+* 4024 - Invalid version
+* 4025 - Passing options to subscribe has not been implemented
+* 4026 - Not subscribed to document
+* 4027 - Presence data superseded
+* 4028 - OT Type does not support presence
### 5000 - Internal error
@@ -402,3 +524,7 @@ The `41xx` and `51xx` codes are reserved for use by ShareDB DB adapters, and the
* 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/examples/counter/package.json b/examples/counter/package.json
index 49b84460c..42b2e5c90 100644
--- a/examples/counter/package.json
+++ b/examples/counter/package.json
@@ -17,7 +17,7 @@
"dependencies": {
"express": "^4.14.0",
"sharedb": "^1.0.0-beta",
- "websocket-json-stream": "^0.0.1",
+ "@teamwork/websocket-json-stream": "^2.0.0",
"ws": "^1.1.0"
},
"devDependencies": {
diff --git a/examples/counter/server.js b/examples/counter/server.js
index d4b466965..f6575bfeb 100644
--- a/examples/counter/server.js
+++ b/examples/counter/server.js
@@ -2,7 +2,7 @@ var http = require('http');
var express = require('express');
var ShareDB = require('sharedb');
var WebSocket = require('ws');
-var WebSocketJSONStream = require('websocket-json-stream');
+var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
var backend = new ShareDB();
createDoc(startServer);
@@ -29,7 +29,7 @@ function startServer() {
// Connect any incoming WebSocket connection to ShareDB
var wss = new WebSocket.Server({server: server});
- wss.on('connection', function(ws, req) {
+ wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);
backend.listen(stream);
});
diff --git a/examples/leaderboard/README.md b/examples/leaderboard/README.md
index 5a4c8e002..e3b520501 100644
--- a/examples/leaderboard/README.md
+++ b/examples/leaderboard/README.md
@@ -2,7 +2,7 @@

-This is a port of [https://github.com/percolatestudio/react-leaderboard](Leaderboard) to
+This is a port of [Leaderboard](https://github.com/percolatestudio/react-leaderboard) to
ShareDB.
In this demo, data is not persisted. To persist data, run a Mongo
diff --git a/examples/leaderboard/package.json b/examples/leaderboard/package.json
index 7defaaa02..6ee5782c8 100644
--- a/examples/leaderboard/package.json
+++ b/examples/leaderboard/package.json
@@ -24,7 +24,7 @@
"sharedb-mingo-memory": "^1.0.0-beta",
"through2": "^2.0.1",
"underscore": "^1.8.3",
- "websocket-json-stream": "^0.0.3",
+ "@teamwork/websocket-json-stream": "^2.0.0",
"ws": "^1.1.0"
},
"devDependencies": {
diff --git a/examples/leaderboard/server/index.js b/examples/leaderboard/server/index.js
index 6b7972169..6aebe997f 100644
--- a/examples/leaderboard/server/index.js
+++ b/examples/leaderboard/server/index.js
@@ -1,14 +1,13 @@
-var http = require("http");
-var ShareDB = require("sharedb");
-var connect = require("connect");
+var http = require('http');
+var ShareDB = require('sharedb');
+var connect = require('connect');
var serveStatic = require('serve-static');
var ShareDBMingoMemory = require('sharedb-mingo-memory');
-var WebSocketJSONStream = require('websocket-json-stream');
+var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
var WebSocket = require('ws');
-var util = require('util');
// Start ShareDB
-var share = ShareDB({db: new ShareDBMingoMemory()});
+var share = new ShareDB({db: new ShareDBMingoMemory()});
// Create a WebSocket server
var app = connect();
@@ -16,10 +15,10 @@ app.use(serveStatic('.'));
var server = http.createServer(app);
var wss = new WebSocket.Server({server: server});
server.listen(8080);
-console.log("Listening on http://localhost:8080");
+console.log('Listening on http://localhost:8080');
// Connect any incoming WebSocket connection with ShareDB
-wss.on('connection', function(ws, req) {
+wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);
share.listen(stream);
});
@@ -27,11 +26,13 @@ wss.on('connection', function(ws, req) {
// Create initial documents
var connection = share.connect();
connection.createFetchQuery('players', {}, {}, function(err, results) {
- if (err) { throw err; }
+ if (err) {
+ throw err;
+ }
if (results.length === 0) {
- var names = ["Ada Lovelace", "Grace Hopper", "Marie Curie",
- "Carl Friedrich Gauss", "Nikola Tesla", "Claude Shannon"];
+ var names = ['Ada Lovelace', 'Grace Hopper', 'Marie Curie',
+ 'Carl Friedrich Gauss', 'Nikola Tesla', 'Claude Shannon'];
names.forEach(function(name, index) {
var doc = connection.get('players', ''+index);
diff --git a/examples/rich-text/package.json b/examples/rich-text/package.json
index 0bf0a48e8..2249e29c1 100644
--- a/examples/rich-text/package.json
+++ b/examples/rich-text/package.json
@@ -18,7 +18,7 @@
"quill": "^1.0.0-beta.11",
"rich-text": "^3.0.1",
"sharedb": "^1.0.0-beta",
- "websocket-json-stream": "^0.0.1",
+ "@teamwork/websocket-json-stream": "^2.0.0",
"ws": "^1.1.0"
},
"devDependencies": {
diff --git a/examples/rich-text/server.js b/examples/rich-text/server.js
index 5dfb02bcc..f0654cdf8 100644
--- a/examples/rich-text/server.js
+++ b/examples/rich-text/server.js
@@ -3,7 +3,7 @@ var express = require('express');
var ShareDB = require('sharedb');
var richText = require('rich-text');
var WebSocket = require('ws');
-var WebSocketJSONStream = require('websocket-json-stream');
+var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
ShareDB.types.register(richText.type);
var backend = new ShareDB();
@@ -32,7 +32,7 @@ function startServer() {
// Connect any incoming WebSocket connection to ShareDB
var wss = new WebSocket.Server({server: server});
- wss.on('connection', function(ws, req) {
+ wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);
backend.listen(stream);
});
diff --git a/examples/textarea/client.js b/examples/textarea/client.js
index 964239da3..4cbf55208 100644
--- a/examples/textarea/client.js
+++ b/examples/textarea/client.js
@@ -2,14 +2,35 @@ var sharedb = require('sharedb/lib/client');
var StringBinding = require('sharedb-string-binding');
// Open WebSocket connection to ShareDB server
+var WebSocket = require('reconnecting-websocket');
var socket = new WebSocket('ws://' + window.location.host);
var connection = new sharedb.Connection(socket);
+var element = document.querySelector('textarea');
+var statusSpan = document.getElementById('status-span');
+statusSpan.innerHTML = 'Not Connected';
+
+element.style.backgroundColor = 'gray';
+socket.onopen = function() {
+ statusSpan.innerHTML = 'Connected';
+ element.style.backgroundColor = 'white';
+};
+
+socket.onclose = function() {
+ statusSpan.innerHTML = 'Closed';
+ element.style.backgroundColor = 'gray';
+};
+
+socket.onerror = function() {
+ statusSpan.innerHTML = 'Error';
+ element.style.backgroundColor = 'red';
+};
+
// Create local Doc instance mapped to 'examples' collection document with id 'textarea'
var doc = connection.get('examples', 'textarea');
doc.subscribe(function(err) {
if (err) throw err;
- var element = document.querySelector('textarea');
- var binding = new StringBinding(element, doc);
+
+ var binding = new StringBinding(element, doc, ['content']);
binding.setup();
});
diff --git a/examples/textarea/package.json b/examples/textarea/package.json
index 8e436c2f6..1a91a3aa8 100644
--- a/examples/textarea/package.json
+++ b/examples/textarea/package.json
@@ -15,9 +15,10 @@
"license": "MIT",
"dependencies": {
"express": "^4.14.0",
+ "reconnecting-websocket": "^3.0.3",
"sharedb": "^1.0.0-beta",
"sharedb-string-binding": "^1.0.0",
- "websocket-json-stream": "^0.0.1",
+ "@teamwork/websocket-json-stream": "^2.0.0",
"ws": "^1.1.0"
},
"devDependencies": {
diff --git a/examples/textarea/server.js b/examples/textarea/server.js
index 55fbbf4d1..165c8ea0d 100644
--- a/examples/textarea/server.js
+++ b/examples/textarea/server.js
@@ -2,7 +2,7 @@ var http = require('http');
var express = require('express');
var ShareDB = require('sharedb');
var WebSocket = require('ws');
-var WebSocketJSONStream = require('websocket-json-stream');
+var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
var backend = new ShareDB();
createDoc(startServer);
@@ -14,7 +14,7 @@ function createDoc(callback) {
doc.fetch(function(err) {
if (err) throw err;
if (doc.type === null) {
- doc.create('', callback);
+ doc.create({content: ''}, callback);
return;
}
callback();
@@ -29,7 +29,7 @@ function startServer() {
// Connect any incoming WebSocket connection to ShareDB
var wss = new WebSocket.Server({server: server});
- wss.on('connection', function(ws, req) {
+ wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);
backend.listen(stream);
});
diff --git a/examples/textarea/static/index.html b/examples/textarea/static/index.html
index c30403443..163c52bf4 100644
--- a/examples/textarea/static/index.html
+++ b/examples/textarea/static/index.html
@@ -1,6 +1,7 @@
ShareDB Textarea
+
+
Text Area Example with Reconnecting Websockets
+
Connection Status:
diff --git a/lib/agent.js b/lib/agent.js
index ac9c12d70..dbd35318c 100644
--- a/lib/agent.js
+++ b/lib/agent.js
@@ -1,7 +1,7 @@
var hat = require('hat');
var util = require('./util');
var types = require('./types');
-var ShareDBError = require('./error');
+var logger = require('./logger');
/**
* Agent deserializes the wire protocol messages received from the stream and
@@ -26,8 +26,7 @@ function Agent(backend, stream) {
// Map from queryId -> emitter
this.subscribedQueries = {};
- // The max presence sequence number received from the client.
- this.maxPresenceSeq = 0;
+ this._agentPresence = new backend.presence.AgentPresence(this);
// We need to track this manually to make sure we don't reply to messages
// after the stream was closed.
@@ -51,7 +50,7 @@ module.exports = Agent;
// Close the agent with the client.
Agent.prototype.close = function(err) {
if (err) {
- console.warn('Agent closed due to error', this.clientId, err.stack || err);
+ logger.warn('Agent closed due to error', this.clientId, err.stack || err);
}
if (this.closed) return;
// This will end the writable stream and emit 'finish'
@@ -59,8 +58,14 @@ Agent.prototype.close = function(err) {
};
Agent.prototype._cleanup = function() {
+ // Only clean up once if the stream emits both 'end' and 'close'.
+ if (this.closed) return;
+
this.closed = true;
+ this.backend.agentsCount--;
+ if (!this.stream.isServer) this.backend.remoteAgentsCount--;
+
// Clean up doc subscription streams
for (var collection in this.subscribedDocs) {
var docs = this.subscribedDocs[collection];
@@ -99,27 +104,25 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) {
// Log then silently ignore errors in a subscription stream, since these
// may not be the client's fault, and they were not the result of a
// direct request by the client
- console.error('Doc subscription stream error', collection, id, data.error);
+ logger.error('Doc subscription stream error', collection, id, data.error);
return;
}
- if (data.a === 'p') {
- // Send other clients' presence data
- if (data.src !== agent.clientId) agent.send(data);
+ if (agent._agentPresence.isPresenceMessage(data)) {
+ agent._agentPresence.processPresenceData(data);
return;
}
if (agent._isOwnOp(collection, data)) return;
agent._sendOp(collection, id, data);
});
stream.on('end', function() {
- var presence = agent._createPresence(collection, id);
- agent.backend.sendPresence(presence);
// The op stream is done sending, so release its reference
var streams = agent.subscribedDocs[collection];
- if (!streams) return;
+ if (!streams || streams[id] !== stream) return;
delete streams[id];
if (util.hasKeys(streams)) return;
delete agent.subscribedDocs[collection];
});
+ this._agentPresence.subscribeToStream(collection, id, stream);
};
Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query) {
@@ -148,7 +151,7 @@ Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query
// Log then silently ignore errors in a subscription stream, since these
// may not be the client's fault, and they were not the result of a
// direct request by the client
- console.error('Query subscription stream error', collection, query, err);
+ logger.error('Query subscription stream error', collection, query, err);
};
emitter.onOp = function(op) {
@@ -197,23 +200,29 @@ Agent.prototype._sendOps = function(collection, id, ops) {
}
};
+function getReplyErrorObject(err) {
+ if (typeof err === 'string') {
+ return {
+ code: 4001,
+ message: err
+ };
+ } else {
+ if (err.stack) {
+ logger.warn(err.stack);
+ }
+ return {
+ code: err.code,
+ message: err.message
+ };
+ }
+}
+
Agent.prototype._reply = function(request, err, message) {
+ var agent = this;
+ var backend = agent.backend;
if (err) {
- if (typeof err === 'string') {
- request.error = {
- code: 4001,
- message: err
- };
- } else {
- if (err.stack) {
- console.warn(err.stack);
- }
- request.error = {
- code: err.code,
- message: err.message
- };
- }
- this.send(request);
+ request.error = getReplyErrorObject(err);
+ agent.send(request);
return;
}
if (!message) message = {};
@@ -227,7 +236,15 @@ Agent.prototype._reply = function(request, err, message) {
if (request.b && !message.data) message.b = request.b;
}
- this.send(message);
+ var middlewareContext = {request: request, reply: message};
+ backend.trigger(backend.MIDDLEWARE_ACTIONS.reply, agent, middlewareContext, function(err) {
+ if (err) {
+ request.error = getReplyErrorObject(err);
+ agent.send(request);
+ } else {
+ agent.send(middlewareContext.reply);
+ }
+ });
};
// Start processing events from the stream
@@ -255,11 +272,9 @@ Agent.prototype._open = function() {
});
});
- this.stream.on('end', function() {
- agent.backend.agentsCount--;
- if (!agent.stream.isServer) agent.backend.remoteAgentsCount--;
- agent._cleanup();
- });
+ var cleanup = agent._cleanup.bind(agent);
+ this.stream.on('end', cleanup);
+ this.stream.on('close', cleanup);
};
// Check a request to see if its valid. Returns an error if there's a problem.
@@ -279,13 +294,8 @@ Agent.prototype._checkRequest = function(request) {
// Bulk request
if (request.c != null && typeof request.c !== 'string') return 'Invalid collection';
if (typeof request.b !== 'object') return 'Invalid bulk subscribe data';
- } else if (request.a === 'p') {
- // Presence
- if (typeof request.c !== 'string') return 'Invalid collection';
- if (typeof request.d !== 'string') return 'Invalid id';
- if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version';
- if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq';
- if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') return 'Invalid "request reply" value';
+ } else {
+ return this._agentPresence.checkRequest(request);
}
};
@@ -318,10 +328,14 @@ Agent.prototype._handleMessage = function(request, callback) {
var op = this._createOp(request);
if (!op) return callback({code: 4000, message: 'Invalid op message'});
return this._submit(request.c, request.d, op, callback);
- case 'p':
- var presence = this._createPresence(request.c, request.d, request.p, request.v, request.r, request.seq);
- return this._presence(presence, 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:
+ if (this._agentPresence.isPresenceMessage(request)) {
+ return this._agentPresence.handlePresenceMessage(request, callback);
+ }
callback({code: 4000, message: 'Invalid or unknown message'});
}
} catch (err) {
@@ -330,7 +344,9 @@ Agent.prototype._handleMessage = function(request, callback) {
};
function getQueryOptions(request) {
var results = request.r;
- var ids, fetch, fetchOps;
+ var ids;
+ var fetch;
+ var fetchOps;
if (results) {
ids = [];
for (var i = 0; i < results.length; i++) {
@@ -359,7 +375,6 @@ function getQueryOptions(request) {
Agent.prototype._queryFetch = function(queryId, collection, query, options, callback) {
// Fetch the results of a query once
- var agent = this;
this.backend.queryFetch(this, collection, query, options, function(err, results, extra) {
if (err) return callback(err);
var message = {
@@ -604,33 +619,10 @@ Agent.prototype._createOp = function(request) {
}
};
-Agent.prototype._presence = function(presence, callback) {
- if (presence.seq <= this.maxPresenceSeq) {
- return process.nextTick(function() {
- callback(new ShareDBError(4026, 'Presence data superseded'));
- });
- }
- this.maxPresenceSeq = presence.seq;
- if (!this.subscribedDocs[presence.c] || !this.subscribedDocs[presence.c][presence.d]) {
- return process.nextTick(function() {
- callback(new ShareDBError(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d));
- });
- }
- this.backend.sendPresence(presence, function(err) {
- if (err) return callback(err);
- callback(null, { seq: presence.seq });
- });
+Agent.prototype._fetchSnapshot = function(collection, id, version, callback) {
+ this.backend.fetchSnapshot(this, collection, id, version, callback);
};
-Agent.prototype._createPresence = function(collection, id, data, version, requestReply, seq) {
- return {
- a: 'p',
- src: this.clientId,
- seq: seq != null ? seq : this.maxPresenceSeq,
- c: collection,
- d: id,
- p: data,
- v: version,
- r: requestReply
- };
+Agent.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) {
+ this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback);
};
diff --git a/lib/backend.js b/lib/backend.js
index a8553683a..54e2ca6a1 100644
--- a/lib/backend.js
+++ b/lib/backend.js
@@ -1,15 +1,28 @@
var async = require('async');
var Agent = require('./agent');
var Connection = require('./client/connection');
+var dummyPresence = require('./presence/dummy');
var emitter = require('./emitter');
var MemoryDB = require('./db/memory');
+var NoOpMilestoneDB = require('./milestone-db/no-op');
var MemoryPubSub = require('./pubsub/memory');
var ot = require('./ot');
var projections = require('./projections');
var QueryEmitter = require('./query-emitter');
+var Snapshot = require('./snapshot');
var StreamSocket = require('./stream-socket');
var SubmitRequest = require('./submit-request');
+var warnDeprecatedDoc = true;
+var warnDeprecatedAfterSubmit = true;
+
+var DOC_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "doc" middleware action. Use "readSnapshots" instead. ' +
+ 'Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.';
+
+var AFFTER_SUBMIT_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "after submit" middleware action. ' +
+ 'Use "afterSubmit" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to ' +
+ 'disable the "after submit" action and this warning.';
+
function Backend(options) {
if (!(this instanceof Backend)) return new Backend(options);
emitter.EventEmitter.call(this);
@@ -19,6 +32,7 @@ function Backend(options) {
this.pubsub = options.pubsub || new MemoryPubSub();
// This contains any extra databases that can be queried
this.extraDbs = options.extraDbs || {};
+ this.milestoneDb = options.milestoneDb || new NoOpMilestoneDB();
// Map from projected collection -> {type, fields}
this.projections = {};
@@ -41,6 +55,10 @@ function Backend(options) {
if (!options.disableSpaceDelimitedActions) {
this._shimAfterSubmit();
}
+
+ this.presence = options.presence || dummyPresence;
+
+ this._backendPresence = new this.presence.BackendPresence(this);
}
module.exports = Backend;
emitter.mixin(Backend);
@@ -62,15 +80,35 @@ Backend.prototype.MIDDLEWARE_ACTIONS = {
op: 'op',
// A query is about to be sent to the database
query: 'query',
- // Received a message from a client
- receive: 'receive',
// Snapshot(s) were received from the database and are about to be returned to a client
readSnapshots: 'readSnapshots',
+ // Received a message from a client
+ receive: 'receive',
+ // About to send a non-error reply to a client message.
+ // WARNING: This gets passed a direct reference to the reply object, so
+ // be cautious with it. While modifications to the reply message are possible
+ // by design, changing existing reply properties can cause weird bugs, since
+ // the rest of ShareDB would be unaware of those changes.
+ reply: 'reply',
// An operation is about to be submitted to the database
submit: 'submit'
};
+Backend.prototype.SNAPSHOT_TYPES = {
+ // The current snapshot is being fetched (eg through backend.fetch)
+ current: 'current',
+ // A specific snapshot is being fetched by version (eg through backend.fetchSnapshot)
+ byVersion: 'byVersion',
+ // A specific snapshot is being fetch by timestamp (eg through backend.fetchSnapshotByTimestamp)
+ byTimestamp: 'byTimestamp'
+};
+
Backend.prototype._shimDocAction = function() {
+ if (warnDeprecatedDoc) {
+ warnDeprecatedDoc = false;
+ console.warn(DOC_ACTION_DEPRECATION_WARNING);
+ }
+
var backend = this;
this.use(this.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
async.each(request.snapshots, function(snapshot, eachCb) {
@@ -83,6 +121,11 @@ Backend.prototype._shimDocAction = function() {
// Shim for backwards compatibility with deprecated middleware action name.
// The action 'after submit' is now 'afterSubmit'.
Backend.prototype._shimAfterSubmit = function() {
+ if (warnDeprecatedAfterSubmit) {
+ warnDeprecatedAfterSubmit = false;
+ console.warn(AFFTER_SUBMIT_ACTION_DEPRECATION_WARNING);
+ }
+
var backend = this;
this.use(backend.MIDDLEWARE_ACTIONS.afterSubmit, function(request, callback) {
backend.trigger(backend.MIDDLEWARE_ACTIONS['after submit'], request.agent, request, callback);
@@ -90,7 +133,7 @@ Backend.prototype._shimAfterSubmit = function() {
};
Backend.prototype.close = function(callback) {
- var wait = 3;
+ var wait = 4;
var backend = this;
function finish(err) {
if (err) {
@@ -102,6 +145,7 @@ Backend.prototype.close = function(callback) {
}
this.pubsub.close(finish);
this.db.close(finish);
+ this.milestoneDb.close(finish);
for (var name in this.extraDbs) {
wait++;
this.extraDbs[name].close(finish);
@@ -122,6 +166,13 @@ Backend.prototype.connect = function(connection, req) {
// not used internal to ShareDB, but it is handy for server-side only user
// code that may cache state on the agent and read it in middleware
connection.agent = agent;
+
+ // Expose the DocPresence passed in through the constructor
+ // to the Doc class, which has access to the connection.
+ connection.DocPresence = this.presence.DocPresence;
+
+ connection._connectionPresence = new this.presence.ConnectionPresence(connection);
+
return connection;
};
@@ -247,7 +298,7 @@ Backend.prototype._sanitizeOpsBulk = function(agent, projection, collection, ops
}, callback);
};
-Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, callback) {
+Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, snapshotType, callback) {
if (projection) {
try {
projections.projectSnapshots(projection.fields, snapshots);
@@ -255,7 +306,13 @@ Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, s
return callback(err);
}
}
- var request = {collection: collection, snapshots: snapshots};
+
+ var request = {
+ collection: collection,
+ snapshots: snapshots,
+ snapshotType: snapshotType
+ };
+
this.trigger(this.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, callback);
};
@@ -274,7 +331,11 @@ Backend.prototype._getSnapshotsFromMap = function(ids, snapshotMap) {
// Non inclusive - gets ops from [from, to). Ie, all relevant ops. If to is
// not defined (null or undefined) then it returns all ops.
-Backend.prototype.getOps = function(agent, index, id, from, to, callback) {
+Backend.prototype.getOps = function(agent, index, id, from, to, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
var start = Date.now();
var projection = this.projections[index];
var collection = (projection) ? projection.target : index;
@@ -287,7 +348,8 @@ Backend.prototype.getOps = function(agent, index, id, from, to, callback) {
from: from,
to: to
};
- backend.db.getOps(collection, id, from, to, null, function(err, ops) {
+ var opsOptions = options && options.opsOptions;
+ backend.db.getOps(collection, id, from, to, opsOptions, function(err, ops) {
if (err) return callback(err);
backend._sanitizeOps(agent, projection, collection, id, ops, function(err) {
if (err) return callback(err);
@@ -297,7 +359,11 @@ Backend.prototype.getOps = function(agent, index, id, from, to, callback) {
});
};
-Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback) {
+Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
var start = Date.now();
var projection = this.projections[index];
var collection = (projection) ? projection.target : index;
@@ -309,7 +375,8 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback)
fromMap: fromMap,
toMap: toMap
};
- backend.db.getOpsBulk(collection, fromMap, toMap, null, function(err, opsMap) {
+ var opsOptions = options && options.opsOptions;
+ backend.db.getOpsBulk(collection, fromMap, toMap, opsOptions, function(err, opsMap) {
if (err) return callback(err);
backend._sanitizeOpsBulk(agent, projection, collection, opsMap, function(err) {
if (err) return callback(err);
@@ -319,7 +386,11 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback)
});
};
-Backend.prototype.fetch = function(agent, index, id, callback) {
+Backend.prototype.fetch = function(agent, index, id, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
var start = Date.now();
var projection = this.projections[index];
var collection = (projection) ? projection.target : index;
@@ -331,19 +402,30 @@ Backend.prototype.fetch = function(agent, index, id, callback) {
collection: collection,
id: id
};
- backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) {
+ var snapshotOptions = options && options.snapshotOptions;
+ backend.db.getSnapshot(collection, id, fields, snapshotOptions, function(err, snapshot) {
if (err) return callback(err);
var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
var snapshots = [snapshot];
- backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, function(err) {
- if (err) return callback(err);
- backend.emit('timing', 'fetch', Date.now() - start, request);
- callback(null, snapshot);
- });
+ backend._sanitizeSnapshots(
+ agent,
+ snapshotProjection,
+ collection,
+ snapshots,
+ backend.SNAPSHOT_TYPES.current,
+ function(err) {
+ if (err) return callback(err);
+ backend.emit('timing', 'fetch', Date.now() - start, request);
+ callback(null, snapshot);
+ });
});
};
-Backend.prototype.fetchBulk = function(agent, index, ids, callback) {
+Backend.prototype.fetchBulk = function(agent, index, ids, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
var start = Date.now();
var projection = this.projections[index];
var collection = (projection) ? projection.target : index;
@@ -355,20 +437,38 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) {
collection: collection,
ids: ids
};
- backend.db.getSnapshotBulk(collection, ids, fields, null, function(err, snapshotMap) {
+ var snapshotOptions = options && options.snapshotOptions;
+ backend.db.getSnapshotBulk(collection, ids, fields, snapshotOptions, function(err, snapshotMap) {
if (err) return callback(err);
var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap);
- backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, function(err) {
- if (err) return callback(err);
- backend.emit('timing', 'fetchBulk', Date.now() - start, request);
- callback(null, snapshotMap);
- });
+ backend._sanitizeSnapshots(
+ agent,
+ snapshotProjection,
+ collection,
+ snapshots,
+ backend.SNAPSHOT_TYPES.current,
+ function(err) {
+ if (err) return callback(err);
+ backend.emit('timing', 'fetchBulk', Date.now() - start, request);
+ callback(null, snapshotMap);
+ });
});
};
// Subscribe to the document from the specified version or null version
-Backend.prototype.subscribe = function(agent, index, id, version, callback) {
+Backend.prototype.subscribe = function(agent, index, id, version, options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
+ if (options) {
+ // We haven't yet implemented the ability to pass options to subscribe. This is because we need to
+ // 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'});
+ }
var start = Date.now();
var projection = this.projections[index];
var collection = (projection) ? projection.target : index;
@@ -529,7 +629,7 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac
query: query,
options: options,
db: null,
- snapshotProjection: null,
+ snapshotProjection: null
};
var backend = this;
backend.trigger(backend.MIDDLEWARE_ACTIONS.query, agent, request, function(err) {
@@ -547,9 +647,15 @@ Backend.prototype._query = function(agent, request, callback) {
var backend = this;
request.db.query(request.collection, request.query, request.fields, request.options, function(err, snapshots, extra) {
if (err) return callback(err);
- backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, function(err) {
- callback(err, snapshots, extra);
- });
+ backend._sanitizeSnapshots(
+ agent,
+ request.snapshotProjection,
+ request.collection,
+ snapshots,
+ backend.SNAPSHOT_TYPES.current,
+ function(err) {
+ callback(err, snapshots, extra);
+ });
});
};
@@ -568,9 +674,121 @@ Backend.prototype.getChannels = function(collection, id) {
];
};
+Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) {
+ var start = Date.now();
+ var backend = this;
+ var projection = this.projections[index];
+ var collection = projection ? projection.target : index;
+ var request = {
+ agent: agent,
+ index: index,
+ collection: collection,
+ id: id,
+ version: version
+ };
+
+ this._fetchSnapshot(collection, id, version, function(error, snapshot) {
+ if (error) return callback(error);
+ var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
+ var snapshots = [snapshot];
+ var snapshotType = backend.SNAPSHOT_TYPES.byVersion;
+ backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) {
+ if (error) return callback(error);
+ backend.emit('timing', 'fetchSnapshot', Date.now() - start, request);
+ callback(null, snapshot);
+ });
+ });
+};
+
+Backend.prototype._fetchSnapshot = function(collection, id, version, callback) {
+ var db = this.db;
+ var backend = this;
+
+ this.milestoneDb.getMilestoneSnapshot(collection, id, version, function(error, milestoneSnapshot) {
+ if (error) return callback(error);
+
+ // Bypass backend.getOps so that we don't call _sanitizeOps. We want to avoid this, because:
+ // - we want to avoid the 'op' middleware, because we later use the 'readSnapshots' middleware in _sanitizeSnapshots
+ // - we handle the projection in _sanitizeSnapshots
+ var from = milestoneSnapshot ? milestoneSnapshot.v : 0;
+ db.getOps(collection, id, from, version, null, function(error, ops) {
+ if (error) return callback(error);
+
+ backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, function(error, snapshot) {
+ if (error) return callback(error);
+
+ if (version > snapshot.v) {
+ return callback({code: 4024, message: 'Requested version exceeds latest snapshot version'});
+ }
+
+ callback(null, snapshot);
+ });
+ });
+ });
+};
+
+Backend.prototype.fetchSnapshotByTimestamp = function(agent, index, id, timestamp, callback) {
+ var start = Date.now();
+ var backend = this;
+ var projection = this.projections[index];
+ var collection = projection ? projection.target : index;
+ var request = {
+ agent: agent,
+ index: index,
+ collection: collection,
+ id: id,
+ timestamp: timestamp
+ };
+
+ this._fetchSnapshotByTimestamp(collection, id, timestamp, function(error, snapshot) {
+ if (error) return callback(error);
+ var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
+ var snapshots = [snapshot];
+ var snapshotType = backend.SNAPSHOT_TYPES.byTimestamp;
+ backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) {
+ if (error) return callback(error);
+ backend.emit('timing', 'fetchSnapshot', Date.now() - start, request);
+ callback(null, snapshot);
+ });
+ });
+};
+
+Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) {
+ var db = this.db;
+ var milestoneDb = this.milestoneDb;
+ var backend = this;
+
+ var milestoneSnapshot;
+ var from = 0;
+ var to = null;
+
+ milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
+ if (error) return callback(error);
+ milestoneSnapshot = snapshot;
+ if (snapshot) from = snapshot.v;
+
+ milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) {
+ if (error) return callback(error);
+ if (snapshot) to = snapshot.v;
+
+ var options = {metadata: true};
+ db.getOps(collection, id, from, to, options, function(error, ops) {
+ if (error) return callback(error);
+ filterOpsInPlaceBeforeTimestamp(ops, timestamp);
+ backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback);
+ });
+ });
+ });
+};
+
+Backend.prototype._buildSnapshotFromOps = function(id, startingSnapshot, ops, callback) {
+ var snapshot = startingSnapshot || new Snapshot(id, 0, null, undefined, null);
+ var error = ot.applyOps(snapshot, ops);
+ callback(error, snapshot);
+};
+
Backend.prototype.sendPresence = function(presence, callback) {
- var channels = [ this.getDocChannel(presence.c, presence.d) ];
- this.pubsub.publish(channels, presence, callback);
+ this._backendPresence.sendPresence(presence, callback);
};
function pluckIds(snapshots) {
@@ -580,3 +798,18 @@ function pluckIds(snapshots) {
}
return ids;
}
+
+function filterOpsInPlaceBeforeTimestamp(ops, timestamp) {
+ if (timestamp === null) {
+ return;
+ }
+
+ for (var i = 0; i < ops.length; i++) {
+ var op = ops[i];
+ var opTimestamp = op.m && op.m.ts;
+ if (opTimestamp > timestamp) {
+ ops.length = i;
+ return;
+ }
+ }
+}
diff --git a/lib/client/connection.js b/lib/client/connection.js
index b8d7f1ccc..cbc07e38f 100644
--- a/lib/client/connection.js
+++ b/lib/client/connection.js
@@ -1,9 +1,12 @@
var Doc = require('./doc');
var Query = require('./query');
+var SnapshotVersionRequest = require('./snapshot-request/snapshot-version-request');
+var SnapshotTimestampRequest = require('./snapshot-request/snapshot-timestamp-request');
var emitter = require('../emitter');
var ShareDBError = require('../error');
var types = require('../types');
var util = require('../util');
+var logger = require('../logger');
function connectionState(socket) {
if (socket.readyState === 0 || socket.readyState === 1) return 'connecting';
@@ -33,13 +36,17 @@ function Connection(socket) {
// (created documents MUST BE UNIQUE)
this.collections = {};
- // Each query is created with an id that the server uses when it sends us
- // info about the query (updates, etc)
+ // Each query and snapshot request is created with an id that the server uses when it sends us
+ // info about the request (updates, etc)
this.nextQueryId = 1;
+ this.nextSnapshotRequestId = 1;
// Map from query ID -> query object.
this.queries = {};
+ // Map from snapshot request ID -> snapshot request
+ this._snapshotRequests = {};
+
// A unique message number for the given id
this.seq = 1;
@@ -111,11 +118,11 @@ Connection.prototype.bindToSocket = function(socket) {
var data = (typeof event.data === 'string') ?
JSON.parse(event.data) : event.data;
} catch (err) {
- console.warn('Failed to parse message', event);
+ logger.warn('Failed to parse message', event);
return;
}
- if (connection.debug) console.log('RECV', JSON.stringify(data));
+ if (connection.debug) logger.info('RECV', JSON.stringify(data));
var request = {data: data};
connection.emit('receive', request);
@@ -151,10 +158,8 @@ Connection.prototype.bindToSocket = function(socket) {
if (reason === 'closed' || reason === 'Closed') {
connection._setState('closed', reason);
-
} else if (reason === 'stopped' || reason === 'Stopped by server') {
connection._setState('stopped', reason);
-
} else {
connection._setState('disconnected', reason);
}
@@ -226,6 +231,10 @@ Connection.prototype.handleMessage = function(message) {
case 'bu':
return this._handleBulkMessage(message, '_handleUnsubscribe');
+ case 'nf':
+ case 'nt':
+ return this._handleSnapshotFetch(err, message);
+
case 'f':
var doc = this.getExisting(message.c, message.d);
if (doc) doc._handleFetch(err, message.data);
@@ -243,13 +252,11 @@ Connection.prototype.handleMessage = function(message) {
if (doc) doc._handleOp(err, message);
return;
- case 'p':
- var doc = this.getExisting(message.c, message.d);
- if (doc) doc._handlePresence(err, message);
- return;
-
default:
- console.warn('Ignoring unrecognized message', message);
+ if (this._connectionPresence.isPresenceMessage(message)) {
+ return this._connectionPresence.handlePresenceMessage(err, message);
+ }
+ logger.warn('Ignoring unrecognized message', message);
}
};
@@ -271,7 +278,7 @@ Connection.prototype._handleBulkMessage = function(message, method) {
if (doc) doc[method](message.error);
}
} else {
- console.error('Invalid bulk message', message);
+ logger.error('Invalid bulk message', message);
}
};
@@ -289,8 +296,8 @@ Connection.prototype._setState = function(newState, reason) {
// 'connecting' from anywhere other than 'disconnected' and getting to
// 'connected' from anywhere other than 'connecting'.
if (
- (newState === 'connecting' && this.state !== 'disconnected' && this.state !== 'stopped' && this.state !== 'closed') ||
- (newState === 'connected' && this.state !== 'connecting')
+ (newState === 'connecting' && this.state !== 'disconnected' && this.state !== 'stopped' && this.state !== 'closed')
+ || (newState === 'connected' && this.state !== 'connecting')
) {
var err = new ShareDBError(5007, 'Cannot transition directly from ' + this.state + ' to ' + newState);
return this.emit('error', err);
@@ -315,6 +322,11 @@ Connection.prototype._setState = function(newState, reason) {
docs[id]._onConnectionStateChanged();
}
}
+ // Emit the event to all snapshots
+ for (var id in this._snapshotRequests) {
+ var snapshotRequest = this._snapshotRequests[id];
+ snapshotRequest._onConnectionStateChanged();
+ }
this.endBulk();
this.emit(newState, reason);
@@ -414,28 +426,14 @@ Connection.prototype.sendOp = function(doc, op) {
};
Connection.prototype.sendPresence = function(doc, data, requestReply) {
- // Ensure the doc is registered so that it receives the reply message
- this._addDoc(doc);
- var message = {
- a: 'p',
- c: doc.collection,
- d: doc.id,
- p: data,
- v: doc.version || 0,
- seq: this.seq++
- };
- if (requestReply) {
- message.r = true;
- }
- this.send(message);
+ this._connectionPresence.sendPresence(doc, data, requestReply);
};
-
/**
* Sends a message down the socket
*/
Connection.prototype.send = function(message) {
- if (this.debug) console.log('SEND', JSON.stringify(message));
+ if (this.debug) logger.info('SEND', JSON.stringify(message));
this.emit('send', message);
this.socket.send(JSON.stringify(message));
@@ -545,7 +543,8 @@ Connection.prototype.createSubscribeQuery = function(collection, q, options, cal
Connection.prototype.hasPending = function() {
return !!(
this._firstDoc(hasPending) ||
- this._firstQuery(hasPending)
+ this._firstQuery(hasPending) ||
+ this._firstSnapshotRequest()
);
};
function hasPending(object) {
@@ -574,6 +573,11 @@ Connection.prototype.whenNothingPending = function(callback) {
query.once('ready', this._nothingPendingRetry(callback));
return;
}
+ var snapshotRequest = this._firstSnapshotRequest();
+ if (snapshotRequest) {
+ snapshotRequest.once('ready', this._nothingPendingRetry(callback));
+ return;
+ }
// Call back when no pending operations
process.nextTick(callback);
};
@@ -606,3 +610,72 @@ Connection.prototype._firstQuery = function(fn) {
}
}
};
+
+Connection.prototype._firstSnapshotRequest = function() {
+ for (var id in this._snapshotRequests) {
+ return this._snapshotRequests[id];
+ }
+};
+
+/**
+ * Fetch a read-only snapshot at a given version
+ *
+ * @param collection - the collection name of the snapshot
+ * @param id - the ID of the snapshot
+ * @param version (optional) - the version number to fetch. If null, the latest version is fetched.
+ * @param callback - (error, snapshot) => void, where snapshot takes the following schema:
+ *
+ * {
+ * id: string; // ID of the snapshot
+ * v: number; // version number of the snapshot
+ * type: string; // the OT type of the snapshot, or null if it doesn't exist or is deleted
+ * data: any; // the snapshot
+ * }
+ *
+ */
+Connection.prototype.fetchSnapshot = function(collection, id, version, callback) {
+ if (typeof version === 'function') {
+ callback = version;
+ version = null;
+ }
+
+ var requestId = this.nextSnapshotRequestId++;
+ var snapshotRequest = new SnapshotVersionRequest(this, requestId, collection, id, version, callback);
+ this._snapshotRequests[snapshotRequest.requestId] = snapshotRequest;
+ snapshotRequest.send();
+};
+
+/**
+ * Fetch a read-only snapshot at a given timestamp
+ *
+ * @param collection - the collection name of the snapshot
+ * @param id - the ID of the snapshot
+ * @param timestamp (optional) - the timestamp to fetch. If null, the latest version is fetched.
+ * @param callback - (error, snapshot) => void, where snapshot takes the following schema:
+ *
+ * {
+ * id: string; // ID of the snapshot
+ * v: number; // version number of the snapshot
+ * type: string; // the OT type of the snapshot, or null if it doesn't exist or is deleted
+ * data: any; // the snapshot
+ * }
+ *
+ */
+Connection.prototype.fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) {
+ if (typeof timestamp === 'function') {
+ callback = timestamp;
+ timestamp = null;
+ }
+
+ var requestId = this.nextSnapshotRequestId++;
+ var snapshotRequest = new SnapshotTimestampRequest(this, requestId, collection, id, timestamp, callback);
+ this._snapshotRequests[snapshotRequest.requestId] = snapshotRequest;
+ snapshotRequest.send();
+};
+
+Connection.prototype._handleSnapshotFetch = function(error, message) {
+ var snapshotRequest = this._snapshotRequests[message.id];
+ if (!snapshotRequest) return;
+ delete this._snapshotRequests[message.id];
+ snapshotRequest._handleResponse(error, message);
+};
diff --git a/lib/client/doc.js b/lib/client/doc.js
index 42d8d37dc..cb504c68a 100644
--- a/lib/client/doc.js
+++ b/lib/client/doc.js
@@ -1,6 +1,8 @@
var emitter = require('../emitter');
+var logger = require('../logger');
var ShareDBError = require('../error');
var types = require('../types');
+var callEach = require('../util').callEach;
/**
* A Doc is a client's view on a sharejs document.
@@ -66,37 +68,13 @@ function Doc(connection, collection, id) {
this.type = null;
this.data = undefined;
- // The current presence data
- // Map of src -> presence data
- // Local src === ''
- this.presence = Object.create(null);
- // The presence objects received from the server
- // Map of src -> presence
- this.receivedPresence = Object.create(null);
- // The minimum amount of time to wait before removing processed presence from this.receivedPresence.
- // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot.
- // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower
- // sequence number arrive after messages with higher sequence numbers.
- this.receivedPresenceTimeout = 60000;
- // If set to true, then the next time the local presence is sent,
- // all other clients will be asked to reply with their own presence data.
- this.requestReplyPresence = true;
- // A list of ops sent by the server. These are needed for transforming presence data,
- // if we get that presence data for an older version of the document.
- // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence
- // data is supposed to be synced in real-time.
- this.cachedOps = [];
- this.cachedOpsTimeout = 60000;
- // The sequence number of the inflight presence request.
- this.inflightPresenceSeq = 0;
+ this._docPresence = new connection.DocPresence(this);
// Array of callbacks or nulls as placeholders
this.inflightFetch = [];
this.inflightSubscribe = [];
this.inflightUnsubscribe = [];
- this.inflightPresence = null;
this.pendingFetch = [];
- this.pendingPresence = null;
// Whether we think we are subscribed on the server. Synchronously set to
// false on calls to unsubscribe and disconnect. Should never be true when
@@ -139,21 +117,17 @@ emitter.mixin(Doc);
Doc.prototype.destroy = function(callback) {
var doc = this;
doc.whenNothingPending(function() {
+ doc._docPresence.destroyPresence();
if (doc.wantSubscribe) {
doc.unsubscribe(function(err) {
if (err) {
- if (callback) callback(err);
- else this.emit('error', err);
- return;
+ if (callback) return callback(err);
+ return doc.emit('error', err);
}
- doc.receivedPresence = Object.create(null);
- doc.cachedOps.length = 0;
doc.connection._destroyDoc(doc);
if (callback) callback();
});
} else {
- doc.receivedPresence = Object.create(null);
- doc.cachedOps.length = 0;
doc.connection._destroyDoc(doc);
if (callback) callback();
}
@@ -175,12 +149,10 @@ Doc.prototype._setType = function(newType) {
if (newType) {
this.type = newType;
-
} else if (newType === null) {
this.type = 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);
return this.emit('error', err);
@@ -219,7 +191,10 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) {
return callback && this.once('no write pending', callback);
}
// Otherwise, we've encounted an error state
- var err = new ShareDBError(5009, 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id);
+ var err = new ShareDBError(
+ 5009,
+ 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id
+ );
if (callback) return callback(err);
return this.emit('error', err);
}
@@ -234,23 +209,28 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) {
if (this.version > snapshot.v) return callback && callback();
this.version = snapshot.v;
- this.cachedOps.length = 0;
+
+ this._docPresence.clearCachedOps();
+
var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type;
this._setType(type);
this.data = (this.type && this.type.deserialize) ?
this.type.deserialize(snapshot.data) :
snapshot.data;
this.emit('load');
- this._processAllReceivedPresence();
+ this._docPresence.processAllReceivedPresence();
callback && callback();
};
Doc.prototype.whenNothingPending = function(callback) {
- if (this.hasPending()) {
- this.once('nothing pending', callback);
- return;
- }
- callback();
+ var doc = this;
+ process.nextTick(function() {
+ if (doc.hasPending()) {
+ doc.once('nothing pending', callback);
+ return;
+ }
+ callback();
+ });
};
Doc.prototype.hasPending = function() {
@@ -261,8 +241,7 @@ Doc.prototype.hasPending = function() {
this.inflightSubscribe.length ||
this.inflightUnsubscribe.length ||
this.pendingFetch.length ||
- this.inflightPresence ||
- this.pendingPresence
+ this._docPresence.hasPendingPresence()
);
};
@@ -360,14 +339,6 @@ Doc.prototype._handleOp = function(err, message) {
return;
}
- var serverOp = {
- src: message.src,
- time: Date.now(),
- create: !!message.create,
- op: message.op,
- del: !!message.del
- };
-
if (this.inflightOp) {
var transformErr = transformX(this.inflightOp, message);
if (transformErr) return this._hardRollback(transformErr);
@@ -379,10 +350,21 @@ Doc.prototype._handleOp = function(err, message) {
}
this.version++;
- this._cacheOp(serverOp);
- this._otApply(message, false);
- this._processAllReceivedPresence();
- return;
+ this._docPresence.cacheOp(message);
+ try {
+ this._otApply(message, false);
+ this._docPresence.processAllReceivedPresence();
+ } catch (error) {
+ return this._hardRollback(error);
+ }
+};
+
+Doc.prototype._handlePresence = function(err, presence) {
+ this._docPresence.handlePresence(err, presence);
+};
+
+Doc.prototype.submitPresence = function(data, callback) {
+ this._docPresence.submitPresence(data, callback);
};
// Called whenever (you guessed it!) the connection state changes. This will
@@ -405,11 +387,9 @@ Doc.prototype._onConnectionStateChanged = function() {
if (this.inflightUnsubscribe.length) {
var callbacks = this.inflightUnsubscribe;
this.inflightUnsubscribe = [];
- this._pausePresence();
callEach(callbacks);
- } else {
- this._pausePresence();
}
+ this._docPresence.pausePresence();
}
};
@@ -465,13 +445,12 @@ Doc.prototype.unsubscribe = function(callback) {
// between sending the message and hearing back, but we cannot know exactly
// when. Thus, immediately mark us as not subscribed
this.subscribed = false;
+ this._docPresence.pausePresence();
if (this.connection.canSend) {
var isDuplicate = this.connection.sendUnsubscribe(this);
pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback);
- this._pausePresence();
return;
}
- this._pausePresence();
if (callback) process.nextTick(callback);
};
@@ -494,21 +473,17 @@ function pushActionCallback(inflight, isDuplicate, callback) {
//
// Only one operation can be in-flight at a time. If an operation is already on
// its way, or we're not currently connected, this method does nothing.
-//
-// If there are no pending ops, this method sends the pending presence data, if possible.
Doc.prototype.flush = function() {
- if (this.paused) return;
+ // Ignore if we can't send or we are already sending an op
+ if (!this.connection.canSend || this.inflightOp) return;
- if (this.connection.canSend && !this.inflightOp && this.pendingOps.length) {
+ // Send first pending op unless paused
+ if (!this.paused && this.pendingOps.length) {
this._sendOp();
}
- if (this.subscribed && !this.inflightPresence && this.pendingPresence && !this.hasWritePending()) {
- this.inflightPresence = this.pendingPresence;
- this.inflightPresenceSeq = this.connection.seq;
- this.pendingPresence = null;
- this.connection.sendPresence(this, this.presence[''], this.requestReplyPresence);
- this.requestReplyPresence = false;
+ if (this.subscribed && !this.hasWritePending()) {
+ this._docPresence.flushPresence();
}
};
@@ -579,8 +554,8 @@ function transformX(client, server) {
Doc.prototype._otApply = function(op, source) {
if (op.op) {
if (!this.type) {
- var err = new ShareDBError(4015, 'Cannot apply op to uncreated document. ' + this.collection + '.' + this.id);
- return this.emit('error', err);
+ // 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);
}
// Iteratively apply multi-component remote operations and rollback ops
@@ -614,7 +589,7 @@ Doc.prototype._otApply = function(op, source) {
// Apply the individual op component
this.emit('before op', componentOp.op, source);
this.data = this.type.apply(this.data, componentOp.op);
- this._transformAllPresence(componentOp);
+ this._docPresence.transformAllPresence(componentOp);
this.emit('op', componentOp.op, source);
}
// Pop whatever was submitted since we started applying this op
@@ -627,7 +602,7 @@ Doc.prototype._otApply = function(op, source) {
this.emit('before op', op.op, source);
// Apply the operation to the local data, mutating it in place
this.data = this.type.apply(this.data, op.op);
- this._transformAllPresence(op);
+ this._docPresence.transformAllPresence(op);
// Emit an 'op' event once the local data includes the changes from the
// op. For locally submitted ops, this will be synchronously with
// submission and before the server or other clients have received the op.
@@ -637,6 +612,8 @@ Doc.prototype._otApply = function(op, source) {
return;
}
+ this._docPresence.transformAllPresence(op);
+
if (op.create) {
this._setType(op.create.type);
this.data = (this.type.deserialize) ?
@@ -644,7 +621,6 @@ Doc.prototype._otApply = function(op, source) {
this.type.createDeserialized(op.create.data) :
this.type.deserialize(this.type.create(op.create.data)) :
this.type.create(op.create.data);
- this._transformAllPresence(op);
this.emit('create', source);
return;
}
@@ -652,7 +628,6 @@ Doc.prototype._otApply = function(op, source) {
if (op.del) {
var oldData = this.data;
this._setType(null);
- this._transformAllPresence(op);
this.emit('del', oldData, source);
return;
}
@@ -718,7 +693,10 @@ Doc.prototype._submit = function(op, source, callback) {
// The op contains either op, create, delete, or none of the above (a no-op).
if (op.op) {
if (!this.type) {
- var err = new ShareDBError(4015, 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id);
+ var err = new ShareDBError(
+ 4015,
+ 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id
+ );
if (callback) return callback(err);
return this.emit('error', err);
}
@@ -726,8 +704,12 @@ Doc.prototype._submit = function(op, source, callback) {
if (this.type.normalize) op.op = this.type.normalize(op.op);
}
- this._pushOp(op, callback);
- this._otApply(op, source);
+ try {
+ this._pushOp(op, callback);
+ this._otApply(op, source);
+ } catch (error) {
+ return this._hardRollback(error);
+ }
// The call to flush is delayed so if submit() is called multiple times
// synchronously, all the ops are combined before being sent to the server.
@@ -900,12 +882,11 @@ Doc.prototype.resume = function() {
Doc.prototype._opAcknowledged = function(message) {
if (this.inflightOp.create) {
this.version = message.v;
- this.cachedOps.length = 0;
-
+ this._docPresence.clearCachedOps();
} else if (message.v !== this.version) {
// We should already be at the same version, because the server should
// have sent all the ops that have happened before acknowledging our op
- console.warn('Invalid version from server. Expected: ' + this.version + ' Received: ' + message.v, message);
+ logger.warn('Invalid version from server. Expected: ' + this.version + ' Received: ' + message.v, message);
// Fetching should get us back to a working document state
return this.fetch();
@@ -913,16 +894,10 @@ Doc.prototype._opAcknowledged = function(message) {
// The op was committed successfully. Increment the version number
this.version++;
- this._cacheOp({
- src: this.inflightOp.src,
- time: Date.now(),
- create: !!this.inflightOp.create,
- op: this.inflightOp.op,
- del: !!this.inflightOp.del
- });
+ this._docPresence.cacheOp(this.inflightOp);
this._clearInflightOp();
- this._processAllReceivedPresence();
+ this._docPresence.processAllReceivedPresence();
};
Doc.prototype._rollback = function(err) {
@@ -947,7 +922,11 @@ Doc.prototype._rollback = function(err) {
// I'm still not 100% sure about this functionality, because its really a
// local op. Basically, the problem is that if the client's op is rejected
// by the server, the editor window should update to reflect the undo.
- this._otApply(op, false);
+ try {
+ this._otApply(op, false);
+ } catch (error) {
+ return this._hardRollback(error);
+ }
this._clearInflightOp(err);
return;
@@ -957,346 +936,59 @@ Doc.prototype._rollback = function(err) {
};
Doc.prototype._hardRollback = function(err) {
- var callbacks = [];
- if (this.inflightPresence) {
- callbacks.push.apply(callbacks, this.inflightPresence);
- this.inflightPresence = null;
- this.inflightPresenceSeq = 0;
- }
- if (this.pendingPresence) {
- callbacks.push.apply(callbacks, this.pendingPresence);
- this.pendingPresence = null;
- }
- if (this.inflightOp) {
- callbacks.push.apply(callbacks, this.inflightOp.callbacks);
- }
- for (var i = 0; i < this.pendingOps.length; i++) {
- callbacks.push.apply(callbacks, this.pendingOps[i].callbacks);
- }
-
+ // Store pending ops so that we can notify their callbacks of the error.
+ // We combine the inflight op and the pending ops, because it's possible
+ // to hit a condition where we have no inflight op, but we do have pending
+ // ops. This can happen when an invalid op is submitted, which causes us
+ // to hard rollback before the pending op was flushed.
+ var pendingOps = [];
+ if (this.inflightOp) pendingOps.push(this.inflightOp);
+ pendingOps = pendingOps.concat(this.pendingOps);
+
+ // Apply a similar technique for presence.
+ var pendingPresence = this._docPresence.getPendingPresence();
+ this._docPresence.hardRollbackPresence();
+
+ // Cancel all pending ops and reset if we can't invert
this._setType(null);
this.version = null;
this.inflightOp = null;
this.pendingOps = [];
- this.cachedOps.length = 0;
- this.receivedPresence = Object.create(null);
- this.requestReplyPresence = true;
-
- var srcList = Object.keys(this.presence);
- var changedSrcList = [];
- for (var i = 0; i < srcList.length; i++) {
- var src = srcList[i];
- if (this._setPresence(src, null)) {
- changedSrcList.push(src);
- }
- }
- this._emitPresence(changedSrcList, false);
- // Fetch the latest from the server to get us back into a working state
+ // Fetch the latest version from the server to get us back into a working state
var doc = this;
this.fetch(function() {
- var called = callEach(callbacks, err);
- if (err && !called) return doc.emit('error', err);
- });
-};
-
-Doc.prototype._clearInflightOp = function(err) {
- var callbacks = this.inflightOp && this.inflightOp.callbacks;
- this.inflightOp = null;
- var called = callbacks && callEach(callbacks, err);
-
- this.flush();
- this._emitNothingPending();
-
- if (err && !called) return this.emit('error', err);
-};
-
-function callEach(callbacks, err) {
- var called = false;
- for (var i = 0; i < callbacks.length; i++) {
- var callback = callbacks[i];
- if (callback) {
- callback(err);
- called = true;
- }
- }
- return called;
-}
-
-// *** Presence
-
-Doc.prototype.submitPresence = function (data, callback) {
- if (data != null) {
- if (!this.type) {
- var doc = this;
- return process.nextTick(function() {
- var err = new ShareDBError(4015, 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id);
- if (callback) return callback(err);
- doc.emit('error', err);
- });
+ // We want to check that no errors are swallowed, so we check that:
+ // - there are callbacks to call, and
+ // - that every single pending op called a callback
+ var allOpsHadCallbacks = !!pendingOps.length;
+ for (var i = 0; i < pendingOps.length; i++) {
+ allOpsHadCallbacks = callEach(pendingOps[i].callbacks, err) && allOpsHadCallbacks;
}
- if (!this.type.createPresence || !this.type.transformPresence) {
- var doc = this;
- return process.nextTick(function() {
- var err = new ShareDBError(4024, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id);
- if (callback) return callback(err);
- doc.emit('error', err);
- });
+ // Apply the same technique for presence.
+ var allPresenceHadCallbacks = !!pendingPresence.length;
+ for (var i = 0; i < pendingPresence.length; i++) {
+ allPresenceHadCallbacks = callEach(pendingPresence[i], err) && allPresenceHadCallbacks;
}
- data = this.type.createPresence(data);
- }
-
- if (this._setPresence('', data, true) || this.pendingPresence || this.inflightPresence) {
- if (!this.pendingPresence) {
- this.pendingPresence = [];
+ // If there are no ops or presence queued, or one of them didn't handle the error,
+ // then we emit the error.
+ if (err && !allOpsHadCallbacks && !allPresenceHadCallbacks) {
+ doc.emit('error', err);
}
- if (callback) {
- this.pendingPresence.push(callback);
- }
-
- } else if (callback) {
- process.nextTick(callback);
- }
-
- var doc = this;
- process.nextTick(function() {
- doc.flush();
});
};
-Doc.prototype._handlePresence = function(err, presence) {
- if (!this.subscribed) return;
-
- var src = presence.src;
- if (!src) {
- // Handle the ACK for the presence data we submitted.
- // this.inflightPresenceSeq would not equal presence.seq after a hard rollback,
- // when all callbacks are flushed with an error.
- if (this.inflightPresenceSeq === presence.seq) {
- var callbacks = this.inflightPresence;
- this.inflightPresence = null;
- this.inflightPresenceSeq = 0;
- var called = callbacks && callEach(callbacks, err);
- if (err && !called) this.emit('error', err);
- this.flush();
- this._emitNothingPending();
- }
- return;
- }
-
- // This shouldn't happen but check just in case.
- if (err) return this.emit('error', err);
-
- if (presence.r && !this.pendingPresence) {
- // Another client requested us to share our current presence data
- this.pendingPresence = [];
- this.flush();
- }
-
- // Ignore older messages which arrived out of order
- if (
- this.receivedPresence[src] && (
- this.receivedPresence[src].seq > presence.seq ||
- (this.receivedPresence[src].seq === presence.seq && presence.v != null)
- )
- ) return;
-
- this.receivedPresence[src] = presence;
-
- if (presence.v == null) {
- // null version should happen only when the server automatically sends
- // null presence for an unsubscribed client
- presence.processedAt = Date.now();
- return this._setPresence(src, null, true);
- }
-
- // Get missing ops first, if necessary
- if (this.version == null || this.version < presence.v) return this.fetch();
-
- this._processReceivedPresence(src, true);
-};
-
-// If emit is true and presence has changed, emits a presence event.
-// Returns true, if presence has changed for src. Otherwise false.
-Doc.prototype._processReceivedPresence = function(src, emit) {
- if (!src) return false;
- var presence = this.receivedPresence[src];
- if (!presence) return false;
-
- if (presence.processedAt != null) {
- if (Date.now() >= presence.processedAt + this.receivedPresenceTimeout) {
- // Remove old received and processed presence
- delete this.receivedPresence[src];
- }
- return false;
- }
-
- if (this.version == null || this.version < presence.v) return false; // keep waiting for the missing snapshot or ops
-
- if (presence.p == null) {
- // Remove presence data as requested
- presence.processedAt = Date.now();
- return this._setPresence(src, null, emit);
- }
-
- if (!this.type || !this.type.createPresence || !this.type.transformPresence) {
- // Remove presence data because the document is not created or its type does not support presence
- presence.processedAt = Date.now();
- return this._setPresence(src, null, emit);
- }
-
- if (this.inflightOp && this.inflightOp.op == null) {
- // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del"
- presence.processedAt = Date.now();
- return this._setPresence(src, null, emit);
- }
-
- for (var i = 0; i < this.pendingOps.length; i++) {
- if (this.pendingOps[i].op == null) {
- // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del"
- presence.processedAt = Date.now();
- return this._setPresence(src, null, emit);
- }
- }
-
- var startIndex = this.cachedOps.length - (this.version - presence.v);
- if (startIndex < 0) {
- // Remove presence data because we can't transform receivedPresence
- presence.processedAt = Date.now();
- return this._setPresence(src, null, emit);
- }
-
- for (var i = startIndex; i < this.cachedOps.length; i++) {
- if (this.cachedOps[i].op == null) {
- // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del"
- presence.processedAt = Date.now();
- return this._setPresence(src, null, emit);
- }
- }
-
- // Make sure the format of the data is correct
- var data = this.type.createPresence(presence.p);
-
- // Transform against past ops
- for (var i = startIndex; i < this.cachedOps.length; i++) {
- var op = this.cachedOps[i];
- data = this.type.transformPresence(data, op.op, presence.src === op.src);
- }
-
- // Transform against pending ops
- if (this.inflightOp) {
- data = this.type.transformPresence(data, this.inflightOp.op, false);
- }
-
- for (var i = 0; i < this.pendingOps.length; i++) {
- data = this.type.transformPresence(data, this.pendingOps[i].op, false);
- }
-
- // Set presence data
- presence.processedAt = Date.now();
- return this._setPresence(src, data, emit);
-};
-
-Doc.prototype._processAllReceivedPresence = function() {
- var srcList = Object.keys(this.receivedPresence);
- var changedSrcList = [];
- for (var i = 0; i < srcList.length; i++) {
- var src = srcList[i];
- if (this._processReceivedPresence(src)) {
- changedSrcList.push(src);
- }
- }
- this._emitPresence(changedSrcList, true);
-};
-
-Doc.prototype._transformPresence = function(src, op) {
- var presenceData = this.presence[src];
- if (op.op != null) {
- var isOwnOperation = src === (op.src || '');
- presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation);
- } else {
- presenceData = null;
- }
- return this._setPresence(src, presenceData);
-};
-
-Doc.prototype._transformAllPresence = function(op) {
- var srcList = Object.keys(this.presence);
- var changedSrcList = [];
- for (var i = 0; i < srcList.length; i++) {
- var src = srcList[i];
- if (this._transformPresence(src, op)) {
- changedSrcList.push(src);
- }
- }
- this._emitPresence(changedSrcList, false);
-};
-
-Doc.prototype._pausePresence = function() {
- if (this.inflightPresence) {
- this.pendingPresence =
- this.pendingPresence ?
- this.inflightPresence.concat(this.pendingPresence) :
- this.inflightPresence;
- this.inflightPresence = null;
- this.inflightPresenceSeq = 0;
- } else if (!this.pendingPresence && this.presence[''] != null) {
- this.pendingPresence = [];
- }
- this.receivedPresence = Object.create(null);
- this.requestReplyPresence = true;
- var srcList = Object.keys(this.presence);
- var changedSrcList = [];
- for (var i = 0; i < srcList.length; i++) {
- var src = srcList[i];
- if (src && this._setPresence(src, null)) {
- changedSrcList.push(src);
- }
- }
- this._emitPresence(changedSrcList, false);
-};
+Doc.prototype._clearInflightOp = function(err) {
+ var inflightOp = this.inflightOp;
-// If emit is true and presence has changed, emits a presence event.
-// Returns true, if presence has changed. Otherwise false.
-Doc.prototype._setPresence = function(src, data, emit) {
- if (data == null) {
- if (this.presence[src] == null) return false;
- delete this.presence[src];
- } else {
- var isPresenceEqual =
- this.presence[src] === data ||
- (this.type.comparePresence && this.type.comparePresence(this.presence[src], data));
- if (isPresenceEqual) return false;
- this.presence[src] = data;
- }
- if (emit) this._emitPresence([ src ], true);
- return true;
-};
+ this.inflightOp = null;
-Doc.prototype._emitPresence = function(srcList, submitted) {
- if (srcList && srcList.length > 0) {
- var doc = this;
- process.nextTick(function() {
- doc.emit('presence', srcList, submitted);
- });
- }
-};
+ var called = callEach(inflightOp.callbacks, err);
-Doc.prototype._cacheOp = function(op) {
- // Remove the old ops.
- var oldOpTime = Date.now() - this.cachedOpsTimeout;
- var i;
- for (i = 0; i < this.cachedOps.length; i++) {
- if (this.cachedOps[i].time >= oldOpTime) {
- break;
- }
- }
- if (i > 0) {
- this.cachedOps.splice(0, i);
- }
+ this.flush();
+ this._emitNothingPending();
- // Cache the new op.
- this.cachedOps.push(op);
+ if (err && !called) return this.emit('error', err);
};
diff --git a/lib/client/index.js b/lib/client/index.js
index 12e17f5d7..78914acaa 100644
--- a/lib/client/index.js
+++ b/lib/client/index.js
@@ -3,3 +3,4 @@ exports.Doc = require('./doc');
exports.Error = require('../error');
exports.Query = require('./query');
exports.types = require('../types');
+exports.logger = require('../logger');
diff --git a/lib/client/query.js b/lib/client/query.js
index 4f8bae764..c406fb4f0 100644
--- a/lib/client/query.js
+++ b/lib/client/query.js
@@ -119,7 +119,6 @@ Query.prototype._handleResponse = function(err, data, extra) {
wait += data.length;
this.results = this._ingestSnapshots(data, finish);
this.extra = extra;
-
} else {
for (var id in data) {
wait++;
diff --git a/lib/client/snapshot-request/snapshot-request.js b/lib/client/snapshot-request/snapshot-request.js
new file mode 100644
index 000000000..95f68055b
--- /dev/null
+++ b/lib/client/snapshot-request/snapshot-request.js
@@ -0,0 +1,54 @@
+var Snapshot = require('../../snapshot');
+var emitter = require('../../emitter');
+
+module.exports = SnapshotRequest;
+
+function SnapshotRequest(connection, requestId, collection, id, callback) {
+ emitter.EventEmitter.call(this);
+
+ if (typeof callback !== 'function') {
+ throw new Error('Callback is required for SnapshotRequest');
+ }
+
+ this.requestId = requestId;
+ this.connection = connection;
+ this.id = id;
+ this.collection = collection;
+ this.callback = callback;
+
+ this.sent = false;
+}
+emitter.mixin(SnapshotRequest);
+
+SnapshotRequest.prototype.send = function() {
+ if (!this.connection.canSend) {
+ return;
+ }
+
+ this.connection.send(this._message());
+ this.sent = true;
+};
+
+SnapshotRequest.prototype._onConnectionStateChanged = function() {
+ if (this.connection.canSend) {
+ if (!this.sent) this.send();
+ } else {
+ // If the connection can't send, then we've had a disconnection, and even if we've already sent
+ // the request previously, we need to re-send it over this reconnected client, so reset the
+ // sent flag to false.
+ this.sent = false;
+ }
+};
+
+SnapshotRequest.prototype._handleResponse = function(error, message) {
+ this.emit('ready');
+
+ if (error) {
+ return this.callback(error);
+ }
+
+ var metadata = message.meta ? message.meta : null;
+ var snapshot = new Snapshot(this.id, message.v, message.type, message.data, metadata);
+
+ this.callback(null, snapshot);
+};
diff --git a/lib/client/snapshot-request/snapshot-timestamp-request.js b/lib/client/snapshot-request/snapshot-timestamp-request.js
new file mode 100644
index 000000000..15789137b
--- /dev/null
+++ b/lib/client/snapshot-request/snapshot-timestamp-request.js
@@ -0,0 +1,26 @@
+var SnapshotRequest = require('./snapshot-request');
+var util = require('../../util');
+
+module.exports = SnapshotTimestampRequest;
+
+function SnapshotTimestampRequest(connection, requestId, collection, id, timestamp, callback) {
+ SnapshotRequest.call(this, connection, requestId, collection, id, callback);
+
+ if (!util.isValidTimestamp(timestamp)) {
+ throw new Error('Snapshot timestamp must be a positive integer or null');
+ }
+
+ this.timestamp = timestamp;
+}
+
+SnapshotTimestampRequest.prototype = Object.create(SnapshotRequest.prototype);
+
+SnapshotTimestampRequest.prototype._message = function() {
+ return {
+ a: 'nt',
+ id: this.requestId,
+ c: this.collection,
+ d: this.id,
+ ts: this.timestamp
+ };
+};
diff --git a/lib/client/snapshot-request/snapshot-version-request.js b/lib/client/snapshot-request/snapshot-version-request.js
new file mode 100644
index 000000000..d352a676a
--- /dev/null
+++ b/lib/client/snapshot-request/snapshot-version-request.js
@@ -0,0 +1,26 @@
+var SnapshotRequest = require('./snapshot-request');
+var util = require('../../util');
+
+module.exports = SnapshotVersionRequest;
+
+function SnapshotVersionRequest(connection, requestId, collection, id, version, callback) {
+ SnapshotRequest.call(this, connection, requestId, collection, id, callback);
+
+ if (!util.isValidVersion(version)) {
+ throw new Error('Snapshot version must be a positive integer or null');
+ }
+
+ this.version = version;
+}
+
+SnapshotVersionRequest.prototype = Object.create(SnapshotRequest.prototype);
+
+SnapshotVersionRequest.prototype._message = function() {
+ return {
+ a: 'nf',
+ id: this.requestId,
+ c: this.collection,
+ d: this.id,
+ v: this.version
+ };
+};
diff --git a/lib/db/index.js b/lib/db/index.js
index 6a65f9b6d..c5adf8123 100644
--- a/lib/db/index.js
+++ b/lib/db/index.js
@@ -7,6 +7,7 @@ function DB(options) {
}
module.exports = DB;
+// When false, Backend will handle projections instead of DB
DB.prototype.projectsSnapshots = false;
DB.prototype.disableSubscribe = false;
diff --git a/lib/db/memory.js b/lib/db/memory.js
index 2c5b75fb6..029c1229b 100644
--- a/lib/db/memory.js
+++ b/lib/db/memory.js
@@ -1,4 +1,5 @@
var DB = require('./index');
+var Snapshot = require('../snapshot');
// In-memory ShareDB database
//
@@ -47,6 +48,7 @@ MemoryDB.prototype.commit = function(collection, id, op, snapshot, options, call
if (err) return callback(err);
err = db._writeSnapshotSync(collection, id, snapshot);
if (err) return callback(err);
+
var succeeded = true;
callback(null, succeeded);
});
@@ -120,7 +122,7 @@ MemoryDB.prototype.query = function(collection, query, fields, options, callback
// two properties:
// - snapshots: array of query result snapshots
// - extra: (optional) other types of results, such as counts
-MemoryDB.prototype._querySync = function(snapshots, query, options) {
+MemoryDB.prototype._querySync = function(snapshots) {
return {snapshots: snapshots};
};
@@ -151,24 +153,15 @@ MemoryDB.prototype._getSnapshotSync = function(collection, id, includeMetadata)
var snapshot;
if (doc) {
var data = clone(doc.data);
- var meta = (includeMetadata) ? clone(doc.m) : undefined;
- snapshot = new MemorySnapshot(id, doc.v, doc.type, data, meta);
+ var meta = (includeMetadata) ? clone(doc.m) : null;
+ snapshot = new Snapshot(id, doc.v, doc.type, data, meta);
} else {
var version = this._getVersionSync(collection, id);
- snapshot = new MemorySnapshot(id, version, null, undefined);
+ snapshot = new Snapshot(id, version, null, undefined, null);
}
return snapshot;
};
-// `id`, and `v` should be on every returned snapshot
-function MemorySnapshot(id, version, type, data, meta) {
- this.id = id;
- this.v = version;
- this.type = type;
- this.data = data;
- if (meta) this.m = meta;
-}
-
MemoryDB.prototype._getOpLogSync = function(collection, id) {
var collectionOps = this.ops[collection] || (this.ops[collection] = {});
return collectionOps[id] || (collectionOps[id] = []);
diff --git a/lib/index.js b/lib/index.js
index 6bc96ba98..df4b3f0a6 100644
--- a/lib/index.js
+++ b/lib/index.js
@@ -5,8 +5,11 @@ Backend.Agent = require('./agent');
Backend.Backend = Backend;
Backend.DB = require('./db');
Backend.Error = require('./error');
+Backend.logger = require('./logger');
Backend.MemoryDB = require('./db/memory');
+Backend.MemoryMilestoneDB = require('./milestone-db/memory');
Backend.MemoryPubSub = require('./pubsub/memory');
+Backend.MilestoneDB = require('./milestone-db');
Backend.ot = require('./ot');
Backend.projections = require('./projections');
Backend.PubSub = require('./pubsub');
diff --git a/lib/logger/index.js b/lib/logger/index.js
new file mode 100644
index 000000000..9a80c1f1c
--- /dev/null
+++ b/lib/logger/index.js
@@ -0,0 +1,3 @@
+var Logger = require('./logger');
+var logger = new Logger();
+module.exports = logger;
diff --git a/lib/logger/logger.js b/lib/logger/logger.js
new file mode 100644
index 000000000..3c70e5a53
--- /dev/null
+++ b/lib/logger/logger.js
@@ -0,0 +1,26 @@
+var SUPPORTED_METHODS = [
+ 'info',
+ 'warn',
+ 'error'
+];
+
+function Logger() {
+ var defaultMethods = {};
+ SUPPORTED_METHODS.forEach(function(method) {
+ // Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628
+ defaultMethods[method] = console[method].bind(console);
+ });
+ this.setMethods(defaultMethods);
+}
+module.exports = Logger;
+
+Logger.prototype.setMethods = function(overrides) {
+ overrides = overrides || {};
+ var logger = this;
+
+ SUPPORTED_METHODS.forEach(function(method) {
+ if (typeof overrides[method] === 'function') {
+ logger[method] = overrides[method];
+ }
+ });
+};
diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js
new file mode 100644
index 000000000..3726b2ca8
--- /dev/null
+++ b/lib/milestone-db/index.js
@@ -0,0 +1,64 @@
+var emitter = require('../emitter');
+var ShareDBError = require('../error');
+var util = require('../util');
+
+module.exports = MilestoneDB;
+function MilestoneDB(options) {
+ emitter.EventEmitter.call(this);
+
+ // The interval at which milestone snapshots should be saved
+ this.interval = options && options.interval;
+}
+emitter.mixin(MilestoneDB);
+
+MilestoneDB.prototype.close = function(callback) {
+ if (callback) process.nextTick(callback);
+};
+
+/**
+ * Fetch a milestone snapshot from the database
+ * @param {string} collection - name of the snapshot's collection
+ * @param {string} id - ID of the snapshot to fetch
+ * @param {number} version - the desired version of the milestone snapshot. The database will return
+ * the most recent milestone snapshot whose version is equal to or less than the provided value
+ * @param {Function} callback - a callback to invoke once the snapshot has been fetched. Should have
+ * the signature (error, snapshot) => void;
+ */
+MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) {
+ var error = new ShareDBError(5019, 'getMilestoneSnapshot MilestoneDB method unimplemented');
+ this._callBackOrEmitError(error, callback);
+};
+
+/**
+ * @param {string} collection - name of the snapshot's collection
+ * @param {Snapshot} snapshot - the milestone snapshot to save
+ * @param {Function} callback (optional) - a callback to invoke after the snapshot has been saved.
+ * Should have the signature (error) => void;
+ */
+MilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) {
+ var error = new ShareDBError(5020, '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');
+ this._callBackOrEmitError(error, callback);
+};
+
+MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) {
+ var error = new ShareDBError(5022, 'getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented');
+ this._callBackOrEmitError(error, callback);
+};
+
+MilestoneDB.prototype._isValidVersion = function(version) {
+ return util.isValidVersion(version);
+};
+
+MilestoneDB.prototype._isValidTimestamp = function(timestamp) {
+ return util.isValidTimestamp(timestamp);
+};
+
+MilestoneDB.prototype._callBackOrEmitError = function(error, callback) {
+ if (callback) return process.nextTick(callback, error);
+ this.emit('error', error);
+};
diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js
new file mode 100644
index 000000000..7b64dee36
--- /dev/null
+++ b/lib/milestone-db/memory.js
@@ -0,0 +1,131 @@
+var MilestoneDB = require('./index');
+var ShareDBError = require('../error');
+
+/**
+ * In-memory ShareDB milestone database
+ *
+ * Milestone snapshots exist to speed up Backend.fetchSnapshot by providing milestones
+ * on top of which fewer ops can be applied to reach a desired version of the document.
+ * This very concept relies on persistence, which means that an in-memory database like
+ * this is in no way appropriate for production use.
+ *
+ * The main purpose of this class is to provide a simple example of implementation,
+ * and for use in tests.
+ */
+module.exports = MemoryMilestoneDB;
+function MemoryMilestoneDB(options) {
+ MilestoneDB.call(this, options);
+
+ // Map from collection name -> doc id -> array of milestone snapshots
+ this._milestoneSnapshots = {};
+}
+
+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'));
+
+ var predicate = versionLessThanOrEqualTo(version);
+ this._findMilestoneSnapshot(collection, id, predicate, callback);
+};
+
+MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) {
+ callback = callback || function(error) {
+ if (error) return this.emit('error', error);
+ 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'));
+
+ var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id);
+ milestoneSnapshots.push(snapshot);
+ milestoneSnapshots.sort(function(a, b) {
+ return a.v - b.v;
+ });
+
+ process.nextTick(callback, null);
+};
+
+MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) {
+ if (!this._isValidTimestamp(timestamp)) {
+ return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp'));
+ }
+
+ var filter = timestampLessThanOrEqualTo(timestamp);
+ this._findMilestoneSnapshot(collection, id, filter, callback);
+};
+
+MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) {
+ if (!this._isValidTimestamp(timestamp)) {
+ return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp'));
+ }
+
+ var filter = timestampGreaterThanOrEqualTo(timestamp);
+ this._findMilestoneSnapshot(collection, id, filter, function(error, snapshot) {
+ if (error) return process.nextTick(callback, error);
+
+ var mtime = snapshot && snapshot.m && snapshot.m.mtime;
+ if (timestamp !== null && mtime < timestamp) {
+ snapshot = undefined;
+ }
+
+ process.nextTick(callback, null, snapshot);
+ });
+};
+
+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'));
+
+ var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id);
+
+ var milestoneSnapshot;
+ for (var i = 0; i < milestoneSnapshots.length; i++) {
+ var nextMilestoneSnapshot = milestoneSnapshots[i];
+ if (breakCondition(milestoneSnapshot, nextMilestoneSnapshot)) {
+ break;
+ } else {
+ milestoneSnapshot = nextMilestoneSnapshot;
+ }
+ }
+
+ process.nextTick(callback, null, milestoneSnapshot);
+};
+
+MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function(collection, id) {
+ var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {});
+ return collectionSnapshots[id] || (collectionSnapshots[id] = []);
+};
+
+function versionLessThanOrEqualTo(version) {
+ return function(currentSnapshot, nextSnapshot) {
+ if (version === null) {
+ return false;
+ }
+
+ return nextSnapshot.v > version;
+ };
+}
+
+function timestampGreaterThanOrEqualTo(timestamp) {
+ return function(currentSnapshot) {
+ if (timestamp === null) {
+ return false;
+ }
+
+ var mtime = currentSnapshot && currentSnapshot.m && currentSnapshot.m.mtime;
+ return mtime >= timestamp;
+ };
+}
+
+function timestampLessThanOrEqualTo(timestamp) {
+ return function(currentSnapshot, nextSnapshot) {
+ if (timestamp === null) {
+ return !!currentSnapshot;
+ }
+
+ var mtime = nextSnapshot && nextSnapshot.m && nextSnapshot.m.mtime;
+ return mtime > timestamp;
+ };
+}
diff --git a/lib/milestone-db/no-op.js b/lib/milestone-db/no-op.js
new file mode 100644
index 000000000..fc235d248
--- /dev/null
+++ b/lib/milestone-db/no-op.js
@@ -0,0 +1,34 @@
+var MilestoneDB = require('./index');
+
+/**
+ * A no-op implementation of the MilestoneDB class.
+ *
+ * This class exists as a simple, silent default drop-in for ShareDB, which allows the backend to call its methods with
+ * no effect.
+ */
+module.exports = NoOpMilestoneDB;
+function NoOpMilestoneDB(options) {
+ MilestoneDB.call(this, options);
+}
+
+NoOpMilestoneDB.prototype = Object.create(MilestoneDB.prototype);
+
+NoOpMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) {
+ var snapshot = undefined;
+ process.nextTick(callback, null, snapshot);
+};
+
+NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) {
+ if (callback) return process.nextTick(callback, null);
+ this.emit('save', collection, snapshot);
+};
+
+NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) {
+ var snapshot = undefined;
+ process.nextTick(callback, null, snapshot);
+};
+
+NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) {
+ var snapshot = undefined;
+ process.nextTick(callback, null, snapshot);
+};
diff --git a/lib/ot.js b/lib/ot.js
index 1dc89bcfc..f04da0145 100644
--- a/lib/ot.js
+++ b/lib/ot.js
@@ -23,10 +23,8 @@ exports.checkOp = function(op) {
if (type == null || typeof type !== 'object') {
return {code: 4008, message: 'Unknown type'};
}
-
} else if (op.del != null) {
if (op.del !== true) return {code: 4009, message: 'del value must be true'};
-
} else if (op.op == null) {
return {code: 4010, message: 'Missing op, create, or del'};
}
@@ -102,7 +100,7 @@ exports.apply = function(snapshot, op) {
function applyOpEdit(snapshot, edit) {
if (!snapshot.type) return {code: 4015, message: 'Document does not exist'};
- if (typeof edit !== 'object') return {code: 5004, message: 'Missing op'};
+ if (edit == null) return {code: 5004, message: 'Missing op'};
var type = types[snapshot.type];
if (!type) return {code: 4008, message: 'Unknown type'};
@@ -149,3 +147,38 @@ exports.transform = function(type, op, appliedOp) {
if (op.v != null) op.v++;
};
+
+/**
+ * Apply an array of ops to the provided snapshot.
+ *
+ * @param snapshot - a Snapshot object which will be mutated by the provided ops
+ * @param ops - an array of ops to apply to the snapshot
+ * @return an error object if applicable
+ */
+exports.applyOps = function(snapshot, ops) {
+ var type = null;
+
+ if (snapshot.type) {
+ type = types[snapshot.type];
+ if (!type) return {code: 4008, message: 'Unknown type'};
+ }
+
+ for (var index = 0; index < ops.length; index++) {
+ var op = ops[index];
+
+ snapshot.v = op.v + 1;
+
+ if (op.create) {
+ type = types[op.create.type];
+ if (!type) return {code: 4008, message: 'Unknown type'};
+ snapshot.data = type.create(op.create.data);
+ snapshot.type = type.uri;
+ } else if (op.del) {
+ snapshot.data = undefined;
+ type = null;
+ snapshot.type = null;
+ } else {
+ snapshot.data = type.apply(snapshot.data, op.op);
+ }
+ }
+};
diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js
new file mode 100644
index 000000000..423729967
--- /dev/null
+++ b/lib/presence/dummy.js
@@ -0,0 +1,70 @@
+/*
+ * Dummy Presence
+ * --------------
+ *
+ * This module provides a dummy implementation of presence that does nothing.
+ * Its purpose is to stand in for a real implementation, to simplify code in doc.js.
+ *
+ */
+var presence = require('./index');
+
+function noop() {}
+function returnEmptyArray() {
+ return [];
+};
+function returnFalse() {
+ return false;
+};
+
+function DocPresence() {}
+DocPresence.prototype = Object.create(presence.DocPresence.prototype);
+Object.assign(DocPresence.prototype, {
+ submitPresence: noop,
+ handlePresence: noop,
+ processAllReceivedPresence: noop,
+ transformAllPresence: noop,
+ pausePresence: noop,
+ cacheOp: noop,
+ flushPresence: noop,
+ destroyPresence: noop,
+ clearCachedOps: noop,
+ hardRollbackPresence: returnEmptyArray,
+ hasPendingPresence: returnFalse,
+ getPendingPresence: returnEmptyArray,
+ _processReceivedPresence: noop,
+ _transformPresence: noop,
+ _setPresence: noop,
+ _emitPresence: noop
+});
+
+function ConnectionPresence() {}
+ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype);
+Object.assign(ConnectionPresence.prototype, {
+ isPresenceMessage: returnFalse,
+ handlePresenceMessage: noop,
+ sendPresence: noop
+});
+
+function AgentPresence() {}
+AgentPresence.prototype = Object.create(presence.AgentPresence.prototype);
+Object.assign(AgentPresence.prototype, {
+ isPresenceMessage: returnFalse,
+ processPresenceData: returnFalse,
+ createPresence: noop,
+ subscribeToStream: noop,
+ checkRequest: noop,
+ handlePresenceMessage: noop
+});
+
+function BackendPresence() {}
+BackendPresence.prototype = Object.create(presence.BackendPresence.prototype);
+Object.assign(BackendPresence.prototype, {
+ sendPresence: noop
+});
+
+module.exports = {
+ DocPresence: DocPresence,
+ ConnectionPresence: ConnectionPresence,
+ AgentPresence: AgentPresence,
+ BackendPresence: BackendPresence
+};
diff --git a/lib/presence/index.js b/lib/presence/index.js
new file mode 100644
index 000000000..cddecd0ad
--- /dev/null
+++ b/lib/presence/index.js
@@ -0,0 +1,6 @@
+module.exports = {
+ DocPresence: function DocPresence() {},
+ ConnectionPresence: function ConnectionPresence() {},
+ AgentPresence: function AgentPresence() {},
+ BackendPresence: function BackendPresence() {}
+};
diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js
new file mode 100644
index 000000000..14c186ed7
--- /dev/null
+++ b/lib/presence/stateless.js
@@ -0,0 +1,539 @@
+/*
+ * Stateless Presence
+ * ------------------
+ *
+ * This module provides an implementation of presence that works,
+ * but has some scalability problems. Each time a client joins a document,
+ * this implementation requests current presence information from all other clients,
+ * via the server. The server does not store any state at all regarding presence,
+ * it exists only in clients, hence the name "Doc Presence".
+ *
+ */
+var ShareDBError = require('../error');
+var presence = require('./index');
+var callEach = require('../util').callEach;
+
+// Check if a message represence presence.
+// Used in both ConnectionPresence and AgentPresence.
+function isPresenceMessage(data) {
+ return data.a === 'p';
+};
+
+
+/*
+ * Stateless Presence implementation of DocPresence
+ * ------------------------------------------------
+ */
+function DocPresence(doc) {
+ this.doc = doc;
+
+ // The current presence data.
+ // Map of src -> presence data
+ // Local src === ''
+ this.doc.presence = {};
+
+ // The presence objects received from the server.
+ // Map of src -> presence
+ this.received = {};
+
+ // The minimum amount of time to wait before removing processed presence from this.presence.received.
+ // The processed presence is removed to avoid leaking memory, in case peers keep connecting and disconnecting a lot.
+ // The processed presence is not removed immediately to enable avoiding race conditions, where messages with lower
+ // sequence number arrive after messages with higher sequence numbers.
+ this.receivedTimeout = 60000;
+
+ // If set to true, then the next time the local presence is sent,
+ // all other clients will be asked to reply with their own presence data.
+ this.requestReply = true;
+
+ // A list of ops sent by the server. These are needed for transforming presence data,
+ // if we get that presence data for an older version of the document.
+ this.cachedOps = [];
+
+ // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence
+ // data is supposed to be synced in real-time.
+ this.cachedOpsTimeout = 60000;
+
+ // The sequence number of the inflight presence request.
+ this.inflightSeq = 0;
+
+ // Callbacks (or null) for pending and inflight presence requests.
+ this.pending = null;
+ this.inflight = null;
+}
+
+DocPresence.prototype = Object.create(presence.DocPresence.prototype);
+
+// Submit presence data to a document.
+// This is the only public facing method.
+// All the others are marked as internal with a leading "_".
+DocPresence.prototype.submitPresence = function(data, callback) {
+ if (data != null) {
+ if (!this.doc.type) {
+ var doc = this.doc;
+ return process.nextTick(function() {
+ var err = new ShareDBError(4015,
+ 'Cannot submit presence. Document has not been created. ' + doc.collection + '.' + doc.id);
+ if (callback) return callback(err);
+ doc.emit('error', err);
+ });
+ }
+
+ if (!this.doc.type.createPresence || !this.doc.type.transformPresence) {
+ var doc = this.doc;
+ return process.nextTick(function() {
+ var err = new ShareDBError(4028,
+ 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id);
+ if (callback) return callback(err);
+ doc.emit('error', err);
+ });
+ }
+
+ data = this.doc.type.createPresence(data);
+ }
+
+ if (this._setPresence('', data, true) || this.pending || this.inflight) {
+ if (!this.pending) {
+ this.pending = [];
+ }
+ if (callback) {
+ this.pending.push(callback);
+ }
+ } else if (callback) {
+ process.nextTick(callback);
+ }
+
+ process.nextTick(this.doc.flush.bind(this.doc));
+};
+
+DocPresence.prototype.handlePresence = function(err, presence) {
+ if (!this.doc.subscribed) return;
+
+ var src = presence.src;
+ if (!src) {
+ // Handle the ACK for the presence data we submitted.
+ // this.inflightSeq would not equal presence.seq after a hard rollback,
+ // when all callbacks are flushed with an error.
+ if (this.inflightSeq === presence.seq) {
+ var callbacks = this.inflight;
+ this.inflight = null;
+ this.inflightSeq = 0;
+ var called = callbacks && callEach(callbacks, err);
+ if (err && !called) this.doc.emit('error', err);
+ this.doc.flush();
+ this.doc._emitNothingPending();
+ }
+ return;
+ }
+
+ // This shouldn't happen but check just in case.
+ if (err) return this.doc.emit('error', err);
+
+ if (presence.r && !this.pending) {
+ // Another client requested us to share our current presence data
+ this.pending = [];
+ this.doc.flush();
+ }
+
+ // Ignore older messages which arrived out of order
+ if (
+ this.received[src] && (
+ this.received[src].seq > presence.seq ||
+ (this.received[src].seq === presence.seq && presence.v != null)
+ )
+ ) return;
+
+ this.received[src] = presence;
+
+ if (presence.v == null) {
+ // null version should happen only when the server automatically sends
+ // null presence for an unsubscribed client
+ presence.processedAt = Date.now();
+ return this._setPresence(src, null, true);
+ }
+
+ // Get missing ops first, if necessary
+ if (this.doc.version == null || this.doc.version < presence.v) return this.doc.fetch();
+
+ this._processReceivedPresence(src, true);
+};
+
+// If emit is true and presence has changed, emits a presence event.
+// Returns true, if presence has changed for src. Otherwise false.
+DocPresence.prototype._processReceivedPresence = function(src, emit) {
+ if (!src) return false;
+ var presence = this.received[src];
+ if (!presence) return false;
+
+ if (presence.processedAt != null) {
+ if (Date.now() >= presence.processedAt + this.receivedTimeout) {
+ // Remove old received and processed presence.
+ delete this.received[src];
+ }
+ return false;
+ }
+
+ if (this.doc.version == null || this.doc.version < presence.v) {
+ // keep waiting for the missing snapshot or ops.
+ return false;
+ }
+
+ if (presence.p == null) {
+ // Remove presence data as requested.
+ presence.processedAt = Date.now();
+ return this._setPresence(src, null, emit);
+ }
+
+ if (!this.doc.type || !this.doc.type.createPresence || !this.doc.type.transformPresence) {
+ // Remove presence data because the document is not created or its type does not support presence
+ presence.processedAt = Date.now();
+ return this._setPresence(src, null, emit);
+ }
+
+ if (this.doc.inflightOp && this.doc.inflightOp.op == null) {
+ // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del"
+ presence.processedAt = Date.now();
+ return this._setPresence(src, null, emit);
+ }
+
+ for (var i = 0; i < this.doc.pendingOps.length; i++) {
+ if (this.doc.pendingOps[i].op == null) {
+ // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del"
+ presence.processedAt = Date.now();
+ return this._setPresence(src, null, emit);
+ }
+ }
+
+ var startIndex = this.cachedOps.length - (this.doc.version - presence.v);
+ if (startIndex < 0) {
+ // Remove presence data because we can't transform presence.received
+ presence.processedAt = Date.now();
+ return this._setPresence(src, null, emit);
+ }
+
+ for (var i = startIndex; i < this.cachedOps.length; i++) {
+ if (this.cachedOps[i].op == null) {
+ // Remove presence data because presence.received can be transformed only against "op", not "create" nor "del"
+ presence.processedAt = Date.now();
+ return this._setPresence(src, null, emit);
+ }
+ }
+
+ // Make sure the format of the data is correct
+ var data = this.doc.type.createPresence(presence.p);
+
+ // Transform against past ops
+ for (var i = startIndex; i < this.cachedOps.length; i++) {
+ var op = this.cachedOps[i];
+ data = this.doc.type.transformPresence(data, op.op, presence.src === op.src);
+ }
+
+ // Transform against pending ops
+ if (this.doc.inflightOp) {
+ data = this.doc.type.transformPresence(data, this.doc.inflightOp.op, false);
+ }
+
+ for (var i = 0; i < this.doc.pendingOps.length; i++) {
+ data = this.doc.type.transformPresence(data, this.doc.pendingOps[i].op, false);
+ }
+
+ // Set presence data
+ presence.processedAt = Date.now();
+ return this._setPresence(src, data, emit);
+};
+
+DocPresence.prototype.processAllReceivedPresence = function() {
+ var srcList = Object.keys(this.received);
+ var changedSrcList = [];
+ for (var i = 0; i < srcList.length; i++) {
+ var src = srcList[i];
+ if (this._processReceivedPresence(src)) {
+ changedSrcList.push(src);
+ }
+ }
+ this._emitPresence(changedSrcList, true);
+};
+
+DocPresence.prototype._transformPresence = function(src, op) {
+ var presenceData = this.doc.presence[src];
+ if (op.op != null) {
+ var isOwnOperation = src === (op.src || '');
+ presenceData = this.doc.type.transformPresence(presenceData, op.op, isOwnOperation);
+ } else {
+ presenceData = null;
+ }
+ return this._setPresence(src, presenceData);
+};
+
+DocPresence.prototype.transformAllPresence = function(op) {
+ var srcList = Object.keys(this.doc.presence);
+ var changedSrcList = [];
+ for (var i = 0; i < srcList.length; i++) {
+ var src = srcList[i];
+ if (this._transformPresence(src, op)) {
+ changedSrcList.push(src);
+ }
+ }
+ this._emitPresence(changedSrcList, false);
+};
+
+DocPresence.prototype.pausePresence = function() {
+ if (!this) return;
+
+ if (this.inflight) {
+ this.pending = this.pending
+ ? this.inflight.concat(this.pending)
+ : this.inflight;
+ this.inflight = null;
+ this.inflightSeq = 0;
+ } else if (!this.pending && this.doc.presence[''] != null) {
+ this.pending = [];
+ }
+ this.received = {};
+ this.requestReply = true;
+ var srcList = Object.keys(this.doc.presence);
+ var changedSrcList = [];
+ for (var i = 0; i < srcList.length; i++) {
+ var src = srcList[i];
+ if (src && this._setPresence(src, null)) {
+ changedSrcList.push(src);
+ }
+ }
+ this._emitPresence(changedSrcList, false);
+};
+
+// If emit is true and presence has changed, emits a presence event.
+// Returns true, if presence has changed. Otherwise false.
+DocPresence.prototype._setPresence = function(src, data, emit) {
+ if (data == null) {
+ if (this.doc.presence[src] == null) return false;
+ delete this.doc.presence[src];
+ } else {
+ var isPresenceEqual =
+ this.doc.presence[src] === data ||
+ (this.doc.type.comparePresence && this.doc.type.comparePresence(this.doc.presence[src], data));
+ if (isPresenceEqual) return false;
+ this.doc.presence[src] = data;
+ }
+ if (emit) this._emitPresence([src], true);
+ return true;
+};
+
+DocPresence.prototype._emitPresence = function(srcList, submitted) {
+ if (srcList && srcList.length > 0) {
+ var doc = this.doc;
+ process.nextTick(function() {
+ doc.emit('presence', srcList, submitted);
+ });
+ }
+};
+
+DocPresence.prototype.cacheOp = function(message) {
+ var op = {
+ src: message.src,
+ time: Date.now(),
+ create: !!message.create,
+ op: message.op,
+ del: !!message.del
+ };
+ // Remove the old ops.
+ var oldOpTime = Date.now() - this.cachedOpsTimeout;
+ var i;
+ for (i = 0; i < this.cachedOps.length; i++) {
+ if (this.cachedOps[i].time >= oldOpTime) {
+ break;
+ }
+ }
+ if (i > 0) {
+ this.cachedOps.splice(0, i);
+ }
+
+ // Cache the new op.
+ this.cachedOps.push(op);
+};
+
+// If there are no pending ops, this method sends the pending presence data, if possible.
+DocPresence.prototype.flushPresence = function() {
+ if (!this.inflight && this.pending) {
+ this.inflight = this.pending;
+ this.inflightSeq = this.doc.connection.seq;
+ this.pending = null;
+ this.doc.connection.sendPresence(this.doc, this.doc.presence[''], this.requestReply);
+ this.requestReply = false;
+ }
+};
+
+DocPresence.prototype.destroyPresence = function() {
+ this.received = {};
+ this.clearCachedOps();
+};
+
+DocPresence.prototype.clearCachedOps = function() {
+ this.cachedOps.length = 0;
+};
+
+// Reset presence-related properties.
+DocPresence.prototype.hardRollbackPresence = function() {
+ this.inflight = null;
+ this.inflightSeq = 0;
+ this.pending = null;
+ this.cachedOps.length = 0;
+ this.received = {};
+ this.requestReply = true;
+
+ var srcList = Object.keys(this.doc.presence);
+ var changedSrcList = [];
+ for (var i = 0; i < srcList.length; i++) {
+ var src = srcList[i];
+ if (this._setPresence(src, null)) {
+ changedSrcList.push(src);
+ }
+ }
+ this._emitPresence(changedSrcList, false);
+};
+
+DocPresence.prototype.hasPendingPresence = function() {
+ return this.inflight || this.pending;
+};
+
+DocPresence.prototype.getPendingPresence = function() {
+ var pendingPresence = [];
+ if (this.inflight) pendingPresence.push(this.inflight);
+ if (this.pending) pendingPresence.push(this.pending);
+ return pendingPresence;
+};
+
+
+/*
+ * Stateless Presence implementation of ConnectionPresence
+ * -------------------------------------------------------
+ */
+function ConnectionPresence(connection) {
+ this.connection = connection;
+}
+ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype);
+
+ConnectionPresence.prototype.isPresenceMessage = isPresenceMessage;
+
+ConnectionPresence.prototype.handlePresenceMessage = function(err, message) {
+ var doc = this.connection.getExisting(message.c, message.d);
+ if (doc) doc._handlePresence(err, message);
+};
+
+ConnectionPresence.prototype.sendPresence = function(doc, data, requestReply) {
+ // Ensure the doc is registered so that it receives the reply message
+ this.connection._addDoc(doc);
+ var message = {
+ a: 'p',
+ c: doc.collection,
+ d: doc.id,
+ p: data,
+ v: doc.version || 0,
+ seq: this.connection.seq++
+ };
+ if (requestReply) {
+ message.r = true;
+ }
+ this.connection.send(message);
+};
+
+
+/*
+ * Stateless Presence implementation of AgentPresence
+ * --------------------------------------------------
+ */
+function AgentPresence(agent) {
+ this.agent = agent;
+
+ // The max presence sequence number received from the client.
+ this.maxPresenceSeq = 0;
+}
+AgentPresence.prototype = Object.create(presence.AgentPresence.prototype);
+
+AgentPresence.prototype.isPresenceMessage = isPresenceMessage;
+
+AgentPresence.prototype.processPresenceData = function(data) {
+ if (data.a === 'p') {
+ // Send other clients' presence data
+ if (data.src !== this.agent.clientId) this.agent.send(data);
+ return true;
+ }
+};
+
+AgentPresence.prototype.createPresence = function(collection, id, data, version, requestReply, seq) {
+ return {
+ a: 'p',
+ src: this.agent.clientId,
+ seq: seq != null ? seq : this.maxPresenceSeq,
+ c: collection,
+ d: id,
+ p: data,
+ v: version,
+ r: requestReply
+ };
+};
+
+AgentPresence.prototype.subscribeToStream = function(collection, id, stream) {
+ var agent = this.agent;
+ stream.on('end', function() {
+ agent.backend.sendPresence(agent._agentPresence.createPresence(collection, id));
+ });
+};
+
+AgentPresence.prototype.checkRequest = function(request) {
+ if (request.a === 'p') {
+ if (typeof request.c !== 'string') return 'Invalid collection';
+ if (typeof request.d !== 'string') return 'Invalid id';
+ if (typeof request.v !== 'number' || request.v < 0) return 'Invalid version';
+ if (typeof request.seq !== 'number' || request.seq <= 0) return 'Invalid seq';
+ if (typeof request.r !== 'undefined' && typeof request.r !== 'boolean') {
+ return 'Invalid "request reply" value';
+ }
+ }
+};
+
+AgentPresence.prototype.handlePresenceMessage = function(request, callback) {
+ var presence = this.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq);
+ if (presence.seq <= this.maxPresenceSeq) {
+ return process.nextTick(function() {
+ callback(new ShareDBError(4027, 'Presence data superseded'));
+ });
+ }
+ this.maxPresenceSeq = presence.seq;
+ if (!this.agent.subscribedDocs[presence.c] || !this.agent.subscribedDocs[presence.c][presence.d]) {
+ return process.nextTick(function() {
+ callback(new ShareDBError(4026, [
+ 'Cannot send presence. Not subscribed to document:',
+ presence.c,
+ presence.d
+ ].join(' ')));
+ });
+ }
+ this.agent.backend.sendPresence(presence, function(err) {
+ if (err) return callback(err);
+ callback(null, {seq: presence.seq});
+ });
+};
+
+
+/*
+ * Stateless Presence implementation of BackendPresence
+ * ----------------------------------------------------
+ */
+function BackendPresence(backend) {
+ this.backend = backend;
+}
+BackendPresence.prototype = Object.create(presence.BackendPresence.prototype);
+
+BackendPresence.prototype.sendPresence = function(presence, callback) {
+ var channels = [this.backend.getDocChannel(presence.c, presence.d)];
+ this.backend.pubsub.publish(channels, presence, callback);
+};
+
+
+module.exports = {
+ DocPresence: DocPresence,
+ ConnectionPresence: ConnectionPresence,
+ AgentPresence: AgentPresence,
+ BackendPresence: BackendPresence
+};
diff --git a/lib/projections.js b/lib/projections.js
index 48baa5ae7..d9a48ea03 100644
--- a/lib/projections.js
+++ b/lib/projections.js
@@ -41,7 +41,7 @@ function projectEdit(fields, op) {
var path = c.p;
if (path.length === 0) {
- var newC = {p:[]};
+ var newC = {p: []};
if (c.od !== undefined || c.oi !== undefined) {
if (c.od !== undefined) {
diff --git a/lib/query-emitter.js b/lib/query-emitter.js
index 9599c71cf..fe5c5b01a 100644
--- a/lib/query-emitter.js
+++ b/lib/query-emitter.js
@@ -21,10 +21,10 @@ function QueryEmitter(request, stream, ids, extra) {
this.canPollDoc = this.db.canPollDoc(this.collection, this.query);
this.pollDebounce =
(typeof this.options.pollDebounce === 'number') ? this.options.pollDebounce :
- (typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce : 0;
+ (typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce : 0;
this.pollInterval =
(typeof this.options.pollInterval === 'number') ? this.options.pollInterval :
- (typeof this.db.pollInterval === 'number') ? this.db.pollInterval : 0;
+ (typeof this.db.pollInterval === 'number') ? this.db.pollInterval : 0;
this._polling = false;
this._pendingPoll = null;
@@ -186,13 +186,20 @@ QueryEmitter.prototype.queryPoll = function(callback) {
emitter.db.getSnapshotBulk(emitter.collection, inserted, emitter.fields, null, function(err, snapshotMap) {
if (err) return emitter._finishPoll(err, callback, pending);
var snapshots = emitter.backend._getSnapshotsFromMap(inserted, snapshotMap);
- emitter.backend._sanitizeSnapshots(emitter.agent, emitter.snapshotProjection, emitter.collection, snapshots, function(err) {
- if (err) return emitter._finishPoll(err, callback, pending);
- emitter._emitTiming('queryEmitter.pollGetSnapshotBulk', start);
- var diff = mapDiff(idsDiff, snapshotMap);
- emitter.onDiff(diff);
- emitter._finishPoll(err, callback, pending);
- });
+ var snapshotType = emitter.backend.SNAPSHOT_TYPES.current;
+ emitter.backend._sanitizeSnapshots(
+ emitter.agent,
+ emitter.snapshotProjection,
+ emitter.collection,
+ snapshots,
+ snapshotType,
+ function(err) {
+ if (err) return emitter._finishPoll(err, callback, pending);
+ emitter._emitTiming('queryEmitter.pollGetSnapshotBulk', start);
+ var diff = mapDiff(idsDiff, snapshotMap);
+ emitter.onDiff(diff);
+ emitter._finishPoll(err, callback, pending);
+ });
});
} else {
emitter.onDiff(idsDiff);
@@ -234,12 +241,19 @@ QueryEmitter.prototype.queryPollDoc = function(id, callback) {
emitter.db.getSnapshot(emitter.collection, id, emitter.fields, null, function(err, snapshot) {
if (err) return callback(err);
var snapshots = [snapshot];
- emitter.backend._sanitizeSnapshots(emitter.agent, emitter.snapshotProjection, emitter.collection, snapshots, function(err) {
- if (err) return callback(err);
- emitter.onDiff([new arraydiff.InsertDiff(index, snapshots)]);
- emitter._emitTiming('queryEmitter.pollDocGetSnapshot', start);
- callback();
- });
+ var snapshotType = emitter.backend.SNAPSHOT_TYPES.current;
+ emitter.backend._sanitizeSnapshots(
+ emitter.agent,
+ emitter.snapshotProjection,
+ emitter.collection,
+ snapshots,
+ snapshotType,
+ function(err) {
+ if (err) return callback(err);
+ emitter.onDiff([new arraydiff.InsertDiff(index, snapshots)]);
+ emitter._emitTiming('queryEmitter.pollDocGetSnapshot', start);
+ callback();
+ });
});
return;
}
diff --git a/lib/snapshot.js b/lib/snapshot.js
new file mode 100644
index 000000000..548a7e25b
--- /dev/null
+++ b/lib/snapshot.js
@@ -0,0 +1,8 @@
+module.exports = Snapshot;
+function Snapshot(id, version, type, data, meta) {
+ this.id = id;
+ this.v = version;
+ this.type = type;
+ this.data = data;
+ this.m = meta;
+}
diff --git a/lib/stream-socket.js b/lib/stream-socket.js
index e9c5303fa..696c24f35 100644
--- a/lib/stream-socket.js
+++ b/lib/stream-socket.js
@@ -1,5 +1,6 @@
var Duplex = require('stream').Duplex;
var inherits = require('util').inherits;
+var logger = require('./logger');
var util = require('./util');
function StreamSocket() {
@@ -36,7 +37,7 @@ function ServerStream(socket) {
this.socket = socket;
this.on('error', function(error) {
- console.warn('ShareDB client message stream error', error);
+ logger.warn('ShareDB client message stream error', error);
socket.close('stopped');
});
diff --git a/lib/submit-request.js b/lib/submit-request.js
index 5ff3d1997..068be3123 100644
--- a/lib/submit-request.js
+++ b/lib/submit-request.js
@@ -21,6 +21,11 @@ function SubmitRequest(backend, agent, index, id, op, options) {
// For custom use in middleware
this.custom = {};
+ // Whether or not to store a milestone snapshot. If left as null, the milestone
+ // snapshots are saved according to the interval provided to the milestone db
+ // options. If overridden to a boolean value, then that value is used instead of
+ // the interval logic.
+ this.saveMilestoneSnapshot = null;
this.suppressPublish = backend.suppressPublish;
this.maxRetries = backend.maxSubmitRetries;
this.retries = 0;
@@ -49,7 +54,6 @@ SubmitRequest.prototype.submit = function(callback) {
request._addSnapshotMeta();
if (op.v == null) {
-
if (op.create && snapshot.type && op.src) {
// If the document was already created by another op, we will return a
// 'Document already exists' error in response and fail to submit this
@@ -142,25 +146,34 @@ SubmitRequest.prototype.commit = function(callback) {
if (err) return callback(err);
// Try committing the operation and snapshot to the database atomically
- backend.db.commit(request.collection, request.id, request.op, request.snapshot, request.options, function(err, succeeded) {
- if (err) return callback(err);
- if (!succeeded) {
- // Between our fetch and our call to commit, another client committed an
- // operation. We expect this to be relatively infrequent but normal.
- return request.retry(callback);
- }
- if (!request.suppressPublish) {
- var op = request.op;
- op.c = request.collection;
- op.d = request.id;
- op.m = undefined;
- // Needed for agent to detect if it can ignore sending the op back to
- // the client that submitted it in subscriptions
- if (request.collection !== request.index) op.i = request.index;
- backend.pubsub.publish(request.channels, op);
- }
- callback();
- });
+ backend.db.commit(
+ request.collection,
+ request.id,
+ request.op,
+ request.snapshot,
+ request.options,
+ function(err, succeeded) {
+ if (err) return callback(err);
+ if (!succeeded) {
+ // Between our fetch and our call to commit, another client committed an
+ // operation. We expect this to be relatively infrequent but normal.
+ return request.retry(callback);
+ }
+ if (!request.suppressPublish) {
+ var op = request.op;
+ op.c = request.collection;
+ op.d = request.id;
+ op.m = undefined;
+ // Needed for agent to detect if it can ignore sending the op back to
+ // the client that submitted it in subscriptions
+ if (request.collection !== request.index) op.i = request.index;
+ backend.pubsub.publish(request.channels, op);
+ }
+ if (request._shouldSaveMilestoneSnapshot(request.snapshot)) {
+ request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot);
+ }
+ callback();
+ });
});
};
@@ -216,6 +229,15 @@ SubmitRequest.prototype._addSnapshotMeta = function() {
meta.mtime = this.start;
};
+SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) {
+ // If the flag is null, it's not been overridden by the consumer, so apply the interval
+ if (this.saveMilestoneSnapshot === null) {
+ return snapshot && snapshot.v % this.backend.milestoneDb.interval === 0;
+ }
+
+ return this.saveMilestoneSnapshot;
+};
+
// Non-fatal client errors:
SubmitRequest.prototype.alreadySubmittedError = function() {
return {code: 4001, message: 'Op already submitted'};
@@ -235,7 +257,10 @@ SubmitRequest.prototype.projectionError = function() {
};
// Fatal internal errors:
SubmitRequest.prototype.missingOpsError = function() {
- return {code: 5001, message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'};
+ return {
+ code: 5001,
+ 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'};
diff --git a/lib/util.js b/lib/util.js
index 5c8021c0d..4c4783430 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -6,3 +6,31 @@ exports.hasKeys = function(object) {
for (var key in object) return true;
return false;
};
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
+exports.isInteger = Number.isInteger || function(value) {
+ return typeof value === 'number' &&
+ isFinite(value) &&
+ Math.floor(value) === value;
+};
+
+exports.isValidVersion = function(version) {
+ if (version === null) return true;
+ return exports.isInteger(version) && version >= 0;
+};
+
+exports.isValidTimestamp = function(timestamp) {
+ return exports.isValidVersion(timestamp);
+};
+
+exports.callEach = function(callbacks, err) {
+ var called = false;
+ for (var i = 0; i < callbacks.length; i++) {
+ var callback = callbacks[i];
+ if (callback) {
+ callback(err);
+ called = true;
+ }
+ }
+ return called;
+};
diff --git a/package.json b/package.json
index 934a886c2..b30da8173 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sharedb",
- "version": "1.0.0-beta.9",
+ "version": "1.0.0-beta.23",
"description": "JSON OT database backend",
"main": "lib/index.js",
"dependencies": {
@@ -13,16 +13,19 @@
},
"devDependencies": {
"coveralls": "^2.11.8",
+ "eslint": "^5.16.0",
+ "eslint-config-google": "^0.13.0",
"expect.js": "^0.3.1",
"istanbul": "^0.4.2",
- "jshint": "^2.9.2",
- "mocha": "^5.1.1",
- "sharedb-mingo-memory": "^1.0.0-beta"
+ "lolex": "^3.0.0",
+ "mocha": "^5.2.0",
+ "sinon": "^6.1.5"
},
"scripts": {
- "test": "./node_modules/.bin/mocha && npm run jshint",
+ "test": "./node_modules/.bin/mocha && npm run lint",
"test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha",
- "jshint": "./node_modules/.bin/jshint lib/*.js test/*.js"
+ "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'",
+ "lint:fix": "npm run lint -- --fix"
},
"repository": {
"type": "git",
diff --git a/test/backend.js b/test/backend.js
new file mode 100644
index 000000000..991c6716b
--- /dev/null
+++ b/test/backend.js
@@ -0,0 +1,104 @@
+var Backend = require('../lib/backend');
+var expect = require('expect.js');
+
+describe('Backend', function() {
+ var backend;
+
+ beforeEach(function() {
+ backend = new Backend();
+ });
+
+ afterEach(function(done) {
+ backend.close(done);
+ });
+
+ describe('a simple document', function() {
+ beforeEach(function(done) {
+ var doc = backend.connect().get('books', '1984');
+ doc.create({title: '1984'}, function(error) {
+ if (error) return done(error);
+ doc.submitOp({p: ['author'], oi: 'George Orwell'}, done);
+ });
+ });
+
+ describe('getOps', function() {
+ it('fetches all the ops', function(done) {
+ backend.getOps(null, 'books', '1984', 0, null, function(error, ops) {
+ if (error) return done(error);
+ expect(ops).to.have.length(2);
+ expect(ops[0].create.data).to.eql({title: '1984'});
+ expect(ops[1].op).to.eql([{p: ['author'], oi: 'George Orwell'}]);
+ done();
+ });
+ });
+
+ it('fetches the ops with metadata', function(done) {
+ var options = {
+ opsOptions: {metadata: true}
+ };
+ backend.getOps(null, 'books', '1984', 0, null, options, function(error, ops) {
+ if (error) return done(error);
+ expect(ops).to.have.length(2);
+ expect(ops[0].m).to.be.ok();
+ expect(ops[1].m).to.be.ok();
+ done();
+ });
+ });
+ });
+
+ describe('fetch', function() {
+ it('fetches the document', function(done) {
+ backend.fetch(null, 'books', '1984', function(error, doc) {
+ if (error) return done(error);
+ expect(doc.data).to.eql({
+ title: '1984',
+ author: 'George Orwell'
+ });
+ done();
+ });
+ });
+
+ it('fetches the document with metadata', function(done) {
+ var options = {
+ snapshotOptions: {metadata: true}
+ };
+ backend.fetch(null, 'books', '1984', options, function(error, doc) {
+ if (error) return done(error);
+ expect(doc.m).to.be.ok();
+ done();
+ });
+ });
+ });
+
+ describe('subscribe', function() {
+ it('subscribes to the document', function(done) {
+ backend.subscribe(null, 'books', '1984', null, function(error, stream, snapshot) {
+ if (error) return done(error);
+ expect(stream.open).to.be(true);
+ expect(snapshot.data).to.eql({
+ title: '1984',
+ author: 'George Orwell'
+ });
+ var op = {op: {p: ['publication'], oi: 1949}};
+ stream.on('data', function(data) {
+ expect(data.op).to.eql(op.op);
+ done();
+ });
+ backend.submit(null, 'books', '1984', op, null, function(error) {
+ if (error) return done(error);
+ });
+ });
+ });
+
+ it('does not support subscribing to the document with options', function(done) {
+ var options = {
+ opsOptions: {metadata: true}
+ };
+ backend.subscribe(null, 'books', '1984', null, options, function(error) {
+ expect(error.code).to.be(4025);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/test/client/connection.js b/test/client/connection.js
index 677e87e44..b38ff53fa 100644
--- a/test/client/connection.js
+++ b/test/client/connection.js
@@ -3,7 +3,6 @@ var Backend = require('../../lib/backend');
var Connection = require('../../lib/client/connection');
describe('client connection', function() {
-
beforeEach(function() {
this.backend = new Backend();
});
@@ -42,8 +41,8 @@ describe('client connection', function() {
request.agent.close();
next();
});
- var connection = this.backend.connect();
- })
+ this.backend.connect();
+ });
it('emits stopped event on call to agent.close()', function(done) {
this.backend.use('connect', function(request, next) {
@@ -56,6 +55,27 @@ describe('client connection', function() {
});
});
+ it('subscribing to same doc closes old stream and adds new stream to agent', function(done) {
+ var connection = this.backend.connect();
+ var agent = connection.agent;
+ var collection = 'test';
+ var docId = 'abcd-1234';
+ var doc = connection.get(collection, docId);
+ doc.subscribe(function(err) {
+ if (err) return done(err);
+ var originalStream = agent.subscribedDocs[collection][docId];
+ doc.subscribe(function() {
+ if (err) return done(err);
+ expect(originalStream).to.have.property('open', false);
+ var newStream = agent.subscribedDocs[collection][docId];
+ expect(newStream).to.have.property('open', true);
+ expect(newStream).to.not.be(originalStream);
+ connection.close();
+ done();
+ });
+ });
+ });
+
it('emits socket errors as "connection error" events', function(done) {
var connection = this.backend.connect();
connection.on('connection error', function(err) {
@@ -80,45 +100,65 @@ describe('client connection', function() {
});
});
+ it('updates after connection socket stream emits "close"', function(done) {
+ var backend = this.backend;
+ var connection = backend.connect();
+ connection.on('connected', function() {
+ connection.socket.stream.emit('close');
+ expect(backend.agentsCount).equal(0);
+ done();
+ });
+ });
+
+ it('updates correctly after stream emits both "end" and "close"', function(done) {
+ var backend = this.backend;
+ var connection = backend.connect();
+ connection.on('connected', function() {
+ connection.socket.stream.emit('end');
+ connection.socket.stream.emit('close');
+ expect(backend.agentsCount).equal(0);
+ done();
+ });
+ });
+
it('does not increment when agent connect is rejected', function() {
var backend = this.backend;
backend.use('connect', function(request, next) {
next({message: 'Error'});
});
expect(backend.agentsCount).equal(0);
- var connection = backend.connect();
+ backend.connect();
expect(backend.agentsCount).equal(0);
});
});
describe('state management using setSocket', function() {
-
- it('initial connection.state is connecting, if socket.readyState is CONNECTING', function () {
- // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-connecting
- var socket = {readyState: 0};
- var connection = new Connection(socket);
- expect(connection.state).equal('connecting');
+ it('initial connection.state is connecting, if socket.readyState is CONNECTING', function() {
+ // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-connecting
+ var socket = {readyState: 0};
+ var connection = new Connection(socket);
+ expect(connection.state).equal('connecting');
});
- it('initial connection.state is connecting, if socket.readyState is OPEN', function () {
- // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-open
- var socket = {readyState: 1};
- var connection = new Connection(socket);
- expect(connection.state).equal('connecting');
+ it('initial connection.state is connecting, if socket.readyState is OPEN', function() {
+ // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-open
+ var socket = {readyState: 1};
+ var connection = new Connection(socket);
+ expect(connection.state).equal('connecting');
});
- it('initial connection.state is disconnected, if socket.readyState is CLOSING', function () {
- // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closing
- var socket = {readyState: 2};
- var connection = new Connection(socket);
- expect(connection.state).equal('disconnected');
+ it('initial connection.state is disconnected, if socket.readyState is CLOSING', function() {
+ // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closing
+ var socket = {readyState: 2};
+ var connection = new Connection(socket);
+ expect(connection.state).equal('disconnected');
});
- it('initial connection.state is disconnected, if socket.readyState is CLOSED', function () {
- // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closed
- var socket = {readyState: 3};
- var connection = new Connection(socket);
- expect(connection.state).equal('disconnected');
+ it('initial connection.state is disconnected, if socket.readyState is CLOSED', function() {
+ // https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closed
+ var socket = {readyState: 3};
+ var connection = new Connection(socket);
+ expect(connection.state).equal('disconnected');
});
it('initial state is connecting', function() {
@@ -156,7 +196,5 @@ describe('client connection', function() {
done();
});
});
-
});
-
});
diff --git a/test/client/doc.js b/test/client/doc.js
index b44f52a2b..9b5316e3f 100644
--- a/test/client/doc.js
+++ b/test/client/doc.js
@@ -1,8 +1,8 @@
var Backend = require('../../lib/backend');
var expect = require('expect.js');
+var util = require('../util');
-describe('client query subscribe', function() {
-
+describe('Doc', function() {
beforeEach(function() {
this.backend = new Backend();
this.connection = this.backend.connect();
@@ -14,32 +14,42 @@ describe('client query subscribe', function() {
expect(doc).equal(doc2);
});
- it('calling doc.destroy unregisters it', function() {
- var doc = this.connection.get('dogs', 'fido');
- expect(this.connection.getExisting('dogs', 'fido')).equal(doc);
+ it('calling doc.destroy unregisters it', function(done) {
+ var connection = this.connection;
+ var doc = connection.get('dogs', 'fido');
+ expect(connection.getExisting('dogs', 'fido')).equal(doc);
- doc.destroy();
- expect(this.connection.getExisting('dogs', 'fido')).equal(undefined);
+ doc.destroy(function(err) {
+ if (err) return done(err);
+ expect(connection.getExisting('dogs', 'fido')).equal(undefined);
- var doc2 = this.connection.get('dogs', 'fido');
- expect(doc).not.equal(doc2);
+ var doc2 = connection.get('dogs', 'fido');
+ expect(doc).not.equal(doc2);
+ done();
+ });
+
+ // destroy is async
+ expect(connection.getExisting('dogs', 'fido')).equal(doc);
});
- it('getting then destroying then getting returns a new doc object', function() {
- var doc = this.connection.get('dogs', 'fido');
- doc.destroy();
- var doc2 = this.connection.get('dogs', 'fido');
- expect(doc).not.equal(doc2);
- expect(doc).eql(doc2);
+ it('getting then destroying then getting returns a new doc object', function(done) {
+ var connection = this.connection;
+ var doc = connection.get('dogs', 'fido');
+ doc.destroy(function(err) {
+ if (err) return done(err);
+ var doc2 = connection.get('dogs', 'fido');
+ expect(doc).not.equal(doc2);
+ expect(doc).eql(doc2);
+ done();
+ });
});
- it('doc.destroy() calls back', function(done) {
+ it('doc.destroy() works without a callback', function() {
var doc = this.connection.get('dogs', 'fido');
- doc.destroy(done);
+ doc.destroy();
});
describe('applyStack', function() {
-
beforeEach(function(done) {
this.doc = this.connection.get('dogs', 'fido');
this.doc2 = this.backend.connect().get('dogs', 'fido');
@@ -209,7 +219,170 @@ describe('client query subscribe', function() {
verifyConsistency(doc, doc2, doc3, handlers, done);
});
});
+ });
+
+ describe('submitting ops in callbacks', function() {
+ beforeEach(function() {
+ this.doc = this.connection.get('dogs', 'scooby');
+ });
+
+ it('succeeds with valid op', function(done) {
+ var doc = this.doc;
+ doc.create({name: 'Scooby Doo'}, function(error) {
+ expect(error).to.not.be.ok();
+ // Build valid op that deletes a substring at index 0 of name.
+ var textOpComponents = [{p: 0, d: 'Scooby '}];
+ var op = [{p: ['name'], t: 'text0', o: textOpComponents}];
+ doc.submitOp(op, function(error) {
+ if (error) return done(error);
+ expect(doc.data).eql({name: 'Doo'});
+ done();
+ });
+ });
+ });
+ it('fails with invalid op', function(done) {
+ var doc = this.doc;
+ doc.create({name: 'Scooby Doo'}, function(error) {
+ expect(error).to.not.be.ok();
+ // Build op that tries to delete an invalid substring at index 0 of name.
+ var textOpComponents = [{p: 0, d: 'invalid'}];
+ var op = [{p: ['name'], t: 'text0', o: textOpComponents}];
+ doc.submitOp(op, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+ });
});
+ describe('submitting an invalid op', function() {
+ var doc;
+ var invalidOp;
+ var validOp;
+
+ beforeEach(function(done) {
+ // This op is invalid because we try to perform a list deletion
+ // on something that isn't a list
+ invalidOp = {p: ['name'], ld: 'Scooby'};
+
+ validOp = {p: ['snacks'], oi: true};
+
+ doc = this.connection.get('dogs', 'scooby');
+ doc.create({name: 'Scooby'}, function(error) {
+ if (error) return done(error);
+ doc.whenNothingPending(done);
+ });
+ });
+
+ it('returns an error to the submitOp callback', function(done) {
+ doc.submitOp(invalidOp, function(error) {
+ expect(error.message).to.equal('Referenced element not a list');
+ done();
+ });
+ });
+
+ it('rolls the doc back to a usable state', function(done) {
+ util.callInSeries([
+ function(next) {
+ doc.submitOp(invalidOp, function(error) {
+ expect(error).to.be.ok();
+ next();
+ });
+ },
+ function(next) {
+ doc.whenNothingPending(next);
+ },
+ function(next) {
+ expect(doc.data).to.eql({name: 'Scooby'});
+ doc.submitOp(validOp, next);
+ },
+ function(next) {
+ expect(doc.data).to.eql({name: 'Scooby', snacks: true});
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('rescues an irreversible op collision', function(done) {
+ // This test case attempts to reconstruct the following corner case, with
+ // two independent references to the same document. We submit two simultaneous, but
+ // incompatible operations (eg one of them changes the data structure the other op is
+ // attempting to manipulate).
+ //
+ // The second document to attempt to submit should have its op rejected, and its
+ // state successfully rolled back to a usable state.
+ var doc1 = this.backend.connect().get('dogs', 'snoopy');
+ var doc2 = this.backend.connect().get('dogs', 'snoopy');
+
+ var pauseSubmit = false;
+ var fireSubmit;
+ this.backend.use('submit', function(request, callback) {
+ if (pauseSubmit) {
+ fireSubmit = function() {
+ pauseSubmit = false;
+ callback();
+ };
+ } else {
+ fireSubmit = null;
+ callback();
+ }
+ });
+
+ util.callInSeries([
+ function(next) {
+ doc1.create({colours: ['white']}, next);
+ },
+ function(next) {
+ doc1.whenNothingPending(next);
+ },
+ function(next) {
+ doc2.fetch(next);
+ },
+ function(next) {
+ doc2.whenNothingPending(next);
+ },
+ // Both documents start off at the same v1 state, with colours as a list
+ function(next) {
+ expect(doc1.data).to.eql({colours: ['white']});
+ expect(doc2.data).to.eql({colours: ['white']});
+ next();
+ },
+ // doc1 successfully submits an op which changes our list into a string in v2
+ function(next) {
+ doc1.submitOp({p: ['colours'], oi: 'white,black'}, next);
+ },
+ // This next step is a little fiddly. We abuse the middleware to pause the op submission and
+ // ensure that we get this repeatable sequence of events:
+ // 1. doc2 is still on v1, where 'colours' is a list (but it's a string in v2)
+ // 2. doc2 submits an op that assumes 'colours' is still a list
+ // 3. doc2 fetches v2 before the op submission completes - 'colours' is no longer a list locally
+ // 4. doc2's op is rejected by the server, because 'colours' is not a list on the server
+ // 5. doc2 attempts to roll back the inflight op by turning a list insertion into a list deletion
+ // 6. doc2 applies this list deletion to a field that is no longer a list
+ // 7. type.apply throws, because this is an invalid op
+ function(next) {
+ pauseSubmit = true;
+ doc2.submitOp({p: ['colours', '0'], li: 'black'}, function(error) {
+ expect(error.message).to.equal('Referenced element not a list');
+ next();
+ });
+
+ doc2.fetch(function(error) {
+ if (error) return next(error);
+ fireSubmit();
+ });
+ },
+ // Validate that - despite the error in doc2.submitOp - doc2 has been returned to a
+ // workable state in v2
+ function(next) {
+ expect(doc1.data).to.eql({colours: 'white,black'});
+ expect(doc2.data).to.eql(doc1.data);
+ doc2.submitOp({p: ['colours'], oi: 'white,black,red'}, next);
+ },
+ done
+ ]);
+ });
+ });
});
diff --git a/test/client/number-type.js b/test/client/number-type.js
new file mode 100644
index 000000000..fa95056e4
--- /dev/null
+++ b/test/client/number-type.js
@@ -0,0 +1,23 @@
+// A simple number type, where:
+//
+// - snapshot is an integer
+// - operation is an integer
+exports.type = {
+ name: 'number-type',
+ uri: 'http://sharejs.org/types/number-type',
+ create: create,
+ apply: apply,
+ transform: transform
+};
+
+function create(data) {
+ return data | 0;
+}
+
+function apply(snapshot, op) {
+ return snapshot + op;
+}
+
+function transform(op1) {
+ return op1;
+}
diff --git a/test/client/pending.js b/test/client/pending.js
index 4896440d4..2b22c7a08 100644
--- a/test/client/pending.js
+++ b/test/client/pending.js
@@ -2,7 +2,6 @@ var expect = require('expect.js');
var Backend = require('../../lib/backend');
describe('client connection', function() {
-
beforeEach(function() {
this.backend = new Backend();
});
@@ -89,5 +88,4 @@ describe('client connection', function() {
});
});
});
-
});
diff --git a/test/client/presence-type.js b/test/client/presence-type.js
index 51ad272a0..7648d0e2a 100644
--- a/test/client/presence-type.js
+++ b/test/client/presence-type.js
@@ -46,24 +46,19 @@ function apply(snapshot, op) {
}
function transform(op1, op2, side) {
- return op1.index < op2.index || (op1.index === op2.index && side === 'left') ?
- op1 :
- {
- index: op1.index + 1,
- value: op1.value
- };
+ return op1.index < op2.index || (op1.index === op2.index && side === 'left')
+ ? op1
+ : {index: op1.index + 1, value: op1.value};
}
function createPresence(data) {
- return { index: (data && data.index) | 0 };
+ return {index: (data && data.index) | 0};
}
function transformPresence(presence, op, isOwnOperation) {
- return presence.index < op.index || (presence.index === op.index && !isOwnOperation) ?
- presence :
- {
- index: presence.index + 1
- };
+ return presence.index < op.index || (presence.index === op.index && !isOwnOperation)
+ ? presence
+ : {index: presence.index + 1};
}
function comparePresence(presence1, presence2) {
@@ -77,6 +72,7 @@ function createPresence2(data) {
}
function transformPresence2(presence, op, isOwnOperation) {
- return presence < op.index || (presence === op.index && !isOwnOperation) ?
- presence : presence + 1;
+ return presence < op.index || (presence === op.index && !isOwnOperation)
+ ? presence
+ : presence + 1;
}
diff --git a/test/client/presence.js b/test/client/presence.js
index 2bf7e7dab..c5fa39ff1 100644
--- a/test/client/presence.js
+++ b/test/client/presence.js
@@ -1,7 +1,11 @@
var async = require('async');
+var lolex = require('lolex');
var util = require('../util');
var errorHandler = util.errorHandler;
var Backend = require('../../lib/backend');
+var presence = require('../../lib/presence');
+var dummyPresence = require('../../lib/presence/dummy');
+var statelessPresence = require('../../lib/presence/stateless');
var ShareDBError = require('../../lib/error');
var expect = require('expect.js');
var types = require('../../lib/types');
@@ -10,18 +14,42 @@ types.register(presenceType.type);
types.register(presenceType.type2);
types.register(presenceType.type3);
+describe('client presence', function() {
+ it('should use dummyPresence if presence option not provided', function() {
+ var backend = new Backend();
+ var connection = backend.connect();
+ var doc = connection.get('dogs', 'fido');
+ expect(doc._docPresence instanceof dummyPresence.DocPresence).to.be(true);
+ });
+
+ it('should use presence option if provided', function() {
+ var backend = new Backend({presence: statelessPresence});
+ var connection = backend.connect();
+ var doc = connection.get('dogs', 'fido');
+ expect(doc._docPresence instanceof statelessPresence.DocPresence).to.be(true);
+ });
+
+ it('DummyPresence should subclass Presence', function() {
+ expect(dummyPresence.DocPresence.prototype instanceof presence.DocPresence).to.be(true);
+ });
+
+ it('StatelessPresence should subclass Presence', function() {
+ expect(statelessPresence.DocPresence.prototype instanceof presence.DocPresence).to.be(true);
+ });
+});
+
[
'wrapped-presence-no-compare',
'wrapped-presence-with-compare',
'unwrapped-presence'
].forEach(function(typeName) {
function p(index) {
- return typeName === 'unwrapped-presence' ? index : { index: index };
+ return typeName === 'unwrapped-presence' ? index : {index: index};
}
describe('client presence (' + typeName + ')', function() {
beforeEach(function() {
- this.backend = new Backend();
+ this.backend = new Backend({presence: statelessPresence});
this.connection = this.backend.connect();
this.connection2 = this.backend.connect();
this.doc = this.connection.get('dogs', 'fido');
@@ -38,10 +66,10 @@ types.register(presenceType.type3);
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
this.doc2.once('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.data).to.eql([]);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
@@ -57,14 +85,14 @@ types.register(presenceType.type3);
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
- this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done));
- this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done));
- this.doc.requestReplyPresence = false;
+ this.doc.submitOp({index: 0, value: 'a'}, errorHandler(done));
+ this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done));
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
this.doc2.once('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b' ]);
+ expect(this.doc2.data).to.eql(['a', 'b']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
done();
}.bind(this));
@@ -79,20 +107,20 @@ types.register(presenceType.type3);
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b' ]);
+ expect(this.doc2.data).to.eql(['a', 'b']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
done();
}.bind(this));
// A hack to send presence for a future version.
this.doc.version += 2;
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), function(err) {
if (err) return done(err);
this.doc.version -= 2;
- this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done));
- this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done));
+ this.doc.submitOp({index: 0, value: 'a'}, errorHandler(done));
+ this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done));
}.bind(this));
}.bind(this)
], allDone);
@@ -100,23 +128,23 @@ types.register(presenceType.type3);
it('handles presence sent for earlier revisions (own ops, presence.index < op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
- this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }),
- this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }),
+ this.doc.submitOp.bind(this.doc, {index: 1, value: 'b'}),
+ this.doc.submitOp.bind(this.doc, {index: 2, value: 'c'}),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 1;
- this.doc.data = [ 'a' ];
- this.doc.requestReplyPresence = false;
+ this.doc.data = ['a'];
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(0), errorHandler(done));
}.bind(this)
], allDone);
@@ -124,23 +152,23 @@ types.register(presenceType.type3);
it('handles presence sent for earlier revisions (own ops, presence.index === op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
- this.doc.submitOp.bind(this.doc, { index: 1, value: 'c' }),
- this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }),
+ this.doc.submitOp.bind(this.doc, {index: 1, value: 'c'}),
+ this.doc.submitOp.bind(this.doc, {index: 1, value: 'b'}),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(3));
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 1;
- this.doc.data = [ 'a' ];
- this.doc.requestReplyPresence = false;
+ this.doc.data = ['a'];
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
}.bind(this)
], allDone);
@@ -148,23 +176,23 @@ types.register(presenceType.type3);
it('handles presence sent for earlier revisions (own ops, presence.index > op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
- this.doc.submitOp.bind(this.doc, { index: 0, value: 'b' }),
- this.doc.submitOp.bind(this.doc, { index: 0, value: 'a' }),
+ this.doc.submitOp.bind(this.doc, {index: 0, value: 'b'}),
+ this.doc.submitOp.bind(this.doc, {index: 0, value: 'a'}),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(3));
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 1;
- this.doc.data = [ 'c' ];
- this.doc.requestReplyPresence = false;
+ this.doc.data = ['c'];
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
}.bind(this)
], allDone);
@@ -172,23 +200,23 @@ types.register(presenceType.type3);
it('handles presence sent for earlier revisions (non-own ops, presence.index < op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
- this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }),
- this.doc2.submitOp.bind(this.doc2, { index: 2, value: 'c' }),
+ this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'b'}),
+ this.doc2.submitOp.bind(this.doc2, {index: 2, value: 'c'}),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 1;
- this.doc.data = [ 'a' ];
- this.doc.requestReplyPresence = false;
+ this.doc.data = ['a'];
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(0), errorHandler(done));
}.bind(this)
], allDone);
@@ -196,23 +224,23 @@ types.register(presenceType.type3);
it('handles presence sent for earlier revisions (non-own ops, presence.index === op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
- this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'c' }),
- this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }),
+ this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'c'}),
+ this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'b'}),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 1;
- this.doc.data = [ 'a' ];
- this.doc.requestReplyPresence = false;
+ this.doc.data = ['a'];
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
}.bind(this)
], allDone);
@@ -220,23 +248,23 @@ types.register(presenceType.type3);
it('handles presence sent for earlier revisions (non-own ops, presence.index > op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
- this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'b' }),
- this.doc2.submitOp.bind(this.doc2, { index: 0, value: 'a' }),
+ this.doc2.submitOp.bind(this.doc2, {index: 0, value: 'b'}),
+ this.doc2.submitOp.bind(this.doc2, {index: 0, value: 'a'}),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(3));
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 1;
- this.doc.data = [ 'c' ];
- this.doc.requestReplyPresence = false;
+ this.doc.data = ['c'];
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
}.bind(this)
], allDone);
@@ -247,31 +275,31 @@ types.register(presenceType.type3);
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.create.bind(this.doc, [], typeName),
- this.doc.submitOp.bind(this.doc, { index: 0, value: 'a' }),
+ this.doc.submitOp.bind(this.doc, {index: 0, value: 'a'}),
this.doc.del.bind(this.doc),
- this.doc.create.bind(this.doc, [ 'b' ], typeName),
+ this.doc.create.bind(this.doc, ['b'], typeName),
function(done) {
this.doc2.once('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'b' ]);
+ expect(this.doc2.data).to.eql(['b']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(0), errorHandler(done));
}.bind(this),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'b' ]);
+ expect(this.doc2.data).to.eql(['b']);
expect(this.doc2.presence).to.not.have.key(this.connection.id);
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 2;
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
}.bind(this)
], allDone);
@@ -279,35 +307,35 @@ types.register(presenceType.type3);
it('handles presence sent for earlier revisions (no cached ops)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
- this.doc.submitOp.bind(this.doc, { index: 1, value: 'b' }),
- this.doc.submitOp.bind(this.doc, { index: 2, value: 'c' }),
+ this.doc.submitOp.bind(this.doc, {index: 1, value: 'b'}),
+ this.doc.submitOp.bind(this.doc, {index: 2, value: 'c'}),
function(done) {
this.doc2.once('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(0), errorHandler(done));
}.bind(this),
function(done) {
- this.doc2.cachedOps = [];
+ this.doc2._docPresence.cachedOps = [];
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
- expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]);
+ expect(this.doc2.data).to.eql(['a', 'b', 'c']);
expect(this.doc2.presence).to.not.have.key(this.connection.id);
done();
}.bind(this));
// A hack to send presence for an older version.
this.doc.version = 1;
- this.doc.data = [ 'a' ];
- this.doc.requestReplyPresence = false;
+ this.doc.data = ['a'];
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
}.bind(this)
], allDone);
@@ -323,7 +351,7 @@ types.register(presenceType.type3);
setTimeout,
function(done) {
this.doc.on('presence', function(srcList, submitted) {
- expect(srcList.sort()).to.eql([ '', this.connection2.id ]);
+ expect(srcList.sort()).to.eql(['', this.connection2.id]);
expect(submitted).to.equal(false);
expect(this.doc.presence).to.not.have.key('');
expect(this.doc.presence).to.not.have.key(this.connection2.id);
@@ -344,7 +372,7 @@ types.register(presenceType.type3);
setTimeout,
function(done) {
this.doc.on('presence', function(srcList, submitted) {
- expect(srcList.sort()).to.eql([ '', this.connection2.id ]);
+ expect(srcList.sort()).to.eql(['', this.connection2.id]);
expect(submitted).to.equal(false);
expect(this.doc.presence).to.not.have.key('');
expect(this.doc.presence).to.not.have.key(this.connection2.id);
@@ -357,7 +385,7 @@ types.register(presenceType.type3);
it('transforms presence against local op (presence.index != op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['a', 'c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -365,20 +393,20 @@ types.register(presenceType.type3);
setTimeout,
function(done) {
this.doc.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection2.id ]);
+ expect(srcList).to.eql([this.connection2.id]);
expect(submitted).to.equal(false);
expect(this.doc.presence['']).to.eql(p(0));
expect(this.doc.presence[this.connection2.id]).to.eql(p(3));
done();
}.bind(this));
- this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done));
+ this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done));
}.bind(this)
], allDone);
});
it('transforms presence against non-local op (presence.index != op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['a', 'c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -386,20 +414,20 @@ types.register(presenceType.type3);
setTimeout,
function(done) {
this.doc.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection2.id ]);
+ expect(srcList).to.eql([this.connection2.id]);
expect(submitted).to.equal(false);
expect(this.doc.presence['']).to.eql(p(0));
expect(this.doc.presence[this.connection2.id]).to.eql(p(3));
done();
}.bind(this));
- this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done));
+ this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done));
}.bind(this)
], allDone);
});
it('transforms presence against local op (presence.index == op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['a', 'c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(1)),
@@ -407,20 +435,20 @@ types.register(presenceType.type3);
setTimeout,
function(done) {
this.doc.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ '' ]);
+ expect(srcList).to.eql(['']);
expect(submitted).to.equal(false);
expect(this.doc.presence['']).to.eql(p(2));
expect(this.doc.presence[this.connection2.id]).to.eql(p(1));
done();
}.bind(this));
- this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done));
+ this.doc.submitOp({index: 1, value: 'b'}, errorHandler(done));
}.bind(this)
], allDone);
});
it('transforms presence against non-local op (presence.index == op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a', 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['a', 'c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(1)),
@@ -428,89 +456,93 @@ types.register(presenceType.type3);
setTimeout,
function(done) {
this.doc.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection2.id ]);
+ expect(srcList).to.eql([this.connection2.id]);
expect(submitted).to.equal(false);
expect(this.doc.presence['']).to.eql(p(1));
expect(this.doc.presence[this.connection2.id]).to.eql(p(2));
done();
}.bind(this));
- this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done));
+ this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done));
}.bind(this)
], allDone);
});
it('caches local ops', function(allDone) {
- var op = { index: 1, value: 'b' };
+ var op = {index: 1, value: 'b'};
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.submitOp.bind(this.doc, op),
this.doc.del.bind(this.doc),
function(done) {
- expect(this.doc.cachedOps.length).to.equal(3);
- expect(this.doc.cachedOps[0].create).to.equal(true);
- expect(this.doc.cachedOps[1].op).to.equal(op);
- expect(this.doc.cachedOps[2].del).to.equal(true);
+ expect(this.doc._docPresence.cachedOps.length).to.equal(3);
+ expect(this.doc._docPresence.cachedOps[0].create).to.equal(true);
+ expect(this.doc._docPresence.cachedOps[1].op).to.equal(op);
+ expect(this.doc._docPresence.cachedOps[2].del).to.equal(true);
done();
}.bind(this)
], allDone);
});
it('caches non-local ops', function(allDone) {
- var op = { index: 1, value: 'b' };
+ var op = {index: 1, value: 'b'};
async.series([
this.doc2.subscribe.bind(this.doc2),
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.submitOp.bind(this.doc, op),
this.doc.del.bind(this.doc),
setTimeout,
function(done) {
- expect(this.doc2.cachedOps.length).to.equal(3);
- expect(this.doc2.cachedOps[0].create).to.equal(true);
- expect(this.doc2.cachedOps[1].op).to.eql(op);
- expect(this.doc2.cachedOps[2].del).to.equal(true);
+ expect(this.doc2._docPresence.cachedOps.length).to.equal(3);
+ expect(this.doc2._docPresence.cachedOps[0].create).to.equal(true);
+ expect(this.doc2._docPresence.cachedOps[1].op).to.eql(op);
+ expect(this.doc2._docPresence.cachedOps[2].del).to.equal(true);
done();
}.bind(this)
], allDone);
});
it('expires cached ops', function(allDone) {
- var op1 = { index: 1, value: 'b' };
- var op2 = { index: 2, value: 'b' };
- var op3 = { index: 3, value: 'b' };
- this.doc.cachedOpsTimeout = 60;
+ var clock = lolex.install();
+ var op1 = {index: 1, value: 'b'};
+ var op2 = {index: 2, value: 'b'};
+ var op3 = {index: 3, value: 'b'};
+ this.doc._docPresence.cachedOpsTimeout = 60;
async.series([
// Cache 2 ops.
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.submitOp.bind(this.doc, op1),
function(done) {
- expect(this.doc.cachedOps.length).to.equal(2);
- expect(this.doc.cachedOps[0].create).to.equal(true);
- expect(this.doc.cachedOps[1].op).to.equal(op1);
+ expect(this.doc._docPresence.cachedOps.length).to.equal(2);
+ expect(this.doc._docPresence.cachedOps[0].create).to.equal(true);
+ expect(this.doc._docPresence.cachedOps[1].op).to.equal(op1);
done();
}.bind(this),
// Cache another op before the first 2 expire.
- function (callback) {
+ function(callback) {
setTimeout(callback, 30);
+ clock.next();
},
this.doc.submitOp.bind(this.doc, op2),
function(done) {
- expect(this.doc.cachedOps.length).to.equal(3);
- expect(this.doc.cachedOps[0].create).to.equal(true);
- expect(this.doc.cachedOps[1].op).to.equal(op1);
- expect(this.doc.cachedOps[2].op).to.equal(op2);
+ expect(this.doc._docPresence.cachedOps.length).to.equal(3);
+ expect(this.doc._docPresence.cachedOps[0].create).to.equal(true);
+ expect(this.doc._docPresence.cachedOps[1].op).to.equal(op1);
+ expect(this.doc._docPresence.cachedOps[2].op).to.equal(op2);
done();
}.bind(this),
// Cache another op after the first 2 expire.
- function (callback) {
+ function(callback) {
setTimeout(callback, 31);
+ clock.next();
},
this.doc.submitOp.bind(this.doc, op3),
function(done) {
- expect(this.doc.cachedOps.length).to.equal(2);
- expect(this.doc.cachedOps[0].op).to.equal(op2);
- expect(this.doc.cachedOps[1].op).to.equal(op3);
+ expect(this.doc._docPresence.cachedOps.length).to.equal(2);
+ expect(this.doc._docPresence.cachedOps[0].op).to.equal(op2);
+ expect(this.doc._docPresence.cachedOps[1].op).to.equal(op3);
+ clock.uninstall();
done();
}.bind(this)
], allDone);
@@ -518,22 +550,22 @@ types.register(presenceType.type3);
it('requests reply presence when sending presence for the first time', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc.submitPresence.bind(this.doc, p(0)),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
if (srcList[0] === '') {
- expect(srcList).to.eql([ '' ]);
+ expect(srcList).to.eql(['']);
expect(submitted).to.equal(true);
expect(this.doc2.presence['']).to.eql(p(1));
expect(this.doc2.presence).to.not.have.key(this.connection.id);
} else {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(this.doc2.presence['']).to.eql(p(1));
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
- expect(this.doc2.requestReplyPresence).to.equal(false);
+ expect(this.doc2._docPresence.requestReply).to.equal(false);
done();
}
}.bind(this));
@@ -576,7 +608,7 @@ types.register(presenceType.type3);
function(done) {
this.doc.submitPresence(p(0), function(err) {
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4024);
+ expect(err.code).to.equal(4028);
done();
});
}.bind(this)
@@ -590,7 +622,7 @@ types.register(presenceType.type3);
function(done) {
this.doc.on('error', function(err) {
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4024);
+ expect(err.code).to.equal(4028);
done();
});
this.doc.submitPresence(p(0));
@@ -607,17 +639,17 @@ types.register(presenceType.type3);
it('sends presence once, if submitted multiple times synchronously', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(2));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(0), errorHandler(done));
this.doc.submitPresence(p(1), errorHandler(done));
this.doc.submitPresence(p(2), errorHandler(done));
@@ -627,16 +659,16 @@ types.register(presenceType.type3);
it('buffers presence until subscribed', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
setTimeout(function() {
this.doc.subscribe(function(err) {
@@ -650,12 +682,12 @@ types.register(presenceType.type3);
it('buffers presence when disconnected', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
done();
@@ -664,7 +696,7 @@ types.register(presenceType.type3);
this.doc.submitPresence(p(1), errorHandler(done));
process.nextTick(function() {
this.backend.connect(this.connection);
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
}.bind(this));
}.bind(this)
], allDone);
@@ -672,17 +704,17 @@ types.register(presenceType.type3);
it('submits presence without a callback', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(0));
}.bind(this)
], allDone);
@@ -690,20 +722,20 @@ types.register(presenceType.type3);
it('hasPending is true, if there is pending presence', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
function(done) {
expect(this.doc.hasPending()).to.equal(false);
this.doc.submitPresence(p(0));
expect(this.doc.hasPending()).to.equal(true);
- expect(!!this.doc.pendingPresence).to.equal(true);
- expect(!!this.doc.inflightPresence).to.equal(false);
+ expect(!!this.doc._docPresence.pending).to.equal(true);
+ expect(!!this.doc._docPresence.inflight).to.equal(false);
this.doc.whenNothingPending(done);
}.bind(this),
function(done) {
expect(this.doc.hasPending()).to.equal(false);
- expect(!!this.doc.pendingPresence).to.equal(false);
- expect(!!this.doc.inflightPresence).to.equal(false);
+ expect(!!this.doc._docPresence.pending).to.equal(false);
+ expect(!!this.doc._docPresence.inflight).to.equal(false);
done();
}.bind(this)
], allDone);
@@ -711,26 +743,26 @@ types.register(presenceType.type3);
it('hasPending is true, if there is inflight presence', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
function(done) {
expect(this.doc.hasPending()).to.equal(false);
this.doc.submitPresence(p(0));
expect(this.doc.hasPending()).to.equal(true);
- expect(!!this.doc.pendingPresence).to.equal(true);
- expect(!!this.doc.inflightPresence).to.equal(false);
+ expect(!!this.doc._docPresence.pending).to.equal(true);
+ expect(!!this.doc._docPresence.inflight).to.equal(false);
process.nextTick(done);
}.bind(this),
function(done) {
expect(this.doc.hasPending()).to.equal(true);
- expect(!!this.doc.pendingPresence).to.equal(false);
- expect(!!this.doc.inflightPresence).to.equal(true);
+ expect(!!this.doc._docPresence.pending).to.equal(false);
+ expect(!!this.doc._docPresence.inflight).to.equal(true);
this.doc.whenNothingPending(done);
}.bind(this),
function(done) {
expect(this.doc.hasPending()).to.equal(false);
- expect(!!this.doc.pendingPresence).to.equal(false);
- expect(!!this.doc.inflightPresence).to.equal(false);
+ expect(!!this.doc._docPresence.pending).to.equal(false);
+ expect(!!this.doc._docPresence.inflight).to.equal(false);
done();
}.bind(this)
], allDone);
@@ -738,7 +770,7 @@ types.register(presenceType.type3);
it('receives presence after doc is deleted', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -746,14 +778,14 @@ types.register(presenceType.type3);
function(done) {
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
// The call to `del` transforms the presence and fires the event.
// The call to `submitPresence` does not fire the event because presence is already null.
expect(submitted).to.equal(false);
expect(this.doc2.presence).to.not.have.key(this.connection.id);
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
this.doc2.del(errorHandler(done));
}.bind(this)
@@ -762,7 +794,7 @@ types.register(presenceType.type3);
it('clears peer presence on peer disconnection', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -776,7 +808,7 @@ types.register(presenceType.type3);
var connectionId = this.connection.id;
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ connectionId ]);
+ expect(srcList).to.eql([connectionId]);
expect(submitted).to.equal(true);
expect(this.doc2.presence).to.not.have.key(connectionId);
expect(this.doc2.presence['']).to.eql(p(1));
@@ -789,7 +821,7 @@ types.register(presenceType.type3);
it('clears peer presence on own disconnection', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -803,7 +835,7 @@ types.register(presenceType.type3);
var connectionId = this.connection.id;
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ connectionId ]);
+ expect(srcList).to.eql([connectionId]);
expect(submitted).to.equal(false);
expect(this.doc2.presence).to.not.have.key(connectionId);
expect(this.doc2.presence['']).to.eql(p(1));
@@ -816,7 +848,7 @@ types.register(presenceType.type3);
it('clears peer presence on peer unsubscribe', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -830,7 +862,7 @@ types.register(presenceType.type3);
var connectionId = this.connection.id;
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ connectionId ]);
+ expect(srcList).to.eql([connectionId]);
expect(submitted).to.equal(true);
expect(this.doc2.presence).to.not.have.key(connectionId);
expect(this.doc2.presence['']).to.eql(p(1));
@@ -843,7 +875,7 @@ types.register(presenceType.type3);
it('clears peer presence on own unsubscribe', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -857,7 +889,7 @@ types.register(presenceType.type3);
var connectionId = this.connection.id;
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ connectionId ]);
+ expect(srcList).to.eql([connectionId]);
expect(submitted).to.equal(false);
expect(this.doc2.presence).to.not.have.key(connectionId);
expect(this.doc2.presence['']).to.eql(p(1));
@@ -870,7 +902,7 @@ types.register(presenceType.type3);
it('pauses inflight and pending presence on disconnect', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
function(done) {
var called = 0;
@@ -892,7 +924,7 @@ types.register(presenceType.type3);
it('pauses inflight and pending presence on unsubscribe', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
function(done) {
var called = 0;
@@ -914,7 +946,7 @@ types.register(presenceType.type3);
it('re-synchronizes presence after reconnecting', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -940,7 +972,7 @@ types.register(presenceType.type3);
it('re-synchronizes presence after resubscribing', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -963,93 +995,93 @@ types.register(presenceType.type3);
], allDone);
});
- it('transforms received presence against inflight and pending ops (presence.index < op.index)', function(allDone) {
+ it('transforms received presence against inflight/pending ops (presence.index < op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(0));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(0), errorHandler(done));
- this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done))
- this.doc2.submitOp({ index: 2, value: 'c' }, errorHandler(done))
+ this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done));
+ this.doc2.submitOp({index: 2, value: 'c'}, errorHandler(done));
}.bind(this)
], allDone);
});
- it('transforms received presence against inflight and pending ops (presence.index === op.index)', function(allDone) {
+ it('transforms received presence against inflight/pending ops (presence.index === op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
- this.doc2.submitOp({ index: 1, value: 'c' }, errorHandler(done))
- this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done))
+ this.doc2.submitOp({index: 1, value: 'c'}, errorHandler(done));
+ this.doc2.submitOp({index: 1, value: 'b'}, errorHandler(done));
}.bind(this)
], allDone);
});
- it('transforms received presence against inflight and pending ops (presence.index > op.index)', function(allDone) {
+ it('transforms received presence against inflight/pending ops (presence.index > op.index)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(3));
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(1), errorHandler(done));
- this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done))
- this.doc2.submitOp({ index: 0, value: 'a' }, errorHandler(done))
+ this.doc2.submitOp({index: 0, value: 'b'}, errorHandler(done));
+ this.doc2.submitOp({index: 0, value: 'a'}, errorHandler(done));
}.bind(this)
], allDone);
});
it('transforms received presence against inflight delete', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(1)),
setTimeout,
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
// The call to `del` transforms the presence and fires the event.
// The call to `submitPresence` does not fire the event because presence is already null.
expect(submitted).to.equal(false);
expect(this.doc2.presence).to.not.have.key(this.connection.id);
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(2), errorHandler(done));
this.doc2.del(errorHandler(done));
- this.doc2.create([ 'c' ], typeName, errorHandler(done));
+ this.doc2.create(['c'], typeName, errorHandler(done));
}.bind(this)
], allDone);
});
it('transforms received presence against a pending delete', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(1)),
@@ -1058,31 +1090,31 @@ types.register(presenceType.type3);
var firstCall = true;
this.doc2.on('presence', function(srcList, submitted) {
if (firstCall) return firstCall = false;
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
// The call to `del` transforms the presence and fires the event.
// The call to `submitPresence` does not fire the event because presence is already null.
expect(submitted).to.equal(false);
expect(this.doc2.presence).to.not.have.key(this.connection.id);
done();
}.bind(this));
- this.doc.requestReplyPresence = false;
+ this.doc._docPresence.requestReply = false;
this.doc.submitPresence(p(2), errorHandler(done));
- this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done));
+ this.doc2.submitOp({index: 0, value: 'b'}, errorHandler(done));
this.doc2.del(errorHandler(done));
- this.doc2.create([ 'c' ], typeName, errorHandler(done));
+ this.doc2.create(['c'], typeName, errorHandler(done));
}.bind(this)
], allDone);
});
it('emits the same presence only if comparePresence is not implemented (local presence)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc.submitPresence.bind(this.doc, p(1)),
function(done) {
this.doc.on('presence', function(srcList, submitted) {
if (typeName === 'wrapped-presence-no-compare') {
- expect(srcList).to.eql([ '' ]);
+ expect(srcList).to.eql(['']);
expect(submitted).to.equal(true);
expect(this.doc.presence['']).to.eql(p(1));
done();
@@ -1097,7 +1129,7 @@ types.register(presenceType.type3);
it('emits the same presence only if comparePresence is not implemented (non-local presence)', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(1)),
@@ -1105,7 +1137,7 @@ types.register(presenceType.type3);
function(done) {
this.doc2.on('presence', function(srcList, submitted) {
if (typeName === 'wrapped-presence-no-compare') {
- expect(srcList).to.eql([ this.connection.id ]);
+ expect(srcList).to.eql([this.connection.id]);
expect(submitted).to.equal(true);
expect(this.doc2.presence[this.connection.id]).to.eql(p(1));
done();
@@ -1120,7 +1152,7 @@ types.register(presenceType.type3);
it('returns an error when not subscribed on the server', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
function(done) {
this.connection.sendUnsubscribe(this.doc);
@@ -1130,16 +1162,16 @@ types.register(presenceType.type3);
this.doc.on('error', done);
this.doc.submitPresence(p(0), function(err) {
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4025);
+ expect(err.code).to.equal(4026);
done();
- }.bind(this));
+ });
}.bind(this)
], allDone);
});
it('emits an error when not subscribed on the server and no callback is provided', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
function(done) {
this.connection.sendUnsubscribe(this.doc);
@@ -1148,9 +1180,9 @@ types.register(presenceType.type3);
function(done) {
this.doc.on('error', function(err) {
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4025);
+ expect(err.code).to.equal(4026);
done();
- }.bind(this));
+ });
this.doc.submitPresence(p(0));
}.bind(this)
], allDone);
@@ -1158,7 +1190,7 @@ types.register(presenceType.type3);
it('returns an error when the server gets an old sequence number', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc.submitPresence.bind(this.doc, p(0)),
setTimeout,
@@ -1167,25 +1199,25 @@ types.register(presenceType.type3);
this.connection.seq--;
this.doc.submitPresence(p(1), function(err) {
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4026);
+ expect(err.code).to.equal(4027);
done();
- }.bind(this));
+ });
}.bind(this)
], allDone);
});
it('emits an error when the server gets an old sequence number and no callback is provided', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc.submitPresence.bind(this.doc, p(0)),
setTimeout,
function(done) {
this.doc.on('error', function(err) {
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4026);
+ expect(err.code).to.equal(4027);
done();
- }.bind(this));
+ });
this.connection.seq--;
this.doc.submitPresence(p(1));
}.bind(this)
@@ -1194,7 +1226,7 @@ types.register(presenceType.type3);
it('does not publish presence unnecessarily', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc.submitPresence.bind(this.doc, p(0)),
setTimeout,
@@ -1207,19 +1239,19 @@ types.register(presenceType.type3);
if (typeName === 'wrapped-presence-no-compare') {
// The OT type does not support comparing presence.
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4026);
+ expect(err.code).to.equal(4027);
} else {
expect(err).to.not.be.ok();
}
done();
- }.bind(this));
+ });
}.bind(this)
], allDone);
});
it('does not publish presence unnecessarily when no callback is provided', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc.submitPresence.bind(this.doc, p(0)),
setTimeout,
@@ -1228,12 +1260,12 @@ types.register(presenceType.type3);
if (typeName === 'wrapped-presence-no-compare') {
// The OT type does not support comparing presence.
expect(err).to.be.an(Error);
- expect(err.code).to.equal(4026);
+ expect(err.code).to.equal(4027);
done();
} else {
done(err);
}
- }.bind(this));
+ });
// Decremented sequence number would cause the server to return an error, however,
// the message won't be sent to the server at all because the presence data has not changed.
this.connection.seq--;
@@ -1247,7 +1279,7 @@ types.register(presenceType.type3);
it('returns an error when publishing presence fails', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
setTimeout,
function(done) {
@@ -1263,14 +1295,14 @@ types.register(presenceType.type3);
expect(err).to.be.an(Error);
expect(err.code).to.equal(-1);
done();
- }.bind(this));
+ });
}.bind(this)
], allDone);
});
it('emits an error when publishing presence fails and no callback is provided', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['c'], typeName),
this.doc.subscribe.bind(this.doc),
setTimeout,
function(done) {
@@ -1285,7 +1317,7 @@ types.register(presenceType.type3);
expect(err).to.be.an(Error);
expect(err.code).to.equal(-1);
done();
- }.bind(this));
+ });
this.doc.submitPresence(p(0));
}.bind(this)
], allDone);
@@ -1293,7 +1325,7 @@ types.register(presenceType.type3);
it('clears presence on hard rollback and emits an error', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a', 'b', 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['a', 'b', 'c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -1310,16 +1342,16 @@ types.register(presenceType.type3);
};
process.nextTick(done);
}.bind(this),
- this.doc.submitPresence.bind(this.doc, p(1)), // inflightPresence
+ this.doc.submitPresence.bind(this.doc, p(1)), // presence.inflight
process.nextTick, // wait for "presence" event
- this.doc.submitPresence.bind(this.doc, p(2)), // pendingPresence
+ this.doc.submitPresence.bind(this.doc, p(2)), // presence.pending
process.nextTick, // wait for "presence" event
function(done) {
var presenceEmitted = false;
this.doc.on('presence', function(srcList, submitted) {
expect(presenceEmitted).to.equal(false);
presenceEmitted = true;
- expect(srcList.sort()).to.eql([ '', this.connection2.id ]);
+ expect(srcList.sort()).to.eql(['', this.connection2.id]);
expect(submitted).to.equal(false);
expect(this.doc.presence).to.not.have.key('');
expect(this.doc.presence).to.not.have.key(this.connection2.id);
@@ -1330,7 +1362,7 @@ types.register(presenceType.type3);
expect(err).to.be.an(Error);
expect(err.code).to.equal(4000);
done();
- }.bind(this));
+ });
// send an invalid op
this.doc._submit({}, null);
@@ -1340,7 +1372,7 @@ types.register(presenceType.type3);
it('clears presence on hard rollback and executes all callbacks', function(allDone) {
async.series([
- this.doc.create.bind(this.doc, [ 'a', 'b', 'c' ], typeName),
+ this.doc.create.bind(this.doc, ['a', 'b', 'c'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
this.doc.submitPresence.bind(this.doc, p(0)),
@@ -1367,14 +1399,14 @@ types.register(presenceType.type3);
if (++called < 3) return;
done();
}
- this.doc.submitPresence(p(1), callback); // inflightPresence
+ this.doc.submitPresence(p(1), callback); // presence.inflight
process.nextTick(function() { // wait for presence event
- this.doc.submitPresence(p(2), callback); // pendingPresence
+ this.doc.submitPresence(p(2), callback); // presence.pending
process.nextTick(function() { // wait for presence event
this.doc.on('presence', function(srcList, submitted) {
expect(presenceEmitted).to.equal(false);
presenceEmitted = true;
- expect(srcList.sort()).to.eql([ '', this.connection2.id ]);
+ expect(srcList.sort()).to.eql(['', this.connection2.id]);
expect(submitted).to.equal(false);
expect(this.doc.presence).to.not.have.key('');
expect(this.doc.presence).to.not.have.key(this.connection2.id);
@@ -1382,7 +1414,7 @@ types.register(presenceType.type3);
this.doc.on('error', done);
// send an invalid op
- this.doc._submit({ index: 3, value: 'b' }, null, callback);
+ this.doc._submit({index: 3, value: 'b'}, null, callback);
}.bind(this));
}.bind(this));
}.bind(this)
@@ -1400,21 +1432,21 @@ types.register(presenceType.type3);
return handleMessage.apply(this, arguments);
};
if (expireCache) {
- this.doc.receivedPresenceTimeout = 0;
+ this.doc._docPresence.receivedTimeout = 0;
}
async.series([
- this.doc.create.bind(this.doc, [ 'a' ], typeName),
+ this.doc.create.bind(this.doc, ['a'], typeName),
this.doc.subscribe.bind(this.doc),
this.doc2.subscribe.bind(this.doc2),
function(done) {
- this.doc2.requestReplyPresence = false;
+ this.doc2._docPresence.requestReply = false;
this.doc2.submitPresence(p(0), done);
}.bind(this),
setTimeout,
- this.doc2.submitOp.bind(this.doc2, { index: 1, value: 'b' }), // forces processing of all received presence
+ this.doc2.submitOp.bind(this.doc2, {index: 1, value: 'b'}), // forces processing of all received presence
setTimeout,
function(done) {
- expect(this.doc.data).to.eql([ 'a', 'b' ]);
+ expect(this.doc.data).to.eql(['a', 'b']);
expect(this.doc.presence[this.connection2.id]).to.eql(p(0));
// Replay the `lastPresence` with modified payload.
lastPresence.p = p(1);
@@ -1433,9 +1465,28 @@ types.register(presenceType.type3);
};
}
- it('ignores an old message (cache not expired, presence.seq === cachedPresence.seq)', testReceivedMessageExpiry(false, false));
- it('ignores an old message (cache not expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(false, true));
- it('processes an old message (cache expired, presence.seq === cachedPresence.seq)', testReceivedMessageExpiry(true, false));
- it('processes an old message (cache expired, presence.seq < cachedPresence.seq)', testReceivedMessageExpiry(true, true));
+ it('ignores an old message (cache not expired, presence.seq === cachedPresence.seq)',
+ testReceivedMessageExpiry(false, false));
+
+ it('ignores an old message (cache not expired, presence.seq < cachedPresence.seq)',
+ testReceivedMessageExpiry(false, true));
+
+ it('processes an old message (cache expired, presence.seq === cachedPresence.seq)',
+ testReceivedMessageExpiry(true, false));
+
+ it('processes an old message (cache expired, presence.seq < cachedPresence.seq)',
+ testReceivedMessageExpiry(true, true));
+
+ it('invokes presence.destroy inside doc.destroy', function(done) {
+ var presence = this.doc._docPresence;
+ presence.cachedOps = ['foo'];
+ presence.received = {bar: true};
+ this.doc.destroy(function(err) {
+ if (err) return done(err);
+ expect(presence.cachedOps).to.eql([]);
+ expect(presence.received).to.eql({});
+ done();
+ });
+ });
});
});
diff --git a/test/client/projections.js b/test/client/projections.js
index fb1c993e5..77359563e 100644
--- a/test/client/projections.js
+++ b/test/client/projections.js
@@ -2,328 +2,331 @@ var expect = require('expect.js');
var util = require('../util');
module.exports = function() {
-describe('client projections', function() {
-
- beforeEach(function(done) {
- this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true});
- this.connection = this.backend.connect();
- var data = {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}};
- this.connection.get('dogs', 'fido').create(data, done);
- });
+ describe('client projections', function() {
+ beforeEach(function(done) {
+ this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true});
+ this.connection = this.backend.connect();
+ var data = {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}};
+ this.connection.get('dogs', 'fido').create(data, done);
+ });
- ['fetch', 'subscribe'].forEach(function(method) {
- it('snapshot ' + method, function(done) {
- var connection2 = this.backend.connect();
- var fido = connection2.get('dogs_summary', 'fido');
- fido[method](function(err) {
- if (err) return done(err);
- expect(fido.data).eql({age: 3, owner: {name: 'jim'}});
- expect(fido.version).eql(1);
- done();
+ ['fetch', 'subscribe'].forEach(function(method) {
+ it('snapshot ' + method, function(done) {
+ var connection2 = this.backend.connect();
+ var fido = connection2.get('dogs_summary', 'fido');
+ fido[method](function(err) {
+ if (err) return done(err);
+ expect(fido.data).eql({age: 3, owner: {name: 'jim'}});
+ expect(fido.version).eql(1);
+ done();
+ });
});
});
- });
- ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
- it('snapshot ' + method, function(done) {
- var connection2 = this.backend.connect();
- connection2[method]('dogs_summary', {}, null, function(err, results) {
- if (err) return done(err);
- expect(results.length).eql(1);
- expect(results[0].data).eql({age: 3, owner: {name: 'jim'}});
- expect(results[0].version).eql(1);
- done();
+ ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
+ it('snapshot ' + method, function(done) {
+ var connection2 = this.backend.connect();
+ connection2[method]('dogs_summary', {}, null, function(err, results) {
+ if (err) return done(err);
+ expect(results.length).eql(1);
+ expect(results[0].data).eql({age: 3, owner: {name: 'jim'}});
+ expect(results[0].version).eql(1);
+ done();
+ });
});
});
- });
- function opTests(test) {
- it('projected field', function(done) {
- test.call(this,
- {p: ['age'], na: 1},
- {age: 4, owner: {name: 'jim'}},
- done
- );
- });
+ function opTests(test) {
+ it('projected field', function(done) {
+ test.call(this,
+ {p: ['age'], na: 1},
+ {age: 4, owner: {name: 'jim'}},
+ done
+ );
+ });
- it('non-projected field', function(done) {
- test.call(this,
- {p: ['color'], oi: 'brown', od: 'gold'},
- {age: 3, owner: {name: 'jim'}},
- done
- );
- });
+ it('non-projected field', function(done) {
+ test.call(this,
+ {p: ['color'], oi: 'brown', od: 'gold'},
+ {age: 3, owner: {name: 'jim'}},
+ done
+ );
+ });
- it('parent field replace', function(done) {
- test.call(this,
- {p: [], oi: {age: 2, color: 'brown', owner: false}, od: {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}},
- {age: 2, owner: false},
- done
- );
- });
+ it('parent field replace', function(done) {
+ test.call(this,
+ {
+ p: [],
+ oi: {age: 2, color: 'brown', owner: false},
+ od: {age: 3, color: 'gold', owner: {name: 'jim'},
+ litter: {count: 4}}
+ },
+ {age: 2, owner: false},
+ done
+ );
+ });
- it('parent field set', function(done) {
- test.call(this,
- {p: [], oi: {age: 2, color: 'brown', owner: false}},
- {age: 2, owner: false},
- done
- );
- });
+ it('parent field set', function(done) {
+ test.call(this,
+ {p: [], oi: {age: 2, color: 'brown', owner: false}},
+ {age: 2, owner: false},
+ done
+ );
+ });
- it('projected child field', function(done) {
- test.call(this,
- {p: ['owner', 'sex'], oi: 'male'},
- {age: 3, owner: {name: 'jim', sex: 'male'}},
- done
- );
- });
+ it('projected child field', function(done) {
+ test.call(this,
+ {p: ['owner', 'sex'], oi: 'male'},
+ {age: 3, owner: {name: 'jim', sex: 'male'}},
+ done
+ );
+ });
- it('non-projected child field', function(done) {
- test.call(this,
- {p: ['litter', 'count'], na: 1},
- {age: 3, owner: {name: 'jim'}},
- done
- );
- });
- }
+ it('non-projected child field', function(done) {
+ test.call(this,
+ {p: ['litter', 'count'], na: 1},
+ {age: 3, owner: {name: 'jim'}},
+ done
+ );
+ });
+ }
- describe('op fetch', function() {
- function test(op, expected, done) {
- var connection = this.connection;
- var connection2 = this.backend.connect();
- var fido = connection2.get('dogs_summary', 'fido');
- fido.fetch(function(err) {
- if (err) return done(err);
- connection.get('dogs', 'fido').submitOp(op, function(err) {
+ describe('op fetch', function() {
+ function test(op, expected, done) {
+ var connection = this.connection;
+ var connection2 = this.backend.connect();
+ var fido = connection2.get('dogs_summary', 'fido');
+ fido.fetch(function(err) {
if (err) return done(err);
- fido.fetch(function(err) {
+ connection.get('dogs', 'fido').submitOp(op, function(err) {
if (err) return done(err);
+ fido.fetch(function(err) {
+ if (err) return done(err);
+ expect(fido.data).eql(expected);
+ expect(fido.version).eql(2);
+ done();
+ });
+ });
+ });
+ };
+ opTests(test);
+ });
+
+ describe('op subscribe', function() {
+ function test(op, expected, done) {
+ var connection = this.connection;
+ var connection2 = this.backend.connect();
+ var fido = connection2.get('dogs_summary', 'fido');
+ fido.subscribe(function(err) {
+ if (err) return done(err);
+ fido.on('op', function() {
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
});
+ connection.get('dogs', 'fido').submitOp(op);
});
- });
- };
- opTests(test);
- });
+ };
+ opTests(test);
+ });
- describe('op subscribe', function() {
- function test(op, expected, done) {
- var connection = this.connection;
- var connection2 = this.backend.connect();
- var fido = connection2.get('dogs_summary', 'fido');
- fido.subscribe(function(err) {
- if (err) return done(err);
- fido.on('op', function() {
- expect(fido.data).eql(expected);
- expect(fido.version).eql(2);
- done();
+ describe('op fetch query', function() {
+ function test(op, expected, done) {
+ var connection = this.connection;
+ var connection2 = this.backend.connect();
+ var fido = connection2.get('dogs_summary', 'fido');
+ fido.fetch(function(err) {
+ if (err) return done(err);
+ connection.get('dogs', 'fido').submitOp(op, function(err) {
+ if (err) return done(err);
+ connection2.createFetchQuery('dogs_summary', {}, null, function(err) {
+ if (err) return done(err);
+ expect(fido.data).eql(expected);
+ expect(fido.version).eql(2);
+ done();
+ });
+ });
});
- connection.get('dogs', 'fido').submitOp(op);
- });
- };
- opTests(test);
- });
+ };
+ opTests(test);
+ });
- describe('op fetch query', function() {
- function test(op, expected, done) {
- var connection = this.connection;
- var connection2 = this.backend.connect();
- var fido = connection2.get('dogs_summary', 'fido');
- fido.fetch(function(err) {
- if (err) return done(err);
- connection.get('dogs', 'fido').submitOp(op, function(err) {
+ describe('op subscribe query', function() {
+ function test(op, expected, done) {
+ var connection = this.connection;
+ var connection2 = this.backend.connect();
+ var fido = connection2.get('dogs_summary', 'fido');
+ connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
if (err) return done(err);
- connection2.createFetchQuery('dogs_summary', {}, null, function(err) {
- if (err) return done(err);
+ fido.on('op', function() {
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
});
+ connection.get('dogs', 'fido').submitOp(op);
});
+ };
+ opTests(test);
+ });
+
+ function queryUpdateTests(test) {
+ it('doc create', function(done) {
+ test.call(this,
+ function(connection, callback) {
+ var data = {age: 5, color: 'spotted', owner: {name: 'sue'}, litter: {count: 6}};
+ connection.get('dogs', 'spot').create(data, callback);
+ },
+ function(err, results) {
+ var sorted = util.sortById(results.slice());
+ expect(sorted.length).eql(2);
+ expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
+ expect(util.pluck(sorted, 'data')).eql([
+ {age: 3, owner: {name: 'jim'}},
+ {age: 5, owner: {name: 'sue'}}
+ ]);
+ done();
+ }
+ );
});
- };
- opTests(test);
- });
+ }
- describe('op subscribe query', function() {
- function test(op, expected, done) {
- var connection = this.connection;
- var connection2 = this.backend.connect();
- var fido = connection2.get('dogs_summary', 'fido');
- connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
- if (err) return done(err);
- fido.on('op', function() {
- expect(fido.data).eql(expected);
- expect(fido.version).eql(2);
- done();
+ describe('subscribe query', function() {
+ function test(trigger, callback) {
+ var connection = this.connection;
+ var connection2 = this.backend.connect();
+ var query = connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
+ if (err) return callback(err);
+ query.on('insert', function() {
+ callback(null, query.results);
+ });
+ trigger(connection);
});
- connection.get('dogs', 'fido').submitOp(op);
- });
- };
- opTests(test);
- });
+ }
+ queryUpdateTests(test);
+ });
- function queryUpdateTests(test) {
- it('doc create', function(done) {
- test.call(this,
- function(connection, callback) {
- var data = {age: 5, color: 'spotted', owner: {name: 'sue'}, litter: {count: 6}};
- connection.get('dogs', 'spot').create(data, callback);
- },
- function(err, results) {
- var sorted = util.sortById(results.slice());
- expect(sorted.length).eql(2);
- expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
- expect(util.pluck(sorted, 'data')).eql([
- {age: 3, owner: {name: 'jim'}},
- {age: 5, owner: {name: 'sue'}}
- ]);
- done();
- }
- );
+ describe('fetch query', function() {
+ function test(trigger, callback) {
+ var connection = this.connection;
+ var connection2 = this.backend.connect();
+ trigger(connection, function(err) {
+ if (err) return callback(err);
+ connection2.createFetchQuery('dogs_summary', {}, null, callback);
+ });
+ }
+ queryUpdateTests(test);
});
- }
- describe('subscribe query', function() {
- function test(trigger, callback) {
- var connection = this.connection;
- var connection2 = this.backend.connect();
- var query = connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
- if (err) return callback(err);
- query.on('insert', function() {
- callback(null, query.results);
+ describe('submit on projected doc', function() {
+ function test(op, expected, done) {
+ var doc = this.connection.get('dogs', 'fido');
+ var projected = this.backend.connect().get('dogs_summary', 'fido');
+ projected.fetch(function(err) {
+ if (err) return done(err);
+ projected.submitOp(op, function(err) {
+ if (err) return done(err);
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql(expected);
+ expect(doc.version).equal(2);
+ done();
+ });
+ });
});
- trigger(connection);
+ }
+ function testError(op, done) {
+ var doc = this.connection.get('dogs', 'fido');
+ var projected = this.backend.connect().get('dogs_summary', 'fido');
+ projected.fetch(function(err) {
+ if (err) return done(err);
+ projected.submitOp(op, function(err) {
+ expect(err).ok();
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}});
+ expect(doc.version).equal(1);
+ done();
+ });
+ });
+ });
+ }
+
+ it('can set on projected field', function(done) {
+ test.call(this,
+ {p: ['age'], na: 1},
+ {age: 4, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}},
+ done
+ );
});
- }
- queryUpdateTests(test);
- });
- describe('fetch query', function() {
- function test(trigger, callback) {
- var connection = this.connection;
- var connection2 = this.backend.connect();
- trigger(connection, function(err) {
- if (err) return callback(err);
- connection2.createFetchQuery('dogs_summary', {}, null, callback);
+ it('can set on child of projected field', function(done) {
+ test.call(this,
+ {p: ['owner', 'sex'], oi: 'male'},
+ {age: 3, color: 'gold', owner: {name: 'jim', sex: 'male'}, litter: {count: 4}},
+ done
+ );
+ });
+
+ it('cannot set on non-projected field', function(done) {
+ testError.call(this,
+ {p: ['color'], od: 'gold', oi: 'tan'},
+ done
+ );
+ });
+
+ it('cannot set on root path of projected doc', function(done) {
+ testError.call(this,
+ {p: [], oi: null},
+ done
+ );
});
- }
- queryUpdateTests(test);
- });
- describe('submit on projected doc', function() {
- function test(op, expected, done) {
- var doc = this.connection.get('dogs', 'fido');
- var projected = this.backend.connect().get('dogs_summary', 'fido');
- projected.fetch(function(err) {
- if (err) return done(err);
- projected.submitOp(op, function(err) {
+ it('can delete on projected doc', function(done) {
+ var doc = this.connection.get('dogs', 'fido');
+ var projected = this.backend.connect().get('dogs_summary', 'fido');
+ projected.fetch(function(err) {
if (err) return done(err);
- doc.fetch(function(err) {
+ projected.del(function(err) {
if (err) return done(err);
- expect(doc.data).eql(expected);
- expect(doc.version).equal(2);
- done();
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql(undefined);
+ expect(doc.version).equal(2);
+ done();
+ });
});
});
});
- }
- function testError(op, done) {
- var doc = this.connection.get('dogs', 'fido');
- var projected = this.backend.connect().get('dogs_summary', 'fido');
- projected.fetch(function(err) {
- if (err) return done(err);
- projected.submitOp(op, function(err) {
- expect(err).ok();
+
+ it('can create a projected doc with only projected fields', function(done) {
+ var doc = this.connection.get('dogs', 'spot');
+ var projected = this.backend.connect().get('dogs_summary', 'spot');
+ var data = {age: 5};
+ projected.create(data, function(err) {
+ if (err) return done(err);
doc.fetch(function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}});
+ expect(doc.data).eql({age: 5});
expect(doc.version).equal(1);
done();
});
});
});
- }
-
- it('can set on projected field', function(done) {
- test.call(this,
- {p: ['age'], na: 1},
- {age: 4, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}},
- done
- );
- });
-
- it('can set on child of projected field', function(done) {
- test.call(this,
- {p: ['owner', 'sex'], oi: 'male'},
- {age: 3, color: 'gold', owner: {name: 'jim', sex: 'male'}, litter: {count: 4}},
- done
- );
- });
-
- it('cannot set on non-projected field', function(done) {
- testError.call(this,
- {p: ['color'], od: 'gold', oi: 'tan'},
- done
- );
- });
-
- it('cannot set on root path of projected doc', function(done) {
- testError.call(this,
- {p: [], oi: null},
- done
- );
- });
- it('can delete on projected doc', function(done) {
- var doc = this.connection.get('dogs', 'fido');
- var projected = this.backend.connect().get('dogs_summary', 'fido');
- projected.fetch(function(err) {
- if (err) return done(err);
- projected.del(function(err) {
- if (err) return done(err);
+ it('cannot create a projected doc with non-projected fields', function(done) {
+ var doc = this.connection.get('dogs', 'spot');
+ var projected = this.backend.connect().get('dogs_summary', 'spot');
+ var data = {age: 5, foo: 'bar'};
+ projected.create(data, function(err) {
+ expect(err).ok();
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
- expect(doc.version).equal(2);
+ expect(doc.version).equal(0);
done();
});
});
});
});
-
- it('can create a projected doc with only projected fields', function(done) {
- var doc = this.connection.get('dogs', 'spot');
- var projected = this.backend.connect().get('dogs_summary', 'spot');
- var data = {age: 5};
- projected.create(data, function(err) {
- if (err) return done(err);
- doc.fetch(function(err) {
- if (err) return done(err);
- expect(doc.data).eql({age: 5});
- expect(doc.version).equal(1);
- done();
- });
- });
- });
-
- it('cannot create a projected doc with non-projected fields', function(done) {
- var doc = this.connection.get('dogs', 'spot');
- var projected = this.backend.connect().get('dogs_summary', 'spot');
- var data = {age: 5, foo: 'bar'};
- projected.create(data, function(err) {
- expect(err).ok();
- doc.fetch(function(err) {
- if (err) return done(err);
- expect(doc.data).eql(undefined);
- expect(doc.version).equal(0);
- done();
- });
- });
- });
});
-
-});
};
diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js
index c63d4833a..166c859eb 100644
--- a/test/client/query-subscribe.js
+++ b/test/client/query-subscribe.js
@@ -3,445 +3,483 @@ var async = require('async');
var util = require('../util');
module.exports = function(options) {
-var getQuery = options.getQuery;
+ var getQuery = options.getQuery;
-describe('client query subscribe', function() {
- before(function() {
- if (!getQuery) return this.skip();
- this.matchAllDbQuery = getQuery({query: {}});
- });
-
- it('creating a document updates a subscribed query', function(done) {
- var connection = this.backend.connect();
- var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
- if (err) return done(err);
- connection.get('dogs', 'fido').create({age: 3});
- });
- query.on('insert', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['fido']);
- expect(util.pluck(docs, 'data')).eql([{age: 3}]);
- expect(index).equal(0);
- expect(util.pluck(query.results, 'id')).eql(['fido']);
- expect(util.pluck(query.results, 'data')).eql([{age: 3}]);
- done();
+ describe('client query subscribe', function() {
+ before(function() {
+ if (!getQuery) return this.skip();
+ this.matchAllDbQuery = getQuery({query: {}});
});
- });
- it('creating an additional document updates a subscribed query', function(done) {
- var connection = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ it('creating a document updates a subscribed query', function(done) {
+ var connection = this.backend.connect();
+ var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
if (err) return done(err);
- connection.get('dogs', 'taco').create({age: 2});
+ connection.get('dogs', 'fido').create({age: 3});
});
query.on('insert', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['taco']);
- expect(util.pluck(docs, 'data')).eql([{age: 2}]);
- expect(query.results[index]).equal(docs[0]);
- var results = util.sortById(query.results);
- expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
- expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
+ expect(util.pluck(docs, 'id')).eql(['fido']);
+ expect(util.pluck(docs, 'data')).eql([{age: 3}]);
+ expect(index).equal(0);
+ expect(util.pluck(query.results, 'id')).eql(['fido']);
+ expect(util.pluck(query.results, 'data')).eql([{age: 3}]);
done();
});
});
- });
- it('deleting a document updates a subscribed query', function(done) {
- var connection = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ it('creating an additional document updates a subscribed query', function(done) {
+ var connection = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- connection.get('dogs', 'fido').del();
- });
- query.on('remove', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['fido']);
- expect(util.pluck(docs, 'data')).eql([undefined]);
- expect(index).a('number');
- var results = util.sortById(query.results);
- expect(util.pluck(results, 'id')).eql(['spot']);
- expect(util.pluck(results, 'data')).eql([{age: 5}]);
- done();
+ var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ if (err) return done(err);
+ connection.get('dogs', 'taco').create({age: 2});
+ });
+ query.on('insert', function(docs, index) {
+ expect(util.pluck(docs, 'id')).eql(['taco']);
+ expect(util.pluck(docs, 'data')).eql([{age: 2}]);
+ expect(query.results[index]).equal(docs[0]);
+ var results = util.sortById(query.results);
+ expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
+ expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
+ done();
+ });
});
});
- });
- it('subscribed query does not get updated after destroyed', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ it('deleting a document updates a subscribed query', function(done) {
+ var connection = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- query.destroy(function(err) {
+ var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
- connection2.get('dogs', 'taco').create({age: 2}, done);
+ connection.get('dogs', 'fido').del();
+ });
+ query.on('remove', function(docs, index) {
+ expect(util.pluck(docs, 'id')).eql(['fido']);
+ expect(util.pluck(docs, 'data')).eql([undefined]);
+ expect(index).a('number');
+ var results = util.sortById(query.results);
+ expect(util.pluck(results, 'id')).eql(['spot']);
+ expect(util.pluck(results, 'data')).eql([{age: 5}]);
+ done();
});
- });
- query.on('insert', function() {
- done();
});
});
- });
- it('subscribed query does not get updated after connection is disconnected', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ it('subscribed query does not get updated after destroyed', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- connection.close();
- connection2.get('dogs', 'taco').create({age: 2}, done);
- });
- query.on('insert', function() {
- done();
+ var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ if (err) return done(err);
+ query.destroy(function(err) {
+ if (err) return done(err);
+ connection2.get('dogs', 'taco').create({age: 2}, done);
+ });
+ });
+ query.on('insert', function() {
+ done();
+ });
});
});
- });
- it('subscribed query gets update after reconnecting', function(done) {
- var backend = this.backend;
- var connection = backend.connect();
- var connection2 = backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ it('subscribed query does not get updated after connection is disconnected', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- connection.close();
- connection2.get('dogs', 'taco').create({age: 2});
- process.nextTick(function() {
- backend.connect(connection);
+ var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ if (err) return done(err);
+ connection.close();
+ connection2.get('dogs', 'taco').create({age: 2}, done);
+ });
+ query.on('insert', function() {
+ done();
});
- });
- query.on('insert', function() {
- done();
});
});
- });
- it('subscribed query gets simultaneous insert and remove after reconnecting', function(done) {
- var backend = this.backend;
- var connection = backend.connect();
- var connection2 = backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ it('subscribed query gets update after reconnecting', function(done) {
+ var backend = this.backend;
+ var connection = backend.connect();
+ var connection2 = backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- connection.close();
- connection2.get('dogs', 'fido').fetch(function(err) {
+ var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
- connection2.get('dogs', 'fido').del();
+ connection.close();
connection2.get('dogs', 'taco').create({age: 2});
process.nextTick(function() {
backend.connect(connection);
});
});
- });
- var wait = 2;
- function finish() {
- if (--wait) return;
- var results = util.sortById(query.results);
- expect(util.pluck(results, 'id')).eql(['spot', 'taco']);
- expect(util.pluck(results, 'data')).eql([{age: 5}, {age: 2}]);
- done();
- }
- query.on('insert', function(docs) {
- expect(util.pluck(docs, 'id')).eql(['taco']);
- expect(util.pluck(docs, 'data')).eql([{age: 2}]);
- finish();
- });
- query.on('remove', function(docs) {
- expect(util.pluck(docs, 'id')).eql(['fido']);
- expect(util.pluck(docs, 'data')).eql([undefined]);
- finish();
+ query.on('insert', function() {
+ done();
+ });
});
});
- });
- it('creating an additional document updates a subscribed query', function(done) {
- var connection = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ it('subscribed query gets simultaneous insert and remove after reconnecting', function(done) {
+ var backend = this.backend;
+ var connection = backend.connect();
+ var connection2 = backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- connection.get('dogs', 'taco').create({age: 2});
+ var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ if (err) return done(err);
+ connection.close();
+ connection2.get('dogs', 'fido').fetch(function(err) {
+ if (err) return done(err);
+ connection2.get('dogs', 'fido').del();
+ connection2.get('dogs', 'taco').create({age: 2});
+ process.nextTick(function() {
+ backend.connect(connection);
+ });
+ });
+ });
+ var wait = 2;
+ function finish() {
+ if (--wait) return;
+ var results = util.sortById(query.results);
+ expect(util.pluck(results, 'id')).eql(['spot', 'taco']);
+ expect(util.pluck(results, 'data')).eql([{age: 5}, {age: 2}]);
+ done();
+ }
+ query.on('insert', function(docs) {
+ expect(util.pluck(docs, 'id')).eql(['taco']);
+ expect(util.pluck(docs, 'data')).eql([{age: 2}]);
+ finish();
+ });
+ query.on('remove', function(docs) {
+ expect(util.pluck(docs, 'id')).eql(['fido']);
+ expect(util.pluck(docs, 'data')).eql([undefined]);
+ finish();
+ });
});
- query.on('insert', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['taco']);
- expect(util.pluck(docs, 'data')).eql([{age: 2}]);
- expect(query.results[index]).equal(docs[0]);
- var results = util.sortById(query.results);
- expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
- expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
- done();
+ });
+
+ it('creating an additional document updates a subscribed query', function(done) {
+ var connection = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ if (err) return done(err);
+ connection.get('dogs', 'taco').create({age: 2});
+ });
+ query.on('insert', function(docs, index) {
+ expect(util.pluck(docs, 'id')).eql(['taco']);
+ expect(util.pluck(docs, 'data')).eql([{age: 2}]);
+ expect(query.results[index]).equal(docs[0]);
+ var results = util.sortById(query.results);
+ expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
+ expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
+ done();
+ });
});
});
- });
- it('pollDebounce option reduces subsequent poll interval', function(done) {
- var connection = this.backend.connect();
- this.backend.db.canPollDoc = function() {
- return false;
- };
- var query = connection.createSubscribeQuery('items', this.matchAllDbQuery, {pollDebounce: 1000});
- var batchSizes = [];
- var total = 0;
+ it('pollDebounce option reduces subsequent poll interval', function(done) {
+ var connection = this.backend.connect();
+ this.backend.db.canPollDoc = function() {
+ return false;
+ };
+ var query = connection.createSubscribeQuery('items', this.matchAllDbQuery, {pollDebounce: 1000});
+ var batchSizes = [];
+ var total = 0;
- query.on('insert', function(docs) {
- batchSizes.push(docs.length);
- total += docs.length;
- if (total === 1) {
+ query.on('insert', function(docs) {
+ batchSizes.push(docs.length);
+ total += docs.length;
+ if (total === 1) {
// first write received by client. we're debouncing. create 9
// more documents.
- for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({});
- }
- if (total === 10) {
+ for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({});
+ }
+ if (total === 10) {
// first document is its own batch; then subsequent creates
// are debounced until after all other 9 docs are created
- expect(batchSizes).eql([1, 9]);
- done();
- }
- });
+ expect(batchSizes).eql([1, 9]);
+ done();
+ }
+ });
- // create an initial document. this will lead to the 'insert'
- // event firing the first time, while sharedb is definitely
- // debouncing
- connection.get('items', '0').create({});
- });
+ // create an initial document. this will lead to the 'insert'
+ // event firing the first time, while sharedb is definitely
+ // debouncing
+ connection.get('items', '0').create({});
+ });
- it('db.pollDebounce option reduces subsequent poll interval', function(done) {
- var connection = this.backend.connect();
- this.backend.db.canPollDoc = function() {
- return false;
- };
- this.backend.db.pollDebounce = 1000;
- var query = connection.createSubscribeQuery('items', this.matchAllDbQuery);
- var batchSizes = [];
- var total = 0;
+ it('db.pollDebounce option reduces subsequent poll interval', function(done) {
+ var connection = this.backend.connect();
+ this.backend.db.canPollDoc = function() {
+ return false;
+ };
+ this.backend.db.pollDebounce = 1000;
+ var query = connection.createSubscribeQuery('items', this.matchAllDbQuery);
+ var batchSizes = [];
+ var total = 0;
- query.on('insert', function(docs) {
- batchSizes.push(docs.length);
- total += docs.length;
- if (total === 1) {
+ query.on('insert', function(docs) {
+ batchSizes.push(docs.length);
+ total += docs.length;
+ if (total === 1) {
// first write received by client. we're debouncing. create 9
// more documents.
- for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({});
- }
- if (total === 10) {
+ for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({});
+ }
+ if (total === 10) {
// first document is its own batch; then subsequent creates
// are debounced until after all other 9 docs are created
- expect(batchSizes).eql([1, 9]);
- done();
- }
- });
-
- // create an initial document. this will lead to the 'insert'
- // event firing the first time, while sharedb is definitely
- // debouncing
- connection.get('items', '0').create({});
- });
-
- it('pollInterval updates a subscribed query after an unpublished create', function(done) {
- var connection = this.backend.connect();
- this.backend.suppressPublish = true;
- var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
- if (err) return done(err);
- connection.get('dogs', 'fido').create({});
- });
- query.on('insert', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['fido']);
- done();
- });
- });
+ expect(batchSizes).eql([1, 9]);
+ done();
+ }
+ });
- it('db.pollInterval updates a subscribed query after an unpublished create', function(done) {
- var connection = this.backend.connect();
- this.backend.suppressPublish = true;
- this.backend.db.pollInterval = 50;
- var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
- if (err) return done(err);
- connection.get('dogs', 'fido').create({});
+ // create an initial document. this will lead to the 'insert'
+ // event firing the first time, while sharedb is definitely
+ // debouncing
+ connection.get('items', '0').create({});
});
- query.on('insert', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['fido']);
- done();
- });
- });
- it('pollInterval captures additional unpublished creates', function(done) {
- var connection = this.backend.connect();
- this.backend.suppressPublish = true;
- var count = 0;
- var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
- if (err) return done(err);
- connection.get('dogs', count.toString()).create({});
- });
- query.on('insert', function() {
- count++;
- if (count === 3) return done();
- connection.get('dogs', count.toString()).create({});
+ it('pollInterval updates a subscribed query after an unpublished create', function(done) {
+ var connection = this.backend.connect();
+ this.backend.suppressPublish = true;
+ var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
+ if (err) return done(err);
+ connection.get('dogs', 'fido').create({});
+ });
+ query.on('insert', function(docs) {
+ expect(util.pluck(docs, 'id')).eql(['fido']);
+ done();
+ });
});
- });
- it('query extra is returned to client', function(done) {
- var connection = this.backend.connect();
- this.backend.db.query = function(collection, query, fields, options, callback) {
- process.nextTick(function() {
- callback(null, [], {colors: ['brown', 'gold']});
+ it('db.pollInterval updates a subscribed query after an unpublished create', function(done) {
+ var connection = this.backend.connect();
+ this.backend.suppressPublish = true;
+ this.backend.db.pollInterval = 50;
+ var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
+ if (err) return done(err);
+ connection.get('dogs', 'fido').create({});
+ });
+ query.on('insert', function(docs) {
+ expect(util.pluck(docs, 'id')).eql(['fido']);
+ done();
});
- };
- var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
- if (err) return done(err);
- expect(results).eql([]);
- expect(extra).eql({colors: ['brown', 'gold']});
- expect(query.extra).eql({colors: ['brown', 'gold']});
- done();
});
- });
- it('query extra is updated on change', function(done) {
- var connection = this.backend.connect();
- this.backend.db.query = function(collection, query, fields, options, callback) {
- process.nextTick(function() {
- callback(null, [], 1);
+ it('pollInterval captures additional unpublished creates', function(done) {
+ var connection = this.backend.connect();
+ this.backend.suppressPublish = true;
+ var count = 0;
+ var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
+ if (err) return done(err);
+ connection.get('dogs', count.toString()).create({});
});
- };
- this.backend.db.queryPoll = function(collection, query, options, callback) {
- process.nextTick(function() {
- callback(null, [], 2);
+ query.on('insert', function() {
+ count++;
+ if (count === 3) return done();
+ connection.get('dogs', count.toString()).create({});
});
- };
- this.backend.db.canPollDoc = function() {
- return false;
- };
- var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
- if (err) return done(err);
- expect(extra).eql(1);
- expect(query.extra).eql(1);
- });
- query.on('extra', function(extra) {
- expect(extra).eql(2);
- expect(query.extra).eql(2);
- done();
});
- connection.get('dogs', 'fido').create({age: 3});
- });
- it('changing a filtered property removes from a subscribed query', function(done) {
- var connection = this.backend.connect();
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 3}, cb); }
- ], function(err) {
- if (err) return done(err);
- var dbQuery = getQuery({query: {age: 3}});
- var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
+ it('query extra is returned to client', function(done) {
+ var connection = this.backend.connect();
+ this.backend.db.query = function(collection, query, fields, options, callback) {
+ process.nextTick(function() {
+ callback(null, [], {colors: ['brown', 'gold']});
+ });
+ };
+ var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
if (err) return done(err);
- var sorted = util.sortById(results);
- expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
- expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 3}]);
- connection.get('dogs', 'fido').submitOp({p: ['age'], na: 2});
- });
- query.on('remove', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['fido']);
- expect(util.pluck(docs, 'data')).eql([{age: 5}]);
- expect(index).a('number');
- var results = util.sortById(query.results);
- expect(util.pluck(results, 'id')).eql(['spot']);
- expect(util.pluck(results, 'data')).eql([{age: 3}]);
+ expect(results).eql([]);
+ expect(extra).eql({colors: ['brown', 'gold']});
+ expect(query.extra).eql({colors: ['brown', 'gold']});
done();
});
});
- });
- it('changing a filtered property inserts to a subscribed query', function(done) {
- var connection = this.backend.connect();
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var dbQuery = getQuery({query: {age: 3}});
- var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
+ it('query extra is updated on change', function(done) {
+ var connection = this.backend.connect();
+ this.backend.db.query = function(collection, query, fields, options, callback) {
+ process.nextTick(function() {
+ callback(null, [], 1);
+ });
+ };
+ this.backend.db.queryPoll = function(collection, query, options, callback) {
+ process.nextTick(function() {
+ callback(null, [], 2);
+ });
+ };
+ this.backend.db.canPollDoc = function() {
+ return false;
+ };
+ var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
if (err) return done(err);
- var sorted = util.sortById(results);
- expect(util.pluck(sorted, 'id')).eql(['fido']);
- expect(util.pluck(sorted, 'data')).eql([{age: 3}]);
- connection.get('dogs', 'spot').submitOp({p: ['age'], na: -2});
+ expect(extra).eql(1);
+ expect(query.extra).eql(1);
});
- query.on('insert', function(docs, index) {
- expect(util.pluck(docs, 'id')).eql(['spot']);
- expect(util.pluck(docs, 'data')).eql([{age: 3}]);
- expect(index).a('number');
- var results = util.sortById(query.results);
- expect(util.pluck(results, 'id')).eql(['fido', 'spot']);
- expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 3}]);
+ query.on('extra', function(extra) {
+ expect(extra).eql(2);
+ expect(query.extra).eql(2);
done();
});
+ connection.get('dogs', 'fido').create({age: 3});
});
- });
- it('changing a sorted property moves in a subscribed query', function(done) {
- var connection = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
+ it('changing a filtered property removes from a subscribed query', function(done) {
+ var connection = this.backend.connect();
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 3}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ var dbQuery = getQuery({query: {age: 3}});
+ var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
+ if (err) return done(err);
+ var sorted = util.sortById(results);
+ expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
+ expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 3}]);
+ connection.get('dogs', 'fido').submitOp({p: ['age'], na: 2});
+ });
+ query.on('remove', function(docs, index) {
+ expect(util.pluck(docs, 'id')).eql(['fido']);
+ expect(util.pluck(docs, 'data')).eql([{age: 5}]);
+ expect(index).a('number');
+ var results = util.sortById(query.results);
+ expect(util.pluck(results, 'id')).eql(['spot']);
+ expect(util.pluck(results, 'data')).eql([{age: 3}]);
+ done();
+ });
+ });
+ });
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- var dbQuery = getQuery({query: matchAllDbQuery, sort: [['age', 1]]});
- var query = connection.createSubscribeQuery(
- 'dogs',
- dbQuery,
- null,
- function(err, results) {
+ it('changing a filtered property inserts to a subscribed query', function(done) {
+ var connection = this.backend.connect();
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ var dbQuery = getQuery({query: {age: 3}});
+ var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
if (err) return done(err);
+ var sorted = util.sortById(results);
+ expect(util.pluck(sorted, 'id')).eql(['fido']);
+ expect(util.pluck(sorted, 'data')).eql([{age: 3}]);
+ connection.get('dogs', 'spot').submitOp({p: ['age'], na: -2});
+ });
+ query.on('insert', function(docs, index) {
+ expect(util.pluck(docs, 'id')).eql(['spot']);
+ expect(util.pluck(docs, 'data')).eql([{age: 3}]);
+ expect(index).a('number');
+ var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['fido', 'spot']);
- expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}]);
- connection.get('dogs', 'spot').submitOp({p: ['age'], na: -3});
+ expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 3}]);
+ done();
});
+ });
+ });
- query.on('move', function(docs, from, to) {
- expect(docs.length).eql(1);
- expect(from).a('number');
- expect(to).a('number');
- expect(util.pluck(query.results, 'id')).eql(['spot', 'fido']);
- expect(util.pluck(query.results, 'data')).eql([{age: 2}, {age: 3}]);
- done();
+ it('changing a sorted property moves in a subscribed query', function(done) {
+ var connection = this.backend.connect();
+
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ var dbQuery = getQuery({query: {}, sort: [['age', 1]]});
+ var query = connection.createSubscribeQuery(
+ 'dogs',
+ dbQuery,
+ null,
+ function(err, results) {
+ if (err) return done(err);
+ expect(util.pluck(results, 'id')).eql(['fido', 'spot']);
+ expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}]);
+ connection.get('dogs', 'spot').submitOp({p: ['age'], na: -3});
+ });
+
+ query.on('move', function(docs, from, to) {
+ expect(docs.length).eql(1);
+ expect(from).a('number');
+ expect(to).a('number');
+ expect(util.pluck(query.results, 'id')).eql(['spot', 'fido']);
+ expect(util.pluck(query.results, 'data')).eql([{age: 2}, {age: 3}]);
+ done();
+ });
});
});
});
-
-});
};
diff --git a/test/client/query.js b/test/client/query.js
index 0b7912cb3..785c55211 100644
--- a/test/client/query.js
+++ b/test/client/query.js
@@ -3,74 +3,64 @@ var async = require('async');
var util = require('../util');
module.exports = function(options) {
-var getQuery = options.getQuery;
+ var getQuery = options.getQuery;
-describe('client query', function() {
- before(function() {
- if (!getQuery) return this.skip();
- this.matchAllDbQuery = getQuery({query: {}});
- });
-
- ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
- it(method + ' on an empty collection', function(done) {
- var connection = this.backend.connect();
- connection[method]('dogs', this.matchAllDbQuery, null, function(err, results) {
- if (err) return done(err);
- expect(results).eql([]);
- done();
- });
+ describe('client query', function() {
+ before(function() {
+ if (!getQuery) return this.skip();
+ this.matchAllDbQuery = getQuery({query: {}});
});
- it(method + ' on collection with fetched docs', function(done) {
- var connection = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
- function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
- ], function(err) {
- if (err) return done(err);
- connection[method]('dogs', matchAllDbQuery, null, function(err, results) {
+ ['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
+ it(method + ' on an empty collection', function(done) {
+ var connection = this.backend.connect();
+ connection[method]('dogs', this.matchAllDbQuery, null, function(err, results) {
if (err) return done(err);
- var sorted = util.sortById(results);
- expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
- expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
+ expect(results).eql([]);
done();
});
});
- });
- it(method + ' on collection with unfetched docs', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
- function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
- ], function(err) {
- if (err) return done(err);
- connection2[method]('dogs', matchAllDbQuery, null, function(err, results) {
+ it(method + ' on collection with fetched docs', function(done) {
+ var connection = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ },
+ function(cb) {
+ connection.get('cats', 'finn').create({age: 2}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- var sorted = util.sortById(results);
- expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
- expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
- done();
+ connection[method]('dogs', matchAllDbQuery, null, function(err, results) {
+ if (err) return done(err);
+ var sorted = util.sortById(results);
+ expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
+ expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
+ done();
+ });
});
});
- });
- it(method + ' on collection with one fetched doc', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
- function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
- ], function(err) {
- if (err) return done(err);
- connection2.get('dogs', 'fido').fetch(function(err) {
+ it(method + ' on collection with unfetched docs', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ },
+ function(cb) {
+ connection.get('cats', 'finn').create({age: 2}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
connection2[method]('dogs', matchAllDbQuery, null, function(err, results) {
if (err) return done(err);
@@ -81,43 +71,75 @@ describe('client query', function() {
});
});
});
- });
- it(method + ' on collection with one fetched doc missing an op', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- var matchAllDbQuery = this.matchAllDbQuery;
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
- function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
- ], function(err) {
- if (err) return done(err);
- connection2.get('dogs', 'fido').fetch(function(err) {
+ it(method + ' on collection with one fetched doc', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ },
+ function(cb) {
+ connection.get('cats', 'finn').create({age: 2}, cb);
+ }
+ ], function(err) {
if (err) return done(err);
- connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], function(err) {
+ connection2.get('dogs', 'fido').fetch(function(err) {
if (err) return done(err);
- // The results option is meant for making resubscribing more
- // efficient and has no effect on query fetching
- var options = {
- results: [
- connection2.get('dogs', 'fido'),
- connection2.get('dogs', 'spot')
- ]
- };
- connection2[method]('dogs', matchAllDbQuery, options, function(err, results) {
+ connection2[method]('dogs', matchAllDbQuery, null, function(err, results) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
- expect(util.pluck(sorted, 'data')).eql([{age: 4}, {age: 5}]);
+ expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
done();
});
});
});
});
- });
+ it(method + ' on collection with one fetched doc missing an op', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ },
+ function(cb) {
+ connection.get('cats', 'finn').create({age: 2}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ connection2.get('dogs', 'fido').fetch(function(err) {
+ if (err) return done(err);
+ connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], function(err) {
+ if (err) return done(err);
+ // The results option is meant for making resubscribing more
+ // efficient and has no effect on query fetching
+ var options = {
+ results: [
+ connection2.get('dogs', 'fido'),
+ connection2.get('dogs', 'spot')
+ ]
+ };
+ connection2[method]('dogs', matchAllDbQuery, options, function(err, results) {
+ if (err) return done(err);
+ var sorted = util.sortById(results);
+ expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
+ expect(util.pluck(sorted, 'data')).eql([{age: 4}, {age: 5}]);
+ done();
+ });
+ });
+ });
+ });
+ });
+ });
});
-
-});
};
diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js
new file mode 100644
index 000000000..8d7cc4bf3
--- /dev/null
+++ b/test/client/snapshot-timestamp-request.js
@@ -0,0 +1,515 @@
+var Backend = require('../../lib/backend');
+var expect = require('expect.js');
+var util = require('../util');
+var lolex = require('lolex');
+var MemoryDb = require('../../lib/db/memory');
+var MemoryMilestoneDb = require('../../lib/milestone-db/memory');
+var sinon = require('sinon');
+
+describe('SnapshotTimestampRequest', function() {
+ var backend;
+ var clock;
+ var day0 = new Date(2017, 11, 31).getTime();
+ var day1 = new Date(2018, 0, 1).getTime();
+ var day2 = new Date(2018, 0, 2).getTime();
+ var day3 = new Date(2018, 0, 3).getTime();
+ var day4 = new Date(2018, 0, 4).getTime();
+ var day5 = new Date(2018, 0, 5).getTime();
+ var ONE_DAY = 1000 * 60 * 60 * 24;
+
+ beforeEach(function() {
+ clock = lolex.install({now: day1});
+ backend = new Backend();
+ });
+
+ afterEach(function(done) {
+ clock.uninstall();
+ backend.close(done);
+ });
+
+ describe('a document with some simple versions separated by a day', function() {
+ var v0 = {
+ id: 'time-machine',
+ v: 0,
+ type: null,
+ data: undefined,
+ m: null
+ };
+
+ var v1 = {
+ id: 'time-machine',
+ v: 1,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'The Time Machine'
+ },
+ m: null
+ };
+
+ var v2 = {
+ id: 'time-machine',
+ v: 2,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'The Time Machine',
+ author: 'HG Wells'
+ },
+ m: null
+ };
+
+ var v3 = {
+ id: 'time-machine',
+ v: 3,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'The Time Machine',
+ author: 'H.G. Wells'
+ },
+ m: null
+ };
+
+ beforeEach(function(done) {
+ var doc = backend.connect().get('books', 'time-machine');
+ util.callInSeries([
+ function(next) {
+ doc.create({title: 'The Time Machine'}, next);
+ },
+ function(next) {
+ clock.tick(ONE_DAY);
+ doc.submitOp({p: ['author'], oi: 'HG Wells'}, next);
+ },
+ function(next) {
+ clock.tick(ONE_DAY);
+ doc.submitOp({p: ['author'], od: 'HG Wells', oi: 'H.G. Wells'}, next);
+ },
+ done
+ ]);
+ });
+
+ it('fetches the version at exactly day 1', function(done) {
+ util.callInSeries([
+ function(next) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(v1);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the version at exactly day 2', function(done) {
+ util.callInSeries([
+ function(next) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day2, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(v2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the version at exactly day 3', function(done) {
+ util.callInSeries([
+ function(next) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(v3);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the day 2 version when asking for a time halfway between days 2 and 3', function(done) {
+ var halfwayBetweenDays2and3 = (day2 + day3) * 0.5;
+ util.callInSeries([
+ function(next) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', halfwayBetweenDays2and3, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(v2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the day 3 version when asking for a time after day 3', function(done) {
+ util.callInSeries([
+ function(next) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day4, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(v3);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the most recent version when not specifying a timestamp', function(done) {
+ util.callInSeries([
+ function(next) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(v3);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches an empty snapshot if the timestamp is before the document creation', function(done) {
+ util.callInSeries([
+ function(next) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day0, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(v0);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('throws if the timestamp is undefined', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function() {});
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ it('throws without a callback', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine');
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ it('throws if the timestamp is -1', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', -1, function() { });
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ it('errors if the timestamp is a string', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', 'foo', function() { });
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
+ backend.connect().fetchSnapshotByTimestamp('books', 'does-not-exist', day1, function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot).to.eql({
+ id: 'does-not-exist',
+ v: 0,
+ type: null,
+ data: undefined,
+ m: null
+ });
+ done();
+ });
+ });
+
+ it('starts pending, and finishes not pending', function(done) {
+ var connection = backend.connect();
+
+ connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error) {
+ if (error) return done(error);
+ expect(connection.hasPending()).to.be(false);
+ done();
+ });
+
+ expect(connection.hasPending()).to.be(true);
+ });
+
+ it('deletes the request from the connection', function(done) {
+ var connection = backend.connect();
+
+ connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) {
+ if (error) return done(error);
+ expect(connection._snapshotRequests).to.eql({});
+ done();
+ });
+
+ expect(connection._snapshotRequests).to.not.eql({});
+ });
+
+ it('emits a ready event when done', function(done) {
+ var connection = backend.connect();
+
+ connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) {
+ if (error) return done(error);
+ });
+
+ var snapshotRequest = connection._snapshotRequests[1];
+ snapshotRequest.on('ready', done);
+ });
+
+ it('fires the connection.whenNothingPending', function(done) {
+ var connection = backend.connect();
+ var snapshotFetched = false;
+
+ connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) {
+ if (error) return done(error);
+ snapshotFetched = true;
+ });
+
+ connection.whenNothingPending(function() {
+ expect(snapshotFetched).to.be(true);
+ done();
+ });
+ });
+
+ it('can drop its connection and reconnect, and the callback is just called once', function(done) {
+ var connection = backend.connect();
+
+ // Here we hook into middleware to make sure that we get the following flow:
+ // - Connection established
+ // - Connection attempts to fetch a snapshot
+ // - Snapshot is about to be returned
+ // - Connection is dropped before the snapshot is returned
+ // - Connection is re-established
+ // - Connection re-requests the snapshot
+ // - This time the fetch operation is allowed to complete (because of the connectionInterrupted flag)
+ // - The done callback is called just once (if it's called twice, then mocha will complain)
+ var connectionInterrupted = false;
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
+ if (!connectionInterrupted) {
+ connection.close();
+ backend.connect(connection);
+ connectionInterrupted = true;
+ }
+
+ callback();
+ });
+
+ connection.fetchSnapshotByTimestamp('books', 'time-machine', done);
+ });
+
+ it('cannot send the same request twice over a connection', function(done) {
+ var connection = backend.connect();
+
+ // Here we hook into the middleware to make sure that we get the following flow:
+ // - Attempt to fetch a snapshot
+ // - The snapshot request is temporarily stored on the Connection
+ // - Snapshot is about to be returned (ie the request was already successfully sent)
+ // - We attempt to resend the request again
+ // - The done callback is call just once, because the second request does not get sent
+ // (if the done callback is called twice, then mocha will complain)
+ var hasResent = false;
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
+ if (!hasResent) {
+ connection._snapshotRequests[1]._onConnectionStateChanged();
+ hasResent = true;
+ }
+
+ callback();
+ });
+
+ connection.fetchSnapshotByTimestamp('books', 'time-machine', done);
+ });
+
+ describe('readSnapshots middleware', function() {
+ it('triggers the middleware', function(done) {
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
+ function(request) {
+ expect(request.snapshots[0]).to.eql(v3);
+ expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byTimestamp);
+ done();
+ }
+ );
+
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, function() { });
+ });
+
+ it('can have its snapshot manipulated in the middleware', function(done) {
+ backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
+ function(request, callback) {
+ request.snapshots[0].data.title = 'Alice in Wonderland';
+ callback();
+ }
+ ];
+
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot.data.title).to.be('Alice in Wonderland');
+ done();
+ });
+ });
+
+ it('respects errors thrown in the middleware', function(done) {
+ backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
+ function(request, callback) {
+ callback({message: 'foo'});
+ }
+ ];
+
+ backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error) {
+ expect(error.message).to.be('foo');
+ done();
+ });
+ });
+ });
+
+ describe('with a registered projection', function() {
+ beforeEach(function() {
+ backend.addProjection('bookTitles', 'books', {title: true});
+ });
+
+ it('applies the projection to a snapshot', function(done) {
+ backend.connect().fetchSnapshotByTimestamp('bookTitles', 'time-machine', day2, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(snapshot.data.title).to.be('The Time Machine');
+ expect(snapshot.data.author).to.be(undefined);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('milestone snapshots enabled for every other version', function() {
+ var milestoneDb;
+ var db;
+ var backendWithMilestones;
+
+ beforeEach(function() {
+ var options = {interval: 2};
+ db = new MemoryDb();
+ milestoneDb = new MemoryMilestoneDb(options);
+ backendWithMilestones = new Backend({
+ db: db,
+ milestoneDb: milestoneDb
+ });
+ });
+
+ afterEach(function(done) {
+ backendWithMilestones.close(done);
+ });
+
+ describe('a doc with some versions in the milestone database', function() {
+ beforeEach(function(done) {
+ clock.reset();
+
+ var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
+
+ util.callInSeries([
+ function(next) {
+ doc.create({title: 'To Kill a Mocking Bird'}, next);
+ },
+ function(next) {
+ clock.tick(ONE_DAY);
+ doc.submitOp({p: ['author'], oi: 'Harper Lea'}, next);
+ },
+ function(next) {
+ clock.tick(ONE_DAY);
+ doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next);
+ },
+ function(next) {
+ clock.tick(ONE_DAY);
+ doc.submitOp({p: ['year'], oi: 1959}, next);
+ },
+ function(next) {
+ clock.tick(ONE_DAY);
+ doc.submitOp({p: ['year'], od: 1959, oi: 1960}, next);
+ },
+ done
+ ]);
+ });
+
+ it('fetches a snapshot between two milestones using the milestones', function(done) {
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
+ sinon.spy(db, 'getOps');
+ var halfwayBetweenDays3and4 = (day3 + day4) * 0.5;
+
+ backendWithMilestones.connect()
+ .fetchSnapshotByTimestamp('books', 'mocking-bird', halfwayBetweenDays3and4, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 2, 4)).to.be(true);
+
+ expect(snapshot.v).to.be(3);
+ expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
+ done();
+ });
+ });
+
+ it('fetches a snapshot that matches a milestone snapshot', function(done) {
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
+
+ backendWithMilestones.connect()
+ .fetchSnapshotByTimestamp('books', 'mocking-bird', day2, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true);
+
+ expect(snapshot.v).to.be(2);
+ expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lea'});
+ done();
+ });
+ });
+
+ it('fetches a snapshot before any milestones', function(done) {
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
+ sinon.spy(db, 'getOps');
+
+ backendWithMilestones.connect()
+ .fetchSnapshotByTimestamp('books', 'mocking-bird', day1, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 0, 2)).to.be(true);
+
+ expect(snapshot.v).to.be(1);
+ expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird'});
+ done();
+ });
+ });
+
+ it('fetches a snapshot after any milestones', function(done) {
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');
+ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');
+ sinon.spy(db, 'getOps');
+
+ backendWithMilestones.connect()
+ .fetchSnapshotByTimestamp('books', 'mocking-bird', day5, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 4, null)).to.be(true);
+
+ expect(snapshot.v).to.be(5);
+ expect(snapshot.data).to.eql({
+ title: 'To Kill a Mocking Bird',
+ author: 'Harper Lee',
+ year: 1960
+ });
+
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js
new file mode 100644
index 000000000..a39355f38
--- /dev/null
+++ b/test/client/snapshot-version-request.js
@@ -0,0 +1,448 @@
+var Backend = require('../../lib/backend');
+var expect = require('expect.js');
+var MemoryDb = require('../../lib/db/memory');
+var MemoryMilestoneDb = require('../../lib/milestone-db/memory');
+var sinon = require('sinon');
+var util = require('../util');
+
+describe('SnapshotVersionRequest', function() {
+ var backend;
+
+ beforeEach(function() {
+ backend = new Backend();
+ });
+
+ afterEach(function(done) {
+ backend.close(done);
+ });
+
+ describe('a document with some simple versions', function() {
+ var v0 = {
+ id: 'don-quixote',
+ v: 0,
+ type: null,
+ data: undefined,
+ m: null
+ };
+
+ var v1 = {
+ id: 'don-quixote',
+ v: 1,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'Don Quixote'
+ },
+ m: null
+ };
+
+ var v2 = {
+ id: 'don-quixote',
+ v: 2,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'Don Quixote',
+ author: 'Miguel de Cervante'
+ },
+ m: null
+ };
+
+ var v3 = {
+ id: 'don-quixote',
+ v: 3,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'Don Quixote',
+ author: 'Miguel de Cervantes'
+ },
+ m: null
+ };
+
+ beforeEach(function(done) {
+ var doc = backend.connect().get('books', 'don-quixote');
+ doc.create({title: 'Don Quixote'}, function(error) {
+ if (error) return done(error);
+ doc.submitOp({p: ['author'], oi: 'Miguel de Cervante'}, function(error) {
+ if (error) return done(error);
+ doc.submitOp({p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes'}, done);
+ });
+ });
+ });
+
+ it('fetches v1', function(done) {
+ backend.connect().fetchSnapshot('books', 'don-quixote', 1, function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot).to.eql(v1);
+ done();
+ });
+ });
+
+ it('fetches v2', function(done) {
+ backend.connect().fetchSnapshot('books', 'don-quixote', 2, function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot).to.eql(v2);
+ done();
+ });
+ });
+
+ it('fetches v3', function(done) {
+ backend.connect().fetchSnapshot('books', 'don-quixote', 3, function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot).to.eql(v3);
+ done();
+ });
+ });
+
+ it('returns an empty snapshot if the version is 0', function(done) {
+ backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot).to.eql(v0);
+ done();
+ });
+ });
+
+ it('throws if the version is undefined', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function() {});
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ it('fetches the latest version when the optional version is not provided', function(done) {
+ backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot).to.eql(v3);
+ done();
+ });
+ });
+
+ it('throws without a callback', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshot('books', 'don-quixote');
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ it('throws if the version is -1', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshot('books', 'don-quixote', -1, function() {});
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ it('errors if the version is a string', function() {
+ var fetch = function() {
+ backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function() { });
+ };
+
+ expect(fetch).to.throwError();
+ });
+
+ 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.be(4024);
+ expect(snapshot).to.be(undefined);
+ done();
+ });
+ });
+
+ it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
+ backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot).to.eql({
+ id: 'does-not-exist',
+ v: 0,
+ type: null,
+ data: undefined,
+ m: null
+ });
+ done();
+ });
+ });
+
+ it('starts pending, and finishes not pending', function(done) {
+ var connection = backend.connect();
+
+ connection.fetchSnapshot('books', 'don-quixote', null, function(error) {
+ if (error) return done(error);
+ expect(connection.hasPending()).to.be(false);
+ done();
+ });
+
+ expect(connection.hasPending()).to.be(true);
+ });
+
+ it('deletes the request from the connection', function(done) {
+ var connection = backend.connect();
+
+ connection.fetchSnapshot('books', 'don-quixote', function(error) {
+ if (error) return done(error);
+ expect(connection._snapshotRequests).to.eql({});
+ done();
+ });
+
+ expect(connection._snapshotRequests).to.not.eql({});
+ });
+
+ it('emits a ready event when done', function(done) {
+ var connection = backend.connect();
+
+ connection.fetchSnapshot('books', 'don-quixote', function(error) {
+ if (error) return done(error);
+ });
+
+ var snapshotRequest = connection._snapshotRequests[1];
+ snapshotRequest.on('ready', done);
+ });
+
+ it('fires the connection.whenNothingPending', function(done) {
+ var connection = backend.connect();
+ var snapshotFetched = false;
+
+ connection.fetchSnapshot('books', 'don-quixote', function(error) {
+ if (error) return done(error);
+ snapshotFetched = true;
+ });
+
+ connection.whenNothingPending(function() {
+ expect(snapshotFetched).to.be(true);
+ done();
+ });
+ });
+
+ it('can drop its connection and reconnect, and the callback is just called once', function(done) {
+ var connection = backend.connect();
+
+ // Here we hook into middleware to make sure that we get the following flow:
+ // - Connection established
+ // - Connection attempts to fetch a snapshot
+ // - Snapshot is about to be returned
+ // - Connection is dropped before the snapshot is returned
+ // - Connection is re-established
+ // - Connection re-requests the snapshot
+ // - This time the fetch operation is allowed to complete (because of the connectionInterrupted flag)
+ // - The done callback is called just once (if it's called twice, then mocha will complain)
+ var connectionInterrupted = false;
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
+ if (!connectionInterrupted) {
+ connection.close();
+ backend.connect(connection);
+ connectionInterrupted = true;
+ }
+
+ callback();
+ });
+
+ connection.fetchSnapshot('books', 'don-quixote', done);
+ });
+
+ it('cannot send the same request twice over a connection', function(done) {
+ var connection = backend.connect();
+
+ // Here we hook into the middleware to make sure that we get the following flow:
+ // - Attempt to fetch a snapshot
+ // - The snapshot request is temporarily stored on the Connection
+ // - Snapshot is about to be returned (ie the request was already successfully sent)
+ // - We attempt to resend the request again
+ // - The done callback is call just once, because the second request does not get sent
+ // (if the done callback is called twice, then mocha will complain)
+ var hasResent = false;
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
+ if (!hasResent) {
+ connection._snapshotRequests[1]._onConnectionStateChanged();
+ hasResent = true;
+ }
+
+ callback();
+ });
+
+ connection.fetchSnapshot('books', 'don-quixote', done);
+ });
+
+ describe('readSnapshots middleware', function() {
+ it('triggers the middleware', function(done) {
+ backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
+ function(request) {
+ expect(request.snapshots[0]).to.eql(v3);
+ expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byVersion);
+ done();
+ }
+ );
+
+ backend.connect().fetchSnapshot('books', 'don-quixote', 3, function() { });
+ });
+
+ it('can have its snapshot manipulated in the middleware', function(done) {
+ backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
+ function(request, callback) {
+ request.snapshots[0].data.title = 'Alice in Wonderland';
+ callback();
+ }
+ ];
+
+ backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
+ if (error) return done(error);
+ expect(snapshot.data.title).to.be('Alice in Wonderland');
+ done();
+ });
+ });
+
+ it('respects errors thrown in the middleware', function(done) {
+ backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
+ function(request, callback) {
+ callback({message: 'foo'});
+ }
+ ];
+
+ backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) {
+ expect(error.message).to.be('foo');
+ done();
+ });
+ });
+ });
+
+ describe('with a registered projection', function() {
+ beforeEach(function() {
+ backend.addProjection('bookTitles', 'books', {title: true});
+ });
+
+ it('applies the projection to a snapshot', function(done) {
+ backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(snapshot.data.title).to.be('Don Quixote');
+ expect(snapshot.data.author).to.be(undefined);
+ done();
+ });
+ });
+ });
+ });
+
+ describe('a document that is currently deleted', function() {
+ beforeEach(function(done) {
+ var doc = backend.connect().get('books', 'catch-22');
+ doc.create({title: 'Catch 22'}, function(error) {
+ if (error) return done(error);
+ doc.del(function(error) {
+ done(error);
+ });
+ });
+ });
+
+ it('returns a null type', function(done) {
+ backend.connect().fetchSnapshot('books', 'catch-22', null, function(error, snapshot) {
+ expect(snapshot).to.eql({
+ id: 'catch-22',
+ v: 2,
+ type: null,
+ data: undefined,
+ m: null
+ });
+
+ done();
+ });
+ });
+
+ it('fetches v1', function(done) {
+ backend.connect().fetchSnapshot('books', 'catch-22', 1, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(snapshot).to.eql({
+ id: 'catch-22',
+ v: 1,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'Catch 22'
+ },
+ m: null
+ });
+
+ done();
+ });
+ });
+ });
+
+ describe('a document that was deleted and then created again', function() {
+ beforeEach(function(done) {
+ var doc = backend.connect().get('books', 'hitchhikers-guide');
+ doc.create({title: 'Hitchhiker\'s Guide to the Galaxy'}, function(error) {
+ if (error) return done(error);
+ doc.del(function(error) {
+ if (error) return done(error);
+ doc.create({title: 'The Restaurant at the End of the Universe'}, function(error) {
+ done(error);
+ });
+ });
+ });
+ });
+
+ it('fetches the latest version of the document', function(done) {
+ backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function(error, snapshot) {
+ if (error) return done(error);
+
+ expect(snapshot).to.eql({
+ id: 'hitchhikers-guide',
+ v: 3,
+ type: 'http://sharejs.org/types/JSONv0',
+ data: {
+ title: 'The Restaurant at the End of the Universe'
+ },
+ m: null
+ });
+
+ done();
+ });
+ });
+ });
+
+ describe('milestone snapshots enabled for every other version', function() {
+ var milestoneDb;
+ var db;
+ var backendWithMilestones;
+
+ beforeEach(function() {
+ var options = {interval: 2};
+ db = new MemoryDb();
+ milestoneDb = new MemoryMilestoneDb(options);
+ backendWithMilestones = new Backend({
+ db: db,
+ milestoneDb: milestoneDb
+ });
+ });
+
+ afterEach(function(done) {
+ backendWithMilestones.close(done);
+ });
+
+ it('fetches a snapshot using the milestone', function(done) {
+ var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
+
+ util.callInSeries([
+ function(next) {
+ doc.create({title: 'To Kill a Mocking Bird'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], oi: 'Harper Lea'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next);
+ },
+ function(next) {
+ sinon.spy(milestoneDb, 'getMilestoneSnapshot');
+ sinon.spy(db, 'getOps');
+ backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next);
+ },
+ function(snapshot, next) {
+ expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.be(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.be(true);
+ expect(snapshot.v).to.be(3);
+ expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
+ next();
+ },
+ done
+ ]);
+ });
+ });
+});
diff --git a/test/client/submit.js b/test/client/submit.js
index 4334b57e8..592aa9eff 100644
--- a/test/client/submit.js
+++ b/test/client/submit.js
@@ -2,28 +2,40 @@ var async = require('async');
var expect = require('expect.js');
var types = require('../../lib/types');
var deserializedType = require('./deserialized-type');
+var numberType = require('./number-type');
types.register(deserializedType.type);
types.register(deserializedType.type2);
+types.register(numberType.type);
module.exports = function() {
-describe('client submit', function() {
-
- it('can fetch an uncreated doc', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- expect(doc.data).equal(undefined);
- expect(doc.version).equal(null);
- doc.fetch(function(err) {
- if (err) return done(err);
+ describe('client submit', function() {
+ it('can fetch an uncreated doc', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
expect(doc.data).equal(undefined);
- expect(doc.version).equal(0);
- done();
+ expect(doc.version).equal(null);
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.data).equal(undefined);
+ expect(doc.version).equal(0);
+ done();
+ });
+ });
+
+ it('can fetch then create a new doc', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 3});
+ expect(doc.version).eql(1);
+ done();
+ });
+ });
});
- });
- it('can fetch then create a new doc', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.fetch(function(err) {
- if (err) return done(err);
+ it('can create a new doc without fetching', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
@@ -31,611 +43,603 @@ describe('client submit', function() {
done();
});
});
- });
- it('can create a new doc without fetching', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- expect(doc.data).eql({age: 3});
- expect(doc.version).eql(1);
- done();
- });
- });
+ it('can create then delete then create a doc', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 3});
+ expect(doc.version).eql(1);
- it('can create then delete then create a doc', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- expect(doc.data).eql({age: 3});
- expect(doc.version).eql(1);
+ doc.del(null, function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql(undefined);
+ expect(doc.version).eql(2);
- doc.del(null, function(err) {
- if (err) return done(err);
- expect(doc.data).eql(undefined);
- expect(doc.version).eql(2);
+ doc.create({age: 2}, function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 2});
+ expect(doc.version).eql(3);
+ done();
+ });
+ });
+ });
+ });
- doc.create({age: 2}, function(err) {
+ it('can create then submit an op', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 2});
- expect(doc.version).eql(3);
+ expect(doc.data).eql({age: 5});
+ expect(doc.version).eql(2);
done();
});
});
});
- });
- it('can create then submit an op', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 2}, function(err) {
+ it('can create then submit an op sync', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3});
+ expect(doc.data).eql({age: 3});
+ expect(doc.version).eql(null);
+ doc.submitOp({p: ['age'], na: 2});
+ expect(doc.data).eql({age: 5});
+ expect(doc.version).eql(null);
+ doc.whenNothingPending(done);
+ });
+
+ it('submitting an op from a future version fails', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 5});
- expect(doc.version).eql(2);
- done();
+ doc.version++;
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
+ expect(err).ok();
+ done();
+ });
});
});
- });
-
- it('can create then submit an op sync', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3});
- expect(doc.data).eql({age: 3});
- expect(doc.version).eql(null);
- doc.submitOp({p: ['age'], na: 2});
- expect(doc.data).eql({age: 5});
- expect(doc.version).eql(null);
- doc.whenNothingPending(done);
- });
- it('submitting an op from a future version fails', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.version++;
+ it('cannot submit op on an uncreated doc', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
doc.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).ok();
done();
});
});
- });
-
- it('cannot submit op on an uncreated doc', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.submitOp({p: ['age'], na: 2}, function(err) {
- expect(err).ok();
- done();
- });
- });
- it('cannot delete an uncreated doc', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.del(function(err) {
- expect(err).ok();
- done();
+ it('cannot delete an uncreated doc', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.del(function(err) {
+ expect(err).ok();
+ done();
+ });
});
- });
- it('ops submitted sync get composed', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3});
- doc.submitOp({p: ['age'], na: 2});
- doc.submitOp({p: ['age'], na: 2}, function(err) {
- if (err) return done(err);
- expect(doc.data).eql({age: 7});
- // Version is 1 instead of 3, because the create and ops got composed
- expect(doc.version).eql(1);
+ it('ops submitted sync get composed', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 11});
- // Ops get composed
- expect(doc.version).eql(2);
+ expect(doc.data).eql({age: 7});
+ // Version is 1 instead of 3, because the create and ops got composed
+ expect(doc.version).eql(1);
doc.submitOp({p: ['age'], na: 2});
- doc.del(function(err) {
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
- expect(doc.data).eql(undefined);
- // del DOES NOT get composed
- expect(doc.version).eql(4);
- done();
+ expect(doc.data).eql({age: 11});
+ // Ops get composed
+ expect(doc.version).eql(2);
+ doc.submitOp({p: ['age'], na: 2});
+ doc.del(function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql(undefined);
+ // del DOES NOT get composed
+ expect(doc.version).eql(4);
+ done();
+ });
});
});
});
- });
- it('does not compose ops when doc.preventCompose is true', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.preventCompose = true;
- doc.create({age: 3});
- doc.submitOp({p: ['age'], na: 2});
- doc.submitOp({p: ['age'], na: 2}, function(err) {
- if (err) return done(err);
- expect(doc.data).eql({age: 7});
- // Compare to version in above test
- expect(doc.version).eql(3);
+ it('does not compose ops when doc.preventCompose is true', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.preventCompose = true;
+ doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 11});
+ expect(doc.data).eql({age: 7});
// Compare to version in above test
- expect(doc.version).eql(5);
- done();
+ expect(doc.version).eql(3);
+ doc.submitOp({p: ['age'], na: 2});
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 11});
+ // Compare to version in above test
+ expect(doc.version).eql(5);
+ done();
+ });
});
});
- });
- it('resumes composing after doc.preventCompose is set back to false', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.preventCompose = true;
- doc.create({age: 3});
- doc.submitOp({p: ['age'], na: 2});
- doc.submitOp({p: ['age'], na: 2}, function(err) {
- if (err) return done(err);
- expect(doc.data).eql({age: 7});
- // Compare to version in above test
- expect(doc.version).eql(3);
- // Reset back to start composing ops again
- doc.preventCompose = false;
+ it('resumes composing after doc.preventCompose is set back to false', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.preventCompose = true;
+ doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 11});
+ expect(doc.data).eql({age: 7});
// Compare to version in above test
- expect(doc.version).eql(4);
- done();
+ expect(doc.version).eql(3);
+ // Reset back to start composing ops again
+ doc.preventCompose = false;
+ doc.submitOp({p: ['age'], na: 2});
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 11});
+ // Compare to version in above test
+ expect(doc.version).eql(4);
+ done();
+ });
});
});
- });
- it('can create a new doc then fetch', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.fetch(function(err) {
+ it('can create a new doc then fetch', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 3});
- expect(doc.version).eql(1);
- done();
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 3});
+ expect(doc.version).eql(1);
+ done();
+ });
});
});
- });
- it('calling create on the same doc twice fails', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.create({age: 4}, function(err) {
- expect(err).ok();
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
- done();
+ it('calling create on the same doc twice fails', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc.create({age: 4}, function(err) {
+ expect(err).ok();
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 3});
+ done();
+ });
});
});
- });
- it('trying to create an already created doc without fetching fails and fetches', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.create({age: 4}, function(err) {
- expect(err).ok();
- expect(doc2.version).equal(1);
- expect(doc2.data).eql({age: 3});
- done();
+ it('trying to create an already created doc without fetching fails and fetches', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc2.create({age: 4}, function(err) {
+ expect(err).ok();
+ expect(doc2.version).equal(1);
+ expect(doc2.data).eql({age: 3});
+ done();
+ });
});
});
- });
- it('server fetches and transforms by already committed op', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('server fetches and transforms by already committed op', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- doc2.submitOp({p: ['age'], na: 2}, function(err) {
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
- expect(doc2.version).equal(3);
- expect(doc2.data).eql({age: 6});
- done();
+ doc2.submitOp({p: ['age'], na: 2}, function(err) {
+ if (err) return done(err);
+ expect(doc2.version).equal(3);
+ expect(doc2.data).eql({age: 6});
+ done();
+ });
});
});
});
});
- });
- it('submit fails if the server is missing ops required for transforming', function(done) {
- this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
- callback(null, []);
- };
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('submit fails if the server is missing ops required for transforming', function(done) {
+ this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
+ callback(null, []);
+ };
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- doc2.submitOp({p: ['age'], na: 2}, function(err) {
- expect(err).ok();
- done();
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ if (err) return done(err);
+ doc2.submitOp({p: ['age'], na: 2}, function(err) {
+ expect(err).ok();
+ done();
+ });
});
});
});
});
- });
- it('submit fails if ops returned are not the expected version', function(done) {
- var getOpsToSnapshot = this.backend.db.getOpsToSnapshot;
- this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
- getOpsToSnapshot.call(this, collection, id, from, snapshot, options, function(err, ops) {
- ops[0].v++;
- callback(null, ops);
- });
- };
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('submit fails if ops returned are not the expected version', function(done) {
+ var getOpsToSnapshot = this.backend.db.getOpsToSnapshot;
+ this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
+ getOpsToSnapshot.call(this, collection, id, from, snapshot, options, function(err, ops) {
+ ops[0].v++;
+ callback(null, ops);
+ });
+ };
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- doc2.submitOp({p: ['age'], na: 2}, function(err) {
- expect(err).ok();
- done();
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ if (err) return done(err);
+ doc2.submitOp({p: ['age'], na: 2}, function(err) {
+ expect(err).ok();
+ done();
+ });
});
});
});
});
- });
- function delayedReconnect(backend, connection) {
+ function delayedReconnect(backend, connection) {
// Disconnect after the message has sent and before the server will have
// had a chance to reply
- process.nextTick(function() {
- connection.close();
- // Reconnect once the server has a chance to save the op data
- setTimeout(function() {
- backend.connect(connection);
- }, 100);
- });
- }
-
- it('resends create when disconnected before ack', function(done) {
- var backend = this.backend;
- var doc = backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
- done();
- });
- delayedReconnect(backend, doc.connection);
- });
-
- it('resent create on top of deleted doc gets proper starting version', function(done) {
- var backend = this.backend;
- var doc = backend.connect().get('dogs', 'fido');
- doc.create({age: 4}, function(err) {
- if (err) return done(err);
- doc.del(function(err) {
- if (err) return done(err);
-
- var doc2 = backend.connect().get('dogs', 'fido');
- doc2.create({age: 3}, function(err) {
- if (err) return done(err);
- expect(doc2.version).equal(3);
- expect(doc2.data).eql({age: 3});
- done();
- });
- delayedReconnect(backend, doc2.connection);
+ process.nextTick(function() {
+ connection.close();
+ // Reconnect once the server has a chance to save the op data
+ setTimeout(function() {
+ backend.connect(connection);
+ }, 100);
});
- });
- });
+ }
- it('resends delete when disconnected before ack', function(done) {
- var backend = this.backend;
- var doc = backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.del(function(err) {
+ it('resends create when disconnected before ack', function(done) {
+ var backend = this.backend;
+ var doc = backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.version).equal(2);
- expect(doc.data).eql(undefined);
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 3});
done();
});
delayedReconnect(backend, doc.connection);
});
- });
- it('op submitted during inflight create does not compose and gets flushed', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3});
- // Submit an op after message is sent but before server has a chance to reply
- process.nextTick(function() {
- doc.submitOp({p: ['age'], na: 2}, function(err) {
+ it('resent create on top of deleted doc gets proper starting version', function(done) {
+ var backend = this.backend;
+ var doc = backend.connect().get('dogs', 'fido');
+ doc.create({age: 4}, function(err) {
if (err) return done(err);
- expect(doc.version).equal(2);
- expect(doc.data).eql({age: 5});
- done();
+ doc.del(function(err) {
+ if (err) return done(err);
+
+ var doc2 = backend.connect().get('dogs', 'fido');
+ doc2.create({age: 3}, function(err) {
+ if (err) return done(err);
+ expect(doc2.version).equal(3);
+ expect(doc2.data).eql({age: 3});
+ done();
+ });
+ delayedReconnect(backend, doc2.connection);
+ });
});
});
- });
- it('can commit then fetch in a new connection to get the same data', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('resends delete when disconnected before ack', function(done) {
+ var backend = this.backend;
+ var doc = backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 3});
- expect(doc2.data).eql({age: 3});
- expect(doc.version).eql(1);
- expect(doc2.version).eql(1);
- expect(doc.data).not.equal(doc2.data);
- done();
+ doc.del(function(err) {
+ if (err) return done(err);
+ expect(doc.version).equal(2);
+ expect(doc.data).eql(undefined);
+ done();
+ });
+ delayedReconnect(backend, doc.connection);
});
});
- });
- it('an op submitted concurrently is transformed by the first', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
- if (err) return done(err);
- var count = 0;
+ it('op submitted during inflight create does not compose and gets flushed', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3});
+ // Submit an op after message is sent but before server has a chance to reply
+ process.nextTick(function() {
doc.submitOp({p: ['age'], na: 2}, function(err) {
- count++;
if (err) return done(err);
- if (count === 1) {
- expect(doc.data).eql({age: 5});
- expect(doc.version).eql(2);
- } else {
- expect(doc.data).eql({age: 12});
- expect(doc.version).eql(3);
- done();
- }
+ expect(doc.version).equal(2);
+ expect(doc.data).eql({age: 5});
+ done();
});
- doc2.submitOp({p: ['age'], na: 7}, function(err) {
- count++;
+ });
+ });
+
+ it('can commit then fetch in a new connection to get the same data', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc2.fetch(function(err) {
if (err) return done(err);
- if (count === 1) {
- expect(doc2.data).eql({age: 10});
- expect(doc2.version).eql(2);
- } else {
- expect(doc2.data).eql({age: 12});
- expect(doc2.version).eql(3);
- done();
- }
+ expect(doc.data).eql({age: 3});
+ expect(doc2.data).eql({age: 3});
+ expect(doc.version).eql(1);
+ expect(doc2.version).eql(1);
+ expect(doc.data).not.equal(doc2.data);
+ done();
});
});
});
- });
- it('second of two concurrent creates is rejected', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- var count = 0;
- doc.create({age: 3}, function(err) {
- count++;
- if (count === 1) {
+ it('an op submitted concurrently is transformed by the first', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.version).eql(1);
- expect(doc.data).eql({age: 3});
- } else {
- expect(err).ok();
- expect(doc.version).eql(1);
- expect(doc.data).eql({age: 5});
- done();
- }
- });
- doc2.create({age: 5}, function(err) {
- count++;
- if (count === 1) {
- if (err) return done(err);
- expect(doc2.version).eql(1);
- expect(doc2.data).eql({age: 5});
- } else {
- expect(err).ok();
- expect(doc2.version).eql(1);
- expect(doc2.data).eql({age: 3});
- done();
- }
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ var count = 0;
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
+ count++;
+ if (err) return done(err);
+ if (count === 1) {
+ expect(doc.data).eql({age: 5});
+ expect(doc.version).eql(2);
+ } else {
+ expect(doc.data).eql({age: 12});
+ expect(doc.version).eql(3);
+ done();
+ }
+ });
+ doc2.submitOp({p: ['age'], na: 7}, function(err) {
+ count++;
+ if (err) return done(err);
+ if (count === 1) {
+ expect(doc2.data).eql({age: 10});
+ expect(doc2.version).eql(2);
+ } else {
+ expect(doc2.data).eql({age: 12});
+ expect(doc2.version).eql(3);
+ done();
+ }
+ });
+ });
+ });
});
- });
- it('concurrent delete operations transform', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
- if (err) return done(err);
- var count = 0;
- doc.del(function(err) {
- count++;
+ it('second of two concurrent creates is rejected', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ var count = 0;
+ doc.create({age: 3}, function(err) {
+ count++;
+ if (count === 1) {
if (err) return done(err);
- if (count === 1) {
- expect(doc.version).eql(2);
- expect(doc.data).eql(undefined);
- } else {
- expect(doc.version).eql(3);
- expect(doc.data).eql(undefined);
- done();
- }
- });
- doc2.del(function(err) {
- count++;
+ expect(doc.version).eql(1);
+ expect(doc.data).eql({age: 3});
+ } else {
+ expect(err).ok();
+ expect(doc.version).eql(1);
+ expect(doc.data).eql({age: 5});
+ done();
+ }
+ });
+ doc2.create({age: 5}, function(err) {
+ count++;
+ if (count === 1) {
if (err) return done(err);
- if (count === 1) {
- expect(doc2.version).eql(2);
- expect(doc2.data).eql(undefined);
- } else {
- expect(doc2.version).eql(3);
- expect(doc2.data).eql(undefined);
- done();
- }
- });
+ expect(doc2.version).eql(1);
+ expect(doc2.data).eql({age: 5});
+ } else {
+ expect(err).ok();
+ expect(doc2.version).eql(1);
+ expect(doc2.data).eql({age: 3});
+ done();
+ }
});
});
- });
- it('submits retry below the backend.maxSubmitRetries threshold', function(done) {
- this.backend.maxSubmitRetries = 10;
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('concurrent delete operations transform', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- var count = 0;
- var cb = function(err) {
- count++;
+ doc2.fetch(function(err) {
if (err) return done(err);
- if (count > 1) done();
- };
- doc.submitOp({p: ['age'], na: 2}, cb);
- doc2.submitOp({p: ['age'], na: 7}, cb);
+ var count = 0;
+ doc.del(function(err) {
+ count++;
+ if (err) return done(err);
+ if (count === 1) {
+ expect(doc.version).eql(2);
+ expect(doc.data).eql(undefined);
+ } else {
+ expect(doc.version).eql(3);
+ expect(doc.data).eql(undefined);
+ done();
+ }
+ });
+ doc2.del(function(err) {
+ count++;
+ if (err) return done(err);
+ if (count === 1) {
+ expect(doc2.version).eql(2);
+ expect(doc2.data).eql(undefined);
+ } else {
+ expect(doc2.version).eql(3);
+ expect(doc2.data).eql(undefined);
+ done();
+ }
+ });
+ });
});
});
- });
- it('submits fail above the backend.maxSubmitRetries threshold', function(done) {
- this.backend.maxSubmitRetries = 0;
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('submits retry below the backend.maxSubmitRetries threshold', function(done) {
+ this.backend.maxSubmitRetries = 10;
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- var count = 0;
- var cb = function(err) {
- count++;
- if (count === 1) {
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ var count = 0;
+ var cb = function(err) {
+ count++;
if (err) return done(err);
- } else {
- expect(err).ok();
- done();
- }
- };
- doc.submitOp({p: ['age'], na: 2}, cb);
- doc2.submitOp({p: ['age'], na: 7}, cb);
+ if (count > 1) done();
+ };
+ doc.submitOp({p: ['age'], na: 2}, cb);
+ doc2.submitOp({p: ['age'], na: 7}, cb);
+ });
});
});
- });
- it('pending delete transforms incoming ops', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('submits fail above the backend.maxSubmitRetries threshold', function(done) {
+ var backend = this.backend;
+ this.backend.maxSubmitRetries = 0;
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.submitOp({p: ['age'], na: 1}, function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- async.parallel([
- function(cb) { doc.del(cb); },
- function(cb) { doc.create({age: 5}, cb); }
- ], function(err) {
- if (err) return done(err);
- expect(doc.version).equal(4);
- expect(doc.data).eql({age: 5});
+ var docCallback;
+ var doc2Callback;
+ // The submit retry happens just after an op is committed. This hook into the middleware
+ // catches both ops just before they're about to be committed. This ensures that both ops
+ // are certainly working on the same snapshot (ie one op hasn't been committed before the
+ // other fetches the snapshot to apply to). By storing the callbacks, we can then
+ // manually trigger the callbacks, first calling doc, and when we know that's been committed,
+ // we then commit doc2.
+ backend.use('commit', function(request, callback) {
+ if (request.op.op[0].na === 2) docCallback = callback;
+ if (request.op.op[0].na === 7) doc2Callback = callback;
+
+ // Wait until both ops have been applied to the same snapshot and are about to be committed
+ if (docCallback && doc2Callback) {
+ // Trigger the first op's commit and then the second one later, which will cause the
+ // second op to retry
+ docCallback();
+ }
+ });
+ doc.submitOp({p: ['age'], na: 2}, function(error) {
+ if (error) return done(error);
+ // When we know the first op has been committed, we try to commit the second op, which will
+ // fail because it's working on an out-of-date snapshot. It will retry, but exceed the
+ // maxSubmitRetries limit of 0
+ doc2Callback();
+ });
+ doc2.submitOp({p: ['age'], na: 7}, function(error) {
+ expect(error).ok();
done();
});
});
});
});
- });
- it('pending delete transforms incoming delete', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('pending delete transforms incoming ops', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.del(function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- async.parallel([
- function(cb) { doc.del(cb); },
- function(cb) { doc.create({age: 5}, cb); }
- ], function(err) {
+ doc2.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
- expect(doc.version).equal(4);
- expect(doc.data).eql({age: 5});
- done();
+ async.parallel([
+ function(cb) {
+ doc.del(cb);
+ },
+ function(cb) {
+ doc.create({age: 5}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(doc.version).equal(4);
+ expect(doc.data).eql({age: 5});
+ done();
+ });
});
});
});
});
- });
- it('submitting op after delete returns error', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('pending delete transforms incoming delete', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.del(function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
- expect(err).ok();
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
- done();
+ doc2.del(function(err) {
+ if (err) return done(err);
+ async.parallel([
+ function(cb) {
+ doc.del(cb);
+ },
+ function(cb) {
+ doc.create({age: 5}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(doc.version).equal(4);
+ expect(doc.data).eql({age: 5});
+ done();
+ });
});
});
});
});
- });
- it('transforming pending op by server delete returns error', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('submitting op after delete returns error', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.del(function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- doc.pause();
- doc.submitOp({p: ['age'], na: 1}, function(err) {
- expect(err).ok();
- expect(doc.version).equal(2);
- expect(doc.data).eql(undefined);
- done();
+ doc2.del(function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ expect(err).ok();
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 3});
+ done();
+ });
});
- doc.fetch();
});
});
});
- });
- it('transforming pending op by server create returns error', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.del(function(err) {
+ it('transforming pending op by server delete returns error', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
if (err) return done(err);
- doc2.create({age: 5}, function(err) {
+ doc2.del(function(err) {
if (err) return done(err);
doc.pause();
- doc.create({age: 9}, function(err) {
- expect(err).ok();
- expect(doc.version).equal(3);
- expect(doc.data).eql({age: 5});
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ expect(err.code).to.equal(4017);
+ expect(doc.version).equal(2);
+ expect(doc.data).eql(undefined);
done();
});
doc.fetch();
@@ -643,88 +647,117 @@ describe('client submit', function() {
});
});
});
- });
- it('second client can create following delete', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.del(function(err) {
+ it('transforming pending op by server create returns error', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.create({age: 5}, function(err) {
+ doc.del(function(err) {
if (err) return done(err);
- expect(doc2.version).eql(3);
- expect(doc2.data).eql({age: 5});
- done();
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ doc2.create({age: 5}, function(err) {
+ if (err) return done(err);
+ doc.pause();
+ doc.create({age: 9}, function(err) {
+ expect(err.code).to.equal(4018);
+ expect(doc.version).equal(3);
+ expect(doc.data).eql({age: 5});
+ done();
+ });
+ doc.fetch();
+ });
+ });
});
});
});
- });
- it('doc.pause() prevents ops from being sent', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.pause();
- doc.create({age: 3}, done);
- done();
- });
+ it('second client can create following delete', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc.del(function(err) {
+ if (err) return done(err);
+ doc2.create({age: 5}, function(err) {
+ if (err) return done(err);
+ expect(doc2.version).eql(3);
+ expect(doc2.data).eql({age: 5});
+ done();
+ });
+ });
+ });
+ });
- it('can call doc.resume() without pausing', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.resume();
- doc.create({age: 3}, done);
- });
+ it('doc.pause() prevents ops from being sent', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.pause();
+ doc.create({age: 3}, done);
+ done();
+ });
- it('doc.resume() resumes sending ops after pause', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.pause();
- doc.create({age: 3}, done);
- doc.resume();
- });
+ it('can call doc.resume() without pausing', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.resume();
+ doc.create({age: 3}, done);
+ });
- it('pending ops are transformed by ops from other clients', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
- if (err) return done(err);
- doc.pause();
- doc.submitOp({p: ['age'], na: 1});
- doc.submitOp({p: ['color'], oi: 'gold'});
- expect(doc.version).equal(1);
+ it('doc.resume() resumes sending ops after pause', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.pause();
+ doc.create({age: 3}, done);
+ doc.resume();
+ });
- doc2.submitOp({p: ['age'], na: 5});
- process.nextTick(function() {
- doc2.submitOp({p: ['sex'], oi: 'female'}, function(err) {
- if (err) return done(err);
- expect(doc2.version).equal(3);
+ it('pending ops are transformed by ops from other clients', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ doc.pause();
+ doc.submitOp({p: ['age'], na: 1});
+ doc.submitOp({p: ['color'], oi: 'gold'});
+ expect(doc.version).equal(1);
- async.parallel([
- function(cb) { doc.fetch(cb); },
- function(cb) { doc2.fetch(cb); }
- ], function(err) {
+ doc2.submitOp({p: ['age'], na: 5});
+ process.nextTick(function() {
+ doc2.submitOp({p: ['sex'], oi: 'female'}, function(err) {
if (err) return done(err);
- expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
- expect(doc.version).equal(3);
- expect(doc.hasPending()).equal(true);
-
- expect(doc2.data).eql({age: 8, sex: 'female'});
expect(doc2.version).equal(3);
- expect(doc2.hasPending()).equal(false);
- doc.resume();
- doc.whenNothingPending(function() {
- doc2.fetch(function(err) {
- if (err) return done(err);
- expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
- expect(doc.version).equal(4);
- expect(doc.hasPending()).equal(false);
-
- expect(doc2.data).eql({age: 9, color: 'gold', sex: 'female'});
- expect(doc2.version).equal(4);
- expect(doc2.hasPending()).equal(false);
- done();
+ async.parallel([
+ function(cb) {
+ doc.fetch(cb);
+ },
+ function(cb) {
+ doc2.fetch(cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
+ expect(doc.version).equal(3);
+ expect(doc.hasPending()).equal(true);
+
+ expect(doc2.data).eql({age: 8, sex: 'female'});
+ expect(doc2.version).equal(3);
+ expect(doc2.hasPending()).equal(false);
+
+ doc.resume();
+ doc.whenNothingPending(function() {
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
+ expect(doc.version).equal(4);
+ expect(doc.hasPending()).equal(false);
+
+ expect(doc2.data).eql({age: 9, color: 'gold', sex: 'female'});
+ expect(doc2.version).equal(4);
+ expect(doc2.hasPending()).equal(false);
+ done();
+ });
});
});
});
@@ -732,444 +765,453 @@ describe('client submit', function() {
});
});
});
- });
- it('snapshot fetch does not revert the version of deleted doc without pending ops', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- this.backend.use('doc', function(request, next) {
- doc.create({age: 3});
- doc.del(next);
- });
- doc.fetch(function(err) {
- if (err) return done(err);
- expect(doc.version).equal(2);
- done();
+ it('snapshot fetch does not revert the version of deleted doc without pending ops', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ this.backend.use('doc', function(request, next) {
+ doc.create({age: 3});
+ doc.del(next);
+ });
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.version).equal(2);
+ done();
+ });
});
- });
- it('snapshot fetch does not revert the version of deleted doc with pending ops', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- this.backend.use('doc', function(request, next) {
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- next();
+ it('snapshot fetch does not revert the version of deleted doc with pending ops', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ this.backend.use('doc', function(request, next) {
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ next();
+ });
+ process.nextTick(function() {
+ doc.pause();
+ doc.del(done);
+ });
});
- process.nextTick(function() {
- doc.pause();
- doc.del(done);
+ doc.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc.version).equal(1);
+ doc.resume();
});
});
- doc.fetch(function(err) {
- if (err) return done(err);
- expect(doc.version).equal(1);
- doc.resume();
- });
- });
- it('snapshot fetch from query does not advance version of doc with pending ops', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({name: 'kido'}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('snapshot fetch from query does not advance version of doc with pending ops', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({name: 'kido'}, function(err) {
if (err) return done(err);
- doc2.submitOp({p: ['name', 0], si: 'f'}, function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- expect(doc2.data).eql({name: 'fkido'});
- doc.connection.createFetchQuery('dogs', {}, null, function(err) {
+ doc2.submitOp({p: ['name', 0], si: 'f'}, function(err) {
if (err) return done(err);
- doc.resume();
+ expect(doc2.data).eql({name: 'fkido'});
+ doc.connection.createFetchQuery('dogs', {}, null, function(err) {
+ if (err) return done(err);
+ doc.resume();
+ });
});
});
});
- });
- process.nextTick(function() {
- doc.pause();
- doc.submitOp({p: ['name', 0], sd: 'k'}, function(err) {
- if (err) return done(err);
+ process.nextTick(function() {
doc.pause();
- doc2.fetch(function(err) {
+ doc.submitOp({p: ['name', 0], sd: 'k'}, function(err) {
if (err) return done(err);
- expect(doc2.version).equal(3);
- expect(doc2.data).eql({name: 'fido'});
- done();
- });
- });
- doc.del();
- });
- });
-
- it('passing an error in submit middleware rejects a create and calls back with the erorr', function(done) {
- this.backend.use('submit', function(request, next) {
- next({message: 'Custom error'});
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- expect(err.message).equal('Custom error');
- expect(doc.version).equal(0);
- expect(doc.data).equal(undefined);
- done();
- });
- expect(doc.version).equal(null);
- expect(doc.data).eql({age: 3});
- });
-
- it('passing an error in submit middleware rejects a create and throws the erorr', function(done) {
- this.backend.use('submit', function(request, next) {
- next({message: 'Custom error'});
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3});
- expect(doc.version).equal(null);
- expect(doc.data).eql({age: 3});
- doc.on('error', function(err) {
- expect(err.message).equal('Custom error');
- expect(doc.version).equal(0);
- expect(doc.data).equal(undefined);
- done();
- });
- });
-
- it('passing an error in submit middleware rejects pending ops after failed create', function(done) {
- var submitCount = 0;
- this.backend.use('submit', function(request, next) {
- submitCount++;
- if (submitCount === 1) return next({message: 'Custom error'});
- next();
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- async.parallel([
- function(cb) {
- doc.create({age: 3}, function(err) {
- expect(err.message).equal('Custom error');
- expect(doc.version).equal(0);
- expect(doc.data).equal(undefined);
- cb();
- });
- expect(doc.version).equal(null);
- expect(doc.data).eql({age: 3});
- },
- function(cb) {
- process.nextTick(function() {
- doc.submitOp({p: ['age'], na: 1}, function(err) {
- expect(err.message).equal('Custom error');
- expect(doc.version).equal(0);
- expect(doc.data).equal(undefined);
- expect(submitCount).equal(1);
- cb();
+ doc.pause();
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc2.version).equal(3);
+ expect(doc2.data).eql({name: 'fido'});
+ done();
});
- expect(doc.version).equal(null);
- expect(doc.data).eql({age: 4});
});
- }
- ], done);
- });
-
- it('request.rejectedError() soft rejects a create', function(done) {
- this.backend.use('submit', function(request, next) {
- next(request.rejectedError());
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- expect(doc.version).equal(0);
- expect(doc.data).equal(undefined);
- done();
- });
- expect(doc.version).equal(null);
- expect(doc.data).eql({age: 3});
- });
-
- it('request.rejectedError() soft rejects a create without callback', function(done) {
- this.backend.use('submit', function(request, next) {
- next(request.rejectedError());
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3});
- expect(doc.version).equal(null);
- expect(doc.data).eql({age: 3});
- doc.whenNothingPending(function() {
- expect(doc.version).equal(0);
- expect(doc.data).equal(undefined);
- done();
+ doc.del();
+ });
});
- });
- it('passing an error in submit middleware rejects an op and calls back with the erorr', function(done) {
- this.backend.use('submit', function(request, next) {
- if (request.op.op) return next({message: 'Custom error'});
- next();
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ it('passing an error in submit middleware rejects a create and calls back with the erorr', function(done) {
+ this.backend.use('submit', function(request, next) {
+ next({message: 'Custom error'});
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
expect(err.message).equal('Custom error');
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
+ expect(doc.version).equal(0);
+ expect(doc.data).equal(undefined);
done();
});
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 4});
+ expect(doc.version).equal(null);
+ expect(doc.data).eql({age: 3});
});
- });
- it('passing an error in submit middleware rejects an op and emits the erorr', function(done) {
- this.backend.use('submit', function(request, next) {
- if (request.op.op) return next({message: 'Custom error'});
- next();
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1});
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 4});
+ it('passing an error in submit middleware rejects a create and throws the erorr', function(done) {
+ this.backend.use('submit', function(request, next) {
+ next({message: 'Custom error'});
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3});
+ expect(doc.version).equal(null);
+ expect(doc.data).eql({age: 3});
doc.on('error', function(err) {
expect(err.message).equal('Custom error');
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
+ expect(doc.version).equal(0);
+ expect(doc.data).equal(undefined);
done();
});
});
- });
- it('passing an error in submit middleware transforms pending ops after failed op', function(done) {
- var submitCount = 0;
- this.backend.use('submit', function(request, next) {
- submitCount++;
- if (submitCount === 2) return next({message: 'Custom error'});
- next();
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
+ it('passing an error in submit middleware rejects pending ops after failed create', function(done) {
+ var submitCount = 0;
+ this.backend.use('submit', function(request, next) {
+ submitCount++;
+ if (submitCount === 1) return next({message: 'Custom error'});
+ next();
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
async.parallel([
function(cb) {
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ doc.create({age: 3}, function(err) {
expect(err.message).equal('Custom error');
+ expect(doc.version).equal(0);
+ expect(doc.data).equal(undefined);
cb();
});
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 4});
+ expect(doc.version).equal(null);
+ expect(doc.data).eql({age: 3});
},
function(cb) {
process.nextTick(function() {
- doc.submitOp({p: ['age'], na: 5}, cb);
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 9});
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ expect(err.message).equal('Custom error');
+ expect(doc.version).equal(0);
+ expect(doc.data).equal(undefined);
+ expect(submitCount).equal(1);
+ cb();
+ });
+ expect(doc.version).equal(null);
+ expect(doc.data).eql({age: 4});
});
}
- ], function(err) {
+ ], done);
+ });
+
+ it('request.rejectedError() soft rejects a create', function(done) {
+ this.backend.use('submit', function(request, next) {
+ next(request.rejectedError());
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.version).equal(2);
- expect(doc.data).eql({age: 8});
- expect(submitCount).equal(3);
+ expect(doc.version).equal(0);
+ expect(doc.data).equal(undefined);
done();
});
+ expect(doc.version).equal(null);
+ expect(doc.data).eql({age: 3});
});
- });
- it('request.rejectedError() soft rejects an op', function(done) {
- this.backend.use('submit', function(request, next) {
- if (request.op.op) return next(request.rejectedError());
- next();
+ it('request.rejectedError() soft rejects a create without callback', function(done) {
+ this.backend.use('submit', function(request, next) {
+ next(request.rejectedError());
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3});
+ expect(doc.version).equal(null);
+ expect(doc.data).eql({age: 3});
+ doc.whenNothingPending(function() {
+ expect(doc.version).equal(0);
+ expect(doc.data).equal(undefined);
+ done();
+ });
});
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+
+ it('passing an error in submit middleware rejects an op and calls back with the erorr', function(done) {
+ this.backend.use('submit', function(request, next) {
+ if (request.op.op) return next({message: 'Custom error'});
+ next();
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ expect(err.message).equal('Custom error');
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 3});
+ done();
+ });
expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
- done();
+ expect(doc.data).eql({age: 4});
});
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 4});
});
- });
- it('request.rejectedError() soft rejects an op without callback', function(done) {
- this.backend.use('submit', function(request, next) {
- if (request.op.op) return next(request.rejectedError());
- next();
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1});
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 4});
- doc.whenNothingPending(function() {
+ it('passing an error in submit middleware rejects an op and emits the erorr', function(done) {
+ this.backend.use('submit', function(request, next) {
+ if (request.op.op) return next({message: 'Custom error'});
+ next();
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 1});
expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
- done();
+ expect(doc.data).eql({age: 4});
+ doc.on('error', function(err) {
+ expect(err.message).equal('Custom error');
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 3});
+ done();
+ });
});
});
- });
- it('setting op.op to null makes it a no-op while returning success to the submitting client', function(done) {
- this.backend.use('submit', function(request, next) {
- if (request.op) request.op.op = null;
- next();
+ it('passing an error in submit middleware transforms pending ops after failed op', function(done) {
+ var submitCount = 0;
+ this.backend.use('submit', function(request, next) {
+ submitCount++;
+ if (submitCount === 2) return next({message: 'Custom error'});
+ next();
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ async.parallel([
+ function(cb) {
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ expect(err.message).equal('Custom error');
+ cb();
+ });
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 4});
+ },
+ function(cb) {
+ process.nextTick(function() {
+ doc.submitOp({p: ['age'], na: 5}, cb);
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 9});
+ });
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(doc.version).equal(2);
+ expect(doc.data).eql({age: 8});
+ expect(submitCount).equal(3);
+ done();
+ });
+ });
});
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+
+ it('request.rejectedError() soft rejects an op', function(done) {
+ this.backend.use('submit', function(request, next) {
+ if (request.op.op) return next(request.rejectedError());
+ next();
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.version).equal(2);
- expect(doc.data).eql({age: 4});
- doc2.fetch(function(err) {
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
- expect(doc2.version).equal(2);
- expect(doc2.data).eql({age: 3});
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 3});
done();
});
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 4});
});
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 4});
});
- });
- it('submitting an invalid op message returns error', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc._submit({}, null, function(err) {
- expect(err).ok();
- done();
+ it('request.rejectedError() soft rejects an op without callback', function(done) {
+ this.backend.use('submit', function(request, next) {
+ if (request.op.op) return next(request.rejectedError());
+ next();
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 1});
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 4});
+ doc.whenNothingPending(function() {
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 3});
+ done();
+ });
});
});
- });
- it('hasWritePending is false when create\'s callback is executed', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- expect(doc.hasWritePending()).equal(false);
- done();
+ it('setting op.op to null makes it a no-op while returning success to the submitting client', function(done) {
+ this.backend.use('submit', function(request, next) {
+ if (request.op) request.op.op = null;
+ next();
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ if (err) return done(err);
+ expect(doc.version).equal(2);
+ expect(doc.data).eql({age: 4});
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc2.version).equal(2);
+ expect(doc2.data).eql({age: 3});
+ done();
+ });
+ });
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 4});
+ });
});
- });
- it('hasWritePending is false when submimtOp\'s callback is executed', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 2}, function(err) {
+ it('submitting an invalid op message returns error', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.hasWritePending()).equal(false);
- done();
+ doc._submit({}, null, function(err) {
+ expect(err).ok();
+ done();
+ });
});
});
- });
- it('hasWritePending is false when del\'s callback is executed', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc.del(function(err) {
+ it('allows snapshot and op to be a non-object', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create(5, numberType.type.uri, function(err) {
if (err) return done(err);
- expect(doc.hasWritePending()).equal(false);
- done();
+ expect(doc.data).to.equal(5);
+ doc.submitOp(2, function(err) {
+ if (err) return done(err);
+ expect(doc.data).to.equal(7);
+ done();
+ });
});
});
- });
- describe('type.deserialize', function() {
- it('can create a new doc', function(done) {
+ it('hasWritePending is false when create\'s callback is executed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
- doc.create([3], deserializedType.type.uri, function(err) {
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.data).a(deserializedType.Node);
- expect(doc.data).eql({value: 3, next: null});
+ expect(doc.hasWritePending()).equal(false);
done();
});
});
- it('is stored serialized in backend', function(done) {
- var db = this.backend.db;
+ it('hasWritePending is false when submimtOp\'s callback is executed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
- doc.create([3], deserializedType.type.uri, function(err) {
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- db.getSnapshot('dogs', 'fido', null, null, function(err, snapshot) {
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
- expect(snapshot.data).eql([3]);
+ expect(doc.hasWritePending()).equal(false);
done();
});
});
});
- it('deserializes on fetch', function(done) {
+ it('hasWritePending is false when del\'s callback is executed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- var backend = this.backend;
- doc.create([3], deserializedType.type.uri, function(err) {
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.fetch(function(err) {
+ doc.del(function(err) {
if (err) return done(err);
- expect(doc2.data).a(deserializedType.Node);
- expect(doc2.data).eql({value: 3, next: null});
+ expect(doc.hasWritePending()).equal(false);
done();
});
});
});
- it('can create then submit an op', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create([3], deserializedType.type.uri, function(err) {
- if (err) return done(err);
- doc.submitOp({insert: 0, value: 2}, function(err) {
+ describe('type.deserialize', function() {
+ it('can create a new doc', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create([3], deserializedType.type.uri, function(err) {
if (err) return done(err);
- expect(doc.data).eql({value: 2, next: {value: 3, next: null}});
+ expect(doc.data).a(deserializedType.Node);
+ expect(doc.data).eql({value: 3, next: null});
done();
});
});
- });
- it('server fetches and transforms by already committed op', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- var backend = this.backend;
- doc.create([3], deserializedType.type.uri, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it('is stored serialized in backend', function(done) {
+ var db = this.backend.db;
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create([3], deserializedType.type.uri, function(err) {
+ if (err) return done(err);
+ db.getSnapshot('dogs', 'fido', null, null, function(err, snapshot) {
+ if (err) return done(err);
+ expect(snapshot.data).eql([3]);
+ done();
+ });
+ });
+ });
+
+ it('deserializes on fetch', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create([3], deserializedType.type.uri, function(err) {
+ if (err) return done(err);
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ expect(doc2.data).a(deserializedType.Node);
+ expect(doc2.data).eql({value: 3, next: null});
+ done();
+ });
+ });
+ });
+
+ it('can create then submit an op', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create([3], deserializedType.type.uri, function(err) {
if (err) return done(err);
doc.submitOp({insert: 0, value: 2}, function(err) {
if (err) return done(err);
- doc2.submitOp({insert: 1, value: 4}, function(err) {
+ expect(doc.data).eql({value: 2, next: {value: 3, next: null}});
+ done();
+ });
+ });
+ });
+
+ it('server fetches and transforms by already committed op', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create([3], deserializedType.type.uri, function(err) {
+ if (err) return done(err);
+ doc2.fetch(function(err) {
+ if (err) return done(err);
+ doc.submitOp({insert: 0, value: 2}, function(err) {
if (err) return done(err);
- expect(doc2.data).eql({value: 2, next: {value: 3, next: {value: 4, next: null}}});
- done();
+ doc2.submitOp({insert: 1, value: 4}, function(err) {
+ if (err) return done(err);
+ expect(doc2.data).eql({value: 2, next: {value: 3, next: {value: 4, next: null}}});
+ done();
+ });
});
});
});
});
});
- });
- describe('type.createDeserialized', function() {
- it('can create a new doc', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create([3], deserializedType.type2.uri, function(err) {
- if (err) return done(err);
- expect(doc.data).a(deserializedType.Node);
- expect(doc.data).eql({value: 3, next: null});
- done();
+ describe('type.createDeserialized', function() {
+ it('can create a new doc', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create([3], deserializedType.type2.uri, function(err) {
+ if (err) return done(err);
+ expect(doc.data).a(deserializedType.Node);
+ expect(doc.data).eql({value: 3, next: null});
+ done();
+ });
});
- });
- it('can create a new doc from deserialized form', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create(new deserializedType.Node(3), deserializedType.type2.uri, function(err) {
- if (err) return done(err);
- expect(doc.data).a(deserializedType.Node);
- expect(doc.data).eql({value: 3, next: null});
- done();
+ it('can create a new doc from deserialized form', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create(new deserializedType.Node(3), deserializedType.type2.uri, function(err) {
+ if (err) return done(err);
+ expect(doc.data).a(deserializedType.Node);
+ expect(doc.data).eql({value: 3, next: null});
+ done();
+ });
});
});
});
-
-});
};
diff --git a/test/client/subscribe.js b/test/client/subscribe.js
index b24a94749..2af398681 100644
--- a/test/client/subscribe.js
+++ b/test/client/subscribe.js
@@ -2,627 +2,688 @@ var expect = require('expect.js');
var async = require('async');
module.exports = function() {
-describe('client subscribe', function() {
+ describe('client subscribe', function() {
+ it('can call bulk without doing any actions', function() {
+ var connection = this.backend.connect();
+ connection.startBulk();
+ connection.endBulk();
+ });
- it('can call bulk without doing any actions', function() {
- var connection = this.backend.connect();
- connection.startBulk();
- connection.endBulk();
- });
+ ['fetch', 'subscribe'].forEach(function(method) {
+ it(method + ' gets initial data', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc2[method](function(err) {
+ if (err) return done(err);
+ expect(doc2.version).eql(1);
+ expect(doc2.data).eql({age: 3});
+ done();
+ });
+ });
+ });
- ['fetch', 'subscribe'].forEach(function(method) {
- it(method + ' gets initial data', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2[method](function(err) {
+ it(method + ' twice simultaneously calls back', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc2.version).eql(1);
- expect(doc2.data).eql({age: 3});
- done();
+ async.parallel([
+ function(cb) {
+ doc2[method](cb);
+ },
+ function(cb) {
+ doc2[method](cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(doc2.version).eql(1);
+ expect(doc2.data).eql({age: 3});
+ done();
+ });
});
});
- });
- it(method + ' twice simultaneously calls back', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- async.parallel([
- function(cb) { doc2[method](cb); },
- function(cb) { doc2[method](cb); }
- ], function(err) {
+ it(method + ' twice in bulk simultaneously calls back', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc2.version).eql(1);
- expect(doc2.data).eql({age: 3});
- done();
+ doc2.connection.startBulk();
+ async.parallel([
+ function(cb) {
+ doc2[method](cb);
+ },
+ function(cb) {
+ doc2[method](cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(doc2.version).eql(1);
+ expect(doc2.data).eql({age: 3});
+ done();
+ });
+ doc2.connection.endBulk();
});
});
- });
- it(method + ' twice in bulk simultaneously calls back', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.connection.startBulk();
+ it(method + ' bulk on same collection', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
async.parallel([
- function(cb) { doc2[method](cb); },
- function(cb) { doc2[method](cb); }
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ },
+ function(cb) {
+ connection.get('cats', 'finn').create({age: 2}, cb);
+ }
], function(err) {
if (err) return done(err);
- expect(doc2.version).eql(1);
- expect(doc2.data).eql({age: 3});
- done();
+ var fido = connection2.get('dogs', 'fido');
+ var spot = connection2.get('dogs', 'spot');
+ var finn = connection2.get('cats', 'finn');
+ connection2.startBulk();
+ async.parallel([
+ function(cb) {
+ fido[method](cb);
+ },
+ function(cb) {
+ spot[method](cb);
+ },
+ function(cb) {
+ finn[method](cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(fido.data).eql({age: 3});
+ expect(spot.data).eql({age: 5});
+ expect(finn.data).eql({age: 2});
+ done();
+ });
+ connection2.endBulk();
});
- doc2.connection.endBulk();
});
- });
- it(method + ' bulk on same collection', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
- function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
- ], function(err) {
- if (err) return done(err);
+ it(method + ' bulk on same collection from known version', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
var fido = connection2.get('dogs', 'fido');
var spot = connection2.get('dogs', 'spot');
var finn = connection2.get('cats', 'finn');
connection2.startBulk();
async.parallel([
- function(cb) { fido[method](cb); },
- function(cb) { spot[method](cb); },
- function(cb) { finn[method](cb); }
+ function(cb) {
+ fido[method](cb);
+ },
+ function(cb) {
+ spot[method](cb);
+ },
+ function(cb) {
+ finn[method](cb);
+ }
], function(err) {
if (err) return done(err);
- expect(fido.data).eql({age: 3});
- expect(spot.data).eql({age: 5});
- expect(finn.data).eql({age: 2});
- done();
- });
- connection2.endBulk();
- });
- });
+ expect(fido.version).equal(0);
+ expect(spot.version).equal(0);
+ expect(finn.version).equal(0);
+ expect(fido.data).equal(undefined);
+ expect(spot.data).equal(undefined);
+ expect(finn.data).equal(undefined);
- it(method + ' bulk on same collection from known version', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- var fido = connection2.get('dogs', 'fido');
- var spot = connection2.get('dogs', 'spot');
- var finn = connection2.get('cats', 'finn');
- connection2.startBulk();
- async.parallel([
- function(cb) { fido[method](cb); },
- function(cb) { spot[method](cb); },
- function(cb) { finn[method](cb); }
- ], function(err) {
- if (err) return done(err);
- expect(fido.version).equal(0);
- expect(spot.version).equal(0);
- expect(finn.version).equal(0);
- expect(fido.data).equal(undefined);
- expect(spot.data).equal(undefined);
- expect(finn.data).equal(undefined);
-
- async.parallel([
- function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
- function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
- function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
- ], function(err) {
- if (err) return done(err);
- connection2.startBulk();
async.parallel([
- function(cb) { fido[method](cb); },
- function(cb) { spot[method](cb); },
- function(cb) { finn[method](cb); }
+ function(cb) {
+ connection.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').create({age: 5}, cb);
+ },
+ function(cb) {
+ connection.get('cats', 'finn').create({age: 2}, cb);
+ }
], function(err) {
if (err) return done(err);
- expect(fido.data).eql({age: 3});
- expect(spot.data).eql({age: 5});
- expect(finn.data).eql({age: 2});
-
- // Test sending a fetch without any new ops being created
connection2.startBulk();
async.parallel([
- function(cb) { fido[method](cb); },
- function(cb) { spot[method](cb); },
- function(cb) { finn[method](cb); }
+ function(cb) {
+ fido[method](cb);
+ },
+ function(cb) {
+ spot[method](cb);
+ },
+ function(cb) {
+ finn[method](cb);
+ }
], function(err) {
if (err) return done(err);
+ expect(fido.data).eql({age: 3});
+ expect(spot.data).eql({age: 5});
+ expect(finn.data).eql({age: 2});
- // Create new ops and test if they are received
+ // Test sending a fetch without any new ops being created
+ connection2.startBulk();
async.parallel([
- function(cb) { connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb); },
- function(cb) { connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); },
- function(cb) { connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb); }
+ function(cb) {
+ fido[method](cb);
+ },
+ function(cb) {
+ spot[method](cb);
+ },
+ function(cb) {
+ finn[method](cb);
+ }
], function(err) {
if (err) return done(err);
- connection2.startBulk();
+
+ // Create new ops and test if they are received
async.parallel([
- function(cb) { fido[method](cb); },
- function(cb) { spot[method](cb); },
- function(cb) { finn[method](cb); }
+ function(cb) {
+ connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb);
+ },
+ function(cb) {
+ connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb);
+ },
+ function(cb) {
+ connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb);
+ }
], function(err) {
if (err) return done(err);
- expect(fido.data).eql({age: 4});
- expect(spot.data).eql({age: 6});
- expect(finn.data).eql({age: 3});
- done();
+ connection2.startBulk();
+ async.parallel([
+ function(cb) {
+ fido[method](cb);
+ },
+ function(cb) {
+ spot[method](cb);
+ },
+ function(cb) {
+ finn[method](cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ expect(fido.data).eql({age: 4});
+ expect(spot.data).eql({age: 6});
+ expect(finn.data).eql({age: 3});
+ done();
+ });
+ connection2.endBulk();
});
- connection2.endBulk();
});
+ connection2.endBulk();
});
connection2.endBulk();
});
- connection2.endBulk();
});
+ connection2.endBulk();
});
- connection2.endBulk();
- });
- it(method + ' gets new ops', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.fetch(function(err) {
+ it(method + ' gets new ops', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ doc2.fetch(function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
- done();
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ if (err) return done(err);
+ doc2.on('op', function() {
+ done();
+ });
+ doc2[method]();
});
- doc2[method]();
});
});
});
- });
- it(method + ' calls back after reconnect', function(done) {
- var backend = this.backend;
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2[method](function(err) {
+ it(method + ' calls back after reconnect', function(done) {
+ var backend = this.backend;
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc2.version).eql(1);
- expect(doc2.data).eql({age: 3});
- done();
- });
- doc2.connection.close();
- process.nextTick(function() {
- backend.connect(doc2.connection);
+ doc2[method](function(err) {
+ if (err) return done(err);
+ expect(doc2.version).eql(1);
+ expect(doc2.data).eql({age: 3});
+ done();
+ });
+ doc2.connection.close();
+ process.nextTick(function() {
+ backend.connect(doc2.connection);
+ });
});
});
- });
- it(method + ' returns error passed to doc read middleware', function(done) {
- this.backend.use('doc', function(request, next) {
- next({message: 'Reject doc read'});
- });
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2[method](function(err) {
- expect(err.message).equal('Reject doc read');
- expect(doc2.version).eql(null);
- expect(doc2.data).eql(undefined);
- done();
+ it(method + ' returns error passed to doc read middleware', function(done) {
+ this.backend.use('doc', function(request, next) {
+ next({message: 'Reject doc read'});
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc2[method](function(err) {
+ expect(err.message).equal('Reject doc read');
+ expect(doc2.version).eql(null);
+ expect(doc2.data).eql(undefined);
+ done();
+ });
});
});
- });
- it(method + ' emits error passed to doc read middleware', function(done) {
- this.backend.use('doc', function(request, next) {
- next({message: 'Reject doc read'});
+ it(method + ' emits error passed to doc read middleware', function(done) {
+ this.backend.use('doc', function(request, next) {
+ next({message: 'Reject doc read'});
+ });
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc2[method]();
+ doc2.on('error', function(err) {
+ expect(err.message).equal('Reject doc read');
+ expect(doc2.version).eql(null);
+ expect(doc2.data).eql(undefined);
+ done();
+ });
+ });
});
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2[method]();
- doc2.on('error', function(err) {
- expect(err.message).equal('Reject doc read');
- expect(doc2.version).eql(null);
- expect(doc2.data).eql(undefined);
- done();
+
+ it(method + ' will call back when ops are pending', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc.pause();
+ doc.submitOp({p: ['age'], na: 1});
+ doc[method](done);
});
});
- });
- it(method + ' will call back when ops are pending', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
+ it(method + ' will not call back when creating the doc is pending', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
- doc.submitOp({p: ['age'], na: 1});
+ doc.create({age: 3});
doc[method](done);
+ // HACK: Delay done call to keep from closing the db connection too soon
+ setTimeout(done, 10);
});
- });
-
- it(method + ' will not call back when creating the doc is pending', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.pause();
- doc.create({age: 3});
- doc[method](done);
- // HACK: Delay done call to keep from closing the db connection too soon
- setTimeout(done, 10);
- });
-
- it(method + ' will wait for write when doc is locally created', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.pause();
- var calls = 0;
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- calls++;
- });
- doc[method](function(err) {
- if (err) return done(err);
- expect(calls).equal(1);
- expect(doc.version).equal(1);
- expect(doc.data).eql({age: 3});
- done();
- });
- setTimeout(function() {
- doc.resume();
- }, 10);
- });
- it(method + ' will wait for write when doc is locally created and will fail to submit', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc2.create({age: 5}, function(err) {
- if (err) return done(err);
+ it(method + ' will wait for write when doc is locally created', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
var calls = 0;
doc.create({age: 3}, function(err) {
- expect(err).ok();
+ if (err) return done(err);
calls++;
});
doc[method](function(err) {
if (err) return done(err);
expect(calls).equal(1);
expect(doc.version).equal(1);
- expect(doc.data).eql({age: 5});
+ expect(doc.data).eql({age: 3});
done();
});
setTimeout(function() {
doc.resume();
}, 10);
});
- });
- });
-
- it('unsubscribe calls back immediately on disconnect', function(done) {
- var backend = this.backend;
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.subscribe(function(err) {
- if (err) return done(err);
- doc.unsubscribe(done);
- doc.connection.close();
- });
- });
-
- it('unsubscribe calls back immediately when already disconnected', function(done) {
- var backend = this.backend;
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.subscribe(function(err) {
- if (err) return done(err);
- doc.connection.close();
- doc.unsubscribe(done);
- });
- });
- it('subscribed client gets create from other client', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc2.subscribe(function(err) {
- if (err) return done(err);
- doc2.on('create', function(context) {
- expect(context).equal(false);
- expect(doc2.version).eql(1);
- expect(doc2.data).eql({age: 3});
- done();
+ it(method + ' will wait for write when doc is locally created and will fail to submit', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc2.create({age: 5}, function(err) {
+ if (err) return done(err);
+ doc.pause();
+ var calls = 0;
+ doc.create({age: 3}, function(err) {
+ expect(err).ok();
+ calls++;
+ });
+ doc[method](function(err) {
+ if (err) return done(err);
+ expect(calls).equal(1);
+ expect(doc.version).equal(1);
+ expect(doc.data).eql({age: 5});
+ done();
+ });
+ setTimeout(function() {
+ doc.resume();
+ }, 10);
+ });
});
- doc.create({age: 3});
});
- });
- it('subscribed client gets op from other client', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe(function(err) {
+ it('unsubscribe calls back immediately on disconnect', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.subscribe(function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
- expect(doc2.version).eql(2);
- expect(doc2.data).eql({age: 4});
- done();
- });
- doc.submitOp({p: ['age'], na: 1});
+ doc.unsubscribe(done);
+ doc.connection.close();
});
});
- });
- it('disconnecting stops op updates', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe(function(err) {
+ it('unsubscribe calls back immediately when already disconnected', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.subscribe(function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
- done();
- });
- doc2.connection.close();
- doc.submitOp({p: ['age'], na: 1}, done);
+ doc.connection.close();
+ doc.unsubscribe(done);
});
});
- });
- it('backend.suppressPublish stops op updates', function(done) {
- var backend = this.backend;
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
+ it('subscribed client gets create from other client', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
doc2.subscribe(function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
+ doc2.on('create', function(context) {
+ expect(context).equal(false);
+ expect(doc2.version).eql(1);
+ expect(doc2.data).eql({age: 3});
done();
});
- backend.suppressPublish = true;
- doc.submitOp({p: ['age'], na: 1}, done);
+ doc.create({age: 3});
});
});
- });
- it('unsubscribe stops op updates', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe(function(err) {
+ it('subscribed client gets op from other client', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
- done();
- });
- doc2.unsubscribe(function(err) {
+ doc2.subscribe(function(err) {
if (err) return done(err);
- doc.submitOp({p: ['age'], na: 1}, done);
+ doc2.on('op', function() {
+ expect(doc2.version).eql(2);
+ expect(doc2.data).eql({age: 4});
+ done();
+ });
+ doc.submitOp({p: ['age'], na: 1});
});
});
});
- });
- it('doc destroy stops op updates', function(done) {
- var connection1 = this.backend.connect();
- var connection2 = this.backend.connect();
- var doc = connection1.get('dogs', 'fido');
- var doc2 = connection2.get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe(function(err) {
+ it('disconnecting stops op updates', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
- done(new Error('Should not get op event'));
- });
- doc2.destroy(function(err) {
+ doc2.subscribe(function(err) {
if (err) return done(err);
- expect(connection2.getExisting('dogs', 'fido')).equal(undefined);
+ doc2.on('op', function() {
+ done();
+ });
+ doc2.connection.close();
doc.submitOp({p: ['age'], na: 1}, done);
});
});
});
- });
-
- it('doc destroy removes doc from connection when doc is not subscribed', function(done) {
- var connection = this.backend.connect();
- var doc = connection.get('dogs', 'fido');
- expect(connection.getExisting('dogs', 'fido')).equal(doc);
- doc.destroy(function(err) {
- if (err) return done(err);
- expect(connection.getExisting('dogs', 'fido')).equal(undefined);
- done();
- });
- });
- it('bulk unsubscribe stops op updates', function(done) {
- var connection = this.backend.connect();
- var connection2 = this.backend.connect();
- var doc = connection.get('dogs', 'fido');
- var fido = connection2.get('dogs', 'fido');
- var spot = connection2.get('dogs', 'spot');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- async.parallel([
- function(cb) { fido.subscribe(cb); },
- function(cb) { spot.subscribe(cb); }
- ], function(err) {
+ it('backend.suppressPublish stops op updates', function(done) {
+ var backend = this.backend;
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- fido.connection.startBulk();
- async.parallel([
- function(cb) { fido.unsubscribe(cb); },
- function(cb) { spot.unsubscribe(cb); }
- ], function(err) {
+ doc2.subscribe(function(err) {
if (err) return done(err);
- fido.on('op', function(op, context) {
+ doc2.on('op', function() {
done();
});
+ backend.suppressPublish = true;
doc.submitOp({p: ['age'], na: 1}, done);
});
- fido.connection.endBulk();
});
});
- });
- it('a subscribed doc is re-subscribed after reconnect and gets any missing ops', function(done) {
- var backend = this.backend;
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe(function(err) {
+ it('unsubscribe stops op updates', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
- expect(doc2.version).eql(2);
- expect(doc2.data).eql({age: 4});
- done();
- });
-
- doc2.connection.close();
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ doc2.subscribe(function(err) {
if (err) return done(err);
- backend.connect(doc2.connection);
+ doc2.on('op', function() {
+ done();
+ });
+ doc2.unsubscribe(function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 1}, done);
+ });
});
});
});
- });
- it('calling subscribe, unsubscribe, subscribe sync leaves a doc subscribed', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe();
- doc2.unsubscribe();
- doc2.subscribe(function(err) {
+ it('doc destroy stops op updates', function(done) {
+ var connection1 = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var doc = connection1.get('dogs', 'fido');
+ var doc2 = connection2.get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- doc2.on('op', function(op, context) {
- done();
+ doc2.subscribe(function(err) {
+ if (err) return done(err);
+ doc2.on('op', function() {
+ done(new Error('Should not get op event'));
+ });
+ doc2.destroy(function(err) {
+ if (err) return done(err);
+ expect(connection2.getExisting('dogs', 'fido')).equal(undefined);
+ doc.submitOp({p: ['age'], na: 1}, done);
+ });
});
- doc.submitOp({p: ['age'], na: 1});
});
});
- });
- it('doc fetches ops to catch up if it receives a future op', function(done) {
- var backend = this.backend;
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe(function(err) {
+ it('doc destroy removes doc from connection when doc is not subscribed', function(done) {
+ var connection = this.backend.connect();
+ var doc = connection.get('dogs', 'fido');
+ expect(connection.getExisting('dogs', 'fido')).equal(doc);
+ doc.destroy(function(err) {
if (err) return done(err);
- var expected = [
- [{p: ['age'], na: 1}],
- [{p: ['age'], na: 5}],
- ];
- doc2.on('op', function(op, context) {
- var item = expected.shift();
- expect(op).eql(item);
- if (expected.length) return;
- expect(doc2.version).equal(3);
- expect(doc2.data).eql({age: 9});
- done();
- });
- backend.suppressPublish = true;
- doc.submitOp({p: ['age'], na: 1}, function(err) {
- if (err) return done(err);
- backend.suppressPublish = false;
- doc.submitOp({p: ['age'], na: 5});
- });
+ expect(connection.getExisting('dogs', 'fido')).equal(undefined);
+ done();
});
});
- });
- it('doc fetches ops to catch up if it receives multiple future ops', function(done) {
- var backend = this.backend;
- var doc = this.backend.connect().get('dogs', 'fido');
- var doc2 = this.backend.connect().get('dogs', 'fido');
- // Delaying op replies will cause multiple future ops to be received
- // before the fetch to catch up completes
- backend.use('op', function(request, next) {
- setTimeout(next, 10 * Math.random());
- });
- doc.create({age: 3}, function(err) {
- if (err) return done(err);
- doc2.subscribe(function(err) {
+ it('bulk unsubscribe stops op updates', function(done) {
+ var connection = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var doc = connection.get('dogs', 'fido');
+ var fido = connection2.get('dogs', 'fido');
+ var spot = connection2.get('dogs', 'spot');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- var wait = 4;
- doc2.on('op', function(op, context) {
- if (--wait) return;
- expect(doc2.version).eql(5);
- expect(doc2.data).eql({age: 122});
- done();
- });
- backend.suppressPublish = true;
- doc.submitOp({p: ['age'], na: 1}, function(err) {
+ async.parallel([
+ function(cb) {
+ fido.subscribe(cb);
+ },
+ function(cb) {
+ spot.subscribe(cb);
+ }
+ ], function(err) {
if (err) return done(err);
- backend.suppressPublish = false;
- doc.submitOp({p: ['age'], na: 5}, function(err) {
+ fido.connection.startBulk();
+ async.parallel([
+ function(cb) {
+ fido.unsubscribe(cb);
+ },
+ function(cb) {
+ spot.unsubscribe(cb);
+ }
+ ], function(err) {
if (err) return done(err);
- doc.submitOp({p: ['age'], na: 13}, function(err) {
- if (err) return done(err);
- doc.submitOp({p: ['age'], na: 100});
+ fido.on('op', function() {
+ done();
});
+ doc.submitOp({p: ['age'], na: 1}, done);
});
+ fido.connection.endBulk();
});
});
});
- });
- describe('doc.subscribed', function() {
- it('is set to false initially', function() {
+ it('a subscribed doc is re-subscribed after reconnect and gets any missing ops', function(done) {
+ var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
- expect(doc.subscribed).equal(false);
- });
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
+ if (err) return done(err);
+ doc2.subscribe(function(err) {
+ if (err) return done(err);
+ doc2.on('op', function() {
+ expect(doc2.version).eql(2);
+ expect(doc2.data).eql({age: 4});
+ done();
+ });
- it('remains false before subscribe call completes', function() {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.subscribe();
- expect(doc.subscribed).equal(false);
+ doc2.connection.close();
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ if (err) return done(err);
+ backend.connect(doc2.connection);
+ });
+ });
+ });
});
- it('is set to true after subscribe completes', function(done) {
+ it('calling subscribe, unsubscribe, subscribe sync leaves a doc subscribed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
- doc.subscribe(function(err) {
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.subscribed).equal(true);
- done();
+ doc2.subscribe();
+ doc2.unsubscribe();
+ doc2.subscribe(function(err) {
+ if (err) return done(err);
+ doc2.on('op', function() {
+ done();
+ });
+ doc.submitOp({p: ['age'], na: 1});
+ });
});
});
- it('is not set to true after subscribe completes if already unsubscribed', function(done) {
+ it('doc fetches ops to catch up if it receives a future op', function(done) {
+ var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
- doc.subscribe(function(err) {
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.subscribed).equal(false);
- done();
+ doc2.subscribe(function(err) {
+ if (err) return done(err);
+ var expected = [
+ [{p: ['age'], na: 1}],
+ [{p: ['age'], na: 5}]
+ ];
+ doc2.on('op', function(op) {
+ var item = expected.shift();
+ expect(op).eql(item);
+ if (expected.length) return;
+ expect(doc2.version).equal(3);
+ expect(doc2.data).eql({age: 9});
+ done();
+ });
+ backend.suppressPublish = true;
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ if (err) return done(err);
+ backend.suppressPublish = false;
+ doc.submitOp({p: ['age'], na: 5});
+ });
+ });
});
- doc.unsubscribe();
});
- it('is set to false sychronously in unsubscribe', function(done) {
+ it('doc fetches ops to catch up if it receives multiple future ops', function(done) {
+ var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
- doc.subscribe(function(err) {
+ var doc2 = this.backend.connect().get('dogs', 'fido');
+ // Delaying op replies will cause multiple future ops to be received
+ // before the fetch to catch up completes
+ backend.use('op', function(request, next) {
+ setTimeout(next, 10 * Math.random());
+ });
+ doc.create({age: 3}, function(err) {
if (err) return done(err);
- expect(doc.subscribed).equal(true);
- doc.unsubscribe();
- expect(doc.subscribed).equal(false);
- done();
+ doc2.subscribe(function(err) {
+ if (err) return done(err);
+ var wait = 4;
+ doc2.on('op', function() {
+ if (--wait) return;
+ expect(doc2.version).eql(5);
+ expect(doc2.data).eql({age: 122});
+ done();
+ });
+ backend.suppressPublish = true;
+ doc.submitOp({p: ['age'], na: 1}, function(err) {
+ if (err) return done(err);
+ backend.suppressPublish = false;
+ doc.submitOp({p: ['age'], na: 5}, function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 13}, function(err) {
+ if (err) return done(err);
+ doc.submitOp({p: ['age'], na: 100});
+ });
+ });
+ });
+ });
});
});
- it('is set to false sychronously on disconnect', function(done) {
- var doc = this.backend.connect().get('dogs', 'fido');
- doc.subscribe(function(err) {
- if (err) return done(err);
- expect(doc.subscribed).equal(true);
- doc.connection.close();
+ describe('doc.subscribed', function() {
+ it('is set to false initially', function() {
+ var doc = this.backend.connect().get('dogs', 'fido');
expect(doc.subscribed).equal(false);
- done();
+ });
+
+ it('remains false before subscribe call completes', function() {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.subscribe();
+ expect(doc.subscribed).equal(false);
+ });
+
+ it('is set to true after subscribe completes', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.subscribe(function(err) {
+ if (err) return done(err);
+ expect(doc.subscribed).equal(true);
+ done();
+ });
+ });
+
+ it('is not set to true after subscribe completes if already unsubscribed', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.subscribe(function(err) {
+ if (err) return done(err);
+ expect(doc.subscribed).equal(false);
+ done();
+ });
+ doc.unsubscribe();
+ });
+
+ it('is set to false sychronously in unsubscribe', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.subscribe(function(err) {
+ if (err) return done(err);
+ expect(doc.subscribed).equal(true);
+ doc.unsubscribe();
+ expect(doc.subscribed).equal(false);
+ done();
+ });
+ });
+
+ it('is set to false sychronously on disconnect', function(done) {
+ var doc = this.backend.connect().get('dogs', 'fido');
+ doc.subscribe(function(err) {
+ if (err) return done(err);
+ expect(doc.subscribed).equal(true);
+ doc.connection.close();
+ expect(doc.subscribed).equal(false);
+ done();
+ });
});
});
});
-});
};
diff --git a/test/db-memory.js b/test/db-memory.js
index 9185dd507..0f0d01a92 100644
--- a/test/db-memory.js
+++ b/test/db-memory.js
@@ -2,11 +2,6 @@ var expect = require('expect.js');
var DB = require('../lib/db');
var MemoryDB = require('../lib/db/memory');
-// Extend from MemoryDB as defined in this package, not the one that
-// sharedb-mingo-memory depends on.
-var ShareDbMingo = require('sharedb-mingo-memory').extendMemoryDB(MemoryDB);
-var getQuery = require('sharedb-mingo-memory/get-query');
-
describe('DB base class', function() {
it('can call db.close() without callback', function() {
var db = new DB();
@@ -59,10 +54,80 @@ describe('DB base class', function() {
});
});
+
+// Extension of MemoryDB that supports query filters and sorts on simple
+// top-level properties, which is enough for the core ShareDB tests on
+// query subscription updating.
+function BasicQueryableMemoryDB() {
+ MemoryDB.apply(this, arguments);
+}
+BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype);
+BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB;
+
+BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query) {
+ if (query.filter) {
+ snapshots = snapshots.filter(function(snapshot) {
+ for (var queryKey in query.filter) {
+ // This fake only supports simple property equality filters, so
+ // throw an error on Mongo-like filter properties with dots.
+ if (queryKey.includes('.')) {
+ throw new Error('Only simple property filters are supported, got:', queryKey);
+ }
+ if (snapshot.data[queryKey] !== query.filter[queryKey]) {
+ return false;
+ }
+ }
+ return true;
+ });
+ }
+
+ if (query.sort) {
+ if (!Array.isArray(query.sort)) {
+ throw new Error('query.sort must be an array');
+ }
+ if (query.sort.length) {
+ snapshots.sort(snapshotComparator(query.sort));
+ }
+ }
+
+ return {snapshots: snapshots};
+};
+
+// sortProperties is an array whose items are each [propertyName, direction].
+function snapshotComparator(sortProperties) {
+ return function(snapshotA, snapshotB) {
+ for (var i = 0; i < sortProperties.length; i++) {
+ var sortProperty = sortProperties[i];
+ var sortKey = sortProperty[0];
+ var sortDirection = sortProperty[1];
+
+ var aPropVal = snapshotA.data[sortKey];
+ var bPropVal = snapshotB.data[sortKey];
+ if (aPropVal < bPropVal) {
+ return -1 * sortDirection;
+ } else if (aPropVal > bPropVal) {
+ return sortDirection;
+ } else if (aPropVal === bPropVal) {
+ continue;
+ } else {
+ throw new Error('Could not compare ' + aPropVal + ' and ' + bPropVal);
+ }
+ }
+ return 0;
+ };
+}
+
+// Run all the DB-based tests against the BasicQueryableMemoryDB.
require('./db')({
- create: function(callback) {
- var db = new ShareDbMingo();
+ create: function(options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
+ var db = new BasicQueryableMemoryDB(options);
callback(null, db);
},
- getQuery: getQuery
+ getQuery: function(options) {
+ return {filter: options.query, sort: options.sort};
+ }
});
diff --git a/test/db.js b/test/db.js
index db3aa1a88..701bdb7a0 100644
--- a/test/db.js
+++ b/test/db.js
@@ -229,7 +229,7 @@ module.exports = function(options) {
it('getSnapshot returns v0 snapshot', function(done) {
this.db.getSnapshot('testcollection', 'test', null, null, function(err, result) {
if (err) return done(err);
- expect(result).eql({id: 'test', type: null, v: 0, data: undefined});
+ expect(result).eql({id: 'test', type: null, v: 0, data: undefined, m: null});
done();
});
});
@@ -238,11 +238,11 @@ module.exports = function(options) {
var data = {x: 5, y: 6};
var op = {v: 0, create: {type: 'json0', data: data}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getSnapshot('testcollection', 'test', null, null, function(err, result) {
if (err) return done(err);
- expect(result).eql({id: 'test', type: 'http://sharejs.org/types/JSONv0', v: 1, data: data});
+ expect(result).eql({id: 'test', type: 'http://sharejs.org/types/JSONv0', v: 1, data: data, m: null});
done();
});
});
@@ -251,9 +251,10 @@ module.exports = function(options) {
it('getSnapshot does not return committed metadata by default', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.getSnapshot('testcollection', 'test', null, null, function(err, result) {
if (err) return done(err);
- expect(result.m).equal(undefined);
+ expect(result.m).equal(null);
done();
});
});
@@ -262,6 +263,7 @@ module.exports = function(options) {
it('getSnapshot returns metadata when option is true', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.getSnapshot('testcollection', 'test', null, {metadata: true}, function(err, result) {
if (err) return done(err);
expect(result.m).eql({test: 3});
@@ -273,6 +275,7 @@ module.exports = function(options) {
it('getSnapshot returns metadata when fields is {$submit: true}', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, result) {
if (err) return done(err);
expect(result.m).eql({test: 3});
@@ -287,13 +290,13 @@ module.exports = function(options) {
var data = {x: 5, y: 6};
var op = {v: 0, create: {type: 'json0', data: data}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getSnapshotBulk('testcollection', ['test2', 'test'], null, null, function(err, resultMap) {
if (err) return done(err);
expect(resultMap).eql({
- test: {id: 'test', type: 'http://sharejs.org/types/JSONv0', v: 1, data: data},
- test2: {id: 'test2', type: null, v: 0, data: undefined}
+ test: {id: 'test', type: 'http://sharejs.org/types/JSONv0', v: 1, data: data, m: null},
+ test2: {id: 'test2', type: null, v: 0, data: undefined, m: null}
});
done();
});
@@ -303,9 +306,10 @@ module.exports = function(options) {
it('getSnapshotBulk does not return committed metadata by default', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.getSnapshotBulk('testcollection', ['test2', 'test'], null, null, function(err, resultMap) {
if (err) return done(err);
- expect(resultMap.test.m).equal(undefined);
+ expect(resultMap.test.m).equal(null);
done();
});
});
@@ -314,6 +318,7 @@ module.exports = function(options) {
it('getSnapshotBulk returns metadata when option is true', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.getSnapshotBulk('testcollection', ['test2', 'test'], null, {metadata: true}, function(err, resultMap) {
if (err) return done(err);
expect(resultMap.test.m).eql({test: 3});
@@ -335,7 +340,7 @@ module.exports = function(options) {
it('getOps returns 1 committed op', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getOps('testcollection', 'test', 0, null, null, function(err, ops) {
if (err) return done(err);
@@ -349,9 +354,9 @@ module.exports = function(options) {
var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var op1 = {v: 1, op: [{p: ['x'], na: 1}]};
var db = this.db;
- submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
db.getOps('testcollection', 'test', 0, null, null, function(err, ops) {
if (err) return done(err);
@@ -366,9 +371,9 @@ module.exports = function(options) {
var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var op1 = {v: 1, op: [{p: ['x'], na: 1}]};
var db = this.db;
- submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
db.getOps('testcollection', 'test', null, null, null, function(err, ops) {
if (err) return done(err);
@@ -383,9 +388,9 @@ module.exports = function(options) {
var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var op1 = {v: 1, op: [{p: ['x'], na: 1}]};
var db = this.db;
- submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
db.getOps('testcollection', 'test', 1, null, null, function(err, ops) {
if (err) return done(err);
@@ -400,9 +405,9 @@ module.exports = function(options) {
var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var op1 = {v: 1, op: [{p: ['x'], na: 1}]};
var db = this.db;
- submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
db.getOps('testcollection', 'test', 0, 1, null, function(err, ops) {
if (err) return done(err);
@@ -416,7 +421,7 @@ module.exports = function(options) {
it('getOps does not return committed metadata by default', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getOps('testcollection', 'test', null, null, null, function(err, ops) {
if (err) return done(err);
@@ -429,7 +434,7 @@ module.exports = function(options) {
it('getOps returns metadata when option is true', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getOps('testcollection', 'test', null, null, {metadata: true}, function(err, ops) {
if (err) return done(err);
@@ -455,9 +460,9 @@ module.exports = function(options) {
it('getOpsBulk returns committed ops', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test2', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test2', op, function(err) {
if (err) return done(err);
db.getOpsBulk('testcollection', {test: 0, test2: 0}, null, null, function(err, opsMap) {
if (err) return done(err);
@@ -474,9 +479,9 @@ module.exports = function(options) {
it('getOpsBulk returns all ops committed from null', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test2', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test2', op, function(err) {
if (err) return done(err);
db.getOpsBulk('testcollection', {test: null, test2: null}, null, null, function(err, opsMap) {
if (err) return done(err);
@@ -494,13 +499,13 @@ module.exports = function(options) {
var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var op1 = {v: 1, op: [{p: ['x'], na: 1}]};
var db = this.db;
- submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test2', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test2', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test2', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test2', op1, function(err) {
if (err) return done(err);
db.getOpsBulk('testcollection', {test: 0, test2: 1}, null, null, function(err, opsMap) {
if (err) return done(err);
@@ -520,13 +525,13 @@ module.exports = function(options) {
var op0 = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var op1 = {v: 1, op: [{p: ['x'], na: 1}]};
var db = this.db;
- submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test2', op0, function(err, succeeded) {
+ submit(db, 'testcollection', 'test2', op0, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
- submit(db, 'testcollection', 'test2', op1, function(err, succeeded) {
+ submit(db, 'testcollection', 'test2', op1, function(err) {
if (err) return done(err);
db.getOpsBulk('testcollection', {test: 1, test2: 0}, {test2: 1}, null, function(err, opsMap) {
if (err) return done(err);
@@ -545,7 +550,7 @@ module.exports = function(options) {
it('getOpsBulk does not return committed metadata by default', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getOpsBulk('testcollection', {test: null}, null, null, function(err, opsMap) {
if (err) return done(err);
@@ -558,7 +563,7 @@ module.exports = function(options) {
it('getOpsBulk returns metadata when option is true', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getOpsBulk('testcollection', {test: null}, null, {metadata: true}, function(err, opsMap) {
if (err) return done(err);
@@ -573,7 +578,7 @@ module.exports = function(options) {
it('getOpsToSnapshot returns committed op', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) {
if (err) return done(err);
@@ -589,7 +594,7 @@ module.exports = function(options) {
it('getOpsToSnapshot does not return committed metadata by default', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) {
if (err) return done(err);
@@ -605,7 +610,7 @@ module.exports = function(options) {
it('getOpsToSnapshot returns metadata when option is true', function(done) {
var op = {v: 0, create: {type: 'json0', data: {x: 5, y: 6}}, m: {test: 3}};
var db = this.db;
- submit(db, 'testcollection', 'test', op, function(err, succeeded) {
+ submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) {
if (err) return done(err);
@@ -621,9 +626,9 @@ module.exports = function(options) {
describe('query', function() {
it('query returns data in the collection', function(done) {
- var snapshot = {v: 1, type: 'json0', data: {x: 5, y: 6}};
+ var snapshot = {v: 1, type: 'json0', data: {x: 5, y: 6}, m: null};
var db = this.db;
- db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err, succeeded) {
+ db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
if (err) return done(err);
db.query('testcollection', {x: 5}, null, null, function(err, results) {
if (err) return done(err);
@@ -645,9 +650,10 @@ module.exports = function(options) {
it('query does not return committed metadata by default', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.query('testcollection', {x: 5}, null, null, function(err, results) {
if (err) return done(err);
- expect(results[0].m).equal(undefined);
+ expect(results[0].m).equal(null);
done();
});
});
@@ -656,6 +662,7 @@ module.exports = function(options) {
it('query returns metadata when option is true', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.query('testcollection', {x: 5}, null, {metadata: true}, function(err, results) {
if (err) return done(err);
expect(results[0].m).eql({test: 3});
@@ -672,6 +679,7 @@ module.exports = function(options) {
var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}};
var db = this.db;
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
+ if (err) return done(err);
db.query('testcollection', {x: 5}, {y: true}, null, function(err, results) {
if (err) return done(err);
expect(results).eql([{type: 'json0', v: 1, data: {y: 6}, id: 'test'}]);
@@ -686,6 +694,7 @@ module.exports = function(options) {
var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}};
var db = this.db;
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
+ if (err) return done(err);
db.query('testcollection', {x: 5}, {}, null, function(err, results) {
if (err) return done(err);
expect(results).eql([{type: 'json0', v: 1, data: {}, id: 'test'}]);
@@ -697,9 +706,10 @@ module.exports = function(options) {
it('query does not return committed metadata by default with projection', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.query('testcollection', {x: 5}, {x: true}, null, function(err, results) {
if (err) return done(err);
- expect(results[0].m).equal(undefined);
+ expect(results[0].m).equal(null);
done();
});
});
@@ -708,6 +718,7 @@ module.exports = function(options) {
it('query returns metadata when option is true with projection', function(done) {
var db = this.db;
commitSnapshotWithMetadata(db, function(err) {
+ if (err) return done(err);
db.query('testcollection', {x: 5}, {x: true}, {metadata: true}, function(err, results) {
if (err) return done(err);
expect(results[0].m).eql({test: 3});
@@ -721,7 +732,7 @@ module.exports = function(options) {
it('returns data in the collection', function(done) {
var snapshot = {v: 1, type: 'json0', data: {x: 5, y: 6}};
var db = this.db;
- db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err, succeeded) {
+ db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
if (err) return done(err);
db.queryPoll('testcollection', {x: 5}, null, function(err, ids) {
if (err) return done(err);
@@ -760,6 +771,7 @@ module.exports = function(options) {
var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}};
var db = this.db;
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
+ if (err) return done(err);
db.queryPollDoc('testcollection', 'test', query, null, function(err, result) {
if (err) return done(err);
expect(result).equal(true);
@@ -775,6 +787,7 @@ module.exports = function(options) {
var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}};
var db = this.db;
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
+ if (err) return done(err);
db.queryPollDoc('testcollection', 'test', query, null, function(err, result) {
if (err) return done(err);
expect(result).equal(false);
@@ -789,10 +802,10 @@ module.exports = function(options) {
// test that getQuery({query: {}, sort: [['foo', 1], ['bar', -1]]})
// sorts by foo first, then bar
var snapshots = [
- {type: 'json0', id: '0', v: 1, data: {foo: 1, bar: 1}},
- {type: 'json0', id: '1', v: 1, data: {foo: 2, bar: 1}},
- {type: 'json0', id: '2', v: 1, data: {foo: 1, bar: 2}},
- {type: 'json0', id: '3', v: 1, data: {foo: 2, bar: 2}}
+ {type: 'json0', id: '0', v: 1, data: {foo: 1, bar: 1}, m: null},
+ {type: 'json0', id: '1', v: 1, data: {foo: 2, bar: 1}, m: null},
+ {type: 'json0', id: '2', v: 1, data: {foo: 1, bar: 2}, m: null},
+ {type: 'json0', id: '3', v: 1, data: {foo: 2, bar: 2}, m: null}
];
var db = this.db;
var dbQuery = getQuery({query: {}, sort: [['foo', 1], ['bar', -1]]});
diff --git a/test/logger.js b/test/logger.js
new file mode 100644
index 000000000..4efbb0cfa
--- /dev/null
+++ b/test/logger.js
@@ -0,0 +1,46 @@
+var Logger = require('../lib/logger/logger');
+var expect = require('expect.js');
+var sinon = require('sinon');
+
+describe('Logger', function() {
+ describe('Stubbing console.warn', function() {
+ beforeEach(function() {
+ sinon.stub(console, 'warn');
+ });
+
+ afterEach(function() {
+ sinon.restore();
+ });
+
+ it('logs to console by default', function() {
+ var logger = new Logger();
+ logger.warn('warning');
+ expect(console.warn.calledOnceWithExactly('warning')).to.be(true);
+ });
+
+ it('overrides console', function() {
+ var customWarn = sinon.stub();
+ var logger = new Logger();
+ logger.setMethods({
+ warn: customWarn
+ });
+
+ logger.warn('warning');
+
+ expect(console.warn.notCalled).to.be(true);
+ expect(customWarn.calledOnceWithExactly('warning')).to.be(true);
+ });
+
+ it('only overrides if provided with a method', function() {
+ var badWarn = 'not a function';
+ var logger = new Logger();
+ logger.setMethods({
+ warn: badWarn
+ });
+
+ logger.warn('warning');
+
+ expect(console.warn.calledOnceWithExactly('warning')).to.be(true);
+ });
+ });
+});
diff --git a/test/middleware.js b/test/middleware.js
index 7f067720d..85ceb686e 100644
--- a/test/middleware.js
+++ b/test/middleware.js
@@ -1,11 +1,9 @@
-var async = require('async');
var Backend = require('../lib/backend');
var expect = require('expect.js');
var util = require('./util');
var types = require('../lib/types');
describe('middleware', function() {
-
beforeEach(function() {
this.backend = new Backend();
});
@@ -22,16 +20,13 @@ describe('middleware', function() {
}
describe('use', function() {
-
it('returns itself to allow chaining', function() {
- var response = this.backend.use('submit', function(request, next) {});
+ var response = this.backend.use('submit', function() {});
expect(response).equal(this.backend);
});
-
});
describe('connect', function() {
-
it('passes the agent on connect', function(done) {
var clientId;
this.backend.use('connect', function(request, next) {
@@ -57,7 +52,6 @@ describe('middleware', function() {
done();
});
});
-
});
function testReadDoc(expectFidoOnly, expectFidoAndSpot) {
@@ -136,7 +130,6 @@ describe('middleware', function() {
function expectFidoAndSpot(backend, done) {
var doneAfter = util.callAfter(2, done);
- var i = 0;
backend.use('doc', function(request, next) {
doneAfter();
if (doneAfter.called === 1) {
@@ -221,6 +214,74 @@ describe('middleware', function() {
testReadDoc(expectFidoOnly, expectFidoAndSpot);
});
+ describe('reply', function() {
+ beforeEach(function(done) {
+ this.snapshot = {v: 1, type: 'json0', data: {age: 3}};
+ this.backend.db.commit('dogs', 'fido', {v: 0, create: {}}, this.snapshot, null, done);
+ });
+
+ it('context has request and reply objects', function(done) {
+ var snapshot = this.snapshot;
+ this.backend.use('reply', function(replyContext, next) {
+ expect(replyContext).to.have.property('action', 'reply');
+ expect(replyContext.request).to.eql({a: 'qf', id: 1, c: 'dogs', q: {age: 3}});
+ expect(replyContext.reply).to.eql({
+ data: [{v: 1, data: snapshot.data, d: 'fido'}],
+ extra: undefined,
+ a: 'qf',
+ id: 1
+ });
+ expect(replyContext).to.have.property('agent');
+ expect(replyContext).to.have.property('backend');
+ next();
+ });
+
+ var connection = this.backend.connect();
+ connection.createFetchQuery('dogs', {age: 3}, null, function(err, results) {
+ if (err) {
+ return done(err);
+ }
+ expect(results).to.have.length(1);
+ expect(results[0].data).to.eql(snapshot.data);
+ done();
+ });
+ });
+
+ it('can produce errors that get sent back to client', function(done) {
+ var errorMessage = 'This is an error from reply middleware';
+ this.backend.use('reply', function(_replyContext, next) {
+ next(errorMessage);
+ });
+ var connection = this.backend.connect();
+ var doc = connection.get('dogs', 'fido');
+ doc.fetch(function(err) {
+ expect(err).to.have.property('message', errorMessage);
+ done();
+ });
+ });
+
+ it('can make raw additions to query reply extra', function(done) {
+ var snapshot = this.snapshot;
+ this.backend.use('reply', function(replyContext, next) {
+ expect(replyContext.request.a === 'qf');
+ replyContext.reply.extra = replyContext.reply.extra || {};
+ replyContext.reply.extra.replyMiddlewareValue = 'some value';
+ next();
+ });
+
+ var connection = this.backend.connect();
+ connection.createFetchQuery('dogs', {age: 3}, null, function(err, results, extra) {
+ if (err) {
+ return done(err);
+ }
+ expect(results).to.have.length(1);
+ expect(results[0].data).to.eql(snapshot.data);
+ expect(extra).to.eql({replyMiddlewareValue: 'some value'});
+ done();
+ });
+ });
+ });
+
describe('submit lifecycle', function() {
// DEPRECATED: 'after submit' is a synonym for 'afterSubmit'
['submit', 'apply', 'commit', 'afterSubmit', 'after submit'].forEach(function(action) {
@@ -237,5 +298,4 @@ describe('middleware', function() {
});
});
});
-
});
diff --git a/test/milestone-db-memory.js b/test/milestone-db-memory.js
new file mode 100644
index 000000000..f1648aaa8
--- /dev/null
+++ b/test/milestone-db-memory.js
@@ -0,0 +1,13 @@
+var MemoryMilestoneDB = require('./../lib/milestone-db/memory');
+
+require('./milestone-db')({
+ create: function(options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = null;
+ }
+
+ var db = new MemoryMilestoneDB(options);
+ callback(null, db);
+ }
+});
diff --git a/test/milestone-db.js b/test/milestone-db.js
new file mode 100644
index 000000000..223423d47
--- /dev/null
+++ b/test/milestone-db.js
@@ -0,0 +1,749 @@
+var expect = require('expect.js');
+var Backend = require('../lib/backend');
+var MilestoneDB = require('../lib/milestone-db');
+var NoOpMilestoneDB = require('../lib/milestone-db/no-op');
+var Snapshot = require('../lib/snapshot');
+var util = require('./util');
+
+describe('Base class', function() {
+ var db;
+
+ beforeEach(function() {
+ db = new MilestoneDB();
+ });
+
+ 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.be(5019);
+ done();
+ });
+ });
+
+ it('emits an error when trying to get a snapshot', function(done) {
+ db.on('error', function(error) {
+ expect(error.code).to.be(5019);
+ done();
+ });
+
+ db.getMilestoneSnapshot('books', '123', 1);
+ });
+
+ it('calls back with an error when trying to save a snapshot', function(done) {
+ db.saveMilestoneSnapshot('books', {}, function(error) {
+ expect(error.code).to.be(5020);
+ done();
+ });
+ });
+
+ it('emits an error when trying to save a snapshot', function(done) {
+ db.on('error', function(error) {
+ expect(error.code).to.be(5020);
+ done();
+ });
+
+ db.saveMilestoneSnapshot('books', {});
+ });
+
+ 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.be(5021);
+ 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.be(5022);
+ done();
+ });
+ });
+});
+
+describe('NoOpMilestoneDB', function() {
+ var db;
+
+ beforeEach(function() {
+ db = new NoOpMilestoneDB();
+ });
+
+ it('does not error when trying to save and fetch a snapshot', function(done) {
+ var snapshot = new Snapshot(
+ 'catcher-in-the-rye',
+ 2,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye'},
+ null
+ );
+
+ util.callInSeries([
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot, next);
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('emits an event when saving without a callback', function(done) {
+ db.on('save', function() {
+ done();
+ });
+
+ db.saveMilestoneSnapshot('books', undefined);
+ });
+});
+
+module.exports = function(options) {
+ var create = options.create;
+
+ describe('Milestone Database', function() {
+ var db;
+ var backend;
+
+ beforeEach(function(done) {
+ create(function(error, createdDb) {
+ if (error) return done(error);
+ db = createdDb;
+ backend = new Backend({milestoneDb: db});
+ done();
+ });
+ });
+
+ afterEach(function(done) {
+ db.close(done);
+ });
+
+ it('can call close() without a callback', function(done) {
+ create(function(error, db) {
+ if (error) return done(error);
+ db.close();
+ done();
+ });
+ });
+
+ it('stores and fetches a milestone snapshot', function(done) {
+ var snapshot = new Snapshot(
+ 'catcher-in-the-rye',
+ 2,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye'},
+ null
+ );
+
+ util.callInSeries([
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot, next);
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
+ },
+ function(retrievedSnapshot, next) {
+ expect(retrievedSnapshot).to.eql(snapshot);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the most recent snapshot before the requested version', function(done) {
+ var snapshot1 = new Snapshot(
+ 'catcher-in-the-rye',
+ 1,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye'},
+ null
+ );
+
+ var snapshot2 = new Snapshot(
+ 'catcher-in-the-rye',
+ 2,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye', author: 'J.D. Salinger'},
+ null
+ );
+
+ var snapshot10 = new Snapshot(
+ 'catcher-in-the-rye',
+ 10,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye', author: 'J.D. Salinger', publicationDate: '1951-07-16'},
+ null
+ );
+
+ util.callInSeries([
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot1, next);
+ },
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot2, next);
+ },
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot10, next);
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the most recent snapshot even if they are inserted in the wrong order', function(done) {
+ var snapshot1 = new Snapshot(
+ 'catcher-in-the-rye',
+ 1,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye'},
+ null
+ );
+
+ var snapshot2 = new Snapshot(
+ 'catcher-in-the-rye',
+ 2,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye', author: 'J.D. Salinger'},
+ null
+ );
+
+ util.callInSeries([
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot2, next);
+ },
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot1, next);
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the most recent snapshot when the version is null', function(done) {
+ var snapshot1 = new Snapshot(
+ 'catcher-in-the-rye',
+ 1,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye'},
+ null
+ );
+
+ var snapshot2 = new Snapshot(
+ 'catcher-in-the-rye',
+ 2,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye', author: 'J.D. Salinger'},
+ null
+ );
+
+ util.callInSeries([
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot1, next);
+ },
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot2, next);
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('errors when fetching an undefined version', function(done) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('errors when fetching version -1', function(done) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', -1, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('errors when fetching version "foo"', function(done) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 'foo', function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('errors when fetching a null collection', function(done) {
+ db.getMilestoneSnapshot(null, 'catcher-in-the-rye', 1, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('errors when fetching a null ID', function(done) {
+ db.getMilestoneSnapshot('books', null, 1, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('errors when saving a null collection', function(done) {
+ var snapshot = new Snapshot(
+ 'catcher-in-the-rye',
+ 1,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye'},
+ null
+ );
+
+ db.saveMilestoneSnapshot(null, snapshot, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('returns undefined if no snapshot exists', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('does not store a milestone snapshot on commit', function(done) {
+ util.callInSeries([
+ function(next) {
+ var doc = backend.connect().get('books', 'catcher-in-the-rye');
+ doc.create({title: 'Catcher in the Rye'}, next);
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('can save without a callback', function(done) {
+ var snapshot = new Snapshot(
+ 'catcher-in-the-rye',
+ 1,
+ 'http://sharejs.org/types/JSONv0',
+ {title: 'Catcher in the Rye'},
+ null
+ );
+
+ db.on('save', function(collection, snapshot) {
+ expect(collection).to.be('books');
+ expect(snapshot).to.eql(snapshot);
+ done();
+ });
+
+ db.saveMilestoneSnapshot('books', snapshot);
+ });
+
+ it('errors when the snapshot is undefined', function(done) {
+ db.saveMilestoneSnapshot('books', undefined, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ describe('snapshots with timestamps', function() {
+ var snapshot1 = new Snapshot(
+ 'catcher-in-the-rye',
+ 1,
+ 'http://sharejs.org/types/JSONv0',
+ {
+ title: 'Catcher in the Rye'
+ },
+ {
+ ctime: 1000,
+ mtime: 1000
+ }
+ );
+
+ var snapshot2 = new Snapshot(
+ 'catcher-in-the-rye',
+ 2,
+ 'http://sharejs.org/types/JSONv0',
+ {
+ title: 'Catcher in the Rye',
+ author: 'JD Salinger'
+ },
+ {
+ ctime: 1000,
+ mtime: 2000
+ }
+ );
+
+ var snapshot3 = new Snapshot(
+ 'catcher-in-the-rye',
+ 3,
+ 'http://sharejs.org/types/JSONv0',
+ {
+ title: 'Catcher in the Rye',
+ author: 'J.D. Salinger'
+ },
+ {
+ ctime: 1000,
+ mtime: 3000
+ }
+ );
+
+ beforeEach(function(done) {
+ util.callInSeries([
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot1, next);
+ },
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot2, next);
+ },
+ function(next) {
+ db.saveMilestoneSnapshot('books', snapshot3, next);
+ },
+ done
+ ]);
+ });
+
+ describe('fetching a snapshot before or at a time', function() {
+ it('fetches a snapshot before a given time', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2500, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches a snapshot at an exact time', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2000, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the first snapshot for a null timestamp', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', null, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot1);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('returns an error for a string timestamp', function(done) {
+ db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('returns an error for a negative timestamp', function(done) {
+ db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', -1, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('returns undefined if there are no snapshots before a time', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 0, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('errors if no collection is provided', function(done) {
+ db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('errors if no ID is provided', function(done) {
+ db.getMilestoneSnapshotAtOrBeforeTime('books', undefined, 0, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+ });
+
+ describe('fetching a snapshot after or at a time', function() {
+ it('fetches a snapshot after a given time', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2500, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot3);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches a snapshot at an exact time', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2000, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot2);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('fetches the last snapshot for a null timestamp', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', null, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.eql(snapshot3);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('returns an error for a string timestamp', function(done) {
+ db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('returns an error for a negative timestamp', function(done) {
+ db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', -1, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('returns undefined if there are no snapshots after a time', function(done) {
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 4000, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ done
+ ]);
+ });
+
+ it('errors if no collection is provided', function(done) {
+ db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+
+ it('errors if no ID is provided', function(done) {
+ db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function(error) {
+ expect(error).to.be.ok();
+ done();
+ });
+ });
+ });
+ });
+
+ describe('milestones enabled for every version', function() {
+ beforeEach(function(done) {
+ var options = {interval: 1};
+
+ create(options, function(error, createdDb) {
+ if (error) return done(error);
+ db = createdDb;
+ backend = new Backend({milestoneDb: db});
+ done();
+ });
+ });
+
+ it('stores a milestone snapshot on commit', function(done) {
+ db.on('save', function(collection, snapshot) {
+ expect(collection).to.be('books');
+ expect(snapshot.data).to.eql({title: 'Catcher in the Rye'});
+ done();
+ });
+
+ var doc = backend.connect().get('books', 'catcher-in-the-rye');
+ doc.create({title: 'Catcher in the Rye'});
+ });
+ });
+
+ describe('milestones enabled for every other version', function() {
+ beforeEach(function(done) {
+ var options = {interval: 2};
+
+ create(options, function(error, createdDb) {
+ if (error) return done(error);
+ db = createdDb;
+ backend = new Backend({milestoneDb: db});
+ done();
+ });
+ });
+
+ it('only stores even-numbered versions', function(done) {
+ db.on('save', function(collection, snapshot) {
+ if (snapshot.v !== 4) return;
+
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot.v).to.be(2);
+ next();
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot.v).to.be(2);
+ next();
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot.v).to.be(4);
+ next();
+ },
+ done
+ ]);
+ });
+
+ var doc = backend.connect().get('books', 'catcher-in-the-rye');
+
+ util.callInSeries([
+ function(next) {
+ doc.create({title: 'Catcher in the Rye'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], oi: 'J.F.Salinger'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger'}, next);
+ }
+ ]);
+ });
+
+ it('can have the saving logic overridden in middleware', function(done) {
+ backend.use('commit', function(request, callback) {
+ request.saveMilestoneSnapshot = request.snapshot.v >= 3;
+ callback();
+ });
+
+ db.on('save', function(collection, snapshot) {
+ if (snapshot.v !== 4) return;
+
+ util.callInSeries([
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot).to.be(undefined);
+ next();
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot.v).to.be(3);
+ next();
+ },
+ function(next) {
+ db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
+ },
+ function(snapshot, next) {
+ expect(snapshot.v).to.be(4);
+ next();
+ },
+ done
+ ]);
+ });
+
+ var doc = backend.connect().get('books', 'catcher-in-the-rye');
+
+ util.callInSeries([
+ function(next) {
+ doc.create({title: 'Catcher in the Rye'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], oi: 'J.F.Salinger'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger'}, next);
+ },
+ function(next) {
+ doc.submitOp({p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger'}, next);
+ }
+ ]);
+ });
+ });
+ });
+};
diff --git a/test/ot.js b/test/ot.js
index 3f663bac4..b58b9c899 100644
--- a/test/ot.js
+++ b/test/ot.js
@@ -3,7 +3,6 @@ var ot = require('../lib/ot');
var type = require('../lib/types').defaultType;
describe('ot', function() {
-
describe('checkOp', function() {
it('fails if op is not an object', function() {
expect(ot.checkOp('hi')).ok();
@@ -28,7 +27,7 @@ describe('ot', function() {
});
it('fails if the type is missing', function() {
- expect(ot.checkOp({create:{type: 'something that does not exist'}})).ok();
+ expect(ot.checkOp({create: {type: 'something that does not exist'}})).ok();
});
it('accepts valid create operations', function() {
@@ -37,11 +36,11 @@ describe('ot', function() {
});
it('accepts valid delete operations', function() {
- expect(ot.checkOp({del:true})).equal();
+ expect(ot.checkOp({del: true})).equal();
});
it('accepts valid ops', function() {
- expect(ot.checkOp({op:[1,2,3]})).equal();
+ expect(ot.checkOp({op: [1, 2, 3]})).equal();
});
});
@@ -106,11 +105,11 @@ describe('ot', function() {
describe('op', function() {
it('fails if the document does not exist', function() {
- expect(ot.apply({v: 6}, {v: 6, op: [1,2,3]})).ok();
+ expect(ot.apply({v: 6}, {v: 6, op: [1, 2, 3]})).ok();
});
it('fails if the type is missing', function() {
- expect(ot.apply({v: 6, type: 'some non existant type'}, {v: 6, op: [1,2,3]})).ok();
+ expect(ot.apply({v: 6, type: 'some non existant type'}, {v: 6, op: [1, 2, 3]})).ok();
});
it('applies the operation to the document data', function() {
@@ -238,5 +237,4 @@ describe('ot', function() {
expect(op).eql({});
});
});
-
});
diff --git a/test/projections.js b/test/projections.js
index 3d6f35687..52dc72618 100644
--- a/test/projections.js
+++ b/test/projections.js
@@ -3,7 +3,6 @@ var projections = require('../lib/projections');
var type = require('../lib/types').defaultType.uri;
describe('projection utility methods', function() {
-
describe('projectSnapshot', function() {
function test(fields, snapshot, expected) {
projections.projectSnapshot(fields, snapshot);
@@ -91,8 +90,8 @@ describe('projection utility methods', function() {
);
test(
{x: true},
- {type: type, data: {x: [1,2,3]}},
- {type: type, data: {x: [1,2,3]}}
+ {type: type, data: {x: [1, 2, 3]}},
+ {type: type, data: {x: [1, 2, 3]}}
);
test(
{x: true},
@@ -186,23 +185,23 @@ describe('projection utility methods', function() {
it('filters root ops', function() {
test(
{},
- {op: [{p: [], od: {a:1, x: 2}, oi: {x: 3}}]},
+ {op: [{p: [], od: {a: 1, x: 2}, oi: {x: 3}}]},
{op: [{p: [], od: {}, oi: {}}]}
);
test(
{x: true},
- {op: [{p: [], od: {a:1, x: 2}, oi: {x: 3}}]},
+ {op: [{p: [], od: {a: 1, x: 2}, oi: {x: 3}}]},
{op: [{p: [], od: {x: 2}, oi: {x: 3}}]}
);
test(
{x: true},
- {op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]},
+ {op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]},
{op: [{p: [], od: {x: 2}, oi: {}}]}
);
test(
- {x: true, a:true, z:true},
- {op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]},
- {op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]}
+ {x: true, a: true, z: true},
+ {op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]},
+ {op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]}
);
test(
{x: true},
@@ -212,7 +211,7 @@ describe('projection utility methods', function() {
// If you make the document something other than an object, it just looks like null.
test(
{x: true},
- {op: [{p: [], od: {a:2, x: 5}, oi: []}]},
+ {op: [{p: [], od: {a: 2, x: 5}, oi: []}]},
{op: [{p: [], od: {x: 5}, oi: null}]}
);
});
@@ -359,7 +358,7 @@ describe('projection utility methods', function() {
{},
{del: true}
);
- expect(projections.isOpAllowed(null, {}, {del:true})).equal(true);
+ expect(projections.isOpAllowed(null, {}, {del: true})).equal(true);
});
it('works with ops', function() {
diff --git a/test/pubsub.js b/test/pubsub.js
index 48f8abb6b..57c193095 100644
--- a/test/pubsub.js
+++ b/test/pubsub.js
@@ -38,7 +38,7 @@ module.exports = function(create) {
it('publish optional callback returns', function(done) {
var pubsub = this.pubsub;
- pubsub.subscribe('x', function(err, stream) {
+ pubsub.subscribe('x', function(err) {
if (err) done(err);
pubsub.publish(['x'], {test: true}, done);
});
@@ -46,10 +46,10 @@ module.exports = function(create) {
it('can subscribe to a channel twice', function(done) {
var pubsub = this.pubsub;
- pubsub.subscribe('y', function(err, stream) {
+ pubsub.subscribe('y', function(err) {
+ if (err) done(err);
pubsub.subscribe('y', function(err, stream) {
if (err) done(err);
- var emitted;
stream.on('data', function(data) {
expect(data).eql({test: true});
done();
diff --git a/test/util.js b/test/util.js
index 362337588..ee32c8744 100644
--- a/test/util.js
+++ b/test/util.js
@@ -45,3 +45,24 @@ exports.callAfter = function(calls, callback) {
callbackAfter.called = 0;
return callbackAfter;
};
+
+exports.callInSeries = function(callbacks, args) {
+ if (!callbacks.length) return;
+ args = args || [];
+ var error = args.shift();
+
+ if (error) {
+ var finalCallback = callbacks[callbacks.length - 1];
+ return finalCallback(error);
+ }
+
+ var callback = callbacks.shift();
+ if (callbacks.length) {
+ args.push(function() {
+ var args = Array.from(arguments);
+ exports.callInSeries(callbacks, args);
+ });
+ }
+
+ callback.apply(callback, args);
+};