+ );
+}
module.exports = Body;
diff --git a/examples/leaderboard/client/Leaderboard.jsx b/examples/leaderboard/client/Leaderboard.jsx
index a3356f7c..f530ed14 100644
--- a/examples/leaderboard/client/Leaderboard.jsx
+++ b/examples/leaderboard/client/Leaderboard.jsx
@@ -4,15 +4,18 @@ var React = require('react');
var _ = require('underscore');
var connection = require('./connection');
-var Leaderboard = React.createClass({
- getInitialState: function() {
- return {
+class Leaderboard extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
selectedPlayerId: null,
players: []
};
- },
+ this.handlePlayerSelected = this.handlePlayerSelected.bind(this);
+ this.handleAddPoints = this.handleAddPoints.bind(this);
+ }
- componentDidMount: function() {
+ componentDidMount() {
var comp = this;
var query = connection.createSubscribeQuery('players', {$sort: {score: -1}});
query.on('ready', update);
@@ -21,30 +24,30 @@ var Leaderboard = React.createClass({
function update() {
comp.setState({players: query.results});
}
- },
+ }
- componentWillUnmount: function() {
+ componentWillUnmount() {
query.destroy();
- },
+ }
- selectedPlayer: function() {
+ selectedPlayer() {
return _.find(this.state.players, function(x) {
return x.id === this.state.selectedPlayerId;
}.bind(this));
- },
+ }
- handlePlayerSelected: function(id) {
+ handlePlayerSelected(id) {
this.setState({selectedPlayerId: id});
- },
+ }
- handleAddPoints: function() {
+ handleAddPoints() {
var op = [{p: ['score'], na: 5}];
connection.get('players', this.state.selectedPlayerId).submitOp(op, function(err) {
if (err) { console.error(err); return; }
});
- },
+ }
- render: function() {
+ render() {
return (
@@ -54,7 +57,7 @@ var Leaderboard = React.createClass({
);
}
-});
+}
module.exports = Leaderboard;
diff --git a/examples/leaderboard/client/Player.jsx b/examples/leaderboard/client/Player.jsx
index 2580bd11..f7c08e3f 100644
--- a/examples/leaderboard/client/Player.jsx
+++ b/examples/leaderboard/client/Player.jsx
@@ -1,18 +1,18 @@
+var PropTypes = require('prop-types');
var React = require('react');
var classNames = require('classnames');
-var Player = React.createClass({
- propTypes: {
- doc: React.PropTypes.object.isRequired,
- onPlayerSelected: React.PropTypes.func.isRequired,
- selected: React.PropTypes.bool.isRequired
- },
+class Player extends React.Component {
+ constructor(props) {
+ super(props);
+ this.handleClick = this.handleClick.bind(this);
+ }
- handleClick: function(event) {
+ handleClick(event) {
this.props.onPlayerSelected(this.props.doc.id);
- },
+ }
- componentDidMount: function() {
+ componentDidMount() {
var comp = this;
var doc = comp.props.doc;
doc.subscribe();
@@ -22,13 +22,13 @@ var Player = React.createClass({
// `comp.props.doc.data` is now updated. re-render component.
comp.forceUpdate();
}
- },
+ }
- componentWillUnmount: function() {
+ componentWillUnmount() {
this.doc.unsubscribe();
- },
+ }
- render: function() {
+ render() {
var classes = {
'player': true,
'selected': this.props.selected
@@ -41,6 +41,12 @@ var Player = React.createClass({
);
}
-});
+}
+
+Player.propTypes = {
+ doc: PropTypes.object.isRequired,
+ onPlayerSelected: PropTypes.func.isRequired,
+ selected: PropTypes.bool.isRequired
+};
module.exports = Player;
diff --git a/examples/leaderboard/client/PlayerList.jsx b/examples/leaderboard/client/PlayerList.jsx
index 469ceef0..a7ea78b0 100644
--- a/examples/leaderboard/client/PlayerList.jsx
+++ b/examples/leaderboard/client/PlayerList.jsx
@@ -1,30 +1,29 @@
+var PropTypes = require('prop-types');
var React = require('react');
var Player = require('./Player.jsx');
var _ = require('underscore');
-var PlayerList = React.createClass({
- propTypes: {
- players: React.PropTypes.array.isRequired,
- selectedPlayerId: React.PropTypes.string
- },
+function PlayerList(props) {
+ var { players, selectedPlayerId } = props;
+ var other = _.omit(props, 'players', 'selectedPlayerId');
- render: function() {
- var { players, selectedPlayerId } = this.props;
- var other = _.omit(this.props, 'players', 'selectedPlayerId');
+ var playerNodes = players.map(function(player, index) {
+ var selected = selectedPlayerId === player.id;
- var playerNodes = players.map(function(player, index) {
- var selected = selectedPlayerId === player.id;
-
- return (
-
- );
- });
return (
-
- {playerNodes}
-
+
);
- }
-});
+ });
+ return (
+
+ {playerNodes}
+
+ );
+}
+
+PlayerList.propTypes = {
+ players: PropTypes.array.isRequired,
+ selectedPlayerId: PropTypes.string
+};
-module.exports = PlayerList;
\ No newline at end of file
+module.exports = PlayerList;
diff --git a/examples/leaderboard/client/PlayerSelector.jsx b/examples/leaderboard/client/PlayerSelector.jsx
index 2a61d425..b778693a 100644
--- a/examples/leaderboard/client/PlayerSelector.jsx
+++ b/examples/leaderboard/client/PlayerSelector.jsx
@@ -1,24 +1,23 @@
+var PropTypes = require('prop-types');
var React = require('react');
-var PlayerSelector = React.createClass({
- propTypes: {
- selectedPlayer: React.PropTypes.object
- },
+function PlayerSelector({ selectedPlayer, onAddPoints }) {
+ var node;
- render: function() {
- var node;
+ if (selectedPlayer) {
+ node =
+
{selectedPlayer.data.name}
+
Add 5 points
+
;
+ } else {
+ node =
Click a player to select
;
+ }
- if (this.props.selectedPlayer) {
- node =
-
{this.props.selectedPlayer.data.name}
-
Add 5 points
-
;
- } else {
- node =
Click a player to select
;
- }
+ return node;
+}
- return node;
- }
-});
+PlayerSelector.propTypes = {
+ selectedPlayer: PropTypes.object
+};
module.exports = PlayerSelector;
diff --git a/examples/leaderboard/client/connection.js b/examples/leaderboard/client/connection.js
index a3c03eaa..a9001050 100644
--- a/examples/leaderboard/client/connection.js
+++ b/examples/leaderboard/client/connection.js
@@ -1,6 +1,7 @@
+var ReconnectingWebSocket = require('reconnecting-websocket');
var sharedb = require('sharedb/lib/client');
// Expose a singleton WebSocket connection to ShareDB server
-var socket = new WebSocket('ws://' + window.location.host);
+var socket = new ReconnectingWebSocket('ws://' + window.location.host);
var connection = new sharedb.Connection(socket);
module.exports = connection;
diff --git a/examples/leaderboard/package.json b/examples/leaderboard/package.json
index 6ee5782c..705cf66b 100644
--- a/examples/leaderboard/package.json
+++ b/examples/leaderboard/package.json
@@ -1,10 +1,10 @@
{
"name": "sharedb-example-leaderboard",
- "version": "0.0.1",
+ "version": "1.0.0",
"description": "React Leaderboard backed by ShareDB",
"main": "server.js",
"scripts": {
- "build": "mkdir -p dist/ && ./node_modules/.bin/browserify -t [ babelify --presets [ react ] ] client/index.jsx -o dist/bundle.js",
+ "build": "mkdir -p static/dist/ && ./node_modules/.bin/browserify -t [ babelify --presets [ react ] ] client/index.jsx -o static/dist/bundle.js",
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server/index.js"
},
@@ -15,21 +15,21 @@
],
"license": "MIT",
"dependencies": {
+ "@teamwork/websocket-json-stream": "^2.0.0",
"classnames": "^2.2.5",
- "connect": "^3.4.1",
- "react": "^15.1.0",
- "react-dom": "^15.1.0",
- "serve-static": "^1.11.1",
+ "express": "^4.17.1",
+ "prop-types": "^15.7.2",
+ "react": "^16.11.0",
+ "react-dom": "^16.11.0",
+ "reconnecting-websocket": "^4.2.0",
"sharedb": "^1.0.0-beta",
"sharedb-mingo-memory": "^1.0.0-beta",
- "through2": "^2.0.1",
"underscore": "^1.8.3",
- "@teamwork/websocket-json-stream": "^2.0.0",
- "ws": "^1.1.0"
+ "ws": "^7.2.0"
},
"devDependencies": {
"babel-preset-react": "^6.5.0",
"babelify": "^7.3.0",
- "browserify": "^13.0.1"
+ "browserify": "^16.5.0"
}
}
diff --git a/examples/leaderboard/server/index.js b/examples/leaderboard/server/index.js
index 6aebe997..242d9dd3 100644
--- a/examples/leaderboard/server/index.js
+++ b/examples/leaderboard/server/index.js
@@ -1,7 +1,6 @@
var http = require('http');
var ShareDB = require('sharedb');
-var connect = require('connect');
-var serveStatic = require('serve-static');
+var express = require('express');
var ShareDBMingoMemory = require('sharedb-mingo-memory');
var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
var WebSocket = require('ws');
@@ -10,8 +9,8 @@ var WebSocket = require('ws');
var share = new ShareDB({db: new ShareDBMingoMemory()});
// Create a WebSocket server
-var app = connect();
-app.use(serveStatic('.'));
+var app = express();
+app.use(express.static('static'));
var server = http.createServer(app);
var wss = new WebSocket.Server({server: server});
server.listen(8080);
diff --git a/examples/leaderboard/index.html b/examples/leaderboard/static/index.html
similarity index 100%
rename from examples/leaderboard/index.html
rename to examples/leaderboard/static/index.html
diff --git a/examples/leaderboard/leaderboard.css b/examples/leaderboard/static/leaderboard.css
similarity index 100%
rename from examples/leaderboard/leaderboard.css
rename to examples/leaderboard/static/leaderboard.css
diff --git a/examples/rich-text/client.js b/examples/rich-text/client.js
index 68541137..5785357d 100644
--- a/examples/rich-text/client.js
+++ b/examples/rich-text/client.js
@@ -1,10 +1,11 @@
+var ReconnectingWebSocket = require('reconnecting-websocket');
var sharedb = require('sharedb/lib/client');
var richText = require('rich-text');
var Quill = require('quill');
sharedb.types.register(richText.type);
// Open WebSocket connection to ShareDB server
-var socket = new WebSocket('ws://' + window.location.host);
+var socket = new ReconnectingWebSocket('ws://' + window.location.host);
var connection = new sharedb.Connection(socket);
// For testing reconnection
@@ -12,7 +13,7 @@ window.disconnect = function() {
connection.close();
};
window.connect = function() {
- var socket = new WebSocket('ws://' + window.location.host);
+ var socket = new ReconnectingWebSocket('ws://' + window.location.host);
connection.bindToSocket(socket);
};
diff --git a/examples/rich-text/package.json b/examples/rich-text/package.json
index 2249e29c..461a2260 100644
--- a/examples/rich-text/package.json
+++ b/examples/rich-text/package.json
@@ -1,6 +1,6 @@
{
"name": "sharedb-example-rich-text",
- "version": "0.0.1",
+ "version": "1.0.0",
"description": "A simple rich-text editor example based on Quill and ShareDB",
"main": "server.js",
"scripts": {
@@ -14,14 +14,15 @@
],
"license": "MIT",
"dependencies": {
- "express": "^4.14.0",
- "quill": "^1.0.0-beta.11",
- "rich-text": "^3.0.1",
- "sharedb": "^1.0.0-beta",
"@teamwork/websocket-json-stream": "^2.0.0",
- "ws": "^1.1.0"
+ "express": "^4.17.1",
+ "quill": "^1.3.7",
+ "reconnecting-websocket": "^4.2.0",
+ "rich-text": "^4.0.0",
+ "sharedb": "^1.0.0-beta",
+ "ws": "^7.2.0"
},
"devDependencies": {
- "browserify": "^13.0.1"
+ "browserify": "^16.5.0"
}
}
diff --git a/examples/textarea/client.js b/examples/textarea/client.js
index 4cbf5520..c5111c2b 100644
--- a/examples/textarea/client.js
+++ b/examples/textarea/client.js
@@ -2,8 +2,8 @@ var sharedb = require('sharedb/lib/client');
var StringBinding = require('sharedb-string-binding');
// Open WebSocket connection to ShareDB server
-var WebSocket = require('reconnecting-websocket');
-var socket = new WebSocket('ws://' + window.location.host);
+var ReconnectingWebSocket = require('reconnecting-websocket');
+var socket = new ReconnectingWebSocket('ws://' + window.location.host);
var connection = new sharedb.Connection(socket);
var element = document.querySelector('textarea');
diff --git a/examples/textarea/package.json b/examples/textarea/package.json
index 1a91a3aa..49c40f12 100644
--- a/examples/textarea/package.json
+++ b/examples/textarea/package.json
@@ -1,6 +1,6 @@
{
"name": "sharedb-example-textarea",
- "version": "0.0.1",
+ "version": "1.0.0",
"description": "A simple client/server app using ShareDB and WebSockets",
"main": "server.js",
"scripts": {
@@ -14,14 +14,14 @@
],
"license": "MIT",
"dependencies": {
- "express": "^4.14.0",
- "reconnecting-websocket": "^3.0.3",
+ "@teamwork/websocket-json-stream": "^2.0.0",
+ "express": "^4.17.1",
+ "reconnecting-websocket": "^4.2.0",
"sharedb": "^1.0.0-beta",
"sharedb-string-binding": "^1.0.0",
- "@teamwork/websocket-json-stream": "^2.0.0",
- "ws": "^1.1.0"
+ "ws": "^7.2.0"
},
"devDependencies": {
- "browserify": "^13.0.1"
+ "browserify": "^16.5.0"
}
}
diff --git a/lib/agent.js b/lib/agent.js
index f548f4d7..9deb83d1 100644
--- a/lib/agent.js
+++ b/lib/agent.js
@@ -1,6 +1,6 @@
var hat = require('hat');
-var util = require('./util');
var types = require('./types');
+var util = require('./util');
var logger = require('./logger');
/**
@@ -105,8 +105,7 @@ Agent.prototype._subscribeToStream = function(collection, id, stream) {
logger.error('Doc subscription stream error', collection, id, data.error);
return;
}
- if (agent._isOwnOp(collection, data)) return;
- agent._sendOp(collection, id, data);
+ agent._onOp(collection, id, data);
});
stream.on('end', function() {
// The op stream is done sending, so release its reference
@@ -149,13 +148,41 @@ Agent.prototype._subscribeToQuery = function(emitter, queryId, collection, query
emitter.onOp = function(op) {
var id = op.d;
- if (agent._isOwnOp(collection, op)) return;
- agent._sendOp(collection, id, op);
+ agent._onOp(collection, id, op);
};
emitter._open();
};
+Agent.prototype._onOp = function(collection, id, op) {
+ if (this._isOwnOp(collection, op)) return;
+
+ // Ops emitted here are coming directly from pubsub, which emits the same op
+ // object to listeners without making a copy. The pattern in middleware is to
+ // manipulate the passed in object, and projections are implemented the same
+ // way currently.
+ //
+ // Deep copying the op would be safest, but deep copies are very expensive,
+ // especially over arbitrary objects. This function makes a shallow copy of an
+ // op, and it requires that projections and any user middleware copy deep
+ // properties as needed when they modify the op.
+ //
+ // Polling of query subscriptions is determined by the same op objects. As a
+ // precaution against op middleware breaking query subscriptions, we delay
+ // before calling into projection and middleware code
+ var agent = this;
+ process.nextTick(function() {
+ var copy = shallowCopy(op);
+ agent.backend.sanitizeOp(agent, collection, id, copy, function(err) {
+ if (err) {
+ logger.error('Error sanitizing op emitted from subscription', collection, id, copy, err);
+ return;
+ }
+ agent._sendOp(collection, id, copy);
+ });
+ });
+};
+
Agent.prototype._isOwnOp = function(collection, op) {
// Detect ops from this client on the same projection. Since the client sent
// these in, the submit reply will be sufficient and we can silently ignore
@@ -186,12 +213,17 @@ Agent.prototype._sendOp = function(collection, id, op) {
this.send(message);
};
-
Agent.prototype._sendOps = function(collection, id, ops) {
for (var i = 0; i < ops.length; i++) {
this._sendOp(collection, id, ops[i]);
}
};
+Agent.prototype._sendOpsBulk = function(collection, opsMap) {
+ for (var id in opsMap) {
+ var ops = opsMap[id];
+ this._sendOps(collection, id, ops);
+ }
+};
function getReplyErrorObject(err) {
if (typeof err === 'string') {
@@ -201,7 +233,7 @@ function getReplyErrorObject(err) {
};
} else {
if (err.stack) {
- logger.warn(err.stack);
+ logger.info(err.stack);
}
return {
code: err.code,
@@ -316,7 +348,8 @@ Agent.prototype._handleMessage = function(request, callback) {
case 'u':
return this._unsubscribe(request.c, request.d, callback);
case 'op':
- var op = this._createOp(request);
+ // Normalize the properties submitted
+ var op = createClientOp(request, this.clientId);
if (!op) return callback({code: 4000, message: 'Invalid op message'});
return this._submit(request.c, request.d, op, callback);
case 'nf':
@@ -493,10 +526,7 @@ Agent.prototype._fetchBulkOps = function(collection, versions, callback) {
var agent = this;
this.backend.getOpsBulk(this, collection, versions, null, function(err, opsMap) {
if (err) return callback(err);
- for (var id in opsMap) {
- var ops = opsMap[id];
- agent._sendOps(collection, id, ops);
- }
+ agent._sendOpsBulk(collection, opsMap);
callback();
});
};
@@ -505,8 +535,18 @@ Agent.prototype._subscribe = function(collection, id, version, callback) {
// If the version is specified, catch the client up by sending all ops
// since the specified version
var agent = this;
- this.backend.subscribe(this, collection, id, version, function(err, stream, snapshot) {
+ this.backend.subscribe(this, collection, id, version, function(err, stream, snapshot, ops) {
if (err) return callback(err);
+ // If we're subscribing from a known version, send any ops committed since
+ // the requested version to bring the client's doc up to date
+ if (ops) {
+ agent._sendOps(collection, id, ops);
+ }
+ // In addition, ops may already be queued on the stream by pubsub.
+ // Subscribe is called before the ops or snapshot are fetched, so it is
+ // possible that some ops may be duplicates. Clients should ignore any
+ // duplicate ops they may receive. This will flush ops already queued and
+ // subscribe to ongoing ops from the stream
agent._subscribeToStream(collection, id, stream);
// Snapshot is returned only when subscribing from a null version.
// Otherwise, ops will have been pushed into the stream
@@ -519,9 +559,13 @@ Agent.prototype._subscribe = function(collection, id, version, callback) {
};
Agent.prototype._subscribeBulk = function(collection, versions, callback) {
+ // See _subscribe() above. This function's logic should match but in bulk
var agent = this;
- this.backend.subscribeBulk(this, collection, versions, function(err, streams, snapshotMap) {
+ this.backend.subscribeBulk(this, collection, versions, function(err, streams, snapshotMap, opsMap) {
if (err) return callback(err);
+ if (opsMap) {
+ agent._sendOpsBulk(collection, opsMap);
+ }
for (var id in streams) {
agent._subscribeToStream(collection, id, streams[id]);
}
@@ -572,45 +616,58 @@ Agent.prototype._submit = function(collection, id, op, callback) {
});
};
-function CreateOp(src, seq, v, create) {
+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);
+};
+
+
+function createClientOp(request, clientId) {
+ // src can be provided if it is not the same as the current agent,
+ // such as a resubmission after a reconnect, but it usually isn't needed
+ var src = request.src || clientId;
+ // c, d, and m arguments are intentionally undefined. These are set later
+ return (request.op) ? new EditOp(src, request.seq, request.v, request.op) :
+ (request.create) ? new CreateOp(src, request.seq, request.v, request.create) :
+ (request.del) ? new DeleteOp(src, request.seq, request.v, request.del) :
+ undefined;
+}
+
+function shallowCopy(object) {
+ var out = {};
+ for (var key in object) {
+ out[key] = object[key];
+ }
+ return out;
+}
+
+function CreateOp(src, seq, v, create, c, d, m) {
this.src = src;
this.seq = seq;
this.v = v;
this.create = create;
- this.m = null;
+ this.c = c;
+ this.d = d;
+ this.m = m;
}
-function EditOp(src, seq, v, op) {
+function EditOp(src, seq, v, op, c, d, m) {
this.src = src;
this.seq = seq;
this.v = v;
this.op = op;
- this.m = null;
+ this.c = c;
+ this.d = d;
+ this.m = m;
}
-function DeleteOp(src, seq, v, del) {
+function DeleteOp(src, seq, v, del, c, d, m) {
this.src = src;
this.seq = seq;
this.v = v;
this.del = del;
- this.m = null;
+ this.c = c;
+ this.d = d;
+ this.m = m;
}
-// Normalize the properties submitted
-Agent.prototype._createOp = function(request) {
- // src can be provided if it is not the same as the current agent,
- // such as a resubmission after a reconnect, but it usually isn't needed
- var src = request.src || this.clientId;
- if (request.op) {
- return new EditOp(src, request.seq, request.v, request.op);
- } else if (request.create) {
- return new CreateOp(src, request.seq, request.v, request.create);
- } else if (request.del) {
- return new DeleteOp(src, request.seq, request.v, request.del);
- }
-};
-
-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 bdac897f..42c20bd0 100644
--- a/lib/backend.js
+++ b/lib/backend.js
@@ -40,8 +40,8 @@ module.exports = Backend;
emitter.mixin(Backend);
Backend.prototype.MIDDLEWARE_ACTIONS = {
- // An operation was successfully submitted to the database.
- afterSubmit: 'afterSubmit',
+ // An operation was successfully written to the database.
+ afterWrite: 'afterWrite',
// An operation is about to be applied to a snapshot before being committed to the database
apply: 'apply',
// An operation was applied to a snapshot; The operation and new snapshot are about to be written to the database.
@@ -154,7 +154,7 @@ Backend.prototype.use = function(action, fn) {
for (var i = 0; i < action.length; i++) {
this.use(action[i], fn);
}
- return;
+ return this;
}
var fns = this.middleware[action] || (this.middleware[action] = []);
fns.push(fn);
@@ -199,7 +199,7 @@ Backend.prototype.submit = function(agent, index, id, op, options, callback) {
if (err) return callback(err);
request.submit(function(err) {
if (err) return callback(err);
- backend.trigger(backend.MIDDLEWARE_ACTIONS.afterSubmit, agent, request, function(err) {
+ backend.trigger(backend.MIDDLEWARE_ACTIONS.afterWrite, agent, request, function(err) {
if (err) return callback(err);
backend._sanitizeOps(agent, request.projection, request.collection, id, request.ops, function(err) {
if (err) return callback(err);
@@ -211,6 +211,12 @@ Backend.prototype.submit = function(agent, index, id, op, options, callback) {
});
};
+Backend.prototype.sanitizeOp = function(agent, index, id, op, callback) {
+ var projection = this.projections[index];
+ var collection = (projection) ? projection.target : index;
+ this._sanitizeOp(agent, projection, collection, id, op, callback);
+};
+
Backend.prototype._sanitizeOp = function(agent, projection, collection, id, op, callback) {
if (projection) {
try {
@@ -265,6 +271,28 @@ Backend.prototype._getSnapshotsFromMap = function(ids, snapshotMap) {
return snapshots;
};
+Backend.prototype._getSanitizedOps = function(agent, projection, collection, id, from, to, opsOptions, callback) {
+ var backend = this;
+ 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);
+ callback(null, ops);
+ });
+ });
+};
+
+Backend.prototype._getSanitizedOpsBulk = function(agent, projection, collection, fromMap, toMap, opsOptions, callback) {
+ var backend = this;
+ 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);
+ callback(null, opsMap);
+ });
+ });
+};
+
// 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) {
@@ -285,13 +313,10 @@ Backend.prototype.getOps = function(agent, index, id, from, to, options, callbac
to: to
};
var opsOptions = options && options.opsOptions;
- backend.db.getOps(collection, id, from, to, opsOptions, function(err, ops) {
+ backend._getSanitizedOps(agent, projection, 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);
- backend.emit('timing', 'getOps', Date.now() - start, request);
- callback(err, ops);
- });
+ backend.emit('timing', 'getOps', Date.now() - start, request);
+ callback(null, ops);
});
};
@@ -312,13 +337,10 @@ Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, options, c
toMap: toMap
};
var opsOptions = options && options.opsOptions;
- backend.db.getOpsBulk(collection, fromMap, toMap, opsOptions, function(err, opsMap) {
+ backend._getSanitizedOpsBulk(agent, projection, 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);
- backend.emit('timing', 'getOpsBulk', Date.now() - start, request);
- callback(err, opsMap);
- });
+ backend.emit('timing', 'getOpsBulk', Date.now() - start, request);
+ callback(null, opsMap);
});
};
@@ -419,21 +441,25 @@ Backend.prototype.subscribe = function(agent, index, id, version, options, callb
};
backend.pubsub.subscribe(channel, function(err, stream) {
if (err) return callback(err);
- stream.initProjection(backend, agent, projection);
if (version == null) {
// Subscribing from null means that the agent doesn't have a document
// and needs to fetch it as well as subscribing
backend.fetch(agent, index, id, function(err, snapshot) {
- if (err) return callback(err);
+ if (err) {
+ stream.destroy();
+ return callback(err);
+ }
backend.emit('timing', 'subscribe.snapshot', Date.now() - start, request);
callback(null, stream, snapshot);
});
} else {
- backend.db.getOps(collection, id, version, null, null, function(err, ops) {
- if (err) return callback(err);
- stream.pushOps(collection, id, ops);
+ backend._getSanitizedOps(agent, projection, collection, id, version, null, null, function(err, ops) {
+ if (err) {
+ stream.destroy();
+ return callback(err);
+ }
backend.emit('timing', 'subscribe.ops', Date.now() - start, request);
- callback(null, stream);
+ callback(null, stream, null, ops);
});
}
});
@@ -457,7 +483,6 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) {
var channel = backend.getDocChannel(collection, id);
backend.pubsub.subscribe(channel, function(err, stream) {
if (err) return eachCb(err);
- stream.initProjection(backend, agent, projection);
streams[id] = stream;
eachCb();
});
@@ -478,17 +503,13 @@ Backend.prototype.subscribeBulk = function(agent, index, versions, callback) {
});
} else {
// If a versions map, get ops since requested versions
- backend.db.getOpsBulk(collection, versions, null, null, function(err, opsMap) {
+ backend._getSanitizedOpsBulk(agent, projection, collection, versions, null, null, function(err, opsMap) {
if (err) {
destroyStreams(streams);
return callback(err);
}
- for (var id in opsMap) {
- var ops = opsMap[id];
- streams[id].pushOps(collection, id, ops);
- }
backend.emit('timing', 'subscribeBulk.ops', Date.now() - start, request);
- callback(null, streams);
+ callback(null, streams, null, opsMap);
});
}
});
@@ -530,7 +551,6 @@ Backend.prototype.querySubscribe = function(agent, index, query, options, callba
}
backend.pubsub.subscribe(request.channel, function(err, stream) {
if (err) return callback(err);
- stream.initProjection(backend, agent, request.projection);
if (options.ids) {
var queryEmitter = new QueryEmitter(request, stream, options.ids);
backend.emit('timing', 'querySubscribe.reconnect', Date.now() - start, request);
diff --git a/lib/client/connection.js b/lib/client/connection.js
index 779b7138..521a8ceb 100644
--- a/lib/client/connection.js
+++ b/lib/client/connection.js
@@ -293,8 +293,15 @@ 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);
@@ -303,7 +310,13 @@ Connection.prototype._setState = function(newState, reason) {
this.state = newState;
this.canSend = (newState === 'connected');
- if (newState === 'disconnected' || newState === 'stopped' || newState === 'closed') this._reset();
+ if (
+ newState === 'disconnected' ||
+ newState === 'stopped' ||
+ newState === 'closed'
+ ) {
+ this._reset();
+ }
// Group subscribes together to help server make more efficient calls
this.startBulk();
diff --git a/lib/op-stream.js b/lib/op-stream.js
index 3368c0dd..a2608fd2 100644
--- a/lib/op-stream.js
+++ b/lib/op-stream.js
@@ -5,12 +5,7 @@ var util = require('./util');
// Stream of operations. Subscribe returns one of these
function OpStream() {
Readable.call(this, {objectMode: true});
-
this.id = null;
- this.backend = null;
- this.agent = null;
- this.projection = null;
-
this.open = true;
}
module.exports = OpStream;
@@ -24,33 +19,15 @@ inherits(OpStream, Readable);
// themselves.
OpStream.prototype._read = util.doNothing;
-OpStream.prototype.initProjection = function(backend, agent, projection) {
- this.backend = backend;
- this.agent = agent;
- this.projection = projection;
-};
-
-OpStream.prototype.pushOp = function(collection, id, op) {
- if (this.backend) {
- var stream = this;
- this.backend._sanitizeOp(this.agent, this.projection, collection, id, op, function(err) {
- if (!stream.open) return;
- stream.push(err ? {error: err} : op);
- });
- } else {
- // Ignore any messages after unsubscribe
- if (!this.open) return;
- this.push(op);
- }
-};
-
-OpStream.prototype.pushOps = function(collection, id, ops) {
- for (var i = 0; i < ops.length; i++) {
- this.pushOp(collection, id, ops[i]);
- }
+OpStream.prototype.pushData = function(data) {
+ // Ignore any messages after unsubscribe
+ if (!this.open) return;
+ // This data gets consumed in Agent#_subscribeToStream
+ this.push(data);
};
OpStream.prototype.destroy = function() {
+ // Only close stream once
if (!this.open) return;
this.open = false;
diff --git a/lib/pubsub/index.js b/lib/pubsub/index.js
index ff02ac00..e454a694 100644
--- a/lib/pubsub/index.js
+++ b/lib/pubsub/index.js
@@ -90,8 +90,7 @@ PubSub.prototype._emit = function(channel, data) {
var channelStreams = this.streams[channel];
if (channelStreams) {
for (var id in channelStreams) {
- var copy = shallowCopy(data);
- channelStreams[id].pushOp(copy.c, copy.d, copy);
+ channelStreams[id].pushData(data);
}
}
};
@@ -129,11 +128,3 @@ PubSub.prototype._removeStream = function(channel, stream) {
this._unsubscribe(channel, this._defaultCallback);
};
-
-function shallowCopy(object) {
- var out = {};
- for (var key in object) {
- out[key] = object[key];
- }
- return out;
-}
diff --git a/lib/query-emitter.js b/lib/query-emitter.js
index fe5c5b01..cfa33884 100644
--- a/lib/query-emitter.js
+++ b/lib/query-emitter.js
@@ -1,5 +1,5 @@
var arraydiff = require('arraydiff');
-var deepEquals = require('deep-is');
+var deepEqual = require('fast-deep-equal');
var ShareDBError = require('./error');
var util = require('./util');
@@ -63,7 +63,12 @@ QueryEmitter.prototype._emitTiming = function(action, start) {
};
QueryEmitter.prototype._update = function(op) {
+ // Note that `op` should not be projected or sanitized yet. It's possible for
+ // a query to filter on a field that's not in the projection. skipPoll checks
+ // to see if an op could possibly affect a query, so it should get passed the
+ // full op. The onOp listener function must call backend.sanitizeOp()
var id = op.d;
+ var pollCallback = this._defaultCallback;
// Check if the op's id matches the query before updating the query results
// and send it through immediately if it does. The current snapshot
@@ -90,24 +95,35 @@ QueryEmitter.prototype._update = function(op) {
// that removed the doc from the query to cause the client-side computed
// list to update.
if (this.ids.indexOf(id) !== -1) {
- this.onOp(op);
+ var emitter = this;
+ pollCallback = function(err) {
+ // Send op regardless of polling error. Clients handle subscription to ops
+ // on the documents that currently match query results independently from
+ // updating which docs match the query
+ emitter.onOp(op);
+ if (err) emitter.onError(err);
+ };
}
// Ignore if the database or user function says we don't need to poll
try {
- if (this.db.skipPoll(this.collection, id, op, this.query)) return this._defaultCallback();
- if (this.skipPoll(this.collection, id, op, this.query)) return this._defaultCallback();
+ if (
+ this.db.skipPoll(this.collection, id, op, this.query) ||
+ this.skipPoll(this.collection, id, op, this.query)
+ ) {
+ return pollCallback();
+ }
} catch (err) {
- return this._defaultCallback(err);
+ return pollCallback(err);
}
if (this.canPollDoc) {
// We can query against only the document that was modified to see if the
// op has changed whether or not it matches the results
- this.queryPollDoc(id, this._defaultCallback);
+ this.queryPollDoc(id, pollCallback);
} else {
// We need to do a full poll of the query, because the query uses limits,
// sorts, or something special
- this.queryPoll(this._defaultCallback);
+ this.queryPoll(pollCallback);
}
};
@@ -173,7 +189,7 @@ QueryEmitter.prototype.queryPoll = function(callback) {
emitter._emitTiming('queryEmitter.poll', start);
// Be nice to not have to do this in such a brute force way
- if (!deepEquals(emitter.extra, extra)) {
+ if (!deepEqual(emitter.extra, extra)) {
emitter.extra = extra;
emitter.onExtra(extra);
}
diff --git a/package.json b/package.json
index b30da817..56a6d272 100644
--- a/package.json
+++ b/package.json
@@ -1,29 +1,29 @@
{
"name": "sharedb",
- "version": "1.0.0-beta.23",
+ "version": "1.0.0-beta.27",
"description": "JSON OT database backend",
"main": "lib/index.js",
"dependencies": {
"arraydiff": "^0.1.1",
"async": "^1.4.2",
- "deep-is": "^0.1.3",
+ "fast-deep-equal": "^2.0.1",
"hat": "0.0.3",
"make-error": "^1.1.1",
"ot-json0": "^1.0.1"
},
"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",
- "mocha": "^5.2.0",
- "sinon": "^6.1.5"
+ "chai": "^4.2.0",
+ "coveralls": "^3.0.7",
+ "eslint": "^6.5.1",
+ "eslint-config-google": "^0.14.0",
+ "lolex": "^5.1.1",
+ "mocha": "^6.2.2",
+ "nyc": "^14.1.1",
+ "sinon": "^7.5.0"
},
"scripts": {
"test": "./node_modules/.bin/mocha && npm run lint",
- "test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha",
+ "test-cover": "node_modules/nyc/bin/nyc.js --temp-dir=coverage -r text -r lcov node_modules/mocha/bin/_mocha && npm run lint",
"lint": "./node_modules/.bin/eslint --ignore-path .gitignore '**/*.js'",
"lint:fix": "npm run lint -- --fix"
},
diff --git a/test/BasicQueryableMemoryDB.js b/test/BasicQueryableMemoryDB.js
new file mode 100644
index 00000000..85d1ca51
--- /dev/null
+++ b/test/BasicQueryableMemoryDB.js
@@ -0,0 +1,91 @@
+var MemoryDB = require('../lib/db/memory');
+
+module.exports = BasicQueryableMemoryDB;
+
+// Extension of MemoryDB that supports query filters and sorts on simple
+// top-level properties, which is enough for the core ShareDB tests on
+// query subscription updating.
+function BasicQueryableMemoryDB() {
+ MemoryDB.apply(this, arguments);
+}
+BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype);
+BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB;
+
+BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query) {
+ if (query.filter) {
+ snapshots = snapshots.filter(function(snapshot) {
+ return querySnapshot(snapshot, query);
+ });
+ }
+
+ 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};
+};
+
+BasicQueryableMemoryDB.prototype.queryPollDoc = function(collection, id, query, options, callback) {
+ var db = this;
+ process.nextTick(function() {
+ var snapshot = db._getSnapshotSync(collection, id);
+ try {
+ var matches = querySnapshot(snapshot, query);
+ } catch (err) {
+ return callback(err);
+ }
+ callback(null, matches);
+ });
+};
+
+BasicQueryableMemoryDB.prototype.canPollDoc = function(collection, query) {
+ return !query.sort;
+};
+
+function querySnapshot(snapshot, query) {
+ // Never match uncreated or deleted snapshots
+ if (snapshot.type == null) return false;
+ // Match any snapshot when there is no query filter
+ if (!query.filter) return true;
+ // Check that each property in the filter equals the snapshot data
+ 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;
+}
+
+// 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;
+ };
+}
diff --git a/test/backend.js b/test/backend.js
index 991c6716..c6177d05 100644
--- a/test/backend.js
+++ b/test/backend.js
@@ -1,5 +1,5 @@
var Backend = require('../lib/backend');
-var expect = require('expect.js');
+var expect = require('chai').expect;
describe('Backend', function() {
var backend;
@@ -39,8 +39,8 @@ describe('Backend', function() {
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();
+ expect(ops[0].m).to.be.ok;
+ expect(ops[1].m).to.be.ok;
done();
});
});
@@ -64,7 +64,7 @@ describe('Backend', function() {
};
backend.fetch(null, 'books', '1984', options, function(error, doc) {
if (error) return done(error);
- expect(doc.m).to.be.ok();
+ expect(doc.m).to.be.ok;
done();
});
});
@@ -74,7 +74,7 @@ describe('Backend', 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(stream.open).to.equal(true);
expect(snapshot.data).to.eql({
title: '1984',
author: 'George Orwell'
@@ -95,7 +95,7 @@ describe('Backend', function() {
opsOptions: {metadata: true}
};
backend.subscribe(null, 'books', '1984', null, options, function(error) {
- expect(error.code).to.be(4025);
+ expect(error.code).to.equal(4025);
done();
});
});
diff --git a/test/client/connection.js b/test/client/connection.js
index b38ff53f..a7a6cd82 100644
--- a/test/client/connection.js
+++ b/test/client/connection.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var Backend = require('../../lib/backend');
var Connection = require('../../lib/client/connection');
@@ -69,7 +69,7 @@ describe('client connection', function() {
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);
+ expect(newStream).to.not.equal(originalStream);
connection.close();
done();
});
diff --git a/test/client/doc.js b/test/client/doc.js
index 9b5316e3..fd0e255a 100644
--- a/test/client/doc.js
+++ b/test/client/doc.js
@@ -1,5 +1,5 @@
var Backend = require('../../lib/backend');
-var expect = require('expect.js');
+var expect = require('chai').expect;
var util = require('../util');
describe('Doc', function() {
@@ -229,7 +229,7 @@ describe('Doc', function() {
it('succeeds with valid op', function(done) {
var doc = this.doc;
doc.create({name: 'Scooby Doo'}, function(error) {
- expect(error).to.not.be.ok();
+ expect(error).to.not.exist;
// 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}];
@@ -244,12 +244,12 @@ describe('Doc', function() {
it('fails with invalid op', function(done) {
var doc = this.doc;
doc.create({name: 'Scooby Doo'}, function(error) {
- expect(error).to.not.be.ok();
+ expect(error).to.not.exist;
// 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();
+ expect(error).instanceOf(Error);
done();
});
});
@@ -286,7 +286,7 @@ describe('Doc', function() {
util.callInSeries([
function(next) {
doc.submitOp(invalidOp, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
next();
});
},
diff --git a/test/client/pending.js b/test/client/pending.js
index 2b22c7a0..bfe84ddd 100644
--- a/test/client/pending.js
+++ b/test/client/pending.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var Backend = require('../../lib/backend');
describe('client connection', function() {
diff --git a/test/client/projections.js b/test/client/projections.js
index 77359563..8b06c9d0 100644
--- a/test/client/projections.js
+++ b/test/client/projections.js
@@ -1,7 +1,10 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var util = require('../util');
-module.exports = function() {
+module.exports = function(options) {
+ var getQuery = options.getQuery;
+ var matchAllQuery = getQuery({query: {}});
+
describe('client projections', function() {
beforeEach(function(done) {
this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true});
@@ -26,7 +29,7 @@ module.exports = function() {
['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
it('snapshot ' + method, function(done) {
var connection2 = this.backend.connect();
- connection2[method]('dogs_summary', {}, null, function(err, results) {
+ connection2[method]('dogs_summary', matchAllQuery, null, function(err, results) {
if (err) return done(err);
expect(results.length).eql(1);
expect(results[0].data).eql({age: 3, owner: {name: 'jim'}});
@@ -139,7 +142,7 @@ module.exports = function() {
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) {
+ connection2.createFetchQuery('dogs_summary', matchAllQuery, null, function(err) {
if (err) return done(err);
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
@@ -156,7 +159,7 @@ module.exports = function() {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
- connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
+ connection2.createSubscribeQuery('dogs_summary', matchAllQuery, null, function(err) {
if (err) return done(err);
fido.on('op', function() {
expect(fido.data).eql(expected);
@@ -194,7 +197,7 @@ module.exports = function() {
function test(trigger, callback) {
var connection = this.connection;
var connection2 = this.backend.connect();
- var query = connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
+ var query = connection2.createSubscribeQuery('dogs_summary', matchAllQuery, null, function(err) {
if (err) return callback(err);
query.on('insert', function() {
callback(null, query.results);
@@ -203,6 +206,30 @@ module.exports = function() {
});
}
queryUpdateTests(test);
+
+ it('query subscribe on projection will update based on fields not in projection', function(done) {
+ // Create a query on a field not in the projection. The query doesn't
+ // match the doc created in beforeEach
+ var fido = this.connection.get('dogs', 'fido');
+ var connection2 = this.backend.connect();
+ var dbQuery = getQuery({query: {color: 'black'}});
+ var query = connection2.createSubscribeQuery('dogs_summary', dbQuery, null, function(err, results) {
+ if (err) return done(err);
+ expect(results).to.have.length(0);
+
+ // Submit an op on a field not in the projection, where the op should
+ // cause the doc to be included in the query results
+ fido.submitOp({p: ['color'], od: 'gold', oi: 'black'}, null, function(err) {
+ if (err) return done(err);
+ setTimeout(function() {
+ expect(query.results).to.have.length(1);
+ expect(query.results[0].id).to.equal('fido');
+ expect(query.results[0].data).to.eql({age: 3, owner: {name: 'jim'}});
+ done();
+ }, 10);
+ });
+ });
+ });
});
describe('fetch query', function() {
@@ -211,7 +238,7 @@ module.exports = function() {
var connection2 = this.backend.connect();
trigger(connection, function(err) {
if (err) return callback(err);
- connection2.createFetchQuery('dogs_summary', {}, null, callback);
+ connection2.createFetchQuery('dogs_summary', matchAllQuery, null, callback);
});
}
queryUpdateTests(test);
@@ -240,7 +267,7 @@ module.exports = function() {
projected.fetch(function(err) {
if (err) return done(err);
projected.submitOp(op, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}});
@@ -318,7 +345,7 @@ module.exports = function() {
var projected = this.backend.connect().get('dogs_summary', 'spot');
var data = {age: 5, foo: 'bar'};
projected.create(data, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
diff --git a/test/client/query-subscribe.js b/test/client/query-subscribe.js
index 166c859e..8667fc81 100644
--- a/test/client/query-subscribe.js
+++ b/test/client/query-subscribe.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var async = require('async');
var util = require('../util');
@@ -83,6 +83,40 @@ module.exports = function(options) {
});
});
+ it('subscribed query removes document from results before sending delete op to other clients', function(done) {
+ var connection1 = this.backend.connect();
+ var connection2 = this.backend.connect();
+ var matchAllDbQuery = this.matchAllDbQuery;
+ async.parallel([
+ function(cb) {
+ connection1.get('dogs', 'fido').create({age: 3}, cb);
+ },
+ function(cb) {
+ connection1.get('dogs', 'spot').create({age: 5}, cb);
+ }
+ ], function(err) {
+ if (err) return done(err);
+ var query = connection2.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
+ if (err) return done(err);
+ connection1.get('dogs', 'fido').del();
+ });
+ var removed = false;
+ connection2.get('dogs', 'fido').on('del', function() {
+ expect(removed).equal(true);
+ done();
+ });
+ query.on('remove', function(docs, index) {
+ removed = true;
+ expect(util.pluck(docs, 'id')).eql(['fido']);
+ expect(util.pluck(docs, 'data')).eql([{age: 3}]);
+ 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}]);
+ });
+ });
+ });
+
it('subscribed query does not get updated after destroyed', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
@@ -202,7 +236,9 @@ module.exports = function(options) {
});
query.on('remove', function(docs) {
expect(util.pluck(docs, 'id')).eql(['fido']);
- expect(util.pluck(docs, 'data')).eql([undefined]);
+ // We don't assert the value of data, because the del op could be
+ // applied by the client before or after the query result is removed.
+ // Order of ops & query result updates is not currently guaranteed
finish();
});
});
diff --git a/test/client/query.js b/test/client/query.js
index 785c5521..b6efc1a2 100644
--- a/test/client/query.js
+++ b/test/client/query.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var async = require('async');
var util = require('../util');
diff --git a/test/client/snapshot-timestamp-request.js b/test/client/snapshot-timestamp-request.js
index 8d7cc4bf..445c53d8 100644
--- a/test/client/snapshot-timestamp-request.js
+++ b/test/client/snapshot-timestamp-request.js
@@ -1,5 +1,5 @@
var Backend = require('../../lib/backend');
-var expect = require('expect.js');
+var expect = require('chai').expect;
var util = require('../util');
var lolex = require('lolex');
var MemoryDb = require('../../lib/db/memory');
@@ -183,7 +183,7 @@ describe('SnapshotTimestampRequest', function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function() {});
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
it('throws without a callback', function() {
@@ -191,7 +191,7 @@ describe('SnapshotTimestampRequest', function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine');
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
it('throws if the timestamp is -1', function() {
@@ -199,7 +199,7 @@ describe('SnapshotTimestampRequest', function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', -1, function() { });
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
it('errors if the timestamp is a string', function() {
@@ -207,7 +207,7 @@ describe('SnapshotTimestampRequest', function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', 'foo', function() { });
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
@@ -229,11 +229,11 @@ describe('SnapshotTimestampRequest', function() {
connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error) {
if (error) return done(error);
- expect(connection.hasPending()).to.be(false);
+ expect(connection.hasPending()).to.equal(false);
done();
});
- expect(connection.hasPending()).to.be(true);
+ expect(connection.hasPending()).to.equal(true);
});
it('deletes the request from the connection', function(done) {
@@ -269,7 +269,7 @@ describe('SnapshotTimestampRequest', function() {
});
connection.whenNothingPending(function() {
- expect(snapshotFetched).to.be(true);
+ expect(snapshotFetched).to.equal(true);
done();
});
});
@@ -328,7 +328,7 @@ describe('SnapshotTimestampRequest', function() {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
function(request) {
expect(request.snapshots[0]).to.eql(v3);
- expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byTimestamp);
+ expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byTimestamp);
done();
}
);
@@ -346,7 +346,7 @@ describe('SnapshotTimestampRequest', function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function(error, snapshot) {
if (error) return done(error);
- expect(snapshot.data.title).to.be('Alice in Wonderland');
+ expect(snapshot.data.title).to.equal('Alice in Wonderland');
done();
});
});
@@ -359,7 +359,7 @@ describe('SnapshotTimestampRequest', function() {
];
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error) {
- expect(error.message).to.be('foo');
+ expect(error.message).to.equal('foo');
done();
});
});
@@ -374,8 +374,8 @@ describe('SnapshotTimestampRequest', function() {
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);
+ expect(snapshot.data.title).to.equal('The Time Machine');
+ expect(snapshot.data.author).to.equal(undefined);
done();
});
});
@@ -441,11 +441,11 @@ describe('SnapshotTimestampRequest', function() {
.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(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 2, 4)).to.equal(true);
- expect(snapshot.v).to.be(3);
+ expect(snapshot.v).to.equal(3);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
done();
});
@@ -459,10 +459,10 @@ describe('SnapshotTimestampRequest', function() {
.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(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true);
- expect(snapshot.v).to.be(2);
+ expect(snapshot.v).to.equal(2);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lea'});
done();
});
@@ -477,11 +477,11 @@ describe('SnapshotTimestampRequest', function() {
.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(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 0, 2)).to.equal(true);
- expect(snapshot.v).to.be(1);
+ expect(snapshot.v).to.equal(1);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird'});
done();
});
@@ -496,11 +496,11 @@ describe('SnapshotTimestampRequest', function() {
.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(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true);
+ expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 4, null)).to.equal(true);
- expect(snapshot.v).to.be(5);
+ expect(snapshot.v).to.equal(5);
expect(snapshot.data).to.eql({
title: 'To Kill a Mocking Bird',
author: 'Harper Lee',
diff --git a/test/client/snapshot-version-request.js b/test/client/snapshot-version-request.js
index a39355f3..e2155d45 100644
--- a/test/client/snapshot-version-request.js
+++ b/test/client/snapshot-version-request.js
@@ -1,5 +1,5 @@
var Backend = require('../../lib/backend');
-var expect = require('expect.js');
+var expect = require('chai').expect;
var MemoryDb = require('../../lib/db/memory');
var MemoryMilestoneDb = require('../../lib/milestone-db/memory');
var sinon = require('sinon');
@@ -105,7 +105,7 @@ describe('SnapshotVersionRequest', function() {
backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function() {});
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
it('fetches the latest version when the optional version is not provided', function(done) {
@@ -121,7 +121,7 @@ describe('SnapshotVersionRequest', function() {
backend.connect().fetchSnapshot('books', 'don-quixote');
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
it('throws if the version is -1', function() {
@@ -129,7 +129,7 @@ describe('SnapshotVersionRequest', function() {
backend.connect().fetchSnapshot('books', 'don-quixote', -1, function() {});
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
it('errors if the version is a string', function() {
@@ -137,13 +137,13 @@ describe('SnapshotVersionRequest', function() {
backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function() { });
};
- expect(fetch).to.throwError();
+ expect(fetch).to.throw(Error);
});
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);
+ expect(error.code).to.equal(4024);
+ expect(snapshot).to.equal(undefined);
done();
});
});
@@ -167,11 +167,11 @@ describe('SnapshotVersionRequest', function() {
connection.fetchSnapshot('books', 'don-quixote', null, function(error) {
if (error) return done(error);
- expect(connection.hasPending()).to.be(false);
+ expect(connection.hasPending()).to.equal(false);
done();
});
- expect(connection.hasPending()).to.be(true);
+ expect(connection.hasPending()).to.equal(true);
});
it('deletes the request from the connection', function(done) {
@@ -207,7 +207,7 @@ describe('SnapshotVersionRequest', function() {
});
connection.whenNothingPending(function() {
- expect(snapshotFetched).to.be(true);
+ expect(snapshotFetched).to.equal(true);
done();
});
});
@@ -266,7 +266,7 @@ describe('SnapshotVersionRequest', function() {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
function(request) {
expect(request.snapshots[0]).to.eql(v3);
- expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byVersion);
+ expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byVersion);
done();
}
);
@@ -284,7 +284,7 @@ describe('SnapshotVersionRequest', function() {
backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
if (error) return done(error);
- expect(snapshot.data.title).to.be('Alice in Wonderland');
+ expect(snapshot.data.title).to.equal('Alice in Wonderland');
done();
});
});
@@ -297,7 +297,7 @@ describe('SnapshotVersionRequest', function() {
];
backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) {
- expect(error.message).to.be('foo');
+ expect(error.message).to.equal('foo');
done();
});
});
@@ -312,8 +312,8 @@ describe('SnapshotVersionRequest', function() {
backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function(error, snapshot) {
if (error) return done(error);
- expect(snapshot.data.title).to.be('Don Quixote');
- expect(snapshot.data.author).to.be(undefined);
+ expect(snapshot.data.title).to.equal('Don Quixote');
+ expect(snapshot.data.author).to.equal(undefined);
done();
});
});
@@ -435,9 +435,9 @@ describe('SnapshotVersionRequest', function() {
backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next);
},
function(snapshot, next) {
- expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.be(true);
- expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.be(true);
- expect(snapshot.v).to.be(3);
+ expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.equal(true);
+ expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.equal(true);
+ expect(snapshot.v).to.equal(3);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
next();
},
diff --git a/test/client/submit.js b/test/client/submit.js
index 2e47a444..f626f9eb 100644
--- a/test/client/submit.js
+++ b/test/client/submit.js
@@ -1,5 +1,5 @@
var async = require('async');
-var expect = require('expect.js');
+var expect = require('chai').expect;
var types = require('../../lib/types');
var deserializedType = require('./deserialized-type');
var numberType = require('./number-type');
@@ -96,7 +96,7 @@ module.exports = function() {
if (err) return done(err);
doc.version++;
doc.submitOp({p: ['age'], na: 2}, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
done();
});
});
@@ -105,7 +105,7 @@ module.exports = function() {
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();
+ expect(err).instanceOf(Error);
done();
});
});
@@ -113,7 +113,7 @@ module.exports = function() {
it('cannot delete an uncreated doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.del(function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
done();
});
});
@@ -207,7 +207,7 @@ module.exports = function() {
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.create({age: 4}, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
@@ -221,7 +221,7 @@ module.exports = function() {
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.create({age: 4}, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
expect(doc2.version).equal(1);
expect(doc2.data).eql({age: 3});
done();
@@ -262,7 +262,7 @@ module.exports = function() {
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 2}, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
done();
});
});
@@ -287,7 +287,7 @@ module.exports = function() {
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 2}, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
done();
});
});
@@ -432,7 +432,7 @@ module.exports = function() {
expect(doc.version).eql(1);
expect(doc.data).eql({age: 3});
} else {
- expect(err).ok();
+ expect(err).instanceOf(Error);
expect(doc.version).eql(1);
expect(doc.data).eql({age: 5});
done();
@@ -445,7 +445,7 @@ module.exports = function() {
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 5});
} else {
- expect(err).ok();
+ expect(err).instanceOf(Error);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
@@ -537,15 +537,15 @@ module.exports = function() {
docCallback();
}
});
- doc.submitOp({p: ['age'], na: 2}, function(error) {
- if (error) return done(error);
+ doc.submitOp({p: ['age'], na: 2}, function(err) {
+ if (err) return done(err);
// 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();
+ doc2.submitOp({p: ['age'], na: 7}, function(err) {
+ expect(err).instanceOf(Error);
done();
});
});
@@ -616,7 +616,7 @@ module.exports = function() {
doc2.del(function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
@@ -1070,7 +1070,7 @@ module.exports = function() {
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc._submit({}, null, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
done();
});
});
@@ -1094,8 +1094,9 @@ module.exports = function() {
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});
+ expect(doc.data).instanceOf(deserializedType.Node);
+ expect(doc.data.value).equal(3);
+ expect(doc.data.next).equal(null);
done();
});
});
@@ -1120,8 +1121,9 @@ module.exports = function() {
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});
+ expect(doc2.data).instanceOf(deserializedType.Node);
+ expect(doc2.data.value).equal(3);
+ expect(doc2.data.next).equal(null);
done();
});
});
@@ -1133,7 +1135,9 @@ module.exports = function() {
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}});
+ expect(doc.data.value).eql(2);
+ expect(doc.data.next.value).equal(3);
+ expect(doc.data.next.next).equal(null);
done();
});
});
@@ -1150,7 +1154,10 @@ module.exports = function() {
if (err) return done(err);
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}}});
+ expect(doc2.data.value).equal(2);
+ expect(doc2.data.next.value).equal(3);
+ expect(doc2.data.next.next.value).equal(4);
+ expect(doc2.data.next.next.next).equal(null);
done();
});
});
@@ -1164,8 +1171,9 @@ module.exports = function() {
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});
+ expect(doc.data).instanceOf(deserializedType.Node);
+ expect(doc.data.value).equal(3);
+ expect(doc.data.next).equal(null);
done();
});
});
@@ -1174,8 +1182,9 @@ module.exports = function() {
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});
+ expect(doc.data).instanceOf(deserializedType.Node);
+ expect(doc.data.value).equal(3);
+ expect(doc.data.next).equal(null);
done();
});
});
diff --git a/test/client/subscribe.js b/test/client/subscribe.js
index f1c3a63a..fe53e9c3 100644
--- a/test/client/subscribe.js
+++ b/test/client/subscribe.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var async = require('async');
module.exports = function() {
@@ -339,7 +339,7 @@ module.exports = function() {
doc.pause();
var calls = 0;
doc.create({age: 3}, function(err) {
- expect(err).ok();
+ expect(err).instanceOf(Error);
calls++;
});
doc[method](function(err) {
diff --git a/test/db-memory.js b/test/db-memory.js
index 0f0d01a9..e27ce0fe 100644
--- a/test/db-memory.js
+++ b/test/db-memory.js
@@ -1,6 +1,6 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var DB = require('../lib/db');
-var MemoryDB = require('../lib/db/memory');
+var BasicQueryableMemoryDB = require('./BasicQueryableMemoryDB');
describe('DB base class', function() {
it('can call db.close() without callback', function() {
@@ -16,7 +16,7 @@ describe('DB base class', function() {
it('returns an error if db.commit() is unimplemented', function(done) {
var db = new DB();
db.commit('testcollection', 'test', {}, {}, null, function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
done();
});
});
@@ -24,7 +24,7 @@ describe('DB base class', function() {
it('returns an error if db.getSnapshot() is unimplemented', function(done) {
var db = new DB();
db.getSnapshot('testcollection', 'foo', null, null, function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
done();
});
});
@@ -32,7 +32,7 @@ describe('DB base class', function() {
it('returns an error if db.getOps() is unimplemented', function(done) {
var db = new DB();
db.getOps('testcollection', 'foo', 0, null, null, function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
done();
});
});
@@ -40,7 +40,7 @@ describe('DB base class', function() {
it('returns an error if db.query() is unimplemented', function(done) {
var db = new DB();
db.query('testcollection', {x: 5}, null, null, function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
done();
});
});
@@ -48,75 +48,12 @@ describe('DB base class', function() {
it('returns an error if db.queryPollDoc() is unimplemented', function(done) {
var db = new DB();
db.queryPollDoc('testcollection', 'foo', {x: 5}, null, function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
done();
});
});
});
-
-// Extension of MemoryDB that supports query filters and sorts on simple
-// top-level properties, which is enough for the core ShareDB tests on
-// query subscription updating.
-function BasicQueryableMemoryDB() {
- MemoryDB.apply(this, arguments);
-}
-BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype);
-BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB;
-
-BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query) {
- if (query.filter) {
- snapshots = snapshots.filter(function(snapshot) {
- for (var queryKey in query.filter) {
- // This fake only supports simple property equality filters, so
- // throw an error on Mongo-like filter properties with dots.
- if (queryKey.includes('.')) {
- throw new Error('Only simple property filters are supported, got:', queryKey);
- }
- if (snapshot.data[queryKey] !== query.filter[queryKey]) {
- return false;
- }
- }
- return true;
- });
- }
-
- if (query.sort) {
- if (!Array.isArray(query.sort)) {
- throw new Error('query.sort must be an array');
- }
- if (query.sort.length) {
- snapshots.sort(snapshotComparator(query.sort));
- }
- }
-
- return {snapshots: snapshots};
-};
-
-// sortProperties is an array whose items are each [propertyName, direction].
-function snapshotComparator(sortProperties) {
- return function(snapshotA, snapshotB) {
- for (var i = 0; i < sortProperties.length; i++) {
- var sortProperty = sortProperties[i];
- var sortKey = sortProperty[0];
- var sortDirection = sortProperty[1];
-
- var aPropVal = snapshotA.data[sortKey];
- var bPropVal = snapshotB.data[sortKey];
- if (aPropVal < bPropVal) {
- return -1 * sortDirection;
- } else if (aPropVal > bPropVal) {
- return sortDirection;
- } else if (aPropVal === bPropVal) {
- continue;
- } else {
- throw new Error('Could not compare ' + aPropVal + ' and ' + bPropVal);
- }
- }
- return 0;
- };
-}
-
// Run all the DB-based tests against the BasicQueryableMemoryDB.
require('./db')({
create: function(options, callback) {
diff --git a/test/db.js b/test/db.js
index 701bdb7a..0fd9364b 100644
--- a/test/db.js
+++ b/test/db.js
@@ -1,5 +1,5 @@
var async = require('async');
-var expect = require('expect.js');
+var expect = require('chai').expect;
var Backend = require('../lib/backend');
var ot = require('../lib/ot');
@@ -36,7 +36,7 @@ module.exports = function(options) {
});
});
- require('./client/projections')();
+ require('./client/projections')({getQuery: getQuery});
require('./client/query-subscribe')({getQuery: getQuery});
require('./client/query')({getQuery: getQuery});
require('./client/submit')();
@@ -87,7 +87,7 @@ module.exports = function(options) {
function testCreateCommit(ops, snapshot) {
expect(snapshot.v).eql(1);
expect(ops.length).equal(1);
- expect(ops[0].create).ok();
+ expect(ops[0].create).ok;
}
it('one commit succeeds from 2 simultaneous creates', function(done) {
@@ -117,8 +117,8 @@ module.exports = function(options) {
function testOpCommit(ops, snapshot) {
expect(snapshot.v).equal(2);
expect(ops.length).equal(2);
- expect(ops[0].create).ok();
- expect(ops[1].op).ok();
+ expect(ops[0].create).ok;
+ expect(ops[1].op).ok;
}
function testDelCommit(ops, snapshot) {
@@ -126,7 +126,7 @@ module.exports = function(options) {
expect(ops.length).equal(2);
expect(snapshot.data).equal(undefined);
expect(snapshot.type).equal(null);
- expect(ops[0].create).ok();
+ expect(ops[0].create).ok;
expect(ops[1].del).equal(true);
}
@@ -734,7 +734,8 @@ module.exports = function(options) {
var db = this.db;
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) {
+ var dbQuery = getQuery({query: {x: 5}});
+ db.queryPoll('testcollection', dbQuery, null, function(err, ids) {
if (err) return done(err);
expect(ids).eql(['test']);
done();
@@ -743,7 +744,8 @@ module.exports = function(options) {
});
it('returns nothing when there is no data', function(done) {
- this.db.queryPoll('testcollection', {x: 5}, null, function(err, ids) {
+ var dbQuery = getQuery({query: {x: 5}});
+ this.db.queryPoll('testcollection', dbQuery, null, function(err, ids) {
if (err) return done(err);
expect(ids).eql([]);
done();
@@ -753,11 +755,11 @@ module.exports = function(options) {
describe('queryPollDoc', function() {
it('returns false when the document does not exist', function(done) {
- var query = {};
- if (!this.db.canPollDoc('testcollection', query)) return done();
+ var dbQuery = getQuery({query: {}});
+ if (!this.db.canPollDoc('testcollection', dbQuery)) return done();
var db = this.db;
- db.queryPollDoc('testcollection', 'doesnotexist', query, null, function(err, result) {
+ db.queryPollDoc('testcollection', 'doesnotexist', dbQuery, null, function(err, result) {
if (err) return done(err);
expect(result).equal(false);
done();
@@ -765,14 +767,14 @@ module.exports = function(options) {
});
it('returns true when the document matches', function(done) {
- var query = {x: 5};
- if (!this.db.canPollDoc('testcollection', query)) return done();
+ var dbQuery = getQuery({query: {x: 5}});
+ if (!this.db.canPollDoc('testcollection', dbQuery)) return done();
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) {
+ db.queryPollDoc('testcollection', 'test', dbQuery, null, function(err, result) {
if (err) return done(err);
expect(result).equal(true);
done();
@@ -781,14 +783,14 @@ module.exports = function(options) {
});
it('returns false when the document does not match', function(done) {
- var query = {x: 6};
- if (!this.db.canPollDoc('testcollection', query)) return done();
+ var dbQuery = getQuery({query: {x: 6}});
+ if (!this.db.canPollDoc('testcollection', dbQuery)) return done();
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) {
+ db.queryPollDoc('testcollection', 'test', dbQuery, null, function(err, result) {
if (err) return done(err);
expect(result).equal(false);
done();
diff --git a/test/logger.js b/test/logger.js
index 4efbb0cf..72f75e90 100644
--- a/test/logger.js
+++ b/test/logger.js
@@ -1,5 +1,5 @@
var Logger = require('../lib/logger/logger');
-var expect = require('expect.js');
+var expect = require('chai').expect;
var sinon = require('sinon');
describe('Logger', function() {
@@ -15,7 +15,7 @@ describe('Logger', function() {
it('logs to console by default', function() {
var logger = new Logger();
logger.warn('warning');
- expect(console.warn.calledOnceWithExactly('warning')).to.be(true);
+ expect(console.warn.calledOnceWithExactly('warning')).to.equal(true);
});
it('overrides console', function() {
@@ -27,8 +27,8 @@ describe('Logger', function() {
logger.warn('warning');
- expect(console.warn.notCalled).to.be(true);
- expect(customWarn.calledOnceWithExactly('warning')).to.be(true);
+ expect(console.warn.notCalled).to.equal(true);
+ expect(customWarn.calledOnceWithExactly('warning')).to.equal(true);
});
it('only overrides if provided with a method', function() {
@@ -40,7 +40,7 @@ describe('Logger', function() {
logger.warn('warning');
- expect(console.warn.calledOnceWithExactly('warning')).to.be(true);
+ expect(console.warn.calledOnceWithExactly('warning')).to.equal(true);
});
});
});
diff --git a/test/middleware.js b/test/middleware.js
index 0da9f068..9f1ef9f4 100644
--- a/test/middleware.js
+++ b/test/middleware.js
@@ -1,5 +1,5 @@
var Backend = require('../lib/backend');
-var expect = require('expect.js');
+var expect = require('chai').expect;
var util = require('./util');
var types = require('../lib/types');
@@ -24,6 +24,11 @@ describe('middleware', function() {
var response = this.backend.use('submit', function() {});
expect(response).equal(this.backend);
});
+
+ it('accepts an array of action names', function() {
+ var response = this.backend.use(['submit', 'connect'], function() {});
+ expect(response).equal(this.backend);
+ });
});
describe('connect', function() {
@@ -208,7 +213,7 @@ describe('middleware', function() {
});
describe('submit lifecycle', function() {
- ['submit', 'apply', 'commit', 'afterSubmit'].forEach(function(action) {
+ ['submit', 'apply', 'commit', 'afterWrite'].forEach(function(action) {
it(action + ' gets options passed to backend.submit', function(done) {
var doneAfter = util.callAfter(1, done);
this.backend.use(action, function(request, next) {
@@ -222,4 +227,89 @@ describe('middleware', function() {
});
});
});
+
+ describe('access control', function() {
+ function setupOpMiddleware(backend) {
+ backend.use('apply', function(request, next) {
+ request.priorAccountId = request.snapshot.data && request.snapshot.data.accountId;
+ next();
+ });
+ backend.use('commit', function(request, next) {
+ var accountId = (request.snapshot.data) ?
+ // For created documents, get the accountId from the document data
+ request.snapshot.data.accountId :
+ // For deleted documents, get the accountId from before
+ request.priorAccountId;
+ // Store the accountId for the document on the op for efficient access control
+ request.op.accountId = accountId;
+ next();
+ });
+ backend.use('op', function(request, next) {
+ if (request.op.accountId === request.agent.accountId) {
+ return next();
+ }
+ var err = {message: 'op accountId does not match', code: 'ERR_OP_READ_FORBIDDEN'};
+ return next(err);
+ });
+ }
+
+ it('is possible to cache add additional top-level fields on ops for access control', function(done) {
+ setupOpMiddleware(this.backend);
+ var connection1 = this.backend.connect();
+ var connection2 = this.backend.connect();
+ connection2.agent.accountId = 'foo';
+
+ // Fetching the snapshot here will cause subsequent fetches to get ops
+ connection2.get('dogs', 'fido').fetch(function(err) {
+ if (err) return done(err);
+ var data = {accountId: 'foo', age: 2};
+ connection1.get('dogs', 'fido').create(data, function(err) {
+ if (err) return done(err);
+ // This will go through the 'op' middleware and should pass
+ connection2.get('dogs', 'fido').fetch(done);
+ });
+ });
+ });
+
+ it('op middleware can reject ops', function(done) {
+ setupOpMiddleware(this.backend);
+ var connection1 = this.backend.connect();
+ var connection2 = this.backend.connect();
+ connection2.agent.accountId = 'baz';
+
+ // Fetching the snapshot here will cause subsequent fetches to get ops
+ connection2.get('dogs', 'fido').fetch(function(err) {
+ if (err) return done(err);
+ var data = {accountId: 'foo', age: 2};
+ connection1.get('dogs', 'fido').create(data, function(err) {
+ if (err) return done(err);
+ // This will go through the 'op' middleware and fail;
+ connection2.get('dogs', 'fido').fetch(function(err) {
+ expect(err.code).equal('ERR_OP_READ_FORBIDDEN');
+ done();
+ });
+ });
+ });
+ });
+
+ it('pubsub subscribe can check top-level fields for access control', function(done) {
+ setupOpMiddleware(this.backend);
+ var connection1 = this.backend.connect();
+ var connection2 = this.backend.connect();
+ connection2.agent.accountId = 'foo';
+
+ // Fetching the snapshot here will cause subsequent fetches to get ops
+ connection2.get('dogs', 'fido').subscribe(function(err) {
+ if (err) return done(err);
+ var data = {accountId: 'foo', age: 2};
+ connection1.get('dogs', 'fido').create(data, function(err) {
+ if (err) return done(err);
+ // The subscribed op will go through the 'op' middleware and should pass
+ connection2.get('dogs', 'fido').on('create', function() {
+ done();
+ });
+ });
+ });
+ });
+ });
});
diff --git a/test/milestone-db.js b/test/milestone-db.js
index 223423d4..b7521636 100644
--- a/test/milestone-db.js
+++ b/test/milestone-db.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var Backend = require('../lib/backend');
var MilestoneDB = require('../lib/milestone-db');
var NoOpMilestoneDB = require('../lib/milestone-db/no-op');
@@ -14,14 +14,14 @@ describe('Base class', function() {
it('calls back with an error when trying to get a snapshot', function(done) {
db.getMilestoneSnapshot('books', '123', 1, function(error) {
- expect(error.code).to.be(5019);
+ expect(error.code).to.equal(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);
+ expect(error.code).to.equal(5019);
done();
});
@@ -30,14 +30,14 @@ describe('Base class', function() {
it('calls back with an error when trying to save a snapshot', function(done) {
db.saveMilestoneSnapshot('books', {}, function(error) {
- expect(error.code).to.be(5020);
+ expect(error.code).to.equal(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);
+ expect(error.code).to.equal(5020);
done();
});
@@ -46,14 +46,14 @@ describe('Base class', function() {
it('calls back with an error when trying to get a snapshot before a time', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime('books', '123', 1000, function(error) {
- expect(error.code).to.be(5021);
+ expect(error.code).to.equal(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);
+ expect(error.code).to.equal(5022);
done();
});
});
@@ -83,7 +83,7 @@ describe('NoOpMilestoneDB', function() {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
done
@@ -269,35 +269,35 @@ module.exports = function(options) {
it('errors when fetching an undefined version', function(done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
it('errors when fetching version -1', function(done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', -1, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
it('errors when fetching version "foo"', function(done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 'foo', function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
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();
+ expect(error).instanceOf(Error);
done();
});
});
it('errors when fetching a null ID', function(done) {
db.getMilestoneSnapshot('books', null, 1, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
@@ -312,7 +312,7 @@ module.exports = function(options) {
);
db.saveMilestoneSnapshot(null, snapshot, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
@@ -323,7 +323,7 @@ module.exports = function(options) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
done
@@ -340,7 +340,7 @@ module.exports = function(options) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
done
@@ -357,7 +357,7 @@ module.exports = function(options) {
);
db.on('save', function(collection, snapshot) {
- expect(collection).to.be('books');
+ expect(collection).to.equal('books');
expect(snapshot).to.eql(snapshot);
done();
});
@@ -367,7 +367,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).instanceOf(Error);
done();
});
});
@@ -471,14 +471,14 @@ 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) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
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();
+ expect(error).instanceOf(Error);
done();
});
});
@@ -489,7 +489,7 @@ module.exports = function(options) {
db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 0, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
done
@@ -498,14 +498,14 @@ module.exports = function(options) {
it('errors if no collection is provided', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
it('errors if no ID is provided', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime('books', undefined, 0, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
@@ -553,14 +553,14 @@ 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) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
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();
+ expect(error).instanceOf(Error);
done();
});
});
@@ -571,7 +571,7 @@ module.exports = function(options) {
db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 4000, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
done
@@ -580,14 +580,14 @@ module.exports = function(options) {
it('errors if no collection is provided', function(done) {
db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
it('errors if no ID is provided', function(done) {
db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function(error) {
- expect(error).to.be.ok();
+ expect(error).instanceOf(Error);
done();
});
});
@@ -608,7 +608,7 @@ module.exports = function(options) {
it('stores a milestone snapshot on commit', function(done) {
db.on('save', function(collection, snapshot) {
- expect(collection).to.be('books');
+ expect(collection).to.equal('books');
expect(snapshot.data).to.eql({title: 'Catcher in the Rye'});
done();
});
@@ -639,28 +639,28 @@ module.exports = function(options) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
},
function(snapshot, next) {
- expect(snapshot.v).to.be(2);
+ expect(snapshot.v).to.equal(2);
next();
},
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next);
},
function(snapshot, next) {
- expect(snapshot.v).to.be(2);
+ expect(snapshot.v).to.equal(2);
next();
},
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
},
function(snapshot, next) {
- expect(snapshot.v).to.be(4);
+ expect(snapshot.v).to.equal(4);
next();
},
done
@@ -699,28 +699,28 @@ module.exports = function(options) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
},
function(snapshot, next) {
- expect(snapshot).to.be(undefined);
+ expect(snapshot).to.equal(undefined);
next();
},
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next);
},
function(snapshot, next) {
- expect(snapshot.v).to.be(3);
+ expect(snapshot.v).to.equal(3);
next();
},
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
},
function(snapshot, next) {
- expect(snapshot.v).to.be(4);
+ expect(snapshot.v).to.equal(4);
next();
},
done
diff --git a/test/ot.js b/test/ot.js
index b58b9c89..379d017d 100644
--- a/test/ot.js
+++ b/test/ot.js
@@ -1,33 +1,33 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
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();
- expect(ot.checkOp()).ok();
- expect(ot.checkOp(123)).ok();
- expect(ot.checkOp([])).ok();
+ expect(ot.checkOp('hi')).ok;
+ expect(ot.checkOp()).ok;
+ expect(ot.checkOp(123)).ok;
+ expect(ot.checkOp([])).ok;
});
it('fails if op data is missing op, create and del', function() {
- expect(ot.checkOp({v: 5})).ok();
+ expect(ot.checkOp({v: 5})).ok;
});
it('fails if src/seq data is invalid', function() {
- expect(ot.checkOp({del: true, v: 5, src: 'hi'})).ok();
- expect(ot.checkOp({del: true, v: 5, seq: 123})).ok();
- expect(ot.checkOp({del: true, v: 5, src: 'hi', seq: 'there'})).ok();
+ expect(ot.checkOp({del: true, v: 5, src: 'hi'})).ok;
+ expect(ot.checkOp({del: true, v: 5, seq: 123})).ok;
+ expect(ot.checkOp({del: true, v: 5, src: 'hi', seq: 'there'})).ok;
});
it('fails if a create operation is missing its type', function() {
- expect(ot.checkOp({create: {}})).ok();
- expect(ot.checkOp({create: 123})).ok();
+ expect(ot.checkOp({create: {}})).ok;
+ expect(ot.checkOp({create: 123})).ok;
});
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() {
@@ -54,12 +54,12 @@ describe('ot', function() {
describe('apply', function() {
it('fails if the versions dont match', function() {
- expect(ot.apply({v: 0}, {v: 1, create: {type: type.uri}})).ok();
- expect(ot.apply({v: 0}, {v: 1, del: true})).ok();
- expect(ot.apply({v: 0}, {v: 1, op: []})).ok();
- expect(ot.apply({v: 5}, {v: 4, create: {type: type.uri}})).ok();
- expect(ot.apply({v: 5}, {v: 4, del: true})).ok();
- expect(ot.apply({v: 5}, {v: 4, op: []})).ok();
+ expect(ot.apply({v: 0}, {v: 1, create: {type: type.uri}})).ok;
+ expect(ot.apply({v: 0}, {v: 1, del: true})).ok;
+ expect(ot.apply({v: 0}, {v: 1, op: []})).ok;
+ expect(ot.apply({v: 5}, {v: 4, create: {type: type.uri}})).ok;
+ expect(ot.apply({v: 5}, {v: 4, del: true})).ok;
+ expect(ot.apply({v: 5}, {v: 4, op: []})).ok;
});
it('allows the version field to be missing', function() {
@@ -71,7 +71,7 @@ describe('ot', function() {
describe('create', function() {
it('fails if the document already exists', function() {
var doc = {v: 6, create: {type: type.uri}};
- expect(ot.apply({v: 6, type: type.uri, data: 'hi'}, doc)).ok();
+ expect(ot.apply({v: 6, type: type.uri, data: 'hi'}, doc)).ok;
// The doc should be unmodified
expect(doc).eql({v: 6, create: {type: type.uri}});
});
@@ -105,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() {
@@ -138,21 +138,21 @@ describe('ot', function() {
it('fails if the version is specified on both and does not match', function() {
var op1 = {v: 5, op: [{p: [10], si: 'hi'}]};
var op2 = {v: 6, op: [{p: [5], si: 'abcde'}]};
- expect(ot.transform(type.uri, op1, op2)).ok();
+ expect(ot.transform(type.uri, op1, op2)).ok;
expect(op1).eql({v: 5, op: [{p: [10], si: 'hi'}]});
});
// There's 9 cases here.
it('create by create fails', function() {
- expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, create: {type: type.uri}})).ok();
+ expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, create: {type: type.uri}})).ok;
});
it('create by delete fails', function() {
- expect(ot.transform(null, {create: {type: type.uri}}, {del: true})).ok();
+ expect(ot.transform(null, {create: {type: type.uri}}, {del: true})).ok;
});
it('create by op fails', function() {
- expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, op: [15, 'hi']})).ok();
+ expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, op: [15, 'hi']})).ok;
});
it('create by noop ok', function() {
@@ -162,7 +162,7 @@ describe('ot', function() {
});
it('delete by create fails', function() {
- expect(ot.transform(null, {del: true}, {create: {type: type.uri}})).ok();
+ expect(ot.transform(null, {del: true}, {create: {type: type.uri}})).ok;
});
it('delete by delete ok', function() {
@@ -198,11 +198,11 @@ describe('ot', function() {
});
it('op by create fails', function() {
- expect(ot.transform(null, {op: {}}, {create: {type: type.uri}})).ok();
+ expect(ot.transform(null, {op: {}}, {create: {type: type.uri}})).ok;
});
it('op by delete fails', function() {
- expect(ot.transform(type.uri, {v: 10, op: []}, {v: 10, del: true})).ok();
+ expect(ot.transform(type.uri, {v: 10, op: []}, {v: 10, del: true})).ok;
});
it('op by op ok', function() {
diff --git a/test/projections.js b/test/projections.js
index 52dc7261..7bf5eb2a 100644
--- a/test/projections.js
+++ b/test/projections.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
var projections = require('../lib/projections');
var type = require('../lib/types').defaultType.uri;
@@ -12,7 +12,7 @@ describe('projection utility methods', function() {
it('throws on snapshots with the wrong type', function() {
expect(function() {
projections.projectSnapshot({}, {type: 'other', data: 123});
- }).throwException();
+ }).throw(Error);
});
it('empty object filters out all properties', function() {
@@ -244,7 +244,7 @@ describe('projection utility methods', function() {
it('throws on create ops with the wrong type', function() {
expect(function() {
projections.projectOp({}, {create: {type: 'other', data: 123}});
- }).throwException();
+ }).throw(Error);
});
it('strips data in creates', function() {
diff --git a/test/pubsub-memory.js b/test/pubsub-memory.js
index 498de699..371d982e 100644
--- a/test/pubsub-memory.js
+++ b/test/pubsub-memory.js
@@ -1,6 +1,6 @@
var MemoryPubSub = require('../lib/pubsub/memory');
var PubSub = require('../lib/pubsub');
-var expect = require('expect.js');
+var expect = require('chai').expect;
require('./pubsub')(function(callback) {
callback(null, new MemoryPubSub());
@@ -13,7 +13,7 @@ describe('PubSub base class', function() {
it('returns an error if _subscribe is unimplemented', function(done) {
var pubsub = new PubSub();
pubsub.subscribe('x', function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
expect(err.code).to.equal(5015);
done();
});
@@ -22,7 +22,7 @@ describe('PubSub base class', function() {
it('emits an error if _subscribe is unimplemented and callback is not provided', function(done) {
var pubsub = new PubSub();
pubsub.on('error', function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
expect(err.code).to.equal(5015);
done();
});
@@ -37,7 +37,7 @@ describe('PubSub base class', function() {
pubsub.subscribe('x', function(err, stream) {
if (err) return done(err);
pubsub.on('error', function(err) {
- expect(err).to.be.an(Error);
+ expect(err).instanceOf(Error);
expect(err.code).to.equal(5016);
done();
});
@@ -49,7 +49,7 @@ describe('PubSub base class', function() {
var pubsub = new PubSub();
pubsub.on('error', done);
pubsub.publish(['x', 'y'], {test: true}, function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
expect(err.code).to.equal(5017);
done();
});
@@ -58,7 +58,7 @@ describe('PubSub base class', function() {
it('emits an error if _publish is unimplemented and callback is not provided', function(done) {
var pubsub = new PubSub();
pubsub.on('error', function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
expect(err.code).to.equal(5017);
done();
});
@@ -68,7 +68,7 @@ describe('PubSub base class', function() {
it('can emit events', function(done) {
var pubsub = new PubSub();
pubsub.on('error', function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
expect(err.message).equal('test error');
done();
});
diff --git a/test/pubsub.js b/test/pubsub.js
index 57c19309..123b9496 100644
--- a/test/pubsub.js
+++ b/test/pubsub.js
@@ -1,4 +1,4 @@
-var expect = require('expect.js');
+var expect = require('chai').expect;
module.exports = function(create) {
describe('pubsub', function() {
@@ -78,7 +78,7 @@ module.exports = function(create) {
it('can emit events', function(done) {
this.pubsub.on('error', function(err) {
- expect(err).an(Error);
+ expect(err).instanceOf(Error);
expect(err.message).equal('test error');
done();
});