From 78306cca088610167aa7c75c41ea74bf7e2359a8 Mon Sep 17 00:00:00 2001 From: Maximilian Alexander Date: Thu, 26 Jan 2017 14:11:04 -0800 Subject: [PATCH 001/181] adding reconnecting websocket example in text area and updating readme --- examples/textarea/client.js | 23 ++++++++++++++++++++++- examples/textarea/package.json | 1 + examples/textarea/static/index.html | 6 ++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/examples/textarea/client.js b/examples/textarea/client.js index 964239da3..e02b99d17 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 +const 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'); +status.innerHTML = "Not Connected" + +element.style.backgroundColor = "gray"; +socket.onopen = function(){ + status.innerHTML = "Connected" + element.style.backgroundColor = "white"; +}; + +socket.onclose = function(){ + status.innerHTML = "Closed" + element.style.backgroundColor = "gray"; +}; + +socket.onerror = function() { + status.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); binding.setup(); }); diff --git a/examples/textarea/package.json b/examples/textarea/package.json index 8e436c2f6..d04946413 100644 --- a/examples/textarea/package.json +++ b/examples/textarea/package.json @@ -15,6 +15,7 @@ "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", 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:

From e1f7ab058940236818b385b74742358fd4faea5a Mon Sep 17 00:00:00 2001 From: Maximilian Alexander Date: Thu, 26 Jan 2017 14:11:28 -0800 Subject: [PATCH 002/181] Updating Readme --- README.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ebdf2a661..87419f86c 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 [https://github.com/pladaria/reconnecting-websocket](reconnecting-websocket). + + + ## Example apps [ From 24cae9c4f7c2dbc71a31376a4ecb6f15c89652bf Mon Sep 17 00:00:00 2001 From: Zach Millman Date: Fri, 23 Feb 2018 14:04:56 -0800 Subject: [PATCH 003/181] Add note about projectsSnapshots property --- lib/db/index.js | 1 + 1 file changed, 1 insertion(+) 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; From 927b4eb457c7ebd0898953af9847a503f214eab0 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Mon, 25 Jun 2018 17:10:59 +0100 Subject: [PATCH 004/181] Allow snapshot and op to be a non-object --- lib/ot.js | 2 +- package-lock.json | 1481 ++++++++++++++++++++++++++++++++++++ test/client/number-type.js | 23 + test/client/submit.js | 15 + 4 files changed, 1520 insertions(+), 1 deletion(-) create mode 100644 package-lock.json create mode 100644 test/client/number-type.js diff --git a/lib/ot.js b/lib/ot.js index 1dc89bcfc..8cf708521 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -102,7 +102,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'}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..0df7dc86a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1481 @@ +{ + "name": "sharedb", + "version": "1.0.0-beta.9", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "abbrev": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", + "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", + "dev": true + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", + "dev": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arraydiff": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/arraydiff/-/arraydiff-0.1.3.tgz", + "integrity": "sha1-hqVDbXty8b3aX9bXTock5C+Dzk0=" + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true + }, + "aws4": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", + "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", + "dev": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true, + "optional": true + }, + "caseless": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", + "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", + "dev": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "dev": true, + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "cli": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "dev": true, + "requires": { + "exit": "0.1.2", + "glob": "^7.1.1" + }, + "dependencies": { + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "optional": true, + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true, + "optional": true + } + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "requires": { + "date-now": "^0.1.4" + } + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true + }, + "coveralls": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.3.tgz", + "integrity": "sha512-iiAmn+l1XqRwNLXhW8Rs5qHZRFMYp9ZIPjEOVRpC/c4so6Y/f4/lFi0FfR5B9cCqgyhkJ5cZmbvcVRfP8MHchw==", + "dev": true, + "requires": { + "js-yaml": "3.6.1", + "lcov-parse": "0.0.10", + "log-driver": "1.2.5", + "minimist": "1.2.0", + "request": "2.79.0" + } + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "requires": { + "boom": "2.x.x" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true + }, + "debug": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", + "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true, + "optional": true + }, + "deep-is": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", + "dev": true + }, + "dom-serializer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", + "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", + "dev": true, + "requires": { + "domelementtype": "~1.1.1", + "entities": "~1.1.1" + }, + "dependencies": { + "domelementtype": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", + "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", + "dev": true + }, + "entities": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", + "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", + "dev": true + } + } + }, + "domelementtype": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", + "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", + "dev": true + }, + "domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", + "dev": true, + "requires": { + "domelementtype": "1" + } + }, + "domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", + "dev": true, + "requires": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "~0.1.0" + } + }, + "entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "escodegen": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", + "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", + "dev": true, + "requires": { + "esprima": "^2.7.1", + "estraverse": "^1.9.1", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.2.0" + } + }, + "esprima": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", + "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", + "dev": true + }, + "estraverse": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", + "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", + "dev": true + }, + "esutils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", + "dev": true + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, + "expect.js": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", + "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.5", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "generate-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", + "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", + "dev": true + }, + "generate-object-property": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", + "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", + "dev": true, + "requires": { + "is-property": "^1.0.0" + } + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "glob": { + "version": "5.0.15", + "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", + "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", + "dev": true, + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "graceful-readlink": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", + "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", + "dev": true + }, + "growl": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", + "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", + "dev": true + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "dev": true, + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "dev": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-validator": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", + "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", + "dev": true, + "requires": { + "chalk": "^1.1.1", + "commander": "^2.9.0", + "is-my-json-valid": "^2.12.4", + "pinkie-promise": "^2.0.0" + } + }, + "has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "has-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", + "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", + "dev": true + }, + "hat": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", + "integrity": "sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=" + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "requires": { + "boom": "2.x.x", + "cryptiles": "2.x.x", + "hoek": "2.x.x", + "sntp": "1.x.x" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", + "dev": true, + "requires": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + } + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "requires": { + "assert-plus": "^0.2.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-my-ip-valid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", + "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", + "dev": true + }, + "is-my-json-valid": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", + "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", + "dev": true, + "requires": { + "generate-function": "^2.0.0", + "generate-object-property": "^1.1.0", + "is-my-ip-valid": "^1.0.0", + "jsonpointer": "^4.0.0", + "xtend": "^4.0.0" + } + }, + "is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "istanbul": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", + "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", + "dev": true, + "requires": { + "abbrev": "1.0.x", + "async": "1.x", + "escodegen": "1.8.x", + "esprima": "2.7.x", + "glob": "^5.0.15", + "handlebars": "^4.0.1", + "js-yaml": "3.x", + "mkdirp": "0.5.x", + "nopt": "3.x", + "once": "1.x", + "resolve": "1.1.x", + "supports-color": "^3.1.0", + "which": "^1.1.1", + "wordwrap": "^1.0.0" + }, + "dependencies": { + "supports-color": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", + "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "js-yaml": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", + "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^2.6.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "jshint": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.5.tgz", + "integrity": "sha1-HnJSkVzmgbQIJ+4UJIxG006apiw=", + "dev": true, + "requires": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "3.7.x", + "minimatch": "~3.0.2", + "shelljs": "0.3.x", + "strip-json-comments": "1.0.x" + } + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "json3": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", + "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", + "dev": true + }, + "jsonpointer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", + "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", + "dev": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true, + "optional": true + }, + "lcov-parse": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", + "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", + "dev": true + }, + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lodash": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz", + "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=", + "dev": true + }, + "lodash._baseassign": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", + "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", + "dev": true, + "requires": { + "lodash._basecopy": "^3.0.0", + "lodash.keys": "^3.0.0" + } + }, + "lodash._basecopy": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", + "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", + "dev": true + }, + "lodash._basecreate": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", + "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", + "dev": true + }, + "lodash._getnative": { + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", + "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", + "dev": true + }, + "lodash._isiterateecall": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", + "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", + "dev": true + }, + "lodash.create": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", + "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", + "dev": true, + "requires": { + "lodash._baseassign": "^3.0.0", + "lodash._basecreate": "^3.0.0", + "lodash._isiterateecall": "^3.0.0" + } + }, + "lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", + "dev": true + }, + "lodash.isarray": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", + "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", + "dev": true + }, + "lodash.keys": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", + "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", + "dev": true, + "requires": { + "lodash._getnative": "^3.0.0", + "lodash.isarguments": "^3.0.0", + "lodash.isarray": "^3.0.0" + } + }, + "log-driver": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", + "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=", + "dev": true + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==" + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "~1.33.0" + } + }, + "mingo": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/mingo/-/mingo-2.2.2.tgz", + "integrity": "sha1-vmnUhq5uCsVLl53F9EEtshhR9pM=", + "dev": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + } + } + }, + "mocha": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", + "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.9.0", + "debug": "2.6.8", + "diff": "3.2.0", + "escape-string-regexp": "1.0.5", + "glob": "7.1.1", + "growl": "1.9.2", + "he": "1.1.1", + "json3": "3.3.2", + "lodash.create": "3.1.1", + "mkdirp": "0.5.1", + "supports-color": "3.1.2" + }, + "dependencies": { + "commander": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", + "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", + "dev": true, + "requires": { + "graceful-readlink": ">= 1.0.0" + } + }, + "glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "supports-color": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", + "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", + "dev": true, + "requires": { + "has-flag": "^1.0.0" + } + } + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "dev": true, + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", + "dev": true + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", + "dev": true + } + } + }, + "optionator": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", + "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.4", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "wordwrap": "~1.0.0" + } + }, + "ot-json0": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ot-json0/-/ot-json0-1.1.0.tgz", + "integrity": "sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "requires": { + "pinkie": "^2.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", + "dev": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + }, + "qs": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", + "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", + "dev": true + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "dev": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.79.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", + "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", + "dev": true, + "requires": { + "aws-sign2": "~0.6.0", + "aws4": "^1.2.1", + "caseless": "~0.11.0", + "combined-stream": "~1.0.5", + "extend": "~3.0.0", + "forever-agent": "~0.6.1", + "form-data": "~2.1.1", + "har-validator": "~2.0.6", + "hawk": "~3.1.3", + "http-signature": "~1.1.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.7", + "oauth-sign": "~0.8.1", + "qs": "~6.3.0", + "stringstream": "~0.0.4", + "tough-cookie": "~2.3.0", + "tunnel-agent": "~0.4.1", + "uuid": "^3.0.0" + } + }, + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "optional": true, + "requires": { + "align-text": "^0.1.1" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "sharedb": { + "version": "1.0.0-beta.9", + "resolved": "https://registry.npmjs.org/sharedb/-/sharedb-1.0.0-beta.9.tgz", + "integrity": "sha1-LX20J83hIJJNLasIzpLZq6134As=", + "dev": true, + "requires": { + "arraydiff": "^0.1.1", + "async": "^1.4.2", + "deep-is": "^0.1.3", + "hat": "0.0.3", + "make-error": "^1.1.1", + "ot-json0": "^1.0.1" + } + }, + "sharedb-mingo-memory": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/sharedb-mingo-memory/-/sharedb-mingo-memory-1.0.0.tgz", + "integrity": "sha1-vS5171YTCrheE5uMlMVSFv4+TQM=", + "dev": true, + "requires": { + "mingo": "^2.2.0", + "sharedb": "^1.0.0-beta" + } + }, + "shelljs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", + "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "requires": { + "hoek": "2.x.x" + } + }, + "source-map": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", + "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", + "dev": true, + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "dev": true, + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", + "dev": true + }, + "stringstream": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", + "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", + "dev": true + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", + "dev": true + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", + "dev": true + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "requires": { + "punycode": "^1.4.1" + } + }, + "tunnel-agent": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", + "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", + "dev": true + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "optional": true, + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true, + "optional": true + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true + } + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true, + "optional": true + }, + "wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "optional": true, + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } +} diff --git a/test/client/number-type.js b/test/client/number-type.js new file mode 100644 index 000000000..d25401ebc --- /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, op2, side) { + return op1; +} diff --git a/test/client/submit.js b/test/client/submit.js index 4e508e66e..b80abe0af 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -2,8 +2,10 @@ 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() { @@ -1044,6 +1046,19 @@ describe('client submit', function() { }); }); + 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.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) { var doc = this.backend.connect().get('dogs', 'fido'); From 90b46d6a9be8de497a4d9ba84e7b71c0dc8c9140 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 12 Jul 2018 11:39:28 +0200 Subject: [PATCH 005/181] Remove package-lock.json --- package-lock.json | 1481 --------------------------------------------- 1 file changed, 1481 deletions(-) delete mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 0df7dc86a..000000000 --- a/package-lock.json +++ /dev/null @@ -1,1481 +0,0 @@ -{ - "name": "sharedb", - "version": "1.0.0-beta.9", - "lockfileVersion": 1, - "requires": true, - "dependencies": { - "abbrev": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz", - "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU=", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "dev": true, - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" - } - }, - "amdefine": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", - "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", - "dev": true - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arraydiff": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/arraydiff/-/arraydiff-0.1.3.tgz", - "integrity": "sha1-hqVDbXty8b3aX9bXTock5C+Dzk0=" - }, - "asn1": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", - "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", - "dev": true - }, - "assert-plus": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", - "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", - "dev": true - }, - "async": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", - "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "aws-sign2": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", - "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", - "dev": true - }, - "aws4": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.7.0.tgz", - "integrity": "sha512-32NDda82rhwD9/JBCCkB+MRYDp0oSvlo2IL6rQWA10PQi7tDUM3eqMSltXmY+Oyl/7N3P3qNtAlv7X0d9bI28w==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", - "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", - "dev": true, - "optional": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "boom": { - "version": "2.10.1", - "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", - "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "browser-stdout": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", - "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", - "dev": true - }, - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", - "dev": true, - "optional": true - }, - "caseless": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz", - "integrity": "sha1-cVuW6phBWTzDMGeSP17GDr2k99c=", - "dev": true - }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" - } - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", - "dev": true, - "requires": { - "exit": "0.1.2", - "glob": "^7.1.1" - }, - "dependencies": { - "glob": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", - "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "dev": true, - "optional": true, - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", - "dev": true, - "optional": true - } - } - }, - "combined-stream": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", - "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", - "dev": true - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true - }, - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true - }, - "coveralls": { - "version": "2.13.3", - "resolved": "https://registry.npmjs.org/coveralls/-/coveralls-2.13.3.tgz", - "integrity": "sha512-iiAmn+l1XqRwNLXhW8Rs5qHZRFMYp9ZIPjEOVRpC/c4so6Y/f4/lFi0FfR5B9cCqgyhkJ5cZmbvcVRfP8MHchw==", - "dev": true, - "requires": { - "js-yaml": "3.6.1", - "lcov-parse": "0.0.10", - "log-driver": "1.2.5", - "minimist": "1.2.0", - "request": "2.79.0" - } - }, - "cryptiles": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", - "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", - "dev": true, - "requires": { - "boom": "2.x.x" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "debug": { - "version": "2.6.8", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", - "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true, - "optional": true - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "diff": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", - "dev": true - }, - "dom-serializer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.0.tgz", - "integrity": "sha1-BzxpdUbOB4DOI75KKOKT5AvDDII=", - "dev": true, - "requires": { - "domelementtype": "~1.1.1", - "entities": "~1.1.1" - }, - "dependencies": { - "domelementtype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.1.3.tgz", - "integrity": "sha1-vSh3PiZCiBrsUVRJJCmcXNgiGFs=", - "dev": true - }, - "entities": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.1.tgz", - "integrity": "sha1-blwtClYhtdra7O+AuQ7ftc13cvA=", - "dev": true - } - } - }, - "domelementtype": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.0.tgz", - "integrity": "sha1-sXrtguirWeUt2cGbF1bg/BhyBMI=", - "dev": true - }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "ecc-jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", - "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", - "dev": true, - "optional": true, - "requires": { - "jsbn": "~0.1.0" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "escodegen": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz", - "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=", - "dev": true, - "requires": { - "esprima": "^2.7.1", - "estraverse": "^1.9.1", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.2.0" - } - }, - "esprima": { - "version": "2.7.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz", - "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE=", - "dev": true - }, - "estraverse": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz", - "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q=", - "dev": true - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expect.js": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/expect.js/-/expect.js-0.3.1.tgz", - "integrity": "sha1-sKWaDS7/VDdUTr8M6qYBWEHQm1s=", - "dev": true - }, - "extend": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", - "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", - "dev": true - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true - }, - "form-data": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", - "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.5", - "mime-types": "^2.1.12" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true - }, - "generate-function": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", - "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", - "dev": true - }, - "generate-object-property": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", - "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", - "dev": true, - "requires": { - "is-property": "^1.0.0" - } - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, - "growl": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", - "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", - "dev": true - }, - "handlebars": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", - "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", - "dev": true, - "requires": { - "async": "^1.4.0", - "optimist": "^0.6.1", - "source-map": "^0.4.4", - "uglify-js": "^2.6" - }, - "dependencies": { - "source-map": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", - "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", - "dev": true, - "requires": { - "amdefine": ">=0.0.4" - } - } - } - }, - "har-validator": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz", - "integrity": "sha1-zcvAgYgmWtEZtqWnyKtw7s+10n0=", - "dev": true, - "requires": { - "chalk": "^1.1.1", - "commander": "^2.9.0", - "is-my-json-valid": "^2.12.4", - "pinkie-promise": "^2.0.0" - } - }, - "has-ansi": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "has-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz", - "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo=", - "dev": true - }, - "hat": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/hat/-/hat-0.0.3.tgz", - "integrity": "sha1-uwFKnmSzeIrtgAWRdBPU/z1QLYo=" - }, - "hawk": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", - "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", - "dev": true, - "requires": { - "boom": "2.x.x", - "cryptiles": "2.x.x", - "hoek": "2.x.x", - "sntp": "1.x.x" - } - }, - "he": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", - "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", - "dev": true - }, - "hoek": { - "version": "2.16.3", - "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", - "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", - "dev": true - }, - "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "http-signature": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", - "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", - "dev": true, - "requires": { - "assert-plus": "^0.2.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-my-ip-valid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-my-ip-valid/-/is-my-ip-valid-1.0.0.tgz", - "integrity": "sha512-gmh/eWXROncUzRnIa1Ubrt5b8ep/MGSnfAUI3aRp+sqTCs1tv1Isl8d8F6JmkN3dXKc3ehZMrtiPN9eL03NuaQ==", - "dev": true - }, - "is-my-json-valid": { - "version": "2.17.2", - "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.17.2.tgz", - "integrity": "sha512-IBhBslgngMQN8DDSppmgDv7RNrlFotuuDsKcrCP3+HbFaVivIBU7u9oiiErw8sH4ynx3+gOGQ3q2otkgiSi6kg==", - "dev": true, - "requires": { - "generate-function": "^2.0.0", - "generate-object-property": "^1.1.0", - "is-my-ip-valid": "^1.0.0", - "jsonpointer": "^4.0.0", - "xtend": "^4.0.0" - } - }, - "is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", - "dev": true - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", - "dev": true - }, - "istanbul": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz", - "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=", - "dev": true, - "requires": { - "abbrev": "1.0.x", - "async": "1.x", - "escodegen": "1.8.x", - "esprima": "2.7.x", - "glob": "^5.0.15", - "handlebars": "^4.0.1", - "js-yaml": "3.x", - "mkdirp": "0.5.x", - "nopt": "3.x", - "once": "1.x", - "resolve": "1.1.x", - "supports-color": "^3.1.0", - "which": "^1.1.1", - "wordwrap": "^1.0.0" - }, - "dependencies": { - "supports-color": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz", - "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "js-yaml": { - "version": "3.6.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.6.1.tgz", - "integrity": "sha1-bl/mfYsgXOTSL60Ft3geja3MSzA=", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^2.6.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", - "dev": true, - "optional": true - }, - "jshint": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.9.5.tgz", - "integrity": "sha1-HnJSkVzmgbQIJ+4UJIxG006apiw=", - "dev": true, - "requires": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "3.7.x", - "minimatch": "~3.0.2", - "shelljs": "0.3.x", - "strip-json-comments": "1.0.x" - } - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json3": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.2.tgz", - "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", - "dev": true - }, - "jsonpointer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", - "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", - "dev": true - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", - "dev": true, - "optional": true - }, - "lcov-parse": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/lcov-parse/-/lcov-parse-0.0.10.tgz", - "integrity": "sha1-GwuP+ayceIklBYK3C3ExXZ2m2aM=", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lodash": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.7.0.tgz", - "integrity": "sha1-Nni9irmVBXwHreg27S7wh9qBHUU=", - "dev": true - }, - "lodash._baseassign": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/lodash._baseassign/-/lodash._baseassign-3.2.0.tgz", - "integrity": "sha1-jDigmVAPIVrQnlnxci/QxSv+Ck4=", - "dev": true, - "requires": { - "lodash._basecopy": "^3.0.0", - "lodash.keys": "^3.0.0" - } - }, - "lodash._basecopy": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/lodash._basecopy/-/lodash._basecopy-3.0.1.tgz", - "integrity": "sha1-jaDmqHbPNEwK2KVIghEd08XHyjY=", - "dev": true - }, - "lodash._basecreate": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-3.0.3.tgz", - "integrity": "sha1-G8ZhYU2qf8MRt9A78WgGoCE8+CE=", - "dev": true - }, - "lodash._getnative": { - "version": "3.9.1", - "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz", - "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U=", - "dev": true - }, - "lodash._isiterateecall": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/lodash._isiterateecall/-/lodash._isiterateecall-3.0.9.tgz", - "integrity": "sha1-UgOte6Ql+uhCRg5pbbnPPmqsBXw=", - "dev": true - }, - "lodash.create": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lodash.create/-/lodash.create-3.1.1.tgz", - "integrity": "sha1-1/KEnw29p+BGgruM1yqwIkYd6+c=", - "dev": true, - "requires": { - "lodash._baseassign": "^3.0.0", - "lodash._basecreate": "^3.0.0", - "lodash._isiterateecall": "^3.0.0" - } - }, - "lodash.isarguments": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", - "integrity": "sha1-L1c9hcaiQon/AGY7SRwdM4/zRYo=", - "dev": true - }, - "lodash.isarray": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/lodash.isarray/-/lodash.isarray-3.0.4.tgz", - "integrity": "sha1-eeTriMNqgSKvhvhEqpvNhRtfu1U=", - "dev": true - }, - "lodash.keys": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-3.1.2.tgz", - "integrity": "sha1-TbwEcrFWvlCgsoaFXRvQsMZWCYo=", - "dev": true, - "requires": { - "lodash._getnative": "^3.0.0", - "lodash.isarguments": "^3.0.0", - "lodash.isarray": "^3.0.0" - } - }, - "log-driver": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/log-driver/-/log-driver-1.2.5.tgz", - "integrity": "sha1-euTsJXMC/XkNVXyxDJcQDYV7AFY=", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true - }, - "make-error": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", - "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==" - }, - "mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "dev": true - }, - "mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "dev": true, - "requires": { - "mime-db": "~1.33.0" - } - }, - "mingo": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/mingo/-/mingo-2.2.2.tgz", - "integrity": "sha1-vmnUhq5uCsVLl53F9EEtshhR9pM=", - "dev": true - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", - "dev": true - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "requires": { - "minimist": "0.0.8" - }, - "dependencies": { - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true - } - } - }, - "mocha": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-3.5.3.tgz", - "integrity": "sha512-/6na001MJWEtYxHOV1WLfsmR4YIynkUEhBwzsb+fk2qmQ3iqsi258l/Q2MWHJMImAcNpZ8DEdYAK72NHoIQ9Eg==", - "dev": true, - "requires": { - "browser-stdout": "1.3.0", - "commander": "2.9.0", - "debug": "2.6.8", - "diff": "3.2.0", - "escape-string-regexp": "1.0.5", - "glob": "7.1.1", - "growl": "1.9.2", - "he": "1.1.1", - "json3": "3.3.2", - "lodash.create": "3.1.1", - "mkdirp": "0.5.1", - "supports-color": "3.1.2" - }, - "dependencies": { - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, - "glob": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", - "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "supports-color": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.1.2.tgz", - "integrity": "sha1-cqJiiU2dQIuVbKBf83su2KbiotU=", - "dev": true, - "requires": { - "has-flag": "^1.0.0" - } - } - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "oauth-sign": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", - "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "dev": true, - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "minimist": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", - "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=", - "dev": true - }, - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=", - "dev": true - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "ot-json0": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ot-json0/-/ot-json0-1.1.0.tgz", - "integrity": "sha512-wf5fci7GGpMYRDnbbdIFQymvhsbFACMHtxjivQo5KgvAHlxekyfJ9aPsRr6YfFQthQkk4bmsl5yESrZwC/oMYQ==" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true - }, - "qs": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.3.2.tgz", - "integrity": "sha1-51vV9uJoEioqDgvaYwslUMFmUCw=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true - }, - "request": { - "version": "2.79.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.79.0.tgz", - "integrity": "sha1-Tf5b9r6LjNw3/Pk+BLZVd3InEN4=", - "dev": true, - "requires": { - "aws-sign2": "~0.6.0", - "aws4": "^1.2.1", - "caseless": "~0.11.0", - "combined-stream": "~1.0.5", - "extend": "~3.0.0", - "forever-agent": "~0.6.1", - "form-data": "~2.1.1", - "har-validator": "~2.0.6", - "hawk": "~3.1.3", - "http-signature": "~1.1.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.7", - "oauth-sign": "~0.8.1", - "qs": "~6.3.0", - "stringstream": "~0.0.4", - "tough-cookie": "~2.3.0", - "tunnel-agent": "~0.4.1", - "uuid": "^3.0.0" - } - }, - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "dev": true, - "optional": true, - "requires": { - "align-text": "^0.1.1" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sharedb": { - "version": "1.0.0-beta.9", - "resolved": "https://registry.npmjs.org/sharedb/-/sharedb-1.0.0-beta.9.tgz", - "integrity": "sha1-LX20J83hIJJNLasIzpLZq6134As=", - "dev": true, - "requires": { - "arraydiff": "^0.1.1", - "async": "^1.4.2", - "deep-is": "^0.1.3", - "hat": "0.0.3", - "make-error": "^1.1.1", - "ot-json0": "^1.0.1" - } - }, - "sharedb-mingo-memory": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/sharedb-mingo-memory/-/sharedb-mingo-memory-1.0.0.tgz", - "integrity": "sha1-vS5171YTCrheE5uMlMVSFv4+TQM=", - "dev": true, - "requires": { - "mingo": "^2.2.0", - "sharedb": "^1.0.0-beta" - } - }, - "shelljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", - "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", - "dev": true - }, - "sntp": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", - "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", - "dev": true, - "requires": { - "hoek": "2.x.x" - } - }, - "source-map": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz", - "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=", - "dev": true, - "optional": true, - "requires": { - "amdefine": ">=0.0.4" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - }, - "sshpk": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", - "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "stringstream": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.6.tgz", - "integrity": "sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - }, - "tough-cookie": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", - "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", - "dev": true, - "requires": { - "punycode": "^1.4.1" - } - }, - "tunnel-agent": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", - "integrity": "sha1-Y3PbdpCf5XDgjXNYM2Xtgop07us=", - "dev": true - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "dev": true, - "optional": true, - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - }, - "dependencies": { - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true, - "optional": true - } - } - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "dev": true, - "optional": true - }, - "uuid": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", - "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - } - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", - "dev": true, - "optional": true - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", - "dev": true - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true - }, - "xtend": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", - "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", - "dev": true - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "dev": true, - "optional": true, - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } - } -} From 3569a631f70bb16a38aebe297240308d5e81fb9b Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 12 Jul 2018 13:30:32 +0200 Subject: [PATCH 006/181] Make whenNothingPending always async --- lib/client/doc.js | 9 ++++++++- test/client/doc.js | 43 +++++++++++++++++++++++++++---------------- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 05e17976d..66be2e166 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -200,7 +200,14 @@ Doc.prototype.whenNothingPending = function(callback) { 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() { diff --git a/test/client/doc.js b/test/client/doc.js index b44f52a2b..49078cb40 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,7 +1,7 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -describe('client query subscribe', function() { +describe('client doc subscribe', function() { beforeEach(function() { this.backend = new Backend(); @@ -14,28 +14,39 @@ 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() { From 7c846217f25fe4aaafdd427268ac2695bd7695a0 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 12 Jul 2018 13:30:49 +0200 Subject: [PATCH 007/181] Update tested nodejs versions --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7bd066b20..21efafe46 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,8 @@ language: node_js node_js: - "10" - - "9" - "8" - "6" - - "4" script: "npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From 14e5180fd1bdc27882167a47ce1f52f46f1ad570 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 15:20:53 +0200 Subject: [PATCH 008/181] Update mocha and fix 2 tests See https://github.com/mochajs/mocha/releases/tag/v5.0.2 --- package.json | 2 +- test/client/submit.js | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 35fc64bc6..4e38aba84 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", - "mocha": "^3.2.0", + "mocha": "^5.2.0", "sharedb-mingo-memory": "^1.0.0-beta" }, "scripts": { diff --git a/test/client/submit.js b/test/client/submit.js index 4e508e66e..ee3ae3e0d 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -608,11 +608,16 @@ describe('client submit', function() { doc2.del(function(err) { if (err) return done(err); doc.pause(); + var calledBack = false; + doc.on('error', function(err) { + expect(calledBack).equal(true); + done(); + }); doc.submitOp({p: ['age'], na: 1}, function(err) { expect(err).ok(); expect(doc.version).equal(2); expect(doc.data).eql(undefined); - done(); + calledBack = true; }); doc.fetch(); }); @@ -632,11 +637,16 @@ describe('client submit', function() { doc2.create({age: 5}, function(err) { if (err) return done(err); doc.pause(); + var calledBack = false; + doc.on('error', function() { + expect(calledBack).equal(true); + done(); + }); doc.create({age: 9}, function(err) { expect(err).ok(); expect(doc.version).equal(3); expect(doc.data).eql({age: 5}); - done(); + calledBack = true; }); doc.fetch(); }); From e45dcc9ffa415a2afd845d161b1c051c9380919b Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 16:05:07 +0200 Subject: [PATCH 009/181] Fix sharedb does not exist --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7bd066b20..736e5fe78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,8 @@ language: node_js node_js: - "10" - - "9" - "8" - "6" - - "4" -script: "npm run jshint && npm run test-cover" +script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From d5a03f3c2c1f1b72a67d3c76e6816972061fa773 Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Thu, 19 Jul 2018 16:40:39 +0200 Subject: [PATCH 010/181] Remove unused variable --- test/client/submit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/client/submit.js b/test/client/submit.js index ee3ae3e0d..6523880a8 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -609,7 +609,7 @@ describe('client submit', function() { if (err) return done(err); doc.pause(); var calledBack = false; - doc.on('error', function(err) { + doc.on('error', function() { expect(calledBack).equal(true); done(); }); From 0518ab7bc694a2ac7ac0a6592025cf065ae6b33e Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 19 Jul 2018 15:50:04 +0100 Subject: [PATCH 011/181] Fix flaky maxSubmitRetries test This is a slightly speculative fix for a test that fails intermittently on `sharedb-mongo`. I believe these intermittent failures are due to a race condition in a concurrency test. The test works by attempting to fire two commits off at the same time, and hoping that one of them is committed just before the other, so that a `SubmitRequest.retry` is triggered whilst the `maxSubmitRetries` is set to `0`, resulting in an error that is expected. However, I believe it's possible for these commits to (in some cases) happen sequentially rather than concurrently, and fail to error. This change attempts to force them into this retry condition by: - Catching both ops in the `commit` middleware, _just_ before they're about to be committed (and hit a `retry` if applicable) - Waiting until both ops have reached this state - Triggering the first op's `commit` - Then in the callback of that op, triggering the second op's `commit` - The second op should now find that the first op has beaten it to committing, and trigger a `retry` --- test/client/submit.js | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/test/client/submit.js b/test/client/submit.js index 4e508e66e..e4858bc62 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -509,6 +509,7 @@ describe('client submit', function() { }); 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'); @@ -516,18 +517,24 @@ describe('client submit', function() { if (err) return done(err); doc2.fetch(function(err) { if (err) return done(err); - var count = 0; - var cb = function(err) { - count++; - if (count === 1) { - if (err) return done(err); - } else { - expect(err).ok(); - done(); + var docCallback; + var doc2Callback; + backend.use('commit', function (request, callback) { + if (request.op.op[0].na === 2) docCallback = callback; + if (request.op.op[0].na === 7) doc2Callback = callback; + + if (docCallback && doc2Callback) { + docCallback(); } - }; - doc.submitOp({p: ['age'], na: 2}, cb); - doc2.submitOp({p: ['age'], na: 7}, cb); + }); + doc.submitOp({p: ['age'], na: 2}, function (error) { + if (error) return done(error); + doc2Callback(); + }); + doc2.submitOp({p: ['age'], na: 7}, function (error) { + expect(error).ok(); + done(); + }); }); }); }); From 032eb988f1680e23e9c96a73cdaf19c7ce980887 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 19 Jul 2018 17:31:18 +0100 Subject: [PATCH 012/181] Work around circular sharedb and sharedb-mingo-memory dependency See [this issue][1]. [1]: https://github.com/share/sharedb/pull/226#issuecomment-406297957 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7bd066b20..d3e71313a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,6 @@ node_js: - "8" - "6" - "4" -script: "npm run jshint && npm run test-cover" +script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From e9ff681e3dba94a17364caae276ff78de3647fa5 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Fri, 20 Jul 2018 07:36:56 +0100 Subject: [PATCH 013/181] Document race condition test fix --- test/client/submit.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/client/submit.js b/test/client/submit.js index e4858bc62..6edc2eb74 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -519,16 +519,28 @@ describe('client submit', function() { if (err) return done(err); 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) { From ec4e64738f5bed31a424c897a18c798931f2524f Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 20 Jul 2018 12:28:54 +0200 Subject: [PATCH 014/181] Warn about using deprecated features --- README.md | 1 - lib/backend.js | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e65f0187..2c8985db5 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,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 diff --git a/lib/backend.js b/lib/backend.js index 3156cfc82..7b29d9104 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -9,6 +9,8 @@ var projections = require('./projections'); var QueryEmitter = require('./query-emitter'); var StreamSocket = require('./stream-socket'); var SubmitRequest = require('./submit-request'); +var warnDeprecatedDoc = true; +var warnDeprecatedAfterSubmit = true; function Backend(options) { if (!(this instanceof Backend)) return new Backend(options); @@ -71,6 +73,11 @@ Backend.prototype.MIDDLEWARE_ACTIONS = { }; Backend.prototype._shimDocAction = function() { + if (warnDeprecatedDoc) { + warnDeprecatedDoc = false; + console.warn('DEPRECATED: "doc" middleware action. Use "readSnapshots" instead. Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.'); + } + var backend = this; this.use(this.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { async.each(request.snapshots, function(snapshot, eachCb) { @@ -83,6 +90,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('DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.'); + } + var backend = this; this.use(backend.MIDDLEWARE_ACTIONS.afterSubmit, function(request, callback) { backend.trigger(backend.MIDDLEWARE_ACTIONS['after submit'], request.agent, request, callback); From f1571310fce79825ba318c0fab56e66b00b3a98d Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Fri, 20 Jul 2018 12:42:28 +0200 Subject: [PATCH 015/181] Add workaround for circular dependency --- .travis.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7bd066b20..736e5fe78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,8 @@ language: node_js node_js: - "10" - - "9" - "8" - "6" - - "4" -script: "npm run jshint && npm run test-cover" +script: "ln -s .. node_modules/sharedb; npm run jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" From f36fc81c822b4b74ad67dfcec489f5f66fa94ae3 Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 23 Jul 2018 14:28:20 -0700 Subject: [PATCH 016/181] add package-lock.json to gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index cd1d217b2..26f870af5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,4 +30,5 @@ coverage # Dependency directories node_modules +package-lock.json jspm_packages From 5f4bd902d5679a1fa87f7e954f1c407d18a9049d Mon Sep 17 00:00:00 2001 From: Nate Smith Date: Mon, 23 Jul 2018 14:30:46 -0700 Subject: [PATCH 017/181] Doc::destroy: fix call to `this` inside error case of async callback --- lib/client/doc.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index d75e83085..71f4e2041 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -107,9 +107,8 @@ Doc.prototype.destroy = function(callback) { 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.connection._destroyDoc(doc); if (callback) callback(); From 7913314e5bc8bd2befd1fd25d90bbe8be4daa946 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 25 Jul 2018 09:18:12 -0700 Subject: [PATCH 018/181] Remove sharedb-mingo-memory circular dependency --- .travis.yml | 2 +- package.json | 3 +- test/client/query-subscribe.js | 3 +- test/db-memory.js | 75 ++++++++++++++++++++++++++++++---- 4 files changed, 71 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 736e5fe78..21efafe46 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 jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/package.json b/package.json index 4e38aba84..10acc3bfe 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,7 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", - "mocha": "^5.2.0", - "sharedb-mingo-memory": "^1.0.0-beta" + "mocha": "^5.2.0" }, "scripts": { "test": "./node_modules/.bin/mocha && npm run jshint", diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index c63d4833a..108fe0de5 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -413,14 +413,13 @@ describe('client query subscribe', function() { it('changing a sorted property moves in 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 dbQuery = getQuery({query: matchAllDbQuery, sort: [['age', 1]]}); + var dbQuery = getQuery({query: {}, sort: [['age', 1]]}); var query = connection.createSubscribeQuery( 'dogs', dbQuery, diff --git a/test/db-memory.js b/test/db-memory.js index 9185dd507..ccfd2938b 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,76 @@ 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, options) { + 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(); + var db = new BasicQueryableMemoryDB(); callback(null, db); }, - getQuery: getQuery + getQuery: function(options) { + return {filter: options.query, sort: options.sort}; + } }); From 08f3339155fb145e4da4bcba74a4b0f37bf49f83 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 1 Aug 2018 15:25:58 -0700 Subject: [PATCH 019/181] 1.0.0-beta.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 10acc3bfe..4a38e5a65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.9", + "version": "1.0.0-beta.10", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From f691f481c1c5504d34a778f7f3d877c780a7a8e2 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Fri, 29 Jun 2018 17:54:27 +0100 Subject: [PATCH 020/181] Add support for fetching a particular version of a snapshot This change follows on from discussion in [this issue][1]. Its primary aim is to allow clients to fetch an historical version of a document (a snapshot), by providing either: - a version `number`, or - a desired `Date` The entry-point for this feature is added to the `Connection` class, deliberately separate from the `Doc`, because `Doc` is concerned with "live" document actions such as subscribing and submitting ops, whereas fetching an historical version of a document should not be associated with these ideas. The feature is called with: ```javascript connection.getSnapshot(collection, id, version, callback): void; ``` The details of the interface are detailed in the README, and in the code documentation. This change includes support for projections, and use of the `readSnapshots` middleware. It also hooks into `Connection`'s `hasPending` method. Performance optimisations are deemed out-of-scope for this change (see the [issue][1] for more details). Note that this change also adds a development dependency on [`lolex`][2] which is used for mocking the time. [1]: https://github.com/share/sharedb/issues/218 [2]: https://github.com/sinonjs/lolex --- .vscode/launch.json | 31 +++ README.md | 24 +++ lib/agent.js | 6 + lib/backend.js | 80 +++++++- lib/client/connection.js | 56 +++++- lib/client/snapshot-request.js | 78 ++++++++ package.json | 1 + test/client/snapshot-request.js | 333 ++++++++++++++++++++++++++++++++ 8 files changed, 606 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 lib/client/snapshot-request.js create mode 100644 test/client/snapshot-request.js diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..88e2f008d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,31 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "program": "${workspaceFolder}/lib/index.js" + }, + { + "name": "Unit tests", + "type": "node", + "request": "launch", + "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", + "stopOnEntry": false, + "args": [ + "--no-timeouts", + "--colors", + ], + "cwd": "${workspaceRoot}", + "runtimeExecutable": null, + "env": { + "NODE_ENV": "test" + }, + "sourceMaps": true + }, + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 2c8985db5..b9cf43534 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,29 @@ changes. Returns a [`ShareDB.Query`](#class-sharedbquery) instance. * `options.*` All other options are passed through to the database adapter. +`connection.getSnapshot(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 | Date)_ + Either the desired snapshot version number, or a `Date` object for the time at which you want the snapshot +* `callback` _(Function)_ + Called with `(error, snapshot)`, where `snapshot` takes the following form: + + ```javascript + { + collection: string; // collection name of the snapshot + id: string; // ID of the snapshot + version: number; // version number of the snapshot + timestamp: number; // the UNIX timestamp of the snapshot + deleted: boolean; // true if the returned version is a deleted snapshot + data: any; // the snapshot + } + ``` + ### Class: `ShareDB.Doc` `doc.type` _(String_) @@ -375,6 +398,7 @@ 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 - Invalid version ### 5000 - Internal error diff --git a/lib/agent.js b/lib/agent.js index d1a944de4..9a12308f7 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -300,6 +300,8 @@ 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 'sv': + return this._getSnapshot(request.c, request.d, request.v, request.ts, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); } @@ -582,3 +584,7 @@ Agent.prototype._createOp = function(request) { return new DeleteOp(src, request.seq, request.v, request.del); } }; + +Agent.prototype._getSnapshot = function (collection, id, version, timestamp, callback) { + this.backend.getSnapshot(this, collection, id, version, timestamp, callback); +}; diff --git a/lib/backend.js b/lib/backend.js index 7b29d9104..2a69c8506 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -9,6 +9,8 @@ var projections = require('./projections'); var QueryEmitter = require('./query-emitter'); var StreamSocket = require('./stream-socket'); var SubmitRequest = require('./submit-request'); +var types = require('./types'); + var warnDeprecatedDoc = true; var warnDeprecatedAfterSubmit = true; @@ -284,9 +286,13 @@ Backend.prototype._getSnapshotsFromMap = function(ids, snapshotMap) { return snapshots; }; +Backend.prototype.getOps = function(agent, index, id, from, to, callback) { + return this._getOps(agent, index, id, from, to, null, callback); +}; + // 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) { var start = Date.now(); var projection = this.projections[index]; var collection = (projection) ? projection.target : index; @@ -299,7 +305,7 @@ 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) { + backend.db.getOps(collection, id, from, to, options, function(err, ops) { if (err) return callback(err); backend._sanitizeOps(agent, projection, collection, id, ops, function(err) { if (err) return callback(err); @@ -580,6 +586,76 @@ Backend.prototype.getChannels = function(collection, id) { ]; }; +Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, callback) { + var backend = this; + this._getSnapshot(agent, index, id, version, timestamp, function (error, snapshot) { + if (error) return callback(error); + + var request = { + collection: index, + id: id, + version: snapshot.version, + timestamp: snapshot.timestamp, + snapshots: snapshot.data ? [snapshot.data] : [], + deleted: snapshot.deleted + }; + + backend.trigger(backend.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, function (error) { + if (error) return callback(error); + callback(null, { + data: request.snapshots[0], + version: request.version, + timestamp: request.timestamp, + deleted: request.deleted + }); + }); + }); +}; + +Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { + version = typeof version === 'number' ? version + 1 : null; + var options = { metadata: true }; + + this._getOps(agent, index, id, 0, version, options, function (error, ops) { + if (error) return callback(error); + + var type; + var snapshot; + var deleted; + var op; + + for (var index = 0; index < ops.length; index++) { + op = ops[index]; + + if (timestamp && op.m.ts > timestamp) { + op = ops[index - 1]; + break; + } + + if (op.create) { + type = types.map[op.create.type]; + if (!type) return callback({ code: 4008, message: 'Unknown type' }); + snapshot = op.create.data; + deleted = false; + } else if (op.del) { + snapshot = undefined; + deleted = true; + } else { + snapshot = type.apply(snapshot, op.op); + } + } + + if (!snapshot && !deleted) return callback({ code: 4015, message: 'Document does not exist' }); + + callback(null, { + data: snapshot, + timestamp: op.m && op.m.ts, + version: op.v, + deleted: deleted + }); + }); +}; + function pluckIds(snapshots) { var ids = []; for (var i = 0; i < snapshots.length; i++) { diff --git a/lib/client/connection.js b/lib/client/connection.js index f4cc298e6..c4fbc655d 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -1,5 +1,6 @@ var Doc = require('./doc'); var Query = require('./query'); +var SnapshotRequest = require('./snapshot-request'); var emitter = require('../emitter'); var ShareDBError = require('../error'); var types = require('../types'); @@ -40,6 +41,9 @@ function Connection(socket) { // 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; @@ -226,6 +230,9 @@ Connection.prototype.handleMessage = function(message) { case 'bu': return this._handleBulkMessage(message, '_handleUnsubscribe'); + case 'sv': + return this._handleSnapshot(err, message); + case 'f': var doc = this.getExisting(message.c, message.d); if (doc) doc._handleFetch(err, message.data); @@ -310,6 +317,11 @@ Connection.prototype._setState = function(newState, reason) { docs[id]._onConnectionStateChanged(); } } + // Emit the even to all snapshots + for (var id in this.snapshotRequests) { + var snapshotRequest = this.snapshotRequests[id]; + snapshotRequest._onConnectionStateChanged(); + } this.endBulk(); this.emit(newState, reason); @@ -523,7 +535,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(hasPending) ); }; function hasPending(object) { @@ -584,3 +597,44 @@ Connection.prototype._firstQuery = function(fn) { } } }; + +Connection.prototype._firstSnapshotRequest = function (fn) { + for (var id in this.snapshotRequests) { + var snapshotRequest = this.snapshotRequests[id]; + if (fn(snapshotRequest)) { + return snapshotRequest; + } + } +}; + +/** + * Get a read-only snapshot at a given version or time + * + * @param collection - the collection name of the snapshot + * @param id - the ID of the snapshot + * @param version - the version number, or Date of the snapshot to fetch. If an exact version or Date match is not made, + * then the next lowest version is returned. ie if a document has 6 versions, asking for v7 will return v6. If ops + * were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will return the 02:00 version. + * @param callback - (error, snapshot) => void, where snapshot takes the following schema: + * + * { + * collection: string; // collection name of the snapshot + * id: string; // ID of the snapshot + * version: number; // version number of the snapshot + * timestamp: number; // the UNIX timestamp of the snapshot + * deleted: boolean; // true if the returned version is a deleted snapshot + * data: any; // the snapshot + * } + * + */ +Connection.prototype.getSnapshot = function(collection, id, version, callback) { + var snapshotRequest = new SnapshotRequest(this, collection, id, version, callback); + this.snapshotRequests[snapshotRequest.requestId] = snapshotRequest; + snapshotRequest.send(); +}; + +Connection.prototype._handleSnapshot = function (error, message) { + var snapshotRequest = this.snapshotRequests[message.id]; + if (!snapshotRequest) return; + snapshotRequest._handleResponse(error, message); +}; diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js new file mode 100644 index 000000000..7a3c05310 --- /dev/null +++ b/lib/client/snapshot-request.js @@ -0,0 +1,78 @@ +var hat = require('hat'); + +module.exports = SnapshotRequest; + +function SnapshotRequest(connection, collection, id, version, callback) { + this.requestId = hat(); + + this.connection = connection; + this.id = id; + this.collection = collection; + this.callback = callback; + + this.parseVersion(version); + + this.ready = false; + this.sent = false; +} + +SnapshotRequest.prototype.parseVersion = function (version) { + if (typeof version === 'number') { + this.version = version; + } else if (version instanceof Date) { + this.timestamp = version.getTime(); + } else if (!version) { + this.version = null; + } else { + this.callback({ code: 4024, message: 'Invalid version' }); + } +} + +SnapshotRequest.prototype.hasPending = function () { + return !this.ready; +}; + +SnapshotRequest.prototype.send = function () { + if (!this.connection.canSend) { + return; + } + + var message = { + a: 'sv', + id: this.requestId, + c: this.collection, + d: this.id, + v: this.version, + ts: this.timestamp + }; + + this.connection.send(message); + this.sent = true; +}; + +SnapshotRequest.prototype._onConnectionStateChanged = function () { + if (this.connection.canSend && !this.sent) { + this.send(); + } +}; + +SnapshotRequest.prototype._handleResponse = function (error, message) { + this.ready = true; + + if (!this.callback) { + return; + } + + if (error) { + return this.callback(error); + } + + this.callback(null, { + id: this.id, + collection: this.collection, + version: message.version, + data: message.data, + timestamp: message.timestamp, + deleted: message.deleted + }); +}; diff --git a/package.json b/package.json index 4a38e5a65..fa080f75f 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", + "lolex": "^2.7.0", "mocha": "^5.2.0" }, "scripts": { diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js new file mode 100644 index 000000000..c1d22e7bf --- /dev/null +++ b/test/client/snapshot-request.js @@ -0,0 +1,333 @@ +var Backend = require('../../lib/backend'); +var SnapshotRequest = require('../../lib/client/snapshot-request'); +var expect = require('expect.js'); +var lolex = require("lolex"); + +describe('SnapshotRequest', function () { + var backend; + var clock; + + var DAY0 = new Date("2018-05-30"); + var DAY1 = new Date("2018-06-01"); + var DAY2 = new Date("2018-06-02"); + var DAY3 = new Date("2018-06-03"); + var DAY4 = new Date("2018-06-04"); + 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 a day apart', function () { + var v0 = { + id: 'don-quixote', + collection: 'books', + version: 0, + timestamp: DAY1.getTime(), + deleted: false, + data: { + title: 'Don Quixote' + } + }; + + var v1 = { + id: 'don-quixote', + collection: 'books', + version: 1, + timestamp: DAY2.getTime(), + deleted: false, + data: { + title: 'Don Quixote', + author: 'Miguel de Cervante' + } + }; + + var v2 = { + id: 'don-quixote', + collection: 'books', + version: 2, + timestamp: DAY3.getTime(), + deleted: false, + data: { + title: 'Don Quixote', + author: 'Miguel de Cervantes' + } + }; + + beforeEach(function (done) { + var doc = backend.connect().get('books', 'don-quixote'); + doc.create({ title: 'Don Quixote' }, function (error) { + if (error) done(error); + clock.tick(ONE_DAY); + doc.submitOp({ p: ['author'], oi: 'Miguel de Cervante' }, function (error) { + if (error) done(error); + clock.tick(ONE_DAY); + doc.submitOp({ p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes' }, done); + }); + }); + }); + + it('fetches v0', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v0); + done(); + }); + }); + + it('fetches v1', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 1, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v1); + done(); + }); + }); + + it('fetches v2', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v2); + done(); + }); + }); + + it('fetches the version from Day 1', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', DAY1, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v0); + done(); + }); + }); + + it('fetches the version from Day 2', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', DAY2, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v1); + done(); + }); + }); + + it('fetches the version from Day 3', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', DAY3, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v2); + done(); + }); + }); + + it('fetches the latest version if the version is undefined', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v2); + done(); + }); + }); + + it('errors if the version is -1', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { + expect(error.code).to.be(4015); + expect(snapshot).to.be(undefined); + done(); + }); + }); + + it('returns the latest version of the document if asking for a later version', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v2); + done(); + }); + }); + + it('errors if trying to fetch a snapshot before the document existed', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', DAY0, function (error, snapshot) { + expect(error.code).to.be(4015); + expect(snapshot).to.be(undefined); + done(); + }); + }); + + it('fetches the latest version if asking for a time after the last op', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', DAY4, function (error, snapshot) { + if (error) done(error); + expect(snapshot).to.eql(v2); + done(); + }); + }); + + it('errors if trying to fetch a non-existent document', function (done) { + backend.connect().getSnapshot('books', 'does-not-exist', 0, function (error, snapshot) { + expect(error.code).to.be(4015); + expect(snapshot).to.be(undefined); + done(); + }); + }); + + it('starts pending, and finishes not pending', function (done) { + var connection = backend.connect(); + + connection.getSnapshot('books', 'don-quixote', null, function (error, snapshot) { + expect(connection.hasPending()).to.be(false); + done(); + }); + + expect(connection.hasPending()).to.be(true); + }); + + describe('readSnapshots middleware', function (done) { + it('triggers the middleware', function (done) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, + function (request) { + expect(request.collection).to.be('books'); + expect(request.id).to.be('don-quixote'); + expect(request.version).to.be(2); + expect(request.timestamp).to.be(DAY3.getTime()); + expect(request.snapshots).to.eql([v2.data]); + expect(request.deleted).to.be(false); + + done(); + } + ); + + backend.connect().getSnapshot('books', 'don-quixote'); + }); + + it('can have its snapshot manipulated in the middleware', function (done) { + backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ + function (request, callback) { + request.snapshots[0].title = 'Alice in Wonderland'; + callback(); + }, + ]; + + backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + if (error) 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().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + 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().getSnapshot('bookTitles', 'don-quixote', 2, function (error, snapshot) { + if (error) 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) done(error); + clock.tick(ONE_DAY); + doc.del(function (error) { + done(error); + }); + }); + }); + + it('returns a deleted flag', function (done) { + backend.connect().getSnapshot('books', 'catch-22', null, function (error, snapshot) { + expect(snapshot).to.eql({ + id: 'catch-22', + collection: 'books', + version: 1, + timestamp: DAY2.getTime(), + deleted: true, + data: undefined + }); + + done(); + }); + }); + + it('fetches v0', function (done) { + backend.connect().getSnapshot('books', 'catch-22', 0, function (error, snapshot) { + if (error) done(error); + + expect(snapshot).to.eql({ + id: 'catch-22', + collection: 'books', + version: 0, + timestamp: DAY1.getTime(), + deleted: false, + data: { + title: 'Catch 22', + } + }); + + 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) done(error); + clock.tick(ONE_DAY); + doc.del(function (error) { + if (error) done (error); + clock.tick(ONE_DAY); + 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().getSnapshot('books', 'hitchhikers-guide', null, function (error, snapshot) { + if (error) done(error); + + expect(snapshot).to.eql({ + id: 'hitchhikers-guide', + collection: 'books', + version: 2, + timestamp: DAY3.getTime(), + deleted: false, + data: { + title: 'The Restaurant at the End of the Universe', + } + }); + + done(); + }); + }); + }); +}); From d29b26dbfc7a0a060111be35efe10bc6c826f21a Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 3 Jul 2018 15:30:05 +0100 Subject: [PATCH 021/181] Only call `done` once --- test/client/snapshot-request.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index c1d22e7bf..c3106f9cf 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -211,7 +211,7 @@ describe('SnapshotRequest', function () { ]; backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot.data.title).to.be('Alice in Wonderland'); done(); }); @@ -238,7 +238,7 @@ describe('SnapshotRequest', function () { it('applies the projection to a snapshot', function (done) { backend.connect().getSnapshot('bookTitles', 'don-quixote', 2, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot.data.title).to.be('Don Quixote'); expect(snapshot.data.author).to.be(undefined); @@ -252,7 +252,7 @@ describe('SnapshotRequest', function () { beforeEach(function (done) { var doc = backend.connect().get('books', 'catch-22'); doc.create({ title: 'Catch 22' }, function (error) { - if (error) done(error); + if (error) return done(error); clock.tick(ONE_DAY); doc.del(function (error) { done(error); @@ -302,7 +302,7 @@ describe('SnapshotRequest', function () { if (error) done(error); clock.tick(ONE_DAY); doc.del(function (error) { - if (error) done (error); + if (error) return done (error); clock.tick(ONE_DAY); doc.create({ title: 'The Restaurant at the End of the Universe' }, function (error) { done(error); From b7b3abe4fdfcb3de78d5cfd904d8a5159ad03341 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 4 Jul 2018 12:26:52 +0100 Subject: [PATCH 022/181] Correctly parse epoch timestamp when getting a snapshot --- lib/backend.js | 2 +- test/client/snapshot-request.js | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/backend.js b/lib/backend.js index 2a69c8506..f1d56a7b0 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -627,7 +627,7 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, for (var index = 0; index < ops.length; index++) { op = ops[index]; - if (timestamp && op.m.ts > timestamp) { + if (typeof timestamp === 'number' && op.m.ts > timestamp) { op = ops[index - 1]; break; } diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index c3106f9cf..e467f4a72 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -157,6 +157,14 @@ describe('SnapshotRequest', function () { }); }); + it('errors if trying to fetch a snapshot at the epoch', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', new Date(0), function (error, snapshot) { + expect(error.code).to.be(4015); + expect(snapshot).to.be(undefined); + done(); + }); + }); + it('fetches the latest version if asking for a time after the last op', function (done) { backend.connect().getSnapshot('books', 'don-quixote', DAY4, function (error, snapshot) { if (error) done(error); From 0f2235504275b0ab2aaddb0bcf59863e12cfcb74 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 13:50:21 +0100 Subject: [PATCH 023/181] Replace Snapshot `deleted` flag with `type` In order to remain consistent with `Doc`, this change replaces the snapshot returned by `getSnapshot` with the `type` field, which will be `null` if the document does not exist, or was deleted (for the requested version only) --- README.md | 2 +- lib/backend.js | 16 +++-- lib/client/connection.js | 2 +- lib/client/snapshot-request.js | 2 +- test/client/snapshot-request.js | 103 +++++++++++++++++++++----------- 5 files changed, 78 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index b9cf43534..84b296e4e 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ Get a read-only snapshot of a document at the requested version. id: string; // ID of the snapshot version: number; // version number of the snapshot timestamp: number; // the UNIX timestamp of the snapshot - deleted: boolean; // true if the returned version is a deleted snapshot + type: any; // the OT type of the snapshot, or null if it doesn't exist or is deleted data: any; // the snapshot } ``` diff --git a/lib/backend.js b/lib/backend.js index f1d56a7b0..337fd798a 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -597,7 +597,7 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c version: snapshot.version, timestamp: snapshot.timestamp, snapshots: snapshot.data ? [snapshot.data] : [], - deleted: snapshot.deleted + type: snapshot.type }; backend.trigger(backend.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, function (error) { @@ -606,7 +606,7 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c data: request.snapshots[0], version: request.version, timestamp: request.timestamp, - deleted: request.deleted + type: request.type }); }); }); @@ -621,7 +621,6 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, var type; var snapshot; - var deleted; var op; for (var index = 0; index < ops.length; index++) { @@ -636,22 +635,21 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, type = types.map[op.create.type]; if (!type) return callback({ code: 4008, message: 'Unknown type' }); snapshot = op.create.data; - deleted = false; } else if (op.del) { snapshot = undefined; - deleted = true; + type = null; } else { snapshot = type.apply(snapshot, op.op); } } - if (!snapshot && !deleted) return callback({ code: 4015, message: 'Document does not exist' }); + type = type ? { name: type.name, uri: type.uri} : null; callback(null, { data: snapshot, - timestamp: op.m && op.m.ts, - version: op.v, - deleted: deleted + timestamp: op && op.m && op.m.ts, + version: op && op.v, + type: type }); }); }; diff --git a/lib/client/connection.js b/lib/client/connection.js index c4fbc655d..60eed2ca1 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -622,7 +622,7 @@ Connection.prototype._firstSnapshotRequest = function (fn) { * id: string; // ID of the snapshot * version: number; // version number of the snapshot * timestamp: number; // the UNIX timestamp of the snapshot - * deleted: boolean; // true if the returned version is a deleted snapshot + * type: any; // the OT type of the snapshot, or null if it doesn't exist or is deleted * data: any; // the snapshot * } * diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 7a3c05310..6c1172674 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -73,6 +73,6 @@ SnapshotRequest.prototype._handleResponse = function (error, message) { version: message.version, data: message.data, timestamp: message.timestamp, - deleted: message.deleted + type: message.type }); }; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index e467f4a72..a81c821c2 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -1,5 +1,4 @@ var Backend = require('../../lib/backend'); -var SnapshotRequest = require('../../lib/client/snapshot-request'); var expect = require('expect.js'); var lolex = require("lolex"); @@ -29,12 +28,24 @@ describe('SnapshotRequest', function () { }); describe('a document with some simple versions a day apart', function () { + var emptySnapshot = { + id: 'don-quixote', + collection: 'books', + version: undefined, + timestamp: undefined, + type: null, + data: undefined + }; + var v0 = { id: 'don-quixote', collection: 'books', version: 0, timestamp: DAY1.getTime(), - deleted: false, + type: { + name: 'json0', + uri: 'http://sharejs.org/types/JSONv0' + }, data: { title: 'Don Quixote' } @@ -45,7 +56,10 @@ describe('SnapshotRequest', function () { collection: 'books', version: 1, timestamp: DAY2.getTime(), - deleted: false, + type: { + name: 'json0', + uri: 'http://sharejs.org/types/JSONv0' + }, data: { title: 'Don Quixote', author: 'Miguel de Cervante' @@ -57,7 +71,10 @@ describe('SnapshotRequest', function () { collection: 'books', version: 2, timestamp: DAY3.getTime(), - deleted: false, + type: { + name: 'json0', + uri: 'http://sharejs.org/types/JSONv0' + }, data: { title: 'Don Quixote', author: 'Miguel de Cervantes' @@ -67,10 +84,10 @@ describe('SnapshotRequest', function () { beforeEach(function (done) { var doc = backend.connect().get('books', 'don-quixote'); doc.create({ title: 'Don Quixote' }, function (error) { - if (error) done(error); + if (error) return done(error); clock.tick(ONE_DAY); doc.submitOp({ p: ['author'], oi: 'Miguel de Cervante' }, function (error) { - if (error) done(error); + if (error) return done(error); clock.tick(ONE_DAY); doc.submitOp({ p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes' }, done); }); @@ -79,7 +96,7 @@ describe('SnapshotRequest', function () { it('fetches v0', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v0); done(); }); @@ -87,7 +104,7 @@ describe('SnapshotRequest', function () { it('fetches v1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 1, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v1); done(); }); @@ -95,7 +112,7 @@ describe('SnapshotRequest', function () { it('fetches v2', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v2); done(); }); @@ -103,7 +120,7 @@ describe('SnapshotRequest', function () { it('fetches the version from Day 1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', DAY1, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v0); done(); }); @@ -111,7 +128,7 @@ describe('SnapshotRequest', function () { it('fetches the version from Day 2', function (done) { backend.connect().getSnapshot('books', 'don-quixote', DAY2, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v1); done(); }); @@ -119,7 +136,7 @@ describe('SnapshotRequest', function () { it('fetches the version from Day 3', function (done) { backend.connect().getSnapshot('books', 'don-quixote', DAY3, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v2); done(); }); @@ -127,56 +144,63 @@ describe('SnapshotRequest', function () { it('fetches the latest version if the version is undefined', function (done) { backend.connect().getSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v2); done(); }); }); - it('errors if the version is -1', function (done) { + it('returns an empty snapshot if the version is -1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { - expect(error.code).to.be(4015); - expect(snapshot).to.be(undefined); + if (error) return done(error); + expect(snapshot).to.eql(emptySnapshot); done(); }); }); it('returns the latest version of the document if asking for a later version', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v2); done(); }); }); - it('errors if trying to fetch a snapshot before the document existed', function (done) { + it('returns an empty snapshot when trying to fetch a snapshot before the document existed', function (done) { backend.connect().getSnapshot('books', 'don-quixote', DAY0, function (error, snapshot) { - expect(error.code).to.be(4015); - expect(snapshot).to.be(undefined); + if (error) return done(error); + expect(snapshot).to.eql(emptySnapshot); done(); }); }); - it('errors if trying to fetch a snapshot at the epoch', function (done) { + it('returns an empty snapshot if trying to fetch a snapshot at the epoch', function (done) { backend.connect().getSnapshot('books', 'don-quixote', new Date(0), function (error, snapshot) { - expect(error.code).to.be(4015); - expect(snapshot).to.be(undefined); + if (error) return done(error); + expect(snapshot).to.eql(emptySnapshot); done(); }); }); it('fetches the latest version if asking for a time after the last op', function (done) { backend.connect().getSnapshot('books', 'don-quixote', DAY4, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql(v2); done(); }); }); - it('errors if trying to fetch a non-existent document', function (done) { + it('returns an empty snapshot if trying to fetch a non-existent document', function (done) { backend.connect().getSnapshot('books', 'does-not-exist', 0, function (error, snapshot) { - expect(error.code).to.be(4015); - expect(snapshot).to.be(undefined); + if (error) return done(error); + expect(snapshot).to.eql({ + id: 'does-not-exist', + collection: 'books', + version: undefined, + timestamp: undefined, + type: null, + data: undefined + }); done(); }); }); @@ -201,7 +225,10 @@ describe('SnapshotRequest', function () { expect(request.version).to.be(2); expect(request.timestamp).to.be(DAY3.getTime()); expect(request.snapshots).to.eql([v2.data]); - expect(request.deleted).to.be(false); + expect(request.type).to.eql({ + name: 'json0', + uri: 'http://sharejs.org/types/JSONv0' + }); done(); } @@ -268,14 +295,14 @@ describe('SnapshotRequest', function () { }); }); - it('returns a deleted flag', function (done) { + it('returns a null type', function (done) { backend.connect().getSnapshot('books', 'catch-22', null, function (error, snapshot) { expect(snapshot).to.eql({ id: 'catch-22', collection: 'books', version: 1, timestamp: DAY2.getTime(), - deleted: true, + type: null, data: undefined }); @@ -285,14 +312,17 @@ describe('SnapshotRequest', function () { it('fetches v0', function (done) { backend.connect().getSnapshot('books', 'catch-22', 0, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql({ id: 'catch-22', collection: 'books', version: 0, timestamp: DAY1.getTime(), - deleted: false, + type: { + name: 'json0', + uri: 'http://sharejs.org/types/JSONv0' + }, data: { title: 'Catch 22', } @@ -307,7 +337,7 @@ describe('SnapshotRequest', 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) done(error); + if (error) return done(error); clock.tick(ONE_DAY); doc.del(function (error) { if (error) return done (error); @@ -321,14 +351,17 @@ describe('SnapshotRequest', function () { it('fetches the latest version of the document', function (done) { backend.connect().getSnapshot('books', 'hitchhikers-guide', null, function (error, snapshot) { - if (error) done(error); + if (error) return done(error); expect(snapshot).to.eql({ id: 'hitchhikers-guide', collection: 'books', version: 2, timestamp: DAY3.getTime(), - deleted: false, + type: { + name: 'json0', + uri: 'http://sharejs.org/types/JSONv0' + }, data: { title: 'The Restaurant at the End of the Universe', } From cf9bd25d5d768819c08802ae49d59f1b1fa66d47 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 13:51:23 +0100 Subject: [PATCH 024/181] Change snapshot version (sv) action to snapshot fetch (sf) --- lib/agent.js | 2 +- lib/client/connection.js | 2 +- lib/client/snapshot-request.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 9a12308f7..67e381400 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -300,7 +300,7 @@ 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 'sv': + case 'sf': return this._getSnapshot(request.c, request.d, request.v, request.ts, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); diff --git a/lib/client/connection.js b/lib/client/connection.js index 60eed2ca1..bebdb77a5 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -230,7 +230,7 @@ Connection.prototype.handleMessage = function(message) { case 'bu': return this._handleBulkMessage(message, '_handleUnsubscribe'); - case 'sv': + case 'sf': return this._handleSnapshot(err, message); case 'f': diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 6c1172674..a4f189eda 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -38,7 +38,7 @@ SnapshotRequest.prototype.send = function () { } var message = { - a: 'sv', + a: 'sf', id: this.requestId, c: this.collection, d: this.id, From 7cfc1343610330c5b3bf76970029911a0fbf7b94 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 13:52:03 +0100 Subject: [PATCH 025/181] Create snapshots with `type.create` --- lib/backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backend.js b/lib/backend.js index 337fd798a..27de37f22 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -634,7 +634,7 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, if (op.create) { type = types.map[op.create.type]; if (!type) return callback({ code: 4008, message: 'Unknown type' }); - snapshot = op.create.data; + snapshot = type.create(op.create.data); } else if (op.del) { snapshot = undefined; type = null; From c94ae65da1a1d2638e64430711c25027b305b9ff Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 13:57:03 +0100 Subject: [PATCH 026/181] Keep snapshot fetching op local to loop --- lib/backend.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 27de37f22..df912eff7 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -621,13 +621,18 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, var type; var snapshot; - var op; + var fetchedTimestamp; + var fetchedVersion; for (var index = 0; index < ops.length; index++) { - op = ops[index]; - - if (typeof timestamp === 'number' && op.m.ts > timestamp) { - op = ops[index - 1]; + var op = ops[index]; + fetchedTimestamp = op.m.ts; + fetchedVersion = op.v; + + if (typeof timestamp === 'number' && fetchedTimestamp > timestamp) { + var previousOp = ops[index - 1]; + fetchedTimestamp = previousOp ? previousOp.m.ts : undefined; + fetchedVersion = previousOp ? previousOp.v : undefined; break; } @@ -647,8 +652,8 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback(null, { data: snapshot, - timestamp: op && op.m && op.m.ts, - version: op && op.v, + timestamp: fetchedTimestamp, + version: fetchedVersion, type: type }); }); From c9141b0e254cc8bca1f65e0c44f3a012c51f68b3 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 14:02:41 +0100 Subject: [PATCH 027/181] Make `getSnapshot` version an optional parameter --- README.md | 2 +- lib/client/connection.js | 11 ++++++++--- lib/client/snapshot-request.js | 2 +- test/client/snapshot-request.js | 8 ++++++++ 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 84b296e4e..daf96650e 100644 --- a/README.md +++ b/README.md @@ -234,7 +234,7 @@ Get a read-only snapshot of a document at the requested version. Collection name of the snapshot * `id` _(String)_ ID of the snapshot -* `version` _(number | Date)_ +* `version` _(number | Date) [optional]_ Either the desired snapshot version number, or a `Date` object for the time at which you want the snapshot * `callback` _(Function)_ Called with `(error, snapshot)`, where `snapshot` takes the following form: diff --git a/lib/client/connection.js b/lib/client/connection.js index bebdb77a5..ce3e072ed 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -612,9 +612,9 @@ Connection.prototype._firstSnapshotRequest = function (fn) { * * @param collection - the collection name of the snapshot * @param id - the ID of the snapshot - * @param version - the version number, or Date of the snapshot to fetch. If an exact version or Date match is not made, - * then the next lowest version is returned. ie if a document has 6 versions, asking for v7 will return v6. If ops - * were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will return the 02:00 version. + * @param version (optional) - the version number, or Date of the snapshot to fetch. If an exact version or Date match is + * not made, then the next lower version is returned. ie if a document has 6 versions, asking for v7 will return v6. If + * ops were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will return the 02:00 version. * @param callback - (error, snapshot) => void, where snapshot takes the following schema: * * { @@ -628,6 +628,11 @@ Connection.prototype._firstSnapshotRequest = function (fn) { * */ Connection.prototype.getSnapshot = function(collection, id, version, callback) { + if (typeof version === 'function') { + callback = version; + version = null; + } + var snapshotRequest = new SnapshotRequest(this, collection, id, version, callback); this.snapshotRequests[snapshotRequest.requestId] = snapshotRequest; snapshotRequest.send(); diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index a4f189eda..e9b80dbc9 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -24,7 +24,7 @@ SnapshotRequest.prototype.parseVersion = function (version) { } else if (!version) { this.version = null; } else { - this.callback({ code: 4024, message: 'Invalid version' }); + this.callback && this.callback({ code: 4024, message: 'Invalid version' }); } } diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index a81c821c2..5534bf65f 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -150,6 +150,14 @@ describe('SnapshotRequest', function () { }); }); + it('fetches the latest version when the optional version is not provided', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v2); + done(); + }); + }); + it('returns an empty snapshot if the version is -1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { if (error) return done(error); From 5edd003a2d36d09d90fbf39f45c545565b2be042 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 14:05:29 +0100 Subject: [PATCH 028/181] Update `getSnapshot` falsiness check to avoid empty strings, etc. --- lib/client/snapshot-request.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index e9b80dbc9..468d1ae69 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -21,7 +21,7 @@ SnapshotRequest.prototype.parseVersion = function (version) { this.version = version; } else if (version instanceof Date) { this.timestamp = version.getTime(); - } else if (!version) { + } else if (version == null) { this.version = null; } else { this.callback && this.callback({ code: 4024, message: 'Invalid version' }); From 1c934a1f32c90bb1d8b4a2e0d0563d2d2d92ee2a Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 14:06:36 +0100 Subject: [PATCH 029/181] Store `getSnapshot` `typeof` check in a local variable --- lib/backend.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/backend.js b/lib/backend.js index df912eff7..e76b6ddff 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -615,6 +615,7 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { version = typeof version === 'number' ? version + 1 : null; var options = { metadata: true }; + var timestampIsNumber = typeof timestamp === 'number'; this._getOps(agent, index, id, 0, version, options, function (error, ops) { if (error) return callback(error); @@ -629,7 +630,7 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, fetchedTimestamp = op.m.ts; fetchedVersion = op.v; - if (typeof timestamp === 'number' && fetchedTimestamp > timestamp) { + if (timestampIsNumber && fetchedTimestamp > timestamp) { var previousOp = ops[index - 1]; fetchedTimestamp = previousOp ? previousOp.m.ts : undefined; fetchedVersion = previousOp ? previousOp.v : undefined; From d25ee6eb2dd67c5400c7c20c3cf9f06c722e84f2 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 15:10:04 +0100 Subject: [PATCH 030/181] Set SnapshotRequest `sent` flag to false if connection cannot send --- lib/client/connection.js | 2 +- lib/client/snapshot-request.js | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index ce3e072ed..be0c61e02 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -317,7 +317,7 @@ Connection.prototype._setState = function(newState, reason) { docs[id]._onConnectionStateChanged(); } } - // Emit the even to all snapshots + // Emit the event to all snapshots for (var id in this.snapshotRequests) { var snapshotRequest = this.snapshotRequests[id]; snapshotRequest._onConnectionStateChanged(); diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 468d1ae69..8fc1866b7 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -53,6 +53,8 @@ SnapshotRequest.prototype.send = function () { SnapshotRequest.prototype._onConnectionStateChanged = function () { if (this.connection.canSend && !this.sent) { this.send(); + } else if (!this.connection.canSend) { + this.sent = false; } }; From dc2d022fa0604fc074293023b829ab55abccf472 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 15:12:43 +0100 Subject: [PATCH 031/181] Check if snapshot version is finite --- lib/backend.js | 2 +- lib/client/snapshot-request.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index e76b6ddff..ad4004f2f 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -613,7 +613,7 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c }; Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { - version = typeof version === 'number' ? version + 1 : null; + version = (typeof version === 'number' && isFinite(version)) ? version + 1 : null; var options = { metadata: true }; var timestampIsNumber = typeof timestamp === 'number'; diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 8fc1866b7..8e3120ec1 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -17,7 +17,7 @@ function SnapshotRequest(connection, collection, id, version, callback) { } SnapshotRequest.prototype.parseVersion = function (version) { - if (typeof version === 'number') { + if (typeof version === 'number' && isFinite(version)) { this.version = version; } else if (version instanceof Date) { this.timestamp = version.getTime(); From 4a90170092d5b18ab2f13f7c1b21c7f2ada5cd34 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 10 Jul 2018 15:26:12 +0100 Subject: [PATCH 032/181] Error on getSnapshot with version -1 --- lib/client/snapshot-request.js | 2 +- test/client/snapshot-request.js | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 8e3120ec1..129919b71 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -17,7 +17,7 @@ function SnapshotRequest(connection, collection, id, version, callback) { } SnapshotRequest.prototype.parseVersion = function (version) { - if (typeof version === 'number' && isFinite(version)) { + if (typeof version === 'number' && isFinite(version) && version >= 0) { this.version = version; } else if (version instanceof Date) { this.timestamp = version.getTime(); diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 5534bf65f..d0909a476 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -158,10 +158,14 @@ describe('SnapshotRequest', function () { }); }); - it('returns an empty snapshot if the version is -1', function (done) { + it('can call without a callback', function () { + backend.connect().getSnapshot('books', 'don-quixote'); + }); + + it('errors if the version is -1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(emptySnapshot); + expect(error.code).to.be(4024); + expect(snapshot).to.be(undefined); done(); }); }); From 9640d736c8fd884b70e15c95daf6389aacd41ae9 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 11 Jul 2018 11:56:58 +0100 Subject: [PATCH 033/181] Review markups This change makes some tweaks to the `getSnapshot` feature after code review: - pass `type` around as just the URI, and resolve to the full object client-side - small tweak to variable declaration logic in `Backend._getSnapshot` - prevent memory leak of snapshot requests - update response for `-1` version fetch --- lib/backend.js | 12 ++++---- lib/client/connection.js | 1 + lib/client/snapshot-request.js | 5 ++-- test/client/snapshot-request.js | 53 +++++++++++++++++---------------- 4 files changed, 37 insertions(+), 34 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index ad4004f2f..e2f176bd8 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -627,16 +627,14 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, for (var index = 0; index < ops.length; index++) { var op = ops[index]; - fetchedTimestamp = op.m.ts; - fetchedVersion = op.v; - if (timestampIsNumber && fetchedTimestamp > timestamp) { - var previousOp = ops[index - 1]; - fetchedTimestamp = previousOp ? previousOp.m.ts : undefined; - fetchedVersion = previousOp ? previousOp.v : undefined; + if (timestampIsNumber && op.m.ts > timestamp) { break; } + fetchedTimestamp = op.m.ts; + fetchedVersion = op.v; + if (op.create) { type = types.map[op.create.type]; if (!type) return callback({ code: 4008, message: 'Unknown type' }); @@ -649,7 +647,7 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, } } - type = type ? { name: type.name, uri: type.uri} : null; + type = type ? type.uri : null; callback(null, { data: snapshot, diff --git a/lib/client/connection.js b/lib/client/connection.js index be0c61e02..49109fb48 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -641,5 +641,6 @@ Connection.prototype.getSnapshot = function(collection, id, version, callback) { Connection.prototype._handleSnapshot = 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/snapshot-request.js b/lib/client/snapshot-request.js index 129919b71..f4e3610c5 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -1,4 +1,5 @@ var hat = require('hat'); +var types = require('../types'); module.exports = SnapshotRequest; @@ -17,7 +18,7 @@ function SnapshotRequest(connection, collection, id, version, callback) { } SnapshotRequest.prototype.parseVersion = function (version) { - if (typeof version === 'number' && isFinite(version) && version >= 0) { + if (typeof version === 'number' && isFinite(version)) { this.version = version; } else if (version instanceof Date) { this.timestamp = version.getTime(); @@ -75,6 +76,6 @@ SnapshotRequest.prototype._handleResponse = function (error, message) { version: message.version, data: message.data, timestamp: message.timestamp, - type: message.type + type: types.map[message.type] || null }); }; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index d0909a476..46ce89b6e 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -1,10 +1,12 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); var lolex = require("lolex"); +var types = require('../../lib/types'); describe('SnapshotRequest', function () { var backend; var clock; + var json0 = types.map['json0']; var DAY0 = new Date("2018-05-30"); var DAY1 = new Date("2018-06-01"); @@ -42,10 +44,7 @@ describe('SnapshotRequest', function () { collection: 'books', version: 0, timestamp: DAY1.getTime(), - type: { - name: 'json0', - uri: 'http://sharejs.org/types/JSONv0' - }, + type: json0, data: { title: 'Don Quixote' } @@ -56,10 +55,7 @@ describe('SnapshotRequest', function () { collection: 'books', version: 1, timestamp: DAY2.getTime(), - type: { - name: 'json0', - uri: 'http://sharejs.org/types/JSONv0' - }, + type: json0, data: { title: 'Don Quixote', author: 'Miguel de Cervante' @@ -71,10 +67,7 @@ describe('SnapshotRequest', function () { collection: 'books', version: 2, timestamp: DAY3.getTime(), - type: { - name: 'json0', - uri: 'http://sharejs.org/types/JSONv0' - }, + type: json0, data: { title: 'Don Quixote', author: 'Miguel de Cervantes' @@ -162,8 +155,15 @@ describe('SnapshotRequest', function () { backend.connect().getSnapshot('books', 'don-quixote'); }); - it('errors if the version is -1', function (done) { + it('returns an empty snapshot if the version is -1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { + expect(snapshot).to.eql(emptySnapshot); + done(); + }); + }); + + it('errors if the version is a string', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 'foo', function (error, snapshot) { expect(error.code).to.be(4024); expect(snapshot).to.be(undefined); done(); @@ -228,6 +228,18 @@ describe('SnapshotRequest', function () { expect(connection.hasPending()).to.be(true); }); + it('deletes the request from the connection', function (done) { + var connection = backend.connect(); + + connection.getSnapshot('books', 'don-quixote', function (error) { + if (error) return done(error); + expect(connection.snapshotRequests).to.eql({}); + done(); + }); + + expect(connection.snapshotRequests).to.not.eql({}); + }); + describe('readSnapshots middleware', function (done) { it('triggers the middleware', function (done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, @@ -237,10 +249,7 @@ describe('SnapshotRequest', function () { expect(request.version).to.be(2); expect(request.timestamp).to.be(DAY3.getTime()); expect(request.snapshots).to.eql([v2.data]); - expect(request.type).to.eql({ - name: 'json0', - uri: 'http://sharejs.org/types/JSONv0' - }); + expect(request.type).to.be('http://sharejs.org/types/JSONv0'); done(); } @@ -331,10 +340,7 @@ describe('SnapshotRequest', function () { collection: 'books', version: 0, timestamp: DAY1.getTime(), - type: { - name: 'json0', - uri: 'http://sharejs.org/types/JSONv0' - }, + type: json0, data: { title: 'Catch 22', } @@ -370,10 +376,7 @@ describe('SnapshotRequest', function () { collection: 'books', version: 2, timestamp: DAY3.getTime(), - type: { - name: 'json0', - uri: 'http://sharejs.org/types/JSONv0' - }, + type: json0, data: { title: 'The Restaurant at the End of the Universe', } From fff922cd087c7e0dc1717ca76d8abb6433c89bc5 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 11 Jul 2018 17:07:45 +0100 Subject: [PATCH 034/181] Split `getSnapshot` into two methods This change splits `getSnapshot` apart into: - `getSnapshot` fetches by version number - `getSnapshotAtTime` fetches by timestamp It also updates the logic to error if requesting a version after the current version, or a timestamp after `Date.now()`, since either of these requests would not be idempotent. --- README.md | 17 ++- lib/backend.js | 9 ++ lib/client/connection.js | 45 +++++- lib/client/snapshot-request.js | 16 +- test/client/snapshot-request.js | 257 ++++++++++++++++++-------------- 5 files changed, 211 insertions(+), 133 deletions(-) diff --git a/README.md b/README.md index daf96650e..1f0bfce9f 100644 --- a/README.md +++ b/README.md @@ -234,8 +234,8 @@ Get a read-only snapshot of a document at the requested version. Collection name of the snapshot * `id` _(String)_ ID of the snapshot -* `version` _(number | Date) [optional]_ - Either the desired snapshot version number, or a `Date` object for the time at which you want the snapshot +* `version` _(number) [optional]_ + The version number of the desired snapshot * `callback` _(Function)_ Called with `(error, snapshot)`, where `snapshot` takes the following form: @@ -250,6 +250,18 @@ Get a read-only snapshot of a document at the requested version. } ``` +`connection.getSnapshotAtTime(collection, id, timestamp, callback): void;` +Get a read-only snapshot of a document at the requested timestamp. + +* `collection` _(String)_ + Collection name of the snapshot +* `id` _(String)_ + ID of the snapshot +* `timestamp` _(number) [optional]_ + The timestamp at which you wish to view the snapshot. If an exact timestamp match is not made, then the next lower version is returned. ie if ops were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will return the 02:00 version. +* `callback` _(Function)_ + Called with `(error, snapshot)`, where `snapshot` takes the same form as for `getSnapshot` above. + ### Class: `ShareDB.Doc` `doc.type` _(String_) @@ -399,6 +411,7 @@ Additional fields may be added to the error object for debugging context dependi * 4022 - Database adapter does not support queries * 4023 - Cannot project snapshots of this type * 4024 - Invalid version +* 4025 - Invalid timestamp ### 5000 - Internal error diff --git a/lib/backend.js b/lib/backend.js index e2f176bd8..f6a8354c6 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -613,10 +613,15 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c }; Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { + var requestedVersion = version; version = (typeof version === 'number' && isFinite(version)) ? version + 1 : null; var options = { metadata: true }; var timestampIsNumber = typeof timestamp === 'number'; + if (timestampIsNumber && timestamp > Date.now()) { + return callback({ code: 4025, message: 'Requested timestamp must be in the past' }); + } + this._getOps(agent, index, id, 0, version, options, function (error, ops) { if (error) return callback(error); @@ -649,6 +654,10 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, type = type ? type.uri : null; + if (requestedVersion > fetchedVersion) { + return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); + } + callback(null, { data: snapshot, timestamp: fetchedTimestamp, diff --git a/lib/client/connection.js b/lib/client/connection.js index 49109fb48..e219ba7bd 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -612,9 +612,7 @@ Connection.prototype._firstSnapshotRequest = function (fn) { * * @param collection - the collection name of the snapshot * @param id - the ID of the snapshot - * @param version (optional) - the version number, or Date of the snapshot to fetch. If an exact version or Date match is - * not made, then the next lower version is returned. ie if a document has 6 versions, asking for v7 will return v6. If - * ops were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will return the 02:00 version. + * @param version (optional) - the version number to fetch. If an exact version match is not made, then the next lower * @param callback - (error, snapshot) => void, where snapshot takes the following schema: * * { @@ -633,7 +631,38 @@ Connection.prototype.getSnapshot = function(collection, id, version, callback) { version = null; } - var snapshotRequest = new SnapshotRequest(this, collection, id, version, callback); + if (!this._isNumberOrNully(version)) { + return callback({ code: 4024, message: 'Invalid version' }); + } + + var snapshotRequest = new SnapshotRequest(this, collection, id, callback); + snapshotRequest.version = version; + this.snapshotRequests[snapshotRequest.requestId] = snapshotRequest; + snapshotRequest.send(); +}; + +/** + * Get a read-only snapshot at a given version or time + * + * @param collection - the collection name of the snapshot + * @param id - the ID of the snapshot + * @param version (optional) - the timestamp of the snapshot to fetch. If an exact timestamp match is not made, then the + * next lower version is returned. ie if ops were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will + * return the 02:00 version. + * @param callback - (error, snapshot) => void, where snapshot takes the same schema as getSnapshot (see above) + */ +Connection.prototype.getSnapshotAtTime = function (collection, id, timestamp, callback) { + if (typeof timestamp === 'function') { + callback = timestamp; + timestamp = null; + } + + if (!this._isNumberOrNully(timestamp)) { + return callback({ code: 4025, message: 'Invalid timestamp' }); + } + + var snapshotRequest = new SnapshotRequest(this, collection, id, callback); + snapshotRequest.timestamp = timestamp; this.snapshotRequests[snapshotRequest.requestId] = snapshotRequest; snapshotRequest.send(); }; @@ -644,3 +673,11 @@ Connection.prototype._handleSnapshot = function (error, message) { delete this.snapshotRequests[message.id]; snapshotRequest._handleResponse(error, message); }; + +Connection.prototype._isNumberOrNully = function (number) { + if (number == null) { + return true; + } + + return typeof number === 'number' && isFinite(number); +}; diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index f4e3610c5..211dec543 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -3,7 +3,7 @@ var types = require('../types'); module.exports = SnapshotRequest; -function SnapshotRequest(connection, collection, id, version, callback) { +function SnapshotRequest(connection, collection, id, callback) { this.requestId = hat(); this.connection = connection; @@ -11,24 +11,10 @@ function SnapshotRequest(connection, collection, id, version, callback) { this.collection = collection; this.callback = callback; - this.parseVersion(version); - this.ready = false; this.sent = false; } -SnapshotRequest.prototype.parseVersion = function (version) { - if (typeof version === 'number' && isFinite(version)) { - this.version = version; - } else if (version instanceof Date) { - this.timestamp = version.getTime(); - } else if (version == null) { - this.version = null; - } else { - this.callback && this.callback({ code: 4024, message: 'Invalid version' }); - } -} - SnapshotRequest.prototype.hasPending = function () { return !this.ready; }; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 46ce89b6e..f8d54a114 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -87,160 +87,193 @@ describe('SnapshotRequest', function () { }); }); - it('fetches v0', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v0); - done(); + describe('getSnapshot', () => { + it('fetches v0', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v0); + done(); + }); }); - }); - it('fetches v1', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 1, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v1); - done(); + it('fetches v1', function (done) { + backend.connect().getSnapshot('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().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v2); - done(); + it('fetches v2', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v2); + done(); + }); }); - }); - it('fetches the version from Day 1', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', DAY1, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v0); - done(); + it('fetches the latest version if the version is undefined', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v2); + done(); + }); }); - }); - it('fetches the version from Day 2', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', DAY2, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v1); - done(); + it('fetches the latest version when the optional version is not provided', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v2); + done(); + }); }); - }); - it('fetches the version from Day 3', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', DAY3, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v2); - done(); + it('can call without a callback', function () { + backend.connect().getSnapshot('books', 'don-quixote'); }); - }); - it('fetches the latest version if the version is undefined', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v2); - done(); + it('returns an empty snapshot if the version is -1', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(emptySnapshot); + done(); + }); }); - }); - it('fetches the latest version when the optional version is not provided', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v2); - done(); + it('errors if the version is a string', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 'foo', function (error, snapshot) { + expect(error.code).to.be(4024); + expect(snapshot).to.be(undefined); + done(); + }); }); - }); - it('can call without a callback', function () { - backend.connect().getSnapshot('books', 'don-quixote'); - }); + it('errors if asking for a version that does not exist', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { + expect(error.code).to.be(4024); + expect(snapshot).to.be(undefined); + done(); + }); + }); - it('returns an empty snapshot if the version is -1', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { - expect(snapshot).to.eql(emptySnapshot); - done(); + it('returns an empty snapshot if trying to fetch a non-existent document', function (done) { + backend.connect().getSnapshot('books', 'does-not-exist', 0, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql({ + id: 'does-not-exist', + collection: 'books', + version: undefined, + timestamp: undefined, + type: null, + data: undefined + }); + done(); + }); }); - }); - it('errors if the version is a string', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 'foo', function (error, snapshot) { - expect(error.code).to.be(4024); - expect(snapshot).to.be(undefined); - done(); + it('starts pending, and finishes not pending', function (done) { + var connection = backend.connect(); + + connection.getSnapshot('books', 'don-quixote', null, function (error, snapshot) { + expect(connection.hasPending()).to.be(false); + done(); + }); + + expect(connection.hasPending()).to.be(true); }); - }); - it('returns the latest version of the document if asking for a later version', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v2); - done(); + it('deletes the request from the connection', function (done) { + var connection = backend.connect(); + + connection.getSnapshot('books', 'don-quixote', function (error) { + if (error) return done(error); + expect(connection.snapshotRequests).to.eql({}); + done(); + }); + + expect(connection.snapshotRequests).to.not.eql({}); }); }); - it('returns an empty snapshot when trying to fetch a snapshot before the document existed', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', DAY0, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(emptySnapshot); - done(); + describe('getSnapshotByTimestamp', () => { + it('fetches the version from Day 1', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY1.getTime(), function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v0); + done(); + }); }); - }); - it('returns an empty snapshot if trying to fetch a snapshot at the epoch', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', new Date(0), function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(emptySnapshot); - done(); + it('fetches the version from Day 2', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY2.getTime(), function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v1); + done(); + }); }); - }); - it('fetches the latest version if asking for a time after the last op', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', DAY4, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v2); - done(); + it('fetches the version from Day 3', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY3.getTime(), function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v2); + done(); + }); }); - }); - it('returns an empty snapshot if trying to fetch a non-existent document', function (done) { - backend.connect().getSnapshot('books', 'does-not-exist', 0, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql({ - id: 'does-not-exist', - collection: 'books', - version: undefined, - timestamp: undefined, - type: null, - data: undefined + it('fetches the latest version if the timestamp is undefined', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', undefined, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v2); + done(); }); - done(); }); - }); - it('starts pending, and finishes not pending', function (done) { - var connection = backend.connect(); + it('fetches the latest version when the optional timestamp is not provided', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v2); + done(); + }); + }); - connection.getSnapshot('books', 'don-quixote', null, function (error, snapshot) { - expect(connection.hasPending()).to.be(false); - done(); + it('can call without a callback', function () { + backend.connect().getSnapshotAtTime('books', 'don-quixote'); }); - expect(connection.hasPending()).to.be(true); - }); + it('returns an empty snapshot when trying to fetch a snapshot before the document existed', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY0.getTime(), function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(emptySnapshot); + done(); + }); + }); - it('deletes the request from the connection', function (done) { - var connection = backend.connect(); + it('returns an empty snapshot when trying to fetch a snapshot at the epoch', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', 0, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(emptySnapshot); + done(); + }); + }); - connection.getSnapshot('books', 'don-quixote', function (error) { - if (error) return done(error); - expect(connection.snapshotRequests).to.eql({}); - done(); + it('errors if asking for a time after now', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY4.getTime(), function (error, snapshot) { + expect(error.code).to.be(4025); + expect(snapshot).to.be(undefined); + done(); + }); }); - expect(connection.snapshotRequests).to.not.eql({}); + it('errors if the timestamp is a string', function (done) { + backend.connect().getSnapshotAtTime('books', 'don-quixote', 'foo', function (error, snapshot) { + expect(error.code).to.be(4025); + expect(snapshot).to.be(undefined); + done(); + }); + }); }); - describe('readSnapshots middleware', function (done) { + describe('readSnapshots middleware', function () { it('triggers the middleware', function (done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request) { From b030e270e00d4c398ce896838ac36d317057b22f Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 12 Jul 2018 07:12:20 +0100 Subject: [PATCH 035/181] Initialise empty snapshots with version 0, timestamp 0 To stay consistent with the `Doc` class, this change updates the results of `getSnapshot` to always have `version: 0` and `timestamp: 0` if no snapshot was found (rather than having them `undefined`). --- lib/backend.js | 6 +++--- test/client/snapshot-request.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index f6a8354c6..e793f01cc 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -625,10 +625,10 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, this._getOps(agent, index, id, 0, version, options, function (error, ops) { if (error) return callback(error); - var type; + var type = null; var snapshot; - var fetchedTimestamp; - var fetchedVersion; + var fetchedTimestamp = 0; + var fetchedVersion = 0; for (var index = 0; index < ops.length; index++) { var op = ops[index]; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index f8d54a114..a9da49925 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -33,8 +33,8 @@ describe('SnapshotRequest', function () { var emptySnapshot = { id: 'don-quixote', collection: 'books', - version: undefined, - timestamp: undefined, + version: 0, + timestamp: 0, type: null, data: undefined }; @@ -162,8 +162,8 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'does-not-exist', collection: 'books', - version: undefined, - timestamp: undefined, + version: 0, + timestamp: 0, type: null, data: undefined }); From ce64e8a147351507e52ed6018b6a844318c3e7f7 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 12 Jul 2018 09:20:54 +0100 Subject: [PATCH 036/181] Remove pending and ready from Snapshot Request All `SnapshotRequest`s are pending, so we don't need these methods. Instead, to check for pending, we just check if a `SnapshotRequest` is queued. --- lib/client/connection.js | 5 ++++- lib/client/snapshot-request.js | 7 ------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index e219ba7bd..346546323 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -536,12 +536,15 @@ Connection.prototype.hasPending = function() { return !!( this._firstDoc(hasPending) || this._firstQuery(hasPending) || - this._firstSnapshotRequest(hasPending) + this._firstSnapshotRequest(exists) ); }; function hasPending(object) { return object.hasPending(); } +function exists(object) { + return !!object; +} Connection.prototype.hasWritePending = function() { return !!this._firstDoc(hasWritePending); diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 211dec543..9c9f08983 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -11,14 +11,9 @@ function SnapshotRequest(connection, collection, id, callback) { this.collection = collection; this.callback = callback; - this.ready = false; this.sent = false; } -SnapshotRequest.prototype.hasPending = function () { - return !this.ready; -}; - SnapshotRequest.prototype.send = function () { if (!this.connection.canSend) { return; @@ -46,8 +41,6 @@ SnapshotRequest.prototype._onConnectionStateChanged = function () { }; SnapshotRequest.prototype._handleResponse = function (error, message) { - this.ready = true; - if (!this.callback) { return; } From dd83d0711a36b95df41ea5bdc93aa51b3a869766 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 12 Jul 2018 09:22:38 +0100 Subject: [PATCH 037/181] Update getSnapshot JSDocs --- lib/client/connection.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index 346546323..0a405e347 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -611,11 +611,11 @@ Connection.prototype._firstSnapshotRequest = function (fn) { }; /** - * Get a read-only snapshot at a given version or time + * Get 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 an exact version match is not made, then the next lower + * @param version (optional) - the version number to fetch * @param callback - (error, snapshot) => void, where snapshot takes the following schema: * * { @@ -645,7 +645,7 @@ Connection.prototype.getSnapshot = function(collection, id, version, callback) { }; /** - * Get a read-only snapshot at a given version or time + * Get a read-only snapshot at a given time * * @param collection - the collection name of the snapshot * @param id - the ID of the snapshot From c8559335c36cf2bbe7214b9b9d02a96d6506dd7d Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 12 Jul 2018 10:50:18 +0100 Subject: [PATCH 038/181] Increment fetched snapshot version The version attached to an operation in the database is the version before the operation has been applied to (or the version to which the operation can be applied). That means that when applying op v1, the document is v1 before the op has been applied, and then v2 after the op has been applied. This change updates the `getSnapshot` method to return snapshots with versions consistent with this (ie the snapshots are now 1-based instead of 0-based). --- lib/backend.js | 7 +++--- test/client/snapshot-request.js | 42 ++++++++++++++++++++------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index e793f01cc..c8541c487 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -613,8 +613,7 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c }; Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { - var requestedVersion = version; - version = (typeof version === 'number' && isFinite(version)) ? version + 1 : null; + version = (typeof version === 'number' && isFinite(version)) ? Math.max(0, version) : null; var options = { metadata: true }; var timestampIsNumber = typeof timestamp === 'number'; @@ -638,7 +637,7 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, } fetchedTimestamp = op.m.ts; - fetchedVersion = op.v; + fetchedVersion = op.v + 1; if (op.create) { type = types.map[op.create.type]; @@ -654,7 +653,7 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, type = type ? type.uri : null; - if (requestedVersion > fetchedVersion) { + if (version > fetchedVersion) { return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); } diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index a9da49925..ae2f1c4b0 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -42,7 +42,7 @@ describe('SnapshotRequest', function () { var v0 = { id: 'don-quixote', collection: 'books', - version: 0, + version: 1, timestamp: DAY1.getTime(), type: json0, data: { @@ -53,7 +53,7 @@ describe('SnapshotRequest', function () { var v1 = { id: 'don-quixote', collection: 'books', - version: 1, + version: 2, timestamp: DAY2.getTime(), type: json0, data: { @@ -65,7 +65,7 @@ describe('SnapshotRequest', function () { var v2 = { id: 'don-quixote', collection: 'books', - version: 2, + version: 3, timestamp: DAY3.getTime(), type: json0, data: { @@ -88,30 +88,38 @@ describe('SnapshotRequest', function () { }); describe('getSnapshot', () => { - it('fetches v0', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + it('fetches v1', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 1, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v0); done(); }); }); - it('fetches v1', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 1, function (error, snapshot) { + it('fetches v2', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v1); done(); }); }); - it('fetches v2', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { + it('fetches v3', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v2); done(); }); }); + it('returns an empty snapshot if the version is 0', function (done) { + backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(emptySnapshot); + done(); + }); + }); + it('fetches the latest version if the version is undefined', function (done) { backend.connect().getSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { if (error) return done(error); @@ -149,7 +157,7 @@ describe('SnapshotRequest', function () { }); it('errors if asking for a version that does not exist', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { + backend.connect().getSnapshot('books', 'don-quixote', 4, function (error, snapshot) { expect(error.code).to.be(4024); expect(snapshot).to.be(undefined); done(); @@ -279,7 +287,7 @@ describe('SnapshotRequest', function () { function (request) { expect(request.collection).to.be('books'); expect(request.id).to.be('don-quixote'); - expect(request.version).to.be(2); + expect(request.version).to.be(3); expect(request.timestamp).to.be(DAY3.getTime()); expect(request.snapshots).to.eql([v2.data]); expect(request.type).to.be('http://sharejs.org/types/JSONv0'); @@ -299,7 +307,7 @@ describe('SnapshotRequest', function () { }, ]; - backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + backend.connect().getSnapshot('books', 'don-quixote', function (error, snapshot) { if (error) return done(error); expect(snapshot.data.title).to.be('Alice in Wonderland'); done(); @@ -354,7 +362,7 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'catch-22', collection: 'books', - version: 1, + version: 2, timestamp: DAY2.getTime(), type: null, data: undefined @@ -364,14 +372,14 @@ describe('SnapshotRequest', function () { }); }); - it('fetches v0', function (done) { - backend.connect().getSnapshot('books', 'catch-22', 0, function (error, snapshot) { + it('fetches v1', function (done) { + backend.connect().getSnapshot('books', 'catch-22', 1, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ id: 'catch-22', collection: 'books', - version: 0, + version: 1, timestamp: DAY1.getTime(), type: json0, data: { @@ -407,7 +415,7 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'hitchhikers-guide', collection: 'books', - version: 2, + version: 3, timestamp: DAY3.getTime(), type: json0, data: { From afb77841f90b20ce75ee29f1c3e122fa56fbbbf0 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 12 Jul 2018 11:24:55 +0100 Subject: [PATCH 039/181] Update SnapshotRequest version variable names to match version --- test/client/snapshot-request.js | 38 ++++++++++++++++----------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index ae2f1c4b0..3abb9e67f 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -30,7 +30,7 @@ describe('SnapshotRequest', function () { }); describe('a document with some simple versions a day apart', function () { - var emptySnapshot = { + var v0 = { id: 'don-quixote', collection: 'books', version: 0, @@ -39,7 +39,7 @@ describe('SnapshotRequest', function () { data: undefined }; - var v0 = { + var v1 = { id: 'don-quixote', collection: 'books', version: 1, @@ -50,7 +50,7 @@ describe('SnapshotRequest', function () { } }; - var v1 = { + var v2 = { id: 'don-quixote', collection: 'books', version: 2, @@ -62,7 +62,7 @@ describe('SnapshotRequest', function () { } }; - var v2 = { + var v3 = { id: 'don-quixote', collection: 'books', version: 3, @@ -91,7 +91,7 @@ describe('SnapshotRequest', function () { it('fetches v1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 1, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v0); + expect(snapshot).to.eql(v1); done(); }); }); @@ -99,7 +99,7 @@ describe('SnapshotRequest', function () { it('fetches v2', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v1); + expect(snapshot).to.eql(v2); done(); }); }); @@ -107,7 +107,7 @@ describe('SnapshotRequest', function () { it('fetches v3', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v2); + expect(snapshot).to.eql(v3); done(); }); }); @@ -115,7 +115,7 @@ describe('SnapshotRequest', function () { it('returns an empty snapshot if the version is 0', function (done) { backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(emptySnapshot); + expect(snapshot).to.eql(v0); done(); }); }); @@ -123,7 +123,7 @@ describe('SnapshotRequest', function () { it('fetches the latest version if the version is undefined', function (done) { backend.connect().getSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v2); + expect(snapshot).to.eql(v3); done(); }); }); @@ -131,7 +131,7 @@ describe('SnapshotRequest', function () { it('fetches the latest version when the optional version is not provided', function (done) { backend.connect().getSnapshot('books', 'don-quixote', function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v2); + expect(snapshot).to.eql(v3); done(); }); }); @@ -143,7 +143,7 @@ describe('SnapshotRequest', function () { it('returns an empty snapshot if the version is -1', function (done) { backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(emptySnapshot); + expect(snapshot).to.eql(v0); done(); }); }); @@ -207,7 +207,7 @@ describe('SnapshotRequest', function () { it('fetches the version from Day 1', function (done) { backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY1.getTime(), function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v0); + expect(snapshot).to.eql(v1); done(); }); }); @@ -215,7 +215,7 @@ describe('SnapshotRequest', function () { it('fetches the version from Day 2', function (done) { backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY2.getTime(), function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v1); + expect(snapshot).to.eql(v2); done(); }); }); @@ -223,7 +223,7 @@ describe('SnapshotRequest', function () { it('fetches the version from Day 3', function (done) { backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY3.getTime(), function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v2); + expect(snapshot).to.eql(v3); done(); }); }); @@ -231,7 +231,7 @@ describe('SnapshotRequest', function () { it('fetches the latest version if the timestamp is undefined', function (done) { backend.connect().getSnapshotAtTime('books', 'don-quixote', undefined, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v2); + expect(snapshot).to.eql(v3); done(); }); }); @@ -239,7 +239,7 @@ describe('SnapshotRequest', function () { it('fetches the latest version when the optional timestamp is not provided', function (done) { backend.connect().getSnapshotAtTime('books', 'don-quixote', function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(v2); + expect(snapshot).to.eql(v3); done(); }); }); @@ -251,7 +251,7 @@ describe('SnapshotRequest', function () { it('returns an empty snapshot when trying to fetch a snapshot before the document existed', function (done) { backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY0.getTime(), function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(emptySnapshot); + expect(snapshot).to.eql(v0); done(); }); }); @@ -259,7 +259,7 @@ describe('SnapshotRequest', function () { it('returns an empty snapshot when trying to fetch a snapshot at the epoch', function (done) { backend.connect().getSnapshotAtTime('books', 'don-quixote', 0, function (error, snapshot) { if (error) return done(error); - expect(snapshot).to.eql(emptySnapshot); + expect(snapshot).to.eql(v0); done(); }); }); @@ -289,7 +289,7 @@ describe('SnapshotRequest', function () { expect(request.id).to.be('don-quixote'); expect(request.version).to.be(3); expect(request.timestamp).to.be(DAY3.getTime()); - expect(request.snapshots).to.eql([v2.data]); + expect(request.snapshots).to.eql([v3.data]); expect(request.type).to.be('http://sharejs.org/types/JSONv0'); done(); From 16d6eaa7826e9d0a31071f24a788e48db1f15986 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 12 Jul 2018 20:56:26 +0100 Subject: [PATCH 040/181] Ensure snapshot fetch timestamp is finite --- lib/backend.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/backend.js b/lib/backend.js index c8541c487..c2644602e 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -615,7 +615,7 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { version = (typeof version === 'number' && isFinite(version)) ? Math.max(0, version) : null; var options = { metadata: true }; - var timestampIsNumber = typeof timestamp === 'number'; + var timestampIsNumber = typeof timestamp === 'number' && isFinite(timestamp); if (timestampIsNumber && timestamp > Date.now()) { return callback({ code: 4025, message: 'Requested timestamp must be in the past' }); From 12e1bcc8c7a93c1dc2c6e021f558816a09bb975e Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 19 Jul 2018 11:57:46 +0100 Subject: [PATCH 041/181] Add Snapshot class This change adds a very simple `Snapshot` class, which currently has no methods attached to it. Its primary use at the moment is to contain the response to a snapshot request, and for use in `MemoryDB` in place of `MemorySnapshot`. As part of this change, the structure of the snapshot request return is slightly modified to fit this class, including renaming of `version` to `v`, and moving of `timestamp` to `m.ts`. --- lib/backend.js | 14 ++++---- lib/client/snapshot-request.js | 17 +++++----- lib/db/memory.js | 14 ++------ lib/snapshot.js | 9 +++++ test/client/snapshot-request.js | 58 +++++++++++++++++++++------------ 5 files changed, 66 insertions(+), 46 deletions(-) create mode 100644 lib/snapshot.js diff --git a/lib/backend.js b/lib/backend.js index c2644602e..5a70f4df9 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -594,8 +594,10 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c var request = { collection: index, id: id, - version: snapshot.version, - timestamp: snapshot.timestamp, + v: snapshot.v, + m: { + ts: snapshot.ts + }, snapshots: snapshot.data ? [snapshot.data] : [], type: snapshot.type }; @@ -604,8 +606,8 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c if (error) return callback(error); callback(null, { data: request.snapshots[0], - version: request.version, - timestamp: request.timestamp, + v: request.v, + ts: request.m.ts, type: request.type }); }); @@ -659,8 +661,8 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback(null, { data: snapshot, - timestamp: fetchedTimestamp, - version: fetchedVersion, + ts: fetchedTimestamp, + v: fetchedVersion, type: type }); }); diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 9c9f08983..15151d709 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -1,5 +1,6 @@ var hat = require('hat'); var types = require('../types'); +var Snapshot = require('../snapshot'); module.exports = SnapshotRequest; @@ -49,12 +50,12 @@ SnapshotRequest.prototype._handleResponse = function (error, message) { return this.callback(error); } - this.callback(null, { - id: this.id, - collection: this.collection, - version: message.version, - data: message.data, - timestamp: message.timestamp, - type: types.map[message.type] || null - }); + var type = types.map[message.type] || null; + + var metadata = { + ts: message.ts, + }; + + var snapshot = new Snapshot(this.collection, this.id, message.v, type, message.data, metadata); + this.callback(null, snapshot); }; diff --git a/lib/db/memory.js b/lib/db/memory.js index 2c5b75fb6..e5f0f303f 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 // @@ -152,23 +153,14 @@ MemoryDB.prototype._getSnapshotSync = function(collection, id, includeMetadata) if (doc) { var data = clone(doc.data); var meta = (includeMetadata) ? clone(doc.m) : undefined; - snapshot = new MemorySnapshot(id, doc.v, doc.type, data, meta); + snapshot = new Snapshot(undefined, id, doc.v, doc.type, data, meta); } else { var version = this._getVersionSync(collection, id); - snapshot = new MemorySnapshot(id, version, null, undefined); + snapshot = new Snapshot(undefined, id, version, null, undefined); } 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/snapshot.js b/lib/snapshot.js new file mode 100644 index 000000000..00c953897 --- /dev/null +++ b/lib/snapshot.js @@ -0,0 +1,9 @@ +module.exports = Snapshot; +function Snapshot(collection, id, version, type, data, meta) { + if (collection) this.collection = collection; + this.id = id; + this.v = version; + this.type = type; + this.data = data; + if (meta) this.m = meta; +} diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 3abb9e67f..cf20a7a7e 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -33,44 +33,52 @@ describe('SnapshotRequest', function () { var v0 = { id: 'don-quixote', collection: 'books', - version: 0, - timestamp: 0, + v: 0, type: null, - data: undefined + data: undefined, + m: { + ts: 0 + } }; var v1 = { id: 'don-quixote', collection: 'books', - version: 1, - timestamp: DAY1.getTime(), + v: 1, type: json0, data: { title: 'Don Quixote' + }, + m: { + ts: DAY1.getTime() } }; var v2 = { id: 'don-quixote', collection: 'books', - version: 2, - timestamp: DAY2.getTime(), + v: 2, type: json0, data: { title: 'Don Quixote', author: 'Miguel de Cervante' + }, + m: { + ts: DAY2.getTime() } }; var v3 = { id: 'don-quixote', collection: 'books', - version: 3, - timestamp: DAY3.getTime(), + v: 3, type: json0, data: { title: 'Don Quixote', author: 'Miguel de Cervantes' + }, + m: { + ts: DAY3.getTime() } }; @@ -170,10 +178,12 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'does-not-exist', collection: 'books', - version: 0, - timestamp: 0, + v: 0, type: null, - data: undefined + data: undefined, + m: { + ts: 0 + } }); done(); }); @@ -287,8 +297,8 @@ describe('SnapshotRequest', function () { function (request) { expect(request.collection).to.be('books'); expect(request.id).to.be('don-quixote'); - expect(request.version).to.be(3); - expect(request.timestamp).to.be(DAY3.getTime()); + expect(request.v).to.be(3); + expect(request.m.ts).to.be(DAY3.getTime()); expect(request.snapshots).to.eql([v3.data]); expect(request.type).to.be('http://sharejs.org/types/JSONv0'); @@ -362,10 +372,12 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'catch-22', collection: 'books', - version: 2, - timestamp: DAY2.getTime(), + v: 2, type: null, - data: undefined + data: undefined, + m: { + ts: DAY2.getTime() + } }); done(); @@ -379,11 +391,13 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'catch-22', collection: 'books', - version: 1, - timestamp: DAY1.getTime(), + v: 1, type: json0, data: { title: 'Catch 22', + }, + m: { + ts: DAY1.getTime() } }); @@ -415,11 +429,13 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'hitchhikers-guide', collection: 'books', - version: 3, - timestamp: DAY3.getTime(), + v: 3, type: json0, data: { title: 'The Restaurant at the End of the Universe', + }, + m: { + ts: DAY3.getTime() } }); From b57273d9800fd572d43fe648f0885c89182d1cae Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 26 Jul 2018 07:48:04 +0100 Subject: [PATCH 042/181] Remove snapshot fetch at time This change makes a couple of review markups. The biggest is the removal of the ability to fetch a snapshot at a given time. It has been agreed that this would be a useful API, but we need to think about the ramifications of exposing this API before doing so (eg adding more indexes on database adapters for lookup by time, etc.). In order to move the core functionality forward - that is the ability to fetch a snapshot by version - this change removes the ability to fetch by time, and it is left (for now) to the consumer to look up the version they need themselves. Alongside this change, `getSnapshot` has also been renamed to `fetchSnapshot` to imply that we touch the database layer, and the `Snapshot` class has had its `collections` property removed to be consistent with database adapters, whilst also making `m` a property that is always present. --- README.md | 21 +--- lib/agent.js | 6 +- lib/backend.js | 23 +--- lib/client/connection.js | 45 ++------ lib/client/snapshot-request.js | 10 +- lib/db/memory.js | 6 +- lib/snapshot.js | 5 +- package.json | 1 - test/client/snapshot-request.js | 181 +++++--------------------------- test/db.js | 26 ++--- 10 files changed, 69 insertions(+), 255 deletions(-) diff --git a/README.md b/README.md index 1f0bfce9f..0b49c2cea 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ 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 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. @@ -227,7 +227,7 @@ changes. Returns a [`ShareDB.Query`](#class-sharedbquery) instance. * `options.*` All other options are passed through to the database adapter. -`connection.getSnapshot(collection, id, version, callback): void;` +`connection.fetchSnapshot(collection, id, version, callback): void;` Get a read-only snapshot of a document at the requested version. * `collection` _(String)_ @@ -241,27 +241,13 @@ Get a read-only snapshot of a document at the requested version. ```javascript { - collection: string; // collection name of the snapshot id: string; // ID of the snapshot - version: number; // version number of the snapshot - timestamp: number; // the UNIX timestamp of the snapshot + v: number; // version number of the snapshot type: any; // the OT type of the snapshot, or null if it doesn't exist or is deleted data: any; // the snapshot } ``` -`connection.getSnapshotAtTime(collection, id, timestamp, callback): void;` -Get a read-only snapshot of a document at the requested timestamp. - -* `collection` _(String)_ - Collection name of the snapshot -* `id` _(String)_ - ID of the snapshot -* `timestamp` _(number) [optional]_ - The timestamp at which you wish to view the snapshot. If an exact timestamp match is not made, then the next lower version is returned. ie if ops were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will return the 02:00 version. -* `callback` _(Function)_ - Called with `(error, snapshot)`, where `snapshot` takes the same form as for `getSnapshot` above. - ### Class: `ShareDB.Doc` `doc.type` _(String_) @@ -411,7 +397,6 @@ Additional fields may be added to the error object for debugging context dependi * 4022 - Database adapter does not support queries * 4023 - Cannot project snapshots of this type * 4024 - Invalid version -* 4025 - Invalid timestamp ### 5000 - Internal error diff --git a/lib/agent.js b/lib/agent.js index 67e381400..d5e9a02cf 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -301,7 +301,7 @@ Agent.prototype._handleMessage = function(request, callback) { if (!op) return callback({code: 4000, message: 'Invalid op message'}); return this._submit(request.c, request.d, op, callback); case 'sf': - return this._getSnapshot(request.c, request.d, request.v, request.ts, callback); + return this._fetchSnapshot(request.c, request.d, request.v, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); } @@ -585,6 +585,6 @@ Agent.prototype._createOp = function(request) { } }; -Agent.prototype._getSnapshot = function (collection, id, version, timestamp, callback) { - this.backend.getSnapshot(this, collection, id, version, timestamp, callback); +Agent.prototype._fetchSnapshot = function (collection, id, version, callback) { + this.backend.fetchSnapshot(this, collection, id, version, callback); }; diff --git a/lib/backend.js b/lib/backend.js index 5a70f4df9..846a50a1d 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -586,18 +586,15 @@ Backend.prototype.getChannels = function(collection, id) { ]; }; -Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, callback) { +Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) { var backend = this; - this._getSnapshot(agent, index, id, version, timestamp, function (error, snapshot) { + this._fetchSnapshot(agent, index, id, version, function (error, snapshot) { if (error) return callback(error); var request = { collection: index, id: id, v: snapshot.v, - m: { - ts: snapshot.ts - }, snapshots: snapshot.data ? [snapshot.data] : [], type: snapshot.type }; @@ -607,38 +604,25 @@ Backend.prototype.getSnapshot = function(agent, index, id, version, timestamp, c callback(null, { data: request.snapshots[0], v: request.v, - ts: request.m.ts, type: request.type }); }); }); }; -Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback) { +Backend.prototype._fetchSnapshot = function (agent, index, id, version, callback) { version = (typeof version === 'number' && isFinite(version)) ? Math.max(0, version) : null; var options = { metadata: true }; - var timestampIsNumber = typeof timestamp === 'number' && isFinite(timestamp); - - if (timestampIsNumber && timestamp > Date.now()) { - return callback({ code: 4025, message: 'Requested timestamp must be in the past' }); - } this._getOps(agent, index, id, 0, version, options, function (error, ops) { if (error) return callback(error); var type = null; var snapshot; - var fetchedTimestamp = 0; var fetchedVersion = 0; for (var index = 0; index < ops.length; index++) { var op = ops[index]; - - if (timestampIsNumber && op.m.ts > timestamp) { - break; - } - - fetchedTimestamp = op.m.ts; fetchedVersion = op.v + 1; if (op.create) { @@ -661,7 +645,6 @@ Backend.prototype._getSnapshot = function (agent, index, id, version, timestamp, callback(null, { data: snapshot, - ts: fetchedTimestamp, v: fetchedVersion, type: type }); diff --git a/lib/client/connection.js b/lib/client/connection.js index 0a405e347..045a944ab 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -611,7 +611,7 @@ Connection.prototype._firstSnapshotRequest = function (fn) { }; /** - * Get a read-only snapshot at a given version + * Fetch a read-only snapshot at a given version * * @param collection - the collection name of the snapshot * @param id - the ID of the snapshot @@ -619,53 +619,28 @@ Connection.prototype._firstSnapshotRequest = function (fn) { * @param callback - (error, snapshot) => void, where snapshot takes the following schema: * * { - * collection: string; // collection name of the snapshot * id: string; // ID of the snapshot - * version: number; // version number of the snapshot - * timestamp: number; // the UNIX timestamp of the snapshot + * v: number; // version number of the snapshot * type: any; // the OT type of the snapshot, or null if it doesn't exist or is deleted * data: any; // the snapshot * } * */ -Connection.prototype.getSnapshot = function(collection, id, version, callback) { +Connection.prototype.fetchSnapshot = function(collection, id, version, callback) { if (typeof version === 'function') { callback = version; version = null; } - if (!this._isNumberOrNully(version)) { - return callback({ code: 4024, message: 'Invalid version' }); - } - - var snapshotRequest = new SnapshotRequest(this, collection, id, callback); - snapshotRequest.version = version; - this.snapshotRequests[snapshotRequest.requestId] = snapshotRequest; - snapshotRequest.send(); -}; - -/** - * Get a read-only snapshot at a given time - * - * @param collection - the collection name of the snapshot - * @param id - the ID of the snapshot - * @param version (optional) - the timestamp of the snapshot to fetch. If an exact timestamp match is not made, then the - * next lower version is returned. ie if ops were submitted at 02:00 and 03:00, then asking for a Date at 02:30 will - * return the 02:00 version. - * @param callback - (error, snapshot) => void, where snapshot takes the same schema as getSnapshot (see above) - */ -Connection.prototype.getSnapshotAtTime = function (collection, id, timestamp, callback) { - if (typeof timestamp === 'function') { - callback = timestamp; - timestamp = null; + if (version === undefined) { + version = null; } - if (!this._isNumberOrNully(timestamp)) { - return callback({ code: 4025, message: 'Invalid timestamp' }); + if (!this._isNumberOrNull(version)) { + return callback && callback({ code: 4024, message: 'Invalid version' }); } - var snapshotRequest = new SnapshotRequest(this, collection, id, callback); - snapshotRequest.timestamp = timestamp; + var snapshotRequest = new SnapshotRequest(this, collection, id, version, callback); this.snapshotRequests[snapshotRequest.requestId] = snapshotRequest; snapshotRequest.send(); }; @@ -677,8 +652,8 @@ Connection.prototype._handleSnapshot = function (error, message) { snapshotRequest._handleResponse(error, message); }; -Connection.prototype._isNumberOrNully = function (number) { - if (number == null) { +Connection.prototype._isNumberOrNull = function (number) { + if (number === null) { return true; } diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 15151d709..c1bb9d535 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -4,12 +4,13 @@ var Snapshot = require('../snapshot'); module.exports = SnapshotRequest; -function SnapshotRequest(connection, collection, id, callback) { +function SnapshotRequest(connection, collection, id, version, callback) { this.requestId = hat(); this.connection = connection; this.id = id; this.collection = collection; + this.version = version; this.callback = callback; this.sent = false; @@ -26,7 +27,6 @@ SnapshotRequest.prototype.send = function () { c: this.collection, d: this.id, v: this.version, - ts: this.timestamp }; this.connection.send(message); @@ -52,10 +52,6 @@ SnapshotRequest.prototype._handleResponse = function (error, message) { var type = types.map[message.type] || null; - var metadata = { - ts: message.ts, - }; - - var snapshot = new Snapshot(this.collection, this.id, message.v, type, message.data, metadata); + var snapshot = new Snapshot(this.id, message.v, type, message.data, null); this.callback(null, snapshot); }; diff --git a/lib/db/memory.js b/lib/db/memory.js index e5f0f303f..73a22e6df 100644 --- a/lib/db/memory.js +++ b/lib/db/memory.js @@ -152,11 +152,11 @@ 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 Snapshot(undefined, 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 Snapshot(undefined, id, version, null, undefined); + snapshot = new Snapshot(id, version, null, undefined, null); } return snapshot; }; diff --git a/lib/snapshot.js b/lib/snapshot.js index 00c953897..548a7e25b 100644 --- a/lib/snapshot.js +++ b/lib/snapshot.js @@ -1,9 +1,8 @@ module.exports = Snapshot; -function Snapshot(collection, id, version, type, data, meta) { - if (collection) this.collection = collection; +function Snapshot(id, version, type, data, meta) { this.id = id; this.v = version; this.type = type; this.data = data; - if (meta) this.m = meta; + this.m = meta; } diff --git a/package.json b/package.json index fa080f75f..4a38e5a65 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", - "lolex": "^2.7.0", "mocha": "^5.2.0" }, "scripts": { diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index cf20a7a7e..c22c9c233 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -1,103 +1,74 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -var lolex = require("lolex"); var types = require('../../lib/types'); describe('SnapshotRequest', function () { var backend; - var clock; var json0 = types.map['json0']; - var DAY0 = new Date("2018-05-30"); - var DAY1 = new Date("2018-06-01"); - var DAY2 = new Date("2018-06-02"); - var DAY3 = new Date("2018-06-03"); - var DAY4 = new Date("2018-06-04"); - 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 a day apart', function () { var v0 = { id: 'don-quixote', - collection: 'books', v: 0, type: null, data: undefined, - m: { - ts: 0 - } + m: null }; var v1 = { id: 'don-quixote', - collection: 'books', v: 1, type: json0, data: { title: 'Don Quixote' }, - m: { - ts: DAY1.getTime() - } + m: null }; var v2 = { id: 'don-quixote', - collection: 'books', v: 2, type: json0, data: { title: 'Don Quixote', author: 'Miguel de Cervante' }, - m: { - ts: DAY2.getTime() - } + m: null }; var v3 = { id: 'don-quixote', - collection: 'books', v: 3, type: json0, data: { title: 'Don Quixote', author: 'Miguel de Cervantes' }, - m: { - ts: DAY3.getTime() - } + 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); - clock.tick(ONE_DAY); doc.submitOp({ p: ['author'], oi: 'Miguel de Cervante' }, function (error) { if (error) return done(error); - clock.tick(ONE_DAY); doc.submitOp({ p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes' }, done); }); }); }); - describe('getSnapshot', () => { + describe('fetchSnapshot', () => { it('fetches v1', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 1, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 1, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v1); done(); @@ -105,7 +76,7 @@ describe('SnapshotRequest', function () { }); it('fetches v2', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 2, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 2, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v2); done(); @@ -113,7 +84,7 @@ describe('SnapshotRequest', function () { }); it('fetches v3', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 3, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 3, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v3); done(); @@ -121,7 +92,7 @@ describe('SnapshotRequest', function () { }); it('returns an empty snapshot if the version is 0', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 0, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v0); done(); @@ -129,7 +100,7 @@ describe('SnapshotRequest', function () { }); it('fetches the latest version if the version is undefined', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v3); done(); @@ -137,7 +108,7 @@ describe('SnapshotRequest', function () { }); it('fetches the latest version when the optional version is not provided', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v3); done(); @@ -145,11 +116,11 @@ describe('SnapshotRequest', function () { }); it('can call without a callback', function () { - backend.connect().getSnapshot('books', 'don-quixote'); + backend.connect().fetchSnapshot('books', 'don-quixote'); }); it('returns an empty snapshot if the version is -1', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', -1, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', -1, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql(v0); done(); @@ -157,7 +128,7 @@ describe('SnapshotRequest', function () { }); it('errors if the version is a string', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 'foo', function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function (error, snapshot) { expect(error.code).to.be(4024); expect(snapshot).to.be(undefined); done(); @@ -165,7 +136,7 @@ describe('SnapshotRequest', function () { }); it('errors if asking for a version that does not exist', function (done) { - backend.connect().getSnapshot('books', 'don-quixote', 4, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 4, function (error, snapshot) { expect(error.code).to.be(4024); expect(snapshot).to.be(undefined); done(); @@ -173,17 +144,14 @@ describe('SnapshotRequest', function () { }); it('returns an empty snapshot if trying to fetch a non-existent document', function (done) { - backend.connect().getSnapshot('books', 'does-not-exist', 0, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ id: 'does-not-exist', - collection: 'books', v: 0, type: null, data: undefined, - m: { - ts: 0 - } + m: null }); done(); }); @@ -192,7 +160,7 @@ describe('SnapshotRequest', function () { it('starts pending, and finishes not pending', function (done) { var connection = backend.connect(); - connection.getSnapshot('books', 'don-quixote', null, function (error, snapshot) { + connection.fetchSnapshot('books', 'don-quixote', null, function (error, snapshot) { expect(connection.hasPending()).to.be(false); done(); }); @@ -203,7 +171,7 @@ describe('SnapshotRequest', function () { it('deletes the request from the connection', function (done) { var connection = backend.connect(); - connection.getSnapshot('books', 'don-quixote', function (error) { + connection.fetchSnapshot('books', 'don-quixote', function (error) { if (error) return done(error); expect(connection.snapshotRequests).to.eql({}); done(); @@ -213,84 +181,6 @@ describe('SnapshotRequest', function () { }); }); - describe('getSnapshotByTimestamp', () => { - it('fetches the version from Day 1', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY1.getTime(), function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v1); - done(); - }); - }); - - it('fetches the version from Day 2', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY2.getTime(), function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v2); - done(); - }); - }); - - it('fetches the version from Day 3', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY3.getTime(), function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v3); - done(); - }); - }); - - it('fetches the latest version if the timestamp is undefined', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', undefined, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v3); - done(); - }); - }); - - it('fetches the latest version when the optional timestamp is not provided', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v3); - done(); - }); - }); - - it('can call without a callback', function () { - backend.connect().getSnapshotAtTime('books', 'don-quixote'); - }); - - it('returns an empty snapshot when trying to fetch a snapshot before the document existed', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY0.getTime(), function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v0); - done(); - }); - }); - - it('returns an empty snapshot when trying to fetch a snapshot at the epoch', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', 0, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v0); - done(); - }); - }); - - it('errors if asking for a time after now', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', DAY4.getTime(), function (error, snapshot) { - expect(error.code).to.be(4025); - expect(snapshot).to.be(undefined); - done(); - }); - }); - - it('errors if the timestamp is a string', function (done) { - backend.connect().getSnapshotAtTime('books', 'don-quixote', 'foo', function (error, snapshot) { - expect(error.code).to.be(4025); - expect(snapshot).to.be(undefined); - done(); - }); - }); - }); - describe('readSnapshots middleware', function () { it('triggers the middleware', function (done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, @@ -298,7 +188,6 @@ describe('SnapshotRequest', function () { expect(request.collection).to.be('books'); expect(request.id).to.be('don-quixote'); expect(request.v).to.be(3); - expect(request.m.ts).to.be(DAY3.getTime()); expect(request.snapshots).to.eql([v3.data]); expect(request.type).to.be('http://sharejs.org/types/JSONv0'); @@ -306,7 +195,7 @@ describe('SnapshotRequest', function () { } ); - backend.connect().getSnapshot('books', 'don-quixote'); + backend.connect().fetchSnapshot('books', 'don-quixote'); }); it('can have its snapshot manipulated in the middleware', function (done) { @@ -317,7 +206,7 @@ describe('SnapshotRequest', function () { }, ]; - backend.connect().getSnapshot('books', 'don-quixote', function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', function (error, snapshot) { if (error) return done(error); expect(snapshot.data.title).to.be('Alice in Wonderland'); done(); @@ -331,7 +220,7 @@ describe('SnapshotRequest', function () { }, ]; - backend.connect().getSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 0, function (error, snapshot) { expect(error.message).to.be('foo'); done(); }); @@ -344,7 +233,7 @@ describe('SnapshotRequest', function () { }); it('applies the projection to a snapshot', function (done) { - backend.connect().getSnapshot('bookTitles', 'don-quixote', 2, function (error, snapshot) { + backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function (error, snapshot) { if (error) return done(error); expect(snapshot.data.title).to.be('Don Quixote'); @@ -360,7 +249,6 @@ describe('SnapshotRequest', function () { var doc = backend.connect().get('books', 'catch-22'); doc.create({ title: 'Catch 22' }, function (error) { if (error) return done(error); - clock.tick(ONE_DAY); doc.del(function (error) { done(error); }); @@ -368,16 +256,13 @@ describe('SnapshotRequest', function () { }); it('returns a null type', function (done) { - backend.connect().getSnapshot('books', 'catch-22', null, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'catch-22', null, function (error, snapshot) { expect(snapshot).to.eql({ id: 'catch-22', - collection: 'books', v: 2, type: null, data: undefined, - m: { - ts: DAY2.getTime() - } + m: null }); done(); @@ -385,20 +270,17 @@ describe('SnapshotRequest', function () { }); it('fetches v1', function (done) { - backend.connect().getSnapshot('books', 'catch-22', 1, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'catch-22', 1, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ id: 'catch-22', - collection: 'books', v: 1, type: json0, data: { title: 'Catch 22', }, - m: { - ts: DAY1.getTime() - } + m: null }); done(); @@ -411,10 +293,8 @@ describe('SnapshotRequest', function () { var doc = backend.connect().get('books', 'hitchhikers-guide'); doc.create({ title: 'Hitchhiker\'s Guide to the Galaxy' }, function (error) { if (error) return done(error); - clock.tick(ONE_DAY); doc.del(function (error) { if (error) return done (error); - clock.tick(ONE_DAY); doc.create({ title: 'The Restaurant at the End of the Universe' }, function (error) { done(error); }); @@ -423,20 +303,17 @@ describe('SnapshotRequest', function () { }); it('fetches the latest version of the document', function (done) { - backend.connect().getSnapshot('books', 'hitchhikers-guide', null, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function (error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ id: 'hitchhikers-guide', - collection: 'books', v: 3, type: json0, data: { title: 'The Restaurant at the End of the Universe', }, - m: { - ts: DAY3.getTime() - } + m: null }); done(); diff --git a/test/db.js b/test/db.js index db3aa1a88..efba94850 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(); }); }); @@ -242,7 +242,7 @@ module.exports = function(options) { 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(); }); }); @@ -253,7 +253,7 @@ module.exports = function(options) { commitSnapshotWithMetadata(db, function(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(); }); }); @@ -292,8 +292,8 @@ module.exports = function(options) { 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(); }); @@ -305,7 +305,7 @@ module.exports = function(options) { commitSnapshotWithMetadata(db, function(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(); }); }); @@ -621,7 +621,7 @@ 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) { if (err) return done(err); @@ -647,7 +647,7 @@ module.exports = function(options) { commitSnapshotWithMetadata(db, function(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(); }); }); @@ -699,7 +699,7 @@ module.exports = function(options) { commitSnapshotWithMetadata(db, function(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(); }); }); @@ -789,10 +789,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]]}); From 1ae5680b700bbd8f647146fd8de5fe0a880237df Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 2 Aug 2018 13:21:40 +0100 Subject: [PATCH 043/181] Review markups This change adds some review markups: - use `nf` instead of `sf` for "snapshot fetch" message type, to not confuse with "subscribe" - remove the internal `_getOps` method that was added for fetching metadata, which is no longer needed - add a `util.isInteger` method - rename `connection.snapshotRequests` to `_snapshotRequests` to signify that it's internal and the API could change - make sure that snapshot requests emit an event that is captured in `connection.whenNothingPending` - make snapshot requests use an incremental ID instead of a random one to be consistent with `Query` - make `SnapshotRequest` callbacks mandatory - make `SnapshotRequest` return a string type instead of the full object --- lib/agent.js | 2 +- lib/backend.js | 13 +- lib/client/connection.js | 47 +++---- lib/client/snapshot-request.js | 31 +++-- lib/util.js | 7 + test/client/snapshot-request.js | 223 +++++++++++++++++++------------- 6 files changed, 180 insertions(+), 143 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index d5e9a02cf..3ef558362 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -300,7 +300,7 @@ 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 'sf': + case 'nf': return this._fetchSnapshot(request.c, request.d, request.v, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); diff --git a/lib/backend.js b/lib/backend.js index 846a50a1d..674d3af95 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -286,13 +286,9 @@ Backend.prototype._getSnapshotsFromMap = function(ids, snapshotMap) { return snapshots; }; -Backend.prototype.getOps = function(agent, index, id, from, to, callback) { - return this._getOps(agent, index, id, from, to, null, callback); -}; - // 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, options, callback) { +Backend.prototype.getOps = function(agent, index, id, from, to, callback) { var start = Date.now(); var projection = this.projections[index]; var collection = (projection) ? projection.target : index; @@ -305,7 +301,7 @@ Backend.prototype._getOps = function(agent, index, id, from, to, options, callba from: from, to: to }; - backend.db.getOps(collection, id, from, to, options, function(err, ops) { + backend.db.getOps(collection, id, from, to, null, function(err, ops) { if (err) return callback(err); backend._sanitizeOps(agent, projection, collection, id, ops, function(err) { if (err) return callback(err); @@ -611,10 +607,7 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) }; Backend.prototype._fetchSnapshot = function (agent, index, id, version, callback) { - version = (typeof version === 'number' && isFinite(version)) ? Math.max(0, version) : null; - var options = { metadata: true }; - - this._getOps(agent, index, id, 0, version, options, function (error, ops) { + this.getOps(agent, index, id, 0, version, function (error, ops) { if (error) return callback(error); var type = null; diff --git a/lib/client/connection.js b/lib/client/connection.js index 045a944ab..4196ed56b 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -34,15 +34,16 @@ 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 = {}; + this._snapshotRequests = {}; // A unique message number for the given id this.seq = 1; @@ -230,7 +231,7 @@ Connection.prototype.handleMessage = function(message) { case 'bu': return this._handleBulkMessage(message, '_handleUnsubscribe'); - case 'sf': + case 'nf': return this._handleSnapshot(err, message); case 'f': @@ -318,8 +319,8 @@ Connection.prototype._setState = function(newState, reason) { } } // Emit the event to all snapshots - for (var id in this.snapshotRequests) { - var snapshotRequest = this.snapshotRequests[id]; + for (var id in this._snapshotRequests) { + var snapshotRequest = this._snapshotRequests[id]; snapshotRequest._onConnectionStateChanged(); } this.endBulk(); @@ -568,6 +569,11 @@ Connection.prototype.whenNothingPending = function(callback) { query.once('ready', this._nothingPendingRetry(callback)); return; } + var snapshotRequest = this._firstSnapshotRequest(exists); + if (snapshotRequest) { + snapshotRequest.once('ready', this._nothingPendingRetry(callback)); + return; + } // Call back when no pending operations process.nextTick(callback); }; @@ -602,8 +608,8 @@ Connection.prototype._firstQuery = function(fn) { }; Connection.prototype._firstSnapshotRequest = function (fn) { - for (var id in this.snapshotRequests) { - var snapshotRequest = this.snapshotRequests[id]; + for (var id in this._snapshotRequests) { + var snapshotRequest = this._snapshotRequests[id]; if (fn(snapshotRequest)) { return snapshotRequest; } @@ -632,30 +638,15 @@ Connection.prototype.fetchSnapshot = function(collection, id, version, callback) version = null; } - if (version === undefined) { - version = null; - } - - if (!this._isNumberOrNull(version)) { - return callback && callback({ code: 4024, message: 'Invalid version' }); - } - - var snapshotRequest = new SnapshotRequest(this, collection, id, version, callback); - this.snapshotRequests[snapshotRequest.requestId] = snapshotRequest; + var requestId = this.nextSnapshotRequestId++; + var snapshotRequest = new SnapshotRequest(this, requestId, collection, id, version, callback); + this._snapshotRequests[snapshotRequest.requestId] = snapshotRequest; snapshotRequest.send(); }; Connection.prototype._handleSnapshot = function (error, message) { - var snapshotRequest = this.snapshotRequests[message.id]; + var snapshotRequest = this._snapshotRequests[message.id]; if (!snapshotRequest) return; - delete this.snapshotRequests[message.id]; + delete this._snapshotRequests[message.id]; snapshotRequest._handleResponse(error, message); }; - -Connection.prototype._isNumberOrNull = function (number) { - if (number === null) { - return true; - } - - return typeof number === 'number' && isFinite(number); -}; diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index c1bb9d535..8909940e0 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -1,20 +1,30 @@ -var hat = require('hat'); -var types = require('../types'); var Snapshot = require('../snapshot'); +var util = require('../util'); +var emitter = require('../emitter'); module.exports = SnapshotRequest; -function SnapshotRequest(connection, collection, id, version, callback) { - this.requestId = hat(); +function SnapshotRequest(connection, requestId, collection, id, version, callback) { + emitter.EventEmitter.call(this); + if (typeof callback !== 'function') { + throw new Error('Callback is required for SnapshotRequest'); + } + + if (version != null && !util.isInteger(version)) { + throw new Error('Snapshot version must be an integer'); + } + + this.requestId = requestId; this.connection = connection; this.id = id; this.collection = collection; - this.version = version; + this.version = util.isInteger(version) ? Math.max(0, version) : null; this.callback = callback; this.sent = false; } +emitter.mixin(SnapshotRequest); SnapshotRequest.prototype.send = function () { if (!this.connection.canSend) { @@ -22,7 +32,7 @@ SnapshotRequest.prototype.send = function () { } var message = { - a: 'sf', + a: 'nf', id: this.requestId, c: this.collection, d: this.id, @@ -42,16 +52,11 @@ SnapshotRequest.prototype._onConnectionStateChanged = function () { }; SnapshotRequest.prototype._handleResponse = function (error, message) { - if (!this.callback) { - return; - } - if (error) { return this.callback(error); } - var type = types.map[message.type] || null; - - var snapshot = new Snapshot(this.id, message.v, type, message.data, null); + var snapshot = new Snapshot(this.id, message.v, message.type, message.data, null); this.callback(null, snapshot); + this.emit('ready'); }; diff --git a/lib/util.js b/lib/util.js index 5c8021c0d..2817765a7 100644 --- a/lib/util.js +++ b/lib/util.js @@ -6,3 +6,10 @@ 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; +}; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index c22c9c233..304cb1f4d 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -1,10 +1,10 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -var types = require('../../lib/types'); +var StreamSocket = require('../../lib/stream-socket'); +var Connection = require('../../lib/client/connection'); describe('SnapshotRequest', function () { var backend; - var json0 = types.map['json0']; beforeEach(function () { backend = new Backend(); @@ -26,7 +26,7 @@ describe('SnapshotRequest', function () { var v1 = { id: 'don-quixote', v: 1, - type: json0, + type: 'http://sharejs.org/types/JSONv0', data: { title: 'Don Quixote' }, @@ -36,7 +36,7 @@ describe('SnapshotRequest', function () { var v2 = { id: 'don-quixote', v: 2, - type: json0, + type: 'http://sharejs.org/types/JSONv0', data: { title: 'Don Quixote', author: 'Miguel de Cervante' @@ -47,7 +47,7 @@ describe('SnapshotRequest', function () { var v3 = { id: 'don-quixote', v: 3, - type: json0, + type: 'http://sharejs.org/types/JSONv0', data: { title: 'Don Quixote', author: 'Miguel de Cervantes' @@ -66,121 +66,162 @@ describe('SnapshotRequest', function () { }); }); - describe('fetchSnapshot', () => { - 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 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 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('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('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('fetches the latest version if the version is undefined', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v3); - done(); - }); + it('fetches the latest version if the version is undefined', function (done) { + backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v3); + done(); }); + }); - 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('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('can call without a callback', function () { + it('throws without a callback', function () { + var fetch = function () { backend.connect().fetchSnapshot('books', 'don-quixote'); + }; + + expect(fetch).to.throwError("Callback is required"); + }); + + it('returns an empty snapshot if the version is -1', function (done) { + backend.connect().fetchSnapshot('books', 'don-quixote', -1, function (error, snapshot) { + if (error) return done(error); + expect(snapshot).to.eql(v0); + done(); }); + }); - it('returns an empty snapshot if the version is -1', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', -1, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v0); - done(); - }); + it('errors if the version is a string', function () { + var fetch = function () { + backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function () { }); + } + + expect(fetch).to.throwError("version must be an integer"); + }); + + 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('errors if the version is a string', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', 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('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('starts pending, and finishes not pending', function (done) { + var connection = backend.connect(); + + connection.fetchSnapshot('books', 'don-quixote', null, function (error, snapshot) { + expect(connection.hasPending()).to.be(false); + 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(); - }); + 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(); }); - it('starts pending, and finishes not pending', function (done) { - var connection = backend.connect(); + expect(connection._snapshotRequests).to.not.eql({}); + }); - connection.fetchSnapshot('books', 'don-quixote', null, function (error, snapshot) { - expect(connection.hasPending()).to.be(false); - done(); - }); + it('emits a ready event when done', function (done) { + var connection = backend.connect(); - expect(connection.hasPending()).to.be(true); + connection.fetchSnapshot('books', 'don-quixote', function (error) { + if (error) return done(error); }); - it('deletes the request from the connection', function (done) { - var connection = backend.connect(); + var snapshotRequest = connection._snapshotRequests[1]; + snapshotRequest.on('ready', done); + }); - connection.fetchSnapshot('books', 'don-quixote', function (error) { - if (error) return done(error); - expect(connection.snapshotRequests).to.eql({}); - done(); - }); + it('fires the connection.whenNothingPending', function (done) { + var connection = backend.connect(); + var snapshotFetched = false; - expect(connection.snapshotRequests).to.not.eql({}); + 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 socket = new StreamSocket(); + var connection = backend.connect(new Connection(socket)); + + connection.fetchSnapshot('books', 'don-quixote', function (error) { + if (error) return done(error); + done(); + }); + + connection.close(); + backend.connect(connection); + }); + describe('readSnapshots middleware', function () { it('triggers the middleware', function (done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, @@ -195,7 +236,7 @@ describe('SnapshotRequest', function () { } ); - backend.connect().fetchSnapshot('books', 'don-quixote'); + backend.connect().fetchSnapshot('books', 'don-quixote', function () { }); }); it('can have its snapshot manipulated in the middleware', function (done) { @@ -276,7 +317,7 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'catch-22', v: 1, - type: json0, + type: 'http://sharejs.org/types/JSONv0', data: { title: 'Catch 22', }, @@ -294,7 +335,7 @@ describe('SnapshotRequest', function () { 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); + if (error) return done(error); doc.create({ title: 'The Restaurant at the End of the Universe' }, function (error) { done(error); }); @@ -309,7 +350,7 @@ describe('SnapshotRequest', function () { expect(snapshot).to.eql({ id: 'hitchhikers-guide', v: 3, - type: json0, + type: 'http://sharejs.org/types/JSONv0', data: { title: 'The Restaurant at the End of the Universe', }, From 0f3d526658a132b31bbef982ea0bac6b57227447 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 2 Aug 2018 13:26:23 +0100 Subject: [PATCH 044/181] Remove VS Code config --- .gitignore | 3 +++ .vscode/launch.json | 31 ------------------------------- 2 files changed, 3 insertions(+), 31 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index 26f870af5..3005c1397 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,9 @@ # Emacs \#*\# +# VS Code +.vscode/ + # Logs logs *.log diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 88e2f008d..000000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Launch Program", - "program": "${workspaceFolder}/lib/index.js" - }, - { - "name": "Unit tests", - "type": "node", - "request": "launch", - "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", - "stopOnEntry": false, - "args": [ - "--no-timeouts", - "--colors", - ], - "cwd": "${workspaceRoot}", - "runtimeExecutable": null, - "env": { - "NODE_ENV": "test" - }, - "sourceMaps": true - }, - ] -} \ No newline at end of file From a39c14121e233fb826b3490fc030c89b1a4a8a3c Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 2 Aug 2018 16:02:33 +0100 Subject: [PATCH 045/181] Simplify reconnection test case --- test/client/snapshot-request.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 304cb1f4d..ab76cc2a3 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -1,7 +1,5 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -var StreamSocket = require('../../lib/stream-socket'); -var Connection = require('../../lib/client/connection'); describe('SnapshotRequest', function () { var backend; @@ -210,8 +208,7 @@ describe('SnapshotRequest', function () { }); it('can drop its connection and reconnect, and the callback is just called once', function (done) { - var socket = new StreamSocket(); - var connection = backend.connect(new Connection(socket)); + var connection = backend.connect(); connection.fetchSnapshot('books', 'don-quixote', function (error) { if (error) return done(error); From b4764ac17707e3c456f557f085ec2e267fe84bbf Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 8 Aug 2018 07:00:24 +0100 Subject: [PATCH 046/181] Update fetchSnapshot documentation --- README.md | 2 +- lib/client/connection.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0b49c2cea..aef1de31e 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ Get a read-only snapshot of a document at the requested version. { id: string; // ID of the snapshot v: number; // version number of the snapshot - type: any; // the OT type of the snapshot, or null if it doesn't exist or is deleted + type: string; // the OT type of the snapshot, or null if it doesn't exist or is deleted data: any; // the snapshot } ``` diff --git a/lib/client/connection.js b/lib/client/connection.js index 4196ed56b..87f6fcc33 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -627,7 +627,7 @@ Connection.prototype._firstSnapshotRequest = function (fn) { * { * id: string; // ID of the snapshot * v: number; // version number of the snapshot - * type: any; // the OT type of the snapshot, or null if it doesn't exist or is deleted + * type: string; // the OT type of the snapshot, or null if it doesn't exist or is deleted * data: any; // the snapshot * } * From e80fe46ec8ea562b8a31aa16fc8e0f598eaf0ec3 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 9 Aug 2018 12:03:47 +0100 Subject: [PATCH 047/181] Review markups This change: - bypasses `_sanitizeOps` and instead uses `_sanitizeSnapshot` - adds timing to `Backend.fetchSnapshot` - throws for negative versions --- lib/backend.js | 56 +++++++++++++++++---------------- lib/client/connection.js | 18 ++++------- lib/client/snapshot-request.js | 21 ++++++++++--- test/client/snapshot-request.js | 37 ++++++++++------------ test/db.js | 8 ++--- 5 files changed, 72 insertions(+), 68 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 674d3af95..60e6d72ed 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -7,6 +7,7 @@ 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 types = require('./types'); @@ -583,35 +584,39 @@ Backend.prototype.getChannels = function(collection, id) { }; Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) { + var start = Date.now(); var backend = this; - this._fetchSnapshot(agent, index, id, version, function (error, snapshot) { - if (error) return callback(error); - - var request = { - collection: index, - id: id, - v: snapshot.v, - snapshots: snapshot.data ? [snapshot.data] : [], - type: snapshot.type - }; + var projection = this.projections[index]; + var collection = projection ? projection.target : index; + var request = { + agent: agent, + index: index, + collection: collection, + id: id, + version: version + }; - backend.trigger(backend.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, function (error) { + this._fetchSnapshot(collection, id, version, function (error, snapshot) { + if (error) return callback(error); + var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); + var snapshots = [snapshot]; + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, function (error) { if (error) return callback(error); - callback(null, { - data: request.snapshots[0], - v: request.v, - type: request.type - }); + backend.emit('timing', 'fetchSnapshot', Date.now() - start, request); + callback(null, snapshot); }); }); }; -Backend.prototype._fetchSnapshot = function (agent, index, id, version, callback) { - this.getOps(agent, index, id, 0, version, function (error, ops) { +Backend.prototype._fetchSnapshot = function (collection, id, version, callback) { + // 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 + this.db.getOps(collection, id, 0, version, null, function (error, ops) { if (error) return callback(error); var type = null; - var snapshot; + var data; var fetchedVersion = 0; for (var index = 0; index < ops.length; index++) { @@ -621,12 +626,12 @@ Backend.prototype._fetchSnapshot = function (agent, index, id, version, callback if (op.create) { type = types.map[op.create.type]; if (!type) return callback({ code: 4008, message: 'Unknown type' }); - snapshot = type.create(op.create.data); + data = type.create(op.create.data); } else if (op.del) { - snapshot = undefined; + data = undefined; type = null; } else { - snapshot = type.apply(snapshot, op.op); + data = type.apply(data, op.op); } } @@ -636,11 +641,8 @@ Backend.prototype._fetchSnapshot = function (agent, index, id, version, callback return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); } - callback(null, { - data: snapshot, - v: fetchedVersion, - type: type - }); + var snapshot = new Snapshot(id, fetchedVersion, type, data, null); + callback(null, snapshot); }); }; diff --git a/lib/client/connection.js b/lib/client/connection.js index 87f6fcc33..da51948be 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -232,7 +232,7 @@ Connection.prototype.handleMessage = function(message) { return this._handleBulkMessage(message, '_handleUnsubscribe'); case 'nf': - return this._handleSnapshot(err, message); + return this._handleSnapshotFetch(err, message); case 'f': var doc = this.getExisting(message.c, message.d); @@ -537,15 +537,12 @@ Connection.prototype.hasPending = function() { return !!( this._firstDoc(hasPending) || this._firstQuery(hasPending) || - this._firstSnapshotRequest(exists) + this._firstSnapshotRequest() ); }; function hasPending(object) { return object.hasPending(); } -function exists(object) { - return !!object; -} Connection.prototype.hasWritePending = function() { return !!this._firstDoc(hasWritePending); @@ -569,7 +566,7 @@ Connection.prototype.whenNothingPending = function(callback) { query.once('ready', this._nothingPendingRetry(callback)); return; } - var snapshotRequest = this._firstSnapshotRequest(exists); + var snapshotRequest = this._firstSnapshotRequest(); if (snapshotRequest) { snapshotRequest.once('ready', this._nothingPendingRetry(callback)); return; @@ -607,12 +604,9 @@ Connection.prototype._firstQuery = function(fn) { } }; -Connection.prototype._firstSnapshotRequest = function (fn) { +Connection.prototype._firstSnapshotRequest = function () { for (var id in this._snapshotRequests) { - var snapshotRequest = this._snapshotRequests[id]; - if (fn(snapshotRequest)) { - return snapshotRequest; - } + return this._snapshotRequests[id]; } }; @@ -644,7 +638,7 @@ Connection.prototype.fetchSnapshot = function(collection, id, version, callback) snapshotRequest.send(); }; -Connection.prototype._handleSnapshot = function (error, message) { +Connection.prototype._handleSnapshotFetch = function (error, message) { var snapshotRequest = this._snapshotRequests[message.id]; if (!snapshotRequest) return; delete this._snapshotRequests[message.id]; diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 8909940e0..54679d0cd 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -11,21 +11,33 @@ function SnapshotRequest(connection, requestId, collection, id, version, callbac throw new Error('Callback is required for SnapshotRequest'); } - if (version != null && !util.isInteger(version)) { - throw new Error('Snapshot version must be an integer'); + if (!this.isValidVersion(version)) { + throw new Error('Snapshot version must be a positive integer or null'); } this.requestId = requestId; this.connection = connection; this.id = id; this.collection = collection; - this.version = util.isInteger(version) ? Math.max(0, version) : null; + this.version = version; this.callback = callback; this.sent = false; } emitter.mixin(SnapshotRequest); +SnapshotRequest.prototype.isValidVersion = function (version) { + if (version === null) { + return true; + } + + if (!util.isInteger(version)) { + return false; + } + + return version >= 0; +} + SnapshotRequest.prototype.send = function () { if (!this.connection.canSend) { return; @@ -52,11 +64,12 @@ SnapshotRequest.prototype._onConnectionStateChanged = function () { }; SnapshotRequest.prototype._handleResponse = function (error, message) { + this.emit('ready'); + if (error) { return this.callback(error); } var snapshot = new Snapshot(this.id, message.v, message.type, message.data, null); this.callback(null, snapshot); - this.emit('ready'); }; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index ab76cc2a3..42d04d237 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -96,13 +96,13 @@ describe('SnapshotRequest', function () { }); }); - it('fetches the latest version if the version is undefined', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v3); - 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) { @@ -117,15 +117,15 @@ describe('SnapshotRequest', function () { backend.connect().fetchSnapshot('books', 'don-quixote'); }; - expect(fetch).to.throwError("Callback is required"); + expect(fetch).to.throwError(); }); - it('returns an empty snapshot if the version is -1', function (done) { - backend.connect().fetchSnapshot('books', 'don-quixote', -1, function (error, snapshot) { - if (error) return done(error); - expect(snapshot).to.eql(v0); - done(); - }); + 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 () { @@ -133,7 +133,7 @@ describe('SnapshotRequest', function () { backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function () { }); } - expect(fetch).to.throwError("version must be an integer"); + expect(fetch).to.throwError(); }); it('errors if asking for a version that does not exist', function (done) { @@ -223,12 +223,7 @@ describe('SnapshotRequest', function () { it('triggers the middleware', function (done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request) { - expect(request.collection).to.be('books'); - expect(request.id).to.be('don-quixote'); - expect(request.v).to.be(3); - expect(request.snapshots).to.eql([v3.data]); - expect(request.type).to.be('http://sharejs.org/types/JSONv0'); - + expect(request.snapshots[0]).to.eql(v3); done(); } ); @@ -239,7 +234,7 @@ describe('SnapshotRequest', function () { it('can have its snapshot manipulated in the middleware', function (done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ function (request, callback) { - request.snapshots[0].title = 'Alice in Wonderland'; + request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); }, ]; diff --git a/test/db.js b/test/db.js index efba94850..257763173 100644 --- a/test/db.js +++ b/test/db.js @@ -789,10 +789,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}, 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 } + {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]]}); From be70a31064e5107be8807c4a80223df34949f17d Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 13 Aug 2018 11:42:30 -0700 Subject: [PATCH 048/181] 1.0.0-beta.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a38e5a65..a49f0a088 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.10", + "version": "1.0.0-beta.11", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 1adbe7452934105c59a80e37b5de9c57384b8cfc Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Tue, 14 Aug 2018 14:46:32 +0100 Subject: [PATCH 049/181] Add Milestone Snapshots This non-breaking change introduces the concept of "Milestone Snapshots". A milestone snapshot is a snapshot of a document for a given version, which is persisted in the database. The purpose of this is to speed up the `Backend.fetchSnapshot` method, which currently has to fetch all the ops required to build a snapshot from v0. Instead, `fetchSnapshot` can now fetch the most recent, relevant milestone snapshot and build on top of that with fewer ops. In order to do this, the database adapter API has been updated to include two new methods: - `saveMilestoneSnapshot(collection, snapshot, callback): void;` stores the provided snapshot against the collection - `getMilestoneSnapshot(collection, id, version, callback): void` fetches the most recent snapshot whose version is equal to or less than the provided `version` (or the most recent version if version is `null`, in keeping with the `to` argument in `getOps`). The adapter also has the responsibility of saving the appropriate milestone snapshots when a new op is committed. --- README.md | 2 + lib/backend.js | 63 ++++--- lib/db/index.js | 14 ++ lib/db/memory.js | 61 ++++++ package.json | 3 +- test/client/snapshot-request.js | 51 +++++ test/db-memory.js | 8 +- test/db.js | 323 ++++++++++++++++++++++++++++++++ 8 files changed, 497 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index aef1de31e..dc45ad892 100644 --- a/README.md +++ b/README.md @@ -420,3 +420,5 @@ 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 - saveMilestoneSnapshot DB method unimplemented +* 5020 - Milestone snapshots are disabled diff --git a/lib/backend.js b/lib/backend.js index 60e6d72ed..756b5a688 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -609,40 +609,53 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) }; Backend.prototype._fetchSnapshot = function (collection, id, version, callback) { - // 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 - this.db.getOps(collection, id, 0, version, null, function (error, ops) { + var db = this.db; + db.getMilestoneSnapshot(collection, id, version, function (error, milestoneSnapshot) { if (error) return callback(error); - var type = null; - var data; - var fetchedVersion = 0; + // 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); - for (var index = 0; index < ops.length; index++) { - var op = ops[index]; - fetchedVersion = op.v + 1; + var type = null; + var data; + var fetchedVersion = 0; - if (op.create) { - type = types.map[op.create.type]; + if (milestoneSnapshot) { + type = types.map[milestoneSnapshot.type]; if (!type) return callback({ code: 4008, message: 'Unknown type' }); - data = type.create(op.create.data); - } else if (op.del) { - data = undefined; - type = null; - } else { - data = type.apply(data, op.op); + data = milestoneSnapshot.data; + fetchedVersion = milestoneSnapshot.v; } - } - type = type ? type.uri : null; + for (var index = 0; index < ops.length; index++) { + var op = ops[index]; + fetchedVersion = op.v + 1; + + if (op.create) { + type = types.map[op.create.type]; + if (!type) return callback({ code: 4008, message: 'Unknown type' }); + data = type.create(op.create.data); + } else if (op.del) { + data = undefined; + type = null; + } else { + data = type.apply(data, op.op); + } + } - if (version > fetchedVersion) { - return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); - } + type = type ? type.uri : null; - var snapshot = new Snapshot(id, fetchedVersion, type, data, null); - callback(null, snapshot); + if (version > fetchedVersion) { + return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); + } + + var snapshot = new Snapshot(id, fetchedVersion, type, data, null); + callback(null, snapshot); + }); }); }; diff --git a/lib/db/index.js b/lib/db/index.js index c5adf8123..e1c24e5be 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -4,6 +4,12 @@ var ShareDBError = require('../error'); function DB(options) { // pollDebounce is the minimum time in ms between query polls this.pollDebounce = options && options.pollDebounce; + + this.milestoneSnapshots = (options && options.milestoneSnapshots) || {}; + // Whether we should store/fetch milestone snapshots + this.milestoneSnapshots.enabled = !!this.milestoneSnapshots.enabled; + // The number of versions to skip before storing the next milestone snapshot + this.milestoneSnapshots.interval = this.milestoneSnapshots.interval || 1000; } module.exports = DB; @@ -103,3 +109,11 @@ DB.prototype.canPollDoc = function() { DB.prototype.skipPoll = function() { return false; }; + +DB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { + callback(null, undefined); +}; + +DB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) { + callback(new ShareDBError(5019, 'saveMilestoneSnapshot DB method unimplemented')); +}; diff --git a/lib/db/memory.js b/lib/db/memory.js index 73a22e6df..b0e9601a1 100644 --- a/lib/db/memory.js +++ b/lib/db/memory.js @@ -1,5 +1,6 @@ var DB = require('./index'); var Snapshot = require('../snapshot'); +var ShareDBError = require('../error'); // In-memory ShareDB database // @@ -22,6 +23,9 @@ function MemoryDB(options) { // the list. this.ops = {}; + // Map form collection name -> doc id -> array of milestone snapshots + this._milestoneSnapshots = {}; + this.closed = false; }; module.exports = MemoryDB; @@ -48,7 +52,16 @@ 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; + + if (db._shouldSaveMilestoneSnapshot(snapshot)) { + return db.saveMilestoneSnapshot(collection, snapshot, function (error) { + if (error) return callback(error); + callback(null, succeeded); + }); + } + callback(null, succeeded); }); }; @@ -171,6 +184,54 @@ MemoryDB.prototype._getVersionSync = function(collection, id) { return (collectionOps && collectionOps[id] && collectionOps[id].length) || 0; }; +MemoryDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { + if (!this.milestoneSnapshots.enabled) { + return callback(null, undefined); + } + + var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); + + let milestoneSnapshot; + for (var i = 0; i < milestoneSnapshots.length; i++) { + var nextMilestoneSnapshot = milestoneSnapshots[i]; + if (nextMilestoneSnapshot.v <= version || version === null) { + milestoneSnapshot = nextMilestoneSnapshot; + } else { + break; + } + } + + callback(null, milestoneSnapshot); +}; + +MemoryDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { + if (!this.milestoneSnapshots.enabled) { + return callback(new ShareDBError(5020, 'Milestone snapshots are disabled')); + } + + if (!snapshot) { + return callback(null); + } + + var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); + milestoneSnapshots.push(snapshot); + milestoneSnapshots.sort(function (a, b) { + return a.v - b.v; + }); + + callback(null); +}; + +MemoryDB.prototype._getMilestoneSnapshotsSync = function (collection, id) { + var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {}); + return collectionSnapshots[id] || (collectionSnapshots[id] = []); +}; + +MemoryDB.prototype._shouldSaveMilestoneSnapshot = function (snapshot) { + return this.milestoneSnapshots.enabled + && snapshot.v % this.milestoneSnapshots.interval === 0; +} + function clone(obj) { return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj)); } diff --git a/package.json b/package.json index a49f0a088..95691a149 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,8 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", - "mocha": "^5.2.0" + "mocha": "^5.2.0", + "sinon": "^6.1.5" }, "scripts": { "test": "./node_modules/.bin/mocha && npm run jshint", diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 42d04d237..7217070f2 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -1,5 +1,7 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); +var MemoryDB = require('../../lib/db/memory'); +var sinon = require('sinon'); describe('SnapshotRequest', function () { var backend; @@ -353,4 +355,53 @@ describe('SnapshotRequest', function () { }); }); }); + + describe('milestone snapshots enabled for every other version', function () { + var db; + + beforeEach(function (done) { + var options = { + milestoneSnapshots: { + enabled: true, + interval: 2 + } + }; + db = new MemoryDB(options); + backend = new Backend({ db: db }); + + var tests = this; + db.saveMilestoneSnapshot('test-implementation', undefined, function (error) { + // Only run this test block if milestone snapshots are implemented on the driver + if (error) { + if (error.code === 5019) return tests.skip(); + if (error.code !== 5020) return done(error); + } + done(); + }); + }); + + it('fetches a snapshot using the milestone', function (done) { + var doc = backend.connect().get('books', 'mocking-bird'); + doc.create({ title: 'To Kill a Mocking Bird' }, function (error) { + if (error) return done(error); + doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, function (error) { + if (error) return done(error); + doc.submitOp({ p: ['author'], od: 'Harper Lea', oi: 'Harper Lee' }, function (error) { + if (error) return done(error); + sinon.spy(db, 'getMilestoneSnapshot'); + sinon.spy(db, 'getOps'); + backend.connect().fetchSnapshot('books', 'mocking-bird', 3, function (error, snapshot) { + if (error) return done(error); + expect(db.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' }); + done(); + }); + }); + }); + }); + }); + }); }); diff --git a/test/db-memory.js b/test/db-memory.js index ccfd2938b..b8b8e064e 100644 --- a/test/db-memory.js +++ b/test/db-memory.js @@ -119,8 +119,12 @@ function snapshotComparator(sortProperties) { // Run all the DB-based tests against the BasicQueryableMemoryDB. require('./db')({ - create: function(callback) { - var db = new BasicQueryableMemoryDB(); + create: function(options, callback) { + if (typeof options === 'function') { + callback = options; + options = null; + } + var db = new BasicQueryableMemoryDB(options); callback(null, db); }, getQuery: function(options) { diff --git a/test/db.js b/test/db.js index 257763173..b56cf0172 100644 --- a/test/db.js +++ b/test/db.js @@ -2,6 +2,7 @@ var async = require('async'); var expect = require('expect.js'); var Backend = require('../lib/backend'); var ot = require('../lib/ot'); +var Snapshot = require('../lib/snapshot'); module.exports = function(options) { var create = options.create; @@ -810,5 +811,327 @@ module.exports = function(options) { }); }); }); + + describe('milestone snapshots', function () { + var db; + var backend; + + beforeEach(function (done) { + var tests = this; + this.db.saveMilestoneSnapshot('test-implementation', undefined, function (error) { + // Only run this test block if milestone snapshots are implemented on the driver + if (error) { + if (error.code === 5019) return tests.skip(); + if (error.code !== 5020) return done(error); + } + done(); + }); + }); + + describe('milestone snapshots not enabled (default)', function () { + beforeEach(function () { + db = this.db; + backend = this.backend; + }); + + it('errors when directly storing milestone snapshots', function (done) { + var snapshot = new Snapshot( + 'catcher-in-the-rye', + 1, + 'http://sharejs.org/types/JSONv0', + { title: 'Catcher in the Rye' }, + null + ); + + db.saveMilestoneSnapshot('books', snapshot, function (error) { + expect(error.code).to.be(5020); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.be(undefined); + done(); + }); + }); + }); + + it('does not store milestone snapshots on commit', function (done) { + var doc = backend.connect().get('books', 'catcher-in-the-rye'); + doc.create({ title: 'Catcher in the Rye' }, function (error) { + if (error) return done(error); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { + expect(retrievedSnapshot).to.be(undefined); + done(); + }); + }); + }); + }); + + describe('milestone snapshots enabled for every version', function () { + beforeEach(function (done) { + var options = { + milestoneSnapshots: { + enabled: true, + interval: 1 + } + }; + + create(options, function (err, createdDb) { + if (err) return done(err); + db = createdDb; + backend = new Backend({ db: createdDb }); + done(); + }); + }); + + afterEach(function (done) { + backend.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 + ); + + db.saveMilestoneSnapshot('books', snapshot, function (error) { + if (error) return done(error); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.eql(snapshot); + 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 + ); + + db.saveMilestoneSnapshot('books', snapshot1, function (error) { + if (error) return done(error); + db.saveMilestoneSnapshot('books', snapshot2, function (error) { + if (error) return done(error); + db.saveMilestoneSnapshot('books', snapshot10, function (error) { + if (error) return done(error); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.eql(snapshot2); + 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 + ); + + db.saveMilestoneSnapshot('books', snapshot2, function (error) { + if (error) return done(error); + db.saveMilestoneSnapshot('books', snapshot1, function (error) { + if (error) return done(error); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.eql(snapshot2); + 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 + ); + + db.saveMilestoneSnapshot('books', snapshot1, function (error) { + if (error) return done(error); + db.saveMilestoneSnapshot('books', snapshot2, function (error) { + if (error) return done(error); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.eql(snapshot2); + done(); + }); + }); + }); + }); + + it('returns undefined if no snapshot exists', function (done) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.be(undefined); + done(); + }); + }); + + it('stores a milestone snapshot on commit', function (done) { + var doc = backend.connect().get('books', 'catcher-in-the-rye'); + doc.create({ title: 'Catcher in the Rye' }, function (error) { + if (error) return done(error); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot.id).to.eql('catcher-in-the-rye'); + expect(retrievedSnapshot.v).to.eql(1); + expect(retrievedSnapshot.type).to.eql('http://sharejs.org/types/JSONv0'); + expect(retrievedSnapshot.data).to.eql({ title: 'Catcher in the Rye' }); + expect(retrievedSnapshot.m).to.be.ok; + done(); + }); + }); + }); + + it('does not error when trying to save an undefined snapshot', function (done) { + db.saveMilestoneSnapshot('books', undefined, done); + }); + }); + + describe('milestone snapshots enabled for every other version', function () { + beforeEach(function (done) { + var options = { + milestoneSnapshots: { + enabled: true, + interval: 2 + } + }; + + create(options, function (err, createdDb) { + if (err) return done(err); + db = createdDb; + backend = new Backend({ db: createdDb }); + done(); + }); + }); + + afterEach(function (done) { + backend.close(done); + }); + + it('only stores even-numbered versions', function (done) { + var doc = backend.connect().get('books', 'catcher-in-the-rye'); + + 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); + }, + function (next) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.be(undefined); + next(); + }); + }, + function (next) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot.v).to.be(2); + next(); + }); + }, + function (next) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot.v).to.be(2); + next(); + }); + }, + function (next) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot.v).to.be(4); + next(); + }); + } + ], done); + }); + + it('can directly store an odd-numbered version', function (done) { + var snapshot = new Snapshot( + 'catcher-in-the-rye', + 1, + 'http://sharejs.org/types/JSONv0', + { title: 'Catcher in the Rye' }, + null + ); + + db.saveMilestoneSnapshot('books', snapshot, function (error) { + if (error) return done(error); + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { + if (error) return done(error); + expect(retrievedSnapshot).to.eql(snapshot); + done(); + }); + }); + }); + }); + }); + + function callInSeries(callbacks, done) { + if (!callbacks.length) return done(); + var callback = callbacks.shift(); + callback(function (error) { + if (error) return done(error); + callInSeries(callbacks, done); + }); + } + }); }; From 8f725229c102a12c7611bfdf290fb99894471972 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 16 Aug 2018 11:47:57 +0100 Subject: [PATCH 050/181] Move milestone snapshots into their own database adapter The concept of milestone snapshots can be completely decoupled from the existing database adapter. This change reflects that concept in code by extracting the milestone snapshot methods from the core database adapter and into their own `MilestoneDB` class. This means that a milestone database could be implemented using a completely separate database from the primary ShareDB database. As well as this change, some tweaks to the interface have been made: - Saving a snapshot does not require a callback. It emits an error if no callback is provided. This allows us to make saving a milestone on commit non-blocking, which avoid slowing down commits. - No default interval is provided for storing milestones. A default could be implemented by any adapters extending the base class, which can choose different defaults depending on the implementation. --- README.md | 2 - lib/backend.js | 4 +- lib/db/index.js | 14 -- lib/db/memory.js | 59 ----- lib/milestone-db/index.js | 39 ++++ lib/milestone-db/memory.js | 61 ++++++ lib/submit-request.js | 8 + test/client/snapshot-request.js | 75 ++++--- test/db.js | 322 --------------------------- test/milestone-db-memory.js | 13 ++ test/milestone-db.js | 372 ++++++++++++++++++++++++++++++++ test/util.js | 21 ++ 12 files changed, 553 insertions(+), 437 deletions(-) create mode 100644 lib/milestone-db/index.js create mode 100644 lib/milestone-db/memory.js create mode 100644 test/milestone-db-memory.js create mode 100644 test/milestone-db.js diff --git a/README.md b/README.md index dc45ad892..aef1de31e 100644 --- a/README.md +++ b/README.md @@ -420,5 +420,3 @@ 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 - saveMilestoneSnapshot DB method unimplemented -* 5020 - Milestone snapshots are disabled diff --git a/lib/backend.js b/lib/backend.js index 756b5a688..7e246641a 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -3,6 +3,7 @@ var Agent = require('./agent'); var Connection = require('./client/connection'); var emitter = require('./emitter'); var MemoryDB = require('./db/memory'); +var MemoryMilestoneDB = require('./milestone-db/memory'); var MemoryPubSub = require('./pubsub/memory'); var ot = require('./ot'); var projections = require('./projections'); @@ -24,6 +25,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 MemoryMilestoneDB(); // Map from projected collection -> {type, fields} this.projections = {}; @@ -610,7 +612,7 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) Backend.prototype._fetchSnapshot = function (collection, id, version, callback) { var db = this.db; - db.getMilestoneSnapshot(collection, id, version, function (error, milestoneSnapshot) { + 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: diff --git a/lib/db/index.js b/lib/db/index.js index e1c24e5be..c5adf8123 100644 --- a/lib/db/index.js +++ b/lib/db/index.js @@ -4,12 +4,6 @@ var ShareDBError = require('../error'); function DB(options) { // pollDebounce is the minimum time in ms between query polls this.pollDebounce = options && options.pollDebounce; - - this.milestoneSnapshots = (options && options.milestoneSnapshots) || {}; - // Whether we should store/fetch milestone snapshots - this.milestoneSnapshots.enabled = !!this.milestoneSnapshots.enabled; - // The number of versions to skip before storing the next milestone snapshot - this.milestoneSnapshots.interval = this.milestoneSnapshots.interval || 1000; } module.exports = DB; @@ -109,11 +103,3 @@ DB.prototype.canPollDoc = function() { DB.prototype.skipPoll = function() { return false; }; - -DB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { - callback(null, undefined); -}; - -DB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) { - callback(new ShareDBError(5019, 'saveMilestoneSnapshot DB method unimplemented')); -}; diff --git a/lib/db/memory.js b/lib/db/memory.js index b0e9601a1..aec53dfa7 100644 --- a/lib/db/memory.js +++ b/lib/db/memory.js @@ -23,9 +23,6 @@ function MemoryDB(options) { // the list. this.ops = {}; - // Map form collection name -> doc id -> array of milestone snapshots - this._milestoneSnapshots = {}; - this.closed = false; }; module.exports = MemoryDB; @@ -54,14 +51,6 @@ MemoryDB.prototype.commit = function(collection, id, op, snapshot, options, call if (err) return callback(err); var succeeded = true; - - if (db._shouldSaveMilestoneSnapshot(snapshot)) { - return db.saveMilestoneSnapshot(collection, snapshot, function (error) { - if (error) return callback(error); - callback(null, succeeded); - }); - } - callback(null, succeeded); }); }; @@ -184,54 +173,6 @@ MemoryDB.prototype._getVersionSync = function(collection, id) { return (collectionOps && collectionOps[id] && collectionOps[id].length) || 0; }; -MemoryDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { - if (!this.milestoneSnapshots.enabled) { - return callback(null, undefined); - } - - var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); - - let milestoneSnapshot; - for (var i = 0; i < milestoneSnapshots.length; i++) { - var nextMilestoneSnapshot = milestoneSnapshots[i]; - if (nextMilestoneSnapshot.v <= version || version === null) { - milestoneSnapshot = nextMilestoneSnapshot; - } else { - break; - } - } - - callback(null, milestoneSnapshot); -}; - -MemoryDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { - if (!this.milestoneSnapshots.enabled) { - return callback(new ShareDBError(5020, 'Milestone snapshots are disabled')); - } - - if (!snapshot) { - return callback(null); - } - - var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); - milestoneSnapshots.push(snapshot); - milestoneSnapshots.sort(function (a, b) { - return a.v - b.v; - }); - - callback(null); -}; - -MemoryDB.prototype._getMilestoneSnapshotsSync = function (collection, id) { - var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {}); - return collectionSnapshots[id] || (collectionSnapshots[id] = []); -}; - -MemoryDB.prototype._shouldSaveMilestoneSnapshot = function (snapshot) { - return this.milestoneSnapshots.enabled - && snapshot.v % this.milestoneSnapshots.interval === 0; -} - function clone(obj) { return (obj === undefined) ? undefined : JSON.parse(JSON.stringify(obj)); } diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js new file mode 100644 index 000000000..b8dc6dd04 --- /dev/null +++ b/lib/milestone-db/index.js @@ -0,0 +1,39 @@ +var emitter = require('../emitter'); + +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) 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) { + callback(null, undefined); +}; + +/** + * @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, wasSaved) => void; + */ +MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { + var saved = false; + if (callback) return callback(null, saved); + this.emit('save', saved, collection, snapshot); +}; diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js new file mode 100644 index 000000000..e085917e1 --- /dev/null +++ b/lib/milestone-db/memory.js @@ -0,0 +1,61 @@ +var MilestoneDB = require('./index'); + +/** + * 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 form collection name -> doc id -> array of milestone snapshots + this._milestoneSnapshots = {}; +} + +MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); + +MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { + var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); + + let milestoneSnapshot; + for (var i = 0; i < milestoneSnapshots.length; i++) { + var nextMilestoneSnapshot = milestoneSnapshots[i]; + if (nextMilestoneSnapshot.v <= version || version === null) { + milestoneSnapshot = nextMilestoneSnapshot; + } else { + break; + } + } + + callback(null, milestoneSnapshot); +}; + +MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { + var saved = false; + if (!snapshot) { + if (callback) return callback(null, saved); + this.emit('save', saved, collection, snapshot); + } + + var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); + milestoneSnapshots.push(snapshot); + milestoneSnapshots.sort(function (a, b) { + return a.v - b.v; + }); + + saved = true; + if (callback) return callback(null, saved); + this.emit('save', saved, collection, snapshot); +}; + +MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function (collection, id) { + var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {}); + return collectionSnapshots[id] || (collectionSnapshots[id] = []); +}; diff --git a/lib/submit-request.js b/lib/submit-request.js index 5ff3d1997..5e196b9b5 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -159,6 +159,9 @@ SubmitRequest.prototype.commit = function(callback) { 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 +219,11 @@ SubmitRequest.prototype._addSnapshotMeta = function() { meta.mtime = this.start; }; +SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function (snapshot) { + return snapshot + && snapshot.v % this.backend.milestoneDb.interval === 0; +}; + // Non-fatal client errors: SubmitRequest.prototype.alreadySubmittedError = function() { return {code: 4001, message: 'Op already submitted'}; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 7217070f2..2ee4fd0b5 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -1,7 +1,9 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -var MemoryDB = require('../../lib/db/memory'); +var MemoryDb = require('../../lib/db/memory'); +var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); var sinon = require('sinon'); +var util = require('../util'); describe('SnapshotRequest', function () { var backend; @@ -357,51 +359,46 @@ describe('SnapshotRequest', function () { }); describe('milestone snapshots enabled for every other version', function () { + var milestoneDb; var db; - beforeEach(function (done) { - var options = { - milestoneSnapshots: { - enabled: true, - interval: 2 - } - }; - db = new MemoryDB(options); - backend = new Backend({ db: db }); - - var tests = this; - db.saveMilestoneSnapshot('test-implementation', undefined, function (error) { - // Only run this test block if milestone snapshots are implemented on the driver - if (error) { - if (error.code === 5019) return tests.skip(); - if (error.code !== 5020) return done(error); - } - done(); + beforeEach(function () { + var options = { interval: 2 }; + db = new MemoryDb(); + milestoneDb = new MemoryMilestoneDb(options); + backend = new Backend({ + db: db, + milestoneDb: milestoneDb }); }); it('fetches a snapshot using the milestone', function (done) { var doc = backend.connect().get('books', 'mocking-bird'); - doc.create({ title: 'To Kill a Mocking Bird' }, function (error) { - if (error) return done(error); - doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, function (error) { - if (error) return done(error); - doc.submitOp({ p: ['author'], od: 'Harper Lea', oi: 'Harper Lee' }, function (error) { - if (error) return done(error); - sinon.spy(db, 'getMilestoneSnapshot'); - sinon.spy(db, 'getOps'); - backend.connect().fetchSnapshot('books', 'mocking-bird', 3, function (error, snapshot) { - if (error) return done(error); - expect(db.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' }); - done(); - }); - }); - }); - }); + + 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'); + backend.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/db.js b/test/db.js index b56cf0172..396557f89 100644 --- a/test/db.js +++ b/test/db.js @@ -811,327 +811,5 @@ module.exports = function(options) { }); }); }); - - describe('milestone snapshots', function () { - var db; - var backend; - - beforeEach(function (done) { - var tests = this; - this.db.saveMilestoneSnapshot('test-implementation', undefined, function (error) { - // Only run this test block if milestone snapshots are implemented on the driver - if (error) { - if (error.code === 5019) return tests.skip(); - if (error.code !== 5020) return done(error); - } - done(); - }); - }); - - describe('milestone snapshots not enabled (default)', function () { - beforeEach(function () { - db = this.db; - backend = this.backend; - }); - - it('errors when directly storing milestone snapshots', function (done) { - var snapshot = new Snapshot( - 'catcher-in-the-rye', - 1, - 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, - null - ); - - db.saveMilestoneSnapshot('books', snapshot, function (error) { - expect(error.code).to.be(5020); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.be(undefined); - done(); - }); - }); - }); - - it('does not store milestone snapshots on commit', function (done) { - var doc = backend.connect().get('books', 'catcher-in-the-rye'); - doc.create({ title: 'Catcher in the Rye' }, function (error) { - if (error) return done(error); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { - expect(retrievedSnapshot).to.be(undefined); - done(); - }); - }); - }); - }); - - describe('milestone snapshots enabled for every version', function () { - beforeEach(function (done) { - var options = { - milestoneSnapshots: { - enabled: true, - interval: 1 - } - }; - - create(options, function (err, createdDb) { - if (err) return done(err); - db = createdDb; - backend = new Backend({ db: createdDb }); - done(); - }); - }); - - afterEach(function (done) { - backend.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 - ); - - db.saveMilestoneSnapshot('books', snapshot, function (error) { - if (error) return done(error); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.eql(snapshot); - 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 - ); - - db.saveMilestoneSnapshot('books', snapshot1, function (error) { - if (error) return done(error); - db.saveMilestoneSnapshot('books', snapshot2, function (error) { - if (error) return done(error); - db.saveMilestoneSnapshot('books', snapshot10, function (error) { - if (error) return done(error); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.eql(snapshot2); - 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 - ); - - db.saveMilestoneSnapshot('books', snapshot2, function (error) { - if (error) return done(error); - db.saveMilestoneSnapshot('books', snapshot1, function (error) { - if (error) return done(error); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.eql(snapshot2); - 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 - ); - - db.saveMilestoneSnapshot('books', snapshot1, function (error) { - if (error) return done(error); - db.saveMilestoneSnapshot('books', snapshot2, function (error) { - if (error) return done(error); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.eql(snapshot2); - done(); - }); - }); - }); - }); - - it('returns undefined if no snapshot exists', function (done) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.be(undefined); - done(); - }); - }); - - it('stores a milestone snapshot on commit', function (done) { - var doc = backend.connect().get('books', 'catcher-in-the-rye'); - doc.create({ title: 'Catcher in the Rye' }, function (error) { - if (error) return done(error); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot.id).to.eql('catcher-in-the-rye'); - expect(retrievedSnapshot.v).to.eql(1); - expect(retrievedSnapshot.type).to.eql('http://sharejs.org/types/JSONv0'); - expect(retrievedSnapshot.data).to.eql({ title: 'Catcher in the Rye' }); - expect(retrievedSnapshot.m).to.be.ok; - done(); - }); - }); - }); - - it('does not error when trying to save an undefined snapshot', function (done) { - db.saveMilestoneSnapshot('books', undefined, done); - }); - }); - - describe('milestone snapshots enabled for every other version', function () { - beforeEach(function (done) { - var options = { - milestoneSnapshots: { - enabled: true, - interval: 2 - } - }; - - create(options, function (err, createdDb) { - if (err) return done(err); - db = createdDb; - backend = new Backend({ db: createdDb }); - done(); - }); - }); - - afterEach(function (done) { - backend.close(done); - }); - - it('only stores even-numbered versions', function (done) { - var doc = backend.connect().get('books', 'catcher-in-the-rye'); - - 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); - }, - function (next) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.be(undefined); - next(); - }); - }, - function (next) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot.v).to.be(2); - next(); - }); - }, - function (next) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot.v).to.be(2); - next(); - }); - }, - function (next) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot.v).to.be(4); - next(); - }); - } - ], done); - }); - - it('can directly store an odd-numbered version', function (done) { - var snapshot = new Snapshot( - 'catcher-in-the-rye', - 1, - 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye' }, - null - ); - - db.saveMilestoneSnapshot('books', snapshot, function (error) { - if (error) return done(error); - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, function (error, retrievedSnapshot) { - if (error) return done(error); - expect(retrievedSnapshot).to.eql(snapshot); - done(); - }); - }); - }); - }); - }); - - function callInSeries(callbacks, done) { - if (!callbacks.length) return done(); - var callback = callbacks.shift(); - callback(function (error) { - if (error) return done(error); - callInSeries(callbacks, done); - }); - } - }); }; 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..9f2c7ec94 --- /dev/null +++ b/test/milestone-db.js @@ -0,0 +1,372 @@ +var expect = require('expect.js'); +var Backend = require('../lib/backend'); +var MilestoneDB = require('../lib/milestone-db'); +var Snapshot = require('../lib/snapshot'); +var util = require('./util'); + +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); + }); + + describe('base class', function () { + beforeEach(function () { + db = new MilestoneDB(); + backend = new Backend({ milestoneDb: db }); + }); + + 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 (wasSaved, next) { + expect(wasSaved).to.be(false); + 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 (saved) { + expect(saved).to.be(false); + done(); + }); + + db.saveMilestoneSnapshot('books', undefined); + }); + }); + + 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 (wasSaved, next) { + expect(wasSaved).to.be(true); + 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 (wasSaved, next) { + db.saveMilestoneSnapshot('books', snapshot2, next); + }, + function (wasSaved, next) { + db.saveMilestoneSnapshot('books', snapshot10, next); + }, + function (wasSaved, 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 (wasSaved, next) { + db.saveMilestoneSnapshot('books', snapshot1, next); + }, + function (wasSaved, 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 (wasSaved, next) { + db.saveMilestoneSnapshot('books', snapshot2, next); + }, + function (wasSaved, next) { + db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); + }, + function (snapshot, next) { + expect(snapshot).to.eql(snapshot2); + next(); + }, + 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 (saved, collection, snapshot) { + expect(saved).to.be(true); + expect(collection).to.be('books'); + expect(snapshot).to.eql(snapshot); + done(); + }); + + db.saveMilestoneSnapshot('books', snapshot); + }); + + it('does not error when the snapshot is undefined', function (done) { + db.saveMilestoneSnapshot('books', undefined, done); + }); + + it('emits an event when a snapshot does not save', function (done) { + db.on('save', function (saved) { + expect(saved).to.be(false); + done(); + }); + + db.saveMilestoneSnapshot('books', undefined); + }); + + 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 (saved, collection, snapshot) { + expect(saved).to.be(true); + 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 (saved, 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); + } + ]); + }); + }); + }); +}; diff --git a/test/util.js b/test/util.js index 2bb7e1114..5f982ed6e 100644 --- a/test/util.js +++ b/test/util.js @@ -39,3 +39,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); +}; From 10902b72ae0362471fdc5409f323430a65f4d2b0 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 16 Aug 2018 12:13:34 +0100 Subject: [PATCH 051/181] Allow milestone snapshot saving logic to be overridden in the middleware This change adds a flag to `SubmitRequest`: `saveMilestoneSnapshot`. This flag defaults to `null`. If this value is left as `null`, then milestone snapshots will be written according to the interval set in the options passed into the provided `MilestoneDB` instance. The value can be overridden in middleware, like in the test case in this commit. If the value of `request.saveMilestoneSnapshot` is changed to a `boolean`, then all interval logic is ignored, and the value of that flag is used directly to determine if a milestone should be saved. This allows more complex milestone logic, such as: - saving a milestone once per day - different intervals depending on collection - non-uniform milestone intervals --- lib/submit-request.js | 13 ++++++++-- test/milestone-db.js | 60 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/lib/submit-request.js b/lib/submit-request.js index 5e196b9b5..ddd9f6f13 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; @@ -220,8 +225,12 @@ SubmitRequest.prototype._addSnapshotMeta = function() { }; SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function (snapshot) { - return snapshot - && snapshot.v % this.backend.milestoneDb.interval === 0; + // 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: diff --git a/test/milestone-db.js b/test/milestone-db.js index 9f2c7ec94..4ea02d556 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -367,6 +367,66 @@ module.exports = function (options) { } ]); }); + + 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 (saved, 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); + } + ]); + }); }); }); }; From e85d59e54e6561c263b914432dae1b546b7dbf83 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 16 Aug 2018 17:50:34 +0100 Subject: [PATCH 052/181] Export milestone db classes --- lib/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/index.js b/lib/index.js index 6bc96ba98..abe82cbc1 100644 --- a/lib/index.js +++ b/lib/index.js @@ -6,7 +6,9 @@ Backend.Backend = Backend; Backend.DB = require('./db'); Backend.Error = require('./error'); 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'); From 37c079745cbc6d31c77ffec0af893290f6864bc4 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Fri, 17 Aug 2018 08:42:45 +0100 Subject: [PATCH 053/181] Review markups This commit makes some changes requested in the [pull request][1]. Namely: - provide the base class of `MilestoneDB` to `Backend` by default to be super-clear that no milestones will be saved by default - making all callbacks call asynchronously using [`process.nextTick`][2] - cosmetic changes [1]: https://github.com/share/sharedb/pull/236 [2]: https://jsblog.insiderattack.net/event-loop-best-practices-nodejs-event-loop-part-5-e29b2b50bfe2 --- lib/backend.js | 4 ++-- lib/db/memory.js | 1 - lib/milestone-db/index.js | 6 +++--- lib/milestone-db/memory.js | 10 +++++----- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 7e246641a..da348ec63 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -3,7 +3,7 @@ var Agent = require('./agent'); var Connection = require('./client/connection'); var emitter = require('./emitter'); var MemoryDB = require('./db/memory'); -var MemoryMilestoneDB = require('./milestone-db/memory'); +var MilestoneDB = require('./milestone-db'); var MemoryPubSub = require('./pubsub/memory'); var ot = require('./ot'); var projections = require('./projections'); @@ -25,7 +25,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 MemoryMilestoneDB(); + this.milestoneDb = options.milestoneDb || new MilestoneDB(); // Map from projected collection -> {type, fields} this.projections = {}; diff --git a/lib/db/memory.js b/lib/db/memory.js index aec53dfa7..9c1a47b56 100644 --- a/lib/db/memory.js +++ b/lib/db/memory.js @@ -1,6 +1,5 @@ var DB = require('./index'); var Snapshot = require('../snapshot'); -var ShareDBError = require('../error'); // In-memory ShareDB database // diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index b8dc6dd04..bc3529e28 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -10,7 +10,7 @@ function MilestoneDB(options) { emitter.mixin(MilestoneDB); MilestoneDB.prototype.close = function(callback) { - if (callback) callback(); + if (callback) process.nextTick(callback); }; /** @@ -23,7 +23,7 @@ MilestoneDB.prototype.close = function(callback) { * the signature (error, snapshot) => void; */ MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { - callback(null, undefined); + process.nextTick(callback, null, undefined); }; /** @@ -34,6 +34,6 @@ MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, */ MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { var saved = false; - if (callback) return callback(null, saved); + if (callback) return process.nextTick(callback, null, saved); this.emit('save', saved, collection, snapshot); }; diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index e085917e1..a06498f44 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -15,7 +15,7 @@ module.exports = MemoryMilestoneDB; function MemoryMilestoneDB(options) { MilestoneDB.call(this, options); - // Map form collection name -> doc id -> array of milestone snapshots + // Map from collection name -> doc id -> array of milestone snapshots this._milestoneSnapshots = {}; } @@ -24,7 +24,7 @@ MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); - let milestoneSnapshot; + var milestoneSnapshot; for (var i = 0; i < milestoneSnapshots.length; i++) { var nextMilestoneSnapshot = milestoneSnapshots[i]; if (nextMilestoneSnapshot.v <= version || version === null) { @@ -34,13 +34,13 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, ver } } - callback(null, milestoneSnapshot); + process.nextTick(callback, null, milestoneSnapshot); }; MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { var saved = false; if (!snapshot) { - if (callback) return callback(null, saved); + if (callback) return process.nextTick(callback, null, saved); this.emit('save', saved, collection, snapshot); } @@ -51,7 +51,7 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh }); saved = true; - if (callback) return callback(null, saved); + if (callback) return process.nextTick(callback, null, saved); this.emit('save', saved, collection, snapshot); }; From 0249c91f1906042cff01415d52a61262bab73453 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 17 Aug 2018 14:14:48 -0700 Subject: [PATCH 054/181] 1.0.0-beta.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 95691a149..4f9aed27b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.11", + "version": "1.0.0-beta.12", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From c60d7a9b165b4f6b671b531b9882efc514edf018 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 20 Aug 2018 11:43:02 +0100 Subject: [PATCH 055/181] Expose `fetchSnapshot` requested version in `readSnapshots` middleware In the `readSnapshots` middleware, it's currently impossible to determine whether a snapshot is being returned as the result of a `fetch` or a `fetchSnaphot`. This change adds a version `v` field to the `request` when the middleware is invoked from `fetchSnapshot`. This field is populated with the value of the requested version, and can be used to determine that the snapshot is the result of a `fetchSnapshot` rather than a `fetch`. --- lib/backend.js | 15 ++++++++++++--- test/client/snapshot-request.js | 3 ++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index da348ec63..9e7f419b3 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -264,7 +264,15 @@ 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, request, callback) { + if (typeof request === 'function') { + callback = request; + request = {}; + } + + request.collection = collection; + request.snapshots = snapshots; + if (projection) { try { projections.projectSnapshots(projection.fields, snapshots); @@ -272,7 +280,7 @@ Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, s return callback(err); } } - var request = {collection: collection, snapshots: snapshots}; + this.trigger(this.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, callback); }; @@ -602,7 +610,8 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, function (error) { + var middlewareRequest = { v: version }; + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, middlewareRequest, function (error) { if (error) return callback(error); backend.emit('timing', 'fetchSnapshot', Date.now() - start, request); callback(null, snapshot); diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 2ee4fd0b5..b4862ec63 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -228,11 +228,12 @@ describe('SnapshotRequest', function () { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request) { expect(request.snapshots[0]).to.eql(v3); + expect(request.v).to.be(3); done(); } ); - backend.connect().fetchSnapshot('books', 'don-quixote', function () { }); + backend.connect().fetchSnapshot('books', 'don-quixote', 3, function () { }); }); it('can have its snapshot manipulated in the middleware', function (done) { From c1f58a5bc80674ad02632668bf6e77de9fc6da67 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 23 Aug 2018 08:32:33 +0100 Subject: [PATCH 056/181] Close milestone database when calling `Backend.close` --- lib/backend.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/backend.js b/lib/backend.js index da348ec63..fcee083b4 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -107,7 +107,7 @@ Backend.prototype._shimAfterSubmit = function() { }; Backend.prototype.close = function(callback) { - var wait = 3; + var wait = 4; var backend = this; function finish(err) { if (err) { @@ -119,6 +119,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); From a676d5e6661f6ecda66d07f1e844279385ddadf4 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 29 Aug 2018 10:20:32 +0100 Subject: [PATCH 057/181] Add a `NoOpMilestoneDB` implementation This change updates our `MilestoneDB` base class to be an empty shell that throws whenever any of its methods are called in order to help implementers. We then also introduce a `NoOpMilestoneDB` implementation, which implements all the methods, but does nothing. This is so that we can drop this implementation into the `Backend` as a sensible default that does nothing at all when called (instead of eg using `MemoryMilestoneDB` and having the surprising behaviour of working until you restart your application). As part of this, the base call signature of the `saveMilestoneSnapshot` callback is changed to remove the `wasSaved` flag. A snapshot should be assumed to have saved if no error is returned in that callback. --- README.md | 2 + lib/backend.js | 4 +- lib/milestone-db/index.js | 13 +-- lib/milestone-db/memory.js | 10 +-- lib/milestone-db/no-op.js | 23 ++++++ test/milestone-db.js | 161 ++++++++++++++++++++++--------------- 6 files changed, 135 insertions(+), 78 deletions(-) create mode 100644 lib/milestone-db/no-op.js diff --git a/README.md b/README.md index aef1de31e..80c36c988 100644 --- a/README.md +++ b/README.md @@ -420,3 +420,5 @@ 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 diff --git a/lib/backend.js b/lib/backend.js index da348ec63..70935121d 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -3,7 +3,7 @@ var Agent = require('./agent'); var Connection = require('./client/connection'); var emitter = require('./emitter'); var MemoryDB = require('./db/memory'); -var MilestoneDB = require('./milestone-db'); +var NoOpMilestoneDB = require('./milestone-db/no-op'); var MemoryPubSub = require('./pubsub/memory'); var ot = require('./ot'); var projections = require('./projections'); @@ -25,7 +25,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 MilestoneDB(); + this.milestoneDb = options.milestoneDb || new NoOpMilestoneDB(); // Map from projected collection -> {type, fields} this.projections = {}; diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index bc3529e28..609afcf60 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -1,4 +1,5 @@ var emitter = require('../emitter'); +var ShareDBError = require('../error'); module.exports = MilestoneDB; function MilestoneDB(options) { @@ -23,17 +24,19 @@ MilestoneDB.prototype.close = function(callback) { * the signature (error, snapshot) => void; */ MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { - process.nextTick(callback, null, undefined); + var error = new ShareDBError(5019, 'getMilestoneSnapshot MilestoneDB method unimplemented'); + if (callback) return process.nextTick(callback, error); + this.emit('error', error); }; /** * @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, wasSaved) => void; + * Should have the signature (error) => void; */ MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { - var saved = false; - if (callback) return process.nextTick(callback, null, saved); - this.emit('save', saved, collection, snapshot); + var error = new ShareDBError(5020, 'saveMilestoneSnapshot MilestoneDB method unimplemented'); + 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 index a06498f44..e77320ba4 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -38,10 +38,9 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, ver }; MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { - var saved = false; if (!snapshot) { - if (callback) return process.nextTick(callback, null, saved); - this.emit('save', saved, collection, snapshot); + if (callback) return process.nextTick(callback, null); + this.emit('save', collection, snapshot); } var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); @@ -50,9 +49,8 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh return a.v - b.v; }); - saved = true; - if (callback) return process.nextTick(callback, null, saved); - this.emit('save', saved, collection, snapshot); + if (callback) return process.nextTick(callback, null); + this.emit('save', collection, snapshot); }; MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function (collection, id) { diff --git a/lib/milestone-db/no-op.js b/lib/milestone-db/no-op.js new file mode 100644 index 000000000..6b51bdfd9 --- /dev/null +++ b/lib/milestone-db/no-op.js @@ -0,0 +1,23 @@ +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) { + process.nextTick(callback, null, undefined); +}; + +NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { + if (callback) return process.nextTick(callback, null); + this.emit('save', collection, snapshot); +}; diff --git a/test/milestone-db.js b/test/milestone-db.js index 4ea02d556..e4403deb4 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -1,9 +1,90 @@ 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', {}); + }); +}); + +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; @@ -24,47 +105,6 @@ module.exports = function (options) { db.close(done); }); - describe('base class', function () { - beforeEach(function () { - db = new MilestoneDB(); - backend = new Backend({ milestoneDb: db }); - }); - - 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 (wasSaved, next) { - expect(wasSaved).to.be(false); - 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 (saved) { - expect(saved).to.be(false); - done(); - }); - - db.saveMilestoneSnapshot('books', undefined); - }); - }); - it('can call close() without a callback', function (done) { create(function (error, db) { if (error) return done(error); @@ -86,8 +126,7 @@ module.exports = function (options) { function (next) { db.saveMilestoneSnapshot('books', snapshot, next); }, - function (wasSaved, next) { - expect(wasSaved).to.be(true); + function (next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, function (retrievedSnapshot, next) { @@ -127,13 +166,13 @@ module.exports = function (options) { function (next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (wasSaved, next) { + function (next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (wasSaved, next) { + function (next) { db.saveMilestoneSnapshot('books', snapshot10, next); }, - function (wasSaved, next) { + function (next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, function (snapshot, next) { @@ -165,10 +204,10 @@ module.exports = function (options) { function (next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (wasSaved, next) { + function (next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (wasSaved, next) { + function (next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, function (snapshot, next) { @@ -200,10 +239,10 @@ module.exports = function (options) { function (next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (wasSaved, next) { + function (next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (wasSaved, next) { + function (next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, function (snapshot, next) { @@ -253,8 +292,7 @@ module.exports = function (options) { null ); - db.on('save', function (saved, collection, snapshot) { - expect(saved).to.be(true); + db.on('save', function (collection, snapshot) { expect(collection).to.be('books'); expect(snapshot).to.eql(snapshot); done(); @@ -263,17 +301,11 @@ module.exports = function (options) { db.saveMilestoneSnapshot('books', snapshot); }); - it('does not error when the snapshot is undefined', function (done) { - db.saveMilestoneSnapshot('books', undefined, done); - }); - - it('emits an event when a snapshot does not save', function (done) { - db.on('save', function (saved) { - expect(saved).to.be(false); + it('errors when the snapshot is undefined', function (done) { + db.saveMilestoneSnapshot('books', undefined, function (error) { + expect(error).to.be.ok; done(); }); - - db.saveMilestoneSnapshot('books', undefined); }); describe('milestones enabled for every version', function () { @@ -289,8 +321,7 @@ module.exports = function (options) { }); it('stores a milestone snapshot on commit', function (done) { - db.on('save', function (saved, collection, snapshot) { - expect(saved).to.be(true); + db.on('save', function (collection, snapshot) { expect(collection).to.be('books'); expect(snapshot.data).to.eql({ title: 'Catcher in the Rye' }); done(); @@ -314,7 +345,7 @@ module.exports = function (options) { }); it('only stores even-numbered versions', function (done) { - db.on('save', function (saved, collection, snapshot) { + db.on('save', function (collection, snapshot) { if (snapshot.v !== 4) return; util.callInSeries([ @@ -374,7 +405,7 @@ module.exports = function (options) { callback(); }); - db.on('save', function (saved, collection, snapshot) { + db.on('save', function (collection, snapshot) { if (snapshot.v !== 4) return; util.callInSeries([ From f2b30f1811a7695289454a503b6f27c181e0614e Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 29 Aug 2018 11:10:53 +0100 Subject: [PATCH 058/181] Add argument checking tests for MilestoneDB --- lib/milestone-db/index.js | 6 +++ lib/milestone-db/memory.js | 20 +++++++--- test/milestone-db.js | 80 +++++++++++++++++++++++++++++++++++++- 3 files changed, 99 insertions(+), 7 deletions(-) diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index 609afcf60..ed409a115 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -1,5 +1,6 @@ var emitter = require('../emitter'); var ShareDBError = require('../error'); +var util = require('../util'); module.exports = MilestoneDB; function MilestoneDB(options) { @@ -40,3 +41,8 @@ MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, ca if (callback) return process.nextTick(callback, error); this.emit('error', error); }; + +MilestoneDB.prototype._isValidVersion = function (version) { + if (version == null) return true; + return util.isInteger(version) && version >= 0; +}; diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index e77320ba4..28d58c3ec 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -1,4 +1,5 @@ var MilestoneDB = require('./index'); +var ShareDBError = require('../error'); /** * In-memory ShareDB milestone database @@ -22,12 +23,17 @@ function MemoryMilestoneDB(options) { MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { + if (!callback) callback = function () {}; + if (!collection) return process.nextTick(callback, new ShareDBError(4001, 'Invalid collection')); + if (!id) return process.nextTick(callback, new ShareDBError(4001, 'Invalid ID')); + if (!this._isValidVersion(version)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid version')); + var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); var milestoneSnapshot; for (var i = 0; i < milestoneSnapshots.length; i++) { var nextMilestoneSnapshot = milestoneSnapshots[i]; - if (nextMilestoneSnapshot.v <= version || version === null) { + if (nextMilestoneSnapshot.v <= version || version == null) { milestoneSnapshot = nextMilestoneSnapshot; } else { break; @@ -38,10 +44,13 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, ver }; MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { - if (!snapshot) { - if (callback) return process.nextTick(callback, null); + 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, 'Invalid collection')); + if (!snapshot) return callback(new ShareDBError(4001, 'Invalid snapshot')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); milestoneSnapshots.push(snapshot); @@ -49,8 +58,7 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh return a.v - b.v; }); - if (callback) return process.nextTick(callback, null); - this.emit('save', collection, snapshot); + process.nextTick(callback, null); }; MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function (collection, id) { diff --git a/test/milestone-db.js b/test/milestone-db.js index e4403deb4..b26cf755b 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -253,6 +253,84 @@ module.exports = function (options) { ]); }); + it('fetches the most recent snapshot when the version is undefined', 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', undefined, next); + }, + function (snapshot, next) { + expect(snapshot).to.eql(snapshot2); + next(); + }, + 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) { @@ -303,7 +381,7 @@ module.exports = function (options) { it('errors when the snapshot is undefined', function (done) { db.saveMilestoneSnapshot('books', undefined, function (error) { - expect(error).to.be.ok; + expect(error).to.be.ok(); done(); }); }); From cb94f33c9384cbdf3343e5bc94c2b463e3bfb96d Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 30 Aug 2018 08:32:16 +0100 Subject: [PATCH 059/181] Commonise valid version logic This change introduces a `util.isValidVersion` method, which is common between `SnapshotRequest` and `MilestoneDB`, and makes sure we're consistent in what constitutes a valid version. We check that versions are either `null` or a positive integer. We reject `undefined`, based on the idea that `null` means that you meant to set something to `null`, but with `undefined` that may not be the case. It's also consistent with the version that can be passed into `getOps` (ie `null` has the special meaning of "please give me the latest version"). --- lib/client/snapshot-request.js | 14 +------------ lib/milestone-db/index.js | 3 +-- lib/milestone-db/memory.js | 10 ++++----- lib/milestone-db/no-op.js | 3 ++- lib/util.js | 5 +++++ test/milestone-db.js | 38 +++++----------------------------- 6 files changed, 19 insertions(+), 54 deletions(-) diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 54679d0cd..4fba42410 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -11,7 +11,7 @@ function SnapshotRequest(connection, requestId, collection, id, version, callbac throw new Error('Callback is required for SnapshotRequest'); } - if (!this.isValidVersion(version)) { + if (!util.isValidVersion(version)) { throw new Error('Snapshot version must be a positive integer or null'); } @@ -26,18 +26,6 @@ function SnapshotRequest(connection, requestId, collection, id, version, callbac } emitter.mixin(SnapshotRequest); -SnapshotRequest.prototype.isValidVersion = function (version) { - if (version === null) { - return true; - } - - if (!util.isInteger(version)) { - return false; - } - - return version >= 0; -} - SnapshotRequest.prototype.send = function () { if (!this.connection.canSend) { return; diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index ed409a115..581f1ecc9 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -43,6 +43,5 @@ MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, ca }; MilestoneDB.prototype._isValidVersion = function (version) { - if (version == null) return true; - return util.isInteger(version) && version >= 0; + return util.isValidVersion(version); }; diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index 28d58c3ec..f239c3f9a 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -24,8 +24,8 @@ MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { if (!callback) callback = function () {}; - if (!collection) return process.nextTick(callback, new ShareDBError(4001, 'Invalid collection')); - if (!id) return process.nextTick(callback, new ShareDBError(4001, 'Invalid ID')); + if (!collection) return process.nextTick(callback, new ShareDBError(4001, 'Missing collection')); + if (!id) return process.nextTick(callback, new ShareDBError(4001, 'Missing ID')); if (!this._isValidVersion(version)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid version')); var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); @@ -33,7 +33,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, ver var milestoneSnapshot; for (var i = 0; i < milestoneSnapshots.length; i++) { var nextMilestoneSnapshot = milestoneSnapshots[i]; - if (nextMilestoneSnapshot.v <= version || version == null) { + if (nextMilestoneSnapshot.v <= version || version === null) { milestoneSnapshot = nextMilestoneSnapshot; } else { break; @@ -49,8 +49,8 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh this.emit('save', collection, snapshot); }.bind(this); - if (!collection) return callback(new ShareDBError(4001, 'Invalid collection')); - if (!snapshot) return callback(new ShareDBError(4001, 'Invalid snapshot')); + 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); diff --git a/lib/milestone-db/no-op.js b/lib/milestone-db/no-op.js index 6b51bdfd9..77204f7ec 100644 --- a/lib/milestone-db/no-op.js +++ b/lib/milestone-db/no-op.js @@ -14,7 +14,8 @@ function NoOpMilestoneDB(options) { NoOpMilestoneDB.prototype = Object.create(MilestoneDB.prototype); NoOpMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { - process.nextTick(callback, null, undefined); + var snapshot = undefined; + process.nextTick(callback, null, snapshot); }; NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { diff --git a/lib/util.js b/lib/util.js index 2817765a7..8f3c48804 100644 --- a/lib/util.js +++ b/lib/util.js @@ -13,3 +13,8 @@ exports.isInteger = Number.isInteger || function (value) { isFinite(value) && Math.floor(value) === value; }; + +exports.isValidVersion = function (version) { + if (version === null) return true; + return exports.isInteger(version) && version >= 0; +}; diff --git a/test/milestone-db.js b/test/milestone-db.js index b26cf755b..92195c71f 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -253,39 +253,11 @@ module.exports = function (options) { ]); }); - it('fetches the most recent snapshot when the version is undefined', 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', undefined, 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) { From f7dcb37c6c03b1f92e5a75981b1d30619b8c517e Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 30 Aug 2018 10:05:13 +0100 Subject: [PATCH 060/181] Improve test coverage for `SnapshotRequest.onConnectionStateChanged` This change improves `SnapshotRequest` test coverage to 100%. Our current test for checking that a dropped connection works didn't actually fail properly when removing `this.sent = false` from `SnapshotRequest._onConnectionStateChanged`, so an improved test has been written that use middleware to wait until the request has definitely been sent, before dropping the connection and reconnecting. It also adds a previously uncovered test case for checking that the connection cannot send the same request multiple times, by using a similar setup to the rewritten dropped connection case. This change also adds some documentation recording why we might want to reset `this.sent = false`, because just looking at the code it's unclear why we would ever want to do that. --- lib/client/snapshot-request.js | 9 ++++++--- test/client/snapshot-request.js | 31 ++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request.js index 54679d0cd..aa22b272b 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request.js @@ -56,9 +56,12 @@ SnapshotRequest.prototype.send = function () { }; SnapshotRequest.prototype._onConnectionStateChanged = function () { - if (this.connection.canSend && !this.sent) { - this.send(); - } else if (!this.connection.canSend) { + 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; } }; diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 2ee4fd0b5..80adc113e 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -214,13 +214,34 @@ describe('SnapshotRequest', function () { it('can drop its connection and reconnect, and the callback is just called once', function (done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', function (error) { - if (error) return done(error); - done(); + 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(); + + var hasResent = false; + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) { + if (!hasResent) { + connection._snapshotRequests[1]._onConnectionStateChanged(); + hasResent = true; + } + + callback(); }); - connection.close(); - backend.connect(connection); + connection.fetchSnapshot('books', 'don-quixote', done); }); describe('readSnapshots middleware', function () { From 41f2395ec802170b37cfab9236c35c4ee1a81bdc Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Fri, 31 Aug 2018 08:22:05 +0100 Subject: [PATCH 061/181] Add a snapshot type to the `readSnapshots` middleware Rather than pass the requested version into the `readSnapshots` middleware as an obscure way of determining if a snapshot is current or historical, this change adds an explicit `snapshotType` parameter to the middleware request, which states whether the snapshot is current or historical. --- lib/backend.js | 33 +++++++++++++++++++-------------- lib/query-emitter.js | 6 ++++-- test/client/snapshot-request.js | 2 +- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 9e7f419b3..b48f38a0d 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -77,6 +77,13 @@ Backend.prototype.MIDDLEWARE_ACTIONS = { submit: 'submit' }; +Backend.prototype.SNAPSHOT_TYPES = { + // The current snapshot is being fetched (eg through backend.fetch) + current: 'current', + // An historical snapshot is being fetched (eg through backend.fetchSnapshot) + historical: 'historical' +}; + Backend.prototype._shimDocAction = function() { if (warnDeprecatedDoc) { warnDeprecatedDoc = false; @@ -264,15 +271,7 @@ Backend.prototype._sanitizeOpsBulk = function(agent, projection, collection, ops }, callback); }; -Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, request, callback) { - if (typeof request === 'function') { - callback = request; - request = {}; - } - - request.collection = collection; - request.snapshots = snapshots; - +Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, snapshotType, callback) { if (projection) { try { projections.projectSnapshots(projection.fields, snapshots); @@ -281,6 +280,12 @@ Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, s } } + var request = { + collection: collection, + snapshots: snapshots, + snapshotType: snapshotType + }; + this.trigger(this.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, callback); }; @@ -360,7 +365,7 @@ Backend.prototype.fetch = function(agent, index, id, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, function(err) { + 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); @@ -384,7 +389,7 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { 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) { + 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); @@ -572,7 +577,7 @@ 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) { + backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) { callback(err, snapshots, extra); }); }); @@ -610,8 +615,8 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - var middlewareRequest = { v: version }; - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, middlewareRequest, function (error) { + var snapshotType = backend.SNAPSHOT_TYPES.historical; + 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); diff --git a/lib/query-emitter.js b/lib/query-emitter.js index 9599c71cf..f79c9a684 100644 --- a/lib/query-emitter.js +++ b/lib/query-emitter.js @@ -186,7 +186,8 @@ 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) { + 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); @@ -234,7 +235,8 @@ 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) { + 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); diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index b4862ec63..5b8ba1218 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -228,7 +228,7 @@ describe('SnapshotRequest', function () { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request) { expect(request.snapshots[0]).to.eql(v3); - expect(request.v).to.be(3); + expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.historical); done(); } ); From 11d982db2ccb5659d5a783623bb1747d3197589d Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 5 Sep 2018 17:42:32 +0100 Subject: [PATCH 062/181] Rename Snapshot middleware "historical" flag to "byVersion" Calling a snapshot "historical" might lead people to assume that it's not the current version, which may not be the case (for example, if you fetch the current version by its ID with `fetchSnapshot` or, pass in a version of `null`). This change updates the snapshot middleware to use the more descriptive `byVersion`, which indicates that the snapshot was fetched by version. --- lib/backend.js | 6 +++--- test/client/snapshot-request.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index b48f38a0d..1e76cc51c 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -80,8 +80,8 @@ Backend.prototype.MIDDLEWARE_ACTIONS = { Backend.prototype.SNAPSHOT_TYPES = { // The current snapshot is being fetched (eg through backend.fetch) current: 'current', - // An historical snapshot is being fetched (eg through backend.fetchSnapshot) - historical: 'historical' + // A specific snapshot is being fetched by version (eg through backend.fetchSnapshot) + byVersion: 'byVersion' }; Backend.prototype._shimDocAction = function() { @@ -615,7 +615,7 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - var snapshotType = backend.SNAPSHOT_TYPES.historical; + 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); diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 5b8ba1218..50c7f60ad 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -228,7 +228,7 @@ describe('SnapshotRequest', function () { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request) { expect(request.snapshots[0]).to.eql(v3); - expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.historical); + expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byVersion); done(); } ); From c23ab1af2cb7e493d56bba9016688bb19c072741 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 5 Sep 2018 13:38:07 -0700 Subject: [PATCH 063/181] 1.0.0-beta.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f9aed27b..b57b01092 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.12", + "version": "1.0.0-beta.13", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 70cb681fcc78c96306a47d0fdf0aba074cc3704e Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 6 Sep 2018 08:48:51 +0100 Subject: [PATCH 064/181] Document complicated snapshot request testing logic --- test/client/snapshot-request.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/client/snapshot-request.js b/test/client/snapshot-request.js index 80adc113e..8711bf7b5 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-request.js @@ -214,6 +214,15 @@ describe('SnapshotRequest', function () { 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) { @@ -231,6 +240,13 @@ describe('SnapshotRequest', function () { 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) { From 82386dbcbe453df5b95b93f4ef78af0ce6f9d1f3 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 19 Sep 2018 09:40:27 -0700 Subject: [PATCH 065/181] 1.0.0-beta.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b57b01092..f88d40387 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.13", + "version": "1.0.0-beta.14", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 674e07f6a49a8d1c4a75766ee16a4763ce889f67 Mon Sep 17 00:00:00 2001 From: LWio Date: Sun, 21 Oct 2018 20:01:52 +0800 Subject: [PATCH 066/181] Fix: correct a link of README the `reconnecting-websocket` url is not right --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 80c36c988..a419b8b18 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ The native Websocket object that you feed to ShareDB's `Connection` constructor 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 [https://github.com/pladaria/reconnecting-websocket](reconnecting-websocket). +In the "textarea" example we show this off using a Reconnecting Websocket implementation from [reconnecting-websocket](https://github.com/pladaria/reconnecting-websocket). From 065ed9dc5c9f3246310d6c79d08650bc197b2b8f Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Mon, 5 Nov 2018 11:50:09 +0000 Subject: [PATCH 067/181] Remove some unnecessary code --- lib/client/doc.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 2c60ee25e..ce152cd8e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -204,10 +204,6 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { }; Doc.prototype.whenNothingPending = function(callback) { - if (this.hasPending()) { - this.once('nothing pending', callback); - return; - } var doc = this; process.nextTick(function() { if (doc.hasPending()) { From a67ad42c384809eb377c9f70c7a45a7552a4be89 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Mon, 5 Nov 2018 16:41:15 -0800 Subject: [PATCH 068/181] Clarify README for `doc.ingestSnapshot(snapshot, callback)` Addresses documentation issue raised in https://github.com/share/sharedb/issues/254 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a419b8b18..e8c86d3d5 100644 --- a/README.md +++ b/README.md @@ -267,7 +267,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. From 002c4c2cce60001c946d80d8aad92b3d0611826f Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 15 Nov 2018 10:23:42 +0000 Subject: [PATCH 069/181] Allow custom logger overrides This change adds the ability to override ShareDB's logging behaviour. By default, ShareDB will still log to `console`. However, this default can be overridden with custom methods on both the backend and in the client. ## Supported methods The ShareDB logger only supports the following methods: - `info` - `warn` - `error` Any method that is not overridden will default to `console`. ## Backend The backend methods can be overridden when instantiating `Backend`: ```javascript var share = new Backend({ logger: { info: () => {}, // Silence info warn: () => alerts.warn(arguments), // Forward warnings error: () => alerts.critical(arguments) // Map errors to critical } }); ``` ## Client Client methods can also be overridden: ```javascript var ShareDB = require('sharedb/lib/client'); ShareDB.logger.setMethods({ info: () => {}, // etc. }); ``` --- README.md | 43 +++++++++++++++++++++++++++++++++++++ lib/agent.js | 9 ++++---- lib/backend.js | 3 +++ lib/client/connection.js | 11 +++++----- lib/client/doc.js | 3 ++- lib/client/index.js | 1 + lib/logger/index.js | 3 +++ lib/logger/logger.js | 21 ++++++++++++++++++ lib/stream-socket.js | 3 ++- test/logger.js | 46 ++++++++++++++++++++++++++++++++++++++++ 10 files changed, 132 insertions(+), 11 deletions(-) create mode 100644 lib/logger/index.js create mode 100644 lib/logger/logger.js create mode 100644 test/logger.js diff --git a/README.md b/README.md index e8c86d3d5..0a12c5a15 100644 --- a/README.md +++ b/README.md @@ -182,6 +182,28 @@ 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 `Backend`: + +```javascript +var share = new Backend({ + logger: { + 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)` @@ -358,6 +380,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 diff --git a/lib/agent.js b/lib/agent.js index 3ef558362..0bd0b496a 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -1,6 +1,7 @@ var hat = require('hat'); var util = require('./util'); var types = require('./types'); +var logger = require('./logger'); /** * Agent deserializes the wire protocol messages received from the stream and @@ -47,7 +48,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' @@ -95,7 +96,7 @@ 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 (agent._isOwnOp(collection, data)) return; @@ -137,7 +138,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) { @@ -195,7 +196,7 @@ Agent.prototype._reply = function(request, err, message) { }; } else { if (err.stack) { - console.warn(err.stack); + logger.warn(err.stack); } request.error = { code: err.code, diff --git a/lib/backend.js b/lib/backend.js index e766202ec..4c0d624d7 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -2,6 +2,7 @@ var async = require('async'); var Agent = require('./agent'); var Connection = require('./client/connection'); var emitter = require('./emitter'); +var logger = require('./logger'); var MemoryDB = require('./db/memory'); var NoOpMilestoneDB = require('./milestone-db/no-op'); var MemoryPubSub = require('./pubsub/memory'); @@ -40,6 +41,8 @@ function Backend(options) { this.agentsCount = 0; this.remoteAgentsCount = 0; + logger.setMethods(options.logger); + // The below shims are for backwards compatibility. These options will be // removed in a future major version if (!options.disableDocAction) { diff --git a/lib/client/connection.js b/lib/client/connection.js index da51948be..9a2a1fef7 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -5,6 +5,7 @@ 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'; @@ -116,11 +117,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); @@ -252,7 +253,7 @@ Connection.prototype.handleMessage = function(message) { return; default: - console.warn('Ignoring unrecognized message', message); + logger.warn('Ignoring unrecognized message', message); } }; @@ -274,7 +275,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); } }; @@ -426,7 +427,7 @@ Connection.prototype.sendOp = function(doc, op) { * 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)); diff --git a/lib/client/doc.js b/lib/client/doc.js index 71f4e2041..eaff16237 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -1,4 +1,5 @@ var emitter = require('../emitter'); +var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); @@ -832,7 +833,7 @@ Doc.prototype._opAcknowledged = function(message) { } 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(); 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/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..6d193e169 --- /dev/null +++ b/lib/logger/logger.js @@ -0,0 +1,21 @@ +var SUPPORTED_METHODS = [ + 'info', + 'warn', + 'error' +]; + +function Logger() { + this.setMethods(console); +} +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/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/test/logger.js b/test/logger.js new file mode 100644 index 000000000..aba680eb7 --- /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); + }); + }); +}); From d41e0347b4b6e6ea1e0a7d8d01bb9292e593cb1b Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 15 Nov 2018 16:01:27 +0000 Subject: [PATCH 070/181] Catch synchronous errors in `Doc._otApply` Fixes https://github.com/share/sharedb/issues/257 As outlined in the above issue, calling `Doc.submitOp` can sometimes `throw` a synchronous error, for example when submitting an invalid op that causes `type.apply` to `throw`. This is surprising behaviour, because an error handler should already be provided in the callback supplied to `submitOp`. What's more, the doc could be left partially mutated and out-of-sync with the server, which could lead to confusing behaviour. This change wraps all internal usage of `Doc._otApply` in `try`/`catch` blocks. If an error is thrown in this method, then we attempt to recover with `Doc._hardRollback`, which will attempt to reset the document and then call the callbacks of the pending ops with the error. This change also prevents `Doc._hardRollback` from emitting an error if at least one pending op callback was invoked, so that we don't branch error handling behaviour. --- lib/client/doc.js | 27 ++++++--- test/client/doc.js | 130 ++++++++++++++++++++++++++++++++++++++++++ test/client/submit.js | 18 ++---- 3 files changed, 153 insertions(+), 22 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 71f4e2041..35c0e13aa 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -326,8 +326,11 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this._otApply(message, false); - return; + try { + this._otApply(message, false); + } catch (error) { + return this._hardRollback(error); + } }; // Called whenever (you guessed it!) the connection state changes. This will @@ -511,8 +514,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 @@ -654,8 +657,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. @@ -866,7 +873,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; @@ -889,7 +900,7 @@ Doc.prototype._hardRollback = function(err) { this.fetch(function() { var called = op && callEach(op.callbacks, err); for (var i = 0; i < pending.length; i++) { - callEach(pending[i].callbacks, err); + called = callEach(pending[i].callbacks, err) || called; } if (err && !called) return doc.emit('error', err); }); diff --git a/test/client/doc.js b/test/client/doc.js index b44f52a2b..f7529d71e 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,5 +1,6 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); +var util = require('../util') describe('client query subscribe', function() { @@ -212,4 +213,133 @@ describe('client query subscribe', function() { }); + 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/submit.js b/test/client/submit.js index 749386d66..82cecbbe3 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -629,16 +629,11 @@ describe('client submit', function() { doc2.del(function(err) { if (err) return done(err); doc.pause(); - var calledBack = false; - doc.on('error', function() { - expect(calledBack).equal(true); - done(); - }); doc.submitOp({p: ['age'], na: 1}, function(err) { - expect(err).ok(); + expect(err.code).to.equal(4017); expect(doc.version).equal(2); expect(doc.data).eql(undefined); - calledBack = true; + done(); }); doc.fetch(); }); @@ -658,16 +653,11 @@ describe('client submit', function() { doc2.create({age: 5}, function(err) { if (err) return done(err); doc.pause(); - var calledBack = false; - doc.on('error', function() { - expect(calledBack).equal(true); - done(); - }); doc.create({age: 9}, function(err) { - expect(err).ok(); + expect(err.code).to.equal(4018); expect(doc.version).equal(3); expect(doc.data).eql({age: 5}); - calledBack = true; + done(); }); doc.fetch(); }); From 1992b47c9cb649cd12b52e5bcd17a63ea4613e42 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 22 Nov 2018 08:34:37 +0000 Subject: [PATCH 071/181] Move logger config out of `Backend` constructor Configuring a package-level logger through an instance constructor is a bit weird and potentially unexpected. This change moves the backend logger configuration to the static level, similar to how it's already set up for the client. Configuration at static level should indicate that these changes will apply across all backend instances. --- README.md | 19 +++++++++---------- lib/backend.js | 3 --- lib/index.js | 1 + 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 0a12c5a15..afc273bec 100644 --- a/README.md +++ b/README.md @@ -186,15 +186,14 @@ Note that only the [JSON0 OT type](https://github.com/ottypes/json0) is supporte 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 `Backend`: +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 share = new Backend({ - logger: { - info: () => {}, // Silence info - warn: () => alerts.warn(arguments), // Forward warnings to alerting service - error: () => alerts.critical(arguments) // Remap errors to critical alerts - } +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 }); ``` @@ -389,9 +388,9 @@ Methods can be overridden by passing a [`console`-like object](https://developer ```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 + info: () => {}, // Silence info + warn: () => alerts.warn(arguments), // Forward warnings to alerting service + error: () => alerts.critical(arguments) // Remap errors to critical alerts }); ``` diff --git a/lib/backend.js b/lib/backend.js index 4c0d624d7..e766202ec 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -2,7 +2,6 @@ var async = require('async'); var Agent = require('./agent'); var Connection = require('./client/connection'); var emitter = require('./emitter'); -var logger = require('./logger'); var MemoryDB = require('./db/memory'); var NoOpMilestoneDB = require('./milestone-db/no-op'); var MemoryPubSub = require('./pubsub/memory'); @@ -41,8 +40,6 @@ function Backend(options) { this.agentsCount = 0; this.remoteAgentsCount = 0; - logger.setMethods(options.logger); - // The below shims are for backwards compatibility. These options will be // removed in a future major version if (!options.disableDocAction) { diff --git a/lib/index.js b/lib/index.js index abe82cbc1..df4b3f0a6 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,6 +5,7 @@ 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'); From 84a70afdd879b5392f51b1f42c67a38220a34340 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 22 Nov 2018 09:37:25 +0000 Subject: [PATCH 072/181] Update hard rollback callback logic We previously followed the following logic for callbacks in a hard rollback: - check there's an inflight op, and that it has a callback - if this isn't the case, then emit the error This logic doesn't work very well in the situation where we don't have an inflight op. This can happen in the following case: - I submit an invalid op - the op is added to the pending ops queue - we attempt to apply the op to the local document before flushing - `type.apply` throws, so the op is never flushed from pending to inflight - we perform a hard rollback - we now find we have no inflight op, but we do have a pending op In this case, since we have no inflight op, we'd emit the error, but also call the pending op callback, causing both a callback call _and_ an error emission, which is a bit surprising. This change updates our logic, so that: - we check that there are some ops to callback - and that all of those ops have callbacks If there are no callbacks, or if any of the ops don't handle the error, then we emit, so that we can be sure of never swallowing an error. We also avoid treating the inflight op any differently to pending ops, so that we avoid behaviour that's dependent on where in its lifecycle an op throws. However, if the client handles all of their op submission errors, then we never emit. --- lib/client/doc.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 35c0e13aa..e648ac849 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -887,22 +887,34 @@ Doc.prototype._rollback = function(err) { }; Doc.prototype._hardRollback = function(err) { + // 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); + // Cancel all pending ops and reset if we can't invert - var op = this.inflightOp; - var pending = this.pendingOps; this._setType(null); this.version = null; this.inflightOp = null; this.pendingOps = []; - // 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 = op && callEach(op.callbacks, err); - for (var i = 0; i < pending.length; i++) { - called = callEach(pending[i].callbacks, err) || called; + // 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 + // If there are no ops queued, or one of them didn't handle the error, + // then we emit the error. + var allOpsHadCallbacks = !!pendingOps.length; + for (var i = 0; i < pendingOps.length; i++) { + allOpsHadCallbacks = callEach(pendingOps[i].callbacks, err) && allOpsHadCallbacks; } - if (err && !called) return doc.emit('error', err); + if (err && !allOpsHadCallbacks) return doc.emit('error', err); }); }; From b46cc6d7cecb8d564852e6f038d178f586a9d1a1 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 22 Nov 2018 18:01:06 -0600 Subject: [PATCH 073/181] 1.0.0-beta.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f88d40387..804db11fb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.14", + "version": "1.0.0-beta.15", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 88bb142d0a8ff1ac3d195c13a9148135c63d90e9 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 28 Nov 2018 11:17:22 -0600 Subject: [PATCH 074/181] 1.0.0-beta.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 804db11fb..244b7d055 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.15", + "version": "1.0.0-beta.16", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 41c40f6416dc379f57bc8fa13557ee931e3fa033 Mon Sep 17 00:00:00 2001 From: David Charbonnier Date: Fri, 30 Nov 2018 13:45:21 +0100 Subject: [PATCH 075/181] fix leak on duplicate subscription - #260 --- lib/agent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/agent.js b/lib/agent.js index 0bd0b496a..12d6f3cf5 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -105,7 +105,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { stream.on('end', function() { // 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]; From 713a8ffe908ad2fd7763312ed82943728c19ec66 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 30 Nov 2018 17:15:28 -0800 Subject: [PATCH 076/181] Add test for re-subscribe memory leak When a doc subscription stream ends, it's supposed to be removed from `agent.subscribedDocs[collection][docId]`. When subscribing to a doc that's already subscribed to, the old stream gets closed, and the new stream gets set onto the above location, which acts as removing the old stream. However, prior to commit 41c40f6, the new stream actually got removed, which means the agent loses its reference to the stream. This test for that scenario fails prior to 41c40f6 and passes with 41c40f6. --- test/client/connection.js | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/client/connection.js b/test/client/connection.js index 677e87e44..e6a9aedfd 100644 --- a/test/client/connection.js +++ b/test/client/connection.js @@ -56,6 +56,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) { From 95c81b841309bd5fd99e01677de9c49ccb33ee9e Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Fri, 30 Nov 2018 17:20:32 -0800 Subject: [PATCH 077/181] 1.0.0-beta.17 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 244b7d055..876447bef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 4a7a1781139403a81802a1b3b6613748b66b26a1 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Fri, 30 Nov 2018 17:09:17 +0000 Subject: [PATCH 078/181] Fetch snapshot by time This change adds the ability fetch a snapshot by time. The motivation for this is that fetching a document by time is quite a "natural" way to think about document history, and allows us to - for example - fetch multiple documents as they were at a given time, without having to look up their exact version numbers first. We add a new `Connection.fetchSnapshotByTimestamp` method, which follows a very similar route to `Connection.fetchSnapshot`, and where possible, as much code is re-used as possible: - both methods use a subclassed child of `SnapshotRequest` - both methods have their requests handled by the same machinery in `Connection` - both methods in the `Backend` have ops applied by a common method, but use their own methods for calls to middleware In order to make this feature possible at scale, this change also adds two new methods to the `MilestoneDB` interface: - `getMilestoneSnapshotAtOrBeforeTime` - `getMilestoneSnapshotAtOrAfterTime` These methods are used to fetch milestone snapshots either side of the requested timestamp, which means we only need to fetch the ops between the two of them to reach the desired timestamp. In the case where a milestone database is not being used, then fetching a snapshot by timestamp is still possible, but it will fetch all the ops for a document, and keep applying them from v0 until the timestamp is reached, which is not particularly scalable. --- README.md | 23 + lib/agent.js | 6 + lib/backend.js | 139 ++++- lib/client/connection.js | 34 +- .../snapshot-request.js | 26 +- .../snapshot-timestamp-request.js | 26 + .../snapshot-version-request.js | 26 + lib/milestone-db/index.js | 25 +- lib/milestone-db/memory.js | 79 ++- lib/milestone-db/no-op.js | 10 + lib/util.js | 4 + package.json | 1 + test/client/snapshot-timestamp-request.js | 514 ++++++++++++++++++ ...request.js => snapshot-version-request.js} | 17 +- test/db.js | 1 - test/milestone-db.js | 236 ++++++++ 16 files changed, 1091 insertions(+), 76 deletions(-) rename lib/client/{ => snapshot-request}/snapshot-request.js (72%) create mode 100644 lib/client/snapshot-request/snapshot-timestamp-request.js create mode 100644 lib/client/snapshot-request/snapshot-version-request.js create mode 100644 test/client/snapshot-timestamp-request.js rename test/client/{snapshot-request.js => snapshot-version-request.js} (96%) diff --git a/README.md b/README.md index afc273bec..ecc2969ed 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,27 @@ Get a read-only snapshot of a document at the requested version. } ``` +`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 +* `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_) @@ -464,3 +485,5 @@ The `41xx` and `51xx` codes are reserved for use by ShareDB DB adapters, and the * 5018 - Required QueryEmitter listener not assigned * 5019 - getMilestoneSnapshot MilestoneDB method unimplemented * 5020 - saveMilestoneSnapshot MilestoneDB method unimplemented +* 5021 - getMilestoneSnapshotBeforeTime MilestoneDB method unimplemented +* 5022 - getMilestoneSnapshotAfterTime MilestoneDB method unimplemented diff --git a/lib/agent.js b/lib/agent.js index 12d6f3cf5..2c4d44fd1 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -303,6 +303,8 @@ Agent.prototype._handleMessage = function(request, callback) { return this._submit(request.c, request.d, op, callback); case 'nf': return this._fetchSnapshot(request.c, request.d, request.v, callback); + case 'nt': + return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); } @@ -589,3 +591,7 @@ Agent.prototype._createOp = function(request) { Agent.prototype._fetchSnapshot = function (collection, id, version, callback) { this.backend.fetchSnapshot(this, collection, id, version, callback); }; + +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 e766202ec..27555d528 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -81,7 +81,9 @@ 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' + byVersion: 'byVersion', + // A specific snapshot is being fetch by timestamp (eg through backend.fetchSnapshotByTimestamp) + byTimestamp: 'byTimestamp' }; Backend.prototype._shimDocAction = function() { @@ -627,6 +629,8 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) 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); @@ -637,45 +641,120 @@ Backend.prototype._fetchSnapshot = function (collection, id, version, callback) db.getOps(collection, id, from, version, null, function (error, ops) { if (error) return callback(error); - var type = null; - var data; - var fetchedVersion = 0; - - if (milestoneSnapshot) { - type = types.map[milestoneSnapshot.type]; - if (!type) return callback({ code: 4008, message: 'Unknown type' }); - data = milestoneSnapshot.data; - fetchedVersion = milestoneSnapshot.v; - } + backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, function (error, snapshot) { + if (error) return callback(error); - for (var index = 0; index < ops.length; index++) { - var op = ops[index]; - fetchedVersion = op.v + 1; - - if (op.create) { - type = types.map[op.create.type]; - if (!type) return callback({ code: 4008, message: 'Unknown type' }); - data = type.create(op.create.data); - } else if (op.del) { - data = undefined; - type = null; - } else { - data = type.apply(data, op.op); + if (version > snapshot.v) { + return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); } - } - type = type ? type.uri : null; + callback(null, snapshot); + }); + }); + }); +}; - if (version > fetchedVersion) { - return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' }); - } +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 + }; - var snapshot = new Snapshot(id, fetchedVersion, type, data, null); + 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); + backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback, function shouldBreak(nextOp) { + var opTimestamp = nextOp && nextOp.m && nextOp.m.ts; + return timestamp !== null && opTimestamp > timestamp; + }); + }); + }); + }); +}; + +Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, callback, shouldBreak) { + if (typeof shouldBreak !== 'function') { + shouldBreak = function () { + return false; + }; + } + + var type = null; + var data; + var fetchedVersion = 0; + + if (startingSnapshot) { + type = types.map[startingSnapshot.type]; + if (!type) return callback({ code: 4008, message: 'Unknown type' }); + data = startingSnapshot.data; + fetchedVersion = startingSnapshot.v; + } + + for (var index = 0; index < ops.length; index++) { + var op = ops[index]; + + if (shouldBreak(op)) { + break; + } + + fetchedVersion = op.v + 1; + + if (op.create) { + type = types.map[op.create.type]; + if (!type) return callback({ code: 4008, message: 'Unknown type' }); + data = type.create(op.create.data); + } else if (op.del) { + data = undefined; + type = null; + } else { + data = type.apply(data, op.op); + } + } + + type = type ? type.uri : null; + + var snapshot = new Snapshot(id, fetchedVersion, type, data, null); + callback(null, snapshot); +}; + function pluckIds(snapshots) { var ids = []; for (var i = 0; i < snapshots.length; i++) { diff --git a/lib/client/connection.js b/lib/client/connection.js index 9a2a1fef7..560391bdc 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -1,6 +1,7 @@ var Doc = require('./doc'); var Query = require('./query'); -var SnapshotRequest = require('./snapshot-request'); +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'); @@ -233,6 +234,7 @@ Connection.prototype.handleMessage = function(message) { return this._handleBulkMessage(message, '_handleUnsubscribe'); case 'nf': + case 'nt': return this._handleSnapshotFetch(err, message); case 'f': @@ -634,7 +636,35 @@ Connection.prototype.fetchSnapshot = function(collection, id, version, callback) } var requestId = this.nextSnapshotRequestId++; - var snapshotRequest = new SnapshotRequest(this, requestId, collection, id, version, callback); + 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 version + * + * @param collection - the collection name of the snapshot + * @param id - the ID of the snapshot + * @param timestamp (optional) - the timestamp to fetch + * @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(); }; diff --git a/lib/client/snapshot-request.js b/lib/client/snapshot-request/snapshot-request.js similarity index 72% rename from lib/client/snapshot-request.js rename to lib/client/snapshot-request/snapshot-request.js index 1b8302594..00ed9b90f 100644 --- a/lib/client/snapshot-request.js +++ b/lib/client/snapshot-request/snapshot-request.js @@ -1,25 +1,19 @@ -var Snapshot = require('../snapshot'); -var util = require('../util'); -var emitter = require('../emitter'); +var Snapshot = require('../../snapshot'); +var emitter = require('../../emitter'); module.exports = SnapshotRequest; -function SnapshotRequest(connection, requestId, collection, id, version, callback) { +function SnapshotRequest(connection, requestId, collection, id, callback) { emitter.EventEmitter.call(this); if (typeof callback !== 'function') { throw new Error('Callback is required for SnapshotRequest'); } - if (!util.isValidVersion(version)) { - throw new Error('Snapshot version must be a positive integer or null'); - } - this.requestId = requestId; this.connection = connection; this.id = id; this.collection = collection; - this.version = version; this.callback = callback; this.sent = false; @@ -31,15 +25,7 @@ SnapshotRequest.prototype.send = function () { return; } - var message = { - a: 'nf', - id: this.requestId, - c: this.collection, - d: this.id, - v: this.version, - }; - - this.connection.send(message); + this.connection.send(this._message()); this.sent = true; }; @@ -61,6 +47,8 @@ SnapshotRequest.prototype._handleResponse = function (error, message) { return this.callback(error); } - var snapshot = new Snapshot(this.id, message.v, message.type, message.data, null); + 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..53c3b2437 --- /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..60a2e3a3c --- /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/milestone-db/index.js b/lib/milestone-db/index.js index 581f1ecc9..48fc1e002 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -26,8 +26,7 @@ MilestoneDB.prototype.close = function(callback) { */ MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { var error = new ShareDBError(5019, 'getMilestoneSnapshot MilestoneDB method unimplemented'); - if (callback) return process.nextTick(callback, error); - this.emit('error', error); + this._callBackOrEmitError(error, callback); }; /** @@ -38,10 +37,28 @@ MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, */ MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { var error = new ShareDBError(5020, 'saveMilestoneSnapshot MilestoneDB method unimplemented'); - if (callback) return process.nextTick(callback, error); - this.emit('error', error); + this._callBackOrEmitError(error, callback); +}; + +MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) { + var error = new ShareDBError(5021, 'getMilestoneSnapshotBeforeTime MilestoneDB method unimplemented'); + this._callBackOrEmitError(error, callback); +}; + +MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) { + var error = new ShareDBError(5022, 'getMilestoneSnapshotAfterTime 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 index f239c3f9a..f2707bc5f 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -23,24 +23,17 @@ function MemoryMilestoneDB(options) { MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { - if (!callback) callback = function () {}; - if (!collection) return process.nextTick(callback, new ShareDBError(4001, 'Missing collection')); - if (!id) return process.nextTick(callback, new ShareDBError(4001, 'Missing ID')); if (!this._isValidVersion(version)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid version')); - var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, id); - - var milestoneSnapshot; - for (var i = 0; i < milestoneSnapshots.length; i++) { - var nextMilestoneSnapshot = milestoneSnapshots[i]; - if (nextMilestoneSnapshot.v <= version || version === null) { - milestoneSnapshot = nextMilestoneSnapshot; - } else { - break; + var shouldBreak = function (currentSnapshot, nextSnapshot) { + if (version === null) { + return false; } - } - process.nextTick(callback, null, milestoneSnapshot); + return nextSnapshot.v > version; + }; + + this._findMilestoneSnapshot(collection, id, shouldBreak, callback); }; MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { @@ -61,6 +54,64 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh 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 shouldBreak = function (currentSnapshot, nextSnapshot) { + if (timestamp === null) { + return !!currentSnapshot; + } + + var mtime = nextSnapshot && nextSnapshot.m && nextSnapshot.m.mtime; + return mtime > timestamp; + }; + + this._findMilestoneSnapshot(collection, id, shouldBreak, callback); +}; + +MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) { + if (!this._isValidTimestamp(timestamp)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); + + var shouldBreak = function (currentSnapshot) { + if (timestamp === null) { + return false; + } + + var mtime = currentSnapshot && currentSnapshot.m && currentSnapshot.m.mtime; + return mtime >= timestamp; + } + + this._findMilestoneSnapshot(collection, id, shouldBreak, 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, shouldBreak, 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 (shouldBreak(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] = []); diff --git a/lib/milestone-db/no-op.js b/lib/milestone-db/no-op.js index 77204f7ec..82d66ba10 100644 --- a/lib/milestone-db/no-op.js +++ b/lib/milestone-db/no-op.js @@ -22,3 +22,13 @@ NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot 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/util.js b/lib/util.js index 8f3c48804..6ca346ffe 100644 --- a/lib/util.js +++ b/lib/util.js @@ -18,3 +18,7 @@ exports.isValidVersion = function (version) { if (version === null) return true; return exports.isInteger(version) && version >= 0; }; + +exports.isValidTimestamp = function (timestamp) { + return exports.isValidVersion(timestamp); +}; diff --git a/package.json b/package.json index 876447bef..1d85a22da 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "expect.js": "^0.3.1", "istanbul": "^0.4.2", "jshint": "^2.9.2", + "lolex": "^3.0.0", "mocha": "^5.2.0", "sinon": "^6.1.5" }, diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js new file mode 100644 index 000000000..9c2aeaad2 --- /dev/null +++ b/test/client/snapshot-timestamp-request.js @@ -0,0 +1,514 @@ +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, snapshot) { + 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, snapshot) { + 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-request.js b/test/client/snapshot-version-request.js similarity index 96% rename from test/client/snapshot-request.js rename to test/client/snapshot-version-request.js index e151a2edf..4b7101e56 100644 --- a/test/client/snapshot-request.js +++ b/test/client/snapshot-version-request.js @@ -5,7 +5,7 @@ var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); var sinon = require('sinon'); var util = require('../util'); -describe('SnapshotRequest', function () { +describe('SnapshotVersionRequest', function () { var backend; beforeEach(function () { @@ -16,7 +16,7 @@ describe('SnapshotRequest', function () { backend.close(done); }); - describe('a document with some simple versions a day apart', function () { + describe('a document with some simple versions', function () { var v0 = { id: 'don-quixote', v: 0, @@ -106,7 +106,7 @@ describe('SnapshotRequest', 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) { @@ -399,19 +399,24 @@ describe('SnapshotRequest', function () { 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); - backend = new Backend({ + backendWithMilestones = new Backend({ db: db, milestoneDb: milestoneDb }); }); + afterEach(function (done) { + backendWithMilestones.close(done); + }); + it('fetches a snapshot using the milestone', function (done) { - var doc = backend.connect().get('books', 'mocking-bird'); + var doc = backendWithMilestones.connect().get('books', 'mocking-bird'); util.callInSeries([ function (next) { @@ -426,7 +431,7 @@ describe('SnapshotRequest', function () { function (next) { sinon.spy(milestoneDb, 'getMilestoneSnapshot'); sinon.spy(db, 'getOps'); - backend.connect().fetchSnapshot('books', 'mocking-bird', 3, next); + backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next); }, function (snapshot, next) { expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.be(true); diff --git a/test/db.js b/test/db.js index 396557f89..257763173 100644 --- a/test/db.js +++ b/test/db.js @@ -2,7 +2,6 @@ var async = require('async'); var expect = require('expect.js'); var Backend = require('../lib/backend'); var ot = require('../lib/ot'); -var Snapshot = require('../lib/snapshot'); module.exports = function(options) { var create = options.create; diff --git a/test/milestone-db.js b/test/milestone-db.js index 92195c71f..1354eb4ee 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -43,6 +43,20 @@ describe('Base class', function () { 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 () { @@ -358,6 +372,228 @@ module.exports = function (options) { }); }); + 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 }; From 5dd070b7eda2b58b7f34288989bb62eea349a045 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 15 Jan 2019 16:58:20 -0800 Subject: [PATCH 079/181] For readSnapshots middleware, add requestMethod and requestParams properties The "readSnapshots" middleware function can now look like this: ``` function(request, next) { let { collection, snapshots, requestMethod, // 'fetch', 'query', etc. requestParams, // object whose shape can vary snapshotType, agent } = request; } ``` --- lib/backend.js | 60 ++++++++++++++++++++++++++++++++++------------ test/middleware.js | 4 ++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index e766202ec..068fd6993 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -272,7 +272,7 @@ Backend.prototype._sanitizeOpsBulk = function(agent, projection, collection, ops }, callback); }; -Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, snapshotType, callback) { +Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, requestContext, callback) { if (projection) { try { projections.projectSnapshots(projection.fields, snapshots); @@ -281,13 +281,10 @@ Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, s } } - var request = { - collection: collection, - snapshots: snapshots, - snapshotType: snapshotType - }; + requestContext.collection = collection; + requestContext.snapshots = snapshots; - this.trigger(this.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, callback); + this.trigger(this.MIDDLEWARE_ACTIONS.readSnapshots, agent, requestContext, callback); }; Backend.prototype._getSnapshotProjection = function(db, projection) { @@ -353,20 +350,28 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback) Backend.prototype.fetch = function(agent, index, id, callback) { var start = Date.now(); var projection = this.projections[index]; - var collection = (projection) ? projection.target : index; + var dbCollection = (projection) ? projection.target : index; var fields = projection && projection.fields; var backend = this; var request = { agent: agent, index: index, - collection: collection, + collection: dbCollection, id: id }; - backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) { + backend.db.getSnapshot(dbCollection, id, fields, null, function(err, snapshot) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) { + var requestContext = { + requestMethod: 'fetch', + requestParams: { + index: index, + id: id + }, + snapshotType: backend.SNAPSHOT_TYPES.current + }; + backend._sanitizeSnapshots(agent, snapshotProjection, dbCollection, snapshots, requestContext, function(err) { if (err) return callback(err); backend.emit('timing', 'fetch', Date.now() - start, request); callback(null, snapshot); @@ -390,7 +395,15 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap); - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) { + var requestContext = { + requestMethod: 'fetchBulk', + requestParams: { + index: index, + ids: ids + }, + snapshotType: backend.SNAPSHOT_TYPES.current + }; + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function(err) { if (err) return callback(err); backend.emit('timing', 'fetchBulk', Date.now() - start, request); callback(null, snapshotMap); @@ -578,7 +591,16 @@ 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, backend.SNAPSHOT_TYPES.current, function(err) { + var requestContext = { + requestMethod: 'query', // TODO: Should this distinguish between queryFetch and querySubscribe? + requestParams: { + index: request.index, + query: request.query, + options: request.options, + }, + snapshotType: backend.SNAPSHOT_TYPES.current + }; + backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, requestContext, function(err) { callback(err, snapshots, extra); }); }); @@ -616,8 +638,16 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) 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) { + var requestContext = { + requestMethod: 'fetchSnapshot', + requestParams: { + index: index, + id: id, + version: version, + }, + snapshotType: backend.SNAPSHOT_TYPES.byVersion + }; + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function (error) { if (error) return callback(error); backend.emit('timing', 'fetchSnapshot', Date.now() - start, request); callback(null, snapshot); diff --git a/test/middleware.js b/test/middleware.js index 7f067720d..b9bc7d099 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -199,6 +199,8 @@ describe('middleware', function() { var doneAfter = util.callAfter(1, done); backend.use('readSnapshots', function(request, next) { expect(request.snapshots).to.have.length(1); + expect(request.requestMethod).to.be.a('string'); + expect(request.requestParams).to.be.ok; expectFido(request); doneAfter(); next(); @@ -210,6 +212,8 @@ describe('middleware', function() { var doneAfter = util.callAfter(1, done); backend.use('readSnapshots', function(request, next) { expect(request.snapshots).to.have.length(2); + expect(request.requestMethod).to.be.a('string'); + expect(request.requestParams).to.be.ok; expectFido(request); expectSpot(request); doneAfter(); From 58e29e6d29b02887995a8fb003f9c44b511af50c Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 16 Jan 2019 13:12:19 -0800 Subject: [PATCH 080/181] Code review changes for readSnapshots middleware - requestMethod -> method, requestParams -> parameters - Distinguish methods 'queryFetch' and 'querySubscribe' --- lib/backend.js | 30 +++++++++++++++--------------- test/middleware.js | 8 ++++---- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 068fd6993..e2cad4ab3 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -350,28 +350,28 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback) Backend.prototype.fetch = function(agent, index, id, callback) { var start = Date.now(); var projection = this.projections[index]; - var dbCollection = (projection) ? projection.target : index; + var collection = (projection) ? projection.target : index; var fields = projection && projection.fields; var backend = this; var request = { agent: agent, index: index, - collection: dbCollection, + collection: collection, id: id }; - backend.db.getSnapshot(dbCollection, id, fields, null, function(err, snapshot) { + backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; var requestContext = { - requestMethod: 'fetch', - requestParams: { + method: 'fetch', + parameters: { index: index, id: id }, snapshotType: backend.SNAPSHOT_TYPES.current }; - backend._sanitizeSnapshots(agent, snapshotProjection, dbCollection, snapshots, requestContext, function(err) { + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function(err) { if (err) return callback(err); backend.emit('timing', 'fetch', Date.now() - start, request); callback(null, snapshot); @@ -396,8 +396,8 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap); var requestContext = { - requestMethod: 'fetchBulk', - requestParams: { + method: 'fetchBulk', + parameters: { index: index, ids: ids }, @@ -512,7 +512,7 @@ Backend.prototype.queryFetch = function(agent, index, query, options, callback) var backend = this; backend._triggerQuery(agent, index, query, options, function(err, request) { if (err) return callback(err); - backend._query(agent, request, function(err, snapshots, extra) { + backend._query(agent, 'queryFetch', request, function(err, snapshots, extra) { if (err) return callback(err); backend.emit('timing', 'queryFetch', Date.now() - start, request); callback(null, snapshots, extra); @@ -546,7 +546,7 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba return; } // Issue query on db to get our initial results - backend._query(agent, request, function(err, snapshots, extra) { + backend._query(agent, 'querySubscribe', request, function(err, snapshots, extra) { if (err) { stream.destroy(); return callback(err); @@ -587,13 +587,13 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac }); }; -Backend.prototype._query = function(agent, request, callback) { +Backend.prototype._query = function(agent, method, 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); var requestContext = { - requestMethod: 'query', // TODO: Should this distinguish between queryFetch and querySubscribe? - requestParams: { + method: method, + parameters: { index: request.index, query: request.query, options: request.options, @@ -639,8 +639,8 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; var requestContext = { - requestMethod: 'fetchSnapshot', - requestParams: { + method: 'fetchSnapshot', + parameters: { index: index, id: id, version: version, diff --git a/test/middleware.js b/test/middleware.js index b9bc7d099..737ce4ce7 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -199,8 +199,8 @@ describe('middleware', function() { var doneAfter = util.callAfter(1, done); backend.use('readSnapshots', function(request, next) { expect(request.snapshots).to.have.length(1); - expect(request.requestMethod).to.be.a('string'); - expect(request.requestParams).to.be.ok; + expect(request.method).to.be.a('string'); + expect(request.parameters).to.be.ok; expectFido(request); doneAfter(); next(); @@ -212,8 +212,8 @@ describe('middleware', function() { var doneAfter = util.callAfter(1, done); backend.use('readSnapshots', function(request, next) { expect(request.snapshots).to.have.length(2); - expect(request.requestMethod).to.be.a('string'); - expect(request.requestParams).to.be.ok; + expect(request.method).to.be.a('string'); + expect(request.parameters).to.be.ok; expectFido(request); expectSpot(request); doneAfter(); From 579b22caf2f5edcc1aad0427635e5aa9699a81c4 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 19 Dec 2018 18:17:31 +0000 Subject: [PATCH 081/181] Simplify `shouldBreak` calls for fetching snapshots by timestamp This change removes or renames `shouldBreak` calls. In `Backend`, for clarity we instead pre-filter ops, and just pass around the ops we want to be applied to a snapshot. In the `MemoryMilestoneDB`, these functions are extracted and renamed to more descriptive break condition names. --- README.md | 4 +-- lib/backend.js | 33 ++++++++++-------- lib/client/connection.js | 2 +- lib/milestone-db/index.js | 4 +-- lib/milestone-db/memory.js | 71 +++++++++++++++++++++----------------- 5 files changed, 63 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index ecc2969ed..af0b1f64b 100644 --- a/README.md +++ b/README.md @@ -485,5 +485,5 @@ The `41xx` and `51xx` codes are reserved for use by ShareDB DB adapters, and the * 5018 - Required QueryEmitter listener not assigned * 5019 - getMilestoneSnapshot MilestoneDB method unimplemented * 5020 - saveMilestoneSnapshot MilestoneDB method unimplemented -* 5021 - getMilestoneSnapshotBeforeTime MilestoneDB method unimplemented -* 5022 - getMilestoneSnapshotAfterTime MilestoneDB method unimplemented +* 5021 - getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented +* 5022 - getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented diff --git a/lib/backend.js b/lib/backend.js index 27555d528..c57561e12 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -701,22 +701,14 @@ Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestam var options = {metadata: true}; db.getOps(collection, id, from, to, options, function (error, ops) { if (error) return callback(error); - backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback, function shouldBreak(nextOp) { - var opTimestamp = nextOp && nextOp.m && nextOp.m.ts; - return timestamp !== null && opTimestamp > timestamp; - }); + filterOpsInPlaceBeforeTimestamp(ops, timestamp); + backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback); }); }); }); }; -Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, callback, shouldBreak) { - if (typeof shouldBreak !== 'function') { - shouldBreak = function () { - return false; - }; - } - +Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, callback) { var type = null; var data; var fetchedVersion = 0; @@ -731,10 +723,6 @@ Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, c for (var index = 0; index < ops.length; index++) { var op = ops[index]; - if (shouldBreak(op)) { - break; - } - fetchedVersion = op.v + 1; if (op.create) { @@ -762,3 +750,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 560391bdc..3464cb3ba 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -642,7 +642,7 @@ Connection.prototype.fetchSnapshot = function(collection, id, version, callback) }; /** - * Fetch a read-only snapshot at a given version + * Fetch a read-only snapshot at a given timestamp * * @param collection - the collection name of the snapshot * @param id - the ID of the snapshot diff --git a/lib/milestone-db/index.js b/lib/milestone-db/index.js index 48fc1e002..1b04ad30e 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -41,12 +41,12 @@ MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, ca }; MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) { - var error = new ShareDBError(5021, 'getMilestoneSnapshotBeforeTime MilestoneDB method unimplemented'); + 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, 'getMilestoneSnapshotAfterTime MilestoneDB method unimplemented'); + var error = new ShareDBError(5022, 'getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented'); this._callBackOrEmitError(error, callback); }; diff --git a/lib/milestone-db/memory.js b/lib/milestone-db/memory.js index f2707bc5f..52f1ba522 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -25,15 +25,8 @@ 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 shouldBreak = function (currentSnapshot, nextSnapshot) { - if (version === null) { - return false; - } - - return nextSnapshot.v > version; - }; - - this._findMilestoneSnapshot(collection, id, shouldBreak, callback); + var predicate = versionLessThanOrEqualTo(version); + this._findMilestoneSnapshot(collection, id, predicate, callback); }; MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { @@ -57,31 +50,15 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) { if (!this._isValidTimestamp(timestamp)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp')); - var shouldBreak = function (currentSnapshot, nextSnapshot) { - if (timestamp === null) { - return !!currentSnapshot; - } - - var mtime = nextSnapshot && nextSnapshot.m && nextSnapshot.m.mtime; - return mtime > timestamp; - }; - - this._findMilestoneSnapshot(collection, id, shouldBreak, callback); + 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 shouldBreak = function (currentSnapshot) { - if (timestamp === null) { - return false; - } - - var mtime = currentSnapshot && currentSnapshot.m && currentSnapshot.m.mtime; - return mtime >= timestamp; - } - - this._findMilestoneSnapshot(collection, id, shouldBreak, function (error, snapshot) { + 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; @@ -93,7 +70,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collec }); }; -MemoryMilestoneDB.prototype._findMilestoneSnapshot = function (collection, id, shouldBreak, callback) { +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')); @@ -102,7 +79,7 @@ MemoryMilestoneDB.prototype._findMilestoneSnapshot = function (collection, id, s var milestoneSnapshot; for (var i = 0; i < milestoneSnapshots.length; i++) { var nextMilestoneSnapshot = milestoneSnapshots[i]; - if (shouldBreak(milestoneSnapshot, nextMilestoneSnapshot)) { + if (breakCondition(milestoneSnapshot, nextMilestoneSnapshot)) { break; } else { milestoneSnapshot = nextMilestoneSnapshot; @@ -116,3 +93,35 @@ MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function (collection, i 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; + }; +} From 8e4627116029fb57a5af08a5d49642301dc8652e Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Wed, 23 Jan 2019 17:51:54 +0000 Subject: [PATCH 082/181] Move snapshot building function into `ot` The function for building a snapshot from ops is useful, and has no dependencies on `Backend`. This change moves it into the `ot` module, where it will be a bit more discoverable and can be reused. --- lib/backend.js | 35 +++-------------------------------- lib/ot.js | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 32 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index c57561e12..edd5a35ab 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -709,38 +709,9 @@ Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestam }; Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, callback) { - var type = null; - var data; - var fetchedVersion = 0; - - if (startingSnapshot) { - type = types.map[startingSnapshot.type]; - if (!type) return callback({ code: 4008, message: 'Unknown type' }); - data = startingSnapshot.data; - fetchedVersion = startingSnapshot.v; - } - - for (var index = 0; index < ops.length; index++) { - var op = ops[index]; - - fetchedVersion = op.v + 1; - - if (op.create) { - type = types.map[op.create.type]; - if (!type) return callback({ code: 4008, message: 'Unknown type' }); - data = type.create(op.create.data); - } else if (op.del) { - data = undefined; - type = null; - } else { - data = type.apply(data, op.op); - } - } - - type = type ? type.uri : null; - - var snapshot = new Snapshot(id, fetchedVersion, type, data, null); - callback(null, snapshot); + var snapshot = startingSnapshot || new Snapshot(id, 0, null, undefined, null); + var error = ot.applyOps(snapshot, ops); + callback(error, snapshot); }; function pluckIds(snapshots) { diff --git a/lib/ot.js b/lib/ot.js index 8cf708521..f44b77835 100644 --- a/lib/ot.js +++ b/lib/ot.js @@ -149,3 +149,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 + * @returns 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); + } + } +}; From 78f5e7f329236f42fa524a0edd4a8861caa75835 Mon Sep 17 00:00:00 2001 From: Roger Ngo Date: Thu, 24 Jan 2019 10:13:14 -0800 Subject: [PATCH 083/181] Making it more clear to access the property of the document and assigning the value within the HTML textarea to that property of the document instead of assigning it as the raw data value for the document. --- examples/textarea/client.js | 2 +- examples/textarea/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/textarea/client.js b/examples/textarea/client.js index e02b99d17..c47035a1c 100644 --- a/examples/textarea/client.js +++ b/examples/textarea/client.js @@ -31,6 +31,6 @@ var doc = connection.get('examples', 'textarea'); doc.subscribe(function(err) { if (err) throw err; - var binding = new StringBinding(element, doc); + var binding = new StringBinding(element, doc, ['content']); binding.setup(); }); diff --git a/examples/textarea/server.js b/examples/textarea/server.js index 55fbbf4d1..0b2184222 100644 --- a/examples/textarea/server.js +++ b/examples/textarea/server.js @@ -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(); From 7938f7fc7548329b74fc353633b9cbc035a15ef4 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 30 Jan 2019 09:19:45 -0800 Subject: [PATCH 084/181] 1.0.0-beta.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1d85a22da..99540ccf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.17", + "version": "1.0.0-beta.18", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 57c85989db517e6e8cd59eee2ad18da03763fdc5 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 6 Feb 2019 09:51:41 -0800 Subject: [PATCH 085/181] Switch readSnapshots middleware context to use named class/constants --- lib/backend.js | 61 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index e2cad4ab3..041839fb4 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -77,6 +77,23 @@ Backend.prototype.MIDDLEWARE_ACTIONS = { submit: 'submit' }; +// Context provided to readSnapshots middleware +function ReadSnapshotsContext(method, parameters, snapshotType) { + this.method = method; + this.parameters = parameters; + this.snapshotType = snapshotType; + this.collection = null; // Set during _sanitizeSnapshots + this.snapshots = null; // Set during _sanitizeSnapshots +} + +Backend.prototype.READ_SNAPSHOTS_METHODS = { + fetch: 'fetch', + fetchBulk: 'fetchBulk', + fetchSnapshot: 'fetchSnapshot', + queryFetch: 'queryFetch', + querySubscribe: 'querySubscribe' +}; + Backend.prototype.SNAPSHOT_TYPES = { // The current snapshot is being fetched (eg through backend.fetch) current: 'current', @@ -363,14 +380,14 @@ Backend.prototype.fetch = function(agent, index, id, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - var requestContext = { - method: 'fetch', - parameters: { + var requestContext = new ReadSnapshotsContext( + backend.READ_SNAPSHOTS_METHODS.fetch, + { index: index, id: id }, - snapshotType: backend.SNAPSHOT_TYPES.current - }; + backend.SNAPSHOT_TYPES.current + ); backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function(err) { if (err) return callback(err); backend.emit('timing', 'fetch', Date.now() - start, request); @@ -395,14 +412,14 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap); - var requestContext = { - method: 'fetchBulk', - parameters: { + var requestContext = new ReadSnapshotsContext( + backend.READ_SNAPSHOTS_METHODS.fetchBulk, + { index: index, ids: ids }, - snapshotType: backend.SNAPSHOT_TYPES.current - }; + backend.SNAPSHOT_TYPES.current + ); backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function(err) { if (err) return callback(err); backend.emit('timing', 'fetchBulk', Date.now() - start, request); @@ -512,7 +529,7 @@ Backend.prototype.queryFetch = function(agent, index, query, options, callback) var backend = this; backend._triggerQuery(agent, index, query, options, function(err, request) { if (err) return callback(err); - backend._query(agent, 'queryFetch', request, function(err, snapshots, extra) { + backend._query(agent, backend.READ_SNAPSHOTS_METHODS.queryFetch, request, function(err, snapshots, extra) { if (err) return callback(err); backend.emit('timing', 'queryFetch', Date.now() - start, request); callback(null, snapshots, extra); @@ -546,7 +563,7 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba return; } // Issue query on db to get our initial results - backend._query(agent, 'querySubscribe', request, function(err, snapshots, extra) { + backend._query(agent, backend.READ_SNAPSHOTS_METHODS.querySubscribe, request, function(err, snapshots, extra) { if (err) { stream.destroy(); return callback(err); @@ -591,15 +608,15 @@ Backend.prototype._query = function(agent, method, 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); - var requestContext = { - method: method, - parameters: { + var requestContext = new ReadSnapshotsContext( + method, + { index: request.index, query: request.query, options: request.options, }, - snapshotType: backend.SNAPSHOT_TYPES.current - }; + backend.SNAPSHOT_TYPES.current + ); backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, requestContext, function(err) { callback(err, snapshots, extra); }); @@ -638,15 +655,15 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - var requestContext = { - method: 'fetchSnapshot', - parameters: { + var requestContext = new ReadSnapshotsContext( + backend.READ_SNAPSHOTS_METHODS.fetchSnapshot, + { index: index, id: id, version: version, }, - snapshotType: backend.SNAPSHOT_TYPES.byVersion - }; + backend.SNAPSHOT_TYPES.byVersion + ); backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function (error) { if (error) return callback(error); backend.emit('timing', 'fetchSnapshot', Date.now() - start, request); From 7e12683c9e25ac6036bd6f5da2520cf79791327f Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 7 Feb 2019 09:44:23 -0800 Subject: [PATCH 086/181] Support new readSnapshots context info in fetchSnapshotByTimestamp --- lib/backend.js | 13 +++++++++++-- test/client/snapshot-timestamp-request.js | 2 ++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 77b19a13c..23a346eaa 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -90,6 +90,7 @@ Backend.prototype.READ_SNAPSHOTS_METHODS = { fetch: 'fetch', fetchBulk: 'fetchBulk', fetchSnapshot: 'fetchSnapshot', + fetchSnapshotByTimestamp: 'fetchSnapshotByTimestamp', queryFetch: 'queryFetch', querySubscribe: 'querySubscribe' }; @@ -718,8 +719,16 @@ Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timesta 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) { + var requestContext = new ReadSnapshotsContext( + backend.READ_SNAPSHOTS_METHODS.fetchSnapshotByTimestamp, + { + index: index, + id: id, + timestamp: timestamp, + }, + backend.SNAPSHOT_TYPES.byTimestamp + ); + backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function (error) { if (error) return callback(error); backend.emit('timing', 'fetchSnapshot', Date.now() - start, request); callback(null, snapshot); diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 9c2aeaad2..fd6f074a6 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -326,6 +326,8 @@ describe('SnapshotTimestampRequest', function () { it('triggers the middleware', function (done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request) { + expect(request.method).to.eql(backend.READ_SNAPSHOTS_METHODS.fetchSnapshotByTimestamp); + expect(request.parameters).to.eql({index: 'books', id: 'time-machine', timestamp: day3}); expect(request.snapshots[0]).to.eql(v3); expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byTimestamp); done(); From 36e40e04a1d4d282f064d18214787905ca5d3625 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 14 Feb 2019 08:42:03 +0000 Subject: [PATCH 087/181] Update snapshot fetch documentation --- README.md | 4 ++-- lib/client/connection.js | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index af0b1f64b..dfc7d4cf5 100644 --- a/README.md +++ b/README.md @@ -256,7 +256,7 @@ Get a read-only snapshot of a document at the requested version. * `id` _(String)_ ID of the snapshot * `version` _(number) [optional]_ - The version number of the desired snapshot + 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: @@ -277,7 +277,7 @@ Get a read-only snapshot of a document at the requested version. * `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 + 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: diff --git a/lib/client/connection.js b/lib/client/connection.js index 3464cb3ba..cd56306b2 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -618,7 +618,7 @@ Connection.prototype._firstSnapshotRequest = function () { * * @param collection - the collection name of the snapshot * @param id - the ID of the snapshot - * @param version (optional) - the version number to fetch + * @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: * * { @@ -646,7 +646,7 @@ Connection.prototype.fetchSnapshot = function(collection, id, version, callback) * * @param collection - the collection name of the snapshot * @param id - the ID of the snapshot - * @param timestamp (optional) - the timestamp to fetch + * @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: * * { From 8c4f979a75090bff6734179703f1534b9346be69 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 14 Feb 2019 14:42:41 -0800 Subject: [PATCH 088/181] 1.0.0-beta.19: For readSnapshots middleware, add request method and parameter info --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 99540ccf3..8d7f56da8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.18", + "version": "1.0.0-beta.19", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 57c12424e55149ff8a45c1705ce3dfa64f64dea5 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 14 Feb 2019 18:45:15 -0800 Subject: [PATCH 089/181] Revert "For readSnapshots middleware, add method and parameter properties to first parameter" --- lib/backend.js | 90 +++++------------------ test/client/snapshot-timestamp-request.js | 2 - test/middleware.js | 4 - 3 files changed, 17 insertions(+), 79 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 23a346eaa..edd5a35ab 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -77,24 +77,6 @@ Backend.prototype.MIDDLEWARE_ACTIONS = { submit: 'submit' }; -// Context provided to readSnapshots middleware -function ReadSnapshotsContext(method, parameters, snapshotType) { - this.method = method; - this.parameters = parameters; - this.snapshotType = snapshotType; - this.collection = null; // Set during _sanitizeSnapshots - this.snapshots = null; // Set during _sanitizeSnapshots -} - -Backend.prototype.READ_SNAPSHOTS_METHODS = { - fetch: 'fetch', - fetchBulk: 'fetchBulk', - fetchSnapshot: 'fetchSnapshot', - fetchSnapshotByTimestamp: 'fetchSnapshotByTimestamp', - queryFetch: 'queryFetch', - querySubscribe: 'querySubscribe' -}; - Backend.prototype.SNAPSHOT_TYPES = { // The current snapshot is being fetched (eg through backend.fetch) current: 'current', @@ -292,7 +274,7 @@ Backend.prototype._sanitizeOpsBulk = function(agent, projection, collection, ops }, callback); }; -Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, requestContext, callback) { +Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, snapshots, snapshotType, callback) { if (projection) { try { projections.projectSnapshots(projection.fields, snapshots); @@ -301,10 +283,13 @@ Backend.prototype._sanitizeSnapshots = function(agent, projection, collection, s } } - requestContext.collection = collection; - requestContext.snapshots = snapshots; + var request = { + collection: collection, + snapshots: snapshots, + snapshotType: snapshotType + }; - this.trigger(this.MIDDLEWARE_ACTIONS.readSnapshots, agent, requestContext, callback); + this.trigger(this.MIDDLEWARE_ACTIONS.readSnapshots, agent, request, callback); }; Backend.prototype._getSnapshotProjection = function(db, projection) { @@ -383,15 +368,7 @@ Backend.prototype.fetch = function(agent, index, id, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - var requestContext = new ReadSnapshotsContext( - backend.READ_SNAPSHOTS_METHODS.fetch, - { - index: index, - id: id - }, - backend.SNAPSHOT_TYPES.current - ); - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function(err) { + 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); @@ -415,15 +392,7 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap); - var requestContext = new ReadSnapshotsContext( - backend.READ_SNAPSHOTS_METHODS.fetchBulk, - { - index: index, - ids: ids - }, - backend.SNAPSHOT_TYPES.current - ); - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function(err) { + 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); @@ -532,7 +501,7 @@ Backend.prototype.queryFetch = function(agent, index, query, options, callback) var backend = this; backend._triggerQuery(agent, index, query, options, function(err, request) { if (err) return callback(err); - backend._query(agent, backend.READ_SNAPSHOTS_METHODS.queryFetch, request, function(err, snapshots, extra) { + backend._query(agent, request, function(err, snapshots, extra) { if (err) return callback(err); backend.emit('timing', 'queryFetch', Date.now() - start, request); callback(null, snapshots, extra); @@ -566,7 +535,7 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba return; } // Issue query on db to get our initial results - backend._query(agent, backend.READ_SNAPSHOTS_METHODS.querySubscribe, request, function(err, snapshots, extra) { + backend._query(agent, request, function(err, snapshots, extra) { if (err) { stream.destroy(); return callback(err); @@ -607,20 +576,11 @@ Backend.prototype._triggerQuery = function(agent, index, query, options, callbac }); }; -Backend.prototype._query = function(agent, method, request, callback) { +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); - var requestContext = new ReadSnapshotsContext( - method, - { - index: request.index, - query: request.query, - options: request.options, - }, - backend.SNAPSHOT_TYPES.current - ); - backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, requestContext, function(err) { + backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) { callback(err, snapshots, extra); }); }); @@ -658,16 +618,8 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - var requestContext = new ReadSnapshotsContext( - backend.READ_SNAPSHOTS_METHODS.fetchSnapshot, - { - index: index, - id: id, - version: version, - }, - backend.SNAPSHOT_TYPES.byVersion - ); - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function (error) { + 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); @@ -719,16 +671,8 @@ Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timesta if (error) return callback(error); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [snapshot]; - var requestContext = new ReadSnapshotsContext( - backend.READ_SNAPSHOTS_METHODS.fetchSnapshotByTimestamp, - { - index: index, - id: id, - timestamp: timestamp, - }, - backend.SNAPSHOT_TYPES.byTimestamp - ); - backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, requestContext, function (error) { + 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); diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index fd6f074a6..9c2aeaad2 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -326,8 +326,6 @@ describe('SnapshotTimestampRequest', function () { it('triggers the middleware', function (done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request) { - expect(request.method).to.eql(backend.READ_SNAPSHOTS_METHODS.fetchSnapshotByTimestamp); - expect(request.parameters).to.eql({index: 'books', id: 'time-machine', timestamp: day3}); expect(request.snapshots[0]).to.eql(v3); expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byTimestamp); done(); diff --git a/test/middleware.js b/test/middleware.js index 737ce4ce7..7f067720d 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -199,8 +199,6 @@ describe('middleware', function() { var doneAfter = util.callAfter(1, done); backend.use('readSnapshots', function(request, next) { expect(request.snapshots).to.have.length(1); - expect(request.method).to.be.a('string'); - expect(request.parameters).to.be.ok; expectFido(request); doneAfter(); next(); @@ -212,8 +210,6 @@ describe('middleware', function() { var doneAfter = util.callAfter(1, done); backend.use('readSnapshots', function(request, next) { expect(request.snapshots).to.have.length(2); - expect(request.method).to.be.a('string'); - expect(request.parameters).to.be.ok; expectFido(request); expectSpot(request); doneAfter(); From cb79636ce9108a5689a0364e1abb2c50196ef1c0 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 27 Feb 2019 16:36:03 -0800 Subject: [PATCH 090/181] In Doc._clearInflightOp, clear inflightOp before calling callbacks Fixes double-callback issue for chained op submissions where the second op is invalid: https://github.com/share/sharedb/issues/272 --- lib/client/doc.js | 5 ++++- test/client/doc.js | 37 ++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 95776db4b..39f3ea08f 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -920,9 +920,12 @@ Doc.prototype._hardRollback = function(err) { }; Doc.prototype._clearInflightOp = function(err) { - var called = callEach(this.inflightOp.callbacks, err); + var inflightOp = this.inflightOp; this.inflightOp = null; + + var called = callEach(inflightOp.callbacks, err); + this.flush(); this._emitNothingPending(); diff --git a/test/client/doc.js b/test/client/doc.js index f7529d71e..dd7ad5396 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -2,7 +2,7 @@ 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(); @@ -213,6 +213,41 @@ describe('client query subscribe', function() { }); + 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; From 02187ca3ef3ff031e80dfbe6a22a98f5fc2d1ac7 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 28 Feb 2019 14:40:37 -0800 Subject: [PATCH 091/181] 1.0.0-beta.20 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d7f56da8..09833005b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.19", + "version": "1.0.0-beta.20", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 31be02e914883118c97f477c9bcd6cc3a34f3c6d Mon Sep 17 00:00:00 2001 From: Greg Kubisa Date: Tue, 5 Mar 2019 14:05:53 +0100 Subject: [PATCH 092/181] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index af0b1f64b..aa06951f2 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,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', ...) From 1c4bb6d7649676f3b2e775b1f2477b4b337401be Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Tue, 26 Mar 2019 18:57:59 -0700 Subject: [PATCH 093/181] Add new "reply" middleware action The "reply" middleware is called just before the backend is about to reply to a client. It's the flip side of the existing "receive" middleware action. The middleware function gets a context with properties: - request - reply - action (always `'reply'` for this middleware) - agent - backend --- lib/agent.js | 51 +++++++++++++++++--------- lib/backend.js | 10 ++++-- test/middleware.js | 90 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 18 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 2c4d44fd1..dc6c493a3 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -187,23 +187,34 @@ 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) { - logger.warn(err.stack); - } - request.error = { - code: err.code, - message: err.message - }; - } - this.send(request); + request.error = getReplyErrorObject(err); + var middlewareContext = {request: request, reply: request}; + backend.trigger(backend.MIDDLEWARE_ACTIONS.reply, agent, middlewareContext, function(_err) { + // If we were already going to send back an error, and the reply middleware runs into + // another error (`_err`), just send back the original error. + agent.send(middlewareContext.reply); + }); return; } if (!message) message = {}; @@ -217,7 +228,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 diff --git a/lib/backend.js b/lib/backend.js index edd5a35ab..d2eed4b71 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -69,10 +69,16 @@ 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 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' }; diff --git a/test/middleware.js b/test/middleware.js index 7f067720d..d4503e3d7 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -221,6 +221,96 @@ 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('triggers on errors', function(done) { + var backend = this.backend; + var connection = this.backend.connect(); + var doc = connection.get('dogs', 'fido'); + doc.fetch(function(err) { + if (err) { + return done(err); + } + // Directly invoke backend DB method to delete doc, so that + // client `doc` is unaware of the change. + debugger; + backend.db.commit('dogs', 'fido', {v: 1, del: true}, {v: 2, data: null}, null, function(err) { + if (err) { + return done(err); + } + // Submitting op on deleted doc should result in an error. + var newOp = {p: ['age'], na: 1}; + backend.use('reply', function(replyContext, next) { + var reply = replyContext.reply; + expect(reply.a).to.eql('op'); + expect(reply.c).to.eql('dogs'); + expect(reply.d).to.eql('fido'); + expect(reply.op).to.eql([newOp]); + expect(reply.error).to.have.property('code', 4017); // "Document was deleted" + next(); + }); + doc.submitOp(newOp, function(err) { + // Expect that there was an error. + expect(err).to.have.property('code', 4017); // "Document was deleted" + 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) { From 46734e2298f3a6ffdded8451620ad3cc4c7b8d18 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 27 Mar 2019 09:12:04 -0700 Subject: [PATCH 094/181] Fix jshint errors --- lib/agent.js | 2 +- test/middleware.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index dc6c493a3..22c4d76dd 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -202,7 +202,7 @@ function getReplyErrorObject(err) { message: err.message }; } -}; +} Agent.prototype._reply = function(request, err, message) { var agent = this; diff --git a/test/middleware.js b/test/middleware.js index d4503e3d7..9b54380b7 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -264,7 +264,6 @@ describe('middleware', function() { } // Directly invoke backend DB method to delete doc, so that // client `doc` is unaware of the change. - debugger; backend.db.commit('dogs', 'fido', {v: 1, del: true}, {v: 2, data: null}, null, function(err) { if (err) { return done(err); @@ -284,7 +283,7 @@ describe('middleware', function() { // Expect that there was an error. expect(err).to.have.property('code', 4017); // "Document was deleted" done(); - }) + }); }); }); }); From ceef172e4cf045fa4cf7cf56710c090e0a92772c Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 27 Mar 2019 09:29:02 -0700 Subject: [PATCH 095/181] Remove error case from "reply" middleware --- lib/agent.js | 7 +------ lib/backend.js | 2 +- test/middleware.js | 34 ---------------------------------- 3 files changed, 2 insertions(+), 41 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 22c4d76dd..ae2f1a82a 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -209,12 +209,7 @@ Agent.prototype._reply = function(request, err, message) { var backend = agent.backend; if (err) { request.error = getReplyErrorObject(err); - var middlewareContext = {request: request, reply: request}; - backend.trigger(backend.MIDDLEWARE_ACTIONS.reply, agent, middlewareContext, function(_err) { - // If we were already going to send back an error, and the reply middleware runs into - // another error (`_err`), just send back the original error. - agent.send(middlewareContext.reply); - }); + agent.send(request); return; } if (!message) message = {}; diff --git a/lib/backend.js b/lib/backend.js index d2eed4b71..442da075c 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -73,7 +73,7 @@ Backend.prototype.MIDDLEWARE_ACTIONS = { readSnapshots: 'readSnapshots', // Received a message from a client receive: 'receive', - // About to reply to a client message. + // 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 diff --git a/test/middleware.js b/test/middleware.js index 9b54380b7..da2f0c14a 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -254,40 +254,6 @@ describe('middleware', function() { }); }); - it('triggers on errors', function(done) { - var backend = this.backend; - var connection = this.backend.connect(); - var doc = connection.get('dogs', 'fido'); - doc.fetch(function(err) { - if (err) { - return done(err); - } - // Directly invoke backend DB method to delete doc, so that - // client `doc` is unaware of the change. - backend.db.commit('dogs', 'fido', {v: 1, del: true}, {v: 2, data: null}, null, function(err) { - if (err) { - return done(err); - } - // Submitting op on deleted doc should result in an error. - var newOp = {p: ['age'], na: 1}; - backend.use('reply', function(replyContext, next) { - var reply = replyContext.reply; - expect(reply.a).to.eql('op'); - expect(reply.c).to.eql('dogs'); - expect(reply.d).to.eql('fido'); - expect(reply.op).to.eql([newOp]); - expect(reply.error).to.have.property('code', 4017); // "Document was deleted" - next(); - }); - doc.submitOp(newOp, function(err) { - // Expect that there was an error. - expect(err).to.have.property('code', 4017); // "Document was deleted" - done(); - }); - }); - }); - }); - it('can make raw additions to query reply extra', function(done) { var snapshot = this.snapshot; this.backend.use('reply', function(replyContext, next) { From f5dbd37e7b74eda2065728a0f20da5ff51b75e50 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 27 Mar 2019 13:12:21 -0700 Subject: [PATCH 096/181] Add test coverage for "reply" middleware producing error --- test/middleware.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/middleware.js b/test/middleware.js index da2f0c14a..4af42fbc6 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -254,6 +254,19 @@ describe('middleware', function() { }); }); + 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) { From 504a1f43fed20597b235de86b7e1b4d38967f861 Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 27 Mar 2019 16:08:16 -0700 Subject: [PATCH 097/181] 1.0.0-beta.21 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 09833005b..668ecc7fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.20", + "version": "1.0.0-beta.21", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From 8a47c18b1f425dd7b9ccb9640bc0601c72eddf5b Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 27 Mar 2019 21:24:04 -0700 Subject: [PATCH 098/181] Update middleware docs: add "reply" action, other tweaks --- README.md | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index aa06951f2..7338b9edf 100644 --- a/README.md +++ b/README.md @@ -149,17 +149,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 From e6dd9b80aef9b98113fdc841b332eb56ce3ddb64 Mon Sep 17 00:00:00 2001 From: curran Date: Sat, 6 Apr 2019 08:41:32 +0530 Subject: [PATCH 099/181] Use @teamwork/websocket-json-stream in textarea example --- examples/textarea/package.json | 2 +- examples/textarea/server.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/textarea/package.json b/examples/textarea/package.json index d04946413..1a91a3aa8 100644 --- a/examples/textarea/package.json +++ b/examples/textarea/package.json @@ -18,7 +18,7 @@ "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 0b2184222..9d359a771 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); From 1ad59384ec55427dcac821fc04e4c003e522c7ec Mon Sep 17 00:00:00 2001 From: Curran Kelleher Date: Tue, 9 Apr 2019 08:23:56 +0530 Subject: [PATCH 100/181] Clean up closed connections. Closes #282 --- lib/agent.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index ae2f1a82a..e3095b2b8 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -258,12 +258,15 @@ Agent.prototype._open = function() { agent._handleMessage(request.data, callback); }); }); - - this.stream.on('end', function() { + + function cleanup() { agent.backend.agentsCount--; if (!agent.stream.isServer) agent.backend.remoteAgentsCount--; agent._cleanup(); - }); + } + + 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. From 186e2abffc17713fae98883060f02aa07b381f37 Mon Sep 17 00:00:00 2001 From: curran Date: Tue, 9 Apr 2019 15:53:10 +0530 Subject: [PATCH 101/181] Add test coverage for emitting 'close'. --- test/client/connection.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/client/connection.js b/test/client/connection.js index e6a9aedfd..1f72c586c 100644 --- a/test/client/connection.js +++ b/test/client/connection.js @@ -101,6 +101,18 @@ 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') + setTimeout(function() { + expect(backend.agentsCount).equal(0); + done(); + }, 10); + }); + }); + it('does not increment when agent connect is rejected', function() { var backend = this.backend; backend.use('connect', function(request, next) { From ef236fae5ff91c1b2c7790dab5dea9f804558352 Mon Sep 17 00:00:00 2001 From: curran Date: Tue, 9 Apr 2019 16:02:59 +0530 Subject: [PATCH 102/181] Handle case of emitting both 'end' and 'close'. Closes #282 --- lib/agent.js | 14 ++++++++------ test/client/connection.js | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index e3095b2b8..b5cef65c1 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -56,8 +56,15 @@ 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]; @@ -259,12 +266,7 @@ Agent.prototype._open = function() { }); }); - function cleanup() { - 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); }; diff --git a/test/client/connection.js b/test/client/connection.js index 1f72c586c..aa00e2db8 100644 --- a/test/client/connection.js +++ b/test/client/connection.js @@ -106,10 +106,19 @@ describe('client connection', function() { var connection = backend.connect(); connection.on('connected', function() { connection.socket.stream.emit('close') - setTimeout(function() { - expect(backend.agentsCount).equal(0); - done(); - }, 10); + 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(); }); }); From abcea41edd5069c3bd2b439fc91def4e07f59b6a Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Thu, 11 Apr 2019 19:35:23 -0700 Subject: [PATCH 103/181] 1.0.0-beta.22 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 668ecc7fc..76dc3f878 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.21", + "version": "1.0.0-beta.22", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From f43b75281afb325cc2188b48e4561aeaed6f56ab Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 10:09:47 +0530 Subject: [PATCH 104/181] Restore tests to working order --- lib/client/doc.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index d85560556..8cde2f2fa 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -981,7 +981,7 @@ Doc.prototype._hardRollback = function(err) { if (this.inflightOp) pendingOps.push(this.inflightOp); pendingOps = pendingOps.concat(this.pendingOps); - // Apply the same technique for presence. + // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; if (this.inflightPresence) pendingPresence.push(this.inflightPresence); if (this.pendingPresence) pendingPresence.push(this.pendingPresence); @@ -991,6 +991,11 @@ Doc.prototype._hardRollback = function(err) { this.version = null; this.inflightOp = null; this.pendingOps = []; + + // Reset presence-related properties. + this.inflightPresence = null; + this.inflightPresenceSeq = 0; + this.pendingPresence = null; this.cachedOps.length = 0; this.receivedPresence = Object.create(null); this.requestReplyPresence = true; @@ -1011,21 +1016,22 @@ Doc.prototype._hardRollback = function(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 - // If there are no ops queued, or one of them didn't handle the error, - // then we emit the error. var allOpsHadCallbacks = !!pendingOps.length; for (var i = 0; i < pendingOps.length; i++) { allOpsHadCallbacks = callEach(pendingOps[i].callbacks, err) && allOpsHadCallbacks; } - if (err && !allOpsHadCallbacks) return doc.emit('error', err); // Apply the same technique for presence. var allPresenceHadCallbacks = !!pendingPresence.length; for (var i = 0; i < pendingPresence.length; i++) { - console.log(pendingPresence[i]) - allPresenceHadCallbacks = callEach(pendingPresence[i].callbacks, err) && allPresenceHadCallbacks; + allPresenceHadCallbacks = callEach(pendingPresence[i], err) && allPresenceHadCallbacks; + } + + // 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) { + return doc.emit('error', err); } - if (err && !allPresenceHadCallbacks) return doc.emit('error', err); }); }; From 940942955803abd5cbf97dc8d6a0325b7a7c3e98 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 10:11:58 +0530 Subject: [PATCH 105/181] Remove extraneous .editorconfig --- .editorconfig | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .editorconfig 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 From c4cf1b8a9c4eab92da6e3448e2fa7dbf5503573a Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 10:14:46 +0530 Subject: [PATCH 106/181] Revert extraneous changes in .travis.yml and package.json --- .travis.yml | 2 +- package.json | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 736e5fe78..21efafe46 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 jshint && npm run test-cover" # Send coverage data to Coveralls after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" diff --git a/package.json b/package.json index 0a1d5bfaa..76dc3f878 100644 --- a/package.json +++ b/package.json @@ -18,8 +18,7 @@ "jshint": "^2.9.2", "lolex": "^3.0.0", "mocha": "^5.2.0", - "sinon": "^6.1.5", - "sharedb-mingo-memory": "^1.0.0-beta" + "sinon": "^6.1.5" }, "scripts": { "test": "./node_modules/.bin/mocha && npm run jshint", From 237d2ad4356c7dbff4a70871bf924a002bf60f12 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 11:59:04 +0530 Subject: [PATCH 107/181] Use lolex to make 'expires cached ops' test more stable. --- test/client/presence.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/client/presence.js b/test/client/presence.js index 9c216c980..ce021add9 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1,4 +1,5 @@ var async = require('async'); +var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); @@ -474,6 +475,7 @@ types.register(presenceType.type3); }); it('expires cached ops', function(allDone) { + var clock = lolex.install(); var op1 = { index: 1, value: 'b' }; var op2 = { index: 2, value: 'b' }; var op3 = { index: 3, value: 'b' }; @@ -492,6 +494,7 @@ types.register(presenceType.type3); // Cache another op before the first 2 expire. function (callback) { setTimeout(callback, 30); + clock.next(); }, this.doc.submitOp.bind(this.doc, op2), function(done) { @@ -505,15 +508,20 @@ types.register(presenceType.type3); // Cache another op after the first 2 expire. function (callback) { setTimeout(callback, 31); + clock.next(); }, this.doc.submitOp.bind(this.doc, op3), function(done) { + console.log('a'); + console.log('b'); 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); + clock.uninstall(); done(); }.bind(this) ], allDone); + console.log('runAll'); }); it('requests reply presence when sending presence for the first time', function(allDone) { From c8d35c5846ad190ea01286008cf55e95d99b98c9 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:07:19 +0530 Subject: [PATCH 108/181] Move doc.presence to doc.presence.current --- README.md | 4 +- lib/client/doc.js | 26 +++--- test/client/presence.js | 173 ++++++++++++++++++++-------------------- 3 files changed, 101 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index fd906bddb..1f8dfffb4 100644 --- a/README.md +++ b/README.md @@ -313,8 +313,8 @@ Unique document ID `doc.data` _(Object)_ 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. +`doc.presence.current` _(Object)_ +Each property under `doc.presence.current` 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. `doc.fetch(function(err) {...})` Populate the fields on `doc` with a snapshot of the document from the server. diff --git a/lib/client/doc.js b/lib/client/doc.js index 8cde2f2fa..359b864fa 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -70,7 +70,9 @@ function Doc(connection, collection, id) { // The current presence data // Map of src -> presence data // Local src === '' - this.presence = Object.create(null); + this.presence = { + current: Object.create(null) + }; // The presence objects received from the server // Map of src -> presence this.receivedPresence = Object.create(null); @@ -514,7 +516,7 @@ Doc.prototype.flush = function() { this.inflightPresence = this.pendingPresence; this.inflightPresenceSeq = this.connection.seq; this.pendingPresence = null; - this.connection.sendPresence(this, this.presence[''], this.requestReplyPresence); + this.connection.sendPresence(this, this.presence.current[''], this.requestReplyPresence); this.requestReplyPresence = false; } }; @@ -1000,7 +1002,7 @@ Doc.prototype._hardRollback = function(err) { this.receivedPresence = Object.create(null); this.requestReplyPresence = true; - var srcList = Object.keys(this.presence); + var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1247,7 +1249,7 @@ Doc.prototype._processAllReceivedPresence = function() { }; Doc.prototype._transformPresence = function(src, op) { - var presenceData = this.presence[src]; + var presenceData = this.presence.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); @@ -1258,7 +1260,7 @@ Doc.prototype._transformPresence = function(src, op) { }; Doc.prototype._transformAllPresence = function(op) { - var srcList = Object.keys(this.presence); + var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1277,12 +1279,12 @@ Doc.prototype._pausePresence = function() { this.inflightPresence; this.inflightPresence = null; this.inflightPresenceSeq = 0; - } else if (!this.pendingPresence && this.presence[''] != null) { + } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; } this.receivedPresence = Object.create(null); this.requestReplyPresence = true; - var srcList = Object.keys(this.presence); + var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1297,14 +1299,14 @@ Doc.prototype._pausePresence = function() { // 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]; + if (this.presence.current[src] == null) return false; + delete this.presence.current[src]; } else { var isPresenceEqual = - this.presence[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence[src], data)); + this.presence.current[src] === data || + (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); if (isPresenceEqual) return false; - this.presence[src] = data; + this.presence.current[src] = data; } if (emit) this._emitPresence([ src ], true); return true; diff --git a/test/client/presence.js b/test/client/presence.js index ce021add9..ad2ddcc90 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -45,7 +45,7 @@ types.register(presenceType.type3); 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)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -66,7 +66,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -83,7 +83,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for a future version. @@ -111,7 +111,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -135,7 +135,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -159,7 +159,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -183,7 +183,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -207,7 +207,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -231,7 +231,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -256,7 +256,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -267,7 +267,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -290,7 +290,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -302,7 +302,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence).to.not.have.key(this.connection.id); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -326,8 +326,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { 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); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc.del(errorHandler(done)); @@ -347,8 +347,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { 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); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc2.del(errorHandler(done)); @@ -368,8 +368,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { 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)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -389,8 +389,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { 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)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -410,8 +410,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { 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)); + expect(this.doc.presence.current['']).to.eql(p(2)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -431,8 +431,8 @@ types.register(presenceType.type3); this.doc.on('presence', function(srcList, submitted) { 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)); + expect(this.doc.presence.current['']).to.eql(p(1)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(2)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -512,8 +512,6 @@ types.register(presenceType.type3); }, this.doc.submitOp.bind(this.doc, op3), function(done) { - console.log('a'); - console.log('b'); 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); @@ -521,7 +519,6 @@ types.register(presenceType.type3); done(); }.bind(this) ], allDone); - console.log('runAll'); }); it('requests reply presence when sending presence for the first time', function(allDone) { @@ -535,12 +532,12 @@ types.register(presenceType.type3); if (srcList[0] === '') { 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); + expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); } else { 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.presence.current['']).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); expect(this.doc2.requestReplyPresence).to.equal(false); done(); } @@ -622,7 +619,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(2)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -641,7 +638,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -649,7 +646,7 @@ types.register(presenceType.type3); setTimeout(function() { this.doc.subscribe(function(err) { if (err) return done(err); - expect(this.doc2.presence).to.eql({}); + expect(this.doc2.presence.current).to.eql({}); }.bind(this)); }.bind(this)); }.bind(this) @@ -665,7 +662,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -687,7 +684,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -752,13 +749,13 @@ types.register(presenceType.type3); this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, function(done) { - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); this.doc2.on('presence', function(srcList, submitted) { 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); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -777,17 +774,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { 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)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -804,17 +801,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { 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)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection2.close(); @@ -831,17 +828,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { 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)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc.unsubscribe(errorHandler(done)); @@ -858,17 +855,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { 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)); + expect(this.doc2.presence.current).to.not.have.key(connectionId); + expect(this.doc2.presence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc2.unsubscribe(errorHandler(done)); @@ -929,18 +926,18 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); this.connection.close(); - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); this.backend.connect(this.connection); process.nextTick(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -955,17 +952,17 @@ types.register(presenceType.type3); this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); this.doc.unsubscribe(errorHandler(done)); - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence).to.not.have.key(this.connection2.id); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); this.doc.subscribe(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence['']).to.eql(p(0)); - expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -980,7 +977,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1000,7 +997,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1020,7 +1017,7 @@ types.register(presenceType.type3); this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1044,7 +1041,7 @@ types.register(presenceType.type3); // 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); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1070,7 +1067,7 @@ types.register(presenceType.type3); // 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); + expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc.requestReplyPresence = false; @@ -1092,7 +1089,7 @@ types.register(presenceType.type3); if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc.presence['']).to.eql(p(1)); + expect(this.doc.presence.current['']).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1115,7 +1112,7 @@ types.register(presenceType.type3); if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1329,8 +1326,8 @@ types.register(presenceType.type3); presenceEmitted = true; 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); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', function(err) { @@ -1384,8 +1381,8 @@ types.register(presenceType.type3); presenceEmitted = true; 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); + expect(this.doc.presence.current).to.not.have.key(''); + expect(this.doc.presence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', done); @@ -1423,7 +1420,7 @@ types.register(presenceType.type3); setTimeout, function(done) { expect(this.doc.data).to.eql([ 'a', 'b' ]); - expect(this.doc.presence[this.connection2.id]).to.eql(p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(p(0)); // Replay the `lastPresence` with modified payload. lastPresence.p = p(1); lastPresence.v++; // +1 to account for the op above @@ -1434,7 +1431,7 @@ types.register(presenceType.type3); process.nextTick(done); }.bind(this), function(done) { - expect(this.doc.presence[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); + expect(this.doc.presence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); process.nextTick(done); }.bind(this) ], allDone); From 3efb82c6d76e77a332de1e78731ecc063fbf6ca8 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:09:06 +0530 Subject: [PATCH 109/181] Move doc.receivedPresence to doc.presence.received --- lib/client/doc.js | 38 +++++++++++++++++++------------------- test/client/presence.js | 2 +- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 359b864fa..b169e0763 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -75,12 +75,12 @@ function Doc(connection, collection, id) { }; // 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. + this.presence.received = Object.create(null); + // 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.receivedPresenceTimeout = 60000; + this.presence.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.requestReplyPresence = true; @@ -148,13 +148,13 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - doc.receivedPresence = Object.create(null); + doc.presence.received = Object.create(null); doc.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc.receivedPresence = Object.create(null); + doc.presence.received = Object.create(null); doc.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); @@ -999,7 +999,7 @@ Doc.prototype._hardRollback = function(err) { this.inflightPresenceSeq = 0; this.pendingPresence = null; this.cachedOps.length = 0; - this.receivedPresence = Object.create(null); + this.presence.received = Object.create(null); this.requestReplyPresence = true; var srcList = Object.keys(this.presence.current); @@ -1134,13 +1134,13 @@ Doc.prototype._handlePresence = function(err, presence) { // 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) + this.presence.received[src] && ( + this.presence.received[src].seq > presence.seq || + (this.presence.received[src].seq === presence.seq && presence.v != null) ) ) return; - this.receivedPresence[src] = presence; + this.presence.received[src] = presence; if (presence.v == null) { // null version should happen only when the server automatically sends @@ -1159,13 +1159,13 @@ Doc.prototype._handlePresence = function(err, presence) { // 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]; + var presence = this.presence.received[src]; if (!presence) return false; if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.receivedPresenceTimeout) { + if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { // Remove old received and processed presence - delete this.receivedPresence[src]; + delete this.presence.received[src]; } return false; } @@ -1185,14 +1185,14 @@ Doc.prototype._processReceivedPresence = function(src, emit) { } if (this.inflightOp && this.inflightOp.op == null) { - // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + // 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.pendingOps.length; i++) { if (this.pendingOps[i].op == null) { - // Remove presence data because receivedPresence can be transformed only against "op", not "create" nor "del" + // 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); } @@ -1200,14 +1200,14 @@ Doc.prototype._processReceivedPresence = function(src, emit) { var startIndex = this.cachedOps.length - (this.version - presence.v); if (startIndex < 0) { - // Remove presence data because we can't transform receivedPresence + // 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 receivedPresence can be transformed only against "op", not "create" nor "del" + // 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); } @@ -1237,7 +1237,7 @@ Doc.prototype._processReceivedPresence = function(src, emit) { }; Doc.prototype._processAllReceivedPresence = function() { - var srcList = Object.keys(this.receivedPresence); + var srcList = Object.keys(this.presence.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -1282,7 +1282,7 @@ Doc.prototype._pausePresence = function() { } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; } - this.receivedPresence = Object.create(null); + this.presence.received = Object.create(null); this.requestReplyPresence = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; diff --git a/test/client/presence.js b/test/client/presence.js index ad2ddcc90..44f6b9ca8 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1405,7 +1405,7 @@ types.register(presenceType.type3); return handleMessage.apply(this, arguments); }; if (expireCache) { - this.doc.receivedPresenceTimeout = 0; + this.doc.presence.receivedTimeout = 0; } async.series([ this.doc.create.bind(this.doc, [ 'a' ], typeName), From f0451e3b70abf2800f24768b06282e7076ec380c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:10:35 +0530 Subject: [PATCH 110/181] Move doc.requestReplyPresence to doc.presence.requestReply --- lib/client/doc.js | 10 ++++----- test/client/presence.js | 50 ++++++++++++++++++++--------------------- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index b169e0763..7f953c403 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -83,7 +83,7 @@ function Doc(connection, collection, id) { this.presence.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.requestReplyPresence = true; + this.presence.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. // The ops are cached for at least 1 minute by default, which should be lots, considering that the presence @@ -516,8 +516,8 @@ Doc.prototype.flush = function() { this.inflightPresence = this.pendingPresence; this.inflightPresenceSeq = this.connection.seq; this.pendingPresence = null; - this.connection.sendPresence(this, this.presence.current[''], this.requestReplyPresence); - this.requestReplyPresence = false; + this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); + this.presence.requestReply = false; } }; @@ -1000,7 +1000,7 @@ Doc.prototype._hardRollback = function(err) { this.pendingPresence = null; this.cachedOps.length = 0; this.presence.received = Object.create(null); - this.requestReplyPresence = true; + this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; @@ -1283,7 +1283,7 @@ Doc.prototype._pausePresence = function() { this.pendingPresence = []; } this.presence.received = Object.create(null); - this.requestReplyPresence = true; + this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { diff --git a/test/client/presence.js b/test/client/presence.js index 44f6b9ca8..902be9752 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -39,7 +39,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); @@ -60,7 +60,7 @@ types.register(presenceType.type3); 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.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.once('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); @@ -88,7 +88,7 @@ types.register(presenceType.type3); }.bind(this)); // A hack to send presence for a future version. this.doc.version += 2; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), function(err) { if (err) return done(err); this.doc.version -= 2; @@ -117,7 +117,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -141,7 +141,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -165,7 +165,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'c' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -189,7 +189,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -213,7 +213,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -237,7 +237,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'c' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -259,7 +259,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { @@ -272,7 +272,7 @@ types.register(presenceType.type3); }.bind(this)); // A hack to send presence for an older version. this.doc.version = 2; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -293,7 +293,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { @@ -308,7 +308,7 @@ types.register(presenceType.type3); // A hack to send presence for an older version. this.doc.version = 1; this.doc.data = [ 'a' ]; - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -538,7 +538,7 @@ types.register(presenceType.type3); expect(srcList).to.eql([ this.connection.id ]); expect(this.doc2.presence.current['']).to.eql(p(1)); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.requestReplyPresence).to.equal(false); + expect(this.doc2.presence.requestReply).to.equal(false); done(); } }.bind(this)); @@ -622,7 +622,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); this.doc.submitPresence(p(1), errorHandler(done)); this.doc.submitPresence(p(2), errorHandler(done)); @@ -641,7 +641,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); setTimeout(function() { this.doc.subscribe(function(err) { @@ -669,7 +669,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.presence.requestReply = false; }.bind(this)); }.bind(this) ], allDone); @@ -687,7 +687,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -758,7 +758,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.del(errorHandler(done)); }.bind(this) @@ -980,7 +980,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.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)) @@ -1000,7 +1000,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.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)) @@ -1020,7 +1020,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.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)) @@ -1044,7 +1044,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.del(errorHandler(done)); this.doc2.create([ 'c' ], typeName, errorHandler(done)); @@ -1070,7 +1070,7 @@ types.register(presenceType.type3); expect(this.doc2.presence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.requestReplyPresence = false; + this.doc.presence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)); this.doc2.del(errorHandler(done)); @@ -1412,7 +1412,7 @@ types.register(presenceType.type3); this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.requestReplyPresence = false; + this.doc2.presence.requestReply = false; this.doc2.submitPresence(p(0), done); }.bind(this), setTimeout, From 5217635b167bd462831344e297ac8e0025602486 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:11:57 +0530 Subject: [PATCH 111/181] Move doc.cachedOps to doc.presence.cachedOps --- lib/client/doc.js | 34 +++++++++++++++++----------------- test/client/presence.js | 40 ++++++++++++++++++++-------------------- 2 files changed, 37 insertions(+), 37 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 7f953c403..3abde219e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -88,8 +88,8 @@ function Doc(connection, collection, id) { // 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; + this.presence.cachedOps = []; + this.presence.cachedOpsTimeout = 60000; // The sequence number of the inflight presence request. this.inflightPresenceSeq = 0; @@ -149,13 +149,13 @@ Doc.prototype.destroy = function(callback) { return doc.emit('error', err); } doc.presence.received = Object.create(null); - doc.cachedOps.length = 0; + doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { doc.presence.received = Object.create(null); - doc.cachedOps.length = 0; + doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -236,7 +236,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (this.version > snapshot.v) return callback && callback(); this.version = snapshot.v; - this.cachedOps.length = 0; + this.presence.cachedOps.length = 0; var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); this.data = (this.type && this.type.deserialize) ? @@ -913,7 +913,7 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - this.cachedOps.length = 0; + this.presence.cachedOps.length = 0; } else if (message.v !== this.version) { // We should already be at the same version, because the server should @@ -998,7 +998,7 @@ Doc.prototype._hardRollback = function(err) { this.inflightPresence = null; this.inflightPresenceSeq = 0; this.pendingPresence = null; - this.cachedOps.length = 0; + this.presence.cachedOps.length = 0; this.presence.received = Object.create(null); this.presence.requestReply = true; @@ -1198,15 +1198,15 @@ Doc.prototype._processReceivedPresence = function(src, emit) { } } - var startIndex = this.cachedOps.length - (this.version - presence.v); + var startIndex = this.presence.cachedOps.length - (this.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) { + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + if (this.presence.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); @@ -1217,8 +1217,8 @@ Doc.prototype._processReceivedPresence = function(src, emit) { 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]; + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + var op = this.presence.cachedOps[i]; data = this.type.transformPresence(data, op.op, presence.src === op.src); } @@ -1323,17 +1323,17 @@ Doc.prototype._emitPresence = function(srcList, submitted) { Doc.prototype._cacheOp = function(op) { // Remove the old ops. - var oldOpTime = Date.now() - this.cachedOpsTimeout; + var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; var i; - for (i = 0; i < this.cachedOps.length; i++) { - if (this.cachedOps[i].time >= oldOpTime) { + for (i = 0; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].time >= oldOpTime) { break; } } if (i > 0) { - this.cachedOps.splice(0, i); + this.presence.cachedOps.splice(0, i); } // Cache the new op. - this.cachedOps.push(op); + this.presence.cachedOps.push(op); }; diff --git a/test/client/presence.js b/test/client/presence.js index 902be9752..27f499227 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -297,7 +297,7 @@ types.register(presenceType.type3); this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { - this.doc2.cachedOps = []; + this.doc2.presence.cachedOps = []; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); @@ -447,10 +447,10 @@ types.register(presenceType.type3); 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.presence.cachedOps.length).to.equal(3); + expect(this.doc.presence.cachedOps[0].create).to.equal(true); + expect(this.doc.presence.cachedOps[1].op).to.equal(op); + expect(this.doc.presence.cachedOps[2].del).to.equal(true); done(); }.bind(this) ], allDone); @@ -465,10 +465,10 @@ types.register(presenceType.type3); 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.presence.cachedOps.length).to.equal(3); + expect(this.doc2.presence.cachedOps[0].create).to.equal(true); + expect(this.doc2.presence.cachedOps[1].op).to.eql(op); + expect(this.doc2.presence.cachedOps[2].del).to.equal(true); done(); }.bind(this) ], allDone); @@ -479,15 +479,15 @@ types.register(presenceType.type3); var op1 = { index: 1, value: 'b' }; var op2 = { index: 2, value: 'b' }; var op3 = { index: 3, value: 'b' }; - this.doc.cachedOpsTimeout = 60; + this.doc.presence.cachedOpsTimeout = 60; async.series([ // Cache 2 ops. 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.presence.cachedOps.length).to.equal(2); + expect(this.doc.presence.cachedOps[0].create).to.equal(true); + expect(this.doc.presence.cachedOps[1].op).to.equal(op1); done(); }.bind(this), @@ -498,10 +498,10 @@ types.register(presenceType.type3); }, 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.presence.cachedOps.length).to.equal(3); + expect(this.doc.presence.cachedOps[0].create).to.equal(true); + expect(this.doc.presence.cachedOps[1].op).to.equal(op1); + expect(this.doc.presence.cachedOps[2].op).to.equal(op2); done(); }.bind(this), @@ -512,9 +512,9 @@ types.register(presenceType.type3); }, 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.presence.cachedOps.length).to.equal(2); + expect(this.doc.presence.cachedOps[0].op).to.equal(op2); + expect(this.doc.presence.cachedOps[1].op).to.equal(op3); clock.uninstall(); done(); }.bind(this) From ac26dae36f2b1ed5ce14351238f8e72afccfdd0c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:13:01 +0530 Subject: [PATCH 112/181] Move doc.inflightPresenceSeq to doc.presence.inflightSeq --- lib/client/doc.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 3abde219e..eb0e4d4ae 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -91,7 +91,7 @@ function Doc(connection, collection, id) { this.presence.cachedOps = []; this.presence.cachedOpsTimeout = 60000; // The sequence number of the inflight presence request. - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -514,7 +514,7 @@ Doc.prototype.flush = function() { if (this.subscribed && !this.inflightPresence && this.pendingPresence && !this.hasWritePending()) { this.inflightPresence = this.pendingPresence; - this.inflightPresenceSeq = this.connection.seq; + this.presence.inflightSeq = this.connection.seq; this.pendingPresence = null; this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); this.presence.requestReply = false; @@ -996,7 +996,7 @@ Doc.prototype._hardRollback = function(err) { // Reset presence-related properties. this.inflightPresence = null; - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; this.pendingPresence = null; this.presence.cachedOps.length = 0; this.presence.received = Object.create(null); @@ -1109,12 +1109,12 @@ Doc.prototype._handlePresence = function(err, presence) { 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, + // this.presence.inflightSeq would not equal presence.seq after a hard rollback, // when all callbacks are flushed with an error. - if (this.inflightPresenceSeq === presence.seq) { + if (this.presence.inflightSeq === presence.seq) { var callbacks = this.inflightPresence; this.inflightPresence = null; - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; var called = callbacks && callEach(callbacks, err); if (err && !called) this.emit('error', err); this.flush(); @@ -1278,7 +1278,7 @@ Doc.prototype._pausePresence = function() { this.inflightPresence.concat(this.pendingPresence) : this.inflightPresence; this.inflightPresence = null; - this.inflightPresenceSeq = 0; + this.presence.inflightSeq = 0; } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; } From 48accccc8dff310dd27bed44721c13b8c73d44af Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:13:59 +0530 Subject: [PATCH 113/181] Move doc.inflightPresence to doc.presence.inflight --- lib/client/doc.js | 26 +++++++++++++------------- test/client/presence.js | 14 +++++++------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index eb0e4d4ae..faa7d4d30 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -97,7 +97,7 @@ function Doc(connection, collection, id) { this.inflightFetch = []; this.inflightSubscribe = []; this.inflightUnsubscribe = []; - this.inflightPresence = null; + this.presence.inflight = null; this.pendingFetch = []; this.pendingPresence = null; @@ -266,7 +266,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.inflightPresence || + this.presence.inflight || this.pendingPresence ); }; @@ -512,8 +512,8 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.subscribed && !this.inflightPresence && this.pendingPresence && !this.hasWritePending()) { - this.inflightPresence = this.pendingPresence; + if (this.subscribed && !this.presence.inflight && this.pendingPresence && !this.hasWritePending()) { + this.presence.inflight = this.pendingPresence; this.presence.inflightSeq = this.connection.seq; this.pendingPresence = null; this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); @@ -985,7 +985,7 @@ Doc.prototype._hardRollback = function(err) { // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; - if (this.inflightPresence) pendingPresence.push(this.inflightPresence); + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); if (this.pendingPresence) pendingPresence.push(this.pendingPresence); // Cancel all pending ops and reset if we can't invert @@ -995,7 +995,7 @@ Doc.prototype._hardRollback = function(err) { this.pendingOps = []; // Reset presence-related properties. - this.inflightPresence = null; + this.presence.inflight = null; this.presence.inflightSeq = 0; this.pendingPresence = null; this.presence.cachedOps.length = 0; @@ -1085,7 +1085,7 @@ Doc.prototype.submitPresence = function (data, callback) { data = this.type.createPresence(data); } - if (this._setPresence('', data, true) || this.pendingPresence || this.inflightPresence) { + if (this._setPresence('', data, true) || this.pendingPresence || this.presence.inflight) { if (!this.pendingPresence) { this.pendingPresence = []; } @@ -1112,8 +1112,8 @@ Doc.prototype._handlePresence = function(err, presence) { // this.presence.inflightSeq would not equal presence.seq after a hard rollback, // when all callbacks are flushed with an error. if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.inflightPresence; - this.inflightPresence = null; + var callbacks = this.presence.inflight; + this.presence.inflight = null; this.presence.inflightSeq = 0; var called = callbacks && callEach(callbacks, err); if (err && !called) this.emit('error', err); @@ -1272,12 +1272,12 @@ Doc.prototype._transformAllPresence = function(op) { }; Doc.prototype._pausePresence = function() { - if (this.inflightPresence) { + if (this.presence.inflight) { this.pendingPresence = this.pendingPresence ? - this.inflightPresence.concat(this.pendingPresence) : - this.inflightPresence; - this.inflightPresence = null; + this.presence.inflight.concat(this.pendingPresence) : + this.presence.inflight; + this.presence.inflight = null; this.presence.inflightSeq = 0; } else if (!this.pendingPresence && this.presence.current[''] != null) { this.pendingPresence = []; diff --git a/test/client/presence.js b/test/client/presence.js index 27f499227..bfc47cf19 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -702,13 +702,13 @@ types.register(presenceType.type3); 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.presence.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.presence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -723,19 +723,19 @@ types.register(presenceType.type3); 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.presence.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.presence.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.presence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -1315,7 +1315,7 @@ 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 process.nextTick, // wait for "presence" event @@ -1372,7 +1372,7 @@ 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 process.nextTick(function() { // wait for presence event From cab69fb8e20771ab7343f8af6973d9b17ba7e760 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:16:48 +0530 Subject: [PATCH 114/181] Move doc.pendingPresence to doc.presence.pending --- lib/client/doc.js | 37 ++++++++++++++++++------------------- test/client/presence.js | 14 +++++++------- 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index faa7d4d30..42b15df41 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -99,7 +99,7 @@ function Doc(connection, collection, id) { this.inflightUnsubscribe = []; this.presence.inflight = null; this.pendingFetch = []; - this.pendingPresence = null; + this.presence.pending = 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 @@ -267,7 +267,7 @@ Doc.prototype.hasPending = function() { this.inflightUnsubscribe.length || this.pendingFetch.length || this.presence.inflight || - this.pendingPresence + this.presence.pending ); }; @@ -512,10 +512,10 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.subscribed && !this.presence.inflight && this.pendingPresence && !this.hasWritePending()) { - this.presence.inflight = this.pendingPresence; + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + this.presence.inflight = this.presence.pending; this.presence.inflightSeq = this.connection.seq; - this.pendingPresence = null; + this.presence.pending = null; this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); this.presence.requestReply = false; } @@ -986,7 +986,7 @@ Doc.prototype._hardRollback = function(err) { // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.pendingPresence) pendingPresence.push(this.pendingPresence); + if (this.presence.pending) pendingPresence.push(this.presence.pending); // Cancel all pending ops and reset if we can't invert this._setType(null); @@ -997,7 +997,7 @@ Doc.prototype._hardRollback = function(err) { // Reset presence-related properties. this.presence.inflight = null; this.presence.inflightSeq = 0; - this.pendingPresence = null; + this.presence.pending = null; this.presence.cachedOps.length = 0; this.presence.received = Object.create(null); this.presence.requestReply = true; @@ -1085,12 +1085,12 @@ Doc.prototype.submitPresence = function (data, callback) { data = this.type.createPresence(data); } - if (this._setPresence('', data, true) || this.pendingPresence || this.presence.inflight) { - if (!this.pendingPresence) { - this.pendingPresence = []; + if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { + if (!this.presence.pending) { + this.presence.pending = []; } if (callback) { - this.pendingPresence.push(callback); + this.presence.pending.push(callback); } } else if (callback) { @@ -1126,9 +1126,9 @@ Doc.prototype._handlePresence = function(err, presence) { // This shouldn't happen but check just in case. if (err) return this.emit('error', err); - if (presence.r && !this.pendingPresence) { + if (presence.r && !this.presence.pending) { // Another client requested us to share our current presence data - this.pendingPresence = []; + this.presence.pending = []; this.flush(); } @@ -1273,14 +1273,13 @@ Doc.prototype._transformAllPresence = function(op) { Doc.prototype._pausePresence = function() { if (this.presence.inflight) { - this.pendingPresence = - this.pendingPresence ? - this.presence.inflight.concat(this.pendingPresence) : - this.presence.inflight; + this.presence.pending = this.presence.pending + ? this.presence.inflight.concat(this.presence.pending) + : this.presence.inflight; this.presence.inflight = null; this.presence.inflightSeq = 0; - } else if (!this.pendingPresence && this.presence.current[''] != null) { - this.pendingPresence = []; + } else if (!this.presence.pending && this.presence.current[''] != null) { + this.presence.pending = []; } this.presence.received = Object.create(null); this.presence.requestReply = true; diff --git a/test/client/presence.js b/test/client/presence.js index bfc47cf19..34f753e64 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -701,13 +701,13 @@ types.register(presenceType.type3); 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.presence.pending).to.equal(true); expect(!!this.doc.presence.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.presence.pending).to.equal(false); expect(!!this.doc.presence.inflight).to.equal(false); done(); }.bind(this) @@ -722,19 +722,19 @@ types.register(presenceType.type3); 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.presence.pending).to.equal(true); expect(!!this.doc.presence.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.presence.pending).to.equal(false); expect(!!this.doc.presence.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.presence.pending).to.equal(false); expect(!!this.doc.presence.inflight).to.equal(false); done(); }.bind(this) @@ -1317,7 +1317,7 @@ types.register(presenceType.type3); }.bind(this), 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; @@ -1374,7 +1374,7 @@ types.register(presenceType.type3); } 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); From 6a0ecc4709b8601fc6bccaf32813fff5590eae6c Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:27:36 +0530 Subject: [PATCH 115/181] Refactor presence fields into object declaration. --- lib/client/doc.js | 60 +++++++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 42b15df41..c6524c4d2 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -67,39 +67,49 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - // The current presence data - // Map of src -> presence data - // Local src === '' + // Properties related to presence are grouped within this object. this.presence = { - current: Object.create(null) + + // The current presence data. + // Map of src -> presence data + // Local src === '' + current: Object.create(null), + + // The presence objects received from the server. + // Map of src -> presence + received: Object.create(null), + + // 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. + 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. + 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. + 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. + cachedOpsTimeout: 60000, + + // The sequence number of the inflight presence request. + inflightSeq: 0, + + // Callbacks (or null) for pending and inflight presence requests. + pending: null, + inflight: null }; - // The presence objects received from the server - // Map of src -> presence - this.presence.received = Object.create(null); - // 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.presence.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.presence.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. - // 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.presence.cachedOps = []; - this.presence.cachedOpsTimeout = 60000; - // The sequence number of the inflight presence request. - this.presence.inflightSeq = 0; // Array of callbacks or nulls as placeholders this.inflightFetch = []; this.inflightSubscribe = []; this.inflightUnsubscribe = []; - this.presence.inflight = null; this.pendingFetch = []; - this.presence.pending = 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 From d41c961af735df4bff9edbb12ff08b77c8a64c1d Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 12:35:45 +0530 Subject: [PATCH 116/181] Simplify object creation; 'change Object.create(null)' to '{}'. --- lib/client/doc.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c6524c4d2..c9ea0950e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -73,11 +73,11 @@ function Doc(connection, collection, id) { // The current presence data. // Map of src -> presence data // Local src === '' - current: Object.create(null), + current: {}, // The presence objects received from the server. // Map of src -> presence - received: Object.create(null), + 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. @@ -158,13 +158,13 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - doc.presence.received = Object.create(null); + doc.presence.received = {}; doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc.presence.received = Object.create(null); + doc.presence.received = {}; doc.presence.cachedOps.length = 0; doc.connection._destroyDoc(doc); if (callback) callback(); @@ -1009,7 +1009,7 @@ Doc.prototype._hardRollback = function(err) { this.presence.inflightSeq = 0; this.presence.pending = null; this.presence.cachedOps.length = 0; - this.presence.received = Object.create(null); + this.presence.received = {}; this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); @@ -1291,7 +1291,7 @@ Doc.prototype._pausePresence = function() { } else if (!this.presence.pending && this.presence.current[''] != null) { this.presence.pending = []; } - this.presence.received = Object.create(null); + this.presence.received = {}; this.presence.requestReply = true; var srcList = Object.keys(this.presence.current); var changedSrcList = []; From fc351fa36112c1d1befbb9a70b87202ef4e504c0 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 14:10:29 +0530 Subject: [PATCH 117/181] Introduce enablePresence option. Closes #128 --- README.md | 4 +++ lib/backend.js | 7 ++++ lib/client/doc.js | 71 ++++++++++++++++++++++++++--------------- test/client/presence.js | 11 ++++++- 4 files changed, 67 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 1f8dfffb4..d82b92ad7 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ default, ShareDB stores all operations forever - nothing is truly deleted. ## User presence synchronization +ShareDB supports synchronization of user presence data. This feature is opt-in, not enabled by default. To enable this feature, pass the `enablePresence: true` option to the ShareDB constructor (e.g. `var share = new ShareDB({ enablePresence: true })`). + 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. ## Server API @@ -96,6 +98,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.enablePresence` _(optional boolean)_ + Enable user presence synchronization. #### Database Adapters * `ShareDB.MemoryDB`, backed by a non-persistent database with no queries diff --git a/lib/backend.js b/lib/backend.js index e23bebd53..6e3145df6 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -48,6 +48,8 @@ function Backend(options) { if (!options.disableSpaceDelimitedActions) { this._shimAfterSubmit(); } + + this.enablePresence = options.enablePresence; } module.exports = Backend; emitter.mixin(Backend); @@ -155,6 +157,11 @@ 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; + + // Pass through information on whether or not presence is enabled, + // so that Doc instances can use it. + connection.enablePresence = this.enablePresence; + return connection; }; diff --git a/lib/client/doc.js b/lib/client/doc.js index c9ea0950e..e6288f6d9 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -68,7 +68,7 @@ function Doc(connection, collection, id) { this.data = undefined; // Properties related to presence are grouped within this object. - this.presence = { + this.presence = connection.enablePresence && { // The current presence data. // Map of src -> presence data @@ -158,14 +158,18 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; + if (doc.presence) { + doc.presence.received = {}; + doc.presence.cachedOps.length = 0; + } doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; + if (doc.presence) { + doc.presence.received = {}; + doc.presence.cachedOps.length = 0; + } doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -246,7 +250,11 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { if (this.version > snapshot.v) return callback && callback(); this.version = snapshot.v; - this.presence.cachedOps.length = 0; + + if (this.presence) { + this.presence.cachedOps.length = 0; + } + var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); this.data = (this.type && this.type.deserialize) ? @@ -276,8 +284,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.presence.inflight || - this.presence.pending + this.presence && (this.presence.inflight || this.presence.pending) ); }; @@ -522,6 +529,8 @@ Doc.prototype.flush = function() { this._sendOp(); } + if (!this.presence) return; + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { this.presence.inflight = this.presence.pending; this.presence.inflightSeq = this.connection.seq; @@ -923,7 +932,10 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - this.presence.cachedOps.length = 0; + + if (this.presence) { + this.presence.cachedOps.length = 0; + } } else if (message.v !== this.version) { // We should already be at the same version, because the server should @@ -995,8 +1007,10 @@ Doc.prototype._hardRollback = function(err) { // Apply the same technique for presence, cleaning up as we go. var pendingPresence = []; - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); + if (this.presence) { + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); + if (this.presence.pending) pendingPresence.push(this.presence.pending); + } // Cancel all pending ops and reset if we can't invert this._setType(null); @@ -1005,22 +1019,24 @@ Doc.prototype._hardRollback = function(err) { this.pendingOps = []; // Reset presence-related properties. - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; - - var srcList = Object.keys(this.presence.current); - var changedSrcList = []; - for (var i = 0; i < srcList.length; i++) { - var src = srcList[i]; - if (this._setPresence(src, null)) { - changedSrcList.push(src); + if (this.presence) { + this.presence.inflight = null; + this.presence.inflightSeq = 0; + this.presence.pending = null; + this.presence.cachedOps.length = 0; + this.presence.received = {}; + this.presence.requestReply = true; + + var srcList = Object.keys(this.presence.current); + 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); } - this._emitPresence(changedSrcList, false); // Fetch the latest version from the server to get us back into a working state var doc = this; @@ -1247,6 +1263,7 @@ Doc.prototype._processReceivedPresence = function(src, emit) { }; Doc.prototype._processAllReceivedPresence = function() { + if (!this.presence) return; var srcList = Object.keys(this.presence.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -1270,6 +1287,7 @@ Doc.prototype._transformPresence = function(src, op) { }; Doc.prototype._transformAllPresence = function(op) { + if (!this.presence) return; var srcList = Object.keys(this.presence.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -1282,6 +1300,8 @@ Doc.prototype._transformAllPresence = function(op) { }; Doc.prototype._pausePresence = function() { + if (!this.presence) return; + if (this.presence.inflight) { this.presence.pending = this.presence.pending ? this.presence.inflight.concat(this.presence.pending) @@ -1331,6 +1351,7 @@ Doc.prototype._emitPresence = function(srcList, submitted) { }; Doc.prototype._cacheOp = function(op) { + if (!this.presence) return; // Remove the old ops. var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; var i; diff --git a/test/client/presence.js b/test/client/presence.js index 34f753e64..aff5ab887 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -11,6 +11,15 @@ types.register(presenceType.type); types.register(presenceType.type2); types.register(presenceType.type3); +describe('client presence', function() { + it('does not expose doc.presence if enablePresence is false', function() { + var backend = new Backend(); + var connection = backend.connect(); + var doc = connection.get('dogs', 'fido'); + expect(typeof doc.presence).to.equal('undefined'); + }); +}); + [ 'wrapped-presence-no-compare', 'wrapped-presence-with-compare', @@ -22,7 +31,7 @@ types.register(presenceType.type3); describe('client presence (' + typeName + ')', function() { beforeEach(function() { - this.backend = new Backend(); + this.backend = new Backend({ enablePresence: true }); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); From 6cd16f3f8f2a105c1badd2ef126d0ec6f56fa6e4 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 14:45:05 +0530 Subject: [PATCH 118/181] Misc cleanup, finishing touches. --- lib/client/doc.js | 57 +++++++++++++++++++----------------- test/client/presence-type.js | 22 ++++++-------- 2 files changed, 39 insertions(+), 40 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index e6288f6d9..12c923d52 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -68,6 +68,9 @@ function Doc(connection, collection, id) { this.data = undefined; // Properties related to presence are grouped within this object. + // If this.presence is falsy (undefined), it means that + // the enablePresence flag was not passed into the ShareDB constructor, + // so the presence features should be disabled.. this.presence = connection.enablePresence && { // The current presence data. @@ -382,14 +385,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); @@ -401,7 +396,13 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this._cacheOp(serverOp); + this._cacheOp({ + src: message.src, + time: Date.now(), + create: !!message.create, + op: message.op, + del: !!message.del + }); try { this._otApply(message, false); this._processAllReceivedPresence(); @@ -523,15 +524,15 @@ function pushActionCallback(inflight, isDuplicate, callback) { // // 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.presence) return; - - if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + if (this.presence && this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { this.presence.inflight = this.presence.pending; this.presence.inflightSeq = this.connection.seq; this.presence.pending = null; @@ -1065,7 +1066,9 @@ Doc.prototype._hardRollback = function(err) { Doc.prototype._clearInflightOp = function(err) { var inflightOp = this.inflightOp; + this.inflightOp = null; + var called = callEach(inflightOp.callbacks, err); this.flush(); @@ -1123,10 +1126,7 @@ Doc.prototype.submitPresence = function (data, callback) { process.nextTick(callback); } - var doc = this; - process.nextTick(function() { - doc.flush(); - }); + process.nextTick(this.flush.bind(this)); }; Doc.prototype._handlePresence = function(err, presence) { @@ -1169,10 +1169,10 @@ Doc.prototype._handlePresence = function(err, presence) { this.presence.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); + // 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 @@ -1190,16 +1190,19 @@ Doc.prototype._processReceivedPresence = function(src, emit) { if (presence.processedAt != null) { if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { - // Remove old received and processed presence - delete this.presence.received[src]; + // Remove old received and processed presence. + delete this.presence.received[src]; } return false; } - if (this.version == null || this.version < presence.v) return false; // keep waiting for the missing snapshot or ops + if (this.version == null || this.version < presence.v) { + // keep waiting for the missing snapshot or ops. + return false; + } if (presence.p == null) { - // Remove presence data as requested + // Remove presence data as requested. presence.processedAt = Date.now(); return this._setPresence(src, null, emit); } @@ -1269,7 +1272,7 @@ Doc.prototype._processAllReceivedPresence = function() { for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; if (this._processReceivedPresence(src)) { - changedSrcList.push(src); + changedSrcList.push(src); } } this._emitPresence(changedSrcList, true); diff --git a/test/client/presence-type.js b/test/client/presence-type.js index 51ad272a0..6138eae7f 100644 --- a/test/client/presence-type.js +++ b/test/client/presence-type.js @@ -46,12 +46,9 @@ 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) { @@ -59,11 +56,9 @@ function createPresence(data) { } 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; } From ad6a5282133de6a3c8d3bd61c28375b1d5c05e49 Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 15:15:27 +0530 Subject: [PATCH 119/181] Split out presence methods into separate module --- lib/client/doc.js | 322 +------------------------------------ lib/client/presence.js | 349 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 354 insertions(+), 317 deletions(-) create mode 100644 lib/client/presence.js diff --git a/lib/client/doc.js b/lib/client/doc.js index 12c923d52..293d36441 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,6 +2,7 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); +var presenceMethods = require('./presence'); /** * A Doc is a client's view on a sharejs document. @@ -55,6 +56,7 @@ var types = require('../types'); */ module.exports = Doc; +Object.assign(Doc.prototype, presenceMethods); function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -71,42 +73,7 @@ function Doc(connection, collection, id) { // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled.. - this.presence = connection.enablePresence && { - - // The current presence data. - // Map of src -> presence data - // Local src === '' - current: {}, - - // The presence objects received from the server. - // Map of src -> presence - 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. - 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. - 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. - 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. - cachedOpsTimeout: 60000, - - // The sequence number of the inflight presence request. - inflightSeq: 0, - - // Callbacks (or null) for pending and inflight presence requests. - pending: null, - inflight: null - }; + this.presence = connection.enablePresence && this._initializePresence(); // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -1089,284 +1056,5 @@ function callEach(callbacks, err) { 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); - }); - } - - if (!this.type.createPresence || !this.type.transformPresence) { - var doc = this; - return process.nextTick(function() { - var err = new ShareDBError(4027, '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.type.createPresence(data); - } - - if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { - if (!this.presence.pending) { - this.presence.pending = []; - } - if (callback) { - this.presence.pending.push(callback); - } - - } else if (callback) { - process.nextTick(callback); - } - - process.nextTick(this.flush.bind(this)); -}; - -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.presence.inflightSeq would not equal presence.seq after a hard rollback, - // when all callbacks are flushed with an error. - if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 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.presence.pending) { - // Another client requested us to share our current presence data - this.presence.pending = []; - this.flush(); - } - - // Ignore older messages which arrived out of order - if ( - this.presence.received[src] && ( - this.presence.received[src].seq > presence.seq || - (this.presence.received[src].seq === presence.seq && presence.v != null) - ) - ) return; - - this.presence.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.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.presence.received[src]; - if (!presence) return false; - - if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { - // Remove old received and processed presence. - delete this.presence.received[src]; - } - return false; - } - - if (this.version == null || this.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.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 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.pendingOps.length; i++) { - if (this.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.presence.cachedOps.length - (this.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.presence.cachedOps.length; i++) { - if (this.presence.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.type.createPresence(presence.p); - - // Transform against past ops - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - var op = this.presence.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() { - if (!this.presence) return; - var srcList = Object.keys(this.presence.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); -}; - -Doc.prototype._transformPresence = function(src, op) { - var presenceData = this.presence.current[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) { - if (!this.presence) return; - var srcList = Object.keys(this.presence.current); - 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.presence) return; - - if (this.presence.inflight) { - this.presence.pending = this.presence.pending - ? this.presence.inflight.concat(this.presence.pending) - : this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - } else if (!this.presence.pending && this.presence.current[''] != null) { - this.presence.pending = []; - } - this.presence.received = {}; - this.presence.requestReply = true; - var srcList = Object.keys(this.presence.current); - 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. -Doc.prototype._setPresence = function(src, data, emit) { - if (data == null) { - if (this.presence.current[src] == null) return false; - delete this.presence.current[src]; - } else { - var isPresenceEqual = - this.presence.current[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); - if (isPresenceEqual) return false; - this.presence.current[src] = data; - } - if (emit) this._emitPresence([ src ], true); - return true; -}; - -Doc.prototype._emitPresence = function(srcList, submitted) { - if (srcList && srcList.length > 0) { - var doc = this; - process.nextTick(function() { - doc.emit('presence', srcList, submitted); - }); - } -}; - -Doc.prototype._cacheOp = function(op) { - if (!this.presence) return; - // Remove the old ops. - var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; - var i; - for (i = 0; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].time >= oldOpTime) { - break; - } - } - if (i > 0) { - this.presence.cachedOps.splice(0, i); - } - - // Cache the new op. - this.presence.cachedOps.push(op); -}; +// Expose callEach to presence methods via Doc prototype. +Doc.prototype._callEach = callEach; diff --git a/lib/client/presence.js b/lib/client/presence.js new file mode 100644 index 000000000..5e9cf01bd --- /dev/null +++ b/lib/client/presence.js @@ -0,0 +1,349 @@ +/* + * Presence Methods + * ---------------- + * + * This module contains definitions for presence-related methods + * that are added as methods to the Doc prototype (e.g. doc.submitPresence). + * + * The value of 'this' in these functions will be the Doc instance. + */ +var ShareDBError = require('../error'); + +// Submit presence data to a document. +// This is the only public facing method. +// All the others are marked as internal with a leading "_". +function submitPresence(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); + }); + } + + if (!this.type.createPresence || !this.type.transformPresence) { + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4027, '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.type.createPresence(data); + } + + if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { + if (!this.presence.pending) { + this.presence.pending = []; + } + if (callback) { + this.presence.pending.push(callback); + } + + } else if (callback) { + process.nextTick(callback); + } + + process.nextTick(this.flush.bind(this)); +} + +// This function generates the initial value for doc.presence. +function _initializePresence() { + + // Return a new object each time, otherwise mutations would bleed across documents. + return { + + // The current presence data. + // Map of src -> presence data + // Local src === '' + current: {}, + + // The presence objects received from the server. + // Map of src -> presence + 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. + 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. + 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. + 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. + cachedOpsTimeout: 60000, + + // The sequence number of the inflight presence request. + inflightSeq: 0, + + // Callbacks (or null) for pending and inflight presence requests. + pending: null, + inflight: null + }; +} + +function _handlePresence(err, presence) { + if (!this.subscribed) return; + + var src = presence.src; + if (!src) { + // Handle the ACK for the presence data we submitted. + // this.presence.inflightSeq would not equal presence.seq after a hard rollback, + // when all callbacks are flushed with an error. + if (this.presence.inflightSeq === presence.seq) { + var callbacks = this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + var called = callbacks && this._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.presence.pending) { + // Another client requested us to share our current presence data + this.presence.pending = []; + this.flush(); + } + + // Ignore older messages which arrived out of order + if ( + this.presence.received[src] && ( + this.presence.received[src].seq > presence.seq || + (this.presence.received[src].seq === presence.seq && presence.v != null) + ) + ) return; + + this.presence.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.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. +function _processReceivedPresence(src, emit) { + if (!src) return false; + var presence = this.presence.received[src]; + if (!presence) return false; + + if (presence.processedAt != null) { + if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { + // Remove old received and processed presence. + delete this.presence.received[src]; + } + return false; + } + + if (this.version == null || this.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.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 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.pendingOps.length; i++) { + if (this.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.presence.cachedOps.length - (this.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.presence.cachedOps.length; i++) { + if (this.presence.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.type.createPresence(presence.p); + + // Transform against past ops + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + var op = this.presence.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); +} + +function _processAllReceivedPresence() { + if (!this.presence) return; + var srcList = Object.keys(this.presence.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); +} + +function _transformPresence(src, op) { + var presenceData = this.presence.current[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); +} + +function _transformAllPresence(op) { + if (!this.presence) return; + var srcList = Object.keys(this.presence.current); + 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); +} + +function _pausePresence() { + if (!this.presence) return; + + if (this.presence.inflight) { + this.presence.pending = this.presence.pending + ? this.presence.inflight.concat(this.presence.pending) + : this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + } else if (!this.presence.pending && this.presence.current[''] != null) { + this.presence.pending = []; + } + this.presence.received = {}; + this.presence.requestReply = true; + var srcList = Object.keys(this.presence.current); + 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. +function _setPresence(src, data, emit) { + if (data == null) { + if (this.presence.current[src] == null) return false; + delete this.presence.current[src]; + } else { + var isPresenceEqual = + this.presence.current[src] === data || + (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); + if (isPresenceEqual) return false; + this.presence.current[src] = data; + } + if (emit) this._emitPresence([ src ], true); + return true; +} + +function _emitPresence(srcList, submitted) { + if (srcList && srcList.length > 0) { + var doc = this; + process.nextTick(function() { + doc.emit('presence', srcList, submitted); + }); + } +} + +function _cacheOp(op) { + if (!this.presence) return; + // Remove the old ops. + var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; + var i; + for (i = 0; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].time >= oldOpTime) { + break; + } + } + if (i > 0) { + this.presence.cachedOps.splice(0, i); + } + + // Cache the new op. + this.presence.cachedOps.push(op); +} + +module.exports = { + submitPresence, + _initializePresence, + _handlePresence, + _processReceivedPresence, + _processAllReceivedPresence, + _transformPresence, + _transformAllPresence, + _pausePresence, + _setPresence, + _emitPresence, + _cacheOp +}; From eaafc98772213e1da7fd2204320cbbbcdad2807e Mon Sep 17 00:00:00 2001 From: curran Date: Wed, 17 Apr 2019 15:33:45 +0530 Subject: [PATCH 120/181] Move more presence-related logic into presence methods module. --- lib/client/doc.js | 56 +++++++++--------------------------------- lib/client/presence.js | 46 +++++++++++++++++++++++++++++++++- 2 files changed, 56 insertions(+), 46 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 293d36441..b134f666a 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -56,7 +56,10 @@ var presenceMethods = require('./presence'); */ module.exports = Doc; + +// Expose presence-related methods on the Doc prototype. Object.assign(Doc.prototype, presenceMethods); + function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -72,7 +75,7 @@ function Doc(connection, collection, id) { // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, - // so the presence features should be disabled.. + // so the presence features should be disabled. this.presence = connection.enablePresence && this._initializePresence(); // Array of callbacks or nulls as placeholders @@ -128,18 +131,12 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - if (doc.presence) { - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; - } + if (doc.presence) doc._destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - if (doc.presence) { - doc.presence.received = {}; - doc.presence.cachedOps.length = 0; - } + if (doc.presence) doc._destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -376,7 +373,6 @@ Doc.prototype._handleOp = function(err, message) { } catch (error) { return this._hardRollback(error); } - return; }; // Called whenever (you guessed it!) the connection state changes. This will @@ -488,8 +484,6 @@ 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() { // Ignore if we can't send or we are already sending an op if (!this.connection.canSend || this.inflightOp) return; @@ -499,12 +493,8 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.presence && this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { - this.presence.inflight = this.presence.pending; - this.presence.inflightSeq = this.connection.seq; - this.presence.pending = null; - this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); - this.presence.requestReply = false; + if (this.presence) { + this._flushPresence(); } }; @@ -973,12 +963,8 @@ Doc.prototype._hardRollback = function(err) { if (this.inflightOp) pendingOps.push(this.inflightOp); pendingOps = pendingOps.concat(this.pendingOps); - // Apply the same technique for presence, cleaning up as we go. - var pendingPresence = []; - if (this.presence) { - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); - } + // Apply a similar technique for presence. + var pendingPresence = this.presence ? this._hardRollbackPresence() : []; // Cancel all pending ops and reset if we can't invert this._setType(null); @@ -986,26 +972,6 @@ Doc.prototype._hardRollback = function(err) { this.inflightOp = null; this.pendingOps = []; - // Reset presence-related properties. - if (this.presence) { - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; - - var srcList = Object.keys(this.presence.current); - 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 version from the server to get us back into a working state var doc = this; this.fetch(function() { @@ -1026,7 +992,7 @@ Doc.prototype._hardRollback = function(err) { // 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) { - return doc.emit('error', err); + doc.emit('error', err); } }); }; diff --git a/lib/client/presence.js b/lib/client/presence.js index 5e9cf01bd..91f635758 100644 --- a/lib/client/presence.js +++ b/lib/client/presence.js @@ -334,6 +334,47 @@ function _cacheOp(op) { this.presence.cachedOps.push(op); } +// If there are no pending ops, this method sends the pending presence data, if possible. +function _flushPresence() { + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + this.presence.inflight = this.presence.pending; + this.presence.inflightSeq = this.connection.seq; + this.presence.pending = null; + this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); + this.presence.requestReply = false; + } +} + +function _destroyPresence() { + this.presence.received = {}; + this.presence.cachedOps.length = 0; +} + +// Reset presence-related properties. +function _hardRollbackPresence() { + var pendingPresence = []; + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); + if (this.presence.pending) pendingPresence.push(this.presence.pending); + + this.presence.inflight = null; + this.presence.inflightSeq = 0; + this.presence.pending = null; + this.presence.cachedOps.length = 0; + this.presence.received = {}; + this.presence.requestReply = true; + + var srcList = Object.keys(this.presence.current); + 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); + return pendingPresence; +} + module.exports = { submitPresence, _initializePresence, @@ -345,5 +386,8 @@ module.exports = { _pausePresence, _setPresence, _emitPresence, - _cacheOp + _cacheOp, + _flushPresence, + _destroyPresence, + _hardRollbackPresence }; From 7259f7e4fe200f828a98c5fa1bb52a76a23b227c Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 17:47:59 +0530 Subject: [PATCH 121/181] Move presence methods such that they are passed into Backend --- .gitignore | 1 + lib/backend.js | 6 +- lib/client/doc.js | 42 ++-- lib/client/presence.js | 1 - lib/presence/dummy.js | 14 ++ lib/presence/stateless.js | 393 ++++++++++++++++++++++++++++++++++++++ test/client/presence.js | 3 +- 7 files changed, 434 insertions(+), 26 deletions(-) create mode 100644 lib/presence/dummy.js create mode 100644 lib/presence/stateless.js diff --git a/.gitignore b/.gitignore index 3005c1397..abd0d58e5 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ coverage # Dependency directories node_modules package-lock.json +yarn.lock jspm_packages diff --git a/lib/backend.js b/lib/backend.js index 6e3145df6..1d86a8603 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -49,7 +49,7 @@ function Backend(options) { this._shimAfterSubmit(); } - this.enablePresence = options.enablePresence; + this.Presence = options.Presence; } module.exports = Backend; emitter.mixin(Backend); @@ -158,9 +158,7 @@ Backend.prototype.connect = function(connection, req) { // code that may cache state on the agent and read it in middleware connection.agent = agent; - // Pass through information on whether or not presence is enabled, - // so that Doc instances can use it. - connection.enablePresence = this.enablePresence; + connection.Presence = this.Presence; return connection; }; diff --git a/lib/client/doc.js b/lib/client/doc.js index b134f666a..1c42b0538 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,7 +2,6 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); -var presenceMethods = require('./presence'); /** * A Doc is a client's view on a sharejs document. @@ -57,9 +56,6 @@ var presenceMethods = require('./presence'); module.exports = Doc; -// Expose presence-related methods on the Doc prototype. -Object.assign(Doc.prototype, presenceMethods); - function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -72,11 +68,17 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - // Properties related to presence are grouped within this object. - // If this.presence is falsy (undefined), it means that - // the enablePresence flag was not passed into the ShareDB constructor, - // so the presence features should be disabled. - this.presence = connection.enablePresence && this._initializePresence(); + if (connection.Presence) { + // TODO don't decorate + // Expose presence-related methods on the Doc prototype. + Object.assign(Doc.prototype, connection.Presence); + + // Properties related to presence are grouped within this object. + // If this.presence is falsy (undefined), it means that + // the enablePresence flag was not passed into the ShareDB constructor, + // so the presence features should be disabled. + this.presence = this._initializePresence(); + } // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -228,7 +230,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - this._processAllReceivedPresence(); + if (this.presence) this._processAllReceivedPresence(); callback && callback(); }; @@ -360,7 +362,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this._cacheOp({ + if (this.presence) this._cacheOp({ src: message.src, time: Date.now(), create: !!message.create, @@ -369,7 +371,7 @@ Doc.prototype._handleOp = function(err, message) { }); try { this._otApply(message, false); - this._processAllReceivedPresence(); + if (this.presence) this._processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } @@ -395,10 +397,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - this._pausePresence(); + if (this.presence) this._pausePresence(); callEach(callbacks); } else { - this._pausePresence(); + if (this.presence) this._pausePresence(); } } }; @@ -600,7 +602,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); + if (this.presence) this._transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -613,7 +615,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); + if (this.presence) this._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. @@ -630,7 +632,7 @@ 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); + if (this.presence) this._transformAllPresence(op); this.emit('create', source); return; } @@ -638,7 +640,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - this._transformAllPresence(op); + if (this.presence) this._transformAllPresence(op); this.emit('del', oldData, source); return; } @@ -906,7 +908,7 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - this._cacheOp({ + if (this.presence) this._cacheOp({ src: this.inflightOp.src, time: Date.now(), create: !!this.inflightOp.create, @@ -915,7 +917,7 @@ Doc.prototype._opAcknowledged = function(message) { }); this._clearInflightOp(); - this._processAllReceivedPresence(); + if (this.presence) this._processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { diff --git a/lib/client/presence.js b/lib/client/presence.js index 91f635758..20b522008 100644 --- a/lib/client/presence.js +++ b/lib/client/presence.js @@ -229,7 +229,6 @@ function _processReceivedPresence(src, emit) { } function _processAllReceivedPresence() { - if (!this.presence) return; var srcList = Object.keys(this.presence.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js new file mode 100644 index 000000000..9fe310999 --- /dev/null +++ b/lib/presence/dummy.js @@ -0,0 +1,14 @@ +function DummyPresence () { } +function noop () {} + +DummyPresence.prototype.flushPresence = noop; +DummyPresence.prototype.destroyPresence = noop; +DummyPresence.prototype.clearCachedOps = noop; // this.presence.cachedOps.length = 0; +DummyPresence.prototype.processAllReceivedPresence = noop; +DummyPresence.prototype.hardRollbackPresence = function () { return []; }; +DummyPresence.prototype.transformAllPresence = noop; +DummyPresence.prototype.cacheOp = noop; +DummyPresence.prototype.hasPending = function () { return false }; // (this.presence.inflight || this.presence.pending) +DummyPresence.prototype.pause = noop; + +module.exports = DummyPresence; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js new file mode 100644 index 000000000..91f635758 --- /dev/null +++ b/lib/presence/stateless.js @@ -0,0 +1,393 @@ +/* + * Presence Methods + * ---------------- + * + * This module contains definitions for presence-related methods + * that are added as methods to the Doc prototype (e.g. doc.submitPresence). + * + * The value of 'this' in these functions will be the Doc instance. + */ +var ShareDBError = require('../error'); + +// Submit presence data to a document. +// This is the only public facing method. +// All the others are marked as internal with a leading "_". +function submitPresence(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); + }); + } + + if (!this.type.createPresence || !this.type.transformPresence) { + var doc = this; + return process.nextTick(function() { + var err = new ShareDBError(4027, '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.type.createPresence(data); + } + + if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { + if (!this.presence.pending) { + this.presence.pending = []; + } + if (callback) { + this.presence.pending.push(callback); + } + + } else if (callback) { + process.nextTick(callback); + } + + process.nextTick(this.flush.bind(this)); +} + +// This function generates the initial value for doc.presence. +function _initializePresence() { + + // Return a new object each time, otherwise mutations would bleed across documents. + return { + + // The current presence data. + // Map of src -> presence data + // Local src === '' + current: {}, + + // The presence objects received from the server. + // Map of src -> presence + 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. + 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. + 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. + 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. + cachedOpsTimeout: 60000, + + // The sequence number of the inflight presence request. + inflightSeq: 0, + + // Callbacks (or null) for pending and inflight presence requests. + pending: null, + inflight: null + }; +} + +function _handlePresence(err, presence) { + if (!this.subscribed) return; + + var src = presence.src; + if (!src) { + // Handle the ACK for the presence data we submitted. + // this.presence.inflightSeq would not equal presence.seq after a hard rollback, + // when all callbacks are flushed with an error. + if (this.presence.inflightSeq === presence.seq) { + var callbacks = this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + var called = callbacks && this._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.presence.pending) { + // Another client requested us to share our current presence data + this.presence.pending = []; + this.flush(); + } + + // Ignore older messages which arrived out of order + if ( + this.presence.received[src] && ( + this.presence.received[src].seq > presence.seq || + (this.presence.received[src].seq === presence.seq && presence.v != null) + ) + ) return; + + this.presence.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.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. +function _processReceivedPresence(src, emit) { + if (!src) return false; + var presence = this.presence.received[src]; + if (!presence) return false; + + if (presence.processedAt != null) { + if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { + // Remove old received and processed presence. + delete this.presence.received[src]; + } + return false; + } + + if (this.version == null || this.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.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 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.pendingOps.length; i++) { + if (this.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.presence.cachedOps.length - (this.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.presence.cachedOps.length; i++) { + if (this.presence.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.type.createPresence(presence.p); + + // Transform against past ops + for (var i = startIndex; i < this.presence.cachedOps.length; i++) { + var op = this.presence.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); +} + +function _processAllReceivedPresence() { + if (!this.presence) return; + var srcList = Object.keys(this.presence.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); +} + +function _transformPresence(src, op) { + var presenceData = this.presence.current[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); +} + +function _transformAllPresence(op) { + if (!this.presence) return; + var srcList = Object.keys(this.presence.current); + 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); +} + +function _pausePresence() { + if (!this.presence) return; + + if (this.presence.inflight) { + this.presence.pending = this.presence.pending + ? this.presence.inflight.concat(this.presence.pending) + : this.presence.inflight; + this.presence.inflight = null; + this.presence.inflightSeq = 0; + } else if (!this.presence.pending && this.presence.current[''] != null) { + this.presence.pending = []; + } + this.presence.received = {}; + this.presence.requestReply = true; + var srcList = Object.keys(this.presence.current); + 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. +function _setPresence(src, data, emit) { + if (data == null) { + if (this.presence.current[src] == null) return false; + delete this.presence.current[src]; + } else { + var isPresenceEqual = + this.presence.current[src] === data || + (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); + if (isPresenceEqual) return false; + this.presence.current[src] = data; + } + if (emit) this._emitPresence([ src ], true); + return true; +} + +function _emitPresence(srcList, submitted) { + if (srcList && srcList.length > 0) { + var doc = this; + process.nextTick(function() { + doc.emit('presence', srcList, submitted); + }); + } +} + +function _cacheOp(op) { + if (!this.presence) return; + // Remove the old ops. + var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; + var i; + for (i = 0; i < this.presence.cachedOps.length; i++) { + if (this.presence.cachedOps[i].time >= oldOpTime) { + break; + } + } + if (i > 0) { + this.presence.cachedOps.splice(0, i); + } + + // Cache the new op. + this.presence.cachedOps.push(op); +} + +// If there are no pending ops, this method sends the pending presence data, if possible. +function _flushPresence() { + if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { + this.presence.inflight = this.presence.pending; + this.presence.inflightSeq = this.connection.seq; + this.presence.pending = null; + this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); + this.presence.requestReply = false; + } +} + +function _destroyPresence() { + this.presence.received = {}; + this.presence.cachedOps.length = 0; +} + +// Reset presence-related properties. +function _hardRollbackPresence() { + var pendingPresence = []; + if (this.presence.inflight) pendingPresence.push(this.presence.inflight); + if (this.presence.pending) pendingPresence.push(this.presence.pending); + + this.presence.inflight = null; + this.presence.inflightSeq = 0; + this.presence.pending = null; + this.presence.cachedOps.length = 0; + this.presence.received = {}; + this.presence.requestReply = true; + + var srcList = Object.keys(this.presence.current); + 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); + return pendingPresence; +} + +module.exports = { + submitPresence, + _initializePresence, + _handlePresence, + _processReceivedPresence, + _processAllReceivedPresence, + _transformPresence, + _transformAllPresence, + _pausePresence, + _setPresence, + _emitPresence, + _cacheOp, + _flushPresence, + _destroyPresence, + _hardRollbackPresence +}; diff --git a/test/client/presence.js b/test/client/presence.js index aff5ab887..f5ee3f1fa 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,6 +3,7 @@ var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); +var StatelessPresence = require('../../lib/presence/stateless'); var ShareDBError = require('../../lib/error'); var expect = require('expect.js'); var types = require('../../lib/types'); @@ -31,7 +32,7 @@ describe('client presence', function() { describe('client presence (' + typeName + ')', function() { beforeEach(function() { - this.backend = new Backend({ enablePresence: true }); + this.backend = new Backend({ Presence: StatelessPresence }); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); From 09f641507ce44ac3d8763dc3c70971079925ffaa Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:22:34 +0530 Subject: [PATCH 122/181] Migrate hardRollbackPresence to presence instance --- lib/client/doc.js | 7 +- lib/client/presence.js | 392 -------------------------------------- lib/presence/stateless.js | 26 +-- 3 files changed, 19 insertions(+), 406 deletions(-) delete mode 100644 lib/client/presence.js diff --git a/lib/client/doc.js b/lib/client/doc.js index 1c42b0538..3513826c6 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -78,6 +78,11 @@ function Doc(connection, collection, id) { // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled. this.presence = this._initializePresence(); + + this.presence.doc = this; + + this.presence.hardRollbackPresence = connection.Presence.hardRollbackPresence; + delete Doc.prototype.hardRollbackPresence; } // Array of callbacks or nulls as placeholders @@ -966,7 +971,7 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence ? this._hardRollbackPresence() : []; + var pendingPresence = this.presence ? this.presence.hardRollbackPresence() : []; // Cancel all pending ops and reset if we can't invert this._setType(null); diff --git a/lib/client/presence.js b/lib/client/presence.js deleted file mode 100644 index 20b522008..000000000 --- a/lib/client/presence.js +++ /dev/null @@ -1,392 +0,0 @@ -/* - * Presence Methods - * ---------------- - * - * This module contains definitions for presence-related methods - * that are added as methods to the Doc prototype (e.g. doc.submitPresence). - * - * The value of 'this' in these functions will be the Doc instance. - */ -var ShareDBError = require('../error'); - -// Submit presence data to a document. -// This is the only public facing method. -// All the others are marked as internal with a leading "_". -function submitPresence(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); - }); - } - - if (!this.type.createPresence || !this.type.transformPresence) { - var doc = this; - return process.nextTick(function() { - var err = new ShareDBError(4027, '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.type.createPresence(data); - } - - if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { - if (!this.presence.pending) { - this.presence.pending = []; - } - if (callback) { - this.presence.pending.push(callback); - } - - } else if (callback) { - process.nextTick(callback); - } - - process.nextTick(this.flush.bind(this)); -} - -// This function generates the initial value for doc.presence. -function _initializePresence() { - - // Return a new object each time, otherwise mutations would bleed across documents. - return { - - // The current presence data. - // Map of src -> presence data - // Local src === '' - current: {}, - - // The presence objects received from the server. - // Map of src -> presence - 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. - 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. - 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. - 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. - cachedOpsTimeout: 60000, - - // The sequence number of the inflight presence request. - inflightSeq: 0, - - // Callbacks (or null) for pending and inflight presence requests. - pending: null, - inflight: null - }; -} - -function _handlePresence(err, presence) { - if (!this.subscribed) return; - - var src = presence.src; - if (!src) { - // Handle the ACK for the presence data we submitted. - // this.presence.inflightSeq would not equal presence.seq after a hard rollback, - // when all callbacks are flushed with an error. - if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - var called = callbacks && this._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.presence.pending) { - // Another client requested us to share our current presence data - this.presence.pending = []; - this.flush(); - } - - // Ignore older messages which arrived out of order - if ( - this.presence.received[src] && ( - this.presence.received[src].seq > presence.seq || - (this.presence.received[src].seq === presence.seq && presence.v != null) - ) - ) return; - - this.presence.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.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. -function _processReceivedPresence(src, emit) { - if (!src) return false; - var presence = this.presence.received[src]; - if (!presence) return false; - - if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { - // Remove old received and processed presence. - delete this.presence.received[src]; - } - return false; - } - - if (this.version == null || this.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.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 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.pendingOps.length; i++) { - if (this.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.presence.cachedOps.length - (this.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.presence.cachedOps.length; i++) { - if (this.presence.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.type.createPresence(presence.p); - - // Transform against past ops - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - var op = this.presence.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); -} - -function _processAllReceivedPresence() { - var srcList = Object.keys(this.presence.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); -} - -function _transformPresence(src, op) { - var presenceData = this.presence.current[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); -} - -function _transformAllPresence(op) { - if (!this.presence) return; - var srcList = Object.keys(this.presence.current); - 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); -} - -function _pausePresence() { - if (!this.presence) return; - - if (this.presence.inflight) { - this.presence.pending = this.presence.pending - ? this.presence.inflight.concat(this.presence.pending) - : this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - } else if (!this.presence.pending && this.presence.current[''] != null) { - this.presence.pending = []; - } - this.presence.received = {}; - this.presence.requestReply = true; - var srcList = Object.keys(this.presence.current); - 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. -function _setPresence(src, data, emit) { - if (data == null) { - if (this.presence.current[src] == null) return false; - delete this.presence.current[src]; - } else { - var isPresenceEqual = - this.presence.current[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); - if (isPresenceEqual) return false; - this.presence.current[src] = data; - } - if (emit) this._emitPresence([ src ], true); - return true; -} - -function _emitPresence(srcList, submitted) { - if (srcList && srcList.length > 0) { - var doc = this; - process.nextTick(function() { - doc.emit('presence', srcList, submitted); - }); - } -} - -function _cacheOp(op) { - if (!this.presence) return; - // Remove the old ops. - var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; - var i; - for (i = 0; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].time >= oldOpTime) { - break; - } - } - if (i > 0) { - this.presence.cachedOps.splice(0, i); - } - - // Cache the new op. - this.presence.cachedOps.push(op); -} - -// If there are no pending ops, this method sends the pending presence data, if possible. -function _flushPresence() { - if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { - this.presence.inflight = this.presence.pending; - this.presence.inflightSeq = this.connection.seq; - this.presence.pending = null; - this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); - this.presence.requestReply = false; - } -} - -function _destroyPresence() { - this.presence.received = {}; - this.presence.cachedOps.length = 0; -} - -// Reset presence-related properties. -function _hardRollbackPresence() { - var pendingPresence = []; - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); - - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; - - var srcList = Object.keys(this.presence.current); - 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); - return pendingPresence; -} - -module.exports = { - submitPresence, - _initializePresence, - _handlePresence, - _processReceivedPresence, - _processAllReceivedPresence, - _transformPresence, - _transformAllPresence, - _pausePresence, - _setPresence, - _emitPresence, - _cacheOp, - _flushPresence, - _destroyPresence, - _hardRollbackPresence -}; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 91f635758..fd7136aea 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -351,27 +351,27 @@ function _destroyPresence() { } // Reset presence-related properties. -function _hardRollbackPresence() { +function hardRollbackPresence() { var pendingPresence = []; - if (this.presence.inflight) pendingPresence.push(this.presence.inflight); - if (this.presence.pending) pendingPresence.push(this.presence.pending); + if (this.inflight) pendingPresence.push(this.inflight); + if (this.pending) pendingPresence.push(this.pending); - this.presence.inflight = null; - this.presence.inflightSeq = 0; - this.presence.pending = null; - this.presence.cachedOps.length = 0; - this.presence.received = {}; - this.presence.requestReply = true; + this.inflight = null; + this.inflightSeq = 0; + this.pending = null; + this.cachedOps.length = 0; + this.received = {}; + this.requestReply = true; - var srcList = Object.keys(this.presence.current); + var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this._setPresence(src, null)) { + if (this.doc._setPresence(src, null)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, false); + this.doc._emitPresence(changedSrcList, false); return pendingPresence; } @@ -389,5 +389,5 @@ module.exports = { _cacheOp, _flushPresence, _destroyPresence, - _hardRollbackPresence + hardRollbackPresence }; From 23a06c348764e9f135f0f0a9dade1f938422e9c5 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:30:18 +0530 Subject: [PATCH 123/181] Migrate _initializePresence --- lib/client/doc.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 3513826c6..6115ad4b4 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -73,16 +73,18 @@ function Doc(connection, collection, id) { // Expose presence-related methods on the Doc prototype. Object.assign(Doc.prototype, connection.Presence); + delete Doc.prototype.hardRollbackPresence; + delete Doc.prototype._initializePresence; + // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled. - this.presence = this._initializePresence(); + this.presence = connection.Presence._initializePresence(); this.presence.doc = this; + Object.assign(this.presence, connection.Presence); - this.presence.hardRollbackPresence = connection.Presence.hardRollbackPresence; - delete Doc.prototype.hardRollbackPresence; } // Array of callbacks or nulls as placeholders From 824346ffcb5662ed1e9b62f753aa8b3e375561d9 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:51:25 +0530 Subject: [PATCH 124/181] Migrate _handlePresence --- lib/client/doc.js | 6 +++++ lib/presence/stateless.js | 46 +++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 6115ad4b4..582bcec51 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -76,10 +76,16 @@ function Doc(connection, collection, id) { delete Doc.prototype.hardRollbackPresence; delete Doc.prototype._initializePresence; + Doc.prototype._handlePresence = function(err, presence) { + this.presence.handlePresence(err, presence); + }; + // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that // the enablePresence flag was not passed into the ShareDB constructor, // so the presence features should be disabled. + // + // TODO convert to constructor. this.presence = connection.Presence._initializePresence(); this.presence.doc = this; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index fd7136aea..a5eb63557 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -92,56 +92,56 @@ function _initializePresence() { }; } -function _handlePresence(err, presence) { - if (!this.subscribed) return; +function handlePresence(err, presence) { + if (!this.doc.subscribed) return; var src = presence.src; if (!src) { // Handle the ACK for the presence data we submitted. - // this.presence.inflightSeq would not equal presence.seq after a hard rollback, + // this.inflightSeq would not equal presence.seq after a hard rollback, // when all callbacks are flushed with an error. - if (this.presence.inflightSeq === presence.seq) { - var callbacks = this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - var called = callbacks && this._callEach(callbacks, err); - if (err && !called) this.emit('error', err); - this.flush(); - this._emitNothingPending(); + if (this.inflightSeq === presence.seq) { + var callbacks = this.inflight; + this.inflight = null; + this.inflightSeq = 0; + var called = callbacks && this.doc._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.emit('error', err); + if (err) return this.doc.emit('error', err); - if (presence.r && !this.presence.pending) { + if (presence.r && !this.pending) { // Another client requested us to share our current presence data - this.presence.pending = []; - this.flush(); + this.pending = []; + this.doc.flush(); } // Ignore older messages which arrived out of order if ( - this.presence.received[src] && ( - this.presence.received[src].seq > presence.seq || - (this.presence.received[src].seq === presence.seq && presence.v != null) + this.received[src] && ( + this.received[src].seq > presence.seq || + (this.received[src].seq === presence.seq && presence.v != null) ) ) return; - this.presence.received[src] = presence; + 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); + return this.doc._setPresence(src, null, true); } // Get missing ops first, if necessary - if (this.version == null || this.version < presence.v) return this.fetch(); + if (this.doc.version == null || this.doc.version < presence.v) return this.doc.fetch(); - this._processReceivedPresence(src, true); + this.doc._processReceivedPresence(src, true); } // If emit is true and presence has changed, emits a presence event. @@ -377,8 +377,8 @@ function hardRollbackPresence() { module.exports = { submitPresence, + handlePresence, _initializePresence, - _handlePresence, _processReceivedPresence, _processAllReceivedPresence, _transformPresence, From d6e3e3d6e5ac7e897b4c4c5a6d8d13af21d4a3b7 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 18:56:07 +0530 Subject: [PATCH 125/181] Migrate _processReceivedPresence --- lib/client/doc.js | 2 ++ lib/presence/stateless.js | 56 +++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 28 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 582bcec51..f545a6aa2 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -75,7 +75,9 @@ function Doc(connection, collection, id) { delete Doc.prototype.hardRollbackPresence; delete Doc.prototype._initializePresence; + delete Doc.prototype._processReceivedPresence; + // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { this.presence.handlePresence(err, presence); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index a5eb63557..56734d5f3 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -141,25 +141,25 @@ function handlePresence(err, presence) { // Get missing ops first, if necessary if (this.doc.version == null || this.doc.version < presence.v) return this.doc.fetch(); - this.doc._processReceivedPresence(src, true); + 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. function _processReceivedPresence(src, emit) { if (!src) return false; - var presence = this.presence.received[src]; + var presence = this.received[src]; if (!presence) return false; if (presence.processedAt != null) { - if (Date.now() >= presence.processedAt + this.presence.receivedTimeout) { + if (Date.now() >= presence.processedAt + this.receivedTimeout) { // Remove old received and processed presence. - delete this.presence.received[src]; + delete this.received[src]; } return false; } - if (this.version == null || this.version < presence.v) { + if (this.doc.version == null || this.doc.version < presence.v) { // keep waiting for the missing snapshot or ops. return false; } @@ -167,65 +167,65 @@ function _processReceivedPresence(src, emit) { if (presence.p == null) { // Remove presence data as requested. presence.processedAt = Date.now(); - return this._setPresence(src, null, emit); + return this.doc._setPresence(src, null, emit); } - if (!this.type || !this.type.createPresence || !this.type.transformPresence) { + 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); + return this.doc._setPresence(src, null, emit); } - if (this.inflightOp && this.inflightOp.op == null) { + 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); + return this.doc._setPresence(src, null, emit); } - for (var i = 0; i < this.pendingOps.length; i++) { - if (this.pendingOps[i].op == null) { + 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); + return this.doc._setPresence(src, null, emit); } } - var startIndex = this.presence.cachedOps.length - (this.version - presence.v); + 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); + return this.doc._setPresence(src, null, emit); } - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].op == null) { + 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); + return this.doc._setPresence(src, null, emit); } } // Make sure the format of the data is correct - var data = this.type.createPresence(presence.p); + var data = this.doc.type.createPresence(presence.p); // Transform against past ops - for (var i = startIndex; i < this.presence.cachedOps.length; i++) { - var op = this.presence.cachedOps[i]; - data = this.type.transformPresence(data, op.op, presence.src === op.src); + 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.inflightOp) { - data = this.type.transformPresence(data, this.inflightOp.op, false); + if (this.doc.inflightOp) { + data = this.doc.type.transformPresence(data, this.doc.inflightOp.op, false); } - for (var i = 0; i < this.pendingOps.length; i++) { - data = this.type.transformPresence(data, this.pendingOps[i].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); + return this.doc._setPresence(src, data, emit); } function _processAllReceivedPresence() { @@ -234,7 +234,7 @@ function _processAllReceivedPresence() { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this._processReceivedPresence(src)) { + if (this.presence._processReceivedPresence(src)) { changedSrcList.push(src); } } From 0382c03a9749fac082a37211dd126de9f32aeb6b Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:02:56 +0530 Subject: [PATCH 126/181] Migrate processAllReceivedPresence --- lib/client/doc.js | 8 +++++--- lib/presence/stateless.js | 11 +++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index f545a6aa2..691e59ffd 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -77,6 +77,8 @@ function Doc(connection, collection, id) { delete Doc.prototype._initializePresence; delete Doc.prototype._processReceivedPresence; + delete Doc.prototype.processAllReceivedPresence; + // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { this.presence.handlePresence(err, presence); @@ -245,7 +247,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - if (this.presence) this._processAllReceivedPresence(); + if (this.presence) this.presence.processAllReceivedPresence(); callback && callback(); }; @@ -386,7 +388,7 @@ Doc.prototype._handleOp = function(err, message) { }); try { this._otApply(message, false); - if (this.presence) this._processAllReceivedPresence(); + if (this.presence) this.presence.processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } @@ -932,7 +934,7 @@ Doc.prototype._opAcknowledged = function(message) { }); this._clearInflightOp(); - if (this.presence) this._processAllReceivedPresence(); + if (this.presence) this.presence.processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 56734d5f3..a8efc9681 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -228,17 +228,16 @@ function _processReceivedPresence(src, emit) { return this.doc._setPresence(src, data, emit); } -function _processAllReceivedPresence() { - if (!this.presence) return; - var srcList = Object.keys(this.presence.received); +function processAllReceivedPresence() { + var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this.presence._processReceivedPresence(src)) { + if (this._processReceivedPresence(src)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, true); + this.doc._emitPresence(changedSrcList, true); } function _transformPresence(src, op) { @@ -380,7 +379,7 @@ module.exports = { handlePresence, _initializePresence, _processReceivedPresence, - _processAllReceivedPresence, + processAllReceivedPresence, _transformPresence, _transformAllPresence, _pausePresence, From 46d8a1b6098e39087e9c0236daf8ca431e3741aa Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:06:08 +0530 Subject: [PATCH 127/181] Migrate _transformPresence --- lib/client/doc.js | 2 +- lib/presence/stateless.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 691e59ffd..1931ae718 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -76,8 +76,8 @@ function Doc(connection, collection, id) { delete Doc.prototype.hardRollbackPresence; delete Doc.prototype._initializePresence; delete Doc.prototype._processReceivedPresence; - delete Doc.prototype.processAllReceivedPresence; + delete Doc.prototype._transformPresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index a8efc9681..c281ba8f8 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -241,14 +241,14 @@ function processAllReceivedPresence() { } function _transformPresence(src, op) { - var presenceData = this.presence.current[src]; + var presenceData = this.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); - presenceData = this.type.transformPresence(presenceData, op.op, isOwnOperation); + presenceData = this.doc.type.transformPresence(presenceData, op.op, isOwnOperation); } else { presenceData = null; } - return this._setPresence(src, presenceData); + return this.doc._setPresence(src, presenceData); } function _transformAllPresence(op) { @@ -257,7 +257,7 @@ function _transformAllPresence(op) { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this._transformPresence(src, op)) { + if (this.presence._transformPresence(src, op)) { changedSrcList.push(src); } } From fc16be76946717c6bdd24ccd51f22b4a7ee41978 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:47:36 +0530 Subject: [PATCH 128/181] Migrate pausePresence --- lib/client/doc.js | 10 ++++++---- lib/presence/stateless.js | 34 +++++++++++++++++----------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 1931ae718..96e39b674 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -78,6 +78,7 @@ function Doc(connection, collection, id) { delete Doc.prototype._processReceivedPresence; delete Doc.prototype.processAllReceivedPresence; delete Doc.prototype._transformPresence; + delete Doc.prototype.pausePresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { @@ -414,10 +415,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - if (this.presence) this._pausePresence(); + if (this.presence) this.presence.pausePresence(); callEach(callbacks); } else { - if (this.presence) this._pausePresence(); + if (this.presence) this.presence.pausePresence(); } } }; @@ -477,10 +478,11 @@ Doc.prototype.unsubscribe = function(callback) { if (this.connection.canSend) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); - this._pausePresence(); + + if (this.presence) this.presence.pausePresence(); return; } - this._pausePresence(); + if (this.presence) this.presence.pausePresence(); if (callback) process.nextTick(callback); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index c281ba8f8..b5addc6a4 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -264,29 +264,29 @@ function _transformAllPresence(op) { this._emitPresence(changedSrcList, false); } -function _pausePresence() { - if (!this.presence) return; - - if (this.presence.inflight) { - this.presence.pending = this.presence.pending - ? this.presence.inflight.concat(this.presence.pending) - : this.presence.inflight; - this.presence.inflight = null; - this.presence.inflightSeq = 0; - } else if (!this.presence.pending && this.presence.current[''] != null) { - this.presence.pending = []; +function pausePresence() { + 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.current[''] != null) { + this.pending = []; } - this.presence.received = {}; - this.presence.requestReply = true; - var srcList = Object.keys(this.presence.current); + this.received = {}; + this.requestReply = true; + var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (src && this._setPresence(src, null)) { + if (src && this.doc._setPresence(src, null)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, false); + this.doc._emitPresence(changedSrcList, false); } // If emit is true and presence has changed, emits a presence event. @@ -382,7 +382,7 @@ module.exports = { processAllReceivedPresence, _transformPresence, _transformAllPresence, - _pausePresence, + pausePresence, _setPresence, _emitPresence, _cacheOp, From 9cb5564d1b3b894ec3f963ad77abe529b3cd6cfd Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 19:59:36 +0530 Subject: [PATCH 129/181] Migrate cacheOp --- lib/client/doc.js | 5 +++-- lib/presence/dummy.js | 1 + lib/presence/stateless.js | 15 +++++++-------- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 96e39b674..446e1199b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -79,6 +79,7 @@ function Doc(connection, collection, id) { delete Doc.prototype.processAllReceivedPresence; delete Doc.prototype._transformPresence; delete Doc.prototype.pausePresence; + delete Doc.prototype.cacheOp; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { @@ -380,7 +381,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - if (this.presence) this._cacheOp({ + if (this.presence) this.presence.cacheOp({ src: message.src, time: Date.now(), create: !!message.create, @@ -927,7 +928,7 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - if (this.presence) this._cacheOp({ + if (this.presence) this.presence.cacheOp({ src: this.inflightOp.src, time: Date.now(), create: !!this.inflightOp.create, diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 9fe310999..ac328f67a 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -1,3 +1,4 @@ +// TODO use this function DummyPresence () { } function noop () {} diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index b5addc6a4..1166306b0 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -315,22 +315,21 @@ function _emitPresence(srcList, submitted) { } } -function _cacheOp(op) { - if (!this.presence) return; +function cacheOp(op) { // Remove the old ops. - var oldOpTime = Date.now() - this.presence.cachedOpsTimeout; + var oldOpTime = Date.now() - this.cachedOpsTimeout; var i; - for (i = 0; i < this.presence.cachedOps.length; i++) { - if (this.presence.cachedOps[i].time >= oldOpTime) { + for (i = 0; i < this.cachedOps.length; i++) { + if (this.cachedOps[i].time >= oldOpTime) { break; } } if (i > 0) { - this.presence.cachedOps.splice(0, i); + this.cachedOps.splice(0, i); } // Cache the new op. - this.presence.cachedOps.push(op); + this.cachedOps.push(op); } // If there are no pending ops, this method sends the pending presence data, if possible. @@ -385,7 +384,7 @@ module.exports = { pausePresence, _setPresence, _emitPresence, - _cacheOp, + cacheOp, _flushPresence, _destroyPresence, hardRollbackPresence From 6461a79a66c49f464e9d188c2eca560f53ac4092 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:00:56 +0530 Subject: [PATCH 130/181] Migrate flushPresence --- lib/client/doc.js | 3 ++- lib/presence/stateless.js | 16 ++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 446e1199b..d354bf18c 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -80,6 +80,7 @@ function Doc(connection, collection, id) { delete Doc.prototype._transformPresence; delete Doc.prototype.pausePresence; delete Doc.prototype.cacheOp; + delete Doc.prototype._flushPresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { @@ -516,7 +517,7 @@ Doc.prototype.flush = function() { } if (this.presence) { - this._flushPresence(); + this.presence.flushPresence(); } }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 1166306b0..99b67fb37 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -333,13 +333,13 @@ function cacheOp(op) { } // If there are no pending ops, this method sends the pending presence data, if possible. -function _flushPresence() { - if (this.subscribed && !this.presence.inflight && this.presence.pending && !this.hasWritePending()) { - this.presence.inflight = this.presence.pending; - this.presence.inflightSeq = this.connection.seq; - this.presence.pending = null; - this.connection.sendPresence(this, this.presence.current[''], this.presence.requestReply); - this.presence.requestReply = false; +function flushPresence() { + if (this.doc.subscribed && !this.inflight && this.pending && !this.doc.hasWritePending()) { + this.inflight = this.pending; + this.inflightSeq = this.doc.connection.seq; + this.pending = null; + this.doc.connection.sendPresence(this.doc, this.current[''], this.requestReply); + this.requestReply = false; } } @@ -385,7 +385,7 @@ module.exports = { _setPresence, _emitPresence, cacheOp, - _flushPresence, + flushPresence, _destroyPresence, hardRollbackPresence }; From 2358022223fb2735d2a5d755e49b2aeb802b6d8e Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:03:40 +0530 Subject: [PATCH 131/181] Migrate transformAllPresence --- lib/client/doc.js | 9 +++++---- lib/presence/stateless.js | 11 +++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index d354bf18c..c66c4240b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -78,6 +78,7 @@ function Doc(connection, collection, id) { delete Doc.prototype._processReceivedPresence; delete Doc.prototype.processAllReceivedPresence; delete Doc.prototype._transformPresence; + delete Doc.prototype.transformAllPresence; delete Doc.prototype.pausePresence; delete Doc.prototype.cacheOp; delete Doc.prototype._flushPresence; @@ -623,7 +624,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); - if (this.presence) this._transformAllPresence(componentOp); + if (this.presence) this.presence.transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -636,7 +637,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); - if (this.presence) this._transformAllPresence(op); + if (this.presence) this.presence.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. @@ -653,7 +654,7 @@ 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); - if (this.presence) this._transformAllPresence(op); + if (this.presence) this.presence.transformAllPresence(op); this.emit('create', source); return; } @@ -661,7 +662,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - if (this.presence) this._transformAllPresence(op); + if (this.presence) this.presence.transformAllPresence(op); this.emit('del', oldData, source); return; } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 99b67fb37..7c1f2d780 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -251,17 +251,16 @@ function _transformPresence(src, op) { return this.doc._setPresence(src, presenceData); } -function _transformAllPresence(op) { - if (!this.presence) return; - var srcList = Object.keys(this.presence.current); +function transformAllPresence(op) { + var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this.presence._transformPresence(src, op)) { + if (this._transformPresence(src, op)) { changedSrcList.push(src); } } - this._emitPresence(changedSrcList, false); + this.doc._emitPresence(changedSrcList, false); } function pausePresence() { @@ -380,7 +379,7 @@ module.exports = { _processReceivedPresence, processAllReceivedPresence, _transformPresence, - _transformAllPresence, + transformAllPresence, pausePresence, _setPresence, _emitPresence, From 41a474373d09347f6790c17592e9e3cf8981137b Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:07:19 +0530 Subject: [PATCH 132/181] Migrate emitPresence --- lib/client/doc.js | 1 + lib/presence/stateless.js | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c66c4240b..eddc55259 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -82,6 +82,7 @@ function Doc(connection, collection, id) { delete Doc.prototype.pausePresence; delete Doc.prototype.cacheOp; delete Doc.prototype._flushPresence; + delete Doc.prototype._emitPresence; // TODO move this with other _handle... definitions. Doc.prototype._handlePresence = function(err, presence) { diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 7c1f2d780..12a746da1 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -237,7 +237,7 @@ function processAllReceivedPresence() { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, true); + this._emitPresence(changedSrcList, true); } function _transformPresence(src, op) { @@ -260,7 +260,7 @@ function transformAllPresence(op) { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, false); + this._emitPresence(changedSrcList, false); } function pausePresence() { @@ -285,7 +285,7 @@ function pausePresence() { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, false); + this._emitPresence(changedSrcList, false); } // If emit is true and presence has changed, emits a presence event. @@ -301,13 +301,13 @@ function _setPresence(src, data, emit) { if (isPresenceEqual) return false; this.presence.current[src] = data; } - if (emit) this._emitPresence([ src ], true); + if (emit) this.presence._emitPresence([ src ], true); return true; } function _emitPresence(srcList, submitted) { if (srcList && srcList.length > 0) { - var doc = this; + var doc = this.doc; process.nextTick(function() { doc.emit('presence', srcList, submitted); }); @@ -368,7 +368,7 @@ function hardRollbackPresence() { changedSrcList.push(src); } } - this.doc._emitPresence(changedSrcList, false); + this._emitPresence(changedSrcList, false); return pendingPresence; } From ba7d8802458bd6f2d8a65cb28add03fe3aeb5c13 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:10:09 +0530 Subject: [PATCH 133/181] Migrate submitPresence --- lib/client/doc.js | 7 ++++++- lib/presence/stateless.js | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index eddc55259..ef581ede0 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -83,9 +83,14 @@ function Doc(connection, collection, id) { delete Doc.prototype.cacheOp; delete Doc.prototype._flushPresence; delete Doc.prototype._emitPresence; + delete Doc.prototype.submitPresence; + + Doc.prototype.submitPresence = function (data, callback) { + this.presence.submitPresence(data, callback); + }; // TODO move this with other _handle... definitions. - Doc.prototype._handlePresence = function(err, presence) { + Doc.prototype._handlePresence = function (err, presence) { this.presence.handlePresence(err, presence); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 12a746da1..20b005301 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -14,8 +14,8 @@ var ShareDBError = require('../error'); // All the others are marked as internal with a leading "_". function submitPresence(data, callback) { if (data != null) { - if (!this.type) { - var doc = this; + 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); @@ -23,8 +23,8 @@ function submitPresence(data, callback) { }); } - if (!this.type.createPresence || !this.type.transformPresence) { - var doc = this; + if (!this.doc.type.createPresence || !this.doc.type.transformPresence) { + var doc = this.doc; return process.nextTick(function() { var err = new ShareDBError(4027, 'Cannot submit presence. Document\'s type does not support presence. ' + doc.collection + '.' + doc.id); if (callback) return callback(err); @@ -32,22 +32,22 @@ function submitPresence(data, callback) { }); } - data = this.type.createPresence(data); + data = this.doc.type.createPresence(data); } - if (this._setPresence('', data, true) || this.presence.pending || this.presence.inflight) { - if (!this.presence.pending) { - this.presence.pending = []; + if (this.doc._setPresence('', data, true) || this.pending || this.inflight) { + if (!this.pending) { + this.pending = []; } if (callback) { - this.presence.pending.push(callback); + this.pending.push(callback); } } else if (callback) { process.nextTick(callback); } - process.nextTick(this.flush.bind(this)); + process.nextTick(this.doc.flush.bind(this.doc)); } // This function generates the initial value for doc.presence. From 3ffd1abbccff6ca366d85520b563ae98e0f685ec Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:13:37 +0530 Subject: [PATCH 134/181] Migrate _setPresence --- lib/client/doc.js | 3 +++ lib/presence/stateless.js | 47 ++++++++++++++++++++------------------- 2 files changed, 27 insertions(+), 23 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index ef581ede0..c6e3a1f4c 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -84,7 +84,10 @@ function Doc(connection, collection, id) { delete Doc.prototype._flushPresence; delete Doc.prototype._emitPresence; delete Doc.prototype.submitPresence; + delete Doc.prototype._destroyPresence; + delete Doc.prototype._setPresence; + // TODO move this outside Doc.prototype.submitPresence = function (data, callback) { this.presence.submitPresence(data, callback); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 20b005301..87e59654a 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -35,7 +35,7 @@ function submitPresence(data, callback) { data = this.doc.type.createPresence(data); } - if (this.doc._setPresence('', data, true) || this.pending || this.inflight) { + if (this._setPresence('', data, true) || this.pending || this.inflight) { if (!this.pending) { this.pending = []; } @@ -135,7 +135,7 @@ function handlePresence(err, presence) { // null version should happen only when the server automatically sends // null presence for an unsubscribed client presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, true); + return this._setPresence(src, null, true); } // Get missing ops first, if necessary @@ -167,26 +167,26 @@ function _processReceivedPresence(src, emit) { if (presence.p == null) { // Remove presence data as requested. presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + 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.doc._setPresence(src, null, emit); + 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.doc._setPresence(src, null, emit); + 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.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } } @@ -194,14 +194,14 @@ function _processReceivedPresence(src, emit) { if (startIndex < 0) { // Remove presence data because we can't transform presence.received presence.processedAt = Date.now(); - return this.doc._setPresence(src, null, emit); + 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.doc._setPresence(src, null, emit); + return this._setPresence(src, null, emit); } } @@ -225,7 +225,7 @@ function _processReceivedPresence(src, emit) { // Set presence data presence.processedAt = Date.now(); - return this.doc._setPresence(src, data, emit); + return this._setPresence(src, data, emit); } function processAllReceivedPresence() { @@ -248,7 +248,7 @@ function _transformPresence(src, op) { } else { presenceData = null; } - return this.doc._setPresence(src, presenceData); + return this._setPresence(src, presenceData); } function transformAllPresence(op) { @@ -281,7 +281,7 @@ function pausePresence() { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (src && this.doc._setPresence(src, null)) { + if (src && this._setPresence(src, null)) { changedSrcList.push(src); } } @@ -292,16 +292,16 @@ function pausePresence() { // Returns true, if presence has changed. Otherwise false. function _setPresence(src, data, emit) { if (data == null) { - if (this.presence.current[src] == null) return false; - delete this.presence.current[src]; + if (this.current[src] == null) return false; + delete this.current[src]; } else { var isPresenceEqual = - this.presence.current[src] === data || - (this.type.comparePresence && this.type.comparePresence(this.presence.current[src], data)); + this.current[src] === data || + (this.doc.type.comparePresence && this.doc.type.comparePresence(this.current[src], data)); if (isPresenceEqual) return false; - this.presence.current[src] = data; + this.current[src] = data; } - if (emit) this.presence._emitPresence([ src ], true); + if (emit) this._emitPresence([ src ], true); return true; } @@ -342,10 +342,11 @@ function flushPresence() { } } -function _destroyPresence() { - this.presence.received = {}; - this.presence.cachedOps.length = 0; -} +// TODO cover with a test +//function _destroyPresence() { +// this.presence.received = {}; +// this.presence.cachedOps.length = 0; +//} // Reset presence-related properties. function hardRollbackPresence() { @@ -364,7 +365,7 @@ function hardRollbackPresence() { var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; - if (this.doc._setPresence(src, null)) { + if (this._setPresence(src, null)) { changedSrcList.push(src); } } @@ -385,6 +386,6 @@ module.exports = { _emitPresence, cacheOp, flushPresence, - _destroyPresence, + //_destroyPresence, hardRollbackPresence }; From 6114bade0b4311f63fb664952b26815499fb043d Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 20:15:07 +0530 Subject: [PATCH 135/181] Clean up intermediate migration steps --- lib/client/doc.js | 35 ++++++++--------------------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c6e3a1f4c..ee667b305 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -69,33 +69,6 @@ function Doc(connection, collection, id) { this.data = undefined; if (connection.Presence) { - // TODO don't decorate - // Expose presence-related methods on the Doc prototype. - Object.assign(Doc.prototype, connection.Presence); - - delete Doc.prototype.hardRollbackPresence; - delete Doc.prototype._initializePresence; - delete Doc.prototype._processReceivedPresence; - delete Doc.prototype.processAllReceivedPresence; - delete Doc.prototype._transformPresence; - delete Doc.prototype.transformAllPresence; - delete Doc.prototype.pausePresence; - delete Doc.prototype.cacheOp; - delete Doc.prototype._flushPresence; - delete Doc.prototype._emitPresence; - delete Doc.prototype.submitPresence; - delete Doc.prototype._destroyPresence; - delete Doc.prototype._setPresence; - - // TODO move this outside - Doc.prototype.submitPresence = function (data, callback) { - this.presence.submitPresence(data, callback); - }; - - // TODO move this with other _handle... definitions. - Doc.prototype._handlePresence = function (err, presence) { - this.presence.handlePresence(err, presence); - }; // Properties related to presence are grouped within this object. // If this.presence is falsy (undefined), it means that @@ -407,6 +380,14 @@ Doc.prototype._handleOp = function(err, message) { } }; +Doc.prototype._handlePresence = function (err, presence) { + this.presence.handlePresence(err, presence); +}; + +Doc.prototype.submitPresence = function (data, callback) { + this.presence.submitPresence(data, callback); +}; + // Called whenever (you guessed it!) the connection state changes. This will // happen when we get disconnected & reconnect. Doc.prototype._onConnectionStateChanged = function() { From 19446cd613f3d9c40df453545851196f46429908 Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 21:23:04 +0530 Subject: [PATCH 136/181] Convert StatelessPresence to a class --- lib/client/doc.js | 3 +-- lib/presence/stateless.js | 11 +++++++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index ee667b305..05ecb1325 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -76,9 +76,8 @@ function Doc(connection, collection, id) { // so the presence features should be disabled. // // TODO convert to constructor. - this.presence = connection.Presence._initializePresence(); + this.presence = new connection.Presence(this); - this.presence.doc = this; Object.assign(this.presence, connection.Presence); } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 87e59654a..43325fccd 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -373,7 +373,12 @@ function hardRollbackPresence() { return pendingPresence; } -module.exports = { +function StatelessPresence(doc) { + this.doc = doc; + Object.assign(this, this._initializePresence()); +} + +Object.assign(StatelessPresence.prototype, { submitPresence, handlePresence, _initializePresence, @@ -388,4 +393,6 @@ module.exports = { flushPresence, //_destroyPresence, hardRollbackPresence -}; +}); + +module.exports = StatelessPresence; From 0089f80f80ca976aafa412cc8fad82e099817e6a Mon Sep 17 00:00:00 2001 From: curran Date: Thu, 18 Apr 2019 21:29:48 +0530 Subject: [PATCH 137/181] Convert StatelessPresence into idiomatic JS class. --- lib/presence/stateless.js | 172 +++++++++++++++++--------------------- 1 file changed, 76 insertions(+), 96 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 43325fccd..4586c36bd 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -1,18 +1,62 @@ /* - * Presence Methods - * ---------------- - * - * This module contains definitions for presence-related methods - * that are added as methods to the Doc prototype (e.g. doc.submitPresence). + * 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 "Stateless Presence". * - * The value of 'this' in these functions will be the Doc instance. */ var ShareDBError = require('../error'); +// TODO inherit from Presence, add test for that. +function StatelessPresence(doc) { + + // The Doc instance that this Presence is attached to. + this.doc = doc; + + // The current presence data. + // Map of src -> presence data + // Local src === '' + this.current = {}; + + // 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; +} + + // Submit presence data to a document. // This is the only public facing method. // All the others are marked as internal with a leading "_". -function submitPresence(data, callback) { +StatelessPresence.prototype.submitPresence = function (data, callback) { if (data != null) { if (!this.doc.type) { var doc = this.doc; @@ -48,51 +92,9 @@ function submitPresence(data, callback) { } process.nextTick(this.doc.flush.bind(this.doc)); -} - -// This function generates the initial value for doc.presence. -function _initializePresence() { - - // Return a new object each time, otherwise mutations would bleed across documents. - return { - - // The current presence data. - // Map of src -> presence data - // Local src === '' - current: {}, +}; - // The presence objects received from the server. - // Map of src -> presence - 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. - 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. - 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. - 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. - cachedOpsTimeout: 60000, - - // The sequence number of the inflight presence request. - inflightSeq: 0, - - // Callbacks (or null) for pending and inflight presence requests. - pending: null, - inflight: null - }; -} - -function handlePresence(err, presence) { +StatelessPresence.prototype.handlePresence = function (err, presence) { if (!this.doc.subscribed) return; var src = presence.src; @@ -142,11 +144,11 @@ function handlePresence(err, presence) { 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. -function _processReceivedPresence(src, emit) { +StatelessPresence.prototype._processReceivedPresence = function (src, emit) { if (!src) return false; var presence = this.received[src]; if (!presence) return false; @@ -226,9 +228,9 @@ function _processReceivedPresence(src, emit) { // Set presence data presence.processedAt = Date.now(); return this._setPresence(src, data, emit); -} +}; -function processAllReceivedPresence() { +StatelessPresence.prototype.processAllReceivedPresence = function () { var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -238,9 +240,9 @@ function processAllReceivedPresence() { } } this._emitPresence(changedSrcList, true); -} +}; -function _transformPresence(src, op) { +StatelessPresence.prototype._transformPresence = function (src, op) { var presenceData = this.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); @@ -249,9 +251,9 @@ function _transformPresence(src, op) { presenceData = null; } return this._setPresence(src, presenceData); -} +}; -function transformAllPresence(op) { +StatelessPresence.prototype.transformAllPresence = function (op) { var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -261,9 +263,9 @@ function transformAllPresence(op) { } } this._emitPresence(changedSrcList, false); -} +}; -function pausePresence() { +StatelessPresence.prototype.pausePresence = function () { if (!this) return; if (this.inflight) { @@ -286,11 +288,11 @@ function pausePresence() { } } this._emitPresence(changedSrcList, false); -} +}; // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed. Otherwise false. -function _setPresence(src, data, emit) { +StatelessPresence.prototype._setPresence = function (src, data, emit) { if (data == null) { if (this.current[src] == null) return false; delete this.current[src]; @@ -303,18 +305,18 @@ function _setPresence(src, data, emit) { } if (emit) this._emitPresence([ src ], true); return true; -} +}; -function _emitPresence(srcList, submitted) { +StatelessPresence.prototype._emitPresence = function (srcList, submitted) { if (srcList && srcList.length > 0) { var doc = this.doc; process.nextTick(function() { doc.emit('presence', srcList, submitted); }); } -} +}; -function cacheOp(op) { +StatelessPresence.prototype.cacheOp = function (op) { // Remove the old ops. var oldOpTime = Date.now() - this.cachedOpsTimeout; var i; @@ -329,10 +331,10 @@ function cacheOp(op) { // Cache the new op. this.cachedOps.push(op); -} +}; // If there are no pending ops, this method sends the pending presence data, if possible. -function flushPresence() { +StatelessPresence.prototype.flushPresence = function () { if (this.doc.subscribed && !this.inflight && this.pending && !this.doc.hasWritePending()) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; @@ -340,16 +342,16 @@ function flushPresence() { this.doc.connection.sendPresence(this.doc, this.current[''], this.requestReply); this.requestReply = false; } -} +}; // TODO cover with a test -//function _destroyPresence() { +//StatelessPresence.prototype._destroyPresence = function () { // this.presence.received = {}; // this.presence.cachedOps.length = 0; -//} +//}; // Reset presence-related properties. -function hardRollbackPresence() { +StatelessPresence.prototype.hardRollbackPresence = function () { var pendingPresence = []; if (this.inflight) pendingPresence.push(this.inflight); if (this.pending) pendingPresence.push(this.pending); @@ -371,28 +373,6 @@ function hardRollbackPresence() { } this._emitPresence(changedSrcList, false); return pendingPresence; -} - -function StatelessPresence(doc) { - this.doc = doc; - Object.assign(this, this._initializePresence()); -} - -Object.assign(StatelessPresence.prototype, { - submitPresence, - handlePresence, - _initializePresence, - _processReceivedPresence, - processAllReceivedPresence, - _transformPresence, - transformAllPresence, - pausePresence, - _setPresence, - _emitPresence, - cacheOp, - flushPresence, - //_destroyPresence, - hardRollbackPresence -}); +}; module.exports = StatelessPresence; From 205405706b43bd2bc7cfe95c4f60554a2b3ea8e2 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:31:30 +0530 Subject: [PATCH 138/181] Add test case that doc invokes presence.destroy inside doc.destroy --- lib/client/doc.js | 4 ++-- lib/presence/stateless.js | 9 ++++----- test/client/presence.js | 11 +++++++++++ 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 05ecb1325..6d05cfca6 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -135,12 +135,12 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - if (doc.presence) doc._destroyPresence(); + if (doc.presence) doc.presence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - if (doc.presence) doc._destroyPresence(); + if (doc.presence) doc.presence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 4586c36bd..37f48ef5a 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -344,11 +344,10 @@ StatelessPresence.prototype.flushPresence = function () { } }; -// TODO cover with a test -//StatelessPresence.prototype._destroyPresence = function () { -// this.presence.received = {}; -// this.presence.cachedOps.length = 0; -//}; +StatelessPresence.prototype.destroyPresence = function () { + this.received = {}; + this.cachedOps.length = 0; +}; // Reset presence-related properties. StatelessPresence.prototype.hardRollbackPresence = function () { diff --git a/test/client/presence.js b/test/client/presence.js index f5ee3f1fa..07547af4c 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -1452,5 +1452,16 @@ describe('client presence', function() { 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.presence; + presence.cachedOps = ['foo']; + presence.received = { bar: true }; + this.doc.destroy(function(err) { + expect(presence.cachedOps).to.eql([]); + expect(presence.received).to.eql({}); + done(); + }); + }); }); }); From 1a64a06865a8a50c87d7f3742a958c01cb4aaa6f Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:52:33 +0530 Subject: [PATCH 139/181] Introduce DummyPresence, use it by default --- lib/client/doc.js | 12 ++++++------ lib/presence/dummy.js | 27 +++++++++++++++++++++------ lib/presence/stateless.js | 8 ++++++++ test/client/presence.js | 5 +++-- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 6d05cfca6..c8d950da7 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,6 +2,7 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); +var DummyPresence = require('../presence/dummy'); /** * A Doc is a client's view on a sharejs document. @@ -77,9 +78,8 @@ function Doc(connection, collection, id) { // // TODO convert to constructor. this.presence = new connection.Presence(this); - - Object.assign(this.presence, connection.Presence); - + } else { + this.presence = new DummyPresence(); } // Array of callbacks or nulls as placeholders @@ -223,7 +223,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.version = snapshot.v; if (this.presence) { - this.presence.cachedOps.length = 0; + this.presence.clearCachedOps(); } var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; @@ -255,7 +255,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.presence && (this.presence.inflight || this.presence.pending) + this.presence.hasPendingPresence() ); }; @@ -905,7 +905,7 @@ Doc.prototype._opAcknowledged = function(message) { this.version = message.v; if (this.presence) { - this.presence.cachedOps.length = 0; + this.presence.clearCachedOps(); } } else if (message.v !== this.version) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index ac328f67a..ad2633a6d 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -1,15 +1,30 @@ +/* + * 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. + */ + // TODO use this +// TODO inherit from Presence, add test for that. function DummyPresence () { } function noop () {} -DummyPresence.prototype.flushPresence = noop; -DummyPresence.prototype.destroyPresence = noop; -DummyPresence.prototype.clearCachedOps = noop; // this.presence.cachedOps.length = 0; +DummyPresence.prototype.submitPresence = noop; +DummyPresence.prototype.handlePresence = noop; DummyPresence.prototype.processAllReceivedPresence = noop; -DummyPresence.prototype.hardRollbackPresence = function () { return []; }; DummyPresence.prototype.transformAllPresence = noop; +DummyPresence.prototype.pausePresence = noop; DummyPresence.prototype.cacheOp = noop; -DummyPresence.prototype.hasPending = function () { return false }; // (this.presence.inflight || this.presence.pending) -DummyPresence.prototype.pause = noop; +DummyPresence.prototype.flushPresence = noop; +DummyPresence.prototype.destroyPresence = noop; +DummyPresence.prototype.clearCachedOps = noop; +DummyPresence.prototype.hardRollbackPresence = function () { return []; }; +DummyPresence.prototype.hasPendingPresence = function () { return false }; +DummyPresence.prototype._processReceivedPresence = noop; +DummyPresence.prototype._transformPresence = noop; +DummyPresence.prototype._setPresence = noop; +DummyPresence.prototype._emitPresence = noop; module.exports = DummyPresence; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 37f48ef5a..35361acab 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -346,6 +346,10 @@ StatelessPresence.prototype.flushPresence = function () { StatelessPresence.prototype.destroyPresence = function () { this.received = {}; + this.clearCachedOps(); +}; + +StatelessPresence.prototype.clearCachedOps = function () { this.cachedOps.length = 0; }; @@ -374,4 +378,8 @@ StatelessPresence.prototype.hardRollbackPresence = function () { return pendingPresence; }; +StatelessPresence.prototype.hasPendingPresence = function () { + return this.inflight || this.pending; +}; + module.exports = StatelessPresence; diff --git a/test/client/presence.js b/test/client/presence.js index 07547af4c..ec85172ff 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,6 +3,7 @@ var lolex = require('lolex'); var util = require('../util'); var errorHandler = util.errorHandler; var Backend = require('../../lib/backend'); +var DummyPresence = require('../../lib/presence/dummy'); var StatelessPresence = require('../../lib/presence/stateless'); var ShareDBError = require('../../lib/error'); var expect = require('expect.js'); @@ -13,11 +14,11 @@ types.register(presenceType.type2); types.register(presenceType.type3); describe('client presence', function() { - it('does not expose doc.presence if enablePresence is false', 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(typeof doc.presence).to.equal('undefined'); + expect(doc.presence instanceof DummyPresence); }); }); From 47193daf4ab13a76cef2b0357c779b8c577e4178 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:57:26 +0530 Subject: [PATCH 140/181] Remove if(this.presence) guards. --- lib/client/doc.js | 57 +++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c8d950da7..c2afdff87 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -69,18 +69,9 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - if (connection.Presence) { - - // Properties related to presence are grouped within this object. - // If this.presence is falsy (undefined), it means that - // the enablePresence flag was not passed into the ShareDB constructor, - // so the presence features should be disabled. - // - // TODO convert to constructor. - this.presence = new connection.Presence(this); - } else { - this.presence = new DummyPresence(); - } + this.presence = connection.Presence + ? new connection.Presence(this) + : new DummyPresence(); // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -222,9 +213,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.version = snapshot.v; - if (this.presence) { - this.presence.clearCachedOps(); - } + this.presence.clearCachedOps(); var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); @@ -232,7 +221,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - if (this.presence) this.presence.processAllReceivedPresence(); + this.presence.processAllReceivedPresence(); callback && callback(); }; @@ -364,7 +353,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - if (this.presence) this.presence.cacheOp({ + this.presence.cacheOp({ src: message.src, time: Date.now(), create: !!message.create, @@ -373,7 +362,7 @@ Doc.prototype._handleOp = function(err, message) { }); try { this._otApply(message, false); - if (this.presence) this.presence.processAllReceivedPresence(); + this.presence.processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } @@ -407,10 +396,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); callEach(callbacks); } else { - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); } } }; @@ -471,10 +460,10 @@ Doc.prototype.unsubscribe = function(callback) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); return; } - if (this.presence) this.presence.pausePresence(); + this.presence.pausePresence(); if (callback) process.nextTick(callback); }; @@ -506,9 +495,7 @@ Doc.prototype.flush = function() { this._sendOp(); } - if (this.presence) { - this.presence.flushPresence(); - } + this.presence.flushPresence(); }; // Helper function to set op to contain a no-op. @@ -613,7 +600,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); - if (this.presence) this.presence.transformAllPresence(componentOp); + this.presence.transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -626,7 +613,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); - if (this.presence) this.presence.transformAllPresence(op); + this.presence.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. @@ -643,7 +630,7 @@ 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); - if (this.presence) this.presence.transformAllPresence(op); + this.presence.transformAllPresence(op); this.emit('create', source); return; } @@ -651,7 +638,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - if (this.presence) this.presence.transformAllPresence(op); + this.presence.transformAllPresence(op); this.emit('del', oldData, source); return; } @@ -903,11 +890,7 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - - if (this.presence) { - this.presence.clearCachedOps(); - } - + this.presence.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 @@ -919,7 +902,7 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - if (this.presence) this.presence.cacheOp({ + this.presence.cacheOp({ src: this.inflightOp.src, time: Date.now(), create: !!this.inflightOp.create, @@ -928,7 +911,7 @@ Doc.prototype._opAcknowledged = function(message) { }); this._clearInflightOp(); - if (this.presence) this.presence.processAllReceivedPresence(); + this.presence.processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { @@ -977,7 +960,7 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence ? this.presence.hardRollbackPresence() : []; + var pendingPresence = this.presence.hardRollbackPresence(); // Cancel all pending ops and reset if we can't invert this._setType(null); From b23661c7eef4f6fe830b95371953bc6999b052c0 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 00:59:59 +0530 Subject: [PATCH 141/181] Optimize cacheOp --- lib/client/doc.js | 16 ++-------------- lib/presence/stateless.js | 9 ++++++++- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index c2afdff87..119032671 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -353,13 +353,7 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this.presence.cacheOp({ - src: message.src, - time: Date.now(), - create: !!message.create, - op: message.op, - del: !!message.del - }); + this.presence.cacheOp(message); try { this._otApply(message, false); this.presence.processAllReceivedPresence(); @@ -902,14 +896,8 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - this.presence.cacheOp({ - src: this.inflightOp.src, - time: Date.now(), - create: !!this.inflightOp.create, - op: this.inflightOp.op, - del: !!this.inflightOp.del - }); + this.presence.cacheOp(this.inflightOp); this._clearInflightOp(); this.presence.processAllReceivedPresence(); }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 35361acab..1f85f4dda 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -316,7 +316,14 @@ StatelessPresence.prototype._emitPresence = function (srcList, submitted) { } }; -StatelessPresence.prototype.cacheOp = function (op) { +StatelessPresence.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; From 521f77b03398cb08175af1b26779e530c29089be Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 01:10:48 +0530 Subject: [PATCH 142/181] Split out getPendingPresence logic from hardRollbackPresence. --- lib/presence/stateless.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 1f85f4dda..23a4610ef 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -362,10 +362,6 @@ StatelessPresence.prototype.clearCachedOps = function () { // Reset presence-related properties. StatelessPresence.prototype.hardRollbackPresence = function () { - var pendingPresence = []; - if (this.inflight) pendingPresence.push(this.inflight); - if (this.pending) pendingPresence.push(this.pending); - this.inflight = null; this.inflightSeq = 0; this.pending = null; @@ -382,11 +378,17 @@ StatelessPresence.prototype.hardRollbackPresence = function () { } } this._emitPresence(changedSrcList, false); - return pendingPresence; }; StatelessPresence.prototype.hasPendingPresence = function () { return this.inflight || this.pending; }; +StatelessPresence.prototype.getPendingPresence = function () { + var pendingPresence = []; + if (this.inflight) pendingPresence.push(this.inflight); + if (this.pending) pendingPresence.push(this.pending); + return pendingPresence; +}; + module.exports = StatelessPresence; From bdb6424c5199e68ea21d6bdc8ea737beeca1daf6 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 01:11:12 +0530 Subject: [PATCH 143/181] Clean up DummyPresence --- lib/client/doc.js | 3 ++- lib/presence/dummy.js | 35 ++++++++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 119032671..60edde765 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -948,7 +948,8 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence.hardRollbackPresence(); + var pendingPresence = this.presence.getPendingPresence(); + this.presence.hardRollbackPresence(); // Cancel all pending ops and reset if we can't invert this._setType(null); diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index ad2633a6d..c7d23d9fc 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -10,21 +10,26 @@ // TODO inherit from Presence, add test for that. function DummyPresence () { } function noop () {} +function returnEmptyArray () { return []; }; +function returnFalse () { return false; }; -DummyPresence.prototype.submitPresence = noop; -DummyPresence.prototype.handlePresence = noop; -DummyPresence.prototype.processAllReceivedPresence = noop; -DummyPresence.prototype.transformAllPresence = noop; -DummyPresence.prototype.pausePresence = noop; -DummyPresence.prototype.cacheOp = noop; -DummyPresence.prototype.flushPresence = noop; -DummyPresence.prototype.destroyPresence = noop; -DummyPresence.prototype.clearCachedOps = noop; -DummyPresence.prototype.hardRollbackPresence = function () { return []; }; -DummyPresence.prototype.hasPendingPresence = function () { return false }; -DummyPresence.prototype._processReceivedPresence = noop; -DummyPresence.prototype._transformPresence = noop; -DummyPresence.prototype._setPresence = noop; -DummyPresence.prototype._emitPresence = noop; +Object.assign(DummyPresence.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 +}); module.exports = DummyPresence; From 0f3084aac391b2c5ccbe4e9b1e854e5f4859f61c Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 01:23:25 +0530 Subject: [PATCH 144/181] Introduce Presence base class inherited by DummyPresence and StatelessPresence --- lib/presence/dummy.js | 7 +++++-- lib/presence/stateless.js | 4 +++- test/client/presence.js | 11 ++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index c7d23d9fc..a893b5a2c 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -5,10 +5,13 @@ * 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('.'); -// TODO use this -// TODO inherit from Presence, add test for that. function DummyPresence () { } + +// Inherit from Presence to support instanceof type checking. +DummyPresence.prototype = Object.create(Presence.prototype); + function noop () {} function returnEmptyArray () { return []; }; function returnFalse () { return false; }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 23a4610ef..d3642b877 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -10,8 +10,8 @@ * */ var ShareDBError = require('../error'); +var Presence = require('.'); -// TODO inherit from Presence, add test for that. function StatelessPresence(doc) { // The Doc instance that this Presence is attached to. @@ -52,6 +52,8 @@ function StatelessPresence(doc) { this.inflight = null; } +// Inherit from Presence to support instanceof type checking. +StatelessPresence.prototype = Object.create(Presence.prototype); // Submit presence data to a document. // This is the only public facing method. diff --git a/test/client/presence.js b/test/client/presence.js index ec85172ff..abca8ea54 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,6 +3,7 @@ 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'); @@ -18,7 +19,15 @@ describe('client presence', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); - expect(doc.presence instanceof DummyPresence); + expect(doc.presence instanceof DummyPresence).to.be(true); + }); + + it('DummyPresence should subclass Presence', function() { + expect(DummyPresence.prototype instanceof Presence).to.be(true); + }); + + it('StatelessPresence should subclass Presence', function() { + expect(StatelessPresence.prototype instanceof Presence).to.be(true); }); }); From 41cd2eae8171ac1a1ca4d70f3cb128ae548334eb Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 11:37:10 +0530 Subject: [PATCH 145/181] Add Presence base class module --- lib/presence/index.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/presence/index.js diff --git a/lib/presence/index.js b/lib/presence/index.js new file mode 100644 index 000000000..2ce09b6d3 --- /dev/null +++ b/lib/presence/index.js @@ -0,0 +1 @@ +module.exports = function Presence () {}; From 986a695348c33732e5787bfc0aa5c6efe35232f9 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 11:51:59 +0530 Subject: [PATCH 146/181] Start disentangling presence logic from Agent --- lib/agent.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index b4da48c0f..351667674 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -27,6 +27,17 @@ function Agent(backend, stream) { // Map from queryId -> emitter this.subscribedQueries = {}; + this.presence = { + agent: this, + processPresenceData: function (data) { + if (data.a === 'p') { + // Send other clients' presence data + if (data.src !== this.agent.clientId) this.agent.send(data); + return true; + } + } + }; + // The max presence sequence number received from the client. this.maxPresenceSeq = 0; @@ -110,17 +121,11 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { 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); - return; - } + if (agent.presence.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 || streams[id] !== stream) return; @@ -128,6 +133,10 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { if (util.hasKeys(streams)) return; delete agent.subscribedDocs[collection]; }); + stream.on('end', function() { + var presence = agent._createPresence(collection, id); + agent.backend.sendPresence(presence); + }); }; Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query) { From ac55884b3aaaca784a6e6239052402243c4063f5 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:17:28 +0530 Subject: [PATCH 147/181] Migrate Agent._createPresence --- lib/agent.js | 42 ++++++++++++++++++++---------------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 351667674..718e5b354 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -35,12 +35,23 @@ function Agent(backend, stream) { if (data.src !== this.agent.clientId) this.agent.send(data); return true; } + }, + // The max presence sequence number received from the client. + maxPresenceSeq: 0, + 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 + }; } }; - // The max presence sequence number received from the client. - this.maxPresenceSeq = 0; - // We need to track this manually to make sure we don't reply to messages // after the stream was closed. this.closed = false; @@ -134,7 +145,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { delete agent.subscribedDocs[collection]; }); stream.on('end', function() { - var presence = agent._createPresence(collection, id); + var presence = agent.presence.createPresence(collection, id); agent.backend.sendPresence(presence); }); }; @@ -352,8 +363,7 @@ Agent.prototype._handleMessage = function(request, callback) { case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); case 'p': - var presence = this._createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); - return this._presence(presence, callback); + return this._handlePresenceMessage(request, callback); default: callback({code: 4000, message: 'Invalid or unknown message'}); } @@ -645,13 +655,14 @@ Agent.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback); }; -Agent.prototype._presence = function(presence, callback) { - if (presence.seq <= this.maxPresenceSeq) { +Agent.prototype._handlePresenceMessage = function(request, callback) { + var presence = this.presence.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); + if (presence.seq <= this.presence.maxPresenceSeq) { return process.nextTick(function() { callback(new ShareDBError(4026, 'Presence data superseded')); }); } - this.maxPresenceSeq = presence.seq; + this.presence.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)); @@ -662,16 +673,3 @@ Agent.prototype._presence = function(presence, callback) { callback(null, { seq: presence.seq }); }); }; - -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 - }; -}; From 9187b346a236198c883e46a39809ca78f7217c9c Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:24:10 +0530 Subject: [PATCH 148/181] Migrate subscribeToStream --- lib/agent.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 718e5b354..ba0e0b06b 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -49,6 +49,12 @@ function Agent(backend, stream) { v: version, r: requestReply }; + }, + subscribeToStream: function (collection, id, stream) { + var agent = this.agent; + stream.on('end', function() { + agent.backend.sendPresence(agent.presence.createPresence(collection, id)); + }); } }; @@ -144,10 +150,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { if (util.hasKeys(streams)) return; delete agent.subscribedDocs[collection]; }); - stream.on('end', function() { - var presence = agent.presence.createPresence(collection, id); - agent.backend.sendPresence(presence); - }); + this.presence.subscribeToStream(collection, id, stream); }; Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query) { From 75077314ee5f892031cbd2560af758e4edf1413d Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:27:59 +0530 Subject: [PATCH 149/181] Migrate _subscribeToQuery --- lib/agent.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index ba0e0b06b..7380e1287 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -55,6 +55,17 @@ function Agent(backend, stream) { stream.on('end', function() { agent.backend.sendPresence(agent.presence.createPresence(collection, id)); }); + }, + 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'; + } + } } }; @@ -322,13 +333,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.presence.checkRequest(request); } }; From 1a1f52aca6c7ed37bf3b8f952dc1127d3d7e4050 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:35:22 +0530 Subject: [PATCH 150/181] Migrate handlePresenceMessage --- lib/agent.js | 50 ++++++++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 7380e1287..53891b306 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -29,6 +29,9 @@ function Agent(backend, stream) { this.presence = { agent: this, + isPresenceData: function (data) { + return data.a === 'p'; + }, processPresenceData: function (data) { if (data.a === 'p') { // Send other clients' presence data @@ -66,6 +69,24 @@ function Agent(backend, stream) { return 'Invalid "request reply" value'; } } + }, + 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(4026, '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(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); + }); + } + this.agent.backend.sendPresence(presence, function(err) { + if (err) return callback(err); + callback(null, { seq: presence.seq }); + }); } }; @@ -149,7 +170,10 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (agent.presence.processPresenceData(data)) return; + if (agent.presence.isPresenceData(data)) { + agent.presence.processPresenceData(data); + return; + } if (agent._isOwnOp(collection, data)) return; agent._sendOp(collection, id, data); }); @@ -371,9 +395,10 @@ Agent.prototype._handleMessage = function(request, callback) { return this._fetchSnapshot(request.c, request.d, request.v, callback); case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); - case 'p': - return this._handlePresenceMessage(request, callback); default: + if (this.presence.isPresenceData(request)) { + return this.presence.handlePresenceMessage(request, callback); + } callback({code: 4000, message: 'Invalid or unknown message'}); } } catch (err) { @@ -663,22 +688,3 @@ Agent.prototype._fetchSnapshot = function (collection, id, version, callback) { Agent.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { this.backend.fetchSnapshotByTimestamp(this, collection, id, timestamp, callback); }; - -Agent.prototype._handlePresenceMessage = function(request, callback) { - var presence = this.presence.createPresence(request.c, request.d, request.p, request.v, request.r, request.seq); - if (presence.seq <= this.presence.maxPresenceSeq) { - return process.nextTick(function() { - callback(new ShareDBError(4026, 'Presence data superseded')); - }); - } - this.presence.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 }); - }); -}; From 49ff5c2b141b8062360d9a282799a2a04853c264 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 12:46:21 +0530 Subject: [PATCH 151/181] Use only flushPresence(), not flush(), _handleSubscribe --- lib/client/doc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 60edde765..4cd25f1e0 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -291,7 +291,7 @@ Doc.prototype._handleSubscribe = function(err, snapshot) { if (this.wantSubscribe) this.subscribed = true; this.ingestSnapshot(snapshot, callback); this._emitNothingPending(); - this.flush(); + this.presence.flushPresence(); }; Doc.prototype._handleUnsubscribe = function(err) { From 87aa90b81e3b609b6ff0503dc03c1edaaa5f9822 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 13:13:26 +0530 Subject: [PATCH 152/181] Disentangle doc internals from flushPresence --- lib/client/doc.js | 6 ++++-- lib/presence/stateless.js | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 4cd25f1e0..7d3cc967b 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -291,7 +291,7 @@ Doc.prototype._handleSubscribe = function(err, snapshot) { if (this.wantSubscribe) this.subscribed = true; this.ingestSnapshot(snapshot, callback); this._emitNothingPending(); - this.presence.flushPresence(); + this.flush(); }; Doc.prototype._handleUnsubscribe = function(err) { @@ -489,7 +489,9 @@ Doc.prototype.flush = function() { this._sendOp(); } - this.presence.flushPresence(); + if (this.subscribed && !this.hasWritePending()) { + this.presence.flushPresence(); + } }; // Helper function to set op to contain a no-op. diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index d3642b877..389f53f94 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -344,7 +344,7 @@ StatelessPresence.prototype.cacheOp = function (message) { // If there are no pending ops, this method sends the pending presence data, if possible. StatelessPresence.prototype.flushPresence = function () { - if (this.doc.subscribed && !this.inflight && this.pending && !this.doc.hasWritePending()) { + if(!this.inflight && this.pending) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; this.pending = null; From 2198e8e46e5a6201b0ce06f40393e9676be0a6d4 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 13:32:55 +0530 Subject: [PATCH 153/181] Begin disentangling presence logic from connection.js --- lib/client/connection.js | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index 222d82338..7b1215f8d 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -63,6 +63,18 @@ function Connection(socket) { this.state = connectionState(socket); this.bindToSocket(socket); + + this.presence = { + connection: this, + // TODO unify with code in agent.js + isPresenceMessage: function (message) { + return message.a === 'p'; + }, + handlePresenceMessage: function (err, message) { + var doc = this.connection.getExisting(message.c, message.d); + if (doc) doc._handlePresence(err, message); + } + }; } emitter.mixin(Connection); @@ -254,12 +266,10 @@ 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: + if (this.presence.isPresenceMessage(message)) { + return this.presence.handlePresenceMessage(err, message); + } logger.warn('Ignoring unrecognized message', message); } }; From cf0168dcf9dde236ad905ca5cdb4ac073c9dc454 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 13:37:02 +0530 Subject: [PATCH 154/181] Decouple sendPresence --- lib/client/connection.js | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index 7b1215f8d..5be290676 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -73,6 +73,22 @@ function Connection(socket) { handlePresenceMessage: function (err, message) { var doc = this.connection.getExisting(message.c, message.d); if (doc) doc._handlePresence(err, message); + }, + 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); } }; } @@ -439,21 +455,11 @@ Connection.prototype.sendOp = function(doc, op) { this.send(message); }; +/** + * Sends presence data down the socket + */ 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.presence.sendPresence(doc, data, requestReply); }; From 4c46a05af7914e52d5dbbd1fb25d1973cec879ba Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 15:16:24 +0530 Subject: [PATCH 155/181] Move Presence class to presence.DocPresence --- lib/backend.js | 5 +++-- lib/client/doc.js | 5 +---- lib/presence/dummy.js | 14 +++++++------ lib/presence/index.js | 6 +++++- lib/presence/stateless.js | 44 ++++++++++++++++++++------------------- test/client/presence.js | 16 +++++++------- 6 files changed, 48 insertions(+), 42 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 1d86a8603..192636a76 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -12,6 +12,7 @@ var Snapshot = require('./snapshot'); var StreamSocket = require('./stream-socket'); var SubmitRequest = require('./submit-request'); var types = require('./types'); +var dummyPresence = require('./presence/dummy'); var warnDeprecatedDoc = true; var warnDeprecatedAfterSubmit = true; @@ -49,7 +50,7 @@ function Backend(options) { this._shimAfterSubmit(); } - this.Presence = options.Presence; + this.presence = options.presence || dummyPresence; } module.exports = Backend; emitter.mixin(Backend); @@ -158,7 +159,7 @@ Backend.prototype.connect = function(connection, req) { // code that may cache state on the agent and read it in middleware connection.agent = agent; - connection.Presence = this.Presence; + connection.DocPresence = this.presence.DocPresence; return connection; }; diff --git a/lib/client/doc.js b/lib/client/doc.js index 7d3cc967b..86336ec7e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,7 +2,6 @@ var emitter = require('../emitter'); var logger = require('../logger'); var ShareDBError = require('../error'); var types = require('../types'); -var DummyPresence = require('../presence/dummy'); /** * A Doc is a client's view on a sharejs document. @@ -69,9 +68,7 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - this.presence = connection.Presence - ? new connection.Presence(this) - : new DummyPresence(); + this.presence = new connection.DocPresence(this); // Array of callbacks or nulls as placeholders this.inflightFetch = []; diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index a893b5a2c..8a4bac340 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -1,22 +1,22 @@ /* * 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('.'); +var presence = require('./index'); -function DummyPresence () { } +function DocPresence () { } // Inherit from Presence to support instanceof type checking. -DummyPresence.prototype = Object.create(Presence.prototype); +DocPresence.prototype = Object.create(presence.DocPresence.prototype); function noop () {} function returnEmptyArray () { return []; }; function returnFalse () { return false; }; -Object.assign(DummyPresence.prototype, { +Object.assign(DocPresence.prototype, { submitPresence: noop, handlePresence: noop, processAllReceivedPresence: noop, @@ -35,4 +35,6 @@ Object.assign(DummyPresence.prototype, { _emitPresence: noop }); -module.exports = DummyPresence; +module.exports = { + DocPresence: DocPresence +}; diff --git a/lib/presence/index.js b/lib/presence/index.js index 2ce09b6d3..e4de5669a 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1 +1,5 @@ -module.exports = function Presence () {}; +var DocPresence = function () {}; + +module.exports = { + DocPresence: DocPresence +}; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 389f53f94..2f65ca930 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -6,13 +6,13 @@ * 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 "Stateless Presence". + * it exists only in clients, hence the name "Doc Presence". * */ var ShareDBError = require('../error'); -var Presence = require('.'); +var presence = require('./index'); -function StatelessPresence(doc) { +function DocPresence(doc) { // The Doc instance that this Presence is attached to. this.doc = doc; @@ -53,12 +53,12 @@ function StatelessPresence(doc) { } // Inherit from Presence to support instanceof type checking. -StatelessPresence.prototype = Object.create(Presence.prototype); +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 "_". -StatelessPresence.prototype.submitPresence = function (data, callback) { +DocPresence.prototype.submitPresence = function (data, callback) { if (data != null) { if (!this.doc.type) { var doc = this.doc; @@ -96,7 +96,7 @@ StatelessPresence.prototype.submitPresence = function (data, callback) { process.nextTick(this.doc.flush.bind(this.doc)); }; -StatelessPresence.prototype.handlePresence = function (err, presence) { +DocPresence.prototype.handlePresence = function (err, presence) { if (!this.doc.subscribed) return; var src = presence.src; @@ -150,7 +150,7 @@ StatelessPresence.prototype.handlePresence = function (err, presence) { // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed for src. Otherwise false. -StatelessPresence.prototype._processReceivedPresence = function (src, emit) { +DocPresence.prototype._processReceivedPresence = function (src, emit) { if (!src) return false; var presence = this.received[src]; if (!presence) return false; @@ -232,7 +232,7 @@ StatelessPresence.prototype._processReceivedPresence = function (src, emit) { return this._setPresence(src, data, emit); }; -StatelessPresence.prototype.processAllReceivedPresence = function () { +DocPresence.prototype.processAllReceivedPresence = function () { var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -244,7 +244,7 @@ StatelessPresence.prototype.processAllReceivedPresence = function () { this._emitPresence(changedSrcList, true); }; -StatelessPresence.prototype._transformPresence = function (src, op) { +DocPresence.prototype._transformPresence = function (src, op) { var presenceData = this.current[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); @@ -255,7 +255,7 @@ StatelessPresence.prototype._transformPresence = function (src, op) { return this._setPresence(src, presenceData); }; -StatelessPresence.prototype.transformAllPresence = function (op) { +DocPresence.prototype.transformAllPresence = function (op) { var srcList = Object.keys(this.current); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -267,7 +267,7 @@ StatelessPresence.prototype.transformAllPresence = function (op) { this._emitPresence(changedSrcList, false); }; -StatelessPresence.prototype.pausePresence = function () { +DocPresence.prototype.pausePresence = function () { if (!this) return; if (this.inflight) { @@ -294,7 +294,7 @@ StatelessPresence.prototype.pausePresence = function () { // If emit is true and presence has changed, emits a presence event. // Returns true, if presence has changed. Otherwise false. -StatelessPresence.prototype._setPresence = function (src, data, emit) { +DocPresence.prototype._setPresence = function (src, data, emit) { if (data == null) { if (this.current[src] == null) return false; delete this.current[src]; @@ -309,7 +309,7 @@ StatelessPresence.prototype._setPresence = function (src, data, emit) { return true; }; -StatelessPresence.prototype._emitPresence = function (srcList, submitted) { +DocPresence.prototype._emitPresence = function (srcList, submitted) { if (srcList && srcList.length > 0) { var doc = this.doc; process.nextTick(function() { @@ -318,7 +318,7 @@ StatelessPresence.prototype._emitPresence = function (srcList, submitted) { } }; -StatelessPresence.prototype.cacheOp = function (message) { +DocPresence.prototype.cacheOp = function (message) { var op = { src: message.src, time: Date.now(), @@ -343,7 +343,7 @@ StatelessPresence.prototype.cacheOp = function (message) { }; // If there are no pending ops, this method sends the pending presence data, if possible. -StatelessPresence.prototype.flushPresence = function () { +DocPresence.prototype.flushPresence = function () { if(!this.inflight && this.pending) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; @@ -353,17 +353,17 @@ StatelessPresence.prototype.flushPresence = function () { } }; -StatelessPresence.prototype.destroyPresence = function () { +DocPresence.prototype.destroyPresence = function () { this.received = {}; this.clearCachedOps(); }; -StatelessPresence.prototype.clearCachedOps = function () { +DocPresence.prototype.clearCachedOps = function () { this.cachedOps.length = 0; }; // Reset presence-related properties. -StatelessPresence.prototype.hardRollbackPresence = function () { +DocPresence.prototype.hardRollbackPresence = function () { this.inflight = null; this.inflightSeq = 0; this.pending = null; @@ -382,15 +382,17 @@ StatelessPresence.prototype.hardRollbackPresence = function () { this._emitPresence(changedSrcList, false); }; -StatelessPresence.prototype.hasPendingPresence = function () { +DocPresence.prototype.hasPendingPresence = function () { return this.inflight || this.pending; }; -StatelessPresence.prototype.getPendingPresence = function () { +DocPresence.prototype.getPendingPresence = function () { var pendingPresence = []; if (this.inflight) pendingPresence.push(this.inflight); if (this.pending) pendingPresence.push(this.pending); return pendingPresence; }; -module.exports = StatelessPresence; +module.exports = { + DocPresence: DocPresence +}; diff --git a/test/client/presence.js b/test/client/presence.js index abca8ea54..c7ee09d93 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -3,9 +3,9 @@ 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 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'); @@ -15,19 +15,19 @@ types.register(presenceType.type2); types.register(presenceType.type3); describe('client presence', function() { - it('should use DummyPresence if Presence option not provided', function() { + it('should use dummyPresence.DocPresence if presence option not provided', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); - expect(doc.presence instanceof DummyPresence).to.be(true); + expect(doc.presence instanceof dummyPresence.DocPresence).to.be(true); }); it('DummyPresence should subclass Presence', function() { - expect(DummyPresence.prototype instanceof Presence).to.be(true); + expect(dummyPresence.DocPresence.prototype instanceof presence.DocPresence).to.be(true); }); it('StatelessPresence should subclass Presence', function() { - expect(StatelessPresence.prototype instanceof Presence).to.be(true); + expect(statelessPresence.DocPresence.prototype instanceof presence.DocPresence).to.be(true); }); }); @@ -42,7 +42,7 @@ describe('client presence', function() { describe('client presence (' + typeName + ')', function() { beforeEach(function() { - this.backend = new Backend({ Presence: StatelessPresence }); + this.backend = new Backend({ presence: statelessPresence }); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); From f0b7b97fbd03cb93348e5c6403c5842912c19b7d Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 16:58:02 +0530 Subject: [PATCH 156/181] Rename doc.presence to doc._docPresence --- lib/client/doc.js | 48 +++---- test/client/presence.js | 286 ++++++++++++++++++++-------------------- 2 files changed, 167 insertions(+), 167 deletions(-) diff --git a/lib/client/doc.js b/lib/client/doc.js index 86336ec7e..963e9fa01 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -68,7 +68,7 @@ function Doc(connection, collection, id) { this.type = null; this.data = undefined; - this.presence = new connection.DocPresence(this); + this._docPresence = new connection.DocPresence(this); // Array of callbacks or nulls as placeholders this.inflightFetch = []; @@ -123,12 +123,12 @@ Doc.prototype.destroy = function(callback) { if (callback) return callback(err); return doc.emit('error', err); } - if (doc.presence) doc.presence.destroyPresence(); + doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - if (doc.presence) doc.presence.destroyPresence(); + doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -210,7 +210,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.version = snapshot.v; - this.presence.clearCachedOps(); + this._docPresence.clearCachedOps(); var type = (snapshot.type === undefined) ? types.defaultType : snapshot.type; this._setType(type); @@ -218,7 +218,7 @@ Doc.prototype.ingestSnapshot = function(snapshot, callback) { this.type.deserialize(snapshot.data) : snapshot.data; this.emit('load'); - this.presence.processAllReceivedPresence(); + this._docPresence.processAllReceivedPresence(); callback && callback(); }; @@ -241,7 +241,7 @@ Doc.prototype.hasPending = function() { this.inflightSubscribe.length || this.inflightUnsubscribe.length || this.pendingFetch.length || - this.presence.hasPendingPresence() + this._docPresence.hasPendingPresence() ); }; @@ -350,21 +350,21 @@ Doc.prototype._handleOp = function(err, message) { } this.version++; - this.presence.cacheOp(message); + this._docPresence.cacheOp(message); try { this._otApply(message, false); - this.presence.processAllReceivedPresence(); + this._docPresence.processAllReceivedPresence(); } catch (error) { return this._hardRollback(error); } }; Doc.prototype._handlePresence = function (err, presence) { - this.presence.handlePresence(err, presence); + this._docPresence.handlePresence(err, presence); }; Doc.prototype.submitPresence = function (data, callback) { - this.presence.submitPresence(data, callback); + this._docPresence.submitPresence(data, callback); }; // Called whenever (you guessed it!) the connection state changes. This will @@ -387,10 +387,10 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - this.presence.pausePresence(); + this._docPresence.pausePresence(); callEach(callbacks); } else { - this.presence.pausePresence(); + this._docPresence.pausePresence(); } } }; @@ -451,10 +451,10 @@ Doc.prototype.unsubscribe = function(callback) { var isDuplicate = this.connection.sendUnsubscribe(this); pushActionCallback(this.inflightUnsubscribe, isDuplicate, callback); - this.presence.pausePresence(); + this._docPresence.pausePresence(); return; } - this.presence.pausePresence(); + this._docPresence.pausePresence(); if (callback) process.nextTick(callback); }; @@ -487,7 +487,7 @@ Doc.prototype.flush = function() { } if (this.subscribed && !this.hasWritePending()) { - this.presence.flushPresence(); + this._docPresence.flushPresence(); } }; @@ -593,7 +593,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.presence.transformAllPresence(componentOp); + this._docPresence.transformAllPresence(componentOp); this.emit('op', componentOp.op, source); } // Pop whatever was submitted since we started applying this op @@ -606,7 +606,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.presence.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. @@ -623,7 +623,7 @@ 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.presence.transformAllPresence(op); + this._docPresence.transformAllPresence(op); this.emit('create', source); return; } @@ -631,7 +631,7 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - this.presence.transformAllPresence(op); + this._docPresence.transformAllPresence(op); this.emit('del', oldData, source); return; } @@ -883,7 +883,7 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - this.presence.clearCachedOps(); + 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 @@ -896,9 +896,9 @@ Doc.prototype._opAcknowledged = function(message) { // The op was committed successfully. Increment the version number this.version++; - this.presence.cacheOp(this.inflightOp); + this._docPresence.cacheOp(this.inflightOp); this._clearInflightOp(); - this.presence.processAllReceivedPresence(); + this._docPresence.processAllReceivedPresence(); }; Doc.prototype._rollback = function(err) { @@ -947,8 +947,8 @@ Doc.prototype._hardRollback = function(err) { pendingOps = pendingOps.concat(this.pendingOps); // Apply a similar technique for presence. - var pendingPresence = this.presence.getPendingPresence(); - this.presence.hardRollbackPresence(); + var pendingPresence = this._docPresence.getPendingPresence(); + this._docPresence.hardRollbackPresence(); // Cancel all pending ops and reset if we can't invert this._setType(null); diff --git a/test/client/presence.js b/test/client/presence.js index c7ee09d93..1dd6a48a2 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -19,7 +19,7 @@ describe('client presence', function() { var backend = new Backend(); var connection = backend.connect(); var doc = connection.get('dogs', 'fido'); - expect(doc.presence instanceof dummyPresence.DocPresence).to.be(true); + expect(doc._docPresence instanceof dummyPresence.DocPresence).to.be(true); }); it('DummyPresence should subclass Presence', function() { @@ -59,13 +59,13 @@ describe('client presence', function() { this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc.presence.requestReply = 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(submitted).to.equal(true); expect(this.doc2.data).to.eql([]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -80,13 +80,13 @@ describe('client presence', function() { function(done) { this.doc.submitOp({ index: 0, value: 'a' }, errorHandler(done)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); - this.doc.presence.requestReply = 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(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -103,12 +103,12 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), function(err) { if (err) return done(err); this.doc.version -= 2; @@ -131,13 +131,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -155,13 +155,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -179,13 +179,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -203,13 +203,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) ], allDone); @@ -227,13 +227,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -251,13 +251,13 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -276,10 +276,10 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { @@ -287,12 +287,12 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -310,25 +310,25 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this), function(done) { - this.doc2.presence.cachedOps = []; + this.doc2._docPresence.cachedOps = []; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).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.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) ], allDone); @@ -346,8 +346,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc.del(errorHandler(done)); @@ -367,8 +367,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc2.del(errorHandler(done)); @@ -388,8 +388,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -409,8 +409,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(3)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -430,8 +430,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(2)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(2)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); done(); }.bind(this)); this.doc.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -451,8 +451,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current['']).to.eql(p(1)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(2)); + expect(this.doc._docPresence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(2)); done(); }.bind(this)); this.doc2.submitOp({ index: 1, value: 'b' }, errorHandler(done)); @@ -467,10 +467,10 @@ describe('client presence', function() { this.doc.submitOp.bind(this.doc, op), this.doc.del.bind(this.doc), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(3); - expect(this.doc.presence.cachedOps[0].create).to.equal(true); - expect(this.doc.presence.cachedOps[1].op).to.equal(op); - expect(this.doc.presence.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); @@ -485,10 +485,10 @@ describe('client presence', function() { this.doc.del.bind(this.doc), setTimeout, function(done) { - expect(this.doc2.presence.cachedOps.length).to.equal(3); - expect(this.doc2.presence.cachedOps[0].create).to.equal(true); - expect(this.doc2.presence.cachedOps[1].op).to.eql(op); - expect(this.doc2.presence.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); @@ -499,15 +499,15 @@ describe('client presence', function() { var op1 = { index: 1, value: 'b' }; var op2 = { index: 2, value: 'b' }; var op3 = { index: 3, value: 'b' }; - this.doc.presence.cachedOpsTimeout = 60; + this.doc._docPresence.cachedOpsTimeout = 60; async.series([ // Cache 2 ops. this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc.submitOp.bind(this.doc, op1), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(2); - expect(this.doc.presence.cachedOps[0].create).to.equal(true); - expect(this.doc.presence.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), @@ -518,10 +518,10 @@ describe('client presence', function() { }, this.doc.submitOp.bind(this.doc, op2), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(3); - expect(this.doc.presence.cachedOps[0].create).to.equal(true); - expect(this.doc.presence.cachedOps[1].op).to.equal(op1); - expect(this.doc.presence.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), @@ -532,9 +532,9 @@ describe('client presence', function() { }, this.doc.submitOp.bind(this.doc, op3), function(done) { - expect(this.doc.presence.cachedOps.length).to.equal(2); - expect(this.doc.presence.cachedOps[0].op).to.equal(op2); - expect(this.doc.presence.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) @@ -552,13 +552,13 @@ describe('client presence', function() { if (srcList[0] === '') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current['']).to.eql(p(1)); - expect(this.doc2.presence.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); } else { expect(srcList).to.eql([ this.connection.id ]); - expect(this.doc2.presence.current['']).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.requestReply).to.equal(false); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.requestReply).to.equal(false); done(); } }.bind(this)); @@ -639,10 +639,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(2)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); - this.doc.presence.requestReply = 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)); @@ -658,15 +658,15 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); setTimeout(function() { this.doc.subscribe(function(err) { if (err) return done(err); - expect(this.doc2.presence.current).to.eql({}); + expect(this.doc2._docPresence.current).to.eql({}); }.bind(this)); }.bind(this)); }.bind(this) @@ -682,14 +682,14 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); this.doc.submitPresence(p(1), errorHandler(done)); process.nextTick(function() { this.backend.connect(this.connection); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; }.bind(this)); }.bind(this) ], allDone); @@ -704,10 +704,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -721,14 +721,14 @@ describe('client presence', function() { expect(this.doc.hasPending()).to.equal(false); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.presence.pending).to.equal(true); - expect(!!this.doc.presence.inflight).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.presence.pending).to.equal(false); - expect(!!this.doc.presence.inflight).to.equal(false); + expect(!!this.doc._docPresence.pending).to.equal(false); + expect(!!this.doc._docPresence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -742,20 +742,20 @@ describe('client presence', function() { expect(this.doc.hasPending()).to.equal(false); this.doc.submitPresence(p(0)); expect(this.doc.hasPending()).to.equal(true); - expect(!!this.doc.presence.pending).to.equal(true); - expect(!!this.doc.presence.inflight).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.presence.pending).to.equal(false); - expect(!!this.doc.presence.inflight).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.presence.pending).to.equal(false); - expect(!!this.doc.presence.inflight).to.equal(false); + expect(!!this.doc._docPresence.pending).to.equal(false); + expect(!!this.doc._docPresence.inflight).to.equal(false); done(); }.bind(this) ], allDone); @@ -769,16 +769,16 @@ describe('client presence', function() { this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, function(done) { - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); this.doc2.on('presence', function(srcList, submitted) { 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.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); this.doc2.del(errorHandler(done)); }.bind(this) @@ -794,17 +794,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -821,17 +821,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.connection2.close(); @@ -848,17 +848,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc.unsubscribe(errorHandler(done)); @@ -875,17 +875,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2.presence.current).to.not.have.key(connectionId); - expect(this.doc2.presence.current['']).to.eql(p(1)); + expect(this.doc2._docPresence.current).to.not.have.key(connectionId); + expect(this.doc2._docPresence.current['']).to.eql(p(1)); done(); }.bind(this)); this.doc2.unsubscribe(errorHandler(done)); @@ -946,18 +946,18 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); this.connection.close(); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); this.backend.connect(this.connection); process.nextTick(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -972,17 +972,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); this.doc.unsubscribe(errorHandler(done)); - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); this.doc.subscribe(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc.presence.current['']).to.eql(p(0)); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -997,10 +997,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); - this.doc.presence.requestReply = 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)) @@ -1017,10 +1017,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); - this.doc.presence.requestReply = 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)) @@ -1037,10 +1037,10 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); - this.doc.presence.requestReply = 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)) @@ -1061,10 +1061,10 @@ describe('client presence', function() { // 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.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.presence.requestReply = 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)); @@ -1087,10 +1087,10 @@ describe('client presence', function() { // 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.current).to.not.have.key(this.connection.id); + expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); done(); }.bind(this)); - this.doc.presence.requestReply = false; + this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(2), errorHandler(done)); this.doc2.submitOp({ index: 0, value: 'b' }, errorHandler(done)); this.doc2.del(errorHandler(done)); @@ -1109,7 +1109,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc.presence.current['']).to.eql(p(1)); + expect(this.doc._docPresence.current['']).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1132,7 +1132,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2.presence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1346,8 +1346,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', function(err) { @@ -1401,8 +1401,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc.presence.current).to.not.have.key(''); - expect(this.doc.presence.current).to.not.have.key(this.connection2.id); + expect(this.doc._docPresence.current).to.not.have.key(''); + expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', done); @@ -1425,14 +1425,14 @@ describe('client presence', function() { return handleMessage.apply(this, arguments); }; if (expireCache) { - this.doc.presence.receivedTimeout = 0; + this.doc._docPresence.receivedTimeout = 0; } async.series([ this.doc.create.bind(this.doc, [ 'a' ], typeName), this.doc.subscribe.bind(this.doc), this.doc2.subscribe.bind(this.doc2), function(done) { - this.doc2.presence.requestReply = false; + this.doc2._docPresence.requestReply = false; this.doc2.submitPresence(p(0), done); }.bind(this), setTimeout, @@ -1440,7 +1440,7 @@ describe('client presence', function() { setTimeout, function(done) { expect(this.doc.data).to.eql([ 'a', 'b' ]); - expect(this.doc.presence.current[this.connection2.id]).to.eql(p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(0)); // Replay the `lastPresence` with modified payload. lastPresence.p = p(1); lastPresence.v++; // +1 to account for the op above @@ -1451,7 +1451,7 @@ describe('client presence', function() { process.nextTick(done); }.bind(this), function(done) { - expect(this.doc.presence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); + expect(this.doc._docPresence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); process.nextTick(done); }.bind(this) ], allDone); @@ -1464,7 +1464,7 @@ describe('client presence', function() { 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.presence; + var presence = this.doc._docPresence; presence.cachedOps = ['foo']; presence.received = { bar: true }; this.doc.destroy(function(err) { From db39b697d2cc627262f974080fc8317737058237 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:01:20 +0530 Subject: [PATCH 157/181] Move doc._docPresence.current back to original API doc.presence. --- lib/presence/stateless.js | 24 +++--- test/client/presence.js | 170 +++++++++++++++++++------------------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 2f65ca930..d35b22613 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -20,7 +20,7 @@ function DocPresence(doc) { // The current presence data. // Map of src -> presence data // Local src === '' - this.current = {}; + this.doc.presence = {}; // The presence objects received from the server. // Map of src -> presence @@ -245,7 +245,7 @@ DocPresence.prototype.processAllReceivedPresence = function () { }; DocPresence.prototype._transformPresence = function (src, op) { - var presenceData = this.current[src]; + var presenceData = this.doc.presence[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); presenceData = this.doc.type.transformPresence(presenceData, op.op, isOwnOperation); @@ -256,7 +256,7 @@ DocPresence.prototype._transformPresence = function (src, op) { }; DocPresence.prototype.transformAllPresence = function (op) { - var srcList = Object.keys(this.current); + var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -276,12 +276,12 @@ DocPresence.prototype.pausePresence = function () { : this.inflight; this.inflight = null; this.inflightSeq = 0; - } else if (!this.pending && this.current[''] != null) { + } else if (!this.pending && this.doc.presence[''] != null) { this.pending = []; } this.received = {}; this.requestReply = true; - var srcList = Object.keys(this.current); + var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; @@ -296,14 +296,14 @@ DocPresence.prototype.pausePresence = function () { // Returns true, if presence has changed. Otherwise false. DocPresence.prototype._setPresence = function (src, data, emit) { if (data == null) { - if (this.current[src] == null) return false; - delete this.current[src]; + if (this.doc.presence[src] == null) return false; + delete this.doc.presence[src]; } else { var isPresenceEqual = - this.current[src] === data || - (this.doc.type.comparePresence && this.doc.type.comparePresence(this.current[src], data)); + this.doc.presence[src] === data || + (this.doc.type.comparePresence && this.doc.type.comparePresence(this.doc.presence[src], data)); if (isPresenceEqual) return false; - this.current[src] = data; + this.doc.presence[src] = data; } if (emit) this._emitPresence([ src ], true); return true; @@ -348,7 +348,7 @@ DocPresence.prototype.flushPresence = function () { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; this.pending = null; - this.doc.connection.sendPresence(this.doc, this.current[''], this.requestReply); + this.doc.connection.sendPresence(this.doc, this.doc.presence[''], this.requestReply); this.requestReply = false; } }; @@ -371,7 +371,7 @@ DocPresence.prototype.hardRollbackPresence = function () { this.received = {}; this.requestReply = true; - var srcList = Object.keys(this.current); + var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { var src = srcList[i]; diff --git a/test/client/presence.js b/test/client/presence.js index 1dd6a48a2..498438ad6 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -65,7 +65,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -86,7 +86,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); }.bind(this) @@ -103,7 +103,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for a future version. @@ -131,7 +131,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -155,7 +155,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -179,7 +179,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -203,7 +203,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -227,7 +227,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -251,7 +251,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); // A hack to send presence for an older version. @@ -276,7 +276,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -287,7 +287,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'b' ]); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -310,7 +310,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -322,7 +322,7 @@ describe('client presence', function() { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); expect(this.doc2.data).to.eql([ 'a', 'b', 'c' ]); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); // A hack to send presence for an older version. @@ -346,8 +346,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc.del(errorHandler(done)); @@ -367,8 +367,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); done(); }.bind(this)); this.doc2.del(errorHandler(done)); @@ -388,8 +388,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); + 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)); @@ -409,8 +409,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(3)); + 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)); @@ -430,8 +430,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(2)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + 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)); @@ -451,8 +451,8 @@ describe('client presence', function() { this.doc.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current['']).to.eql(p(1)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(2)); + 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)); @@ -552,12 +552,12 @@ describe('client presence', function() { if (srcList[0] === '') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); - expect(this.doc2._docPresence.current).to.not.have.key(this.connection.id); + 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(this.doc2._docPresence.current['']).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); expect(this.doc2._docPresence.requestReply).to.equal(false); done(); } @@ -639,7 +639,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(2)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(2)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -658,7 +658,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -666,7 +666,7 @@ describe('client presence', function() { setTimeout(function() { this.doc.subscribe(function(err) { if (err) return done(err); - expect(this.doc2._docPresence.current).to.eql({}); + expect(this.doc2.presence).to.eql({}); }.bind(this)); }.bind(this)); }.bind(this) @@ -682,7 +682,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -704,7 +704,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -769,13 +769,13 @@ describe('client presence', function() { this.doc.submitPresence.bind(this.doc, p(0)), setTimeout, function(done) { - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + 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 ]); // 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._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -794,17 +794,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.connection.close(); @@ -821,17 +821,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.connection2.close(); @@ -848,17 +848,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.doc.unsubscribe(errorHandler(done)); @@ -875,17 +875,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence['']).to.eql(p(1)); var connectionId = this.connection.id; this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ connectionId ]); expect(submitted).to.equal(false); - expect(this.doc2._docPresence.current).to.not.have.key(connectionId); - expect(this.doc2._docPresence.current['']).to.eql(p(1)); + expect(this.doc2.presence).to.not.have.key(connectionId); + expect(this.doc2.presence['']).to.eql(p(1)); done(); }.bind(this)); this.doc2.unsubscribe(errorHandler(done)); @@ -946,18 +946,18 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); this.connection.close(); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence).to.not.have.key(this.connection2.id); this.backend.connect(this.connection); process.nextTick(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -972,17 +972,17 @@ describe('client presence', function() { this.doc2.submitPresence.bind(this.doc2, p(1)), setTimeout, function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); this.doc.unsubscribe(errorHandler(done)); - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence).to.not.have.key(this.connection2.id); this.doc.subscribe(done); }.bind(this), setTimeout, // wait for re-sync function(done) { - expect(this.doc._docPresence.current['']).to.eql(p(0)); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(1)); process.nextTick(done); }.bind(this) ], allDone); @@ -997,7 +997,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(0)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(0)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1017,7 +1017,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1037,7 +1037,7 @@ describe('client presence', function() { this.doc2.on('presence', function(srcList, submitted) { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(3)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(3)); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1061,7 +1061,7 @@ describe('client presence', function() { // 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._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1087,7 +1087,7 @@ describe('client presence', function() { // 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._docPresence.current).to.not.have.key(this.connection.id); + expect(this.doc2.presence).to.not.have.key(this.connection.id); done(); }.bind(this)); this.doc._docPresence.requestReply = false; @@ -1109,7 +1109,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ '' ]); expect(submitted).to.equal(true); - expect(this.doc._docPresence.current['']).to.eql(p(1)); + expect(this.doc.presence['']).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1132,7 +1132,7 @@ describe('client presence', function() { if (typeName === 'wrapped-presence-no-compare') { expect(srcList).to.eql([ this.connection.id ]); expect(submitted).to.equal(true); - expect(this.doc2._docPresence.current[this.connection.id]).to.eql(p(1)); + expect(this.doc2.presence[this.connection.id]).to.eql(p(1)); done(); } else { done(new Error('Unexpected presence event')); @@ -1346,8 +1346,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', function(err) { @@ -1401,8 +1401,8 @@ describe('client presence', function() { presenceEmitted = true; expect(srcList.sort()).to.eql([ '', this.connection2.id ]); expect(submitted).to.equal(false); - expect(this.doc._docPresence.current).to.not.have.key(''); - expect(this.doc._docPresence.current).to.not.have.key(this.connection2.id); + expect(this.doc.presence).to.not.have.key(''); + expect(this.doc.presence).to.not.have.key(this.connection2.id); }.bind(this)); this.doc.on('error', done); @@ -1440,7 +1440,7 @@ describe('client presence', function() { setTimeout, function(done) { expect(this.doc.data).to.eql([ 'a', 'b' ]); - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(p(0)); // Replay the `lastPresence` with modified payload. lastPresence.p = p(1); lastPresence.v++; // +1 to account for the op above @@ -1451,7 +1451,7 @@ describe('client presence', function() { process.nextTick(done); }.bind(this), function(done) { - expect(this.doc._docPresence.current[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); + expect(this.doc.presence[this.connection2.id]).to.eql(expireCache ? p(1) : p(0)); process.nextTick(done); }.bind(this) ], allDone); From 60a567b2f7b0ace9911efba6c766396e4f7bce21 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:22:40 +0530 Subject: [PATCH 158/181] Split out implementation of ConnectionPresence. --- lib/backend.js | 4 ++++ lib/client/connection.js | 33 +++------------------------- lib/presence/dummy.js | 21 +++++++++++++----- lib/presence/index.js | 5 ++--- lib/presence/stateless.js | 46 +++++++++++++++++++++++++++++++++++---- 5 files changed, 66 insertions(+), 43 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 192636a76..98b052abd 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -159,8 +159,12 @@ Backend.prototype.connect = function(connection, req) { // 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; }; diff --git a/lib/client/connection.js b/lib/client/connection.js index 5be290676..c67b1e6da 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -64,33 +64,6 @@ function Connection(socket) { this.bindToSocket(socket); - this.presence = { - connection: this, - // TODO unify with code in agent.js - isPresenceMessage: function (message) { - return message.a === 'p'; - }, - handlePresenceMessage: function (err, message) { - var doc = this.connection.getExisting(message.c, message.d); - if (doc) doc._handlePresence(err, message); - }, - 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); - } - }; } emitter.mixin(Connection); @@ -283,8 +256,8 @@ Connection.prototype.handleMessage = function(message) { return; default: - if (this.presence.isPresenceMessage(message)) { - return this.presence.handlePresenceMessage(err, message); + if (this._connectionPresence.isPresenceMessage(message)) { + return this._connectionPresence.handlePresenceMessage(err, message); } logger.warn('Ignoring unrecognized message', message); } @@ -459,7 +432,7 @@ Connection.prototype.sendOp = function(doc, op) { * Sends presence data down the socket */ Connection.prototype.sendPresence = function(doc, data, requestReply) { - this.presence.sendPresence(doc, data, requestReply); + this._connectionPresence.sendPresence(doc, data, requestReply); }; diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 8a4bac340..7853c9066 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -7,15 +7,13 @@ */ var presence = require('./index'); -function DocPresence () { } - -// Inherit from Presence to support instanceof type checking. -DocPresence.prototype = Object.create(presence.DocPresence.prototype); - 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, @@ -35,6 +33,17 @@ Object.assign(DocPresence.prototype, { _emitPresence: noop }); + +function ConnectionPresence() {} +ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); + +Object.assign(ConnectionPresence.prototype, { + isPresenceMessage: returnFalse, + handlePresenceMessage: noop, + sendPresence: noop +}); + module.exports = { - DocPresence: DocPresence + DocPresence: DocPresence, + ConnectionPresence: ConnectionPresence }; diff --git a/lib/presence/index.js b/lib/presence/index.js index e4de5669a..42898870a 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,5 +1,4 @@ -var DocPresence = function () {}; - module.exports = { - DocPresence: DocPresence + DocPresence: function DocPresence () {}, + ConnectionPresence: function ConnectionPresence () {}, }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index d35b22613..610a9246f 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -12,9 +12,11 @@ var ShareDBError = require('../error'); var presence = require('./index'); +/* + * Stateless Presence implementation of DocPresence + * ------------------------------------------------ + */ function DocPresence(doc) { - - // The Doc instance that this Presence is attached to. this.doc = doc; // The current presence data. @@ -52,7 +54,6 @@ function DocPresence(doc) { this.inflight = null; } -// Inherit from Presence to support instanceof type checking. DocPresence.prototype = Object.create(presence.DocPresence.prototype); // Submit presence data to a document. @@ -393,6 +394,43 @@ DocPresence.prototype.getPendingPresence = function () { return pendingPresence; }; + +/* + * Stateless Presence implementation of ConnectionPresence + * ------------------------------------------------ + */ +function ConnectionPresence(connection) { + this.connection = connection; + Object.assign(this, { + // TODO unify with code in agent.js + isPresenceMessage: function (message) { + return message.a === 'p'; + }, + handlePresenceMessage: function (err, message) { + var doc = this.connection.getExisting(message.c, message.d); + if (doc) doc._handlePresence(err, message); + }, + 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); + } + }); +} +ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); + module.exports = { - DocPresence: DocPresence + DocPresence: DocPresence, + ConnectionPresence: ConnectionPresence }; From 8b6872eb1cea0acac612635297b8062d7f3513ee Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:26:08 +0530 Subject: [PATCH 159/181] Refactor ConnectionPresence to idiomatic JS class. --- lib/presence/stateless.js | 56 ++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 610a9246f..15ccc359c 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -397,39 +397,41 @@ DocPresence.prototype.getPendingPresence = function () { /* * Stateless Presence implementation of ConnectionPresence - * ------------------------------------------------ + * ------------------------------------------------------- */ function ConnectionPresence(connection) { this.connection = connection; - Object.assign(this, { - // TODO unify with code in agent.js - isPresenceMessage: function (message) { - return message.a === 'p'; - }, - handlePresenceMessage: function (err, message) { - var doc = this.connection.getExisting(message.c, message.d); - if (doc) doc._handlePresence(err, message); - }, - 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); - } - }); } ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); +// TODO unify with code in agent.js +ConnectionPresence.prototype.isPresenceMessage = function (message) { + return message.a === 'p'; +}; + +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); +}; + + module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence From 55e1a4d82d505a5fffe6fae9615b20750ace5608 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:35:23 +0530 Subject: [PATCH 160/181] Split out implementation of AgentPresence. --- lib/agent.js | 75 ++++----------------------------------- lib/presence/dummy.js | 20 ++++++++--- lib/presence/index.js | 1 + lib/presence/stateless.js | 72 ++++++++++++++++++++++++++++++++++++- 4 files changed, 94 insertions(+), 74 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 53891b306..66e1f86f1 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -27,68 +27,7 @@ function Agent(backend, stream) { // Map from queryId -> emitter this.subscribedQueries = {}; - this.presence = { - agent: this, - isPresenceData: function (data) { - return data.a === 'p'; - }, - processPresenceData: function (data) { - if (data.a === 'p') { - // Send other clients' presence data - if (data.src !== this.agent.clientId) this.agent.send(data); - return true; - } - }, - // The max presence sequence number received from the client. - maxPresenceSeq: 0, - 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 - }; - }, - subscribeToStream: function (collection, id, stream) { - var agent = this.agent; - stream.on('end', function() { - agent.backend.sendPresence(agent.presence.createPresence(collection, id)); - }); - }, - 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'; - } - } - }, - 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(4026, '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(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); - }); - } - this.agent.backend.sendPresence(presence, function(err) { - if (err) return callback(err); - callback(null, { seq: presence.seq }); - }); - } - }; + 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. @@ -170,8 +109,8 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (agent.presence.isPresenceData(data)) { - agent.presence.processPresenceData(data); + if (agent._agentPresence.isPresenceData(data)) { + agent._agentPresence.processPresenceData(data); return; } if (agent._isOwnOp(collection, data)) return; @@ -185,7 +124,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { if (util.hasKeys(streams)) return; delete agent.subscribedDocs[collection]; }); - this.presence.subscribeToStream(collection, id, stream); + this._agentPresence.subscribeToStream(collection, id, stream); }; Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query) { @@ -358,7 +297,7 @@ Agent.prototype._checkRequest = function(request) { if (request.c != null && typeof request.c !== 'string') return 'Invalid collection'; if (typeof request.b !== 'object') return 'Invalid bulk subscribe data'; } else { - return this.presence.checkRequest(request); + return this._agentPresence.checkRequest(request); } }; @@ -396,8 +335,8 @@ Agent.prototype._handleMessage = function(request, callback) { case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: - if (this.presence.isPresenceData(request)) { - return this.presence.handlePresenceMessage(request, callback); + if (this._agentPresence.isPresenceData(request)) { + return this._agentPresence.handlePresenceMessage(request, callback); } callback({code: 4000, message: 'Invalid or unknown message'}); } diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 7853c9066..c0e871849 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -11,9 +11,8 @@ function noop () {} function returnEmptyArray () { return []; }; function returnFalse () { return false; }; -function DocPresence () { } +function DocPresence () {} DocPresence.prototype = Object.create(presence.DocPresence.prototype); - Object.assign(DocPresence.prototype, { submitPresence: noop, handlePresence: noop, @@ -33,17 +32,28 @@ Object.assign(DocPresence.prototype, { _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, { + isPresenceData: returnFalse, + processPresenceData: returnFalse, + //maxPresenceSeq: 0, + createPresence: noop, + subscribeToStream: noop, + checkRequest: noop, + handlePresenceMessage: noop +}); + module.exports = { DocPresence: DocPresence, - ConnectionPresence: ConnectionPresence + ConnectionPresence: ConnectionPresence, + AgentPresence: AgentPresence }; diff --git a/lib/presence/index.js b/lib/presence/index.js index 42898870a..0a0b43e11 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,4 +1,5 @@ module.exports = { DocPresence: function DocPresence () {}, ConnectionPresence: function ConnectionPresence () {}, + AgentPresence: function AgentPresence () {} }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 15ccc359c..f4874d023 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -431,8 +431,78 @@ ConnectionPresence.prototype.sendPresence = function(doc, data, requestReply) { this.connection.send(message); }; +/* + * Stateless Presence implementation of AgentPresence + * ------------------------------------------------------- + */ +function AgentPresence(agent) { + Object.assign(this, { + agent: agent, + isPresenceData: function (data) { + return data.a === 'p'; + }, + processPresenceData: function (data) { + if (data.a === 'p') { + // Send other clients' presence data + if (data.src !== this.agent.clientId) this.agent.send(data); + return true; + } + }, + // The max presence sequence number received from the client. + maxPresenceSeq: 0, + 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 + }; + }, + subscribeToStream: function (collection, id, stream) { + var agent = this.agent; + stream.on('end', function() { + agent.backend.sendPresence(agent._agentPresence.createPresence(collection, id)); + }); + }, + 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'; + } + } + }, + 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(4026, '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(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); + }); + } + this.agent.backend.sendPresence(presence, function(err) { + if (err) return callback(err); + callback(null, { seq: presence.seq }); + }); + } + }); +} +AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); module.exports = { DocPresence: DocPresence, - ConnectionPresence: ConnectionPresence + ConnectionPresence: ConnectionPresence, + AgentPresence: AgentPresence }; From 7a79d98736bc49a6e52be32f1fc95a1f2468a52e Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:39:34 +0530 Subject: [PATCH 161/181] Refactor AgentPresence to idiomatic JS class. --- lib/presence/stateless.js | 137 ++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 63 deletions(-) diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index f4874d023..008aeabee 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -431,76 +431,87 @@ ConnectionPresence.prototype.sendPresence = function(doc, data, requestReply) { this.connection.send(message); }; + /* * Stateless Presence implementation of AgentPresence - * ------------------------------------------------------- + * -------------------------------------------------- */ function AgentPresence(agent) { - Object.assign(this, { - agent: agent, - isPresenceData: function (data) { - return data.a === 'p'; - }, - processPresenceData: function (data) { - if (data.a === 'p') { - // Send other clients' presence data - if (data.src !== this.agent.clientId) this.agent.send(data); - return true; - } - }, - // The max presence sequence number received from the client. - maxPresenceSeq: 0, - 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 - }; - }, - subscribeToStream: function (collection, id, stream) { - var agent = this.agent; - stream.on('end', function() { - agent.backend.sendPresence(agent._agentPresence.createPresence(collection, id)); - }); - }, - 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'; - } - } - }, - 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(4026, '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(4025, 'Cannot send presence. Not subscribed to document: ' + presence.c + ' ' + presence.d)); - }); - } - this.agent.backend.sendPresence(presence, function(err) { - if (err) return callback(err); - callback(null, { seq: presence.seq }); - }); - } - }); + this.agent = agent; + + // The max presence sequence number received from the client. + this.maxPresenceSeq = 0; } AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); +AgentPresence.prototype.isPresenceData = function (data) { + return data.a === 'p'; +}; + +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(4026, '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(4025, [ + '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 }); + }); +}; + + module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence, From 693492d9f3067f8a6a3fe1f74825c64e2b8aa07d Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:45:17 +0530 Subject: [PATCH 162/181] Unify isPresenceMessage between ConnectionPresence and AgentPresence --- lib/agent.js | 4 ++-- lib/presence/dummy.js | 4 ++-- lib/presence/stateless.js | 15 ++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 66e1f86f1..7801eb80c 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -109,7 +109,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) { logger.error('Doc subscription stream error', collection, id, data.error); return; } - if (agent._agentPresence.isPresenceData(data)) { + if (agent._agentPresence.isPresenceMessage(data)) { agent._agentPresence.processPresenceData(data); return; } @@ -335,7 +335,7 @@ Agent.prototype._handleMessage = function(request, callback) { case 'nt': return this._fetchSnapshotByTimestamp(request.c, request.d, request.ts, callback); default: - if (this._agentPresence.isPresenceData(request)) { + if (this._agentPresence.isPresenceMessage(request)) { return this._agentPresence.handlePresenceMessage(request, callback); } callback({code: 4000, message: 'Invalid or unknown message'}); diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index c0e871849..28eae5b58 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -4,6 +4,7 @@ * * 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'); @@ -43,9 +44,8 @@ Object.assign(ConnectionPresence.prototype, { function AgentPresence() {} AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); Object.assign(AgentPresence.prototype, { - isPresenceData: returnFalse, + isPresenceMessage: returnFalse, processPresenceData: returnFalse, - //maxPresenceSeq: 0, createPresence: noop, subscribeToStream: noop, checkRequest: noop, diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 008aeabee..7f208d882 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -12,6 +12,12 @@ var ShareDBError = require('../error'); var presence = require('./index'); +// Check if a message represence presence. +// Used in both ConnectionPresence and AgentPresence. +function isPresenceMessage(data) { + return data.a === 'p'; +}; + /* * Stateless Presence implementation of DocPresence * ------------------------------------------------ @@ -404,10 +410,7 @@ function ConnectionPresence(connection) { } ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototype); -// TODO unify with code in agent.js -ConnectionPresence.prototype.isPresenceMessage = function (message) { - return message.a === 'p'; -}; +ConnectionPresence.prototype.isPresenceMessage = isPresenceMessage; ConnectionPresence.prototype.handlePresenceMessage = function (err, message) { var doc = this.connection.getExisting(message.c, message.d); @@ -444,9 +447,7 @@ function AgentPresence(agent) { } AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); -AgentPresence.prototype.isPresenceData = function (data) { - return data.a === 'p'; -}; +AgentPresence.prototype.isPresenceMessage = isPresenceMessage; AgentPresence.prototype.processPresenceData = function (data) { if (data.a === 'p') { From 538c3c15f3e18f410539c2bd2672ca5a69f56a98 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 17:56:36 +0530 Subject: [PATCH 163/181] Update README to document presence API --- README.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d82b92ad7..fc7af89cb 100644 --- a/README.md +++ b/README.md @@ -74,9 +74,19 @@ 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. This feature is opt-in, not enabled by default. To enable this feature, pass the `enablePresence: true` option to the ShareDB constructor (e.g. `var share = new ShareDB({ enablePresence: true })`). +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. + +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, which may be problematic in terms of performance when a large number of users are present on the same documentsimultaneously. If you don't expect too many simultaneous users, this should work well. The server does not store any state at all regarding presence (it exists only in clients), hence the name "Stateless Presence". + +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 })`). +``` 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. @@ -98,7 +108,7 @@ __Options__ * `options.pubsub` _(instance of `ShareDB.PubSub`)_ Notify other ShareDB processes when data changes through this pub/sub adapter. Defaults to `ShareDB.MemoryPubSub()`. -* `options.enablePresence` _(optional boolean)_ +* `options.presence` _(implementation of presence API)_ Enable user presence synchronization. #### Database Adapters From 963affae4d2d9257dac5a0d8b0cbb18fd29eda4c Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 18:03:21 +0530 Subject: [PATCH 164/181] Iterate README --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fc7af89cb..2ce8a9070 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,9 @@ default, ShareDB stores all operations forever - nothing is truly deleted. 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. -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, which may be problematic in terms of performance when a large number of users are present on the same documentsimultaneously. If you don't expect too many simultaneous users, this should work well. The server does not store any state at all regarding presence (it exists only in clients), hence the name "Stateless Presence". +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: @@ -88,8 +90,6 @@ var statelessPresence = require('sharedb/lib/presence/stateless'); var share = new ShareDB({ presence: 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), 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. - ## Server API ### Initialization From 810175ed2c88e4a67625743b85c313457b868326 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 18:18:53 +0530 Subject: [PATCH 165/181] Minor cleanup --- lib/client/connection.js | 2 -- lib/client/doc.js | 24 +++--------------------- lib/presence/stateless.js | 3 ++- lib/util.js | 12 ++++++++++++ 4 files changed, 17 insertions(+), 24 deletions(-) diff --git a/lib/client/connection.js b/lib/client/connection.js index c67b1e6da..336050ebe 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -63,7 +63,6 @@ function Connection(socket) { this.state = connectionState(socket); this.bindToSocket(socket); - } emitter.mixin(Connection); @@ -435,7 +434,6 @@ Connection.prototype.sendPresence = function(doc, data, requestReply) { this._connectionPresence.sendPresence(doc, data, requestReply); }; - /** * Sends a message down the socket */ diff --git a/lib/client/doc.js b/lib/client/doc.js index 963e9fa01..e1124c7f4 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -2,6 +2,7 @@ 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. @@ -387,11 +388,9 @@ Doc.prototype._onConnectionStateChanged = function() { if (this.inflightUnsubscribe.length) { var callbacks = this.inflightUnsubscribe; this.inflightUnsubscribe = []; - this._docPresence.pausePresence(); callEach(callbacks); - } else { - this._docPresence.pausePresence(); } + this._docPresence.pausePresence(); } }; @@ -447,14 +446,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._docPresence.pausePresence(); return; } - this._docPresence.pausePresence(); if (callback) process.nextTick(callback); }; @@ -993,18 +990,3 @@ Doc.prototype._clearInflightOp = function(err) { 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; -} - -// Expose callEach to presence methods via Doc prototype. -Doc.prototype._callEach = callEach; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 7f208d882..8feccaaaa 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -11,6 +11,7 @@ */ 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. @@ -115,7 +116,7 @@ DocPresence.prototype.handlePresence = function (err, presence) { var callbacks = this.inflight; this.inflight = null; this.inflightSeq = 0; - var called = callbacks && this.doc._callEach(callbacks, err); + var called = callbacks && callEach(callbacks, err); if (err && !called) this.doc.emit('error', err); this.doc.flush(); this.doc._emitNothingPending(); diff --git a/lib/util.js b/lib/util.js index 6ca346ffe..ad7048a58 100644 --- a/lib/util.js +++ b/lib/util.js @@ -22,3 +22,15 @@ exports.isValidVersion = function (version) { 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; +}; From 0724fd7a1b57bab50644b215b19209ba927886b0 Mon Sep 17 00:00:00 2001 From: curran Date: Fri, 19 Apr 2019 23:40:31 +0530 Subject: [PATCH 166/181] Migrate backend presence logic to decoupled BackendPresence class. --- lib/backend.js | 5 +++-- lib/presence/dummy.js | 9 ++++++++- lib/presence/index.js | 3 ++- lib/presence/stateless.js | 12 +++++++++++- 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/lib/backend.js b/lib/backend.js index 98b052abd..116bd139c 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -51,6 +51,8 @@ function Backend(options) { } this.presence = options.presence || dummyPresence; + + this._backendPresence = new this.presence.BackendPresence(this); } module.exports = Backend; emitter.mixin(Backend); @@ -731,8 +733,7 @@ Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, c }; 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) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 28eae5b58..7eda4b13e 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -52,8 +52,15 @@ Object.assign(AgentPresence.prototype, { 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 + AgentPresence: AgentPresence, + BackendPresence: BackendPresence }; diff --git a/lib/presence/index.js b/lib/presence/index.js index 0a0b43e11..2d85e8fc5 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,5 +1,6 @@ module.exports = { DocPresence: function DocPresence () {}, ConnectionPresence: function ConnectionPresence () {}, - AgentPresence: function AgentPresence () {} + AgentPresence: function AgentPresence () {}, + BackendPresence: function BackendPresence () {} }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 8feccaaaa..58dcbaa22 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -513,9 +513,19 @@ AgentPresence.prototype.handlePresenceMessage = function(request, callback) { }); }; +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 + AgentPresence: AgentPresence, + BackendPresence: BackendPresence }; From 7af8427771f9721907e6419b2b3f4f1fbd0edd57 Mon Sep 17 00:00:00 2001 From: curran Date: Sat, 20 Apr 2019 00:25:57 +0530 Subject: [PATCH 167/181] Finishing touches --- README.md | 8 ++++---- lib/client/connection.js | 3 --- lib/client/doc.js | 8 +++----- lib/presence/stateless.js | 7 +++++++ test/client/presence.js | 9 ++++++++- 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 2ce8a9070..a5b6e3bac 100644 --- a/README.md +++ b/README.md @@ -108,8 +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 API)_ - Enable user presence synchronization. +* `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 @@ -327,8 +327,8 @@ Unique document ID `doc.data` _(Object)_ Document contents. Available after document is fetched or subscribed to. -`doc.presence.current` _(Object)_ -Each property under `doc.presence.current` 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. +`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. 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. diff --git a/lib/client/connection.js b/lib/client/connection.js index 336050ebe..c37c639f1 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -427,9 +427,6 @@ Connection.prototype.sendOp = function(doc, op) { this.send(message); }; -/** - * Sends presence data down the socket - */ Connection.prototype.sendPresence = function(doc, data, requestReply) { this._connectionPresence.sendPresence(doc, data, requestReply); }; diff --git a/lib/client/doc.js b/lib/client/doc.js index e1124c7f4..bd92e6506 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -56,7 +56,6 @@ var callEach = require('../util').callEach; */ module.exports = Doc; - function Doc(connection, collection, id) { emitter.EventEmitter.call(this); @@ -118,18 +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) return callback(err); return doc.emit('error', err); } - doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); }); } else { - doc._docPresence.destroyPresence(); doc.connection._destroyDoc(doc); if (callback) callback(); } @@ -612,6 +610,8 @@ Doc.prototype._otApply = function(op, source) { this.emit('op', op.op, source); return; } + + this._docPresence.transformAllPresence(op); if (op.create) { this._setType(op.create.type); @@ -620,7 +620,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._docPresence.transformAllPresence(op); this.emit('create', source); return; } @@ -628,7 +627,6 @@ Doc.prototype._otApply = function(op, source) { if (op.del) { var oldData = this.data; this._setType(null); - this._docPresence.transformAllPresence(op); this.emit('del', oldData, source); return; } diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 58dcbaa22..f59f48f0c 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -19,6 +19,7 @@ function isPresenceMessage(data) { return data.a === 'p'; }; + /* * Stateless Presence implementation of DocPresence * ------------------------------------------------ @@ -513,6 +514,11 @@ AgentPresence.prototype.handlePresenceMessage = function(request, callback) { }); }; + +/* + * Stateless Presence implementation of BackendPresence + * ---------------------------------------------------- + */ function BackendPresence(backend) { this.backend = backend; } @@ -523,6 +529,7 @@ BackendPresence.prototype.sendPresence = function(presence, callback) { this.backend.pubsub.publish(channels, presence, callback); } + module.exports = { DocPresence: DocPresence, ConnectionPresence: ConnectionPresence, diff --git a/test/client/presence.js b/test/client/presence.js index 498438ad6..88cf1fee4 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -15,13 +15,20 @@ types.register(presenceType.type2); types.register(presenceType.type3); describe('client presence', function() { - it('should use dummyPresence.DocPresence if presence option not provided', 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); }); From 910b384ef870af98aa36a712d525fda5f4b1ea73 Mon Sep 17 00:00:00 2001 From: Sylvain Lesage Date: Thu, 2 May 2019 18:15:24 +0200 Subject: [PATCH 168/181] fix: repair broken link --- examples/leaderboard/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@ ![Demo](demo.gif) -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 From 406d4e09705e6eada15d39313fb2501d821c6bee Mon Sep 17 00:00:00 2001 From: qinyang Date: Thu, 9 May 2019 13:51:46 +0800 Subject: [PATCH 169/181] Update logger.js in chrome 51, will throw error Uncaught TypeError: Illegal invocation --- lib/logger/logger.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger/logger.js b/lib/logger/logger.js index 6d193e169..60bd7ebca 100644 --- a/lib/logger/logger.js +++ b/lib/logger/logger.js @@ -15,7 +15,7 @@ Logger.prototype.setMethods = function (overrides) { SUPPORTED_METHODS.forEach(function (method) { if (typeof overrides[method] === 'function') { - logger[method] = overrides[method]; + logger[method] = overrides[method].bind(overrides); } }); }; From 7c729d7ee9ea4ff4e6f7ea43004ee134dc376478 Mon Sep 17 00:00:00 2001 From: qinyang Date: Fri, 10 May 2019 18:45:22 +0800 Subject: [PATCH 170/181] fix: change the default logger Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628 --- lib/logger/logger.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/logger/logger.js b/lib/logger/logger.js index 60bd7ebca..02bb45177 100644 --- a/lib/logger/logger.js +++ b/lib/logger/logger.js @@ -5,7 +5,12 @@ var SUPPORTED_METHODS = [ ]; function Logger() { - this.setMethods(console); + 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; @@ -15,7 +20,7 @@ Logger.prototype.setMethods = function (overrides) { SUPPORTED_METHODS.forEach(function (method) { if (typeof overrides[method] === 'function') { - logger[method] = overrides[method].bind(overrides); + logger[method] = overrides[method]; } }); }; From 65ce13154049d244a7d3a4e8c8ca22e0d7cf6d1d Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 15 May 2019 09:20:44 -0700 Subject: [PATCH 171/181] 1.0.0-beta.23 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 76dc3f878..af6e8eb6f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sharedb", - "version": "1.0.0-beta.22", + "version": "1.0.0-beta.23", "description": "JSON OT database backend", "main": "lib/index.js", "dependencies": { From f09ad6221ff06b9bd04e6b0e23a4f195104505cf Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 15 May 2019 09:46:10 -0700 Subject: [PATCH 172/181] Update rest of examples to @teamwork/websocket-json-stream --- examples/counter/package.json | 2 +- examples/counter/server.js | 2 +- examples/leaderboard/package.json | 2 +- examples/leaderboard/server/index.js | 2 +- examples/rich-text/package.json | 2 +- examples/rich-text/server.js | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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..8817a4584 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); 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..25410f8bb 100644 --- a/examples/leaderboard/server/index.js +++ b/examples/leaderboard/server/index.js @@ -3,7 +3,7 @@ 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'); 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..486130f95 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(); From 2fb0637990390a712cdd39843f3f24ea225d6cd6 Mon Sep 17 00:00:00 2001 From: Abe Pazos Date: Sun, 19 May 2019 11:35:48 +0200 Subject: [PATCH 173/181] Fix broken status indication in textarea example --- examples/textarea/client.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/textarea/client.js b/examples/textarea/client.js index c47035a1c..dadaef7fa 100644 --- a/examples/textarea/client.js +++ b/examples/textarea/client.js @@ -8,21 +8,21 @@ var connection = new sharedb.Connection(socket); var element = document.querySelector('textarea'); var statusSpan = document.getElementById('status-span'); -status.innerHTML = "Not Connected" +statusSpan.innerHTML = "Not Connected" element.style.backgroundColor = "gray"; socket.onopen = function(){ - status.innerHTML = "Connected" + statusSpan.innerHTML = "Connected" element.style.backgroundColor = "white"; }; socket.onclose = function(){ - status.innerHTML = "Closed" + statusSpan.innerHTML = "Closed" element.style.backgroundColor = "gray"; }; socket.onerror = function() { - status.innerHTML = "Error" + statusSpan.innerHTML = "Error" element.style.backgroundColor = "red"; } From 95ae394b02cbe4f075e6b3024ea23f679aac7d7f Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 23 May 2019 13:39:30 +0100 Subject: [PATCH 174/181] Allow options to be passed for `fetch` and `getOps` The database adapters allow an `options` object to be passed through to them for enabling the return of metadata with a snapshot or ops. Consumers could query the database directly, or even use the database adapters, but this may give inconsistent results when comparing ops with those fetched through `getOps`. For example, the Mongo adapter makes sure that a valid set of ops with unique versions are returned, which may not be the case when querying the database directly. Fetching ops and snapshots through `Backend` methods also ensures that we call the appropriate "sanitize" methods, and trigger the corresponding middleware. However, we don't expose this on `Backend.getOps` or `Backend.fetch`. This change adds an optional `options` argument to these methods, which can then be used to ask for metadata. Note that an options argument has been added to `Backend.subscribe`, but using it will return an error. This is to keep the signature consistent with `fetch` and `getOps`. However, the implementation is beyond the scope of this change, because we'd need to add some way to configure `SubmitRequest.commit` to optionally pass metadata to the appropriate clients, who provided that given option on `subscribe`. --- README.md | 1 + lib/backend.js | 49 ++++++++++++++++++----- test/backend.js | 104 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 test/backend.js diff --git a/README.md b/README.md index 8cbbfecf0..adee7b51e 100644 --- a/README.md +++ b/README.md @@ -467,6 +467,7 @@ Additional fields may be added to the error object for debugging context dependi * 4022 - Database adapter does not support queries * 4023 - Cannot project snapshots of this type * 4024 - Invalid version +* 4025 - Passing options to subscribe has not been implemented ### 5000 - Internal error diff --git a/lib/backend.js b/lib/backend.js index 442da075c..96d086479 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -313,7 +313,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; @@ -326,7 +330,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); @@ -336,7 +341,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; @@ -348,7 +357,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); @@ -358,7 +368,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; @@ -370,7 +384,8 @@ 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]; @@ -382,7 +397,11 @@ Backend.prototype.fetch = function(agent, index, id, callback) { }); }; -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; @@ -394,7 +413,8 @@ 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); @@ -407,7 +427,18 @@ Backend.prototype.fetchBulk = function(agent, index, ids, callback) { }; // 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; diff --git a/test/backend.js b/test/backend.js new file mode 100644 index 000000000..d04746e2b --- /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(); + }); + }); + }); + }); +}); From 1a7bc3e86586965761a3caa653146044000b4d72 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 4 Jul 2019 15:09:38 +0100 Subject: [PATCH 175/181] Move from `jshint` to `eslint` This change addresses three things: 1. Move us from `jshint` to `eslint` (which is the more commonly used linter) 2. Fix our linting glob patterns, which currently only address files at the top of their respective directories (and ignore all files in subdirectories) 3. Enforce stricter linting rules, based roughly on the [Google][1] rules [1]: https://github.com/google/eslint-config-google --- .eslintrc | 139 +++++++ .jshintrc | 18 - .travis.yml | 2 +- lib/agent.js | 10 +- lib/backend.js | 85 ++-- lib/client/connection.js | 12 +- lib/client/doc.js | 13 +- lib/client/query.js | 1 - .../snapshot-request/snapshot-request.js | 6 +- .../snapshot-timestamp-request.js | 2 +- .../snapshot-version-request.js | 4 +- lib/logger/logger.js | 6 +- lib/milestone-db/index.js | 14 +- lib/milestone-db/memory.js | 32 +- lib/milestone-db/no-op.js | 8 +- lib/ot.js | 10 +- lib/projections.js | 2 +- lib/query-emitter.js | 38 +- lib/submit-request.js | 58 +-- lib/util.js | 6 +- package.json | 6 +- test/backend.js | 54 +-- test/client/connection.js | 56 ++- test/client/doc.js | 71 ++-- test/client/pending.js | 2 - test/client/projections.js | 11 +- test/client/query-subscribe.js | 83 +++- test/client/query.js | 52 ++- test/client/snapshot-timestamp-request.js | 204 +++++----- test/client/snapshot-version-request.js | 184 ++++----- test/client/submit.js | 38 +- test/client/subscribe.js | 133 +++++-- test/db.js | 6 +- test/logger.js | 14 +- test/middleware.js | 8 - test/milestone-db.js | 366 +++++++++--------- test/ot.js | 12 +- test/projections.js | 21 +- test/pubsub.js | 1 - test/util.js | 2 +- 40 files changed, 1038 insertions(+), 752 deletions(-) create mode 100644 .eslintrc delete mode 100644 .jshintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 000000000..0ce02cbc0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,139 @@ +{ + "parserOptions": { + "ecmaVersion": 3 + }, + "rules": { + "no-cond-assign": 0, + "no-irregular-whitespace": 2, + "no-unexpected-multiline": 2, + "curly": [ + 2, + "multi-line" + ], + "guard-for-in": 0, + "no-caller": 2, + "no-extend-native": 2, + "no-extra-bind": 2, + "no-invalid-this": 2, + "no-multi-spaces": 2, + "no-multi-str": 2, + "no-new-wrappers": 2, + "no-throw-literal": 2, + "no-with": 2, + "prefer-promise-reject-errors": 2, + "no-unused-vars": [ + 2, + { + "args": "none" + } + ], + "array-bracket-newline": 0, + "array-bracket-spacing": [ + 2, + "never" + ], + "array-element-newline": 0, + "block-spacing": [ + 2, + "never" + ], + "brace-style": 2, + "camelcase": [ + 2, + { + "properties": "never" + } + ], + "comma-spacing": 2, + "comma-style": 2, + "computed-property-spacing": 2, + "eol-last": 2, + "func-call-spacing": 2, + "indent": [ + "error", + 2, + { + "CallExpression": { + "arguments": 1 + }, + "FunctionDeclaration": { + "body": 1, + "parameters": 1 + }, + "FunctionExpression": { + "body": 1, + "parameters": 1 + }, + "MemberExpression": 1, + "ObjectExpression": 1, + "SwitchCase": 1, + "ignoredNodes": [ + "ConditionalExpression" + ] + } + ], + "key-spacing": 2, + "keyword-spacing": 2, + "linebreak-style": 2, + "max-len": [ + 2, + { + "code": 120, + "tabWidth": 2, + "ignoreUrls": true, + } + ], + "new-cap": 2, + "no-array-constructor": 2, + "no-mixed-spaces-and-tabs": 2, + "no-multiple-empty-lines": [ + 2, + { + "max": 2 + } + ], + "no-new-object": 2, + "no-tabs": 2, + "no-trailing-spaces": 2, + "object-curly-spacing": 2, + "one-var": [ + 2, + { + "var": "never", + "let": "never", + "const": "never" + } + ], + "padded-blocks": [ + 2, + "never" + ], + "quote-props": [ + 2, + "as-needed" + ], + "quotes": [ + 2, + "single", + { + "allowTemplateLiterals": true + } + ], + "semi": 2, + "semi-spacing": 2, + "space-before-blocks": 2, + "space-before-function-paren": [ + 2, + { + "asyncArrow": "always", + "anonymous": "never", + "named": "never" + } + ], + "spaced-comment": [ + 2, + "always" + ], + "switch-colon-spacing": 2, + }, +} \ No newline at end of file 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 21efafe46..5dc6cbaa0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ node_js: - "10" - "8" - "6" -script: "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/lib/agent.js b/lib/agent.js index b5cef65c1..db18c4a5d 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -56,7 +56,6 @@ 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; @@ -265,7 +264,7 @@ Agent.prototype._open = function() { agent._handleMessage(request.data, callback); }); }); - + var cleanup = agent._cleanup.bind(agent); this.stream.on('end', cleanup); this.stream.on('close', cleanup); @@ -333,7 +332,7 @@ 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++) { @@ -362,7 +361,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 = { @@ -607,10 +605,10 @@ Agent.prototype._createOp = function(request) { } }; -Agent.prototype._fetchSnapshot = function (collection, id, version, callback) { +Agent.prototype._fetchSnapshot = function(collection, id, version, callback) { this.backend.fetchSnapshot(this, collection, id, version, callback); }; -Agent.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { +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 96d086479..1d5c4fe4f 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -11,7 +11,6 @@ var QueryEmitter = require('./query-emitter'); var Snapshot = require('./snapshot'); var StreamSocket = require('./stream-socket'); var SubmitRequest = require('./submit-request'); -var types = require('./types'); var warnDeprecatedDoc = true; var warnDeprecatedAfterSubmit = true; @@ -95,7 +94,10 @@ Backend.prototype.SNAPSHOT_TYPES = { Backend.prototype._shimDocAction = function() { if (warnDeprecatedDoc) { warnDeprecatedDoc = false; - console.warn('DEPRECATED: "doc" middleware action. Use "readSnapshots" instead. Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.'); + console.warn([ + 'DEPRECATED: "doc" middleware action. Use "readSnapshots" instead.', + 'Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.' + ].join(' ')); } var backend = this; @@ -112,7 +114,10 @@ Backend.prototype._shimDocAction = function() { Backend.prototype._shimAfterSubmit = function() { if (warnDeprecatedAfterSubmit) { warnDeprecatedAfterSubmit = false; - console.warn('DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.'); + console.warn([ + 'DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass', + '`disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.' + ].join(' ')); } var backend = this; @@ -389,11 +394,17 @@ Backend.prototype.fetch = function(agent, index, id, options, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = [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._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); + }); }); }; @@ -418,11 +429,17 @@ Backend.prototype.fetchBulk = function(agent, index, ids, options, callback) { if (err) return callback(err); var snapshotProjection = backend._getSnapshotProjection(backend.db, projection); var snapshots = backend._getSnapshotsFromMap(ids, 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); - }); + 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); + }); }); }; @@ -617,9 +634,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, backend.SNAPSHOT_TYPES.current, function(err) { - callback(err, snapshots, extra); - }); + backend._sanitizeSnapshots( + agent, + request.snapshotProjection, + request.collection, + snapshots, + backend.SNAPSHOT_TYPES.current, + function(err) { + callback(err, snapshots, extra); + }); }); }; @@ -651,12 +674,12 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) version: version }; - this._fetchSnapshot(collection, id, version, function (error, snapshot) { + 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) { + 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); @@ -664,25 +687,25 @@ Backend.prototype.fetchSnapshot = function(agent, index, id, version, callback) }); }; -Backend.prototype._fetchSnapshot = function (collection, id, version, callback) { +Backend.prototype._fetchSnapshot = function(collection, id, version, callback) { var db = this.db; var backend = this; - this.milestoneDb.getMilestoneSnapshot(collection, id, version, function (error, milestoneSnapshot) { + 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) { + db.getOps(collection, id, from, version, null, function(error, ops) { if (error) return callback(error); - backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, function (error, snapshot) { + 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' }); + return callback({code: 4024, message: 'Requested version exceeds latest snapshot version'}); } callback(null, snapshot); @@ -691,7 +714,7 @@ Backend.prototype._fetchSnapshot = function (collection, id, version, callback) }); }; -Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timestamp, callback) { +Backend.prototype.fetchSnapshotByTimestamp = function(agent, index, id, timestamp, callback) { var start = Date.now(); var backend = this; var projection = this.projections[index]; @@ -704,12 +727,12 @@ Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timesta timestamp: timestamp }; - this._fetchSnapshotByTimestamp(collection, id, timestamp, function (error, snapshot) { + 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) { + 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); @@ -717,7 +740,7 @@ Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timesta }); }; -Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { +Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) { var db = this.db; var milestoneDb = this.milestoneDb; var backend = this; @@ -726,17 +749,17 @@ Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestam var from = 0; var to = null; - milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function (error, snapshot) { + 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) { + 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) { + db.getOps(collection, id, from, to, options, function(error, ops) { if (error) return callback(error); filterOpsInPlaceBeforeTimestamp(ops, timestamp); backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, callback); @@ -745,7 +768,7 @@ Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestam }); }; -Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, 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); diff --git a/lib/client/connection.js b/lib/client/connection.js index cd56306b2..779b71383 100644 --- a/lib/client/connection.js +++ b/lib/client/connection.js @@ -158,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); } @@ -295,8 +293,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); @@ -607,7 +605,7 @@ Connection.prototype._firstQuery = function(fn) { } }; -Connection.prototype._firstSnapshotRequest = function () { +Connection.prototype._firstSnapshotRequest = function() { for (var id in this._snapshotRequests) { return this._snapshotRequests[id]; } @@ -657,7 +655,7 @@ Connection.prototype.fetchSnapshot = function(collection, id, version, callback) * } * */ -Connection.prototype.fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) { +Connection.prototype.fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) { if (typeof timestamp === 'function') { callback = timestamp; timestamp = null; @@ -669,7 +667,7 @@ Connection.prototype.fetchSnapshotByTimestamp = function (collection, id, timest snapshotRequest.send(); }; -Connection.prototype._handleSnapshotFetch = function (error, message) { +Connection.prototype._handleSnapshotFetch = function(error, message) { var snapshotRequest = this._snapshotRequests[message.id]; if (!snapshotRequest) return; delete this._snapshotRequests[message.id]; diff --git a/lib/client/doc.js b/lib/client/doc.js index 15798f0e5..d9c6f19fb 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -136,12 +136,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); @@ -180,7 +178,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); } @@ -653,7 +654,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); } @@ -839,7 +843,6 @@ Doc.prototype.resume = function() { Doc.prototype._opAcknowledged = function(message) { if (this.inflightOp.create) { this.version = message.v; - } 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 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 index 00ed9b90f..95f68055b 100644 --- a/lib/client/snapshot-request/snapshot-request.js +++ b/lib/client/snapshot-request/snapshot-request.js @@ -20,7 +20,7 @@ function SnapshotRequest(connection, requestId, collection, id, callback) { } emitter.mixin(SnapshotRequest); -SnapshotRequest.prototype.send = function () { +SnapshotRequest.prototype.send = function() { if (!this.connection.canSend) { return; } @@ -29,7 +29,7 @@ SnapshotRequest.prototype.send = function () { this.sent = true; }; -SnapshotRequest.prototype._onConnectionStateChanged = function () { +SnapshotRequest.prototype._onConnectionStateChanged = function() { if (this.connection.canSend) { if (!this.sent) this.send(); } else { @@ -40,7 +40,7 @@ SnapshotRequest.prototype._onConnectionStateChanged = function () { } }; -SnapshotRequest.prototype._handleResponse = function (error, message) { +SnapshotRequest.prototype._handleResponse = function(error, message) { this.emit('ready'); if (error) { diff --git a/lib/client/snapshot-request/snapshot-timestamp-request.js b/lib/client/snapshot-request/snapshot-timestamp-request.js index 53c3b2437..0e8af04af 100644 --- a/lib/client/snapshot-request/snapshot-timestamp-request.js +++ b/lib/client/snapshot-request/snapshot-timestamp-request.js @@ -15,7 +15,7 @@ function SnapshotTimestampRequest(connection, requestId, collection, id, timesta SnapshotTimestampRequest.prototype = Object.create(SnapshotRequest.prototype); -SnapshotTimestampRequest.prototype._message = function () { +SnapshotTimestampRequest.prototype._message = function() { return { a: 'nt', id: this.requestId, diff --git a/lib/client/snapshot-request/snapshot-version-request.js b/lib/client/snapshot-request/snapshot-version-request.js index 60a2e3a3c..0b64b3821 100644 --- a/lib/client/snapshot-request/snapshot-version-request.js +++ b/lib/client/snapshot-request/snapshot-version-request.js @@ -3,7 +3,7 @@ var util = require('../../util'); module.exports = SnapshotVersionRequest; -function SnapshotVersionRequest (connection, requestId, collection, id, version, callback) { +function SnapshotVersionRequest(connection, requestId, collection, id, version, callback) { SnapshotRequest.call(this, connection, requestId, collection, id, callback); if (!util.isValidVersion(version)) { @@ -15,7 +15,7 @@ function SnapshotVersionRequest (connection, requestId, collection, id, version, SnapshotVersionRequest.prototype = Object.create(SnapshotRequest.prototype); -SnapshotVersionRequest.prototype._message = function () { +SnapshotVersionRequest.prototype._message = function() { return { a: 'nf', id: this.requestId, diff --git a/lib/logger/logger.js b/lib/logger/logger.js index 02bb45177..3c70e5a53 100644 --- a/lib/logger/logger.js +++ b/lib/logger/logger.js @@ -6,7 +6,7 @@ var SUPPORTED_METHODS = [ function Logger() { var defaultMethods = {}; - SUPPORTED_METHODS.forEach(function (method) { + 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); }); @@ -14,11 +14,11 @@ function Logger() { } module.exports = Logger; -Logger.prototype.setMethods = function (overrides) { +Logger.prototype.setMethods = function(overrides) { overrides = overrides || {}; var logger = this; - SUPPORTED_METHODS.forEach(function (method) { + 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 index 1b04ad30e..3726b2ca8 100644 --- a/lib/milestone-db/index.js +++ b/lib/milestone-db/index.js @@ -24,7 +24,7 @@ MilestoneDB.prototype.close = function(callback) { * @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) { +MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { var error = new ShareDBError(5019, 'getMilestoneSnapshot MilestoneDB method unimplemented'); this._callBackOrEmitError(error, callback); }; @@ -35,30 +35,30 @@ MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, * @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) { +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) { +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) { +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) { +MilestoneDB.prototype._isValidVersion = function(version) { return util.isValidVersion(version); }; -MilestoneDB.prototype._isValidTimestamp = function (timestamp) { +MilestoneDB.prototype._isValidTimestamp = function(timestamp) { return util.isValidTimestamp(timestamp); }; -MilestoneDB.prototype._callBackOrEmitError = function (error, callback) { +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 index 52f1ba522..7b64dee36 100644 --- a/lib/milestone-db/memory.js +++ b/lib/milestone-db/memory.js @@ -22,15 +22,15 @@ function MemoryMilestoneDB(options) { MemoryMilestoneDB.prototype = Object.create(MilestoneDB.prototype); -MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { +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) { +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); @@ -40,25 +40,29 @@ MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapsh var milestoneSnapshots = this._getMilestoneSnapshotsSync(collection, snapshot.id); milestoneSnapshots.push(snapshot); - milestoneSnapshots.sort(function (a, b) { + 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')); +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')); +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) { + this._findMilestoneSnapshot(collection, id, filter, function(error, snapshot) { if (error) return process.nextTick(callback, error); var mtime = snapshot && snapshot.m && snapshot.m.mtime; @@ -70,7 +74,7 @@ MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collec }); }; -MemoryMilestoneDB.prototype._findMilestoneSnapshot = function (collection, id, breakCondition, callback) { +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')); @@ -89,13 +93,13 @@ MemoryMilestoneDB.prototype._findMilestoneSnapshot = function (collection, id, b process.nextTick(callback, null, milestoneSnapshot); }; -MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function (collection, id) { +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) { + return function(currentSnapshot, nextSnapshot) { if (version === null) { return false; } @@ -105,7 +109,7 @@ function versionLessThanOrEqualTo(version) { } function timestampGreaterThanOrEqualTo(timestamp) { - return function (currentSnapshot) { + return function(currentSnapshot) { if (timestamp === null) { return false; } @@ -116,7 +120,7 @@ function timestampGreaterThanOrEqualTo(timestamp) { } function timestampLessThanOrEqualTo(timestamp) { - return function (currentSnapshot, nextSnapshot) { + return function(currentSnapshot, nextSnapshot) { if (timestamp === null) { return !!currentSnapshot; } diff --git a/lib/milestone-db/no-op.js b/lib/milestone-db/no-op.js index 82d66ba10..fc235d248 100644 --- a/lib/milestone-db/no-op.js +++ b/lib/milestone-db/no-op.js @@ -13,22 +13,22 @@ function NoOpMilestoneDB(options) { NoOpMilestoneDB.prototype = Object.create(MilestoneDB.prototype); -NoOpMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) { +NoOpMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) { var snapshot = undefined; process.nextTick(callback, null, snapshot); }; -NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) { +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) { +NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) { var snapshot = undefined; process.nextTick(callback, null, snapshot); }; -NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) { +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 f44b77835..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'}; } @@ -155,14 +153,14 @@ exports.transform = function(type, op, appliedOp) { * * @param snapshot - a Snapshot object which will be mutated by the provided ops * @param ops - an array of ops to apply to the snapshot - * @returns an error object if applicable + * @return an error object if applicable */ -exports.applyOps = function (snapshot, ops) { +exports.applyOps = function(snapshot, ops) { var type = null; if (snapshot.type) { type = types[snapshot.type]; - if (!type) return { code: 4008, message: 'Unknown type' }; + if (!type) return {code: 4008, message: 'Unknown type'}; } for (var index = 0; index < ops.length; index++) { @@ -172,7 +170,7 @@ exports.applyOps = function (snapshot, ops) { if (op.create) { type = types[op.create.type]; - if (!type) return { code: 4008, message: 'Unknown type' }; + if (!type) return {code: 4008, message: 'Unknown type'}; snapshot.data = type.create(op.create.data); snapshot.type = type.uri; } else if (op.del) { diff --git a/lib/projections.js b/lib/projections.js index 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 f79c9a684..77b91f8b8 100644 --- a/lib/query-emitter.js +++ b/lib/query-emitter.js @@ -187,13 +187,19 @@ QueryEmitter.prototype.queryPoll = function(callback) { if (err) return emitter._finishPoll(err, callback, pending); var snapshots = emitter.backend._getSnapshotsFromMap(inserted, snapshotMap); 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); - }); + 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); @@ -236,12 +242,18 @@ QueryEmitter.prototype.queryPollDoc = function(id, callback) { if (err) return callback(err); var snapshots = [snapshot]; 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(); - }); + 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/submit-request.js b/lib/submit-request.js index ddd9f6f13..068be3123 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -54,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 @@ -147,28 +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); - } - if (request._shouldSaveMilestoneSnapshot(request.snapshot)) { - request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot); - } - 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(); + }); }); }; @@ -224,7 +229,7 @@ SubmitRequest.prototype._addSnapshotMeta = function() { meta.mtime = this.start; }; -SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function (snapshot) { +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; @@ -252,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 6ca346ffe..7ad8a23fb 100644 --- a/lib/util.js +++ b/lib/util.js @@ -8,17 +8,17 @@ exports.hasKeys = function(object) { }; // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill -exports.isInteger = Number.isInteger || function (value) { +exports.isInteger = Number.isInteger || function(value) { return typeof value === 'number' && isFinite(value) && Math.floor(value) === value; }; -exports.isValidVersion = function (version) { +exports.isValidVersion = function(version) { if (version === null) return true; return exports.isInteger(version) && version >= 0; }; -exports.isValidTimestamp = function (timestamp) { +exports.isValidTimestamp = function(timestamp) { return exports.isValidVersion(timestamp); }; diff --git a/package.json b/package.json index af6e8eb6f..9e4e5b09c 100644 --- a/package.json +++ b/package.json @@ -13,17 +13,17 @@ }, "devDependencies": { "coveralls": "^2.11.8", + "eslint": "^5.16.0", "expect.js": "^0.3.1", "istanbul": "^0.4.2", - "jshint": "^2.9.2", "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 'lib/**/*.js' 'test/**/*.js'" }, "repository": { "type": "git", diff --git a/test/backend.js b/test/backend.js index d04746e2b..991c6716b 100644 --- a/test/backend.js +++ b/test/backend.js @@ -1,42 +1,42 @@ var Backend = require('../lib/backend'); var expect = require('expect.js'); -describe('Backend', function () { +describe('Backend', function() { var backend; - beforeEach(function () { + beforeEach(function() { backend = new Backend(); }); - afterEach(function (done) { + afterEach(function(done) { backend.close(done); }); - describe('a simple document', function () { - beforeEach(function (done) { + describe('a simple document', function() { + beforeEach(function(done) { var doc = backend.connect().get('books', '1984'); - doc.create({ title: '1984' }, function (error) { + doc.create({title: '1984'}, function(error) { if (error) return done(error); - doc.submitOp({ p: ['author'], oi: 'George Orwell' }, done); + 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) { + 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' }]); + 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) { + it('fetches the ops with metadata', function(done) { var options = { opsOptions: {metadata: true} }; - backend.getOps(null, 'books', '1984', 0, null, options, function (error, ops) { + 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(); @@ -46,9 +46,9 @@ describe('Backend', function () { }); }); - describe('fetch', function () { - it('fetches the document', function (done) { - backend.fetch(null, 'books', '1984', function (error, doc) { + 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', @@ -58,11 +58,11 @@ describe('Backend', function () { }); }); - it('fetches the document with metadata', function (done) { + it('fetches the document with metadata', function(done) { var options = { snapshotOptions: {metadata: true} }; - backend.fetch(null, 'books', '1984', options, function (error, doc) { + backend.fetch(null, 'books', '1984', options, function(error, doc) { if (error) return done(error); expect(doc.m).to.be.ok(); done(); @@ -70,9 +70,9 @@ describe('Backend', function () { }); }); - describe('subscribe', function () { - it('subscribes to the document', function (done) { - backend.subscribe(null, 'books', '1984', null, function (error, stream, snapshot) { + 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({ @@ -80,21 +80,21 @@ describe('Backend', function () { author: 'George Orwell' }); var op = {op: {p: ['publication'], oi: 1949}}; - stream.on('data', function (data) { + stream.on('data', function(data) { expect(data.op).to.eql(op.op); done(); }); - backend.submit(null, 'books', '1984', op, null, function (error) { + 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) { + it('does not support subscribing to the document with options', function(done) { var options = { - opsOptions: { metadata: true } + opsOptions: {metadata: true} }; - backend.subscribe(null, 'books', '1984', null, options, function (error) { + 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 aa00e2db8..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) { @@ -105,7 +104,7 @@ describe('client connection', function() { var backend = this.backend; var connection = backend.connect(); connection.on('connected', function() { - connection.socket.stream.emit('close') + connection.socket.stream.emit('close'); expect(backend.agentsCount).equal(0); done(); }); @@ -115,8 +114,8 @@ describe('client connection', function() { var backend = this.backend; var connection = backend.connect(); connection.on('connected', function() { - connection.socket.stream.emit('end') - connection.socket.stream.emit('close') + connection.socket.stream.emit('end'); + connection.socket.stream.emit('close'); expect(backend.agentsCount).equal(0); done(); }); @@ -128,39 +127,38 @@ describe('client connection', function() { 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() { @@ -198,7 +196,5 @@ describe('client connection', function() { done(); }); }); - }); - }); diff --git a/test/client/doc.js b/test/client/doc.js index 738c3055a..9b5316e3f 100644 --- a/test/client/doc.js +++ b/test/client/doc.js @@ -1,9 +1,8 @@ var Backend = require('../../lib/backend'); var expect = require('expect.js'); -var util = require('../util') +var util = require('../util'); describe('Doc', function() { - beforeEach(function() { this.backend = new Backend(); this.connection = this.backend.connect(); @@ -51,7 +50,6 @@ describe('Doc', function() { }); describe('applyStack', function() { - beforeEach(function(done) { this.doc = this.connection.get('dogs', 'fido'); this.doc2 = this.backend.connect().get('dogs', 'fido'); @@ -221,24 +219,23 @@ describe('Doc', function() { verifyConsistency(doc, doc2, doc3, handlers, done); }); }); - }); describe('submitting ops in callbacks', function() { - beforeEach(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) { + 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 }]; + 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' }); + expect(doc.data).eql({name: 'Doo'}); done(); }); }); @@ -246,11 +243,11 @@ describe('Doc', function() { it('fails with invalid op', function(done) { var doc = this.doc; - doc.create({ name: 'Scooby Doo' }, function(error) { + 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 }]; + 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(); @@ -259,48 +256,48 @@ describe('Doc', function() { }); }); - describe('submitting an invalid op', function () { + describe('submitting an invalid op', function() { var doc; var invalidOp; var validOp; - beforeEach(function (done) { + 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}; + validOp = {p: ['snacks'], oi: true}; doc = this.connection.get('dogs', 'scooby'); - doc.create({ name: 'Scooby' }, function (error) { + 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) { + 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) { + it('rolls the doc back to a usable state', function(done) { util.callInSeries([ - function (next) { - doc.submitOp(invalidOp, function (error) { + function(next) { + doc.submitOp(invalidOp, function(error) { expect(error).to.be.ok(); next(); }); }, - function (next) { + function(next) { doc.whenNothingPending(next); }, - function (next) { + function(next) { expect(doc.data).to.eql({name: 'Scooby'}); doc.submitOp(validOp, next); }, - function (next) { + function(next) { expect(doc.data).to.eql({name: 'Scooby', snacks: true}); next(); }, @@ -308,7 +305,7 @@ describe('Doc', function() { ]); }); - it('rescues an irreversible op collision', function (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 @@ -321,9 +318,9 @@ describe('Doc', function() { var pauseSubmit = false; var fireSubmit; - this.backend.use('submit', function (request, callback) { + this.backend.use('submit', function(request, callback) { if (pauseSubmit) { - fireSubmit = function () { + fireSubmit = function() { pauseSubmit = false; callback(); }; @@ -334,26 +331,26 @@ describe('Doc', function() { }); util.callInSeries([ - function (next) { + function(next) { doc1.create({colours: ['white']}, next); }, - function (next) { + function(next) { doc1.whenNothingPending(next); }, - function (next) { + function(next) { doc2.fetch(next); }, - function (next) { + function(next) { doc2.whenNothingPending(next); }, // Both documents start off at the same v1 state, with colours as a list - function (next) { + 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) { + 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 @@ -365,21 +362,21 @@ describe('Doc', function() { // 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) { + function(next) { pauseSubmit = true; - doc2.submitOp({p: ['colours', '0'], li: 'black'}, function (error) { + doc2.submitOp({p: ['colours', '0'], li: 'black'}, function(error) { expect(error.message).to.equal('Referenced element not a list'); next(); }); - doc2.fetch(function (error) { + 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) { + 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); 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/projections.js b/test/client/projections.js index fb1c993e5..471cbe909 100644 --- a/test/client/projections.js +++ b/test/client/projections.js @@ -1,9 +1,10 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ 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(); @@ -56,7 +57,12 @@ describe('client projections', function() { 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}}}, + { + p: [], + oi: {age: 2, color: 'brown', owner: false}, + od: {age: 3, color: 'gold', owner: {name: 'jim'}, + litter: {count: 4}} + }, {age: 2, owner: false}, done ); @@ -324,6 +330,5 @@ describe('client projections', function() { }); }); }); - }); }; diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index 108fe0de5..d7c685ce7 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -1,3 +1,5 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); var util = require('../util'); @@ -31,8 +33,12 @@ describe('client query subscribe', function() { 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('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) { @@ -55,8 +61,12 @@ describe('client query subscribe', function() { 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('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) { @@ -80,8 +90,12 @@ describe('client query subscribe', function() { 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('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) { @@ -102,8 +116,12 @@ describe('client query subscribe', function() { 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('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) { @@ -123,8 +141,12 @@ describe('client query subscribe', function() { 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(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) { @@ -147,8 +169,12 @@ describe('client query subscribe', function() { 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(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) { @@ -188,8 +214,12 @@ describe('client query subscribe', function() { 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('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) { @@ -360,8 +390,12 @@ describe('client query subscribe', function() { 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(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}}); @@ -387,8 +421,12 @@ describe('client query subscribe', function() { 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(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}}); @@ -415,8 +453,12 @@ describe('client query subscribe', function() { 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(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]]}); @@ -441,6 +483,5 @@ describe('client query subscribe', function() { }); }); }); - }); }; diff --git a/test/client/query.js b/test/client/query.js index 0b7912cb3..423cdd2a3 100644 --- a/test/client/query.js +++ b/test/client/query.js @@ -1,3 +1,5 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); var util = require('../util'); @@ -25,9 +27,15 @@ describe('client query', function() { 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(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) { @@ -45,9 +53,15 @@ describe('client query', function() { 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(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) { @@ -65,9 +79,15 @@ describe('client query', function() { 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(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) { @@ -88,9 +108,15 @@ describe('client query', function() { 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(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) { @@ -116,8 +142,6 @@ describe('client query', function() { }); }); }); - }); - }); }; diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 9c2aeaad2..0aeae9f9d 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -6,7 +6,7 @@ var MemoryDb = require('../../lib/db/memory'); var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); var sinon = require('sinon'); -describe('SnapshotTimestampRequest', function () { +describe('SnapshotTimestampRequest', function() { var backend; var clock; var day0 = new Date(2017, 11, 31).getTime(); @@ -17,17 +17,17 @@ describe('SnapshotTimestampRequest', function () { var day5 = new Date(2018, 0, 5).getTime(); var ONE_DAY = 1000 * 60 * 60 * 24; - beforeEach(function () { - clock = lolex.install({ now: day1 }); + beforeEach(function() { + clock = lolex.install({now: day1}); backend = new Backend(); }); - afterEach(function (done) { + afterEach(function(done) { clock.uninstall(); backend.close(done); }); - describe('a document with some simple versions separated by a day', function () { + describe('a document with some simple versions separated by a day', function() { var v0 = { id: 'time-machine', v: 0, @@ -68,30 +68,30 @@ describe('SnapshotTimestampRequest', function () { m: null }; - beforeEach(function (done) { + beforeEach(function(done) { var doc = backend.connect().get('books', 'time-machine'); util.callInSeries([ - function (next) { - doc.create({ title: 'The Time Machine' }, next); + function(next) { + doc.create({title: 'The Time Machine'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], oi: 'HG Wells' }, next); + doc.submitOp({p: ['author'], oi: 'HG Wells'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], od: 'HG Wells', oi: 'H.G. Wells' }, next); + doc.submitOp({p: ['author'], od: 'HG Wells', oi: 'H.G. Wells'}, next); }, done ]); }); - it('fetches the version at exactly day 1', function (done) { + it('fetches the version at exactly day 1', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v1); next(); }, @@ -99,12 +99,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the version at exactly day 2', function (done) { + it('fetches the version at exactly day 2', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day2, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v2); next(); }, @@ -112,12 +112,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the version at exactly day 3', function (done) { + it('fetches the version at exactly day 3', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v3); next(); }, @@ -125,13 +125,13 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the day 2 version when asking for a time halfway between days 2 and 3', function (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) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', halfwayBetweenDays2and3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v2); next(); }, @@ -139,12 +139,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the day 3 version when asking for a time after day 3', function (done) { + it('fetches the day 3 version when asking for a time after day 3', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v3); next(); }, @@ -152,12 +152,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches the most recent version when not specifying a timestamp', function (done) { + it('fetches the most recent version when not specifying a timestamp', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v3); next(); }, @@ -165,12 +165,12 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('fetches an empty snapshot if the timestamp is before the document creation', function (done) { + it('fetches an empty snapshot if the timestamp is before the document creation', function(done) { util.callInSeries([ - function (next) { + function(next) { backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day0, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(v0); next(); }, @@ -178,40 +178,40 @@ describe('SnapshotTimestampRequest', function () { ]); }); - it('throws if the timestamp is undefined', function () { - var fetch = function () { - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function () {}); + 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 () { + 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 () { }); + 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 () { }); - } + 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) { + 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', @@ -224,10 +224,10 @@ describe('SnapshotTimestampRequest', function () { }); }); - it('starts pending, and finishes not pending', function (done) { + it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function (error, snapshot) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error, snapshot) { expect(connection.hasPending()).to.be(false); done(); }); @@ -235,10 +235,10 @@ describe('SnapshotTimestampRequest', function () { expect(connection.hasPending()).to.be(true); }); - it('deletes the request from the connection', function (done) { + it('deletes the request from the connection', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) { if (error) return done(error); expect(connection._snapshotRequests).to.eql({}); done(); @@ -247,10 +247,10 @@ describe('SnapshotTimestampRequest', function () { expect(connection._snapshotRequests).to.not.eql({}); }); - it('emits a ready event when done', function (done) { + it('emits a ready event when done', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) { if (error) return done(error); }); @@ -258,22 +258,22 @@ describe('SnapshotTimestampRequest', function () { snapshotRequest.on('ready', done); }); - it('fires the connection.whenNothingPending', function (done) { + it('fires the connection.whenNothingPending', function(done) { var connection = backend.connect(); var snapshotFetched = false; - connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) { if (error) return done(error); snapshotFetched = true; }); - connection.whenNothingPending(function () { + 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) { + 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: @@ -286,7 +286,7 @@ describe('SnapshotTimestampRequest', function () { // - 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) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!connectionInterrupted) { connection.close(); backend.connect(connection); @@ -299,7 +299,7 @@ describe('SnapshotTimestampRequest', function () { connection.fetchSnapshotByTimestamp('books', 'time-machine', done); }); - it('cannot send the same request twice over a connection', function (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: @@ -310,7 +310,7 @@ describe('SnapshotTimestampRequest', function () { // - 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) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!hasResent) { connection._snapshotRequests[1]._onConnectionStateChanged(); hasResent = true; @@ -322,55 +322,55 @@ describe('SnapshotTimestampRequest', function () { connection.fetchSnapshotByTimestamp('books', 'time-machine', done); }); - describe('readSnapshots middleware', function () { - it('triggers the middleware', function (done) { + describe('readSnapshots middleware', function() { + it('triggers the middleware', function(done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, - function (request) { + 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 () { }); + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, function() { }); }); - it('can have its snapshot manipulated in the middleware', function (done) { + it('can have its snapshot manipulated in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { + function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); }, ]; - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function (error, snapshot) { + 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) { + it('respects errors thrown in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { - callback({ message: 'foo' }); + function(request, callback) { + callback({message: 'foo'}); }, ]; - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function (error, snapshot) { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error, snapshot) { expect(error.message).to.be('foo'); done(); }); }); }); - describe('with a registered projection', function () { - beforeEach(function () { - backend.addProjection('bookTitles', 'books', { title: true }); + 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) { + 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'); @@ -381,13 +381,13 @@ describe('SnapshotTimestampRequest', function () { }); }); - describe('milestone snapshots enabled for every other version', function () { + describe('milestone snapshots enabled for every other version', function() { var milestoneDb; var db; var backendWithMilestones; - beforeEach(function () { - var options = { interval: 2 }; + beforeEach(function() { + var options = {interval: 2}; db = new MemoryDb(); milestoneDb = new MemoryMilestoneDb(options); backendWithMilestones = new Backend({ @@ -396,41 +396,41 @@ describe('SnapshotTimestampRequest', function () { }); }); - afterEach(function (done) { + afterEach(function(done) { backendWithMilestones.close(done); }); - describe('a doc with some versions in the milestone database', function () { - beforeEach(function (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) { + doc.create({title: 'To Kill a Mocking Bird'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, next); + doc.submitOp({p: ['author'], oi: 'Harper Lea'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['author'], od: 'Harper Lea', oi: 'Harper Lee' }, next); + doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['year'], oi: 1959 }, next); + doc.submitOp({p: ['year'], oi: 1959}, next); }, - function (next) { + function(next) { clock.tick(ONE_DAY); - doc.submitOp({ p: ['year'], od: 1959, oi: 1960 }, next); + doc.submitOp({p: ['year'], od: 1959, oi: 1960}, next); }, done ]); }); - it('fetches a snapshot between two milestones using the milestones', function (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'); @@ -445,35 +445,35 @@ describe('SnapshotTimestampRequest', function () { 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' }); + 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) { + 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) { + .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' }); + expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lea'}); done(); }); }); - it('fetches a snapshot before any milestones', function (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) { + .fetchSnapshotByTimestamp('books', 'mocking-bird', day1, function(error, snapshot) { if (error) return done(error); expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); @@ -481,18 +481,18 @@ describe('SnapshotTimestampRequest', function () { 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' }); + expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird'}); done(); }); }); - it('fetches a snapshot after any milestones', function (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) { + .fetchSnapshotByTimestamp('books', 'mocking-bird', day5, function(error, snapshot) { if (error) return done(error); expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true); diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index 4b7101e56..2f959be07 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -5,18 +5,18 @@ var MemoryMilestoneDb = require('../../lib/milestone-db/memory'); var sinon = require('sinon'); var util = require('../util'); -describe('SnapshotVersionRequest', function () { +describe('SnapshotVersionRequest', function() { var backend; - beforeEach(function () { + beforeEach(function() { backend = new Backend(); }); - afterEach(function (done) { + afterEach(function(done) { backend.close(done); }); - describe('a document with some simple versions', function () { + describe('a document with some simple versions', function() { var v0 = { id: 'don-quixote', v: 0, @@ -57,99 +57,99 @@ describe('SnapshotVersionRequest', function () { m: null }; - beforeEach(function (done) { + beforeEach(function(done) { var doc = backend.connect().get('books', 'don-quixote'); - doc.create({ title: 'Don Quixote' }, function (error) { + doc.create({title: 'Don Quixote'}, function(error) { if (error) return done(error); - doc.submitOp({ p: ['author'], oi: 'Miguel de Cervante' }, function (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); + 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) { + 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) { + 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) { + 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) { + 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 () {}); + 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) { + 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 () { + 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 () {}); + 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 () { }); - } + 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) { + 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) { + 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', @@ -162,10 +162,10 @@ describe('SnapshotVersionRequest', function () { }); }); - it('starts pending, and finishes not pending', function (done) { + it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', null, function (error, snapshot) { + connection.fetchSnapshot('books', 'don-quixote', null, function(error, snapshot) { expect(connection.hasPending()).to.be(false); done(); }); @@ -173,10 +173,10 @@ describe('SnapshotVersionRequest', function () { expect(connection.hasPending()).to.be(true); }); - it('deletes the request from the connection', function (done) { + it('deletes the request from the connection', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', function (error) { + connection.fetchSnapshot('books', 'don-quixote', function(error) { if (error) return done(error); expect(connection._snapshotRequests).to.eql({}); done(); @@ -185,10 +185,10 @@ describe('SnapshotVersionRequest', function () { expect(connection._snapshotRequests).to.not.eql({}); }); - it('emits a ready event when done', function (done) { + it('emits a ready event when done', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', function (error) { + connection.fetchSnapshot('books', 'don-quixote', function(error) { if (error) return done(error); }); @@ -196,22 +196,22 @@ describe('SnapshotVersionRequest', function () { snapshotRequest.on('ready', done); }); - it('fires the connection.whenNothingPending', function (done) { + it('fires the connection.whenNothingPending', function(done) { var connection = backend.connect(); var snapshotFetched = false; - connection.fetchSnapshot('books', 'don-quixote', function (error) { + connection.fetchSnapshot('books', 'don-quixote', function(error) { if (error) return done(error); snapshotFetched = true; }); - connection.whenNothingPending(function () { + 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) { + 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: @@ -224,7 +224,7 @@ describe('SnapshotVersionRequest', function () { // - 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) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!connectionInterrupted) { connection.close(); backend.connect(connection); @@ -237,7 +237,7 @@ describe('SnapshotVersionRequest', function () { connection.fetchSnapshot('books', 'don-quixote', done); }); - it('cannot send the same request twice over a connection', function (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: @@ -248,7 +248,7 @@ describe('SnapshotVersionRequest', function () { // - 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) { + backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) { if (!hasResent) { connection._snapshotRequests[1]._onConnectionStateChanged(); hasResent = true; @@ -260,55 +260,55 @@ describe('SnapshotVersionRequest', function () { connection.fetchSnapshot('books', 'don-quixote', done); }); - describe('readSnapshots middleware', function () { - it('triggers the middleware', function (done) { + describe('readSnapshots middleware', function() { + it('triggers the middleware', function(done) { backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, - function (request) { + 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 () { }); + backend.connect().fetchSnapshot('books', 'don-quixote', 3, function() { }); }); - it('can have its snapshot manipulated in the middleware', function (done) { + it('can have its snapshot manipulated in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { + function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); }, ]; - backend.connect().fetchSnapshot('books', 'don-quixote', function (error, snapshot) { + 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) { + it('respects errors thrown in the middleware', function(done) { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ - function (request, callback) { - callback({ message: 'foo' }); + function(request, callback) { + callback({message: 'foo'}); }, ]; - backend.connect().fetchSnapshot('books', 'don-quixote', 0, function (error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) { expect(error.message).to.be('foo'); done(); }); }); }); - describe('with a registered projection', function () { - beforeEach(function () { - backend.addProjection('bookTitles', 'books', { title: true }); + 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) { + 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'); @@ -319,19 +319,19 @@ describe('SnapshotVersionRequest', function () { }); }); - describe('a document that is currently deleted', function () { - beforeEach(function (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) { + doc.create({title: 'Catch 22'}, function(error) { if (error) return done(error); - doc.del(function (error) { + doc.del(function(error) { done(error); }); }); }); - it('returns a null type', function (done) { - backend.connect().fetchSnapshot('books', 'catch-22', null, function (error, snapshot) { + 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, @@ -344,8 +344,8 @@ describe('SnapshotVersionRequest', function () { }); }); - it('fetches v1', function (done) { - backend.connect().fetchSnapshot('books', 'catch-22', 1, function (error, snapshot) { + it('fetches v1', function(done) { + backend.connect().fetchSnapshot('books', 'catch-22', 1, function(error, snapshot) { if (error) return done(error); expect(snapshot).to.eql({ @@ -363,22 +363,22 @@ describe('SnapshotVersionRequest', function () { }); }); - describe('a document that was deleted and then created again', function () { - beforeEach(function (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) { + doc.create({title: 'Hitchhiker\'s Guide to the Galaxy'}, function(error) { if (error) return done(error); - doc.del(function (error) { + doc.del(function(error) { if (error) return done(error); - doc.create({ title: 'The Restaurant at the End of the Universe' }, function (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) { + 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({ @@ -396,13 +396,13 @@ describe('SnapshotVersionRequest', function () { }); }); - describe('milestone snapshots enabled for every other version', function () { + describe('milestone snapshots enabled for every other version', function() { var milestoneDb; var db; var backendWithMilestones; - beforeEach(function () { - var options = { interval: 2 }; + beforeEach(function() { + var options = {interval: 2}; db = new MemoryDb(); milestoneDb = new MemoryMilestoneDb(options); backendWithMilestones = new Backend({ @@ -411,33 +411,33 @@ describe('SnapshotVersionRequest', function () { }); }); - afterEach(function (done) { + afterEach(function(done) { backendWithMilestones.close(done); }); - it('fetches a snapshot using the milestone', function (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.create({title: 'To Kill a Mocking Bird'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, 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) { + doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next); }, - function (next) { + function(next) { sinon.spy(milestoneDb, 'getMilestoneSnapshot'); sinon.spy(db, 'getOps'); backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next); }, - function (snapshot, 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' }); + 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 82cecbbe3..546f4ec9e 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1,3 +1,5 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var async = require('async'); var expect = require('expect.js'); var types = require('../../lib/types'); @@ -9,7 +11,6 @@ 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); @@ -527,7 +528,7 @@ describe('client submit', function() { // 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) { + backend.use('commit', function(request, callback) { if (request.op.op[0].na === 2) docCallback = callback; if (request.op.op[0].na === 7) doc2Callback = callback; @@ -538,14 +539,14 @@ describe('client submit', function() { docCallback(); } }); - doc.submitOp({p: ['age'], na: 2}, function (error) { + 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) { + doc2.submitOp({p: ['age'], na: 7}, function(error) { expect(error).ok(); done(); }); @@ -563,8 +564,12 @@ describe('client submit', function() { doc2.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); async.parallel([ - function(cb) { doc.del(cb); }, - function(cb) { doc.create({age: 5}, cb); } + function(cb) { + doc.del(cb); + }, + function(cb) { + doc.create({age: 5}, cb); + } ], function(err) { if (err) return done(err); expect(doc.version).equal(4); @@ -586,8 +591,12 @@ describe('client submit', function() { doc2.del(function(err) { if (err) return done(err); async.parallel([ - function(cb) { doc.del(cb); }, - function(cb) { doc.create({age: 5}, cb); } + function(cb) { + doc.del(cb); + }, + function(cb) { + doc.create({age: 5}, cb); + } ], function(err) { if (err) return done(err); expect(doc.version).equal(4); @@ -722,8 +731,12 @@ describe('client submit', function() { expect(doc2.version).equal(3); async.parallel([ - function(cb) { doc.fetch(cb); }, - function(cb) { doc2.fetch(cb); } + 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'}); @@ -1067,7 +1080,7 @@ describe('client submit', function() { 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) { + doc.create(5, numberType.type.uri, function(err) { if (err) return done(err); expect(doc.data).to.equal(5); doc.submitOp(2, function(err) { @@ -1105,7 +1118,6 @@ describe('client submit', function() { it('deserializes on fetch', 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) { @@ -1132,7 +1144,6 @@ describe('client submit', function() { 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) { @@ -1171,6 +1182,5 @@ describe('client submit', function() { }); }); }); - }); }; diff --git a/test/client/subscribe.js b/test/client/subscribe.js index b24a94749..ed45b1b90 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -1,9 +1,10 @@ +// FIXME: fix this indentation +/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); module.exports = function() { describe('client subscribe', function() { - it('can call bulk without doing any actions', function() { var connection = this.backend.connect(); connection.startBulk(); @@ -31,8 +32,12 @@ describe('client subscribe', function() { doc.create({age: 3}, function(err) { if (err) return done(err); async.parallel([ - function(cb) { doc2[method](cb); }, - function(cb) { doc2[method](cb); } + function(cb) { + doc2[method](cb); + }, + function(cb) { + doc2[method](cb); + } ], function(err) { if (err) return done(err); expect(doc2.version).eql(1); @@ -49,8 +54,12 @@ describe('client subscribe', function() { if (err) return done(err); doc2.connection.startBulk(); async.parallel([ - function(cb) { doc2[method](cb); }, - function(cb) { doc2[method](cb); } + function(cb) { + doc2[method](cb); + }, + function(cb) { + doc2[method](cb); + } ], function(err) { if (err) return done(err); expect(doc2.version).eql(1); @@ -65,9 +74,15 @@ describe('client subscribe', function() { 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(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 fido = connection2.get('dogs', 'fido'); @@ -75,9 +90,15 @@ describe('client subscribe', function() { 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}); @@ -97,9 +118,15 @@ describe('client subscribe', function() { 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.version).equal(0); @@ -110,16 +137,28 @@ describe('client subscribe', function() { 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(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) { + 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}); @@ -129,24 +168,42 @@ describe('client subscribe', function() { // 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); // Create new ops and test if they are received 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) { + 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); 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: 4}); @@ -302,7 +359,6 @@ describe('client subscribe', function() { }); 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); @@ -312,7 +368,6 @@ describe('client subscribe', function() { }); 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); @@ -445,14 +500,22 @@ describe('client subscribe', function() { doc.create({age: 3}, function(err) { if (err) return done(err); async.parallel([ - function(cb) { fido.subscribe(cb); }, - function(cb) { spot.subscribe(cb); } + function(cb) { + fido.subscribe(cb); + }, + function(cb) { + spot.subscribe(cb); + } ], function(err) { if (err) return done(err); fido.connection.startBulk(); async.parallel([ - function(cb) { fido.unsubscribe(cb); }, - function(cb) { spot.unsubscribe(cb); } + function(cb) { + fido.unsubscribe(cb); + }, + function(cb) { + spot.unsubscribe(cb); + } ], function(err) { if (err) return done(err); fido.on('op', function(op, context) { diff --git a/test/db.js b/test/db.js index 257763173..270a2ea92 100644 --- a/test/db.js +++ b/test/db.js @@ -790,9 +790,9 @@ module.exports = function(options) { // sorts by foo first, then bar var snapshots = [ {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} + {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 index aba680eb7..4efbb0cfa 100644 --- a/test/logger.js +++ b/test/logger.js @@ -2,23 +2,23 @@ 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 () { +describe('Logger', function() { + describe('Stubbing console.warn', function() { + beforeEach(function() { sinon.stub(console, 'warn'); }); - afterEach(function () { + afterEach(function() { sinon.restore(); }); - it('logs to console by default', function () { + 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 () { + it('overrides console', function() { var customWarn = sinon.stub(); var logger = new Logger(); logger.setMethods({ @@ -31,7 +31,7 @@ describe('Logger', function () { expect(customWarn.calledOnceWithExactly('warning')).to.be(true); }); - it('only overrides if provided with a method', function () { + it('only overrides if provided with a method', function() { var badWarn = 'not a function'; var logger = new Logger(); logger.setMethods({ diff --git a/test/middleware.js b/test/middleware.js index 4af42fbc6..f71ddce5d 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) {}); 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) { @@ -305,5 +298,4 @@ describe('middleware', function() { }); }); }); - }); diff --git a/test/milestone-db.js b/test/milestone-db.js index 1354eb4ee..223423d47 100644 --- a/test/milestone-db.js +++ b/test/milestone-db.js @@ -5,22 +5,22 @@ var NoOpMilestoneDB = require('../lib/milestone-db/no-op'); var Snapshot = require('../lib/snapshot'); var util = require('./util'); -describe('Base class', function () { +describe('Base class', function() { var db; - beforeEach(function () { + 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) { + 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) { + it('emits an error when trying to get a snapshot', function(done) { + db.on('error', function(error) { expect(error.code).to.be(5019); done(); }); @@ -28,15 +28,15 @@ describe('Base class', function () { db.getMilestoneSnapshot('books', '123', 1); }); - it('calls back with an error when trying to save a snapshot', function (done) { - db.saveMilestoneSnapshot('books', {}, function (error) { + 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) { + it('emits an error when trying to save a snapshot', function(done) { + db.on('error', function(error) { expect(error.code).to.be(5020); done(); }); @@ -44,45 +44,45 @@ describe('Base class', function () { 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) { + 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) { + 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 () { +describe('NoOpMilestoneDB', function() { var db; - beforeEach(function () { + beforeEach(function() { db = new NoOpMilestoneDB(); }); - it('does not error when trying to save and fetch a snapshot', function (done) { + 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' }, + {title: 'Catcher in the Rye'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -90,8 +90,8 @@ describe('NoOpMilestoneDB', function () { ]); }); - it('emits an event when saving without a callback', function (done) { - db.on('save', function () { + it('emits an event when saving without a callback', function(done) { + db.on('save', function() { done(); }); @@ -99,51 +99,51 @@ describe('NoOpMilestoneDB', function () { }); }); -module.exports = function (options) { +module.exports = function(options) { var create = options.create; - describe('Milestone Database', function () { + describe('Milestone Database', function() { var db; var backend; - beforeEach(function (done) { - create(function (error, createdDb) { + beforeEach(function(done) { + create(function(error, createdDb) { if (error) return done(error); db = createdDb; - backend = new Backend({ milestoneDb: db }); + backend = new Backend({milestoneDb: db}); done(); }); }); - afterEach(function (done) { + afterEach(function(done) { db.close(done); }); - it('can call close() without a callback', function (done) { - create(function (error, db) { + 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) { + 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' }, + {title: 'Catcher in the Rye'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, - function (retrievedSnapshot, next) { + function(retrievedSnapshot, next) { expect(retrievedSnapshot).to.eql(snapshot); next(); }, @@ -151,12 +151,12 @@ module.exports = function (options) { ]); }); - it('fetches the most recent snapshot before the requested version', function (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' }, + {title: 'Catcher in the Rye'}, null ); @@ -164,7 +164,7 @@ module.exports = function (options) { 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger'}, null ); @@ -172,24 +172,24 @@ module.exports = function (options) { 'catcher-in-the-rye', 10, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger', publicationDate: '1951-07-16' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger', publicationDate: '1951-07-16'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot10, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -197,12 +197,12 @@ module.exports = function (options) { ]); }); - it('fetches the most recent snapshot even if they are inserted in the wrong order', function (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' }, + {title: 'Catcher in the Rye'}, null ); @@ -210,21 +210,21 @@ module.exports = function (options) { 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -232,12 +232,12 @@ module.exports = function (options) { ]); }); - it('fetches the most recent snapshot when the version is null', function (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' }, + {title: 'Catcher in the Rye'}, null ); @@ -245,21 +245,21 @@ module.exports = function (options) { 'catcher-in-the-rye', 2, 'http://sharejs.org/types/JSONv0', - { title: 'Catcher in the Rye', author: 'J.D. Salinger' }, + {title: 'Catcher in the Rye', author: 'J.D. Salinger'}, null ); util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -267,62 +267,62 @@ module.exports = function (options) { ]); }); - it('errors when fetching an undefined version', function (done) { - db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function (error) { + 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) { + 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) { + 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) { + 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) { + 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) { + 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' }, + {title: 'Catcher in the Rye'}, null ); - db.saveMilestoneSnapshot(null, snapshot, function (error) { + db.saveMilestoneSnapshot(null, snapshot, function(error) { expect(error).to.be.ok(); done(); }); }); - it('returns undefined if no snapshot exists', function (done) { + it('returns undefined if no snapshot exists', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -330,16 +330,16 @@ module.exports = function (options) { ]); }); - it('does not store a milestone snapshot on commit', function (done) { + it('does not store a milestone snapshot on commit', function(done) { util.callInSeries([ - function (next) { + function(next) { var doc = backend.connect().get('books', 'catcher-in-the-rye'); - doc.create({ title: 'Catcher in the Rye' }, next); + doc.create({title: 'Catcher in the Rye'}, next); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -347,16 +347,16 @@ module.exports = function (options) { ]); }); - it('can save without a callback', function (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' }, + {title: 'Catcher in the Rye'}, null ); - db.on('save', function (collection, snapshot) { + db.on('save', function(collection, snapshot) { expect(collection).to.be('books'); expect(snapshot).to.eql(snapshot); done(); @@ -365,14 +365,14 @@ module.exports = function (options) { db.saveMilestoneSnapshot('books', snapshot); }); - it('errors when the snapshot is undefined', function (done) { - db.saveMilestoneSnapshot('books', undefined, function (error) { + 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 () { + describe('snapshots with timestamps', function() { var snapshot1 = new Snapshot( 'catcher-in-the-rye', 1, @@ -414,28 +414,28 @@ module.exports = function (options) { } ); - beforeEach(function (done) { + beforeEach(function(done) { util.callInSeries([ - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot1, next); }, - function (next) { + function(next) { db.saveMilestoneSnapshot('books', snapshot2, next); }, - function (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) { + describe('fetching a snapshot before or at a time', function() { + it('fetches a snapshot before a given time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2500, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -443,12 +443,12 @@ module.exports = function (options) { ]); }); - it('fetches a snapshot at an exact time', function (done) { + it('fetches a snapshot at an exact time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2000, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -456,12 +456,12 @@ module.exports = function (options) { ]); }); - it('fetches the first snapshot for a null timestamp', function (done) { + it('fetches the first snapshot for a null timestamp', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot1); next(); }, @@ -469,26 +469,26 @@ module.exports = function (options) { ]); }); - it('returns an error for a string timestamp', function (done) { - db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function (error) { + 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) { + 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) { + it('returns undefined if there are no snapshots before a time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 0, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -496,28 +496,28 @@ module.exports = function (options) { ]); }); - it('errors if no collection is provided', function (done) { - db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function (error) { + 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) { + 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) { + describe('fetching a snapshot after or at a time', function() { + it('fetches a snapshot after a given time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2500, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot3); next(); }, @@ -525,12 +525,12 @@ module.exports = function (options) { ]); }); - it('fetches a snapshot at an exact time', function (done) { + it('fetches a snapshot at an exact time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2000, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot2); next(); }, @@ -538,12 +538,12 @@ module.exports = function (options) { ]); }); - it('fetches the last snapshot for a null timestamp', function (done) { + it('fetches the last snapshot for a null timestamp', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', null, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.eql(snapshot3); next(); }, @@ -551,26 +551,26 @@ module.exports = function (options) { ]); }); - it('returns an error for a string timestamp', function (done) { - db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function (error) { + 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) { + 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) { + it('returns undefined if there are no snapshots after a time', function(done) { util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 4000, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, @@ -578,15 +578,15 @@ module.exports = function (options) { ]); }); - it('errors if no collection is provided', function (done) { - db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function (error) { + 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) { + it('errors if no ID is provided', function(done) { + db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function(error) { expect(error).to.be.ok(); done(); }); @@ -594,72 +594,72 @@ module.exports = function (options) { }); }); - describe('milestones enabled for every version', function () { - beforeEach(function (done) { - var options = { interval: 1 }; + describe('milestones enabled for every version', function() { + beforeEach(function(done) { + var options = {interval: 1}; - create(options, function (error, createdDb) { + create(options, function(error, createdDb) { if (error) return done(error); db = createdDb; - backend = new Backend({ milestoneDb: db }); + backend = new Backend({milestoneDb: db}); done(); }); }); - it('stores a milestone snapshot on commit', function (done) { - db.on('save', function (collection, snapshot) { + 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' }); + 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' }); + doc.create({title: 'Catcher in the Rye'}); }); }); - describe('milestones enabled for every other version', function () { - beforeEach(function (done) { - var options = { interval: 2 }; + describe('milestones enabled for every other version', function() { + beforeEach(function(done) { + var options = {interval: 2}; - create(options, function (error, createdDb) { + create(options, function(error, createdDb) { if (error) return done(error); db = createdDb; - backend = new Backend({ milestoneDb: db }); + backend = new Backend({milestoneDb: db}); done(); }); }); - it('only stores even-numbered versions', function (done) { - db.on('save', function (collection, snapshot) { + it('only stores even-numbered versions', function(done) { + db.on('save', function(collection, snapshot) { if (snapshot.v !== 4) return; util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(2); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(2); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(4); next(); }, @@ -670,56 +670,56 @@ module.exports = function (options) { 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.create({title: 'Catcher in the Rye'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], oi: 'J.F.Salinger' }, 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.F.Salinger', oi: 'J.D.Salinger'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], od: 'J.D.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) { + 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) { + db.on('save', function(collection, snapshot) { if (snapshot.v !== 4) return; util.callInSeries([ - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot).to.be(undefined); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(3); next(); }, - function (next) { + function(next) { db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next); }, - function (snapshot, next) { + function(snapshot, next) { expect(snapshot.v).to.be(4); next(); }, @@ -730,17 +730,17 @@ module.exports = function (options) { 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.create({title: 'Catcher in the Rye'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], oi: 'J.F.Salinger' }, 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.F.Salinger', oi: 'J.D.Salinger'}, next); }, - function (next) { - doc.submitOp({ p: ['author'], od: 'J.D.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..4bf6cbf1a 100644 --- a/test/pubsub.js +++ b/test/pubsub.js @@ -49,7 +49,6 @@ module.exports = function(create) { pubsub.subscribe('y', function(err, stream) { 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 5f982ed6e..7eb8aef77 100644 --- a/test/util.js +++ b/test/util.js @@ -52,7 +52,7 @@ exports.callInSeries = function(callbacks, args) { var callback = callbacks.shift(); if (callbacks.length) { - args.push(function () { + args.push(function() { var args = Array.from(arguments); exports.callInSeries(callbacks, args); }); From 8a056199ca8be1345eec9acf686f46e5888067e9 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Thu, 4 Jul 2019 15:11:09 +0100 Subject: [PATCH 176/181] Fix indentation linting Large indentation fixes were skipped in the previous commit in order to keep the diff a bit less noisy. This change purely removes the eslint ignore comments, and fixes the indentation in those files. --- test/client/projections.js | 532 +++++----- test/client/query-subscribe.js | 786 +++++++-------- test/client/query.js | 210 ++-- test/client/submit.js | 1738 ++++++++++++++++---------------- test/client/subscribe.js | 942 +++++++++-------- 5 files changed, 2099 insertions(+), 2109 deletions(-) diff --git a/test/client/projections.js b/test/client/projections.js index 471cbe909..77359563e 100644 --- a/test/client/projections.js +++ b/test/client/projections.js @@ -1,334 +1,332 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ 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 d7c685ce7..ddce30d64 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -1,487 +1,485 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var expect = require('expect.js'); 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, index) { + 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, index) { + 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(); + 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: {}, 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 423cdd2a3..785c55211 100644 --- a/test/client/query.js +++ b/test/client/query.js @@ -1,96 +1,66 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var expect = require('expect.js'); 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); @@ -101,47 +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/submit.js b/test/client/submit.js index 546f4ec9e..63a5a46ff 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1,5 +1,3 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var async = require('async'); var expect = require('expect.js'); var types = require('../../lib/types'); @@ -10,23 +8,34 @@ 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}); @@ -34,638 +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(); - } - }); - 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(); - } + expect(doc.version).equal(2); + expect(doc.data).eql({age: 5}); + 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) { - 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) { + 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); - 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); + 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('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) { + 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); - var count = 0; - doc.del(function(err) { - count++; + doc2.fetch(function(err) { 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(); - } + 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(); + } + }); }); - doc2.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(doc2.version).eql(2); - expect(doc2.data).eql(undefined); - } else { - expect(doc2.version).eql(3); - expect(doc2.data).eql(undefined); - done(); - } - }); + 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(); + } }); }); - }); - 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) { - 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.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 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(); + doc2.fetch(function(err) { + if (err) return done(err); + var count = 0; + var cb = function(err) { + count++; + if (err) return done(err); + 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); + 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(); } - ], function(err) { - if (err) return done(err); - expect(doc.version).equal(4); - expect(doc.data).eql({age: 5}); + }); + 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.code).to.equal(4017); - 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.code).to.equal(4018); - 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(); @@ -673,92 +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(); + }); }); }); }); @@ -766,421 +765,420 @@ 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) { - if (err) return done(err); - expect(doc.version).equal(2); - expect(doc.data).eql({age: 8}); - expect(submitCount).equal(3); - done(); - }); + ], done); }); - }); - 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); - doc.submitOp({p: ['age'], na: 1}, function(err) { + 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(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('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}); + 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(1); - expect(doc.data).eql({age: 3}); + expect(doc.version).equal(0); + expect(doc.data).equal(undefined); 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) { + 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); - 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}); + 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: 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('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}); + doc.on('error', function(err) { + expect(err.message).equal('Custom error'); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); + done(); + }); }); }); - }); - 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.data).to.equal(5); - doc.submitOp(2, function(err) { + 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); - expect(doc.data).to.equal(7); - done(); + 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(); + }); }); }); - }); - describe('type.deserialize', function() { - it('can create a new doc', function(done) { + 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([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}); - done(); + doc.submitOp({p: ['age'], na: 1}, function(err) { + if (err) return done(err); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 3}); + done(); + }); + expect(doc.version).equal(1); + expect(doc.data).eql({age: 4}); }); }); - it('is stored serialized in backend', function(done) { - var db = this.backend.db; + 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([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) { - if (err) return done(err); - expect(snapshot.data).eql([3]); + 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('deserializes on fetch', function(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([3], deserializedType.type.uri, function(err) { + doc.create({age: 3}, function(err) { if (err) return done(err); - doc2.fetch(function(err) { + doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); - expect(doc2.data).a(deserializedType.Node); - expect(doc2.data).eql({value: 3, next: null}); - done(); + 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('can create then submit an op', function(done) { + it('submitting an invalid op message returns error', 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); - doc.submitOp({insert: 0, value: 2}, function(err) { - if (err) return done(err); - expect(doc.data).eql({value: 2, next: {value: 3, next: null}}); + doc._submit({}, null, function(err) { + expect(err).ok(); done(); }); }); }); - it('server fetches and transforms by already committed op', function(done) { + it('allows snapshot and op to be a non-object', 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) { + doc.create(5, numberType.type.uri, function(err) { if (err) return done(err); - doc2.fetch(function(err) { + 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) { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.create([3], deserializedType.type.uri, function(err) { + if (err) return done(err); + expect(doc.data).a(deserializedType.Node); + expect(doc.data).eql({value: 3, next: null}); + done(); + }); + }); + + 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 ed45b1b90..083634fb5 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -1,90 +1,116 @@ -// FIXME: fix this indentation -/* eslint-disable indent */ var expect = require('expect.js'); var async = require('async'); module.exports = function() { -describe('client subscribe', function() { - it('can call bulk without doing any actions', function() { - var connection = this.backend.connect(); - connection.startBulk(); - connection.endBulk(); - }); + describe('client subscribe', function() { + 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) { + ['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); - expect(doc2.version).eql(1); - expect(doc2.data).eql({age: 3}); - done(); + doc2[method](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 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 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 + ' 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(); + 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 + ' bulk on same collection', function(done) { + var connection = this.backend.connect(); + var connection2 = this.backend.connect(); async.parallel([ function(cb) { - doc2[method](cb); + connection.get('dogs', 'fido').create({age: 3}, cb); + }, + function(cb) { + connection.get('dogs', 'spot').create({age: 5}, cb); }, function(cb) { - doc2[method](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'); @@ -101,71 +127,25 @@ describe('client subscribe', function() { } ], 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); + connection.get('dogs', 'fido').create({age: 3}, cb); }, function(cb) { - spot[method](cb); + connection.get('dogs', 'spot').create({age: 5}, cb); }, function(cb) { - finn[method](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) { @@ -179,513 +159,531 @@ describe('client subscribe', function() { } ], 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); + fido[method](cb); }, function(cb) { - connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); + spot[method](cb); }, function(cb) { - connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], 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); + connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb); }, function(cb) { - spot[method](cb); + connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); }, function(cb) { - finn[method](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(op, context) { + 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 doc = this.backend.connect().get('dogs', 'fido'); - doc.subscribe(function(err) { - if (err) return done(err); - doc.unsubscribe(done); - doc.connection.close(); + 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); + }); + }); }); - }); - 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); - doc.connection.close(); - doc.unsubscribe(done); + 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); + doc.unsubscribe(done); + doc.connection.close(); + }); }); - }); - 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('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); + doc.connection.close(); + doc.unsubscribe(done); }); - 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); + 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) { - expect(doc2.version).eql(2); - expect(doc2.data).eql({age: 4}); + doc2.on('create', function(context) { + expect(context).equal(false); + expect(doc2.version).eql(1); + expect(doc2.data).eql({age: 3}); done(); }); - doc.submitOp({p: ['age'], na: 1}); + doc.create({age: 3}); }); }); - }); - 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('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.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}); }); - doc2.connection.close(); - doc.submitOp({p: ['age'], na: 1}, 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); - 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(); + doc2.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); }); - backend.suppressPublish = true; - doc.submitOp({p: ['age'], na: 1}, done); }); }); - }); - 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('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); - doc2.on('op', function(op, context) { - done(); - }); - doc2.unsubscribe(function(err) { + doc2.subscribe(function(err) { if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + backend.suppressPublish = true; doc.submitOp({p: ['age'], na: 1}, done); }); }); }); - }); - 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('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) { - done(new Error('Should not get op event')); + doc2.subscribe(function(err) { + if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + doc2.unsubscribe(function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 1}, done); + }); }); - doc2.destroy(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.subscribe(function(err) { if (err) return done(err); - expect(connection2.getExisting('dogs', 'fido')).equal(undefined); - doc.submitOp({p: ['age'], na: 1}, done); + doc2.on('op', function(op, context) { + 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); + }); }); }); }); - }); - 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('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('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); - fido.connection.startBulk(); async.parallel([ function(cb) { - fido.unsubscribe(cb); + fido.subscribe(cb); }, function(cb) { - spot.unsubscribe(cb); + spot.subscribe(cb); } ], function(err) { if (err) return done(err); - fido.on('op', function(op, context) { - done(); + fido.connection.startBulk(); + async.parallel([ + function(cb) { + fido.unsubscribe(cb); + }, + function(cb) { + spot.unsubscribe(cb); + } + ], function(err) { + if (err) return done(err); + fido.on('op', function(op, context) { + done(); + }); + doc.submitOp({p: ['age'], na: 1}, done); }); - doc.submitOp({p: ['age'], na: 1}, done); + fido.connection.endBulk(); }); - 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('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.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(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) { + if (err) return done(err); + backend.connect(doc2.connection); + }); }); }); }); - }); - 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('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.on('op', function(op, context) { - done(); + doc2.subscribe(); + doc2.unsubscribe(); + doc2.subscribe(function(err) { + if (err) return done(err); + doc2.on('op', function(op, context) { + done(); + }); + doc.submitOp({p: ['age'], na: 1}); }); - 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 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); - 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) { + doc2.subscribe(function(err) { if (err) return done(err); - backend.suppressPublish = false; - doc.submitOp({p: ['age'], na: 5}); + 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}); + }); }); }); }); - }); - 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('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); - 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) { + doc2.subscribe(function(err) { if (err) return done(err); - backend.suppressPublish = false; - doc.submitOp({p: ['age'], na: 5}, function(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) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 13}, function(err) { + backend.suppressPublish = false; + doc.submitOp({p: ['age'], na: 5}, function(err) { if (err) return done(err); - doc.submitOp({p: ['age'], na: 100}); + doc.submitOp({p: ['age'], na: 13}, function(err) { + if (err) return done(err); + doc.submitOp({p: ['age'], na: 100}); + }); }); }); }); }); }); - }); - - describe('doc.subscribed', function() { - it('is set to false initially', function() { - var doc = this.backend.connect().get('dogs', 'fido'); - expect(doc.subscribed).equal(false); - }); - 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(); + describe('doc.subscribed', function() { + it('is set to false initially', function() { + var doc = this.backend.connect().get('dogs', 'fido'); + expect(doc.subscribed).equal(false); }); - }); - 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); + it('remains false before subscribe call completes', function() { + var doc = this.backend.connect().get('dogs', 'fido'); + doc.subscribe(); 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); + 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(); - 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(); + 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(); + }); }); }); }); -}); }; From a551de3444dd90cbcb08362e4b4e57fe16929f34 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:29:19 +0100 Subject: [PATCH 177/181] Extend Google's ESLint config This change actively extends the Google ESLint config. We move the config into a `.js` file so that we can add comments about particular rules that we've overridden. This change also fixes some trailing comma and indentation linting errors. --- .eslintrc | 139 ------------------ .eslintrc.js | 43 ++++++ lib/backend.js | 2 +- .../snapshot-timestamp-request.js | 2 +- .../snapshot-version-request.js | 2 +- lib/query-emitter.js | 4 +- package.json | 1 + test/client/snapshot-timestamp-request.js | 4 +- test/client/snapshot-version-request.js | 8 +- test/client/subscribe.js | 2 +- 10 files changed, 56 insertions(+), 151 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.js diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 0ce02cbc0..000000000 --- a/.eslintrc +++ /dev/null @@ -1,139 +0,0 @@ -{ - "parserOptions": { - "ecmaVersion": 3 - }, - "rules": { - "no-cond-assign": 0, - "no-irregular-whitespace": 2, - "no-unexpected-multiline": 2, - "curly": [ - 2, - "multi-line" - ], - "guard-for-in": 0, - "no-caller": 2, - "no-extend-native": 2, - "no-extra-bind": 2, - "no-invalid-this": 2, - "no-multi-spaces": 2, - "no-multi-str": 2, - "no-new-wrappers": 2, - "no-throw-literal": 2, - "no-with": 2, - "prefer-promise-reject-errors": 2, - "no-unused-vars": [ - 2, - { - "args": "none" - } - ], - "array-bracket-newline": 0, - "array-bracket-spacing": [ - 2, - "never" - ], - "array-element-newline": 0, - "block-spacing": [ - 2, - "never" - ], - "brace-style": 2, - "camelcase": [ - 2, - { - "properties": "never" - } - ], - "comma-spacing": 2, - "comma-style": 2, - "computed-property-spacing": 2, - "eol-last": 2, - "func-call-spacing": 2, - "indent": [ - "error", - 2, - { - "CallExpression": { - "arguments": 1 - }, - "FunctionDeclaration": { - "body": 1, - "parameters": 1 - }, - "FunctionExpression": { - "body": 1, - "parameters": 1 - }, - "MemberExpression": 1, - "ObjectExpression": 1, - "SwitchCase": 1, - "ignoredNodes": [ - "ConditionalExpression" - ] - } - ], - "key-spacing": 2, - "keyword-spacing": 2, - "linebreak-style": 2, - "max-len": [ - 2, - { - "code": 120, - "tabWidth": 2, - "ignoreUrls": true, - } - ], - "new-cap": 2, - "no-array-constructor": 2, - "no-mixed-spaces-and-tabs": 2, - "no-multiple-empty-lines": [ - 2, - { - "max": 2 - } - ], - "no-new-object": 2, - "no-tabs": 2, - "no-trailing-spaces": 2, - "object-curly-spacing": 2, - "one-var": [ - 2, - { - "var": "never", - "let": "never", - "const": "never" - } - ], - "padded-blocks": [ - 2, - "never" - ], - "quote-props": [ - 2, - "as-needed" - ], - "quotes": [ - 2, - "single", - { - "allowTemplateLiterals": true - } - ], - "semi": 2, - "semi-spacing": 2, - "space-before-blocks": 2, - "space-before-function-paren": [ - 2, - { - "asyncArrow": "always", - "anonymous": "never", - "named": "never" - } - ], - "spaced-comment": [ - 2, - "always" - ], - "switch-colon-spacing": 2, - }, -} \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..475941fc2 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,43 @@ +// 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, + } + ], + // 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/lib/backend.js b/lib/backend.js index 1d5c4fe4f..ae11aa450 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -616,7 +616,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) { diff --git a/lib/client/snapshot-request/snapshot-timestamp-request.js b/lib/client/snapshot-request/snapshot-timestamp-request.js index 0e8af04af..15789137b 100644 --- a/lib/client/snapshot-request/snapshot-timestamp-request.js +++ b/lib/client/snapshot-request/snapshot-timestamp-request.js @@ -21,6 +21,6 @@ SnapshotTimestampRequest.prototype._message = function() { id: this.requestId, c: this.collection, d: this.id, - ts: this.timestamp, + ts: this.timestamp }; }; diff --git a/lib/client/snapshot-request/snapshot-version-request.js b/lib/client/snapshot-request/snapshot-version-request.js index 0b64b3821..d352a676a 100644 --- a/lib/client/snapshot-request/snapshot-version-request.js +++ b/lib/client/snapshot-request/snapshot-version-request.js @@ -21,6 +21,6 @@ SnapshotVersionRequest.prototype._message = function() { id: this.requestId, c: this.collection, d: this.id, - v: this.version, + v: this.version }; }; diff --git a/lib/query-emitter.js b/lib/query-emitter.js index 77b91f8b8..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; diff --git a/package.json b/package.json index 9e4e5b09c..058190e06 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "devDependencies": { "coveralls": "^2.11.8", "eslint": "^5.16.0", + "eslint-config-google": "^0.13.0", "expect.js": "^0.3.1", "istanbul": "^0.4.2", "lolex": "^3.0.0", diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 0aeae9f9d..83b24644e 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -340,7 +340,7 @@ describe('SnapshotTimestampRequest', function() { function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); - }, + } ]; backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function(error, snapshot) { @@ -354,7 +354,7 @@ describe('SnapshotTimestampRequest', function() { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ function(request, callback) { callback({message: 'foo'}); - }, + } ]; backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error, snapshot) { diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index 2f959be07..e55fcf2fc 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -278,7 +278,7 @@ describe('SnapshotVersionRequest', function() { function(request, callback) { request.snapshots[0].data.title = 'Alice in Wonderland'; callback(); - }, + } ]; backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) { @@ -292,7 +292,7 @@ describe('SnapshotVersionRequest', function() { backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [ function(request, callback) { callback({message: 'foo'}); - }, + } ]; backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) { @@ -353,7 +353,7 @@ describe('SnapshotVersionRequest', function() { v: 1, type: 'http://sharejs.org/types/JSONv0', data: { - title: 'Catch 22', + title: 'Catch 22' }, m: null }); @@ -386,7 +386,7 @@ describe('SnapshotVersionRequest', function() { v: 3, type: 'http://sharejs.org/types/JSONv0', data: { - title: 'The Restaurant at the End of the Universe', + title: 'The Restaurant at the End of the Universe' }, m: null }); diff --git a/test/client/subscribe.js b/test/client/subscribe.js index 083634fb5..0674d5bb0 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -576,7 +576,7 @@ module.exports = function() { if (err) return done(err); var expected = [ [{p: ['age'], na: 1}], - [{p: ['age'], na: 5}], + [{p: ['age'], na: 5}] ]; doc2.on('op', function(op, context) { var item = expected.shift(); From 3dfc93863a0ebc264c379d528326fd0a8737a864 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:30:22 +0100 Subject: [PATCH 178/181] Use `.gitignore` for defining ESLint's ignore pattern This change uses our `.gitignore` file to define which files ESLint should ignore, so that all of our committed JavaScript has a consistent style. --- examples/leaderboard/server/index.js | 19 ++++++++++--------- examples/textarea/client.js | 26 +++++++++++++------------- examples/textarea/server.js | 2 +- package.json | 3 ++- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/examples/leaderboard/server/index.js b/examples/leaderboard/server/index.js index 25410f8bb..2c10932a1 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('@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,7 +15,7 @@ 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) { @@ -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/textarea/client.js b/examples/textarea/client.js index dadaef7fa..4cbf55208 100644 --- a/examples/textarea/client.js +++ b/examples/textarea/client.js @@ -2,35 +2,35 @@ var sharedb = require('sharedb/lib/client'); var StringBinding = require('sharedb-string-binding'); // Open WebSocket connection to ShareDB server -const WebSocket = require('reconnecting-websocket'); +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" +statusSpan.innerHTML = 'Not Connected'; -element.style.backgroundColor = "gray"; -socket.onopen = function(){ - statusSpan.innerHTML = "Connected" - element.style.backgroundColor = "white"; +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.onclose = function() { + statusSpan.innerHTML = 'Closed'; + element.style.backgroundColor = 'gray'; }; socket.onerror = function() { - statusSpan.innerHTML = "Error" - element.style.backgroundColor = "red"; -} + 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 binding = new StringBinding(element, doc, ['content']); binding.setup(); }); diff --git a/examples/textarea/server.js b/examples/textarea/server.js index 9d359a771..ee419ba1e 100644 --- a/examples/textarea/server.js +++ b/examples/textarea/server.js @@ -14,7 +14,7 @@ function createDoc(callback) { doc.fetch(function(err) { if (err) throw err; if (doc.type === null) { - doc.create({ content: '' }, callback); + doc.create({content: ''}, callback); return; } callback(); diff --git a/package.json b/package.json index 058190e06..b30da8173 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,8 @@ "scripts": { "test": "./node_modules/.bin/mocha && npm run lint", "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha", - "lint": "./node_modules/.bin/eslint 'lib/**/*.js' 'test/**/*.js'" + "lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'", + "lint:fix": "npm run lint -- --fix" }, "repository": { "type": "git", From df5a466b43cd951e7fb5abd010bb463f04ecef86 Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:40:19 +0100 Subject: [PATCH 179/181] Reset `no-unused-vars` linting rule to default --- .eslintrc.js | 2 + examples/counter/server.js | 2 +- examples/leaderboard/server/index.js | 2 +- examples/rich-text/server.js | 2 +- examples/textarea/server.js | 2 +- lib/db/memory.js | 2 +- test/client/number-type.js | 2 +- test/client/query-subscribe.js | 4 +- test/client/snapshot-timestamp-request.js | 5 +- test/client/snapshot-version-request.js | 5 +- test/client/subscribe.js | 22 +++---- test/db-memory.js | 2 +- test/db.js | 77 +++++++++++++---------- test/middleware.js | 2 +- test/pubsub.js | 5 +- 15 files changed, 77 insertions(+), 59 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 475941fc2..8c5f1133e 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -24,6 +24,8 @@ const SHAREDB_RULES = { 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'}], // as-needed quote props are easier to write 'quote-props': ['error', 'as-needed'], 'require-jsdoc': 'off', diff --git a/examples/counter/server.js b/examples/counter/server.js index 8817a4584..f6575bfeb 100644 --- a/examples/counter/server.js +++ b/examples/counter/server.js @@ -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/server/index.js b/examples/leaderboard/server/index.js index 2c10932a1..6aebe997f 100644 --- a/examples/leaderboard/server/index.js +++ b/examples/leaderboard/server/index.js @@ -18,7 +18,7 @@ server.listen(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); }); diff --git a/examples/rich-text/server.js b/examples/rich-text/server.js index 486130f95..f0654cdf8 100644 --- a/examples/rich-text/server.js +++ b/examples/rich-text/server.js @@ -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/server.js b/examples/textarea/server.js index ee419ba1e..165c8ea0d 100644 --- a/examples/textarea/server.js +++ b/examples/textarea/server.js @@ -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/lib/db/memory.js b/lib/db/memory.js index 9c1a47b56..029c1229b 100644 --- a/lib/db/memory.js +++ b/lib/db/memory.js @@ -122,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}; }; diff --git a/test/client/number-type.js b/test/client/number-type.js index d25401ebc..fa95056e4 100644 --- a/test/client/number-type.js +++ b/test/client/number-type.js @@ -18,6 +18,6 @@ function apply(snapshot, op) { return snapshot + op; } -function transform(op1, op2, side) { +function transform(op1) { return op1; } diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js index ddce30d64..166c859eb 100644 --- a/test/client/query-subscribe.js +++ b/test/client/query-subscribe.js @@ -306,7 +306,7 @@ module.exports = function(options) { if (err) return done(err); connection.get('dogs', 'fido').create({}); }); - query.on('insert', function(docs, index) { + query.on('insert', function(docs) { expect(util.pluck(docs, 'id')).eql(['fido']); done(); }); @@ -320,7 +320,7 @@ module.exports = function(options) { if (err) return done(err); connection.get('dogs', 'fido').create({}); }); - query.on('insert', function(docs, index) { + query.on('insert', function(docs) { expect(util.pluck(docs, 'id')).eql(['fido']); done(); }); diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js index 83b24644e..8d7cc4bf3 100644 --- a/test/client/snapshot-timestamp-request.js +++ b/test/client/snapshot-timestamp-request.js @@ -227,7 +227,8 @@ describe('SnapshotTimestampRequest', function() { it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error, snapshot) { + connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error) { + if (error) return done(error); expect(connection.hasPending()).to.be(false); done(); }); @@ -357,7 +358,7 @@ describe('SnapshotTimestampRequest', function() { } ]; - backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error, snapshot) { + backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error) { expect(error.message).to.be('foo'); done(); }); diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js index e55fcf2fc..a39355f38 100644 --- a/test/client/snapshot-version-request.js +++ b/test/client/snapshot-version-request.js @@ -165,7 +165,8 @@ describe('SnapshotVersionRequest', function() { it('starts pending, and finishes not pending', function(done) { var connection = backend.connect(); - connection.fetchSnapshot('books', 'don-quixote', null, function(error, snapshot) { + connection.fetchSnapshot('books', 'don-quixote', null, function(error) { + if (error) return done(error); expect(connection.hasPending()).to.be(false); done(); }); @@ -295,7 +296,7 @@ describe('SnapshotVersionRequest', function() { } ]; - backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) { + backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) { expect(error.message).to.be('foo'); done(); }); diff --git a/test/client/subscribe.js b/test/client/subscribe.js index 0674d5bb0..2af398681 100644 --- a/test/client/subscribe.js +++ b/test/client/subscribe.js @@ -229,7 +229,7 @@ module.exports = function() { if (err) return done(err); doc.submitOp({p: ['age'], na: 1}, function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc2[method](); @@ -396,7 +396,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { expect(doc2.version).eql(2); expect(doc2.data).eql({age: 4}); done(); @@ -413,7 +413,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc2.connection.close(); @@ -430,7 +430,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); backend.suppressPublish = true; @@ -446,7 +446,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc2.unsubscribe(function(err) { @@ -466,7 +466,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(new Error('Should not get op event')); }); doc2.destroy(function(err) { @@ -516,7 +516,7 @@ module.exports = function() { } ], function(err) { if (err) return done(err); - fido.on('op', function(op, context) { + fido.on('op', function() { done(); }); doc.submitOp({p: ['age'], na: 1}, done); @@ -534,7 +534,7 @@ module.exports = function() { if (err) return done(err); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { expect(doc2.version).eql(2); expect(doc2.data).eql({age: 4}); done(); @@ -558,7 +558,7 @@ module.exports = function() { doc2.unsubscribe(); doc2.subscribe(function(err) { if (err) return done(err); - doc2.on('op', function(op, context) { + doc2.on('op', function() { done(); }); doc.submitOp({p: ['age'], na: 1}); @@ -578,7 +578,7 @@ module.exports = function() { [{p: ['age'], na: 1}], [{p: ['age'], na: 5}] ]; - doc2.on('op', function(op, context) { + doc2.on('op', function(op) { var item = expected.shift(); expect(op).eql(item); if (expected.length) return; @@ -610,7 +610,7 @@ module.exports = function() { doc2.subscribe(function(err) { if (err) return done(err); var wait = 4; - doc2.on('op', function(op, context) { + doc2.on('op', function() { if (--wait) return; expect(doc2.version).eql(5); expect(doc2.data).eql({age: 122}); diff --git a/test/db-memory.js b/test/db-memory.js index b8b8e064e..0f0d01a92 100644 --- a/test/db-memory.js +++ b/test/db-memory.js @@ -64,7 +64,7 @@ function BasicQueryableMemoryDB() { BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype); BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB; -BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query, options) { +BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query) { if (query.filter) { snapshots = snapshots.filter(function(snapshot) { for (var queryKey in query.filter) { diff --git a/test/db.js b/test/db.js index 270a2ea92..701bdb7a0 100644 --- a/test/db.js +++ b/test/db.js @@ -238,7 +238,7 @@ 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); @@ -251,6 +251,7 @@ 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(null); @@ -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,7 +290,7 @@ 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); @@ -303,6 +306,7 @@ 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(null); @@ -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); @@ -623,7 +628,7 @@ module.exports = function(options) { it('query returns data in the collection', function(done) { 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,6 +650,7 @@ 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(null); @@ -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,6 +706,7 @@ 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(null); @@ -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); diff --git a/test/middleware.js b/test/middleware.js index f71ddce5d..85ceb686e 100644 --- a/test/middleware.js +++ b/test/middleware.js @@ -21,7 +21,7 @@ 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); }); }); diff --git a/test/pubsub.js b/test/pubsub.js index 4bf6cbf1a..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,7 +46,8 @@ 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); stream.on('data', function(data) { From 40abc1740b8e44fa93a14240b526c11cce8778ea Mon Sep 17 00:00:00 2001 From: Alec Gibson Date: Mon, 15 Jul 2019 12:47:58 +0100 Subject: [PATCH 180/181] Review markups This change makes some linting review markups: - pre-comupte long, concatenated strings - only allow one statement per line --- .eslintrc.js | 2 ++ lib/agent.js | 4 +++- lib/backend.js | 17 +++++++++-------- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 8c5f1133e..ab65eaee8 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -26,6 +26,8 @@ const SHAREDB_RULES = { ], // 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', diff --git a/lib/agent.js b/lib/agent.js index db18c4a5d..f548f4d71 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -332,7 +332,9 @@ Agent.prototype._handleMessage = function(request, callback) { }; function getQueryOptions(request) { var results = request.r; - var ids; var fetch; var fetchOps; + var ids; + var fetch; + var fetchOps; if (results) { ids = []; for (var i = 0; i < results.length; i++) { diff --git a/lib/backend.js b/lib/backend.js index ae11aa450..dc3b025ce 100644 --- a/lib/backend.js +++ b/lib/backend.js @@ -15,6 +15,13 @@ 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); @@ -94,10 +101,7 @@ Backend.prototype.SNAPSHOT_TYPES = { Backend.prototype._shimDocAction = function() { if (warnDeprecatedDoc) { warnDeprecatedDoc = false; - console.warn([ - 'DEPRECATED: "doc" middleware action. Use "readSnapshots" instead.', - 'Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.' - ].join(' ')); + console.warn(DOC_ACTION_DEPRECATION_WARNING); } var backend = this; @@ -114,10 +118,7 @@ Backend.prototype._shimDocAction = function() { Backend.prototype._shimAfterSubmit = function() { if (warnDeprecatedAfterSubmit) { warnDeprecatedAfterSubmit = false; - console.warn([ - 'DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass', - '`disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.' - ].join(' ')); + console.warn(AFFTER_SUBMIT_ACTION_DEPRECATION_WARNING); } var backend = this; From 14b0d31c7ae1b0be075d46267d6c4b549f427e8f Mon Sep 17 00:00:00 2001 From: Eric Hwang Date: Wed, 17 Jul 2019 10:02:38 -0700 Subject: [PATCH 181/181] eslint --fix, plus a bit of manual line wrapping to get under 120 chars --- lib/agent.js | 1 - lib/client/doc.js | 6 +- lib/presence/dummy.js | 12 +- lib/presence/index.js | 8 +- lib/presence/stateless.js | 65 +++---- lib/util.js | 2 +- test/client/presence-type.js | 6 +- test/client/presence.js | 346 ++++++++++++++++++----------------- test/client/submit.js | 4 +- 9 files changed, 231 insertions(+), 219 deletions(-) diff --git a/lib/agent.js b/lib/agent.js index 519157987..dbd35318c 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -2,7 +2,6 @@ var hat = require('hat'); var util = require('./util'); var types = require('./types'); var logger = require('./logger'); -var ShareDBError = require('./error'); /** * Agent deserializes the wire protocol messages received from the stream and diff --git a/lib/client/doc.js b/lib/client/doc.js index 571fc0e03..cb504c68a 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -359,11 +359,11 @@ Doc.prototype._handleOp = function(err, message) { } }; -Doc.prototype._handlePresence = function (err, presence) { +Doc.prototype._handlePresence = function(err, presence) { this._docPresence.handlePresence(err, presence); }; -Doc.prototype.submitPresence = function (data, callback) { +Doc.prototype.submitPresence = function(data, callback) { this._docPresence.submitPresence(data, callback); }; @@ -611,7 +611,7 @@ Doc.prototype._otApply = function(op, source) { this.emit('op', op.op, source); return; } - + this._docPresence.transformAllPresence(op); if (op.create) { diff --git a/lib/presence/dummy.js b/lib/presence/dummy.js index 7eda4b13e..423729967 100644 --- a/lib/presence/dummy.js +++ b/lib/presence/dummy.js @@ -8,11 +8,15 @@ */ var presence = require('./index'); -function noop () {} -function returnEmptyArray () { return []; }; -function returnFalse () { return false; }; +function noop() {} +function returnEmptyArray() { + return []; +}; +function returnFalse() { + return false; +}; -function DocPresence () {} +function DocPresence() {} DocPresence.prototype = Object.create(presence.DocPresence.prototype); Object.assign(DocPresence.prototype, { submitPresence: noop, diff --git a/lib/presence/index.js b/lib/presence/index.js index 2d85e8fc5..cddecd0ad 100644 --- a/lib/presence/index.js +++ b/lib/presence/index.js @@ -1,6 +1,6 @@ module.exports = { - DocPresence: function DocPresence () {}, - ConnectionPresence: function ConnectionPresence () {}, - AgentPresence: function AgentPresence () {}, - BackendPresence: function BackendPresence () {} + DocPresence: function DocPresence() {}, + ConnectionPresence: function ConnectionPresence() {}, + AgentPresence: function AgentPresence() {}, + BackendPresence: function BackendPresence() {} }; diff --git a/lib/presence/stateless.js b/lib/presence/stateless.js index 8f1d27782..14c186ed7 100644 --- a/lib/presence/stateless.js +++ b/lib/presence/stateless.js @@ -65,14 +65,15 @@ function DocPresence(doc) { DocPresence.prototype = Object.create(presence.DocPresence.prototype); // Submit presence data to a document. -// This is the only public facing method. +// This is the only public facing method. // All the others are marked as internal with a leading "_". -DocPresence.prototype.submitPresence = function (data, callback) { +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); + 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); }); @@ -81,7 +82,8 @@ DocPresence.prototype.submitPresence = function (data, callback) { 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); + 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); }); @@ -97,7 +99,6 @@ DocPresence.prototype.submitPresence = function (data, callback) { if (callback) { this.pending.push(callback); } - } else if (callback) { process.nextTick(callback); } @@ -105,7 +106,7 @@ DocPresence.prototype.submitPresence = function (data, callback) { process.nextTick(this.doc.flush.bind(this.doc)); }; -DocPresence.prototype.handlePresence = function (err, presence) { +DocPresence.prototype.handlePresence = function(err, presence) { if (!this.doc.subscribed) return; var src = presence.src; @@ -156,10 +157,10 @@ DocPresence.prototype.handlePresence = function (err, presence) { 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) { +DocPresence.prototype._processReceivedPresence = function(src, emit) { if (!src) return false; var presence = this.received[src]; if (!presence) return false; @@ -241,7 +242,7 @@ DocPresence.prototype._processReceivedPresence = function (src, emit) { return this._setPresence(src, data, emit); }; -DocPresence.prototype.processAllReceivedPresence = function () { +DocPresence.prototype.processAllReceivedPresence = function() { var srcList = Object.keys(this.received); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -253,7 +254,7 @@ DocPresence.prototype.processAllReceivedPresence = function () { this._emitPresence(changedSrcList, true); }; -DocPresence.prototype._transformPresence = function (src, op) { +DocPresence.prototype._transformPresence = function(src, op) { var presenceData = this.doc.presence[src]; if (op.op != null) { var isOwnOperation = src === (op.src || ''); @@ -263,8 +264,8 @@ DocPresence.prototype._transformPresence = function (src, op) { } return this._setPresence(src, presenceData); }; - -DocPresence.prototype.transformAllPresence = function (op) { + +DocPresence.prototype.transformAllPresence = function(op) { var srcList = Object.keys(this.doc.presence); var changedSrcList = []; for (var i = 0; i < srcList.length; i++) { @@ -276,7 +277,7 @@ DocPresence.prototype.transformAllPresence = function (op) { this._emitPresence(changedSrcList, false); }; -DocPresence.prototype.pausePresence = function () { +DocPresence.prototype.pausePresence = function() { if (!this) return; if (this.inflight) { @@ -303,7 +304,7 @@ DocPresence.prototype.pausePresence = function () { // 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) { +DocPresence.prototype._setPresence = function(src, data, emit) { if (data == null) { if (this.doc.presence[src] == null) return false; delete this.doc.presence[src]; @@ -314,11 +315,11 @@ DocPresence.prototype._setPresence = function (src, data, emit) { if (isPresenceEqual) return false; this.doc.presence[src] = data; } - if (emit) this._emitPresence([ src ], true); + if (emit) this._emitPresence([src], true); return true; }; -DocPresence.prototype._emitPresence = function (srcList, submitted) { +DocPresence.prototype._emitPresence = function(srcList, submitted) { if (srcList && srcList.length > 0) { var doc = this.doc; process.nextTick(function() { @@ -327,14 +328,14 @@ DocPresence.prototype._emitPresence = function (srcList, submitted) { } }; -DocPresence.prototype.cacheOp = function (message) { +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; @@ -352,8 +353,8 @@ DocPresence.prototype.cacheOp = function (message) { }; // If there are no pending ops, this method sends the pending presence data, if possible. -DocPresence.prototype.flushPresence = function () { - if(!this.inflight && this.pending) { +DocPresence.prototype.flushPresence = function() { + if (!this.inflight && this.pending) { this.inflight = this.pending; this.inflightSeq = this.doc.connection.seq; this.pending = null; @@ -362,17 +363,17 @@ DocPresence.prototype.flushPresence = function () { } }; -DocPresence.prototype.destroyPresence = function () { +DocPresence.prototype.destroyPresence = function() { this.received = {}; this.clearCachedOps(); }; -DocPresence.prototype.clearCachedOps = function () { +DocPresence.prototype.clearCachedOps = function() { this.cachedOps.length = 0; }; // Reset presence-related properties. -DocPresence.prototype.hardRollbackPresence = function () { +DocPresence.prototype.hardRollbackPresence = function() { this.inflight = null; this.inflightSeq = 0; this.pending = null; @@ -391,11 +392,11 @@ DocPresence.prototype.hardRollbackPresence = function () { this._emitPresence(changedSrcList, false); }; -DocPresence.prototype.hasPendingPresence = function () { +DocPresence.prototype.hasPendingPresence = function() { return this.inflight || this.pending; }; -DocPresence.prototype.getPendingPresence = function () { +DocPresence.prototype.getPendingPresence = function() { var pendingPresence = []; if (this.inflight) pendingPresence.push(this.inflight); if (this.pending) pendingPresence.push(this.pending); @@ -414,7 +415,7 @@ ConnectionPresence.prototype = Object.create(presence.ConnectionPresence.prototy ConnectionPresence.prototype.isPresenceMessage = isPresenceMessage; -ConnectionPresence.prototype.handlePresenceMessage = function (err, message) { +ConnectionPresence.prototype.handlePresenceMessage = function(err, message) { var doc = this.connection.getExisting(message.c, message.d); if (doc) doc._handlePresence(err, message); }; @@ -451,7 +452,7 @@ AgentPresence.prototype = Object.create(presence.AgentPresence.prototype); AgentPresence.prototype.isPresenceMessage = isPresenceMessage; -AgentPresence.prototype.processPresenceData = function (data) { +AgentPresence.prototype.processPresenceData = function(data) { if (data.a === 'p') { // Send other clients' presence data if (data.src !== this.agent.clientId) this.agent.send(data); @@ -472,14 +473,14 @@ AgentPresence.prototype.createPresence = function(collection, id, data, version, }; }; -AgentPresence.prototype.subscribeToStream = function (collection, id, stream) { +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) { +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'; @@ -510,7 +511,7 @@ AgentPresence.prototype.handlePresenceMessage = function(request, callback) { } this.agent.backend.sendPresence(presence, function(err) { if (err) return callback(err); - callback(null, { seq: presence.seq }); + callback(null, {seq: presence.seq}); }); }; @@ -525,9 +526,9 @@ function BackendPresence(backend) { BackendPresence.prototype = Object.create(presence.BackendPresence.prototype); BackendPresence.prototype.sendPresence = function(presence, callback) { - var channels = [ this.backend.getDocChannel(presence.c, presence.d) ]; + var channels = [this.backend.getDocChannel(presence.c, presence.d)]; this.backend.pubsub.publish(channels, presence, callback); -} +}; module.exports = { diff --git a/lib/util.js b/lib/util.js index d96b5429c..4c4783430 100644 --- a/lib/util.js +++ b/lib/util.js @@ -23,7 +23,7 @@ exports.isValidTimestamp = function(timestamp) { return exports.isValidVersion(timestamp); }; -exports.callEach = function (callbacks, err) { +exports.callEach = function(callbacks, err) { var called = false; for (var i = 0; i < callbacks.length; i++) { var callback = callbacks[i]; diff --git a/test/client/presence-type.js b/test/client/presence-type.js index 6138eae7f..7648d0e2a 100644 --- a/test/client/presence-type.js +++ b/test/client/presence-type.js @@ -48,17 +48,17 @@ 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 }; + : {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 }; + : {index: presence.index + 1}; } function comparePresence(presence1, presence2) { diff --git a/test/client/presence.js b/test/client/presence.js index 5cdd46f6f..c5fa39ff1 100644 --- a/test/client/presence.js +++ b/test/client/presence.js @@ -23,7 +23,7 @@ describe('client presence', function() { }); it('should use presence option if provided', function() { - var backend = new Backend({ presence: statelessPresence }); + 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); @@ -44,12 +44,12 @@ describe('client presence', function() { '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({ presence: statelessPresence }); + this.backend = new Backend({presence: statelessPresence}); this.connection = this.backend.connect(); this.connection2 = this.backend.connect(); this.doc = this.connection.get('dogs', 'fido'); @@ -69,7 +69,7 @@ describe('client presence', function() { 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)); @@ -85,14 +85,14 @@ describe('client presence', function() { 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.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)); @@ -107,9 +107,9 @@ describe('client presence', function() { 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)); @@ -119,8 +119,8 @@ describe('client presence', function() { 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); @@ -128,22 +128,22 @@ describe('client presence', function() { 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.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) @@ -152,22 +152,22 @@ describe('client presence', function() { 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.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -176,22 +176,22 @@ describe('client presence', function() { 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.data = ['c']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -200,22 +200,22 @@ describe('client presence', function() { 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.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(0), errorHandler(done)); }.bind(this) @@ -224,22 +224,22 @@ describe('client presence', function() { 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.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -248,22 +248,22 @@ describe('client presence', function() { 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.data = ['c']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -275,14 +275,14 @@ describe('client presence', function() { 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)); @@ -291,9 +291,9 @@ describe('client presence', function() { }.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)); @@ -307,16 +307,16 @@ describe('client presence', function() { 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)); @@ -326,15 +326,15 @@ describe('client presence', function() { function(done) { 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.data = ['a']; this.doc._docPresence.requestReply = false; this.doc.submitPresence(p(1), errorHandler(done)); }.bind(this) @@ -351,7 +351,7 @@ describe('client presence', function() { 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); @@ -372,7 +372,7 @@ describe('client presence', function() { 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); @@ -385,7 +385,7 @@ describe('client presence', function() { 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)), @@ -393,20 +393,20 @@ describe('client presence', function() { 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)), @@ -414,20 +414,20 @@ describe('client presence', function() { 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)), @@ -435,20 +435,20 @@ describe('client presence', function() { 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)), @@ -456,21 +456,21 @@ describe('client presence', function() { 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) { @@ -484,10 +484,10 @@ describe('client presence', function() { }); 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, @@ -503,13 +503,13 @@ describe('client presence', function() { it('expires cached ops', function(allDone) { var clock = lolex.install(); - var op1 = { index: 1, value: 'b' }; - var op2 = { index: 2, value: 'b' }; - var op3 = { index: 3, value: 'b' }; + 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._docPresence.cachedOps.length).to.equal(2); @@ -519,7 +519,7 @@ describe('client presence', function() { }.bind(this), // Cache another op before the first 2 expire. - function (callback) { + function(callback) { setTimeout(callback, 30); clock.next(); }, @@ -533,7 +533,7 @@ describe('client presence', function() { }.bind(this), // Cache another op after the first 2 expire. - function (callback) { + function(callback) { setTimeout(callback, 31); clock.next(); }, @@ -550,19 +550,19 @@ describe('client presence', function() { 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._docPresence.requestReply).to.equal(false); @@ -639,12 +639,12 @@ describe('client presence', function() { 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(); @@ -659,11 +659,11 @@ describe('client presence', function() { 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(); @@ -682,12 +682,12 @@ describe('client presence', function() { 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(); @@ -704,12 +704,12 @@ describe('client presence', function() { 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(); @@ -722,7 +722,7 @@ describe('client presence', function() { 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); @@ -743,7 +743,7 @@ describe('client presence', function() { 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); @@ -770,7 +770,7 @@ describe('client presence', function() { 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)), @@ -778,7 +778,7 @@ describe('client presence', function() { 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); @@ -794,7 +794,7 @@ describe('client presence', function() { 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)), @@ -808,7 +808,7 @@ describe('client presence', function() { 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)); @@ -821,7 +821,7 @@ describe('client presence', function() { 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)), @@ -835,7 +835,7 @@ describe('client presence', function() { 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)); @@ -848,7 +848,7 @@ describe('client presence', function() { 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)), @@ -862,7 +862,7 @@ describe('client presence', function() { 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)); @@ -875,7 +875,7 @@ describe('client presence', function() { 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)), @@ -889,7 +889,7 @@ describe('client presence', function() { 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)); @@ -902,7 +902,7 @@ describe('client presence', function() { 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; @@ -924,7 +924,7 @@ describe('client presence', function() { 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; @@ -946,7 +946,7 @@ describe('client presence', function() { 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)), @@ -972,7 +972,7 @@ describe('client presence', function() { 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)), @@ -995,76 +995,76 @@ describe('client presence', function() { ], 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._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._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._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); @@ -1074,14 +1074,14 @@ describe('client presence', function() { 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)), @@ -1090,7 +1090,7 @@ describe('client presence', function() { 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); @@ -1099,22 +1099,22 @@ describe('client presence', function() { }.bind(this)); 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(); @@ -1129,7 +1129,7 @@ describe('client presence', function() { 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)), @@ -1137,7 +1137,7 @@ describe('client presence', function() { 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(); @@ -1152,7 +1152,7 @@ describe('client presence', function() { 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); @@ -1164,14 +1164,14 @@ describe('client presence', function() { expect(err).to.be.an(Error); 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); @@ -1182,7 +1182,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4026); done(); - }.bind(this)); + }); this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -1190,7 +1190,7 @@ describe('client presence', function() { 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, @@ -1201,14 +1201,14 @@ describe('client presence', function() { expect(err).to.be.an(Error); 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, @@ -1217,7 +1217,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4027); done(); - }.bind(this)); + }); this.connection.seq--; this.doc.submitPresence(p(1)); }.bind(this) @@ -1226,7 +1226,7 @@ describe('client presence', function() { 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, @@ -1244,14 +1244,14 @@ describe('client presence', function() { 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, @@ -1265,7 +1265,7 @@ describe('client presence', function() { } 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--; @@ -1279,7 +1279,7 @@ describe('client presence', function() { 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) { @@ -1295,14 +1295,14 @@ describe('client presence', function() { 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) { @@ -1317,7 +1317,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(-1); done(); - }.bind(this)); + }); this.doc.submitPresence(p(0)); }.bind(this) ], allDone); @@ -1325,7 +1325,7 @@ describe('client presence', function() { 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)), @@ -1351,7 +1351,7 @@ describe('client presence', function() { 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); @@ -1362,7 +1362,7 @@ describe('client presence', function() { expect(err).to.be.an(Error); expect(err.code).to.equal(4000); done(); - }.bind(this)); + }); // send an invalid op this.doc._submit({}, null); @@ -1372,7 +1372,7 @@ describe('client presence', function() { 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)), @@ -1406,7 +1406,7 @@ describe('client presence', function() { 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); @@ -1414,7 +1414,7 @@ describe('client presence', function() { 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) @@ -1435,7 +1435,7 @@ describe('client presence', function() { 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) { @@ -1443,10 +1443,10 @@ describe('client presence', function() { 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); @@ -1465,16 +1465,24 @@ describe('client presence', function() { }; } - 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 }; + 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/submit.js b/test/client/submit.js index f90ccc2da..592aa9eff 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1097,7 +1097,7 @@ module.exports = function() { done(); }); }); - + 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) { @@ -1109,7 +1109,7 @@ module.exports = function() { }); }); }); - + 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) {