Socket
Socket
Sign inDemoInstall

@evercoder/sharedb

Package Overview
Dependencies
112
Maintainers
3
Versions
4
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.0.3 to 1.0.4

.eslintrc.js

3

examples/counter/client.js

@@ -0,5 +1,6 @@

var ReconnectingWebSocket = require('reconnecting-websocket');
var sharedb = require('sharedb/lib/client');
// 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);

@@ -6,0 +7,0 @@

{
"name": "sharedb-example-counter",
"version": "0.0.1",
"version": "1.0.0",
"description": "A simple client/server app using ShareDB and WebSockets",

@@ -18,10 +18,11 @@ "main": "server.js",

"dependencies": {
"@teamwork/websocket-json-stream": "^2.0.0",
"express": "^4.14.0",
"reconnecting-websocket": "^4.2.0",
"sharedb": "^1.0.0-beta",
"websocket-json-stream": "^0.0.1",
"ws": "^1.1.0"
"ws": "^7.2.0"
},
"devDependencies": {
"browserify": "^13.0.1"
"browserify": "^16.5.0"
}
}

@@ -5,3 +5,3 @@ var http = require('http');

var WebSocket = require('ws');
var WebSocketJSONStream = require('websocket-json-stream');
var WebSocketJSONStream = require('@teamwork/websocket-json-stream');

@@ -33,3 +33,3 @@ var backend = new ShareDB();

var wss = new WebSocket.Server({server: server});
wss.on('connection', function(ws, req) {
wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);

@@ -36,0 +36,0 @@ backend.listen(stream);

var React = require('react');
var Leaderboard = require('./Leaderboard.jsx');
var Body = React.createClass({
render: function() {
return (
<div className="app">
<div className="outer">
<div className="logo"></div>
<h1 className="title">Leaderboard</h1>
<div className="subtitle">Select a scientist to give them points</div>
<Leaderboard />
</div>
function Body() {
return (
<div className="app">
<div className="outer">
<div className="logo"></div>
<h1 className="title">Leaderboard</h1>
<div className="subtitle">Select a scientist to give them points</div>
<Leaderboard />
</div>
);
}
});
</div>
);
}
module.exports = Body;

@@ -0,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;

@@ -7,11 +7,14 @@ var PlayerList = require('./PlayerList.jsx');

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;

@@ -25,19 +28,19 @@ var query = connection.createSubscribeQuery('players', {$sort: {score: -1}});

}
},
}
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}];

@@ -47,5 +50,5 @@ connection.get('players', this.state.selectedPlayerId).submitOp(op, function(err) {

});
},
}
render: function() {
render() {
return (

@@ -60,5 +63,5 @@ <div>

}
});
}
module.exports = Leaderboard;

@@ -0,16 +1,16 @@

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;

@@ -25,9 +25,9 @@ var doc = comp.props.doc;

}
},
}
componentWillUnmount: function() {
componentWillUnmount() {
this.doc.unsubscribe();
},
}
render: function() {
render() {
var classes = {

@@ -45,4 +45,10 @@ 'player': true,

}
});
}
Player.propTypes = {
doc: PropTypes.object.isRequired,
onPlayerSelected: PropTypes.func.isRequired,
selected: PropTypes.bool.isRequired
};
module.exports = Player;

@@ -0,1 +1,2 @@

var PropTypes = require('prop-types');
var React = require('react');

@@ -5,27 +6,25 @@ var Player = require('./Player.jsx');

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 (
<Player {...other} doc={player} key={player.id} selected={selected} />
);
});
return (
<div className="playerList">
{playerNodes}
</div>
<Player {...other} doc={player} key={player.id} selected={selected} />
);
}
});
});
return (
<div className="playerList">
{playerNodes}
</div>
);
}
module.exports = PlayerList;
PlayerList.propTypes = {
players: PropTypes.array.isRequired,
selectedPlayerId: PropTypes.string
};
module.exports = PlayerList;

@@ -0,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 = <div className="details">
<div className="name">{selectedPlayer.data.name}</div>
<button className="inc" onClick={onAddPoints}>Add 5 points</button>
</div>;
} else {
node = <div className="message">Click a player to select</div>;
}
if (this.props.selectedPlayer) {
node = <div className="details">
<div className="name">{this.props.selectedPlayer.data.name}</div>
<button className="inc" onClick={this.props.onAddPoints}>Add 5 points</button>
</div>;
} else {
node = <div className="message">Click a player to select</div>;
}
return node;
}
return node;
}
});
PlayerSelector.propTypes = {
selectedPlayer: PropTypes.object
};
module.exports = PlayerSelector;
{
"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",

@@ -18,13 +18,13 @@ "start": "node server/index.js"

"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",
"websocket-json-stream": "^0.0.3",
"ws": "^1.1.0"
"ws": "^7.2.0"
},

@@ -34,4 +34,4 @@ "devDependencies": {

"babelify": "^7.3.0",
"browserify": "^13.0.1"
"browserify": "^16.5.0"
}
}

@@ -5,3 +5,3 @@ # Leaderboard

This is a port of [https://github.com/percolatestudio/react-leaderboard](Leaderboard) to
This is a port of [Leaderboard](https://github.com/percolatestudio/react-leaderboard) to
ShareDB.

@@ -13,8 +13,5 @@

## Run this example
## Install dependencies
First, install dependencies.
Note: Make sure you're in the `examples/leaderboard` folder so that it uses the `package.json` located here).
Make sure you're in the `examples/leaderboard` folder so that it uses the `package.json` located here).
```

@@ -24,12 +21,7 @@ npm install

Then build the client JavaScript file.
## Build JavaScript bundle and run server
```
npm run build
npm run build && npm start
```
Get the server running.
```
npm start
```
Finally, open the example app in the browser. It runs on port 8080 by default:

@@ -36,0 +28,0 @@ [http://localhost:8080](http://localhost:8080)

@@ -1,23 +0,21 @@

var http = require("http");
var ShareDB = require("sharedb");
var connect = require("connect");
var serveStatic = require('serve-static');
var http = require('http');
var ShareDB = require('sharedb');
var express = require('express');
var ShareDBMingoMemory = require('sharedb-mingo-memory');
var WebSocketJSONStream = require('websocket-json-stream');
var WebSocketJSONStream = require('@teamwork/websocket-json-stream');
var WebSocket = require('ws');
var util = require('util');
// Start ShareDB
var share = ShareDB({db: new ShareDBMingoMemory()});
var share = new ShareDB({db: new ShareDBMingoMemory()});
// Create a WebSocket server
var app = connect();
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);
console.log("Listening on http://localhost:8080");
console.log('Listening on http://localhost:8080');
// Connect any incoming WebSocket connection with ShareDB
wss.on('connection', function(ws, req) {
wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);

@@ -30,7 +28,9 @@ share.listen(stream);

connection.createFetchQuery('players', {}, {}, function(err, results) {
if (err) { throw err; }
if (err) {
throw err;
}
if (results.length === 0) {
var names = ["Ada Lovelace", "Grace Hopper", "Marie Curie",
"Carl Friedrich Gauss", "Nikola Tesla", "Claude Shannon"];
var names = ['Ada Lovelace', 'Grace Hopper', 'Marie Curie',
'Carl Friedrich Gauss', 'Nikola Tesla', 'Claude Shannon'];

@@ -37,0 +37,0 @@ names.forEach(function(name, index) {

@@ -0,1 +1,2 @@

var ReconnectingWebSocket = require('reconnecting-websocket');
var sharedb = require('sharedb/lib/client');

@@ -7,3 +8,3 @@ var richText = require('rich-text');

// 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);

@@ -16,3 +17,3 @@

window.connect = function() {
var socket = new WebSocket('ws://' + window.location.host);
var socket = new ReconnectingWebSocket('ws://' + window.location.host);
connection.bindToSocket(socket);

@@ -19,0 +20,0 @@ };

{
"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",

@@ -17,12 +17,13 @@ "main": "server.js",

"dependencies": {
"express": "^4.14.0",
"quill": "^1.0.0-beta.11",
"rich-text": "^3.0.1",
"@teamwork/websocket-json-stream": "^2.0.0",
"express": "^4.17.1",
"quill": "^1.3.7",
"reconnecting-websocket": "^4.2.0",
"rich-text": "^4.0.0",
"sharedb": "^1.0.0-beta",
"websocket-json-stream": "^0.0.1",
"ws": "^1.1.0"
"ws": "^7.2.0"
},
"devDependencies": {
"browserify": "^13.0.1"
"browserify": "^16.5.0"
}
}

@@ -6,3 +6,3 @@ var http = require('http');

var WebSocket = require('ws');
var WebSocketJSONStream = require('websocket-json-stream');
var WebSocketJSONStream = require('@teamwork/websocket-json-stream');

@@ -36,3 +36,3 @@ ShareDB.types.register(richText.type);

var wss = new WebSocket.Server({server: server});
wss.on('connection', function(ws, req) {
wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);

@@ -39,0 +39,0 @@ backend.listen(stream);

@@ -5,4 +5,4 @@ var sharedb = require('sharedb/lib/client');

// Open WebSocket connection to ShareDB server
const 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);

@@ -12,19 +12,19 @@

var statusSpan = document.getElementById('status-span');
status.innerHTML = "Not Connected"
statusSpan.innerHTML = 'Not Connected';
element.style.backgroundColor = "gray";
socket.onopen = function(){
status.innerHTML = "Connected"
element.style.backgroundColor = "white";
element.style.backgroundColor = 'gray';
socket.onopen = function() {
statusSpan.innerHTML = 'Connected';
element.style.backgroundColor = 'white';
};
socket.onclose = function(){
status.innerHTML = "Closed"
element.style.backgroundColor = "gray";
socket.onclose = function() {
statusSpan.innerHTML = 'Closed';
element.style.backgroundColor = 'gray';
};
socket.onerror = function() {
status.innerHTML = "Error"
element.style.backgroundColor = "red";
}
statusSpan.innerHTML = 'Error';
element.style.backgroundColor = 'red';
};

@@ -35,5 +35,5 @@ // Create local Doc instance mapped to 'examples' collection document with id 'textarea'

if (err) throw err;
var binding = new StringBinding(element, doc, ['content']);
binding.setup();
});
{
"name": "sharedb-example-textarea",
"version": "0.0.1",
"version": "1.0.0",
"description": "A simple client/server app using ShareDB and WebSockets",

@@ -17,12 +17,12 @@ "main": "server.js",

"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",
"websocket-json-stream": "^0.0.1",
"ws": "^1.1.0"
"ws": "^7.2.0"
},
"devDependencies": {
"browserify": "^13.0.1"
"browserify": "^16.5.0"
}
}

@@ -5,3 +5,3 @@ var http = require('http');

var WebSocket = require('ws');
var WebSocketJSONStream = require('websocket-json-stream');
var WebSocketJSONStream = require('@teamwork/websocket-json-stream');

@@ -18,3 +18,3 @@ var backend = new ShareDB();

if (doc.type === null) {
doc.create({ content: '' }, callback);
doc.create({content: ''}, callback);
return;

@@ -34,3 +34,3 @@ }

var wss = new WebSocket.Server({server: server});
wss.on('connection', function(ws, req) {
wss.on('connection', function(ws) {
var stream = new WebSocketJSONStream(ws);

@@ -37,0 +37,0 @@ backend.listen(stream);

var hat = require('hat');
var types = require('./types');
var util = require('./util');
var types = require('./types');
var logger = require('./logger');

@@ -59,4 +59,10 @@

Agent.prototype._cleanup = function() {
// Only clean up once if the stream emits both 'end' and 'close'.
if (this.closed) return;
this.closed = true;
this.backend.agentsCount--;
if (!this.stream.isServer) this.backend.remoteAgentsCount--;
// Clean up doc subscription streams

@@ -103,4 +109,3 @@ for (var collection in this.subscribedDocs) {

}
if (agent._isOwnOp(collection, data)) return;
agent._sendOp(collection, id, data);
agent._onOp(collection, id, data);
});

@@ -148,4 +153,3 @@ stream.on('end', function() {

var id = op.d;
if (agent._isOwnOp(collection, op)) return;
agent._sendOp(collection, id, op);
agent._onOp(collection, id, op);
};

@@ -156,2 +160,31 @@

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) {

@@ -187,3 +220,2 @@ // Detect ops from this client on the same projection. Since the client sent

};
Agent.prototype._sendOps = function(collection, id, ops) {

@@ -194,20 +226,32 @@ for (var i = 0; i < ops.length; 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') {
return {
code: 4001,
message: err
};
} else {
if (err.stack) {
logger.info(err.stack);
}
return {
code: err.code,
message: err.message
};
}
}
Agent.prototype._reply = function(request, err, message) {
var agent = this;
var backend = agent.backend;
if (err) {
if (typeof err === 'string') {
request.error = {
code: 4001,
message: err
};
} else {
if (err.stack) {
logger.warn(err.stack);
}
request.error = {
code: err.code,
message: err.message
};
}
this.send(request);
request.error = getReplyErrorObject(err);
agent.send(request);
return;

@@ -226,3 +270,11 @@ }

this.send(message);
var middlewareContext = {request: request, reply: message};
backend.trigger(backend.MIDDLEWARE_ACTIONS.reply, agent, middlewareContext, function(err) {
if (err) {
request.error = getReplyErrorObject(err);
agent.send(request);
} else {
agent.send(middlewareContext.reply);
}
});
};

@@ -255,7 +307,5 @@

this.stream.on('end', function() {
agent.backend.agentsCount--;
if (!agent.stream.isServer) agent.backend.remoteAgentsCount--;
agent._cleanup();
});
var cleanup = agent._cleanup.bind(agent);
this.stream.on('end', cleanup);
this.stream.on('close', cleanup);
};

@@ -309,3 +359,4 @@

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'});

@@ -326,3 +377,5 @@ return this._submit(request.c, request.d, op, callback);

var results = request.r;
var ids, fetch, fetchOps;
var ids;
var fetch;
var fetchOps;
if (results) {

@@ -356,3 +409,2 @@ ids = [];

// Fetch the results of a query once
var agent = this;
this.backend.queryFetch(this, collection, query, options, function(err, results, extra) {

@@ -488,6 +540,3 @@ if (err) return callback(err);

if (err) return callback(err);
for (var id in opsMap) {
var ops = opsMap[id];
agent._sendOps(collection, id, ops);
}
agent._sendOpsBulk(collection, opsMap);
callback();

@@ -501,4 +550,14 @@ });

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);

@@ -516,5 +575,9 @@ // Snapshot is returned only when subscribing from a null version.

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) {

@@ -570,3 +633,31 @@ agent._subscribeToStream(collection, id, streams[id]);

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;

@@ -576,5 +667,7 @@ this.seq = seq;

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;

@@ -584,5 +677,7 @@ this.seq = seq;

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;

@@ -592,24 +687,5 @@ this.seq = seq;

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);
};

@@ -14,3 +14,2 @@ var async = require('async');

var SubmitRequest = require('./submit-request');
var types = require('./types');

@@ -20,2 +19,9 @@ var warnDeprecatedDoc = true;

var DOC_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "doc" middleware action. Use "readSnapshots" instead. ' +
'Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.';
var AFTER_SUBMIT_ACTION_DEPRECATION_WARNING = 'DEPRECATED: "after submit" and "afterSubmit" middleware actions. ' +
'Use "afterWrite" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to ' +
'disable the "after submit" and "afterSubmit" actions and this warning.';
function Backend(options) {

@@ -58,6 +64,8 @@ if (!(this instanceof Backend)) return new Backend(options);

Backend.prototype.MIDDLEWARE_ACTIONS = {
// An operation was successfully submitted to the database.
// DEPRECATED: Synonym for 'afterWrite'
afterSubmit: 'afterSubmit',
// DEPRECATED: Synonym for 'afterSubmit'
// DEPRECATED: Synonym for 'afterWrite'
'after submit': 'after submit',
// 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

@@ -75,6 +83,12 @@ apply: 'apply',

query: 'query',
// Snapshot(s) were received from the database and are about to be returned to a client
readSnapshots: 'readSnapshots',
// Received a message from a client
receive: 'receive',
// Snapshot(s) were received from the database and are about to be returned to a client
readSnapshots: 'readSnapshots',
// About to send a non-error reply to a client message.
// WARNING: This gets passed a direct reference to the reply object, so
// be cautious with it. While modifications to the reply message are possible
// by design, changing existing reply properties can cause weird bugs, since
// the rest of ShareDB would be unaware of those changes.
reply: 'reply',
// An operation is about to be submitted to the database

@@ -96,3 +110,3 @@ submit: 'submit'

warnDeprecatedDoc = false;
console.warn('DEPRECATED: "doc" middleware action. Use "readSnapshots" instead. Pass `disableDocAction: true` option to ShareDB to disable the "doc" action and this warning.');
console.warn(DOC_ACTION_DEPRECATION_WARNING);
}

@@ -110,13 +124,16 @@

// Shim for backwards compatibility with deprecated middleware action name.
// The action 'after submit' is now 'afterSubmit'.
// The actions 'after submit' and 'afterSubmit' are now 'afterWrite'.
Backend.prototype._shimAfterSubmit = function() {
if (warnDeprecatedAfterSubmit) {
warnDeprecatedAfterSubmit = false;
console.warn('DEPRECATED: "after submit" middleware action. Use "afterSubmit" instead. Pass `disableSpaceDelimitedActions: true` option to ShareDB to disable the "after submit" action and this warning.');
console.warn(AFTER_SUBMIT_ACTION_DEPRECATION_WARNING);
}
var backend = this;
this.use(backend.MIDDLEWARE_ACTIONS.afterSubmit, function(request, callback) {
this.use(backend.MIDDLEWARE_ACTIONS.afterWrite, function(request, callback) {
backend.trigger(backend.MIDDLEWARE_ACTIONS['after submit'], request.agent, request, callback);
});
this.use(backend.MIDDLEWARE_ACTIONS.afterWrite, function(request, callback) {
backend.trigger(backend.MIDDLEWARE_ACTIONS['afterSubmit'], request.agent, request, callback);
});
};

@@ -203,3 +220,3 @@

}
return;
return this;
}

@@ -249,3 +266,3 @@ var fns = this.middleware[action] || (this.middleware[action] = []);

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);

@@ -262,2 +279,8 @@ backend._sanitizeOps(agent, request.projection, request.collection, id, request.ops, function(err) {

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) {

@@ -317,5 +340,31 @@ if (projection) {

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, callback) {
Backend.prototype.getOps = function(agent, index, id, from, to, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
var start = Date.now();

@@ -333,13 +382,15 @@ var projection = this.projections[index];

};
backend.db.getOps(collection, id, from, to, null, function(err, ops) {
var opsOptions = options && options.opsOptions;
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);
});
};
Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, callback) {
Backend.prototype.getOpsBulk = function(agent, index, fromMap, toMap, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
var start = Date.now();

@@ -356,13 +407,15 @@ var projection = this.projections[index];

};
backend.db.getOpsBulk(collection, fromMap, toMap, null, function(err, opsMap) {
var opsOptions = options && options.opsOptions;
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);
});
};
Backend.prototype.fetch = function(agent, index, id, callback) {
Backend.prototype.fetch = function(agent, index, id, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
var start = Date.now();

@@ -379,15 +432,26 @@ var projection = this.projections[index];

};
backend.db.getSnapshot(collection, id, fields, null, function(err, snapshot) {
var snapshotOptions = options && options.snapshotOptions;
backend.db.getSnapshot(collection, id, fields, snapshotOptions, function(err, snapshot) {
if (err) return callback(err);
var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
var snapshots = [snapshot];
backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) {
if (err) return callback(err);
backend.emit('timing', 'fetch', Date.now() - start, request);
callback(null, snapshot);
});
backend._sanitizeSnapshots(
agent,
snapshotProjection,
collection,
snapshots,
backend.SNAPSHOT_TYPES.current,
function(err) {
if (err) return callback(err);
backend.emit('timing', 'fetch', Date.now() - start, request);
callback(null, snapshot);
});
});
};
Backend.prototype.fetchBulk = function(agent, index, ids, callback) {
Backend.prototype.fetchBulk = function(agent, index, ids, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
var start = Date.now();

@@ -404,11 +468,18 @@ var projection = this.projections[index];

};
backend.db.getSnapshotBulk(collection, ids, fields, null, function(err, snapshotMap) {
var snapshotOptions = options && options.snapshotOptions;
backend.db.getSnapshotBulk(collection, ids, fields, snapshotOptions, function(err, snapshotMap) {
if (err) return callback(err);
var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);
var snapshots = backend._getSnapshotsFromMap(ids, snapshotMap);
backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) {
if (err) return callback(err);
backend.emit('timing', 'fetchBulk', Date.now() - start, request);
callback(null, snapshotMap);
});
backend._sanitizeSnapshots(
agent,
snapshotProjection,
collection,
snapshots,
backend.SNAPSHOT_TYPES.current,
function(err) {
if (err) return callback(err);
backend.emit('timing', 'fetchBulk', Date.now() - start, request);
callback(null, snapshotMap);
});
});

@@ -418,3 +489,14 @@ };

// Subscribe to the document from the specified version or null version
Backend.prototype.subscribe = function(agent, index, id, version, callback) {
Backend.prototype.subscribe = function(agent, index, id, version, options, callback) {
if (typeof options === 'function') {
callback = options;
options = null;
}
if (options) {
// We haven't yet implemented the ability to pass options to subscribe. This is because we need to
// add the ability to SubmitRequest.commit to optionally pass the metadata to other clients on
// PubSub. This behaviour is not needed right now, but we have added an options object to the
// subscribe() signature so that it remains consistent with getOps() and fetch().
return callback({code: 4025, message: 'Passing options to subscribe has not been implemented'});
}
var start = Date.now();

@@ -434,3 +516,2 @@ var projection = this.projections[index];

if (err) return callback(err);
stream.initProjection(backend, agent, projection);
if (version == null) {

@@ -440,3 +521,6 @@ // Subscribing from null means that the agent doesn't have a document

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);

@@ -446,7 +530,9 @@ 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);
});

@@ -475,3 +561,2 @@ }

if (err) return eachCb(err);
stream.initProjection(backend, agent, projection);
streams[id] = stream;

@@ -497,3 +582,3 @@ eachCb();

// 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) {

@@ -503,8 +588,4 @@ destroyStreams(streams);

}
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);
});

@@ -551,3 +632,2 @@ }

if (err) return callback(err);
stream.initProjection(backend, agent, request.projection);
if (options.ids) {

@@ -587,3 +667,3 @@ var queryEmitter = new QueryEmitter(request, stream, options.ids);

db: null,
snapshotProjection: null,
snapshotProjection: null
};

@@ -606,5 +686,11 @@ var backend = this;

if (err) return callback(err);
backend._sanitizeSnapshots(agent, request.snapshotProjection, request.collection, snapshots, backend.SNAPSHOT_TYPES.current, function(err) {
callback(err, snapshots, extra);
});
backend._sanitizeSnapshots(
agent,
request.snapshotProjection,
request.collection,
snapshots,
backend.SNAPSHOT_TYPES.current,
function(err) {
callback(err, snapshots, extra);
});
});

@@ -641,3 +727,3 @@ };

this._fetchSnapshot(collection, id, version, function (error, snapshot) {
this._fetchSnapshot(collection, id, version, function(error, snapshot) {
if (error) return callback(error);

@@ -647,3 +733,3 @@ var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);

var snapshotType = backend.SNAPSHOT_TYPES.byVersion;
backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function (error) {
backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) {
if (error) return callback(error);

@@ -656,7 +742,7 @@ backend.emit('timing', 'fetchSnapshot', Date.now() - start, request);

Backend.prototype._fetchSnapshot = function (collection, id, version, callback) {
Backend.prototype._fetchSnapshot = function(collection, id, version, callback) {
var db = this.db;
var backend = this;
this.milestoneDb.getMilestoneSnapshot(collection, id, version, function (error, milestoneSnapshot) {
this.milestoneDb.getMilestoneSnapshot(collection, id, version, function(error, milestoneSnapshot) {
if (error) return callback(error);

@@ -668,10 +754,10 @@

var from = milestoneSnapshot ? milestoneSnapshot.v : 0;
db.getOps(collection, id, from, version, null, function (error, ops) {
db.getOps(collection, id, from, version, null, function(error, ops) {
if (error) return callback(error);
backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, function (error, snapshot) {
backend._buildSnapshotFromOps(id, milestoneSnapshot, ops, function(error, snapshot) {
if (error) return callback(error);
if (version > snapshot.v) {
return callback({ code: 4024, message: 'Requested version exceeds latest snapshot version' });
return callback({code: 4024, message: 'Requested version exceeds latest snapshot version'});
}

@@ -685,3 +771,3 @@

Backend.prototype.fetchSnapshotByTimestamp = function (agent, index, id, timestamp, callback) {
Backend.prototype.fetchSnapshotByTimestamp = function(agent, index, id, timestamp, callback) {
var start = Date.now();

@@ -699,3 +785,3 @@ var backend = this;

this._fetchSnapshotByTimestamp(collection, id, timestamp, function (error, snapshot) {
this._fetchSnapshotByTimestamp(collection, id, timestamp, function(error, snapshot) {
if (error) return callback(error);

@@ -705,3 +791,3 @@ var snapshotProjection = backend._getSnapshotProjection(backend.db, projection);

var snapshotType = backend.SNAPSHOT_TYPES.byTimestamp;
backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function (error) {
backend._sanitizeSnapshots(agent, snapshotProjection, collection, snapshots, snapshotType, function(error) {
if (error) return callback(error);

@@ -714,3 +800,3 @@ backend.emit('timing', 'fetchSnapshot', Date.now() - start, request);

Backend.prototype._fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) {
Backend.prototype._fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) {
var db = this.db;

@@ -724,3 +810,3 @@ var milestoneDb = this.milestoneDb;

milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function (error, snapshot) {
milestoneDb.getMilestoneSnapshotAtOrBeforeTime(collection, id, timestamp, function(error, snapshot) {
if (error) return callback(error);

@@ -730,3 +816,3 @@ milestoneSnapshot = snapshot;

milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function (error, snapshot) {
milestoneDb.getMilestoneSnapshotAtOrAfterTime(collection, id, timestamp, function(error, snapshot) {
if (error) return callback(error);

@@ -736,3 +822,3 @@ if (snapshot) to = snapshot.v;

var options = {metadata: true};
db.getOps(collection, id, from, to, options, function (error, ops) {
db.getOps(collection, id, from, to, options, function(error, ops) {
if (error) return callback(error);

@@ -746,3 +832,3 @@ filterOpsInPlaceBeforeTimestamp(ops, timestamp);

Backend.prototype._buildSnapshotFromOps = function (id, startingSnapshot, ops, callback) {
Backend.prototype._buildSnapshotFromOps = function(id, startingSnapshot, ops, callback) {
var snapshot = startingSnapshot || new Snapshot(id, 0, null, undefined, null);

@@ -749,0 +835,0 @@ var error = ot.applyOps(snapshot, ops);

@@ -161,6 +161,4 @@ var Doc = require('./doc');

connection._setState('closed', reason);
} else if (reason === 'stopped' || reason === 'Stopped by server') {
connection._setState('stopped', reason);
} else {

@@ -299,4 +297,11 @@ connection._setState('disconnected', reason);

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'
)
) {

@@ -310,3 +315,9 @@ var err = new ShareDBError(5007, 'Cannot transition directly from ' + this.state + ' to ' + newState);

if (newState === 'disconnected' || newState === 'stopped' || newState === 'closed') this._reset();
if (
newState === 'disconnected' ||
newState === 'stopped' ||
newState === 'closed'
) {
this._reset();
}

@@ -613,3 +624,3 @@ // Group subscribes together to help server make more efficient calls

Connection.prototype._firstSnapshotRequest = function () {
Connection.prototype._firstSnapshotRequest = function() {
for (var id in this._snapshotRequests) {

@@ -625,3 +636,3 @@ return this._snapshotRequests[id];

* @param id - the ID of the snapshot
* @param version (optional) - the version number to fetch
* @param version (optional) - the version number to fetch. If null, the latest version is fetched.
* @param callback - (error, snapshot) => void, where snapshot takes the following schema:

@@ -654,3 +665,3 @@ *

* @param id - the ID of the snapshot
* @param timestamp (optional) - the timestamp to fetch
* @param timestamp (optional) - the timestamp to fetch. If null, the latest version is fetched.
* @param callback - (error, snapshot) => void, where snapshot takes the following schema:

@@ -666,3 +677,3 @@ *

*/
Connection.prototype.fetchSnapshotByTimestamp = function (collection, id, timestamp, callback) {
Connection.prototype.fetchSnapshotByTimestamp = function(collection, id, timestamp, callback) {
if (typeof timestamp === 'function') {

@@ -679,3 +690,3 @@ callback = timestamp;

Connection.prototype._handleSnapshotFetch = function (error, message) {
Connection.prototype._handleSnapshotFetch = function(error, message) {
var snapshotRequest = this._snapshotRequests[message.id];

@@ -682,0 +693,0 @@ if (!snapshotRequest) return;

@@ -142,3 +142,2 @@ var emitter = require('../emitter');

this.type = newType;
} else if (newType === null) {

@@ -148,3 +147,2 @@ this.type = newType;

this.data = undefined;
} else {

@@ -188,3 +186,6 @@ var err = new ShareDBError(4008, 'Missing type ' + newType);

// Otherwise, we've encounted an error state
var err = new ShareDBError(5009, 'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id);
var err = new ShareDBError(
5009,
'Cannot ingest snapshot in doc with null version. ' + this.collection + '.' + this.id
);
if (callback) return callback(err);

@@ -214,7 +215,10 @@ return this.emit('error', err);

Doc.prototype.whenNothingPending = function(callback) {
if (this.hasPending()) {
this.once('nothing pending', callback);
return;
}
callback();
var doc = this;
process.nextTick(function() {
if (doc.hasPending()) {
doc.once('nothing pending', callback);
return;
}
callback();
});
};

@@ -291,3 +295,3 @@

// and we should roll back but not return an error to the user.
if (err.code === 4002) err = null;
if (err.code === 4002 || err.code === 'ERR_OP_SUBMIT_REJECTED') err = null;
return this._rollback(err);

@@ -665,3 +669,6 @@ }

if (!this.type) {
var err = new ShareDBError(4015, 'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id);
var err = new ShareDBError(
4015,
'Cannot submit op. Document has not been created. ' + this.collection + '.' + this.id
);
if (callback) return callback(err);

@@ -744,3 +751,3 @@ return this.emit('error', err);

var last = this.pendingOps[this.pendingOps.length - 1];
if (!last) return;
if (!last || last.sentAt) return;

@@ -853,3 +860,2 @@ // Compose an op into a create by applying it. This effectively makes the op

this.version = message.v;
} else if (message.v !== this.version) {

@@ -938,5 +944,8 @@ // We should already be at the same version, because the server should

Doc.prototype._clearInflightOp = function(err) {
var called = callEach(this.inflightOp.callbacks, err);
var inflightOp = this.inflightOp;
this.inflightOp = null;
var called = callEach(inflightOp.callbacks, err);
this.flush();

@@ -943,0 +952,0 @@ this._emitNothingPending();

@@ -122,3 +122,2 @@ var emitter = require('../emitter');

this.extra = extra;
} else {

@@ -125,0 +124,0 @@ for (var id in data) {

@@ -23,3 +23,3 @@ var Snapshot = require('../../snapshot');

SnapshotRequest.prototype.send = function () {
SnapshotRequest.prototype.send = function() {
if (!this.connection.canSend) {

@@ -33,3 +33,3 @@ return;

SnapshotRequest.prototype._onConnectionStateChanged = function () {
SnapshotRequest.prototype._onConnectionStateChanged = function() {
if (this.connection.canSend) {

@@ -45,3 +45,3 @@ if (!this.sent) this.send();

SnapshotRequest.prototype._handleResponse = function (error, message) {
SnapshotRequest.prototype._handleResponse = function(error, message) {
this.emit('ready');

@@ -48,0 +48,0 @@

@@ -18,3 +18,3 @@ var SnapshotRequest = require('./snapshot-request');

SnapshotTimestampRequest.prototype._message = function () {
SnapshotTimestampRequest.prototype._message = function() {
return {

@@ -25,4 +25,4 @@ a: 'nt',

d: this.id,
ts: this.timestamp,
ts: this.timestamp
};
};

@@ -6,3 +6,3 @@ var SnapshotRequest = require('./snapshot-request');

function SnapshotVersionRequest (connection, requestId, collection, id, version, callback) {
function SnapshotVersionRequest(connection, requestId, collection, id, version, callback) {
SnapshotRequest.call(this, connection, requestId, collection, id, callback);

@@ -19,3 +19,3 @@

SnapshotVersionRequest.prototype._message = function () {
SnapshotVersionRequest.prototype._message = function() {
return {

@@ -26,4 +26,4 @@ a: 'nf',

d: this.id,
v: this.version,
v: this.version
};
};

@@ -125,3 +125,3 @@ var DB = require('./index');

// - extra: (optional) other types of results, such as counts
MemoryDB.prototype._querySync = function(snapshots, query, options) {
MemoryDB.prototype._querySync = function(snapshots) {
return {snapshots: snapshots};

@@ -128,0 +128,0 @@ };

@@ -8,11 +8,16 @@ var SUPPORTED_METHODS = [

function Logger() {
this.setMethods(console);
var defaultMethods = {};
SUPPORTED_METHODS.forEach(function(method) {
// Deal with Chrome issue: https://bugs.chromium.org/p/chromium/issues/detail?id=179628
defaultMethods[method] = console[method].bind(console);
});
this.setMethods(defaultMethods);
}
module.exports = Logger;
Logger.prototype.setMethods = function (overrides) {
Logger.prototype.setMethods = function(overrides) {
overrides = overrides || {};
var logger = this;
SUPPORTED_METHODS.forEach(function (method) {
SUPPORTED_METHODS.forEach(function(method) {
if (typeof overrides[method] === 'function') {

@@ -19,0 +24,0 @@ logger[method] = overrides[method];

@@ -27,3 +27,3 @@ var emitter = require('../emitter');

*/
MilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) {
MilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) {
var error = new ShareDBError(5019, 'getMilestoneSnapshot MilestoneDB method unimplemented');

@@ -39,3 +39,3 @@ this._callBackOrEmitError(error, callback);

*/
MilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) {
MilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) {
var error = new ShareDBError(5020, 'saveMilestoneSnapshot MilestoneDB method unimplemented');

@@ -45,3 +45,3 @@ this._callBackOrEmitError(error, callback);

MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) {
MilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) {
var error = new ShareDBError(5021, 'getMilestoneSnapshotAtOrBeforeTime MilestoneDB method unimplemented');

@@ -51,3 +51,3 @@ this._callBackOrEmitError(error, callback);

MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) {
MilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) {
var error = new ShareDBError(5022, 'getMilestoneSnapshotAtOrAfterTime MilestoneDB method unimplemented');

@@ -57,13 +57,13 @@ this._callBackOrEmitError(error, callback);

MilestoneDB.prototype._isValidVersion = function (version) {
MilestoneDB.prototype._isValidVersion = function(version) {
return util.isValidVersion(version);
};
MilestoneDB.prototype._isValidTimestamp = function (timestamp) {
MilestoneDB.prototype._isValidTimestamp = function(timestamp) {
return util.isValidTimestamp(timestamp);
};
MilestoneDB.prototype._callBackOrEmitError = function (error, callback) {
MilestoneDB.prototype._callBackOrEmitError = function(error, callback) {
if (callback) return process.nextTick(callback, error);
this.emit('error', error);
};

@@ -25,3 +25,3 @@ var MilestoneDB = require('./index');

MemoryMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) {
MemoryMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) {
if (!this._isValidVersion(version)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid version'));

@@ -33,4 +33,4 @@

MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) {
callback = callback || function (error) {
MemoryMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) {
callback = callback || function(error) {
if (error) return this.emit('error', error);

@@ -45,3 +45,3 @@ this.emit('save', collection, snapshot);

milestoneSnapshots.push(snapshot);
milestoneSnapshots.sort(function (a, b) {
milestoneSnapshots.sort(function(a, b) {
return a.v - b.v;

@@ -53,4 +53,6 @@ });

MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) {
if (!this._isValidTimestamp(timestamp)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp'));
MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) {
if (!this._isValidTimestamp(timestamp)) {
return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp'));
}

@@ -61,7 +63,9 @@ var filter = timestampLessThanOrEqualTo(timestamp);

MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) {
if (!this._isValidTimestamp(timestamp)) return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp'));
MemoryMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) {
if (!this._isValidTimestamp(timestamp)) {
return process.nextTick(callback, new ShareDBError(4001, 'Invalid timestamp'));
}
var filter = timestampGreaterThanOrEqualTo(timestamp);
this._findMilestoneSnapshot(collection, id, filter, function (error, snapshot) {
this._findMilestoneSnapshot(collection, id, filter, function(error, snapshot) {
if (error) return process.nextTick(callback, error);

@@ -78,3 +82,3 @@

MemoryMilestoneDB.prototype._findMilestoneSnapshot = function (collection, id, breakCondition, callback) {
MemoryMilestoneDB.prototype._findMilestoneSnapshot = function(collection, id, breakCondition, callback) {
if (!collection) return process.nextTick(callback, new ShareDBError(4001, 'Missing collection'));

@@ -98,3 +102,3 @@ if (!id) return process.nextTick(callback, new ShareDBError(4001, 'Missing ID'));

MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function (collection, id) {
MemoryMilestoneDB.prototype._getMilestoneSnapshotsSync = function(collection, id) {
var collectionSnapshots = this._milestoneSnapshots[collection] || (this._milestoneSnapshots[collection] = {});

@@ -105,3 +109,3 @@ return collectionSnapshots[id] || (collectionSnapshots[id] = []);

function versionLessThanOrEqualTo(version) {
return function (currentSnapshot, nextSnapshot) {
return function(currentSnapshot, nextSnapshot) {
if (version === null) {

@@ -116,3 +120,3 @@ return false;

function timestampGreaterThanOrEqualTo(timestamp) {
return function (currentSnapshot) {
return function(currentSnapshot) {
if (timestamp === null) {

@@ -128,3 +132,3 @@ return false;

function timestampLessThanOrEqualTo(timestamp) {
return function (currentSnapshot, nextSnapshot) {
return function(currentSnapshot, nextSnapshot) {
if (timestamp === null) {

@@ -131,0 +135,0 @@ return !!currentSnapshot;

@@ -16,3 +16,3 @@ var MilestoneDB = require('./index');

NoOpMilestoneDB.prototype.getMilestoneSnapshot = function (collection, id, version, callback) {
NoOpMilestoneDB.prototype.getMilestoneSnapshot = function(collection, id, version, callback) {
var snapshot = undefined;

@@ -22,3 +22,3 @@ process.nextTick(callback, null, snapshot);

NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function (collection, snapshot, callback) {
NoOpMilestoneDB.prototype.saveMilestoneSnapshot = function(collection, snapshot, callback) {
if (callback) return process.nextTick(callback, null);

@@ -28,3 +28,3 @@ this.emit('save', collection, snapshot);

NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function (collection, id, timestamp, callback) {
NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrBeforeTime = function(collection, id, timestamp, callback) {
var snapshot = undefined;

@@ -34,5 +34,5 @@ process.nextTick(callback, null, snapshot);

NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function (collection, id, timestamp, callback) {
NoOpMilestoneDB.prototype.getMilestoneSnapshotAtOrAfterTime = function(collection, id, timestamp, callback) {
var snapshot = undefined;
process.nextTick(callback, null, snapshot);
};

@@ -8,8 +8,3 @@ var inherits = require('util').inherits;

Readable.call(this, {objectMode: true});
this.id = null;
this.backend = null;
this.agent = null;
this.projection = null;
this.open = true;

@@ -28,29 +23,11 @@ }

OpStream.prototype.initProjection = function(backend, agent, projection) {
this.backend = backend;
this.agent = agent;
this.projection = projection;
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.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.destroy = function() {
// Only close stream once
if (!this.open) return;

@@ -57,0 +34,0 @@ this.open = false;

@@ -26,6 +26,4 @@ // This contains the master OT functions for the database. They look like

}
} else if (op.del != null) {
if (op.del !== true) return {code: 4009, message: 'del value must be true'};
} else if (op.op == null) {

@@ -159,5 +157,5 @@ return {code: 4010, message: 'Missing op, create, or del'};

* @param ops - an array of ops to apply to the snapshot
* @returns an error object if applicable
* @return an error object if applicable
*/
exports.applyOps = function (snapshot, ops) {
exports.applyOps = function(snapshot, ops) {
var type = null;

@@ -167,3 +165,3 @@

type = types[snapshot.type];
if (!type) return { code: 4008, message: 'Unknown type' };
if (!type) return {code: 4008, message: 'Unknown type'};
}

@@ -178,3 +176,3 @@

type = types[op.create.type];
if (!type) return { code: 4008, message: 'Unknown type' };
if (!type) return {code: 4008, message: 'Unknown type'};
snapshot.data = type.create(op.create.data);

@@ -181,0 +179,0 @@ snapshot.type = type.uri;

@@ -44,3 +44,3 @@ var json0 = require('ot-json0').type;

if (path.length === 0) {
var newC = {p:[]};
var newC = {p: []};

@@ -47,0 +47,0 @@ if (c.od !== undefined || c.oi !== undefined) {

@@ -93,4 +93,3 @@ var emitter = require('../emitter');

for (var id in channelStreams) {
var copy = shallowCopy(data);
channelStreams[id].pushOp(copy.c, copy.d, copy);
channelStreams[id].pushData(data);
}

@@ -133,9 +132,1 @@ }

};
function shallowCopy(object) {
var out = {};
for (var key in object) {
out[key] = object[key];
}
return out;
}

@@ -20,7 +20,7 @@ var PubSub = require('./index');

MemoryPubSub.prototype._subscribe = function(channel, callback) {
if (callback) process.nextTick(callback);
process.nextTick(callback);
};
MemoryPubSub.prototype._unsubscribe = function(channel, callback) {
if (callback) process.nextTick(callback);
process.nextTick(callback);
};

@@ -37,4 +37,4 @@

}
if (callback) callback();
callback();
});
};
var arraydiff = require('arraydiff');
var deepEquals = require('deep-is');
var deepEqual = require('fast-deep-equal');
var ShareDBError = require('./error');

@@ -24,6 +24,6 @@ var util = require('./util');

(typeof this.options.pollDebounce === 'number') ? this.options.pollDebounce :
(typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce : 0;
(typeof this.db.pollDebounce === 'number') ? this.db.pollDebounce : 0;
this.pollInterval =
(typeof this.options.pollInterval === 'number') ? this.options.pollInterval :
(typeof this.db.pollInterval === 'number') ? this.db.pollInterval : 0;
(typeof this.db.pollInterval === 'number') ? this.db.pollInterval : 0;

@@ -67,3 +67,8 @@ this._polling = false;

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;

@@ -95,3 +100,10 @@ // Check if the op's id matches the query before updating the query results

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);
};
}

@@ -101,6 +113,10 @@

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);
}

@@ -110,7 +126,7 @@ if (this.canPollDoc) {

// 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);
}

@@ -181,3 +197,3 @@ };

// 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;

@@ -196,9 +212,15 @@ emitter.onExtra(extra);

var snapshotType = emitter.backend.SNAPSHOT_TYPES.current;
emitter.backend._sanitizeSnapshots(emitter.agent, emitter.snapshotProjection, emitter.collection, snapshots, snapshotType, function(err) {
if (err) return emitter._finishPoll(err, callback, pending);
emitter._emitTiming('queryEmitter.pollGetSnapshotBulk', start);
var diff = mapDiff(idsDiff, snapshotMap);
emitter.onDiff(diff);
emitter._finishPoll(err, callback, pending);
});
emitter.backend._sanitizeSnapshots(
emitter.agent,
emitter.snapshotProjection,
emitter.collection,
snapshots,
snapshotType,
function(err) {
if (err) return emitter._finishPoll(err, callback, pending);
emitter._emitTiming('queryEmitter.pollGetSnapshotBulk', start);
var diff = mapDiff(idsDiff, snapshotMap);
emitter.onDiff(diff);
emitter._finishPoll(err, callback, pending);
});
});

@@ -246,8 +268,14 @@ } else {

var snapshotType = emitter.backend.SNAPSHOT_TYPES.current;
emitter.backend._sanitizeSnapshots(emitter.agent, emitter.snapshotProjection, emitter.collection, snapshots, snapshotType, function(err) {
if (err) return callback(err);
emitter.onDiff([new arraydiff.InsertDiff(index, snapshots)]);
emitter._emitTiming('queryEmitter.pollDocGetSnapshot', start);
callback();
});
emitter.backend._sanitizeSnapshots(
emitter.agent,
emitter.snapshotProjection,
emitter.collection,
snapshots,
snapshotType,
function(err) {
if (err) return callback(err);
emitter.onDiff([new arraydiff.InsertDiff(index, snapshots)]);
emitter._emitTiming('queryEmitter.pollDocGetSnapshot', start);
callback();
});
});

@@ -254,0 +282,0 @@ return;

@@ -57,3 +57,2 @@ var ot = require('./ot');

if (op.v == null) {
if (op.create && snapshot.type && op.src) {

@@ -151,24 +150,30 @@ // If the document was already created by another op, we will return a

// Try committing the operation and snapshot to the database atomically
backend.db.commit(request.collection, request.id, request.op, request.snapshot, request.options, function(err, succeeded) {
if (err) return callback(err);
if (!succeeded) {
// Between our fetch and our call to commit, another client committed an
// operation. We expect this to be relatively infrequent but normal.
return request.retry(callback);
}
if (!request.suppressPublish) {
var op = request.op;
op.c = request.collection;
op.d = request.id;
op.m = undefined;
// Needed for agent to detect if it can ignore sending the op back to
// the client that submitted it in subscriptions
if (request.collection !== request.index) op.i = request.index;
backend.pubsub.publish(request.channels, op);
}
if (request._shouldSaveMilestoneSnapshot(request.snapshot)) {
request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot);
}
callback();
});
backend.db.commit(
request.collection,
request.id,
request.op,
request.snapshot,
request.options,
function(err, succeeded) {
if (err) return callback(err);
if (!succeeded) {
// Between our fetch and our call to commit, another client committed an
// operation. We expect this to be relatively infrequent but normal.
return request.retry(callback);
}
if (!request.suppressPublish) {
var op = request.op;
op.c = request.collection;
op.d = request.id;
op.m = undefined;
// Needed for agent to detect if it can ignore sending the op back to
// the client that submitted it in subscriptions
if (request.collection !== request.index) op.i = request.index;
backend.pubsub.publish(request.channels, op);
}
if (request._shouldSaveMilestoneSnapshot(request.snapshot)) {
request.backend.milestoneDb.saveMilestoneSnapshot(request.collection, request.snapshot);
}
callback();
});
});

@@ -229,3 +234,3 @@ };

SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function (snapshot) {
SubmitRequest.prototype._shouldSaveMilestoneSnapshot = function(snapshot) {
// If the flag is null, it's not been overridden by the consumer, so apply the interval

@@ -258,3 +263,6 @@ if (this.saveMilestoneSnapshot === null) {

SubmitRequest.prototype.missingOpsError = function() {
return {code: 5001, message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'};
return {
code: 5001,
message: 'Op submit failed. DB missing ops needed to transform it up to the current snapshot version'
};
};

@@ -261,0 +269,0 @@ SubmitRequest.prototype.versionDuringTransformError = function() {

@@ -11,3 +11,3 @@

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger#Polyfill
exports.isInteger = Number.isInteger || function (value) {
exports.isInteger = Number.isInteger || function(value) {
return typeof value === 'number' &&

@@ -18,3 +18,3 @@ isFinite(value) &&

exports.isValidVersion = function (version) {
exports.isValidVersion = function(version) {
if (version === null) return true;

@@ -24,4 +24,4 @@ return exports.isInteger(version) && version >= 0;

exports.isValidTimestamp = function (timestamp) {
exports.isValidTimestamp = function(timestamp) {
return exports.isValidVersion(timestamp);
};
{
"name": "@evercoder/sharedb",
"version": "1.0.3",
"version": "1.0.4",
"description": "JSON OT database backend",

@@ -9,3 +9,3 @@ "main": "lib/index.js",

"async": "^1.4.2",
"deep-is": "^0.1.3",
"fast-deep-equal": "^2.0.1",
"hat": "0.0.3",

@@ -16,14 +16,16 @@ "make-error": "^1.1.1",

"devDependencies": {
"coveralls": "^2.11.8",
"expect.js": "^0.3.1",
"istanbul": "^0.4.2",
"jshint": "^2.9.2",
"lolex": "^3.0.0",
"mocha": "^5.2.0",
"sinon": "^6.1.5"
"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 jshint",
"test-cover": "node_modules/istanbul/lib/cli.js cover node_modules/mocha/bin/_mocha",
"jshint": "./node_modules/.bin/jshint lib/*.js test/*.js"
"test": "./node_modules/.bin/mocha && npm run lint",
"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"
},

@@ -30,0 +32,0 @@ "repository": {

@@ -102,2 +102,4 @@ # ShareDB

through this pub/sub adapter. Defaults to `ShareDB.MemoryPubSub()`.
* `options.milestoneDb` _(instance of ShareDB.MilestoneDB`)_
Store snapshots of documents at a specified interval of versions

@@ -121,6 +123,10 @@ #### Database Adapters

#### Milestone Adapters
* [`sharedb-milestone-mongo`](https://github.com/share/sharedb-milestone-mongo), backed by Mongo
### Listening to WebSocket connections
```js
var WebSocketJSONStream = require('websocket-json-stream');
var WebSocketJSONStream = require('@teamwork/websocket-json-stream');

@@ -159,16 +165,23 @@ // 'ws' is a websocket server connection, as passed into

and new snapshot are about to be written to the database.
* `'afterSubmit'`: An operation was successfully submitted to
* `'afterWrite'`: An operation was successfully written to
the database.
* `'receive'`: Received a message from a client
* `fn` _(Function(request, callback))_
* `'reply'`: About to send a non-error reply to a client message
* `fn` _(Function(context, callback))_
Call this function at the time specified by `action`.
`request` contains a subset of the following properties, as relevant for the action:
* `action`: The action this middleware is handing
* `agent`: An object corresponding to the server agent handing this client
* `req`: The HTTP request being handled
* `collection`: The collection name being handled
* `id`: The document id being handled
* `snapshots`: The retrieved snapshots for the `readSnapshots` action
* `query`: The query object being handled
* `op`: The op being handled
* `context` will always have the following properties:
* `action`: The action this middleware is hanlding
* `agent`: A reference to the server agent handling this client
* `backend`: A reference to this ShareDB backend instance
* `context` can also have additional properties, as relevant for the action:
* `collection`: The collection name being handled
* `id`: The document id being handled
* `op`: The op being handled
* `req`: HTTP request being handled, if provided to `share.listen` (for 'connect')
* `stream`: The duplex Stream provided to `share.listen` (for 'connect')
* `query`: The query object being handled (for 'query')
* `snapshots`: Array of retrieved snapshots (for 'readSnapshots')
* `data`: Received client message (for 'receive')
* `request`: Client message being replied to (for 'reply')
* `reply`: Reply to be sent to the client (for 'reply')

@@ -270,3 +283,3 @@ ### Projections

* `version` _(number) [optional]_
The version number of the desired snapshot
The version number of the desired snapshot. If `null`, the latest version is fetched.
* `callback` _(Function)_

@@ -292,3 +305,3 @@ Called with `(error, snapshot)`, where `snapshot` takes the following form:

* `timestamp` _(number) [optional]_
The timestamp of the desired snapshot. The returned snapshot will be the latest snapshot before the provided timestamp
The timestamp of the desired snapshot. The returned snapshot will be the latest snapshot before the provided timestamp. If `null`, the latest version is fetched.
* `callback` _(Function)_

@@ -324,2 +337,5 @@ Called with `(error, snapshot)`, where `snapshot` takes the following form:

`doc.unsubscribe(function (err) {...})`
Stop listening for document updates. The document data at the time of unsubscribing remains in memory, but no longer stays up-to-date. Resubscribe with `doc.subscribe`.
`doc.ingestSnapshot(snapshot, callback)`

@@ -423,2 +439,215 @@ Ingest snapshot data. The `snapshot` param must include the fields `v` (doc version), `data`, and `type` (OT type). This method is generally called interally as a result of fetch or subscribe and not directly from user code. However, it may still be called directly from user code to pass data that was transferred to the client external to the client's ShareDB connection, such as snapshot data sent along with server rendering of a webpage.

### Class: `ShareDB.Backend`
`Backend` represents the server-side instance of ShareDB. It is primarily responsible for connecting to clients, and sending requests to the database adapters. It is also responsible for some configuration, such as setting up [middleware](#middlewares) and [projections](#projections).
#### `constructor`
```javascript
var Backend = require('sharedb');
var backend = new Backend(options);
```
Constructs a new `Backend` instance, with the provided options:
* `db` _DB (optional)_: an instance of a ShareDB [database adapter](#database-adapters) that provides the data store for ShareDB. If omitted, a new, non-persistent, in-memory adapter will be created, which should _not_ be used in production, but may be useful for testing
* `pubsub` _PubSub (optional)_: an instance of a ShareDB [Pub/Sub adapter](#pubsub-adapters) that provides a channel for notifying other ShareDB instances of changes to data. If omitted, a new, in-memory adapter will be created. Unlike the database adapter, the in-memory instance _may_ be used in a production environment where pub/sub state need only persist across a single, stand-alone server
* `milestoneDb` _MilestoneDB (optional)_: an instance of a ShareDB [milestone adapter](#milestone-adapters) that provides the data store for milestone snapshots, which are historical snapshots of documents stored at a specified version interval. If omitted, this functionality will not be enabled
* `extraDbs` _Object (optional)_: an object whose values are extra `DB` instances which can be [queried](#class-sharedbquery). The keys are the names that can be passed into the query options `db` field
* `suppressPublish` _boolean (optional)_: if set to `true`, any changes committed will _not_ be published on `pubsub`
* `maxSubmitRetries` _number (optional)_: the number of times to allow a submit to be retried. If omitted, the request will retry an unlimited number of times
#### `connect`
```javascript
var connection = backend.connect();
```
Connects to ShareDB and returns an instance of a [`Connection`](#class-sharedbconnection). This is the server-side equivalent of `new ShareDBClient.Connection(socket)` in the browser.
This method also supports infrequently used optional arguments:
```javascript
var connection = backend.connect(connection, req);
```
* `connection` _Connection (optional)_: a [`Connection`](#class-sharedbconnection) instance to bind to the `Backend`
* `req` _Object (optional)_: a connection context object that can contain information such as cookies or session data that will be available in the [middleware](#middlewares)
Returns a [`Connection`](#class-sharedbconnection).
#### `listen`
```javascript
var agent = backend.listen(stream, req);
```
Registers a `Stream` with the backend. This should be called when the server receives a new connection from a client.
* `stream` _Stream_: a [`Stream`](https://nodejs.org/api/stream.html) (or `Stream`-like object) that will be used to communicate between the new `Agent` and the `Backend`
* `req` _Object (optional)_: a connection context object that can contain information such as cookies or session data that will be available in the [middleware](#middlewares)
Returns an [`Agent`](#class-agent), which is also available in the [middleware](#middlewares).
#### `close`
```javascript
backend.close(callback);
```
Disconnects ShareDB and all of its underlying services (database, pubsub, etc.).
* `callback` _Function_: a callback with the signature `function (error: Error): void` that will be called once the services have stopped, or with an `error` if at least one of them could not be stopped
#### `use`
```javascript
backend.use(action, middleware);
```
Adds [middleware](#middlewares) to the `Backend`.
* `action` _string | string[]_: an action, or array of action names defining when to apply the middleware
* `middleware` _Function_: a middleware function with the signature `function (context: Object, callback: Function): void;`. See [middleware](#middlewares) for more details
Returns the `Backend` instance, which allows for multiple chained calls.
#### `addProjection`
```javascript
backend.addProjection(name, collection, fields);
```
Adds a [projection](#projections).
* `name` _string_: the name of the projection
* `collection` _string_: the name of the collection on which to apply the projection
* `fields` _Object_: a declaration of which fields to include in the projection, such as `{ field1: true }`. Defining sub-field projections is not supported.
#### `submit`
```javascript
backend.submit(agent, index, id, op, options, callback);
```
Submits an operation to the `Backend`.
* `agent` _[`Agent`](#class-agent)_: connection agent to pass to the middleware
* `index` _string_: the name of the target collection or projection
* `id` _string_: the document ID
* `op` _Object_: the operation to submit
* `options` _Object_: these options are passed through to the database adapter's `commit` method, so any options that are valid there can be used here
* `callback` _Function_: a callback with the signature `function (error: Error, ops: Object[]): void;`, where `ops` are the ops committed by other clients between the submitted `op` being submitted and committed
#### `getOps`
```javascript
backend.getOps(agent, index, id, from, to, options, callback);
```
Fetches the ops for a document between the requested version numbers, where the `from` value is inclusive, but the `to` value is non-inclusive.
* `agent` _[`Agent`](#class-agent)_: connection agent to pass to the middleware
* `index` _string_: the name of the target collection or projection
* `id` _string_: the document ID
* `from` _number_: the first op version to fetch. If set to `null`, then ops will be fetched from the earliest version
* `to` _number_: The last op version. This version will _not_ be fetched (ie `to` is non-inclusive). If set to `null`, then ops will be fetched up to the latest version
* `options`: _Object (optional)_: options can be passed directly to the database driver's `getOps` inside the `opsOptions` property: `{opsOptions: {metadata: true}}`
* `callback`: _Function_: a callback with the signature `function (error: Error, ops: Object[]): void;`, where `ops` is an array of the requested ops
#### `getOpsBulk`
```javascript
backend.getOpsBulk(agent, index, fromMap, toMap, options, callback);
```
Fetches the ops for multiple documents in a collection between the requested version numbers, where the `from` value is inclusive, but the `to` value is non-inclusive.
* `agent` _[`Agent`](#class-agent)_: connection agent to pass to the middleware
* `index` _string_: the name of the target collection or projection
* `id` _string_: the document ID
* `fromMap` _Object_: an object whose keys are the IDs of the target documents. The values are the first versions requested of each document. For example, `{abc: 3}` will fetch ops for document with ID `abc` from version `3` (inclusive)
* `toMap` _Object_: an object whose keys are the IDs of the target documents. The values are the last versions requested of each document (non-inclusive). For example, `{abc: 3}` will fetch ops for document with ID `abc` up to version `3` (_not_ inclusive)
* `options`: _Object (optional)_: options can be passed directly to the database driver's `getOpsBulk` inside the `opsOptions` property: `{opsOptions: {metadata: true}}`
* `callback`: _Function_: a callback with the signature `function (error: Error, opsMap: Object): void;`, where `opsMap` is an object whose keys are the IDs of the requested documents, and their values are the arrays of requested ops, eg `{abc: []}`
#### `fetch`
```javascript
backend.fetch(agent, index, id, options, callback);
```
Fetch the current snapshot of a document.
* `agent` _[`Agent`](#class-agent)_: connection agent to pass to the middleware
* `index` _string_: the name of the target collection or projection
* `id` _string_: the document ID
* `options`: _Object (optional)_: options can be passed directly to the database driver's `fetch` inside the `snapshotOptions` property: `{snapshotOptions: {metadata: true}}`
* `callback`: _Function_: a callback with the signature `function (error: Error, snapshot: Snapshot): void;`, where `snapshot` is the requested snapshot
#### `fetchBulk`
```javascript
backend.fetchBulk(agent, index, ids, options, callback);
```
Fetch multiple document snapshots from a collection.
* `agent` _[`Agent`](#class-agent)_: connection agent to pass to the middleware
* `index` _string_: the name of the target collection or projection
* `ids` _string[]_: array of document IDs
* `options`: _Object (optional)_: options can be passed directly to the database driver's `fetchBulk` inside the `snapshotOptions` property: `{snapshotOptions: {metadata: true}}`
* `callback`: _Function_: a callback with the signature `function (error: Error, snapshotMap: Object): void;`, where `snapshotMap` is an object whose keys are the requested IDs, and the values are the requested `Snapshot`s
#### `queryFetch`
```javascript
backend.queryFetch(agent, index, query, options, callback);
```
Fetch snapshots that match the provided query. In most cases, querying the backing database directly should be preferred, but `queryFetch` can be used in order to apply middleware, whilst avoiding the overheads associated with using a `Doc` instance.
* `agent` _[`Agent`](#class-agent)_: connection agent to pass to the middleware
* `index` _string_: the name of the target collection or projection
* `query` _Object_: a query object, whose format will depend on the database adapter being used
* `options` _Object_: an object that may contain a `db` property, which specifies which database to run the query against. These extra databases can be attached via the `extraDbs` option in the `Backend` constructor
* `callback` _Function_: a callback with the signature `function (error: Error, snapshots: Snapshot[], extra: Object): void;`, where `snapshots` is an array of the snapshots matching the query, and `extra` is an (optional) object that the database adapter might return with more information about the results (such as counts)
### Class: `ShareDB.Agent`
An `Agent` is the representation of a client's `Connection` state on the server. If the `Connection` was created through `backend.connect` (ie the client is running on the server), then the `Agent` associated with a `Connection` can be accessed through a direct reference: `connection.agent`.
The `Agent` will be made available in all [middleware](#middlewares) requests. The `agent.custom` field is an object that can be used for storing arbitrary information for use in middleware. For example:
```javascript
backend.useMiddleware('connect', function (request, callback) {
// Best practice to clone to prevent mutating the object after connection.
// You may also want to consider a deep clone, depending on the shape of request.req.
Object.assign(request.agent.custom, request.req);
callback();
});
backend.useMiddleware('readSnapshots', function (request, callback) {
var connectionInfo = request.agent.custom;
var snapshots = request.snapshots;
// Use the information provided at connection to determine if a user can access snapshots.
// This should also be checked when fetching and submitting ops.
if (!userCanAccessSnapshots(connectionInfo, snapshots)) {
return callback(new Error('Authentication error'));
}
callback();
});
// Here you should determine what permissions a user has, probably by reading a cookie and
// potentially making some database request to check which documents they can access, or which
// roles they have, etc. If doing this asynchronously, make sure you call backend.connect
// after the permissions have been fetched.
var connectionInfo = getUserPermissions();
// Pass info in as the second argument. This will be made available as request.req in the
// 'connection' middleware.
var connection = backend.connect(null, connectionInfo);
```
### Logging

@@ -484,2 +713,3 @@

* 4024 - Invalid version
* 4025 - Passing options to subscribe has not been implemented

@@ -486,0 +716,0 @@ ### 5000 - Internal error

@@ -1,2 +0,2 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var Backend = require('../../lib/backend');

@@ -6,3 +6,2 @@ var Connection = require('../../lib/client/connection');

describe('client connection', function() {
beforeEach(function() {

@@ -46,4 +45,4 @@ this.backend = new Backend();

});
var connection = this.backend.connect();
})
this.backend.connect();
});

@@ -75,3 +74,3 @@ it('emits stopped event on call to agent.close()', function(done) {

expect(newStream).to.have.property('open', true);
expect(newStream).to.not.be(originalStream);
expect(newStream).to.not.equal(originalStream);
connection.close();

@@ -107,2 +106,23 @@ done();

it('updates after connection socket stream emits "close"', function(done) {
var backend = this.backend;
var connection = backend.connect();
connection.on('connected', function() {
connection.socket.stream.emit('close');
expect(backend.agentsCount).equal(0);
done();
});
});
it('updates correctly after stream emits both "end" and "close"', function(done) {
var backend = this.backend;
var connection = backend.connect();
connection.on('connected', function() {
connection.socket.stream.emit('end');
connection.socket.stream.emit('close');
expect(backend.agentsCount).equal(0);
done();
});
});
it('does not increment when agent connect is rejected', function() {

@@ -114,3 +134,3 @@ var backend = this.backend;

expect(backend.agentsCount).equal(0);
var connection = backend.connect();
backend.connect();
expect(backend.agentsCount).equal(0);

@@ -121,29 +141,28 @@ });

describe('state management using setSocket', function() {
it('initial connection.state is connecting, if socket.readyState is CONNECTING', function () {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-connecting
var socket = {readyState: 0};
var connection = new Connection(socket);
expect(connection.state).equal('connecting');
it('initial connection.state is connecting, if socket.readyState is CONNECTING', function() {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-connecting
var socket = {readyState: 0};
var connection = new Connection(socket);
expect(connection.state).equal('connecting');
});
it('initial connection.state is connecting, if socket.readyState is OPEN', function () {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-open
var socket = {readyState: 1};
var connection = new Connection(socket);
expect(connection.state).equal('connecting');
it('initial connection.state is connecting, if socket.readyState is OPEN', function() {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-open
var socket = {readyState: 1};
var connection = new Connection(socket);
expect(connection.state).equal('connecting');
});
it('initial connection.state is disconnected, if socket.readyState is CLOSING', function () {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closing
var socket = {readyState: 2};
var connection = new Connection(socket);
expect(connection.state).equal('disconnected');
it('initial connection.state is disconnected, if socket.readyState is CLOSING', function() {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closing
var socket = {readyState: 2};
var connection = new Connection(socket);
expect(connection.state).equal('disconnected');
});
it('initial connection.state is disconnected, if socket.readyState is CLOSED', function () {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closed
var socket = {readyState: 3};
var connection = new Connection(socket);
expect(connection.state).equal('disconnected');
it('initial connection.state is disconnected, if socket.readyState is CLOSED', function() {
// https://html.spec.whatwg.org/multipage/web-sockets.html#dom-websocket-closed
var socket = {readyState: 3};
var connection = new Connection(socket);
expect(connection.state).equal('disconnected');
});

@@ -186,5 +205,3 @@

});
});
});
var Backend = require('../../lib/backend');
var expect = require('expect.js');
var expect = require('chai').expect;
var util = require('../util');
var sinon = require('sinon');
var util = require('../util')
describe('client query subscribe', function() {
describe('Doc', function() {
beforeEach(function() {

@@ -19,28 +18,74 @@ this.backend = new Backend();

it('calling doc.destroy unregisters it', function() {
var doc = this.connection.get('dogs', 'fido');
expect(this.connection.getExisting('dogs', 'fido')).equal(doc);
it('calling doc.destroy unregisters it', function(done) {
var connection = this.connection;
var doc = connection.get('dogs', 'fido');
expect(connection.getExisting('dogs', 'fido')).equal(doc);
doc.destroy();
expect(this.connection.getExisting('dogs', 'fido')).equal(undefined);
doc.destroy(function(err) {
if (err) return done(err);
expect(connection.getExisting('dogs', 'fido')).equal(undefined);
var doc2 = this.connection.get('dogs', 'fido');
expect(doc).not.equal(doc2);
var doc2 = connection.get('dogs', 'fido');
expect(doc).not.equal(doc2);
done();
});
// destroy is async
expect(connection.getExisting('dogs', 'fido')).equal(doc);
});
it('getting then destroying then getting returns a new doc object', function() {
it('getting then destroying then getting returns a new doc object', function(done) {
var connection = this.connection;
var doc = connection.get('dogs', 'fido');
doc.destroy(function(err) {
if (err) return done(err);
var doc2 = connection.get('dogs', 'fido');
expect(doc).not.equal(doc2);
expect(doc).eql(doc2);
done();
});
});
it('doc.destroy() works without a callback', function() {
var doc = this.connection.get('dogs', 'fido');
doc.destroy();
var doc2 = this.connection.get('dogs', 'fido');
expect(doc).not.equal(doc2);
expect(doc).eql(doc2);
});
it('doc.destroy() calls back', function(done) {
var doc = this.connection.get('dogs', 'fido');
doc.destroy(done);
describe('when connection closed', function() {
beforeEach(function(done) {
this.op1 = [{p: ['snacks'], oi: true}];
this.op2 = [{p: ['color'], oi: 'gray'}];
this.doc = this.connection.get('dogs', 'fido');
this.doc.create({}, function(err) {
if (err) return done(err);
done();
});
});
it('do not mutate previously inflight op', function(done) {
var doc = this.doc;
var op1 = this.op1;
var op2 = this.op2;
var connection = this.connection;
this.connection.on('send', function() {
expect(doc.pendingOps).to.have.length(0);
expect(doc.inflightOp.op).to.eql(op1);
expect(doc.inflightOp.sentAt).to.not.be.undefined;
connection.close();
expect(doc.pendingOps).to.have.length(1);
doc.submitOp(op2);
expect(doc.pendingOps).to.have.length(2);
expect(doc.pendingOps[0].op).to.eql(op1);
expect(doc.pendingOps[1].op).to.eql(op2);
done();
});
this.doc.submitOp(this.op1, function() {
done(new Error('Connection should have been closed'));
});
});
});
describe('applyStack', function() {
beforeEach(function(done) {

@@ -171,3 +216,3 @@ this.doc = this.connection.get('dogs', 'fido');

doc.on('after op', afterOpBatchHandler);
doc.on('op', function(op, source) {
doc.on('op', function(op) {
receivedOps.push(op);

@@ -193,3 +238,2 @@ });

expect(receivedOps.length == remoteOp.length);
});

@@ -253,6 +297,40 @@

});
});
describe('submitting ops in callbacks', function() {
beforeEach(function() {
this.doc = this.connection.get('dogs', 'scooby');
});
it('succeeds with valid op', function(done) {
var doc = this.doc;
doc.create({name: 'Scooby Doo'}, function(error) {
expect(error).to.not.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}];
doc.submitOp(op, function(error) {
if (error) return done(error);
expect(doc.data).eql({name: 'Doo'});
done();
});
});
});
it('fails with invalid op', function(done) {
var doc = this.doc;
doc.create({name: 'Scooby Doo'}, function(error) {
expect(error).to.not.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).instanceOf(Error);
done();
});
});
});
});
describe('submitting an invalid op', function () {
describe('submitting an invalid op', function() {
var doc;

@@ -262,3 +340,3 @@ var invalidOp;

beforeEach(function (done) {
beforeEach(function(done) {
// This op is invalid because we try to perform a list deletion

@@ -268,6 +346,6 @@ // on something that isn't a list

validOp = {p:['snacks'], oi: true};
validOp = {p: ['snacks'], oi: true};
doc = this.connection.get('dogs', 'scooby');
doc.create({ name: 'Scooby' }, function (error) {
doc.create({name: 'Scooby'}, function(error) {
if (error) return done(error);

@@ -278,4 +356,4 @@ doc.whenNothingPending(done);

it('returns an error to the submitOp callback', function (done) {
doc.submitOp(invalidOp, function (error) {
it('returns an error to the submitOp callback', function(done) {
doc.submitOp(invalidOp, function(error) {
expect(error.message).to.equal('Referenced element not a list');

@@ -286,18 +364,18 @@ done();

it('rolls the doc back to a usable state', function (done) {
it('rolls the doc back to a usable state', function(done) {
util.callInSeries([
function (next) {
doc.submitOp(invalidOp, function (error) {
expect(error).to.be.ok();
function(next) {
doc.submitOp(invalidOp, function(error) {
expect(error).instanceOf(Error);
next();
});
},
function (next) {
function(next) {
doc.whenNothingPending(next);
},
function (next) {
function(next) {
expect(doc.data).to.eql({name: 'Scooby'});
doc.submitOp(validOp, next);
},
function (next) {
function(next) {
expect(doc.data).to.eql({name: 'Scooby', snacks: true});

@@ -310,3 +388,3 @@ next();

it('rescues an irreversible op collision', function (done) {
it('rescues an irreversible op collision', function(done) {
// This test case attempts to reconstruct the following corner case, with

@@ -324,5 +402,5 @@ // two independent references to the same document. We submit two simultaneous, but

var fireSubmit;
this.backend.use('submit', function (request, callback) {
this.backend.use('submit', function(request, callback) {
if (pauseSubmit) {
fireSubmit = function () {
fireSubmit = function() {
pauseSubmit = false;

@@ -338,16 +416,16 @@ callback();

util.callInSeries([
function (next) {
function(next) {
doc1.create({colours: ['white']}, next);
},
function (next) {
function(next) {
doc1.whenNothingPending(next);
},
function (next) {
function(next) {
doc2.fetch(next);
},
function (next) {
function(next) {
doc2.whenNothingPending(next);
},
// Both documents start off at the same v1 state, with colours as a list
function (next) {
function(next) {
expect(doc1.data).to.eql({colours: ['white']});

@@ -358,3 +436,3 @@ expect(doc2.data).to.eql({colours: ['white']});

// doc1 successfully submits an op which changes our list into a string in v2
function (next) {
function(next) {
doc1.submitOp({p: ['colours'], oi: 'white,black'}, next);

@@ -371,5 +449,5 @@ },

// 7. type.apply throws, because this is an invalid op
function (next) {
function(next) {
pauseSubmit = true;
doc2.submitOp({p: ['colours', '0'], li: 'black'}, function (error) {
doc2.submitOp({p: ['colours', '0'], li: 'black'}, function(error) {
expect(error.message).to.equal('Referenced element not a list');

@@ -379,3 +457,3 @@ next();

doc2.fetch(function (error) {
doc2.fetch(function(error) {
if (error) return next(error);

@@ -387,3 +465,3 @@ fireSubmit();

// workable state in v2
function (next) {
function(next) {
expect(doc1.data).to.eql({colours: 'white,black'});

@@ -390,0 +468,0 @@ expect(doc2.data).to.eql(doc1.data);

@@ -21,4 +21,4 @@ // A simple number type, where:

function transform(op1, op2, side) {
function transform(op1) {
return op1;
}

@@ -1,6 +0,5 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var Backend = require('../../lib/backend');
describe('client connection', function() {
beforeEach(function() {

@@ -92,3 +91,2 @@ this.backend = new Backend();

});
});

@@ -1,101 +0,126 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var util = require('../util');
module.exports = function() {
describe('client projections', function() {
module.exports = function(options) {
var getQuery = options.getQuery;
var matchAllQuery = getQuery({query: {}});
beforeEach(function(done) {
this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true});
this.connection = this.backend.connect();
var data = {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}};
this.connection.get('dogs', 'fido').create(data, done);
});
describe('client projections', function() {
beforeEach(function(done) {
this.backend.addProjection('dogs_summary', 'dogs', {age: true, owner: true});
this.connection = this.backend.connect();
var data = {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}};
this.connection.get('dogs', 'fido').create(data, done);
});
['fetch', 'subscribe'].forEach(function(method) {
it('snapshot ' + method, function(done) {
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido[method](function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 3, owner: {name: 'jim'}});
expect(fido.version).eql(1);
done();
['fetch', 'subscribe'].forEach(function(method) {
it('snapshot ' + method, function(done) {
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido[method](function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 3, owner: {name: 'jim'}});
expect(fido.version).eql(1);
done();
});
});
});
});
['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
it('snapshot ' + method, function(done) {
var connection2 = this.backend.connect();
connection2[method]('dogs_summary', {}, null, function(err, results) {
if (err) return done(err);
expect(results.length).eql(1);
expect(results[0].data).eql({age: 3, owner: {name: 'jim'}});
expect(results[0].version).eql(1);
done();
['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
it('snapshot ' + method, function(done) {
var connection2 = this.backend.connect();
connection2[method]('dogs_summary', 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'}});
expect(results[0].version).eql(1);
done();
});
});
});
});
function opTests(test) {
it('projected field', function(done) {
test.call(this,
{p: ['age'], na: 1},
{age: 4, owner: {name: 'jim'}},
done
);
});
function opTests(test) {
it('projected field', function(done) {
test.call(this,
{p: ['age'], na: 1},
{age: 4, owner: {name: 'jim'}},
done
);
});
it('non-projected field', function(done) {
test.call(this,
{p: ['color'], oi: 'brown', od: 'gold'},
{age: 3, owner: {name: 'jim'}},
done
);
});
it('non-projected field', function(done) {
test.call(this,
{p: ['color'], oi: 'brown', od: 'gold'},
{age: 3, owner: {name: 'jim'}},
done
);
});
it('parent field replace', function(done) {
test.call(this,
{p: [], oi: {age: 2, color: 'brown', owner: false}, od: {age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}}},
{age: 2, owner: false},
done
);
});
it('parent field replace', function(done) {
test.call(this,
{
p: [],
oi: {age: 2, color: 'brown', owner: false},
od: {age: 3, color: 'gold', owner: {name: 'jim'},
litter: {count: 4}}
},
{age: 2, owner: false},
done
);
});
it('parent field set', function(done) {
test.call(this,
{p: [], oi: {age: 2, color: 'brown', owner: false}},
{age: 2, owner: false},
done
);
});
it('parent field set', function(done) {
test.call(this,
{p: [], oi: {age: 2, color: 'brown', owner: false}},
{age: 2, owner: false},
done
);
});
it('projected child field', function(done) {
test.call(this,
{p: ['owner', 'sex'], oi: 'male'},
{age: 3, owner: {name: 'jim', sex: 'male'}},
done
);
});
it('projected child field', function(done) {
test.call(this,
{p: ['owner', 'sex'], oi: 'male'},
{age: 3, owner: {name: 'jim', sex: 'male'}},
done
);
});
it('non-projected child field', function(done) {
test.call(this,
{p: ['litter', 'count'], na: 1},
{age: 3, owner: {name: 'jim'}},
done
);
it('non-projected child field', function(done) {
test.call(this,
{p: ['litter', 'count'], na: 1},
{age: 3, owner: {name: 'jim'}},
done
);
});
}
describe('op fetch', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido.fetch(function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').submitOp(op, function(err) {
if (err) return done(err);
fido.fetch(function(err) {
if (err) return done(err);
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
});
});
});
};
opTests(test);
});
}
describe('op fetch', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido.fetch(function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').submitOp(op, function(err) {
describe('op subscribe', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido.subscribe(function(err) {
if (err) return done(err);
fido.fetch(function(err) {
if (err) return done(err);
fido.on('op', function() {
expect(fido.data).eql(expected);

@@ -105,37 +130,37 @@ expect(fido.version).eql(2);

});
connection.get('dogs', 'fido').submitOp(op);
});
});
};
opTests(test);
});
};
opTests(test);
});
describe('op subscribe', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido.subscribe(function(err) {
if (err) return done(err);
fido.on('op', function() {
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
describe('op fetch query', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido.fetch(function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').submitOp(op, function(err) {
if (err) return done(err);
connection2.createFetchQuery('dogs_summary', matchAllQuery, null, function(err) {
if (err) return done(err);
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
});
});
});
connection.get('dogs', 'fido').submitOp(op);
});
};
opTests(test);
});
};
opTests(test);
});
describe('op fetch query', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
fido.fetch(function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').submitOp(op, function(err) {
describe('op subscribe query', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
connection2.createSubscribeQuery('dogs_summary', matchAllQuery, null, function(err) {
if (err) return done(err);
connection2.createFetchQuery('dogs_summary', {}, null, function(err) {
if (err) return done(err);
fido.on('op', function() {
expect(fido.data).eql(expected);

@@ -145,101 +170,170 @@ expect(fido.version).eql(2);

});
connection.get('dogs', 'fido').submitOp(op);
});
};
opTests(test);
});
function queryUpdateTests(test) {
it('doc create', function(done) {
test.call(this,
function(connection, callback) {
var data = {age: 5, color: 'spotted', owner: {name: 'sue'}, litter: {count: 6}};
connection.get('dogs', 'spot').create(data, callback);
},
function(err, results) {
var sorted = util.sortById(results.slice());
expect(sorted.length).eql(2);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([
{age: 3, owner: {name: 'jim'}},
{age: 5, owner: {name: 'sue'}}
]);
done();
}
);
});
};
opTests(test);
});
}
describe('op subscribe query', function() {
function test(op, expected, done) {
var connection = this.connection;
var connection2 = this.backend.connect();
var fido = connection2.get('dogs_summary', 'fido');
connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
if (err) return done(err);
fido.on('op', function() {
expect(fido.data).eql(expected);
expect(fido.version).eql(2);
done();
describe('subscribe query', function() {
function test(trigger, callback) {
var connection = this.connection;
var connection2 = this.backend.connect();
var query = connection2.createSubscribeQuery('dogs_summary', matchAllQuery, null, function(err) {
if (err) return callback(err);
query.on('insert', function() {
callback(null, query.results);
});
trigger(connection);
});
connection.get('dogs', 'fido').submitOp(op);
}
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);
});
});
});
};
opTests(test);
});
});
function queryUpdateTests(test) {
it('doc create', function(done) {
test.call(this,
function(connection, callback) {
var data = {age: 5, color: 'spotted', owner: {name: 'sue'}, litter: {count: 6}};
connection.get('dogs', 'spot').create(data, callback);
},
function(err, results) {
var sorted = util.sortById(results.slice());
expect(sorted.length).eql(2);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([
{age: 3, owner: {name: 'jim'}},
{age: 5, owner: {name: 'sue'}}
]);
done();
}
);
describe('fetch query', function() {
function test(trigger, callback) {
var connection = this.connection;
var connection2 = this.backend.connect();
trigger(connection, function(err) {
if (err) return callback(err);
connection2.createFetchQuery('dogs_summary', matchAllQuery, null, callback);
});
}
queryUpdateTests(test);
});
}
describe('subscribe query', function() {
function test(trigger, callback) {
var connection = this.connection;
var connection2 = this.backend.connect();
var query = connection2.createSubscribeQuery('dogs_summary', {}, null, function(err) {
if (err) return callback(err);
query.on('insert', function() {
callback(null, query.results);
describe('submit on projected doc', function() {
function test(op, expected, done) {
var doc = this.connection.get('dogs', 'fido');
var projected = this.backend.connect().get('dogs_summary', 'fido');
projected.fetch(function(err) {
if (err) return done(err);
projected.submitOp(op, function(err) {
if (err) return done(err);
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql(expected);
expect(doc.version).equal(2);
done();
});
});
});
trigger(connection);
}
function testError(op, done) {
var doc = this.connection.get('dogs', 'fido');
var projected = this.backend.connect().get('dogs_summary', 'fido');
projected.fetch(function(err) {
if (err) return done(err);
projected.submitOp(op, function(err) {
expect(err).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}});
expect(doc.version).equal(1);
done();
});
});
});
}
it('can set on projected field', function(done) {
test.call(this,
{p: ['age'], na: 1},
{age: 4, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}},
done
);
});
}
queryUpdateTests(test);
});
describe('fetch query', function() {
function test(trigger, callback) {
var connection = this.connection;
var connection2 = this.backend.connect();
trigger(connection, function(err) {
if (err) return callback(err);
connection2.createFetchQuery('dogs_summary', {}, null, callback);
it('can set on child of projected field', function(done) {
test.call(this,
{p: ['owner', 'sex'], oi: 'male'},
{age: 3, color: 'gold', owner: {name: 'jim', sex: 'male'}, litter: {count: 4}},
done
);
});
}
queryUpdateTests(test);
});
describe('submit on projected doc', function() {
function test(op, expected, done) {
var doc = this.connection.get('dogs', 'fido');
var projected = this.backend.connect().get('dogs_summary', 'fido');
projected.fetch(function(err) {
if (err) return done(err);
projected.submitOp(op, function(err) {
it('cannot set on non-projected field', function(done) {
testError.call(this,
{p: ['color'], od: 'gold', oi: 'tan'},
done
);
});
it('cannot set on root path of projected doc', function(done) {
testError.call(this,
{p: [], oi: null},
done
);
});
it('can delete on projected doc', function(done) {
var doc = this.connection.get('dogs', 'fido');
var projected = this.backend.connect().get('dogs_summary', 'fido');
projected.fetch(function(err) {
if (err) return done(err);
doc.fetch(function(err) {
projected.del(function(err) {
if (err) return done(err);
expect(doc.data).eql(expected);
expect(doc.version).equal(2);
done();
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
expect(doc.version).equal(2);
done();
});
});
});
});
}
function testError(op, done) {
var doc = this.connection.get('dogs', 'fido');
var projected = this.backend.connect().get('dogs_summary', 'fido');
projected.fetch(function(err) {
if (err) return done(err);
projected.submitOp(op, function(err) {
expect(err).ok();
it('can create a projected doc with only projected fields', function(done) {
var doc = this.connection.get('dogs', 'spot');
var projected = this.backend.connect().get('dogs_summary', 'spot');
var data = {age: 5};
projected.create(data, function(err) {
if (err) return done(err);
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}});
expect(doc.data).eql({age: 5});
expect(doc.version).equal(1);

@@ -250,45 +344,13 @@ done();

});
}
it('can set on projected field', function(done) {
test.call(this,
{p: ['age'], na: 1},
{age: 4, color: 'gold', owner: {name: 'jim'}, litter: {count: 4}},
done
);
});
it('can set on child of projected field', function(done) {
test.call(this,
{p: ['owner', 'sex'], oi: 'male'},
{age: 3, color: 'gold', owner: {name: 'jim', sex: 'male'}, litter: {count: 4}},
done
);
});
it('cannot set on non-projected field', function(done) {
testError.call(this,
{p: ['color'], od: 'gold', oi: 'tan'},
done
);
});
it('cannot set on root path of projected doc', function(done) {
testError.call(this,
{p: [], oi: null},
done
);
});
it('can delete on projected doc', function(done) {
var doc = this.connection.get('dogs', 'fido');
var projected = this.backend.connect().get('dogs_summary', 'fido');
projected.fetch(function(err) {
if (err) return done(err);
projected.del(function(err) {
if (err) return done(err);
it('cannot create a projected doc with non-projected fields', function(done) {
var doc = this.connection.get('dogs', 'spot');
var projected = this.backend.connect().get('dogs_summary', 'spot');
var data = {age: 5, foo: 'bar'};
projected.create(data, function(err) {
expect(err).instanceOf(Error);
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
expect(doc.version).equal(2);
expect(doc.version).equal(0);
done();

@@ -299,35 +361,3 @@ });

});
it('can create a projected doc with only projected fields', function(done) {
var doc = this.connection.get('dogs', 'spot');
var projected = this.backend.connect().get('dogs_summary', 'spot');
var data = {age: 5};
projected.create(data, function(err) {
if (err) return done(err);
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 5});
expect(doc.version).equal(1);
done();
});
});
});
it('cannot create a projected doc with non-projected fields', function(done) {
var doc = this.connection.get('dogs', 'spot');
var projected = this.backend.connect().get('dogs_summary', 'spot');
var data = {age: 5, foo: 'bar'};
projected.create(data, function(err) {
expect(err).ok();
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
expect(doc.version).equal(0);
done();
});
});
});
});
});
};

@@ -1,2 +0,2 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var async = require('async');

@@ -6,157 +6,190 @@ var util = require('../util');

module.exports = function(options) {
var getQuery = options.getQuery;
var getQuery = options.getQuery;
describe('client query subscribe', function() {
before(function() {
if (!getQuery) return this.skip();
this.matchAllDbQuery = getQuery({query: {}});
});
it('creating a document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').create({age: 3});
describe('client query subscribe', function() {
before(function() {
if (!getQuery) return this.skip();
this.matchAllDbQuery = getQuery({query: {}});
});
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['fido']);
expect(util.pluck(docs, 'data')).eql([{age: 3}]);
expect(index).equal(0);
expect(util.pluck(query.results, 'id')).eql(['fido']);
expect(util.pluck(query.results, 'data')).eql([{age: 3}]);
done();
});
});
it('creating an additional document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
it('creating a document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.get('dogs', 'taco').create({age: 2});
connection.get('dogs', 'fido').on('error', done).create({age: 3});
});
query.on('error', done);
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['taco']);
expect(util.pluck(docs, 'data')).eql([{age: 2}]);
expect(query.results[index]).equal(docs[0]);
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
expect(util.pluck(docs, 'id')).eql(['fido']);
expect(util.pluck(docs, 'data')).eql([{age: 3}]);
expect(index).equal(0);
expect(util.pluck(query.results, 'id')).eql(['fido']);
expect(util.pluck(query.results, 'data')).eql([{age: 3}]);
done();
});
});
});
it('deleting a document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
it('creating an additional document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').del();
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.get('dogs', 'taco').on('error', done).create({age: 2});
});
query.on('error', done);
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['taco']);
expect(util.pluck(docs, 'data')).eql([{age: 2}]);
expect(query.results[index]).equal(docs[0]);
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
done();
});
});
query.on('remove', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['fido']);
expect(util.pluck(docs, 'data')).eql([undefined]);
expect(index).a('number');
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['spot']);
expect(util.pluck(results, 'data')).eql([{age: 5}]);
done();
});
it('deleting a document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').del();
});
query.on('error', done);
query.on('remove', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['fido']);
expect(util.pluck(docs, 'data')).eql([undefined]);
expect(index).a('number');
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['spot']);
expect(util.pluck(results, 'data')).eql([{age: 5}]);
done();
});
});
});
});
it('subscribed query does not get updated after destroyed', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
it('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').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection1.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
query.destroy(function(err) {
var query = connection2.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection2.get('dogs', 'taco').create({age: 2}, done);
connection1.get('dogs', 'fido').del();
});
query.on('error', done);
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}]);
});
});
query.on('insert', function() {
done();
});
});
});
it('subscribed query does not get updated after connection is disconnected', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
it('subscribed query does not get updated after destroyed', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
connection.close();
connection2.get('dogs', 'taco').create({age: 2}, done);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
query.destroy(function(err) {
if (err) return done(err);
connection2.get('dogs', 'taco').on('error', done).create({age: 2}, done);
});
});
query.on('error', done);
query.on('insert', function() {
done();
});
});
query.on('insert', function() {
done();
});
});
});
it('subscribed query gets update after reconnecting', function(done) {
var backend = this.backend;
var connection = backend.connect();
var connection2 = backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
it('subscribed query does not get updated after connection is disconnected', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
connection.close();
connection2.get('dogs', 'taco').create({age: 2});
process.nextTick(function() {
backend.connect(connection);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.close();
connection2.get('dogs', 'taco').on('error', done).create({age: 2}, done);
});
query.on('error', done);
query.on('insert', function() {
done();
});
});
query.on('insert', function() {
done();
});
});
});
it('subscribed query gets simultaneous insert and remove after reconnecting', function(done) {
var backend = this.backend;
var connection = backend.connect();
var connection2 = backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
it('subscribed query gets update after reconnecting', function(done) {
var backend = this.backend;
var connection = backend.connect();
var connection2 = backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
connection.close();
connection2.get('dogs', 'fido').fetch(function(err) {
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection2.get('dogs', 'fido').del();
connection2.get('dogs', 'taco').create({age: 2});
connection.close();
connection2.get('dogs', 'taco').on('error', done).create({age: 2});
process.nextTick(function() {

@@ -166,283 +199,347 @@ backend.connect(connection);

});
query.on('error', done);
query.on('insert', function() {
done();
});
});
var wait = 2;
function finish() {
if (--wait) return;
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['spot', 'taco']);
expect(util.pluck(results, 'data')).eql([{age: 5}, {age: 2}]);
done();
}
query.on('insert', function(docs) {
expect(util.pluck(docs, 'id')).eql(['taco']);
expect(util.pluck(docs, 'data')).eql([{age: 2}]);
finish();
});
it('subscribed query gets simultaneous insert and remove after reconnecting', function(done) {
var backend = this.backend;
var connection = backend.connect();
var connection2 = backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.close();
connection2.get('dogs', 'fido').fetch(function(err) {
if (err) return done(err);
connection2.get('dogs', 'fido').del();
connection2.get('dogs', 'taco').on('error', done).create({age: 2});
process.nextTick(function() {
backend.connect(connection);
});
});
});
query.on('error', done);
var wait = 2;
function finish() {
if (--wait) return;
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['spot', 'taco']);
expect(util.pluck(results, 'data')).eql([{age: 5}, {age: 2}]);
done();
}
query.on('insert', function(docs) {
expect(util.pluck(docs, 'id')).eql(['taco']);
expect(util.pluck(docs, 'data')).eql([{age: 2}]);
finish();
});
query.on('remove', function(docs) {
expect(util.pluck(docs, 'id')).eql(['fido']);
// 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();
});
});
query.on('remove', function(docs) {
expect(util.pluck(docs, 'id')).eql(['fido']);
expect(util.pluck(docs, 'data')).eql([undefined]);
finish();
});
});
});
it('creating an additional document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
it('creating an additional document updates a subscribed query', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
connection.get('dogs', 'taco').create({age: 2});
var query = connection.createSubscribeQuery('dogs', matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.get('dogs', 'taco').on('error', done).create({age: 2});
});
query.on('error', done);
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['taco']);
expect(util.pluck(docs, 'data')).eql([{age: 2}]);
expect(query.results[index]).equal(docs[0]);
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
done();
});
});
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['taco']);
expect(util.pluck(docs, 'data')).eql([{age: 2}]);
expect(query.results[index]).equal(docs[0]);
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['fido', 'spot', 'taco']);
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}, {age: 2}]);
done();
});
});
});
it('pollDebounce option reduces subsequent poll interval', function(done) {
var connection = this.backend.connect();
this.backend.db.canPollDoc = function() {
return false;
};
var query = connection.createSubscribeQuery('items', this.matchAllDbQuery, {pollDebounce: 1000});
var batchSizes = [];
var total = 0;
it('pollDebounce option reduces subsequent poll interval', function(done) {
var connection = this.backend.connect();
this.backend.db.canPollDoc = function() {
return false;
};
var query = connection.createSubscribeQuery('items', this.matchAllDbQuery, {pollDebounce: 1000});
query.on('error', done);
var batchSizes = [];
var total = 0;
query.on('insert', function(docs) {
batchSizes.push(docs.length);
total += docs.length;
if (total === 1) {
query.on('insert', function(docs) {
batchSizes.push(docs.length);
total += docs.length;
if (total === 1) {
// first write received by client. we're debouncing. create 9
// more documents.
for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({});
}
if (total === 10) {
for (var i = 1; i < 10; i++) {
connection.get('items', i.toString()).on('error', done).create({});
}
}
if (total === 10) {
// first document is its own batch; then subsequent creates
// are debounced until after all other 9 docs are created
expect(batchSizes).eql([1, 9]);
done();
}
expect(batchSizes).eql([1, 9]);
done();
}
});
// create an initial document. this will lead to the 'insert'
// event firing the first time, while sharedb is definitely
// debouncing
connection.get('items', '0').on('error', done).create({});
});
// create an initial document. this will lead to the 'insert'
// event firing the first time, while sharedb is definitely
// debouncing
connection.get('items', '0').create({});
});
it('db.pollDebounce option reduces subsequent poll interval', function(done) {
var connection = this.backend.connect();
this.backend.db.canPollDoc = function() {
return false;
};
this.backend.db.pollDebounce = 1000;
var query = connection.createSubscribeQuery('items', this.matchAllDbQuery);
query.on('error', done);
var batchSizes = [];
var total = 0;
it('db.pollDebounce option reduces subsequent poll interval', function(done) {
var connection = this.backend.connect();
this.backend.db.canPollDoc = function() {
return false;
};
this.backend.db.pollDebounce = 1000;
var query = connection.createSubscribeQuery('items', this.matchAllDbQuery);
var batchSizes = [];
var total = 0;
query.on('insert', function(docs) {
batchSizes.push(docs.length);
total += docs.length;
if (total === 1) {
query.on('insert', function(docs) {
batchSizes.push(docs.length);
total += docs.length;
if (total === 1) {
// first write received by client. we're debouncing. create 9
// more documents.
for (var i = 1; i < 10; i++) connection.get('items', i.toString()).create({});
}
if (total === 10) {
for (var i = 1; i < 10; i++) {
connection.get('items', i.toString()).on('error', done).create({});
}
}
if (total === 10) {
// first document is its own batch; then subsequent creates
// are debounced until after all other 9 docs are created
expect(batchSizes).eql([1, 9]);
done();
}
});
expect(batchSizes).eql([1, 9]);
done();
}
});
// create an initial document. this will lead to the 'insert'
// event firing the first time, while sharedb is definitely
// debouncing
connection.get('items', '0').create({});
});
it('pollInterval updates a subscribed query after an unpublished create', function(done) {
var connection = this.backend.connect();
this.backend.suppressPublish = true;
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').create({});
// create an initial document. this will lead to the 'insert'
// event firing the first time, while sharedb is definitely
// debouncing
connection.get('items', '0').on('error', done).create({});
});
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['fido']);
done();
});
});
it('db.pollInterval updates a subscribed query after an unpublished create', function(done) {
var connection = this.backend.connect();
this.backend.suppressPublish = true;
this.backend.db.pollInterval = 50;
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').create({});
it('pollInterval updates a subscribed query after an unpublished create', function(done) {
var connection = this.backend.connect();
this.backend.suppressPublish = true;
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').on('error', done).create({});
});
query.on('error', done);
query.on('insert', function(docs) {
expect(util.pluck(docs, 'id')).eql(['fido']);
done();
});
});
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['fido']);
done();
});
});
it('pollInterval captures additional unpublished creates', function(done) {
var connection = this.backend.connect();
this.backend.suppressPublish = true;
var count = 0;
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
if (err) return done(err);
connection.get('dogs', count.toString()).create({});
});
query.on('insert', function() {
count++;
if (count === 3) return done();
connection.get('dogs', count.toString()).create({});
});
});
it('query extra is returned to client', function(done) {
var connection = this.backend.connect();
this.backend.db.query = function(collection, query, fields, options, callback) {
process.nextTick(function() {
callback(null, [], {colors: ['brown', 'gold']});
it('db.pollInterval updates a subscribed query after an unpublished create', function(done) {
var connection = this.backend.connect();
this.backend.suppressPublish = true;
this.backend.db.pollInterval = 50;
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').on('error', done).create({});
});
};
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
if (err) return done(err);
expect(results).eql([]);
expect(extra).eql({colors: ['brown', 'gold']});
expect(query.extra).eql({colors: ['brown', 'gold']});
done();
query.on('error', done);
query.on('insert', function(docs) {
expect(util.pluck(docs, 'id')).eql(['fido']);
done();
});
});
});
it('query extra is updated on change', function(done) {
var connection = this.backend.connect();
this.backend.db.query = function(collection, query, fields, options, callback) {
process.nextTick(function() {
callback(null, [], 1);
it('pollInterval captures additional unpublished creates', function(done) {
var connection = this.backend.connect();
this.backend.suppressPublish = true;
var count = 0;
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, {pollInterval: 50}, function(err) {
if (err) return done(err);
connection.get('dogs', count.toString()).on('error', done).create({});
});
};
this.backend.db.queryPoll = function(collection, query, options, callback) {
process.nextTick(function() {
callback(null, [], 2);
query.on('error', done);
query.on('insert', function() {
count++;
if (count === 3) return done();
connection.get('dogs', count.toString()).on('error', done).create({});
});
};
this.backend.db.canPollDoc = function() {
return false;
};
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
if (err) return done(err);
expect(extra).eql(1);
expect(query.extra).eql(1);
});
query.on('extra', function(extra) {
expect(extra).eql(2);
expect(query.extra).eql(2);
done();
});
connection.get('dogs', 'fido').create({age: 3});
});
it('changing a filtered property removes from a subscribed query', function(done) {
var connection = this.backend.connect();
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 3}, cb); }
], function(err) {
if (err) return done(err);
var dbQuery = getQuery({query: {age: 3}});
var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
it('query extra is returned to client', function(done) {
var connection = this.backend.connect();
this.backend.db.query = function(collection, query, fields, options, callback) {
process.nextTick(function() {
callback(null, [], {colors: ['brown', 'gold']});
});
};
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 3}]);
connection.get('dogs', 'fido').submitOp({p: ['age'], na: 2});
});
query.on('remove', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['fido']);
expect(util.pluck(docs, 'data')).eql([{age: 5}]);
expect(index).a('number');
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['spot']);
expect(util.pluck(results, 'data')).eql([{age: 3}]);
expect(results).eql([]);
expect(extra).eql({colors: ['brown', 'gold']});
expect(query.extra).eql({colors: ['brown', 'gold']});
done();
});
query.on('error', done);
});
});
it('changing a filtered property inserts to a subscribed query', function(done) {
var connection = this.backend.connect();
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var dbQuery = getQuery({query: {age: 3}});
var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
it('query extra is updated on change', function(done) {
var connection = this.backend.connect();
this.backend.db.query = function(collection, query, fields, options, callback) {
process.nextTick(function() {
callback(null, [], 1);
});
};
this.backend.db.queryPoll = function(collection, query, options, callback) {
process.nextTick(function() {
callback(null, [], 2);
});
};
this.backend.db.canPollDoc = function() {
return false;
};
var query = connection.createSubscribeQuery('dogs', this.matchAllDbQuery, null, function(err, results, extra) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido']);
expect(util.pluck(sorted, 'data')).eql([{age: 3}]);
connection.get('dogs', 'spot').submitOp({p: ['age'], na: -2});
expect(extra).eql(1);
expect(query.extra).eql(1);
});
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['spot']);
expect(util.pluck(docs, 'data')).eql([{age: 3}]);
expect(index).a('number');
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['fido', 'spot']);
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 3}]);
query.on('error', done);
query.on('extra', function(extra) {
expect(extra).eql(2);
expect(query.extra).eql(2);
done();
});
connection.get('dogs', 'fido').on('error', done).create({age: 3});
});
});
it('changing a sorted property moves in a subscribed query', function(done) {
var connection = this.backend.connect();
it('changing a filtered property removes from a subscribed query', function(done) {
var connection = this.backend.connect();
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 3}, cb);
}
], function(err) {
if (err) return done(err);
var dbQuery = getQuery({query: {age: 3}});
var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 3}]);
connection.get('dogs', 'fido').submitOp({p: ['age'], na: 2});
});
query.on('error', done);
query.on('remove', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['fido']);
expect(util.pluck(docs, 'data')).eql([{age: 5}]);
expect(index).a('number');
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['spot']);
expect(util.pluck(results, 'data')).eql([{age: 3}]);
done();
});
});
});
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); }
], function(err) {
if (err) return done(err);
var dbQuery = getQuery({query: {}, sort: [['age', 1]]});
var query = connection.createSubscribeQuery(
'dogs',
dbQuery,
null,
function(err, results) {
it('changing a filtered property inserts to a subscribed query', function(done) {
var connection = this.backend.connect();
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
var dbQuery = getQuery({query: {age: 3}});
var query = connection.createSubscribeQuery('dogs', dbQuery, null, function(err, results) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido']);
expect(util.pluck(sorted, 'data')).eql([{age: 3}]);
connection.get('dogs', 'spot').submitOp({p: ['age'], na: -2});
});
query.on('error', done);
query.on('insert', function(docs, index) {
expect(util.pluck(docs, 'id')).eql(['spot']);
expect(util.pluck(docs, 'data')).eql([{age: 3}]);
expect(index).a('number');
var results = util.sortById(query.results);
expect(util.pluck(results, 'id')).eql(['fido', 'spot']);
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}]);
connection.get('dogs', 'spot').submitOp({p: ['age'], na: -3});
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 3}]);
done();
});
});
});
query.on('move', function(docs, from, to) {
expect(docs.length).eql(1);
expect(from).a('number');
expect(to).a('number');
expect(util.pluck(query.results, 'id')).eql(['spot', 'fido']);
expect(util.pluck(query.results, 'data')).eql([{age: 2}, {age: 3}]);
done();
it('changing a sorted property moves in a subscribed query', function(done) {
var connection = this.backend.connect();
async.parallel([
function(cb) {
connection.get('dogs', 'fido').on('error', done).create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').on('error', done).create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
var dbQuery = getQuery({query: {}, sort: [['age', 1]]});
var query = connection.createSubscribeQuery(
'dogs',
dbQuery,
null,
function(err, results) {
if (err) return done(err);
expect(util.pluck(results, 'id')).eql(['fido', 'spot']);
expect(util.pluck(results, 'data')).eql([{age: 3}, {age: 5}]);
connection.get('dogs', 'spot').submitOp({p: ['age'], na: -3});
});
query.on('error', done);
query.on('move', function(docs, from, to) {
expect(docs.length).eql(1);
expect(from).a('number');
expect(to).a('number');
expect(util.pluck(query.results, 'id')).eql(['spot', 'fido']);
expect(util.pluck(query.results, 'data')).eql([{age: 2}, {age: 3}]);
done();
});
});
});
});
});
};

@@ -1,2 +0,2 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var async = require('async');

@@ -6,70 +6,60 @@ var util = require('../util');

module.exports = function(options) {
var getQuery = options.getQuery;
var getQuery = options.getQuery;
describe('client query', function() {
before(function() {
if (!getQuery) return this.skip();
this.matchAllDbQuery = getQuery({query: {}});
});
['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
it(method + ' on an empty collection', function(done) {
var connection = this.backend.connect();
connection[method]('dogs', this.matchAllDbQuery, null, function(err, results) {
if (err) return done(err);
expect(results).eql([]);
done();
});
describe('client query', function() {
before(function() {
if (!getQuery) return this.skip();
this.matchAllDbQuery = getQuery({query: {}});
});
it(method + ' on collection with fetched docs', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
], function(err) {
if (err) return done(err);
connection[method]('dogs', matchAllDbQuery, null, function(err, results) {
['createFetchQuery', 'createSubscribeQuery'].forEach(function(method) {
it(method + ' on an empty collection', function(done) {
var connection = this.backend.connect();
connection[method]('dogs', this.matchAllDbQuery, null, function(err, results) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
expect(results).eql([]);
done();
});
});
});
it(method + ' on collection with unfetched docs', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
], function(err) {
if (err) return done(err);
connection2[method]('dogs', matchAllDbQuery, null, function(err, results) {
it(method + ' on collection with fetched docs', function(done) {
var connection = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').create({age: 5}, cb);
},
function(cb) {
connection.get('cats', 'finn').create({age: 2}, cb);
}
], function(err) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
done();
connection[method]('dogs', matchAllDbQuery, null, function(err, results) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
done();
});
});
});
});
it(method + ' on collection with one fetched doc', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
], function(err) {
if (err) return done(err);
connection2.get('dogs', 'fido').fetch(function(err) {
it(method + ' on collection with unfetched docs', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').create({age: 5}, cb);
},
function(cb) {
connection.get('cats', 'finn').create({age: 2}, cb);
}
], function(err) {
if (err) return done(err);

@@ -85,31 +75,26 @@ connection2[method]('dogs', matchAllDbQuery, null, function(err, results) {

});
});
it(method + ' on collection with one fetched doc missing an op', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
], function(err) {
if (err) return done(err);
connection2.get('dogs', 'fido').fetch(function(err) {
it(method + ' on collection with one fetched doc', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').create({age: 5}, cb);
},
function(cb) {
connection.get('cats', 'finn').create({age: 2}, cb);
}
], function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], function(err) {
connection2.get('dogs', 'fido').fetch(function(err) {
if (err) return done(err);
// The results option is meant for making resubscribing more
// efficient and has no effect on query fetching
var options = {
results: [
connection2.get('dogs', 'fido'),
connection2.get('dogs', 'spot')
]
};
connection2[method]('dogs', matchAllDbQuery, options, function(err, results) {
connection2[method]('dogs', matchAllDbQuery, null, function(err, results) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([{age: 4}, {age: 5}]);
expect(util.pluck(sorted, 'data')).eql([{age: 3}, {age: 5}]);
done();

@@ -120,7 +105,44 @@ });

});
it(method + ' on collection with one fetched doc missing an op', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var matchAllDbQuery = this.matchAllDbQuery;
async.parallel([
function(cb) {
connection.get('dogs', 'fido').create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').create({age: 5}, cb);
},
function(cb) {
connection.get('cats', 'finn').create({age: 2}, cb);
}
], function(err) {
if (err) return done(err);
connection2.get('dogs', 'fido').fetch(function(err) {
if (err) return done(err);
connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], function(err) {
if (err) return done(err);
// The results option is meant for making resubscribing more
// efficient and has no effect on query fetching
var options = {
results: [
connection2.get('dogs', 'fido'),
connection2.get('dogs', 'spot')
]
};
connection2[method]('dogs', matchAllDbQuery, options, function(err, results) {
if (err) return done(err);
var sorted = util.sortById(results);
expect(util.pluck(sorted, 'id')).eql(['fido', 'spot']);
expect(util.pluck(sorted, 'data')).eql([{age: 4}, {age: 5}]);
done();
});
});
});
});
});
});
});
});
};
var Backend = require('../../lib/backend');
var expect = require('expect.js');
var expect = require('chai').expect;
var util = require('../util');

@@ -9,3 +9,3 @@ var lolex = require('lolex');

describe('SnapshotTimestampRequest', function () {
describe('SnapshotTimestampRequest', function() {
var backend;

@@ -21,8 +21,8 @@ var clock;

beforeEach(function () {
clock = lolex.install({ now: day1 });
beforeEach(function() {
clock = lolex.install({now: day1});
backend = new Backend();
});
afterEach(function (done) {
afterEach(function(done) {
clock.uninstall();

@@ -32,3 +32,3 @@ backend.close(done);

describe('a document with some simple versions separated by a day', function () {
describe('a document with some simple versions separated by a day', function() {
var v0 = {

@@ -74,15 +74,15 @@ id: 'time-machine',

beforeEach(function (done) {
beforeEach(function(done) {
var doc = backend.connect().get('books', 'time-machine');
util.callInSeries([
function (next) {
doc.create({ title: 'The Time Machine' }, next);
function(next) {
doc.create({title: 'The Time Machine'}, next);
},
function (next) {
function(next) {
clock.tick(ONE_DAY);
doc.submitOp({ p: ['author'], oi: 'HG Wells' }, next);
doc.submitOp({p: ['author'], oi: 'HG Wells'}, next);
},
function (next) {
function(next) {
clock.tick(ONE_DAY);
doc.submitOp({ p: ['author'], od: 'HG Wells', oi: 'H.G. Wells' }, next);
doc.submitOp({p: ['author'], od: 'HG Wells', oi: 'H.G. Wells'}, next);
},

@@ -93,8 +93,8 @@ done

it('fetches the version at exactly day 1', function (done) {
it('fetches the version at exactly day 1', function(done) {
util.callInSeries([
function (next) {
function(next) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(v1);

@@ -107,8 +107,8 @@ next();

it('fetches the version at exactly day 2', function (done) {
it('fetches the version at exactly day 2', function(done) {
util.callInSeries([
function (next) {
function(next) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day2, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(v2);

@@ -121,8 +121,8 @@ next();

it('fetches the version at exactly day 3', function (done) {
it('fetches the version at exactly day 3', function(done) {
util.callInSeries([
function (next) {
function(next) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(v3);

@@ -135,9 +135,9 @@ next();

it('fetches the day 2 version when asking for a time halfway between days 2 and 3', function (done) {
it('fetches the day 2 version when asking for a time halfway between days 2 and 3', function(done) {
var halfwayBetweenDays2and3 = (day2 + day3) * 0.5;
util.callInSeries([
function (next) {
function(next) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', halfwayBetweenDays2and3, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(v2);

@@ -150,8 +150,8 @@ next();

it('fetches the day 3 version when asking for a time after day 3', function (done) {
it('fetches the day 3 version when asking for a time after day 3', function(done) {
util.callInSeries([
function (next) {
function(next) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day4, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(v3);

@@ -164,8 +164,8 @@ next();

it('fetches the most recent version when not specifying a timestamp', function (done) {
it('fetches the most recent version when not specifying a timestamp', function(done) {
util.callInSeries([
function (next) {
function(next) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(v3);

@@ -178,8 +178,8 @@ next();

it('fetches an empty snapshot if the timestamp is before the document creation', function (done) {
it('fetches an empty snapshot if the timestamp is before the document creation', function(done) {
util.callInSeries([
function (next) {
function(next) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day0, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(v0);

@@ -192,36 +192,36 @@ next();

it('throws if the timestamp is undefined', function () {
var fetch = function () {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function () {});
it('throws if the timestamp is undefined', function() {
var fetch = function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', undefined, function() {});
};
expect(fetch).to.throwError();
expect(fetch).to.throw(Error);
});
it('throws without a callback', function () {
var fetch = function () {
it('throws without a callback', function() {
var fetch = function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine');
};
expect(fetch).to.throwError();
expect(fetch).to.throw(Error);
});
it('throws if the timestamp is -1', function () {
var fetch = function () {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', -1, function () { });
it('throws if the timestamp is -1', function() {
var fetch = function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', -1, function() { });
};
expect(fetch).to.throwError();
expect(fetch).to.throw(Error);
});
it('errors if the timestamp is a string', function () {
var fetch = function () {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', 'foo', function () { });
}
it('errors if the timestamp is a string', function() {
var fetch = function() {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', 'foo', function() { });
};
expect(fetch).to.throwError();
expect(fetch).to.throw(Error);
});
it('returns an empty snapshot if trying to fetch a non-existent document', function (done) {
backend.connect().fetchSnapshotByTimestamp('books', 'does-not-exist', day1, function (error, snapshot) {
it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
backend.connect().fetchSnapshotByTimestamp('books', 'does-not-exist', day1, function(error, snapshot) {
if (error) return done(error);

@@ -239,17 +239,18 @@ expect(snapshot).to.eql({

it('starts pending, and finishes not pending', function (done) {
it('starts pending, and finishes not pending', function(done) {
var connection = backend.connect();
connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function (error, snapshot) {
expect(connection.hasPending()).to.be(false);
connection.fetchSnapshotByTimestamp('books', 'time-machine', null, function(error) {
if (error) return done(error);
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) {
it('deletes the request from the connection', function(done) {
var connection = backend.connect();
connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) {
connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) {
if (error) return done(error);

@@ -263,6 +264,6 @@ expect(connection._snapshotRequests).to.eql({});

it('emits a ready event when done', function (done) {
it('emits a ready event when done', function(done) {
var connection = backend.connect();
connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) {
connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) {
if (error) return done(error);

@@ -275,7 +276,7 @@ });

it('fires the connection.whenNothingPending', function (done) {
it('fires the connection.whenNothingPending', function(done) {
var connection = backend.connect();
var snapshotFetched = false;
connection.fetchSnapshotByTimestamp('books', 'time-machine', function (error) {
connection.fetchSnapshotByTimestamp('books', 'time-machine', function(error) {
if (error) return done(error);

@@ -285,4 +286,4 @@ snapshotFetched = true;

connection.whenNothingPending(function () {
expect(snapshotFetched).to.be(true);
connection.whenNothingPending(function() {
expect(snapshotFetched).to.equal(true);
done();

@@ -292,3 +293,3 @@ });

it('can drop its connection and reconnect, and the callback is just called once', function (done) {
it('can drop its connection and reconnect, and the callback is just called once', function(done) {
var connection = backend.connect();

@@ -306,3 +307,3 @@

var connectionInterrupted = false;
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
if (!connectionInterrupted) {

@@ -320,3 +321,3 @@ connection.close();

it('cannot send the same request twice over a connection', function (done) {
it('cannot send the same request twice over a connection', function(done) {
var connection = backend.connect();

@@ -332,3 +333,3 @@

var hasResent = false;
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
if (!hasResent) {

@@ -345,8 +346,8 @@ connection._snapshotRequests[1]._onConnectionStateChanged();

describe('readSnapshots middleware', function () {
it('triggers the middleware', function (done) {
describe('readSnapshots middleware', function() {
it('triggers the middleware', function(done) {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
function (request) {
function(request) {
expect(request.snapshots[0]).to.eql(v3);
expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byTimestamp);
expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byTimestamp);
done();

@@ -356,16 +357,16 @@ }

backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, function () { });
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day3, function() { });
});
it('can have its snapshot manipulated in the middleware', function (done) {
it('can have its snapshot manipulated in the middleware', function(done) {
backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
function (request, callback) {
function(request, callback) {
request.snapshots[0].data.title = 'Alice in Wonderland';
callback();
},
}
];
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function (error, snapshot) {
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data.title).to.be('Alice in Wonderland');
expect(snapshot.data.title).to.equal('Alice in Wonderland');
done();

@@ -375,11 +376,11 @@ });

it('respects errors thrown in the middleware', function (done) {
it('respects errors thrown in the middleware', function(done) {
backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
function (request, callback) {
callback({ message: 'foo' });
},
function(request, callback) {
callback({message: 'foo'});
}
];
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function (error, snapshot) {
expect(error.message).to.be('foo');
backend.connect().fetchSnapshotByTimestamp('books', 'time-machine', day1, function(error) {
expect(error.message).to.equal('foo');
done();

@@ -390,13 +391,13 @@ });

describe('with a registered projection', function () {
beforeEach(function () {
backend.addProjection('bookTitles', 'books', { title: true });
describe('with a registered projection', function() {
beforeEach(function() {
backend.addProjection('bookTitles', 'books', {title: true});
});
it('applies the projection to a snapshot', function (done) {
backend.connect().fetchSnapshotByTimestamp('bookTitles', 'time-machine', day2, function (error, snapshot) {
it('applies the projection to a snapshot', function(done) {
backend.connect().fetchSnapshotByTimestamp('bookTitles', 'time-machine', day2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data.title).to.be('The Time Machine');
expect(snapshot.data.author).to.be(undefined);
expect(snapshot.data.title).to.equal('The Time Machine');
expect(snapshot.data.author).to.equal(undefined);
done();

@@ -408,3 +409,3 @@ });

describe('milestone snapshots enabled for every other version', function () {
describe('milestone snapshots enabled for every other version', function() {
var milestoneDb;

@@ -414,4 +415,4 @@ var db;

beforeEach(function () {
var options = { interval: 2 };
beforeEach(function() {
var options = {interval: 2};
db = new MemoryDb();

@@ -425,8 +426,8 @@ milestoneDb = new MemoryMilestoneDb(options);

afterEach(function (done) {
afterEach(function(done) {
backendWithMilestones.close(done);
});
describe('a doc with some versions in the milestone database', function () {
beforeEach(function (done) {
describe('a doc with some versions in the milestone database', function() {
beforeEach(function(done) {
clock.reset();

@@ -437,20 +438,20 @@

util.callInSeries([
function (next) {
doc.create({ title: 'To Kill a Mocking Bird' }, next);
function(next) {
doc.create({title: 'To Kill a Mocking Bird'}, next);
},
function (next) {
function(next) {
clock.tick(ONE_DAY);
doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, next);
doc.submitOp({p: ['author'], oi: 'Harper Lea'}, next);
},
function (next) {
function(next) {
clock.tick(ONE_DAY);
doc.submitOp({ p: ['author'], od: 'Harper Lea', oi: 'Harper Lee' }, next);
doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next);
},
function (next) {
function(next) {
clock.tick(ONE_DAY);
doc.submitOp({ p: ['year'], oi: 1959 }, next);
doc.submitOp({p: ['year'], oi: 1959}, next);
},
function (next) {
function(next) {
clock.tick(ONE_DAY);
doc.submitOp({ p: ['year'], od: 1959, oi: 1960 }, next);
doc.submitOp({p: ['year'], od: 1959, oi: 1960}, next);
},

@@ -461,3 +462,3 @@ done

it('fetches a snapshot between two milestones using the milestones', function (done) {
it('fetches a snapshot between two milestones using the milestones', function(done) {
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');

@@ -472,8 +473,8 @@ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');

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.data).to.eql({ title: 'To Kill a Mocking Bird', author: 'Harper Lee' });
expect(snapshot.v).to.equal(3);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
done();

@@ -483,3 +484,3 @@ });

it('fetches a snapshot that matches a milestone snapshot', function (done) {
it('fetches a snapshot that matches a milestone snapshot', function(done) {
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');

@@ -489,10 +490,10 @@ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');

backendWithMilestones.connect()
.fetchSnapshotByTimestamp('books', 'mocking-bird', day2, function (error, snapshot) {
.fetchSnapshotByTimestamp('books', 'mocking-bird', day2, function(error, snapshot) {
if (error) return done(error);
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true);
expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.be(true);
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.equal(true);
expect(milestoneDb.getMilestoneSnapshotAtOrAfterTime.calledOnce).to.equal(true);
expect(snapshot.v).to.be(2);
expect(snapshot.data).to.eql({ title: 'To Kill a Mocking Bird', author: 'Harper Lea' });
expect(snapshot.v).to.equal(2);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lea'});
done();

@@ -502,3 +503,3 @@ });

it('fetches a snapshot before any milestones', function (done) {
it('fetches a snapshot before any milestones', function(done) {
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');

@@ -509,11 +510,11 @@ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');

backendWithMilestones.connect()
.fetchSnapshotByTimestamp('books', 'mocking-bird', day1, function (error, snapshot) {
.fetchSnapshotByTimestamp('books', 'mocking-bird', day1, function(error, snapshot) {
if (error) return done(error);
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true);
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.data).to.eql({ title: 'To Kill a Mocking Bird' });
expect(snapshot.v).to.equal(1);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird'});
done();

@@ -523,3 +524,3 @@ });

it('fetches a snapshot after any milestones', function (done) {
it('fetches a snapshot after any milestones', function(done) {
sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrBeforeTime');

@@ -530,10 +531,10 @@ sinon.spy(milestoneDb, 'getMilestoneSnapshotAtOrAfterTime');

backendWithMilestones.connect()
.fetchSnapshotByTimestamp('books', 'mocking-bird', day5, function (error, snapshot) {
.fetchSnapshotByTimestamp('books', 'mocking-bird', day5, function(error, snapshot) {
if (error) return done(error);
expect(milestoneDb.getMilestoneSnapshotAtOrBeforeTime.calledOnce).to.be(true);
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({

@@ -540,0 +541,0 @@ title: 'To Kill a Mocking Bird',

var Backend = require('../../lib/backend');
var expect = require('expect.js');
var expect = require('chai').expect;
var MemoryDb = require('../../lib/db/memory');

@@ -8,14 +8,14 @@ var MemoryMilestoneDb = require('../../lib/milestone-db/memory');

describe('SnapshotVersionRequest', function () {
describe('SnapshotVersionRequest', function() {
var backend;
beforeEach(function () {
beforeEach(function() {
backend = new Backend();
});
afterEach(function (done) {
afterEach(function(done) {
backend.close(done);
});
describe('a document with some simple versions', function () {
describe('a document with some simple versions', function() {
var v0 = {

@@ -61,9 +61,9 @@ id: 'don-quixote',

beforeEach(function (done) {
beforeEach(function(done) {
var doc = backend.connect().get('books', 'don-quixote');
doc.create({ title: 'Don Quixote' }, function (error) {
doc.create({title: 'Don Quixote'}, function(error) {
if (error) return done(error);
doc.submitOp({ p: ['author'], oi: 'Miguel de Cervante' }, function (error) {
doc.submitOp({p: ['author'], oi: 'Miguel de Cervante'}, function(error) {
if (error) return done(error);
doc.submitOp({ p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes' }, done);
doc.submitOp({p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes'}, done);
});

@@ -73,4 +73,4 @@ });

it('fetches v1', function (done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 1, function (error, snapshot) {
it('fetches v1', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 1, function(error, snapshot) {
if (error) return done(error);

@@ -82,4 +82,4 @@ expect(snapshot).to.eql(v1);

it('fetches v2', function (done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 2, function (error, snapshot) {
it('fetches v2', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 2, function(error, snapshot) {
if (error) return done(error);

@@ -91,4 +91,4 @@ expect(snapshot).to.eql(v2);

it('fetches v3', function (done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 3, function (error, snapshot) {
it('fetches v3', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 3, function(error, snapshot) {
if (error) return done(error);

@@ -100,4 +100,4 @@ expect(snapshot).to.eql(v3);

it('returns an empty snapshot if the version is 0', function (done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 0, function (error, snapshot) {
it('returns an empty snapshot if the version is 0', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) {
if (error) return done(error);

@@ -109,12 +109,12 @@ expect(snapshot).to.eql(v0);

it('throws if the version is undefined', function () {
var fetch = function () {
backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function () {});
it('throws if the version is undefined', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function() {});
};
expect(fetch).to.throwError();
expect(fetch).to.throw(Error);
});
it('fetches the latest version when the optional version is not provided', function (done) {
backend.connect().fetchSnapshot('books', 'don-quixote', function (error, snapshot) {
it('fetches the latest version when the optional version is not provided', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
if (error) return done(error);

@@ -126,30 +126,30 @@ expect(snapshot).to.eql(v3);

it('throws without a callback', function () {
var fetch = function () {
it('throws without a callback', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote');
};
expect(fetch).to.throwError();
expect(fetch).to.throw(Error);
});
it('throws if the version is -1', function () {
var fetch = function () {
backend.connect().fetchSnapshot('books', 'don-quixote', -1, function () {});
it('throws if the version is -1', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote', -1, function() {});
};
expect(fetch).to.throwError();
expect(fetch).to.throw(Error);
});
it('errors if the version is a string', function () {
var fetch = function () {
backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function () { });
}
it('errors if the version is a string', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function() { });
};
expect(fetch).to.throwError();
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);
it('errors if asking for a version that does not exist', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) {
expect(error.code).to.equal(4024);
expect(snapshot).to.equal(undefined);
done();

@@ -159,4 +159,4 @@ });

it('returns an empty snapshot if trying to fetch a non-existent document', function (done) {
backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function (error, snapshot) {
it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function(error, snapshot) {
if (error) return done(error);

@@ -174,17 +174,18 @@ expect(snapshot).to.eql({

it('starts pending, and finishes not pending', function (done) {
it('starts pending, and finishes not pending', function(done) {
var connection = backend.connect();
connection.fetchSnapshot('books', 'don-quixote', null, function (error, snapshot) {
expect(connection.hasPending()).to.be(false);
connection.fetchSnapshot('books', 'don-quixote', null, function(error) {
if (error) return done(error);
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) {
it('deletes the request from the connection', function(done) {
var connection = backend.connect();
connection.fetchSnapshot('books', 'don-quixote', function (error) {
connection.fetchSnapshot('books', 'don-quixote', function(error) {
if (error) return done(error);

@@ -198,6 +199,6 @@ expect(connection._snapshotRequests).to.eql({});

it('emits a ready event when done', function (done) {
it('emits a ready event when done', function(done) {
var connection = backend.connect();
connection.fetchSnapshot('books', 'don-quixote', function (error) {
connection.fetchSnapshot('books', 'don-quixote', function(error) {
if (error) return done(error);

@@ -210,7 +211,7 @@ });

it('fires the connection.whenNothingPending', function (done) {
it('fires the connection.whenNothingPending', function(done) {
var connection = backend.connect();
var snapshotFetched = false;
connection.fetchSnapshot('books', 'don-quixote', function (error) {
connection.fetchSnapshot('books', 'don-quixote', function(error) {
if (error) return done(error);

@@ -220,4 +221,4 @@ snapshotFetched = true;

connection.whenNothingPending(function () {
expect(snapshotFetched).to.be(true);
connection.whenNothingPending(function() {
expect(snapshotFetched).to.equal(true);
done();

@@ -227,3 +228,3 @@ });

it('can drop its connection and reconnect, and the callback is just called once', function (done) {
it('can drop its connection and reconnect, and the callback is just called once', function(done) {
var connection = backend.connect();

@@ -241,3 +242,3 @@

var connectionInterrupted = false;
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
if (!connectionInterrupted) {

@@ -255,3 +256,3 @@ connection.close();

it('cannot send the same request twice over a connection', function (done) {
it('cannot send the same request twice over a connection', function(done) {
var connection = backend.connect();

@@ -267,3 +268,3 @@

var hasResent = false;
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function (request, callback) {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
if (!hasResent) {

@@ -280,8 +281,8 @@ connection._snapshotRequests[1]._onConnectionStateChanged();

describe('readSnapshots middleware', function () {
it('triggers the middleware', function (done) {
describe('readSnapshots middleware', function() {
it('triggers the middleware', function(done) {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
function (request) {
function(request) {
expect(request.snapshots[0]).to.eql(v3);
expect(request.snapshotType).to.be(backend.SNAPSHOT_TYPES.byVersion);
expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byVersion);
done();

@@ -291,16 +292,16 @@ }

backend.connect().fetchSnapshot('books', 'don-quixote', 3, function () { });
backend.connect().fetchSnapshot('books', 'don-quixote', 3, function() { });
});
it('can have its snapshot manipulated in the middleware', function (done) {
it('can have its snapshot manipulated in the middleware', function(done) {
backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
function (request, callback) {
function(request, callback) {
request.snapshots[0].data.title = 'Alice in Wonderland';
callback();
},
}
];
backend.connect().fetchSnapshot('books', 'don-quixote', function (error, snapshot) {
backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data.title).to.be('Alice in Wonderland');
expect(snapshot.data.title).to.equal('Alice in Wonderland');
done();

@@ -310,11 +311,11 @@ });

it('respects errors thrown in the middleware', function (done) {
it('respects errors thrown in the middleware', function(done) {
backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
function (request, callback) {
callback({ message: 'foo' });
},
function(request, callback) {
callback({message: 'foo'});
}
];
backend.connect().fetchSnapshot('books', 'don-quixote', 0, function (error, snapshot) {
expect(error.message).to.be('foo');
backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) {
expect(error.message).to.equal('foo');
done();

@@ -325,13 +326,13 @@ });

describe('with a registered projection', function () {
beforeEach(function () {
backend.addProjection('bookTitles', 'books', { title: true });
describe('with a registered projection', function() {
beforeEach(function() {
backend.addProjection('bookTitles', 'books', {title: true});
});
it('applies the projection to a snapshot', function (done) {
backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function (error, snapshot) {
it('applies the projection to a snapshot', function(done) {
backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data.title).to.be('Don Quixote');
expect(snapshot.data.author).to.be(undefined);
expect(snapshot.data.title).to.equal('Don Quixote');
expect(snapshot.data.author).to.equal(undefined);
done();

@@ -343,8 +344,8 @@ });

describe('a document that is currently deleted', function () {
beforeEach(function (done) {
describe('a document that is currently deleted', function() {
beforeEach(function(done) {
var doc = backend.connect().get('books', 'catch-22');
doc.create({ title: 'Catch 22' }, function (error) {
doc.create({title: 'Catch 22'}, function(error) {
if (error) return done(error);
doc.del(function (error) {
doc.del(function(error) {
done(error);

@@ -355,4 +356,4 @@ });

it('returns a null type', function (done) {
backend.connect().fetchSnapshot('books', 'catch-22', null, function (error, snapshot) {
it('returns a null type', function(done) {
backend.connect().fetchSnapshot('books', 'catch-22', null, function(error, snapshot) {
expect(snapshot).to.eql({

@@ -370,4 +371,4 @@ id: 'catch-22',

it('fetches v1', function (done) {
backend.connect().fetchSnapshot('books', 'catch-22', 1, function (error, snapshot) {
it('fetches v1', function(done) {
backend.connect().fetchSnapshot('books', 'catch-22', 1, function(error, snapshot) {
if (error) return done(error);

@@ -380,3 +381,3 @@

data: {
title: 'Catch 22',
title: 'Catch 22'
},

@@ -391,10 +392,10 @@ m: null

describe('a document that was deleted and then created again', function () {
beforeEach(function (done) {
describe('a document that was deleted and then created again', function() {
beforeEach(function(done) {
var doc = backend.connect().get('books', 'hitchhikers-guide');
doc.create({ title: 'Hitchhiker\'s Guide to the Galaxy' }, function (error) {
doc.create({title: 'Hitchhiker\'s Guide to the Galaxy'}, function(error) {
if (error) return done(error);
doc.del(function (error) {
doc.del(function(error) {
if (error) return done(error);
doc.create({ title: 'The Restaurant at the End of the Universe' }, function (error) {
doc.create({title: 'The Restaurant at the End of the Universe'}, function(error) {
done(error);

@@ -406,4 +407,4 @@ });

it('fetches the latest version of the document', function (done) {
backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function (error, snapshot) {
it('fetches the latest version of the document', function(done) {
backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function(error, snapshot) {
if (error) return done(error);

@@ -416,3 +417,3 @@

data: {
title: 'The Restaurant at the End of the Universe',
title: 'The Restaurant at the End of the Universe'
},

@@ -427,3 +428,3 @@ m: null

describe('milestone snapshots enabled for every other version', function () {
describe('milestone snapshots enabled for every other version', function() {
var milestoneDb;

@@ -433,4 +434,4 @@ var db;

beforeEach(function () {
var options = { interval: 2 };
beforeEach(function() {
var options = {interval: 2};
db = new MemoryDb();

@@ -444,20 +445,20 @@ milestoneDb = new MemoryMilestoneDb(options);

afterEach(function (done) {
afterEach(function(done) {
backendWithMilestones.close(done);
});
it('fetches a snapshot using the milestone', function (done) {
it('fetches a snapshot using the milestone', function(done) {
var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
util.callInSeries([
function (next) {
doc.create({ title: 'To Kill a Mocking Bird' }, next);
function(next) {
doc.create({title: 'To Kill a Mocking Bird'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], oi: 'Harper Lea' }, next);
function(next) {
doc.submitOp({p: ['author'], oi: 'Harper Lea'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], od: 'Harper Lea', oi: 'Harper Lee' }, next);
function(next) {
doc.submitOp({p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}, next);
},
function (next) {
function(next) {
sinon.spy(milestoneDb, 'getMilestoneSnapshot');

@@ -467,7 +468,7 @@ sinon.spy(db, 'getOps');

},
function (snapshot, next) {
expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.be(true);
expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.be(true);
expect(snapshot.v).to.be(3);
expect(snapshot.data).to.eql({ title: 'To Kill a Mocking Bird', author: 'Harper Lee' });
function(snapshot, next) {
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();

@@ -474,0 +475,0 @@ },

var async = require('async');
var expect = require('expect.js');
var expect = require('chai').expect;
var types = require('../../lib/types');

@@ -11,20 +11,30 @@ var deserializedType = require('./deserialized-type');

module.exports = function() {
describe('client submit', function() {
it('can fetch an uncreated doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
expect(doc.data).equal(undefined);
expect(doc.version).equal(null);
doc.fetch(function(err) {
if (err) return done(err);
describe('client submit', function() {
it('can fetch an uncreated doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
expect(doc.data).equal(undefined);
expect(doc.version).equal(0);
done();
expect(doc.version).equal(null);
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).equal(undefined);
expect(doc.version).equal(0);
done();
});
});
});
it('can fetch then create a new doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.fetch(function(err) {
if (err) return done(err);
it('can fetch then create a new doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.fetch(function(err) {
if (err) return done(err);
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc.version).eql(1);
done();
});
});
});
it('can create a new doc without fetching', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {

@@ -37,30 +47,33 @@ if (err) return done(err);

});
});
it('can create a new doc without fetching', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc.version).eql(1);
done();
it('can create then delete then create a doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc.version).eql(1);
doc.del(null, function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
expect(doc.version).eql(2);
doc.create({age: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 2});
expect(doc.version).eql(3);
done();
});
});
});
});
});
it('can create then delete then create a doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc.version).eql(1);
doc.del(null, function(err) {
it('can create then submit an op', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
expect(doc.version).eql(2);
doc.create({age: 2}, function(err) {
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 2});
expect(doc.version).eql(3);
expect(doc.data).eql({age: 5});
expect(doc.version).eql(2);
done();

@@ -70,181 +83,168 @@ });

});
});
it('can create then submit an op', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 2}, function(err) {
it('can create then submit an op sync', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
expect(doc.data).eql({age: 3});
expect(doc.version).eql(null);
doc.submitOp({p: ['age'], na: 2});
expect(doc.data).eql({age: 5});
expect(doc.version).eql(null);
doc.whenNothingPending(done);
});
it('submitting an op from a future version fails', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 5});
expect(doc.version).eql(2);
done();
doc.version++;
doc.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).instanceOf(Error);
done();
});
});
});
});
it('can create then submit an op sync', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
expect(doc.data).eql({age: 3});
expect(doc.version).eql(null);
doc.submitOp({p: ['age'], na: 2});
expect(doc.data).eql({age: 5});
expect(doc.version).eql(null);
doc.whenNothingPending(done);
});
it('submitting an op from a future version fails', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.version++;
it('cannot submit op on an uncreated doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).ok();
expect(err).instanceOf(Error);
done();
});
});
});
it('cannot submit op on an uncreated doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).ok();
done();
it('cannot delete an uncreated doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.del(function(err) {
expect(err).instanceOf(Error);
done();
});
});
});
it('cannot delete an uncreated doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.del(function(err) {
expect(err).ok();
done();
});
});
it('ops submitted sync get composed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 7});
// Version is 1 instead of 3, because the create and ops got composed
expect(doc.version).eql(1);
it('ops submitted sync get composed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 11});
// Ops get composed
expect(doc.version).eql(2);
expect(doc.data).eql({age: 7});
// Version is 1 instead of 3, because the create and ops got composed
expect(doc.version).eql(1);
doc.submitOp({p: ['age'], na: 2});
doc.del(function(err) {
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
// del DOES NOT get composed
expect(doc.version).eql(4);
done();
expect(doc.data).eql({age: 11});
// Ops get composed
expect(doc.version).eql(2);
doc.submitOp({p: ['age'], na: 2});
doc.del(function(err) {
if (err) return done(err);
expect(doc.data).eql(undefined);
// del DOES NOT get composed
expect(doc.version).eql(4);
done();
});
});
});
});
});
it('does not compose ops when doc.preventCompose is true', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.preventCompose = true;
doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 7});
// Compare to version in above test
expect(doc.version).eql(3);
it('does not compose ops when doc.preventCompose is true', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.preventCompose = true;
doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 11});
expect(doc.data).eql({age: 7});
// Compare to version in above test
expect(doc.version).eql(5);
done();
expect(doc.version).eql(3);
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 11});
// Compare to version in above test
expect(doc.version).eql(5);
done();
});
});
});
});
it('resumes composing after doc.preventCompose is set back to false', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.preventCompose = true;
doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 7});
// Compare to version in above test
expect(doc.version).eql(3);
// Reset back to start composing ops again
doc.preventCompose = false;
it('resumes composing after doc.preventCompose is set back to false', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.preventCompose = true;
doc.create({age: 3});
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 11});
expect(doc.data).eql({age: 7});
// Compare to version in above test
expect(doc.version).eql(4);
done();
expect(doc.version).eql(3);
// Reset back to start composing ops again
doc.preventCompose = false;
doc.submitOp({p: ['age'], na: 2});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 11});
// Compare to version in above test
expect(doc.version).eql(4);
done();
});
});
});
});
it('can create a new doc then fetch', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.fetch(function(err) {
it('can create a new doc then fetch', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc.version).eql(1);
done();
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc.version).eql(1);
done();
});
});
});
});
it('calling create on the same doc twice fails', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.create({age: 4}, function(err) {
expect(err).ok();
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
it('calling create on the same doc twice fails', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.create({age: 4}, function(err) {
expect(err).instanceOf(Error);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
});
});
});
});
it('trying to create an already created doc without fetching fails and fetches', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.create({age: 4}, function(err) {
expect(err).ok();
expect(doc2.version).equal(1);
expect(doc2.data).eql({age: 3});
done();
it('trying to create an already created doc without fetching fails and fetches', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.create({age: 4}, function(err) {
expect(err).instanceOf(Error);
expect(doc2.version).equal(1);
expect(doc2.data).eql({age: 3});
done();
});
});
});
});
it('server fetches and transforms by already committed op', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('server fetches and transforms by already committed op', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 2}, function(err) {
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
expect(doc2.version).equal(3);
expect(doc2.data).eql({age: 6});
done();
doc2.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc2.version).equal(3);
expect(doc2.data).eql({age: 6});
done();
});
});

@@ -254,19 +254,19 @@ });

});
});
it('submit fails if the server is missing ops required for transforming', function(done) {
this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
callback(null, []);
};
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('submit fails if the server is missing ops required for transforming', function(done) {
this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
callback(null, []);
};
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).ok();
done();
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).instanceOf(Error);
done();
});
});

@@ -276,23 +276,23 @@ });

});
});
it('submit fails if ops returned are not the expected version', function(done) {
var getOpsToSnapshot = this.backend.db.getOpsToSnapshot;
this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
getOpsToSnapshot.call(this, collection, id, from, snapshot, options, function(err, ops) {
ops[0].v++;
callback(null, ops);
});
};
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('submit fails if ops returned are not the expected version', function(done) {
var getOpsToSnapshot = this.backend.db.getOpsToSnapshot;
this.backend.db.getOpsToSnapshot = function(collection, id, from, snapshot, options, callback) {
getOpsToSnapshot.call(this, collection, id, from, snapshot, options, function(err, ops) {
ops[0].v++;
callback(null, ops);
});
};
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).ok();
done();
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 2}, function(err) {
expect(err).instanceOf(Error);
done();
});
});

@@ -302,57 +302,22 @@ });

});
});
function delayedReconnect(backend, connection) {
function delayedReconnect(backend, connection) {
// Disconnect after the message has sent and before the server will have
// had a chance to reply
process.nextTick(function() {
connection.close();
// Reconnect once the server has a chance to save the op data
setTimeout(function() {
backend.connect(connection);
}, 100);
});
}
it('resends create when disconnected before ack', function(done) {
var backend = this.backend;
var doc = backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
});
delayedReconnect(backend, doc.connection);
});
it('resent create on top of deleted doc gets proper starting version', function(done) {
var backend = this.backend;
var doc = backend.connect().get('dogs', 'fido');
doc.create({age: 4}, function(err) {
if (err) return done(err);
doc.del(function(err) {
if (err) return done(err);
var doc2 = backend.connect().get('dogs', 'fido');
doc2.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc2.version).equal(3);
expect(doc2.data).eql({age: 3});
done();
});
delayedReconnect(backend, doc2.connection);
process.nextTick(function() {
connection.close();
// Reconnect once the server has a chance to save the op data
setTimeout(function() {
backend.connect(connection);
}, 100);
});
});
});
}
it('resends delete when disconnected before ack', function(done) {
var backend = this.backend;
var doc = backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.del(function(err) {
it('resends create when disconnected before ack', function(done) {
var backend = this.backend;
var doc = backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
expect(doc.data).eql(undefined);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();

@@ -362,218 +327,230 @@ });

});
});
it('op submitted during inflight create does not compose and gets flushed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
// Submit an op after message is sent but before server has a chance to reply
process.nextTick(function() {
doc.submitOp({p: ['age'], na: 2}, function(err) {
it('resent create on top of deleted doc gets proper starting version', function(done) {
var backend = this.backend;
var doc = backend.connect().get('dogs', 'fido');
doc.create({age: 4}, function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
expect(doc.data).eql({age: 5});
done();
doc.del(function(err) {
if (err) return done(err);
var doc2 = backend.connect().get('dogs', 'fido');
doc2.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc2.version).equal(3);
expect(doc2.data).eql({age: 3});
done();
});
delayedReconnect(backend, doc2.connection);
});
});
});
});
it('can commit then fetch in a new connection to get the same data', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('resends delete when disconnected before ack', function(done) {
var backend = this.backend;
var doc = backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc2.data).eql({age: 3});
expect(doc.version).eql(1);
expect(doc2.version).eql(1);
expect(doc.data).not.equal(doc2.data);
done();
doc.del(function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
expect(doc.data).eql(undefined);
done();
});
delayedReconnect(backend, doc.connection);
});
});
});
it('an op submitted concurrently is transformed by the first', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
if (err) return done(err);
var count = 0;
it('op submitted during inflight create does not compose and gets flushed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
// Submit an op after message is sent but before server has a chance to reply
process.nextTick(function() {
doc.submitOp({p: ['age'], na: 2}, function(err) {
count++;
if (err) return done(err);
if (count === 1) {
expect(doc.data).eql({age: 5});
expect(doc.version).eql(2);
} else {
expect(doc.data).eql({age: 12});
expect(doc.version).eql(3);
done();
}
expect(doc.version).equal(2);
expect(doc.data).eql({age: 5});
done();
});
doc2.submitOp({p: ['age'], na: 7}, function(err) {
count++;
if (err) return done(err);
if (count === 1) {
expect(doc2.data).eql({age: 10});
expect(doc2.version).eql(2);
} else {
expect(doc2.data).eql({age: 12});
expect(doc2.version).eql(3);
done();
}
});
});
});
});
it('second of two concurrent creates is rejected', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
var count = 0;
doc.create({age: 3}, function(err) {
count++;
if (count === 1) {
it('can commit then fetch in a new connection to get the same data', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.version).eql(1);
expect(doc.data).eql({age: 3});
} else {
expect(err).ok();
expect(doc.version).eql(1);
expect(doc.data).eql({age: 5});
done();
}
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 3});
expect(doc2.data).eql({age: 3});
expect(doc.version).eql(1);
expect(doc2.version).eql(1);
expect(doc.data).not.equal(doc2.data);
done();
});
});
});
doc2.create({age: 5}, function(err) {
count++;
if (count === 1) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 5});
} else {
expect(err).ok();
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
}
});
});
it('concurrent delete operations transform', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('an op submitted concurrently is transformed by the first', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
var count = 0;
doc.del(function(err) {
count++;
doc2.fetch(function(err) {
if (err) return done(err);
if (count === 1) {
expect(doc.version).eql(2);
expect(doc.data).eql(undefined);
} else {
expect(doc.version).eql(3);
expect(doc.data).eql(undefined);
done();
}
var count = 0;
doc.submitOp({p: ['age'], na: 2}, function(err) {
count++;
if (err) return done(err);
if (count === 1) {
expect(doc.data).eql({age: 5});
expect(doc.version).eql(2);
} else {
expect(doc.data).eql({age: 12});
expect(doc.version).eql(3);
done();
}
});
doc2.submitOp({p: ['age'], na: 7}, function(err) {
count++;
if (err) return done(err);
if (count === 1) {
expect(doc2.data).eql({age: 10});
expect(doc2.version).eql(2);
} else {
expect(doc2.data).eql({age: 12});
expect(doc2.version).eql(3);
done();
}
});
});
doc2.del(function(err) {
count++;
});
});
it('second of two concurrent creates is rejected', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
var count = 0;
doc.create({age: 3}, function(err) {
count++;
if (count === 1) {
if (err) return done(err);
if (count === 1) {
expect(doc2.version).eql(2);
expect(doc2.data).eql(undefined);
} else {
expect(doc2.version).eql(3);
expect(doc2.data).eql(undefined);
done();
}
});
expect(doc.version).eql(1);
expect(doc.data).eql({age: 3});
} else {
expect(err).instanceOf(Error);
expect(doc.version).eql(1);
expect(doc.data).eql({age: 5});
done();
}
});
doc2.create({age: 5}, function(err) {
count++;
if (count === 1) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 5});
} else {
expect(err).instanceOf(Error);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
}
});
});
});
it('submits retry below the backend.maxSubmitRetries threshold', function(done) {
this.backend.maxSubmitRetries = 10;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('concurrent delete operations transform', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
var count = 0;
var cb = function(err) {
count++;
doc2.fetch(function(err) {
if (err) return done(err);
if (count > 1) done();
};
doc.submitOp({p: ['age'], na: 2}, cb);
doc2.submitOp({p: ['age'], na: 7}, cb);
var count = 0;
doc.del(function(err) {
count++;
if (err) return done(err);
if (count === 1) {
expect(doc.version).eql(2);
expect(doc.data).eql(undefined);
} else {
expect(doc.version).eql(3);
expect(doc.data).eql(undefined);
done();
}
});
doc2.del(function(err) {
count++;
if (err) return done(err);
if (count === 1) {
expect(doc2.version).eql(2);
expect(doc2.data).eql(undefined);
} else {
expect(doc2.version).eql(3);
expect(doc2.data).eql(undefined);
done();
}
});
});
});
});
});
it('submits fail above the backend.maxSubmitRetries threshold', function(done) {
var backend = this.backend;
this.backend.maxSubmitRetries = 0;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('submits retry below the backend.maxSubmitRetries threshold', function(done) {
this.backend.maxSubmitRetries = 10;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
var docCallback;
var doc2Callback;
// The submit retry happens just after an op is committed. This hook into the middleware
// catches both ops just before they're about to be committed. This ensures that both ops
// are certainly working on the same snapshot (ie one op hasn't been committed before the
// other fetches the snapshot to apply to). By storing the callbacks, we can then
// manually trigger the callbacks, first calling doc, and when we know that's been committed,
// we then commit doc2.
backend.use('commit', function (request, callback) {
if (request.op.op[0].na === 2) docCallback = callback;
if (request.op.op[0].na === 7) doc2Callback = callback;
// Wait until both ops have been applied to the same snapshot and are about to be committed
if (docCallback && doc2Callback) {
// Trigger the first op's commit and then the second one later, which will cause the
// second op to retry
docCallback();
}
doc2.fetch(function(err) {
if (err) return done(err);
var count = 0;
var cb = function(err) {
count++;
if (err) return done(err);
if (count > 1) done();
};
doc.submitOp({p: ['age'], na: 2}, cb);
doc2.submitOp({p: ['age'], na: 7}, cb);
});
doc.submitOp({p: ['age'], na: 2}, function (error) {
if (error) return done(error);
// When we know the first op has been committed, we try to commit the second op, which will
// fail because it's working on an out-of-date snapshot. It will retry, but exceed the
// maxSubmitRetries limit of 0
doc2Callback();
});
doc2.submitOp({p: ['age'], na: 7}, function (error) {
expect(error).ok();
done();
});
});
});
});
it('pending delete transforms incoming ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('submits fail above the backend.maxSubmitRetries threshold', function(done) {
var backend = this.backend;
this.backend.maxSubmitRetries = 0;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.submitOp({p: ['age'], na: 1}, function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
async.parallel([
function(cb) { doc.del(cb); },
function(cb) { doc.create({age: 5}, cb); }
], function(err) {
var docCallback;
var doc2Callback;
// The submit retry happens just after an op is committed. This hook into the middleware
// catches both ops just before they're about to be committed. This ensures that both ops
// are certainly working on the same snapshot (ie one op hasn't been committed before the
// other fetches the snapshot to apply to). By storing the callbacks, we can then
// manually trigger the callbacks, first calling doc, and when we know that's been committed,
// we then commit doc2.
backend.use('commit', function(request, callback) {
if (request.op.op[0].na === 2) docCallback = callback;
if (request.op.op[0].na === 7) doc2Callback = callback;
// Wait until both ops have been applied to the same snapshot and are about to be committed
if (docCallback && doc2Callback) {
// Trigger the first op's commit and then the second one later, which will cause the
// second op to retry
docCallback();
}
});
doc.submitOp({p: ['age'], na: 2}, function(err) {
if (err) return done(err);
expect(doc.version).equal(4);
expect(doc.data).eql({age: 5});
// 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(err) {
expect(err).instanceOf(Error);
done();

@@ -584,21 +561,25 @@ });

});
});
it('pending delete transforms incoming delete', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('pending delete transforms incoming ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.del(function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
async.parallel([
function(cb) { doc.del(cb); },
function(cb) { doc.create({age: 5}, cb); }
], function(err) {
doc2.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
expect(doc.version).equal(4);
expect(doc.data).eql({age: 5});
done();
async.parallel([
function(cb) {
doc.del(cb);
},
function(cb) {
doc.create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
expect(doc.version).equal(4);
expect(doc.data).eql({age: 5});
done();
});
});

@@ -608,18 +589,25 @@ });

});
});
it('submitting op after delete returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('pending delete transforms incoming delete', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.del(function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err).ok();
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
doc2.del(function(err) {
if (err) return done(err);
async.parallel([
function(cb) {
doc.del(cb);
},
function(cb) {
doc.create({age: 5}, cb);
}
], function(err) {
if (err) return done(err);
expect(doc.version).equal(4);
expect(doc.data).eql({age: 5});
done();
});
});

@@ -629,42 +617,37 @@ });

});
});
it('transforming pending op by server delete returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('submitting op after delete returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.del(function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
doc.pause();
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err.code).to.equal(4017);
expect(doc.version).equal(2);
expect(doc.data).eql(undefined);
done();
doc2.del(function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err).instanceOf(Error);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
});
});
doc.fetch();
});
});
});
});
it('transforming pending op by server create returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.del(function(err) {
it('transforming pending op by server delete returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
if (err) return done(err);
doc2.create({age: 5}, function(err) {
doc2.del(function(err) {
if (err) return done(err);
doc.pause();
doc.create({age: 9}, function(err) {
expect(err.code).to.equal(4018);
expect(doc.version).equal(3);
expect(doc.data).eql({age: 5});
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err.code).to.equal(4017);
expect(doc.version).equal(2);
expect(doc.data).eql(undefined);
done();

@@ -677,84 +660,113 @@ });

});
});
it('second client can create following delete', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.del(function(err) {
it('transforming pending op by server create returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.create({age: 5}, function(err) {
doc.del(function(err) {
if (err) return done(err);
expect(doc2.version).eql(3);
expect(doc2.data).eql({age: 5});
done();
doc2.fetch(function(err) {
if (err) return done(err);
doc2.create({age: 5}, function(err) {
if (err) return done(err);
doc.pause();
doc.create({age: 9}, function(err) {
expect(err.code).to.equal(4018);
expect(doc.version).equal(3);
expect(doc.data).eql({age: 5});
done();
});
doc.fetch();
});
});
});
});
});
});
it('doc.pause() prevents ops from being sent', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
doc.create({age: 3}, done);
done();
});
it('second client can create following delete', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.del(function(err) {
if (err) return done(err);
doc2.create({age: 5}, function(err) {
if (err) return done(err);
expect(doc2.version).eql(3);
expect(doc2.data).eql({age: 5});
done();
});
});
});
});
it('can call doc.resume() without pausing', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.resume();
doc.create({age: 3}, done);
});
it('doc.pause() prevents ops from being sent', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
doc.create({age: 3}, done);
done();
});
it('doc.resume() resumes sending ops after pause', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
doc.create({age: 3}, done);
doc.resume();
});
it('can call doc.resume() without pausing', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.resume();
doc.create({age: 3}, done);
});
it('pending ops are transformed by ops from other clients', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('doc.resume() resumes sending ops after pause', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
doc.create({age: 3}, done);
doc.resume();
});
it('pending ops are transformed by ops from other clients', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.pause();
doc.submitOp({p: ['age'], na: 1});
doc.submitOp({p: ['color'], oi: 'gold'});
expect(doc.version).equal(1);
doc2.fetch(function(err) {
if (err) return done(err);
doc.pause();
doc.submitOp({p: ['age'], na: 1});
doc.submitOp({p: ['color'], oi: 'gold'});
expect(doc.version).equal(1);
doc2.submitOp({p: ['age'], na: 5});
process.nextTick(function() {
doc2.submitOp({p: ['sex'], oi: 'female'}, function(err) {
if (err) return done(err);
expect(doc2.version).equal(3);
async.parallel([
function(cb) { doc.fetch(cb); },
function(cb) { doc2.fetch(cb); }
], function(err) {
doc2.submitOp({p: ['age'], na: 5});
process.nextTick(function() {
doc2.submitOp({p: ['sex'], oi: 'female'}, function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
expect(doc.version).equal(3);
expect(doc.hasPending()).equal(true);
expect(doc2.data).eql({age: 8, sex: 'female'});
expect(doc2.version).equal(3);
expect(doc2.hasPending()).equal(false);
doc.resume();
doc.whenNothingPending(function() {
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
expect(doc.version).equal(4);
expect(doc.hasPending()).equal(false);
async.parallel([
function(cb) {
doc.fetch(cb);
},
function(cb) {
doc2.fetch(cb);
}
], function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
expect(doc.version).equal(3);
expect(doc.hasPending()).equal(true);
expect(doc2.data).eql({age: 9, color: 'gold', sex: 'female'});
expect(doc2.version).equal(4);
expect(doc2.hasPending()).equal(false);
done();
expect(doc2.data).eql({age: 8, sex: 'female'});
expect(doc2.version).equal(3);
expect(doc2.hasPending()).equal(false);
doc.resume();
doc.whenNothingPending(function() {
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc.data).eql({age: 9, color: 'gold', sex: 'female'});
expect(doc.version).equal(4);
expect(doc.hasPending()).equal(false);
expect(doc2.data).eql({age: 9, color: 'gold', sex: 'female'});
expect(doc2.version).equal(4);
expect(doc2.hasPending()).equal(false);
done();
});
});

@@ -767,346 +779,272 @@ });

});
});
it('snapshot fetch does not revert the version of deleted doc without pending ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
this.backend.use('doc', function(request, next) {
doc.create({age: 3});
doc.del(next);
it('snapshot fetch does not revert the version of deleted doc without pending ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
this.backend.use('doc', function(request, next) {
doc.create({age: 3});
doc.del(next);
});
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
done();
});
});
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
done();
});
});
it('snapshot fetch does not revert the version of deleted doc with pending ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
this.backend.use('doc', function(request, next) {
doc.create({age: 3}, function(err) {
it('snapshot fetch does not revert the version of deleted doc with pending ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
this.backend.use('doc', function(request, next) {
doc.create({age: 3}, function(err) {
if (err) return done(err);
next();
});
process.nextTick(function() {
doc.pause();
doc.del(done);
});
});
doc.fetch(function(err) {
if (err) return done(err);
next();
expect(doc.version).equal(1);
doc.resume();
});
process.nextTick(function() {
doc.pause();
doc.del(done);
});
});
doc.fetch(function(err) {
if (err) return done(err);
expect(doc.version).equal(1);
doc.resume();
});
});
it('snapshot fetch from query does not advance version of doc with pending ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({name: 'kido'}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it('snapshot fetch from query does not advance version of doc with pending ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({name: 'kido'}, function(err) {
if (err) return done(err);
doc2.submitOp({p: ['name', 0], si: 'f'}, function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc2.data).eql({name: 'fkido'});
doc.connection.createFetchQuery('dogs', {}, null, function(err) {
doc2.submitOp({p: ['name', 0], si: 'f'}, function(err) {
if (err) return done(err);
doc.resume();
expect(doc2.data).eql({name: 'fkido'});
doc.connection.createFetchQuery('dogs', {}, null, function(err) {
if (err) return done(err);
doc.resume();
});
});
});
});
});
process.nextTick(function() {
doc.pause();
doc.submitOp({p: ['name', 0], sd: 'k'}, function(err) {
if (err) return done(err);
process.nextTick(function() {
doc.pause();
doc2.fetch(function(err) {
doc.submitOp({p: ['name', 0], sd: 'k'}, function(err) {
if (err) return done(err);
expect(doc2.version).equal(3);
expect(doc2.data).eql({name: 'fido'});
done();
doc.pause();
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc2.version).equal(3);
expect(doc2.data).eql({name: 'fido'});
done();
});
});
doc.del();
});
doc.del();
});
});
it('passing an error in submit middleware rejects a create and calls back with the erorr', function(done) {
this.backend.use('submit', function(request, next) {
next({message: 'Custom error'});
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
});
it('passing an error in submit middleware rejects a create and throws the erorr', function(done) {
this.backend.use('submit', function(request, next) {
next({message: 'Custom error'});
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
doc.on('error', function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
});
it('passing an error in submit middleware rejects pending ops after failed create', function(done) {
var submitCount = 0;
this.backend.use('submit', function(request, next) {
submitCount++;
if (submitCount === 1) return next({message: 'Custom error'});
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
async.parallel([
function(cb) {
doc.create({age: 3}, function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
cb();
});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
},
function(cb) {
process.nextTick(function() {
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
expect(submitCount).equal(1);
cb();
});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 4});
});
}
], done);
});
it('request.rejectedError() soft rejects a create', function(done) {
this.backend.use('submit', function(request, next) {
next(request.rejectedError());
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
});
it('request.rejectedError() soft rejects a create without callback', function(done) {
this.backend.use('submit', function(request, next) {
next(request.rejectedError());
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
doc.whenNothingPending(function() {
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
});
it('passing an error in submit middleware rejects an op and calls back with the erorr', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next({message: 'Custom error'});
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
it('passing an error in submit middleware rejects a create and calls back with the erorr', function(done) {
this.backend.use('submit', function(request, next) {
next({message: 'Custom error'});
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
});
});
it('passing an error in submit middleware rejects an op and emits the erorr', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next({message: 'Custom error'});
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
it('passing an error in submit middleware rejects a create and throws the erorr', function(done) {
this.backend.use('submit', function(request, next) {
next({message: 'Custom error'});
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
doc.on('error', function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
});
});
it('passing an error in submit middleware transforms pending ops after failed op', function(done) {
var submitCount = 0;
this.backend.use('submit', function(request, next) {
submitCount++;
if (submitCount === 2) return next({message: 'Custom error'});
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
it('passing an error in submit middleware rejects pending ops after failed create', function(done) {
var submitCount = 0;
this.backend.use('submit', function(request, next) {
submitCount++;
if (submitCount === 1) return next({message: 'Custom error'});
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
async.parallel([
function(cb) {
doc.submitOp({p: ['age'], na: 1}, function(err) {
doc.create({age: 3}, function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
cb();
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
},
function(cb) {
process.nextTick(function() {
doc.submitOp({p: ['age'], na: 5}, cb);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 9});
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
expect(submitCount).equal(1);
cb();
});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 4});
});
}
], function(err) {
], done);
});
it('request.rejectedError() soft rejects a create', function(done) {
this.backend.use('submit', function(request, next) {
next(request.rejectedError());
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
expect(doc.data).eql({age: 8});
expect(submitCount).equal(3);
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
});
});
it('request.rejectedError() soft rejects an op', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next(request.rejectedError());
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
it('request.rejectedError() soft rejects a create without callback', function(done) {
this.backend.use('submit', function(request, next) {
next(request.rejectedError());
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3});
expect(doc.version).equal(null);
expect(doc.data).eql({age: 3});
doc.whenNothingPending(function() {
expect(doc.version).equal(0);
expect(doc.data).equal(undefined);
done();
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
});
});
it('request.rejectedError() soft rejects an op without callback', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next(request.rejectedError());
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
doc.whenNothingPending(function() {
it('passing an error in submit middleware rejects an op and calls back with the erorr', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next({message: 'Custom error'});
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
expect(doc.data).eql({age: 4});
});
});
});
it('setting op.op to null makes it a no-op while returning success to the submitting client', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op) request.op.op = null;
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
it('passing an error in submit middleware rejects an op and emits the erorr', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next({message: 'Custom error'});
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
doc.submitOp({p: ['age'], na: 1});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc2.version).equal(2);
expect(doc2.data).eql({age: 3});
doc.on('error', function(err) {
expect(err.message).equal('Custom error');
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
});
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
});
});
it('submitting an invalid op message returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc._submit({}, null, function(err) {
expect(err).ok();
done();
it('passing an error in submit middleware transforms pending ops after failed op', function(done) {
var submitCount = 0;
this.backend.use('submit', function(request, next) {
submitCount++;
if (submitCount === 2) return next({message: 'Custom error'});
next();
});
});
});
it('allows snapshot and op to be a non-object', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create(5, numberType.type.uri, function (err) {
if (err) return done(err);
expect(doc.data).to.equal(5);
doc.submitOp(2, function(err) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).to.equal(7);
done();
async.parallel([
function(cb) {
doc.submitOp({p: ['age'], na: 1}, function(err) {
expect(err.message).equal('Custom error');
cb();
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
},
function(cb) {
process.nextTick(function() {
doc.submitOp({p: ['age'], na: 5}, cb);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 9});
});
}
], function(err) {
if (err) return done(err);
expect(doc.version).equal(2);
expect(doc.data).eql({age: 8});
expect(submitCount).equal(3);
done();
});
});
});
});
describe('type.deserialize', function() {
it('can create a new doc', function(done) {
it('request.rejectedError() soft rejects an op', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next(request.rejectedError());
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc.data).a(deserializedType.Node);
expect(doc.data).eql({value: 3, next: null});
done();
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
});
});
it('is stored serialized in backend', function(done) {
var db = this.backend.db;
it('request.rejectedError() soft rejects an op without callback', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op.op) return next(request.rejectedError());
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
doc.create({age: 3}, function(err) {
if (err) return done(err);
db.getSnapshot('dogs', 'fido', null, null, function(err, snapshot) {
if (err) return done(err);
expect(snapshot.data).eql([3]);
doc.submitOp({p: ['age'], na: 1});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
doc.whenNothingPending(function() {
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();

@@ -1117,24 +1055,33 @@ });

it('deserializes on fetch', function(done) {
it('setting op.op to null makes it a no-op while returning success to the submitting client', function(done) {
this.backend.use('submit', function(request, next) {
if (request.op) request.op.op = null;
next();
});
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
var backend = this.backend;
doc.create([3], deserializedType.type.uri, function(err) {
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
expect(doc2.data).a(deserializedType.Node);
expect(doc2.data).eql({value: 3, next: null});
done();
expect(doc.version).equal(2);
expect(doc.data).eql({age: 4});
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc2.version).equal(2);
expect(doc2.data).eql({age: 3});
done();
});
});
expect(doc.version).equal(1);
expect(doc.data).eql({age: 4});
});
});
it('can create then submit an op', function(done) {
it('submitting an invalid op message returns error', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({insert: 0, value: 2}, function(err) {
if (err) return done(err);
expect(doc.data).eql({value: 2, next: {value: 3, next: null}});
doc._submit({}, null, function(err) {
expect(err).instanceOf(Error);
done();

@@ -1145,16 +1092,86 @@ });

it('server fetches and transforms by already committed op', function(done) {
it('allows snapshot and op to be a non-object', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
var backend = this.backend;
doc.create([3], deserializedType.type.uri, function(err) {
doc.create(5, numberType.type.uri, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
expect(doc.data).to.equal(5);
doc.submitOp(2, function(err) {
if (err) return done(err);
expect(doc.data).to.equal(7);
done();
});
});
});
describe('type.deserialize', function() {
it('can create a new doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
if (err) return done(err);
expect(doc.data).instanceOf(deserializedType.Node);
expect(doc.data.value).equal(3);
expect(doc.data.next).equal(null);
done();
});
});
it('is stored serialized in backend', function(done) {
var db = this.backend.db;
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
if (err) return done(err);
db.getSnapshot('dogs', 'fido', null, null, function(err, snapshot) {
if (err) return done(err);
expect(snapshot.data).eql([3]);
done();
});
});
});
it('deserializes on fetch', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
if (err) return done(err);
expect(doc2.data).instanceOf(deserializedType.Node);
expect(doc2.data.value).equal(3);
expect(doc2.data.next).equal(null);
done();
});
});
});
it('can create then submit an op', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
if (err) return done(err);
doc.submitOp({insert: 0, value: 2}, function(err) {
if (err) return done(err);
doc2.submitOp({insert: 1, value: 4}, function(err) {
expect(doc.data.value).eql(2);
expect(doc.data.next.value).equal(3);
expect(doc.data.next.next).equal(null);
done();
});
});
});
it('server fetches and transforms by already committed op', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type.uri, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
if (err) return done(err);
doc.submitOp({insert: 0, value: 2}, function(err) {
if (err) return done(err);
expect(doc2.data).eql({value: 2, next: {value: 3, next: {value: 4, next: null}}});
done();
doc2.submitOp({insert: 1, value: 4}, function(err) {
if (err) return done(err);
expect(doc2.data.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();
});
});

@@ -1165,27 +1182,27 @@ });

});
});
describe('type.createDeserialized', function() {
it('can create a new doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type2.uri, function(err) {
if (err) return done(err);
expect(doc.data).a(deserializedType.Node);
expect(doc.data).eql({value: 3, next: null});
done();
describe('type.createDeserialized', function() {
it('can create a new doc', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create([3], deserializedType.type2.uri, function(err) {
if (err) return done(err);
expect(doc.data).instanceOf(deserializedType.Node);
expect(doc.data.value).equal(3);
expect(doc.data.next).equal(null);
done();
});
});
});
it('can create a new doc from deserialized form', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create(new deserializedType.Node(3), deserializedType.type2.uri, function(err) {
if (err) return done(err);
expect(doc.data).a(deserializedType.Node);
expect(doc.data).eql({value: 3, next: null});
done();
it('can create a new doc from deserialized form', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create(new deserializedType.Node(3), deserializedType.type2.uri, function(err) {
if (err) return done(err);
expect(doc.data).instanceOf(deserializedType.Node);
expect(doc.data.value).equal(3);
expect(doc.data.next).equal(null);
done();
});
});
});
});
});
};

@@ -1,288 +0,320 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var async = require('async');
module.exports = function() {
describe('client subscribe', function() {
describe('client subscribe', function() {
it('can call bulk without doing any actions', function() {
var connection = this.backend.connect();
connection.startBulk();
connection.endBulk();
});
it('can call bulk without doing any actions', function() {
var connection = this.backend.connect();
connection.startBulk();
connection.endBulk();
});
['fetch', 'subscribe'].forEach(function(method) {
it(method + ' gets initial data', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2[method](function(err) {
['fetch', 'subscribe'].forEach(function(method) {
it(method + ' gets initial data', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
doc2[method](function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
});
});
});
});
it(method + ' twice simultaneously calls back', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
async.parallel([
function(cb) { doc2[method](cb); },
function(cb) { doc2[method](cb); }
], function(err) {
it(method + ' twice simultaneously calls back', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
async.parallel([
function(cb) {
doc2[method](cb);
},
function(cb) {
doc2[method](cb);
}
], function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
});
});
});
});
it(method + ' twice in bulk simultaneously calls back', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.connection.startBulk();
async.parallel([
function(cb) { doc2[method](cb); },
function(cb) { doc2[method](cb); }
], function(err) {
it(method + ' twice in bulk simultaneously calls back', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
doc2.connection.startBulk();
async.parallel([
function(cb) {
doc2[method](cb);
},
function(cb) {
doc2[method](cb);
}
], function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
});
doc2.connection.endBulk();
});
doc2.connection.endBulk();
});
});
it(method + ' bulk on same collection', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
], function(err) {
if (err) return done(err);
var fido = connection2.get('dogs', 'fido');
var spot = connection2.get('dogs', 'spot');
var finn = connection2.get('cats', 'finn');
connection2.startBulk();
it(method + ' bulk on same collection', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
async.parallel([
function(cb) { fido[method](cb); },
function(cb) { spot[method](cb); },
function(cb) { finn[method](cb); }
function(cb) {
connection.get('dogs', 'fido').create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').create({age: 5}, cb);
},
function(cb) {
connection.get('cats', 'finn').create({age: 2}, cb);
}
], function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 3});
expect(spot.data).eql({age: 5});
expect(finn.data).eql({age: 2});
done();
var fido = connection2.get('dogs', 'fido').on('error', done);
var spot = connection2.get('dogs', 'spot').on('error', done);
var finn = connection2.get('cats', 'finn').on('error', done);
connection2.startBulk();
async.parallel([
function(cb) {
fido[method](cb);
},
function(cb) {
spot[method](cb);
},
function(cb) {
finn[method](cb);
}
], function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 3});
expect(spot.data).eql({age: 5});
expect(finn.data).eql({age: 2});
done();
});
connection2.endBulk();
});
connection2.endBulk();
});
});
it(method + ' bulk on same collection from known version', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var fido = connection2.get('dogs', 'fido');
var spot = connection2.get('dogs', 'spot');
var finn = connection2.get('cats', 'finn');
connection2.startBulk();
async.parallel([
function(cb) { fido[method](cb); },
function(cb) { spot[method](cb); },
function(cb) { finn[method](cb); }
], function(err) {
if (err) return done(err);
expect(fido.version).equal(0);
expect(spot.version).equal(0);
expect(finn.version).equal(0);
expect(fido.data).equal(undefined);
expect(spot.data).equal(undefined);
expect(finn.data).equal(undefined);
it(method + ' bulk on same collection from known version', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var fido = connection2.get('dogs', 'fido').on('error', done);
var spot = connection2.get('dogs', 'spot').on('error', done);
var finn = connection2.get('cats', 'finn').on('error', done);
connection2.startBulk();
async.parallel([
function(cb) { connection.get('dogs', 'fido').create({age: 3}, cb); },
function(cb) { connection.get('dogs', 'spot').create({age: 5}, cb); },
function(cb) { connection.get('cats', 'finn').create({age: 2}, cb); }
function(cb) {
fido[method](cb);
},
function(cb) {
spot[method](cb);
},
function(cb) {
finn[method](cb);
}
], function(err) {
if (err) return done(err);
connection2.startBulk();
expect(fido.version).equal(0);
expect(spot.version).equal(0);
expect(finn.version).equal(0);
expect(fido.data).equal(undefined);
expect(spot.data).equal(undefined);
expect(finn.data).equal(undefined);
async.parallel([
function(cb) { fido[method](cb); },
function(cb) { spot[method](cb); },
function(cb) { finn[method](cb); }
function(cb) {
connection.get('dogs', 'fido').create({age: 3}, cb);
},
function(cb) {
connection.get('dogs', 'spot').create({age: 5}, cb);
},
function(cb) {
connection.get('cats', 'finn').create({age: 2}, cb);
}
], function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 3});
expect(spot.data).eql({age: 5});
expect(finn.data).eql({age: 2});
// Test sending a fetch without any new ops being created
connection2.startBulk();
async.parallel([
function(cb) { fido[method](cb); },
function(cb) { spot[method](cb); },
function(cb) { finn[method](cb); }
function(cb) {
fido[method](cb);
},
function(cb) {
spot[method](cb);
},
function(cb) {
finn[method](cb);
}
], function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 3});
expect(spot.data).eql({age: 5});
expect(finn.data).eql({age: 2});
// Create new ops and test if they are received
// Test sending a fetch without any new ops being created
connection2.startBulk();
async.parallel([
function(cb) { connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb); },
function(cb) { connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb); },
function(cb) { connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb); }
function(cb) {
fido[method](cb);
},
function(cb) {
spot[method](cb);
},
function(cb) {
finn[method](cb);
}
], function(err) {
if (err) return done(err);
connection2.startBulk();
// Create new ops and test if they are received
async.parallel([
function(cb) { fido[method](cb); },
function(cb) { spot[method](cb); },
function(cb) { finn[method](cb); }
function(cb) {
connection.get('dogs', 'fido').submitOp([{p: ['age'], na: 1}], cb);
},
function(cb) {
connection.get('dogs', 'spot').submitOp([{p: ['age'], na: 1}], cb);
},
function(cb) {
connection.get('cats', 'finn').submitOp([{p: ['age'], na: 1}], cb);
}
], function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 4});
expect(spot.data).eql({age: 6});
expect(finn.data).eql({age: 3});
done();
connection2.startBulk();
async.parallel([
function(cb) {
fido[method](cb);
},
function(cb) {
spot[method](cb);
},
function(cb) {
finn[method](cb);
}
], function(err) {
if (err) return done(err);
expect(fido.data).eql({age: 4});
expect(spot.data).eql({age: 6});
expect(finn.data).eql({age: 3});
done();
});
connection2.endBulk();
});
connection2.endBulk();
});
connection2.endBulk();
});
connection2.endBulk();
});
connection2.endBulk();
});
connection2.endBulk();
});
connection2.endBulk();
});
it(method + ' gets new ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.fetch(function(err) {
it(method + ' gets new ops', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, function(err) {
doc2.fetch(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
done();
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
doc2.on('op', function() {
done();
});
doc2[method]();
});
doc2[method]();
});
});
});
});
it(method + ' calls back after reconnect', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2[method](function(err) {
it(method + ' calls back after reconnect', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
doc2[method](function(err) {
if (err) return done(err);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
});
doc2.connection.close();
process.nextTick(function() {
backend.connect(doc2.connection);
});
});
doc2.connection.close();
process.nextTick(function() {
backend.connect(doc2.connection);
});
it(method + ' returns error passed to doc read middleware', function(done) {
this.backend.use('doc', function(request, next) {
next({message: 'Reject doc read'});
});
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2[method](function(err) {
expect(err.message).equal('Reject doc read');
expect(doc2.version).eql(null);
expect(doc2.data).eql(undefined);
done();
});
});
});
});
it(method + ' returns error passed to doc read middleware', function(done) {
this.backend.use('doc', function(request, next) {
next({message: 'Reject doc read'});
});
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2[method](function(err) {
expect(err.message).equal('Reject doc read');
expect(doc2.version).eql(null);
expect(doc2.data).eql(undefined);
done();
it(method + ' emits error passed to doc read middleware', function(done) {
this.backend.use('doc', function(request, next) {
next({message: 'Reject doc read'});
});
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2[method]();
doc2.on('error', function(err) {
expect(err.message).equal('Reject doc read');
expect(doc2.version).eql(null);
expect(doc2.data).eql(undefined);
done();
});
});
});
});
it(method + ' emits error passed to doc read middleware', function(done) {
this.backend.use('doc', function(request, next) {
next({message: 'Reject doc read'});
});
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2[method]();
doc2.on('error', function(err) {
expect(err.message).equal('Reject doc read');
expect(doc2.version).eql(null);
expect(doc2.data).eql(undefined);
done();
it(method + ' will call back when ops are pending', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc.pause();
doc.submitOp({p: ['age'], na: 1});
doc[method](done);
});
});
});
it(method + ' will call back when ops are pending', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
it(method + ' will not call back when creating the doc is pending', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.pause();
doc.submitOp({p: ['age'], na: 1});
doc.create({age: 3});
doc[method](done);
// HACK: Delay done call to keep from closing the db connection too soon
setTimeout(done, 10);
});
});
it(method + ' will not call back when creating the doc is pending', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
doc.create({age: 3});
doc[method](done);
// HACK: Delay done call to keep from closing the db connection too soon
setTimeout(done, 10);
});
it(method + ' will wait for write when doc is locally created', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.pause();
var calls = 0;
doc.create({age: 3}, function(err) {
if (err) return done(err);
calls++;
});
doc[method](function(err) {
if (err) return done(err);
expect(calls).equal(1);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 3});
done();
});
setTimeout(function() {
doc.resume();
}, 10);
});
it(method + ' will wait for write when doc is locally created and will fail to submit', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc2.create({age: 5}, function(err) {
if (err) return done(err);
it(method + ' will wait for write when doc is locally created', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.pause();
var calls = 0;
doc.create({age: 3}, function(err) {
expect(err).ok();
if (err) return done(err);
calls++;

@@ -294,3 +326,3 @@ });

expect(doc.version).equal(1);
expect(doc.data).eql({age: 5});
expect(doc.data).eql({age: 3});
done();

@@ -302,102 +334,106 @@ });

});
});
});
it('unsubscribe calls back immediately on disconnect', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
doc.subscribe(function(err) {
if (err) return done(err);
doc.unsubscribe(done);
doc.connection.close();
it(method + ' will wait for write when doc is locally created and will fail to submit', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc2.create({age: 5}, function(err) {
if (err) return done(err);
doc.pause();
var calls = 0;
doc.create({age: 3}, function(err) {
expect(err).instanceOf(Error);
calls++;
});
doc[method](function(err) {
if (err) return done(err);
expect(calls).equal(1);
expect(doc.version).equal(1);
expect(doc.data).eql({age: 5});
done();
});
setTimeout(function() {
doc.resume();
}, 10);
});
});
});
});
it('unsubscribe calls back immediately when already disconnected', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
doc.subscribe(function(err) {
if (err) return done(err);
doc.connection.close();
doc.unsubscribe(done);
it('unsubscribe calls back immediately on disconnect', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.subscribe(function(err) {
if (err) return done(err);
doc.unsubscribe(done);
doc.connection.close();
});
});
});
it('subscribed client gets create from other client', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('create', function(context) {
expect(context).equal(false);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
it('unsubscribe calls back immediately when already disconnected', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.subscribe(function(err) {
if (err) return done(err);
doc.connection.close();
doc.unsubscribe(done);
});
doc.create({age: 3});
});
});
it('subscribed client gets op from other client', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
it('subscribed client gets create from other client', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
expect(doc2.version).eql(2);
expect(doc2.data).eql({age: 4});
doc2.on('create', function(context) {
expect(context).equal(false);
expect(doc2.version).eql(1);
expect(doc2.data).eql({age: 3});
done();
});
doc.submitOp({p: ['age'], na: 1});
doc.create({age: 3});
});
});
});
it('disconnecting stops op updates', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
it('subscribed client gets op from other client', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
done();
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function() {
expect(doc2.version).eql(2);
expect(doc2.data).eql({age: 4});
done();
});
doc.submitOp({p: ['age'], na: 1});
});
doc2.connection.close();
doc.submitOp({p: ['age'], na: 1}, done);
});
});
});
it('backend.suppressPublish stops op updates', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
it('disconnecting stops op updates', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
done();
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function() {
done();
});
doc2.connection.close();
doc.submitOp({p: ['age'], na: 1}, done);
});
backend.suppressPublish = true;
doc.submitOp({p: ['age'], na: 1}, done);
});
});
});
it('unsubscribe stops op updates', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
it('backend.suppressPublish stops op updates', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
done();
});
doc2.unsubscribe(function(err) {
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function() {
done();
});
backend.suppressPublish = true;
doc.submitOp({p: ['age'], na: 1}, done);

@@ -407,164 +443,194 @@ });

});
});
it('doc destroy stops op updates', function(done) {
var connection1 = this.backend.connect();
var connection2 = this.backend.connect();
var doc = connection1.get('dogs', 'fido');
var doc2 = connection2.get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
it('unsubscribe stops op updates', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
done(new Error('Should not get op event'));
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function() {
done();
});
doc2.unsubscribe(function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 1}, done);
});
});
doc2.destroy(function(err) {
});
});
it('doc destroy stops op updates', function(done) {
var connection1 = this.backend.connect();
var connection2 = this.backend.connect();
var doc = connection1.get('dogs', 'fido').on('error', done);
var doc2 = connection2.get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
if (err) return done(err);
expect(connection2.getExisting('dogs', 'fido')).equal(undefined);
doc.submitOp({p: ['age'], na: 1}, done);
doc2.on('op', function() {
done(new Error('Should not get op event'));
});
doc2.destroy(function(err) {
if (err) return done(err);
expect(connection2.getExisting('dogs', 'fido')).equal(undefined);
doc.submitOp({p: ['age'], na: 1}, done);
});
});
});
});
});
it('doc destroy removes doc from connection when doc is not subscribed', function(done) {
var connection = this.backend.connect();
var doc = connection.get('dogs', 'fido');
expect(connection.getExisting('dogs', 'fido')).equal(doc);
doc.destroy(function(err) {
if (err) return done(err);
expect(connection.getExisting('dogs', 'fido')).equal(undefined);
done();
it('doc destroy removes doc from connection when doc is not subscribed', function(done) {
var connection = this.backend.connect();
var doc = connection.get('dogs', 'fido').on('error', done);
expect(connection.getExisting('dogs', 'fido')).equal(doc);
doc.destroy(function(err) {
if (err) return done(err);
expect(connection.getExisting('dogs', 'fido')).equal(undefined);
done();
});
});
});
it('bulk unsubscribe stops op updates', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var doc = connection.get('dogs', 'fido');
var fido = connection2.get('dogs', 'fido');
var spot = connection2.get('dogs', 'spot');
doc.create({age: 3}, function(err) {
if (err) return done(err);
async.parallel([
function(cb) { fido.subscribe(cb); },
function(cb) { spot.subscribe(cb); }
], function(err) {
it('bulk unsubscribe stops op updates', function(done) {
var connection = this.backend.connect();
var connection2 = this.backend.connect();
var doc = connection.get('dogs', 'fido').on('error', done);
var fido = connection2.get('dogs', 'fido').on('error', done);
var spot = connection2.get('dogs', 'spot').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
fido.connection.startBulk();
async.parallel([
function(cb) { fido.unsubscribe(cb); },
function(cb) { spot.unsubscribe(cb); }
function(cb) {
fido.subscribe(cb);
},
function(cb) {
spot.subscribe(cb);
}
], function(err) {
if (err) return done(err);
fido.on('op', function(op, context) {
done();
fido.connection.startBulk();
async.parallel([
function(cb) {
fido.unsubscribe(cb);
},
function(cb) {
spot.unsubscribe(cb);
}
], function(err) {
if (err) return done(err);
fido.on('op', function() {
done();
});
doc.submitOp({p: ['age'], na: 1}, done);
});
doc.submitOp({p: ['age'], na: 1}, done);
fido.connection.endBulk();
});
fido.connection.endBulk();
});
});
});
it('a subscribed doc is re-subscribed after reconnect and gets any missing ops', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
it('a subscribed doc is re-subscribed after reconnect and gets any missing ops', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
expect(doc2.version).eql(2);
expect(doc2.data).eql({age: 4});
done();
});
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function() {
expect(doc2.version).eql(2);
expect(doc2.data).eql({age: 4});
done();
});
doc2.connection.close();
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
backend.connect(doc2.connection);
doc2.connection.close();
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
backend.connect(doc2.connection);
});
});
});
});
});
it('calling subscribe, unsubscribe, subscribe sync leaves a doc subscribed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe();
doc2.unsubscribe();
doc2.subscribe(function(err) {
it('calling subscribe, unsubscribe, subscribe sync leaves a doc subscribed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.on('op', function(op, context) {
done();
doc2.subscribe();
doc2.unsubscribe();
doc2.subscribe(function(err) {
if (err) return done(err);
doc2.on('op', function() {
done();
});
doc.submitOp({p: ['age'], na: 1});
});
doc.submitOp({p: ['age'], na: 1});
});
});
});
it('doc fetches ops to catch up if it receives a future op', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
it('doc fetches ops to catch up if it receives a future op', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.create({age: 3}, function(err) {
if (err) return done(err);
var expected = [
[{p: ['age'], na: 1}],
[{p: ['age'], na: 5}],
];
doc2.on('op', function(op, context) {
var item = expected.shift();
expect(op).eql(item);
if (expected.length) return;
expect(doc2.version).equal(3);
expect(doc2.data).eql({age: 9});
done();
});
backend.suppressPublish = true;
doc.submitOp({p: ['age'], na: 1}, function(err) {
doc2.subscribe(function(err) {
if (err) return done(err);
backend.suppressPublish = false;
doc.submitOp({p: ['age'], na: 5});
var expected = [
[{p: ['age'], na: 1}],
[{p: ['age'], na: 5}]
];
doc2.on('op', function(op) {
var item = expected.shift();
expect(op).eql(item);
if (expected.length) return;
expect(doc2.version).equal(3);
expect(doc2.data).eql({age: 9});
done();
});
backend.suppressPublish = true;
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
backend.suppressPublish = false;
doc.submitOp({p: ['age'], na: 5});
});
});
});
});
});
it('doc fetches ops to catch up if it receives multiple future ops', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido');
var doc2 = this.backend.connect().get('dogs', 'fido');
// Delaying op replies will cause multiple future ops to be received
// before the fetch to catch up completes
backend.use('op', function(request, next) {
setTimeout(next, 10 * Math.random());
});
doc.create({age: 3}, function(err) {
if (err) return done(err);
doc2.subscribe(function(err) {
it('doc fetches ops to catch up if it receives multiple future ops', function(done) {
var backend = this.backend;
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
var doc2 = this.backend.connect().get('dogs', 'fido').on('error', done);
// Delaying op replies will cause multiple future ops to be received
// before the fetch to catch up completes
backend.use('op', function(request, next) {
setTimeout(next, 10 * Math.random());
});
doc.create({age: 3}, function(err) {
if (err) return done(err);
var wait = 4;
doc2.on('op', function(op, context) {
if (--wait) return;
expect(doc2.version).eql(5);
expect(doc2.data).eql({age: 122});
done();
});
backend.suppressPublish = true;
doc.submitOp({p: ['age'], na: 1}, function(err) {
doc2.subscribe(function(err) {
if (err) return done(err);
backend.suppressPublish = false;
doc.submitOp({p: ['age'], na: 5}, function(err) {
var wait = 4;
doc2.on('op', function() {
if (--wait) return;
expect(doc2.version).eql(5);
expect(doc2.data).eql({age: 122});
// Wait for whenNothingPending, because the doc might have kicked
// off multiple fetches, and some could be pending still. We want to
// resolve all inflight requests of the database before closing and
// proceeding to the next test
doc2.whenNothingPending(done);
});
backend.suppressPublish = true;
doc.submitOp({p: ['age'], na: 1}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 13}, function(err) {
backend.suppressPublish = false;
doc.submitOp({p: ['age'], na: 5}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 100});
doc.submitOp({p: ['age'], na: 13}, function(err) {
if (err) return done(err);
doc.submitOp({p: ['age'], na: 100});
});
});

@@ -575,58 +641,58 @@ });

});
});
describe('doc.subscribed', function() {
it('is set to false initially', function() {
var doc = this.backend.connect().get('dogs', 'fido');
expect(doc.subscribed).equal(false);
});
it('remains false before subscribe call completes', function() {
var doc = this.backend.connect().get('dogs', 'fido');
doc.subscribe();
expect(doc.subscribed).equal(false);
});
it('is set to true after subscribe completes', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.subscribe(function(err) {
if (err) return done(err);
expect(doc.subscribed).equal(true);
describe('doc.subscribed', function() {
it('is set to false initially', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
expect(doc.subscribed).equal(false);
done();
});
});
it('is not set to true after subscribe completes if already unsubscribed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.subscribe(function(err) {
if (err) return done(err);
it('remains false before subscribe call completes', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.subscribe(done);
expect(doc.subscribed).equal(false);
done();
});
doc.unsubscribe();
});
it('is set to false sychronously in unsubscribe', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.subscribe(function(err) {
if (err) return done(err);
expect(doc.subscribed).equal(true);
it('is set to true after subscribe completes', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.subscribe(function(err) {
if (err) return done(err);
expect(doc.subscribed).equal(true);
done();
});
});
it('is not set to true after subscribe completes if already unsubscribed', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.subscribe(function(err) {
if (err) return done(err);
expect(doc.subscribed).equal(false);
done();
});
doc.unsubscribe();
expect(doc.subscribed).equal(false);
done();
});
});
it('is set to false sychronously on disconnect', function(done) {
var doc = this.backend.connect().get('dogs', 'fido');
doc.subscribe(function(err) {
if (err) return done(err);
expect(doc.subscribed).equal(true);
doc.connection.close();
expect(doc.subscribed).equal(false);
done();
it('is set to false sychronously in unsubscribe', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.subscribe(function(err) {
if (err) return done(err);
expect(doc.subscribed).equal(true);
doc.unsubscribe();
expect(doc.subscribed).equal(false);
done();
});
});
it('is set to false sychronously on disconnect', function(done) {
var doc = this.backend.connect().get('dogs', 'fido').on('error', done);
doc.subscribe(function(err) {
if (err) return done(err);
expect(doc.subscribed).equal(true);
doc.connection.close();
expect(doc.subscribed).equal(false);
done();
});
});
});
});
});
};

@@ -1,4 +0,4 @@

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');

@@ -19,3 +19,3 @@ describe('DB base class', function() {

db.commit('testcollection', 'test', {}, {}, null, function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
done();

@@ -28,3 +28,3 @@ });

db.getSnapshot('testcollection', 'foo', null, null, function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
done();

@@ -37,3 +37,3 @@ });

db.getOps('testcollection', 'foo', 0, null, null, function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
done();

@@ -46,3 +46,3 @@ });

db.query('testcollection', {x: 5}, null, null, function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
done();

@@ -55,3 +55,3 @@ });

db.queryPollDoc('testcollection', 'foo', {x: 5}, null, function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
done();

@@ -62,65 +62,2 @@ });

// Extension of MemoryDB that supports query filters and sorts on simple
// top-level properties, which is enough for the core ShareDB tests on
// query subscription updating.
function BasicQueryableMemoryDB() {
MemoryDB.apply(this, arguments);
}
BasicQueryableMemoryDB.prototype = Object.create(MemoryDB.prototype);
BasicQueryableMemoryDB.prototype.constructor = BasicQueryableMemoryDB;
BasicQueryableMemoryDB.prototype._querySync = function(snapshots, query, options) {
if (query.filter) {
snapshots = snapshots.filter(function(snapshot) {
for (var queryKey in query.filter) {
// This fake only supports simple property equality filters, so
// throw an error on Mongo-like filter properties with dots.
if (queryKey.includes('.')) {
throw new Error('Only simple property filters are supported, got:', queryKey);
}
if (snapshot.data[queryKey] !== query.filter[queryKey]) {
return false;
}
}
return true;
});
}
if (query.sort) {
if (!Array.isArray(query.sort)) {
throw new Error('query.sort must be an array');
}
if (query.sort.length) {
snapshots.sort(snapshotComparator(query.sort));
}
}
return {snapshots: snapshots};
};
// sortProperties is an array whose items are each [propertyName, direction].
function snapshotComparator(sortProperties) {
return function(snapshotA, snapshotB) {
for (var i = 0; i < sortProperties.length; i++) {
var sortProperty = sortProperties[i];
var sortKey = sortProperty[0];
var sortDirection = sortProperty[1];
var aPropVal = snapshotA.data[sortKey];
var bPropVal = snapshotB.data[sortKey];
if (aPropVal < bPropVal) {
return -1 * sortDirection;
} else if (aPropVal > bPropVal) {
return sortDirection;
} else if (aPropVal === bPropVal) {
continue;
} else {
throw new Error('Could not compare ' + aPropVal + ' and ' + bPropVal);
}
}
return 0;
};
}
// Run all the DB-based tests against the BasicQueryableMemoryDB.

@@ -127,0 +64,0 @@ require('./db')({

var async = require('async');
var expect = require('expect.js');
var expect = require('chai').expect;
var Backend = require('../lib/backend');

@@ -39,3 +39,3 @@ var ot = require('../lib/ot');

require('./client/projections')();
require('./client/projections')({getQuery: getQuery});
require('./client/query-subscribe')({getQuery: getQuery});

@@ -91,3 +91,3 @@ require('./client/query')({getQuery: getQuery});

expect(ops.length).equal(1);
expect(ops[0].create).ok();
expect(ops[0].create).ok;
}

@@ -122,4 +122,4 @@

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;
}

@@ -132,3 +132,3 @@

expect(snapshot.type).equal(null);
expect(ops[0].create).ok();
expect(ops[0].create).ok;
expect(ops[1].del).equal(true);

@@ -245,3 +245,3 @@ }

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -259,2 +259,3 @@ db.getSnapshot('testcollection', 'test', null, null, function(err, result) {

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.getSnapshot('testcollection', 'test', null, null, function(err, result) {

@@ -271,2 +272,3 @@ if (err) return done(err);

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.getSnapshot('testcollection', 'test', null, {metadata: true}, function(err, result) {

@@ -283,2 +285,3 @@ if (err) return done(err);

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, result) {

@@ -298,3 +301,3 @@ if (err) return done(err);

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -315,2 +318,3 @@ db.getSnapshotBulk('testcollection', ['test2', 'test'], null, null, function(err, resultMap) {

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.getSnapshotBulk('testcollection', ['test2', 'test'], null, null, function(err, resultMap) {

@@ -327,2 +331,3 @@ if (err) return done(err);

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.getSnapshotBulk('testcollection', ['test2', 'test'], null, {metadata: true}, function(err, resultMap) {

@@ -349,3 +354,3 @@ if (err) return done(err);

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -364,5 +369,5 @@ db.getOps('testcollection', 'test', 0, null, null, function(err, ops) {

var db = this.db;
submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);

@@ -382,5 +387,5 @@ db.getOps('testcollection', 'test', 0, null, null, function(err, ops) {

var db = this.db;
submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);

@@ -400,5 +405,5 @@ db.getOps('testcollection', 'test', null, null, null, function(err, ops) {

var db = this.db;
submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);

@@ -418,5 +423,5 @@ db.getOps('testcollection', 'test', 1, null, null, function(err, ops) {

var db = this.db;
submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);

@@ -435,3 +440,3 @@ db.getOps('testcollection', 'test', 0, 1, null, function(err, ops) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -449,3 +454,3 @@ db.getOps('testcollection', 'test', null, null, null, function(err, ops) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -476,5 +481,5 @@ db.getOps('testcollection', 'test', null, null, {metadata: true}, function(err, ops) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test2', op, function(err, succeeded) {
submit(db, 'testcollection', 'test2', op, function(err) {
if (err) return done(err);

@@ -496,5 +501,5 @@ db.getOpsBulk('testcollection', {test: 0, test2: 0}, null, null, function(err, opsMap) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test2', op, function(err, succeeded) {
submit(db, 'testcollection', 'test2', op, function(err) {
if (err) return done(err);

@@ -517,9 +522,9 @@ db.getOpsBulk('testcollection', {test: null, test2: null}, null, null, function(err, opsMap) {

var db = this.db;
submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test2', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test2', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test2', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test2', op1, function(err) {
if (err) return done(err);

@@ -544,9 +549,9 @@ db.getOpsBulk('testcollection', {test: 0, test2: 1}, null, null, function(err, opsMap) {

var db = this.db;
submit(db, 'testcollection', 'test', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test2', op0, function(err, succeeded) {
submit(db, 'testcollection', 'test2', op0, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test', op1, function(err) {
if (err) return done(err);
submit(db, 'testcollection', 'test2', op1, function(err, succeeded) {
submit(db, 'testcollection', 'test2', op1, function(err) {
if (err) return done(err);

@@ -570,3 +575,3 @@ db.getOpsBulk('testcollection', {test: 1, test2: 0}, {test2: 1}, null, function(err, opsMap) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -584,3 +589,3 @@ db.getOpsBulk('testcollection', {test: null}, null, null, function(err, opsMap) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -600,3 +605,3 @@ db.getOpsBulk('testcollection', {test: null}, null, {metadata: true}, function(err, opsMap) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -617,3 +622,3 @@ db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -634,3 +639,3 @@ db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) {

var db = this.db;
submit(db, 'testcollection', 'test', op, function(err, succeeded) {
submit(db, 'testcollection', 'test', op, function(err) {
if (err) return done(err);

@@ -653,3 +658,3 @@ db.getSnapshot('testcollection', 'test', {$submit: true}, null, function(err, snapshot) {

var db = this.db;
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err, succeeded) {
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
if (err) return done(err);

@@ -676,2 +681,3 @@ db.query('testcollection', {x: 5}, null, null, function(err, results) {

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.query('testcollection', {x: 5}, null, null, function(err, results) {

@@ -688,2 +694,3 @@ if (err) return done(err);

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.query('testcollection', {x: 5}, null, {metadata: true}, function(err, results) {

@@ -705,2 +712,3 @@ if (err) return done(err);

db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
if (err) return done(err);
db.query('testcollection', {x: 5}, {y: true}, null, function(err, results) {

@@ -720,2 +728,3 @@ if (err) return done(err);

db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
if (err) return done(err);
db.query('testcollection', {x: 5}, {}, null, function(err, results) {

@@ -732,2 +741,3 @@ if (err) return done(err);

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.query('testcollection', {x: 5}, {x: true}, null, function(err, results) {

@@ -744,2 +754,3 @@ if (err) return done(err);

commitSnapshotWithMetadata(db, function(err) {
if (err) return done(err);
db.query('testcollection', {x: 5}, {x: true}, {metadata: true}, function(err, results) {

@@ -758,5 +769,6 @@ if (err) return done(err);

var db = this.db;
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err, succeeded) {
db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
if (err) return done(err);
db.queryPoll('testcollection', {x: 5}, null, function(err, ids) {
var dbQuery = getQuery({query: {x: 5}});
db.queryPoll('testcollection', dbQuery, null, function(err, ids) {
if (err) return done(err);

@@ -770,3 +782,4 @@ expect(ids).eql(['test']);

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);

@@ -781,7 +794,7 @@ expect(ids).eql([]);

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);

@@ -794,4 +807,4 @@ expect(result).equal(false);

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();

@@ -801,3 +814,4 @@ var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}};

db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
db.queryPollDoc('testcollection', 'test', query, null, function(err, result) {
if (err) return done(err);
db.queryPollDoc('testcollection', 'test', dbQuery, null, function(err, result) {
if (err) return done(err);

@@ -811,4 +825,4 @@ expect(result).equal(true);

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();

@@ -818,3 +832,4 @@ var snapshot = {type: 'json0', v: 1, data: {x: 5, y: 6}};

db.commit('testcollection', 'test', {v: 0, create: {}}, snapshot, null, function(err) {
db.queryPollDoc('testcollection', 'test', query, null, function(err, result) {
if (err) return done(err);
db.queryPollDoc('testcollection', 'test', dbQuery, null, function(err, result) {
if (err) return done(err);

@@ -834,5 +849,5 @@ expect(result).equal(false);

{type: 'json0', id: '0', v: 1, data: {foo: 1, bar: 1}, m: null},
{type: 'json0', id: '1', v: 1, data: { foo: 2, bar: 1 }, m: null},
{type: 'json0', id: '2', v: 1, data: { foo: 1, bar: 2 }, m: null},
{type: 'json0', id: '3', v: 1, data: { foo: 2, bar: 2 }, m: null}
{type: 'json0', id: '1', v: 1, data: {foo: 2, bar: 1}, m: null},
{type: 'json0', id: '2', v: 1, data: {foo: 1, bar: 2}, m: null},
{type: 'json0', id: '3', v: 1, data: {foo: 2, bar: 2}, m: null}
];

@@ -839,0 +854,0 @@ var db = this.db;

var Logger = require('../lib/logger/logger');
var expect = require('expect.js');
var expect = require('chai').expect;
var sinon = require('sinon');
describe('Logger', function () {
describe('Stubbing console.warn', function () {
beforeEach(function () {
describe('Logger', function() {
describe('Stubbing console.warn', function() {
beforeEach(function() {
sinon.stub(console, 'warn');
});
afterEach(function () {
afterEach(function() {
sinon.restore();
});
it('logs to console by default', function () {
it('logs to console by default', function() {
var logger = new Logger();
logger.warn('warning');
expect(console.warn.calledOnceWithExactly('warning')).to.be(true);
expect(console.warn.calledOnceWithExactly('warning')).to.equal(true);
});
it('overrides console', function () {
it('overrides console', function() {
var customWarn = sinon.stub();

@@ -30,7 +30,7 @@ var logger = new Logger();

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 () {
it('only overrides if provided with a method', function() {
var badWarn = 'not a function';

@@ -44,5 +44,5 @@ var logger = new Logger();

expect(console.warn.calledOnceWithExactly('warning')).to.be(true);
expect(console.warn.calledOnceWithExactly('warning')).to.equal(true);
});
});
});

@@ -1,4 +0,3 @@

var async = require('async');
var Backend = require('../lib/backend');
var expect = require('expect.js');
var expect = require('chai').expect;
var util = require('./util');

@@ -8,3 +7,2 @@ var types = require('../lib/types');

describe('middleware', function() {
beforeEach(function() {

@@ -26,12 +24,14 @@ this.backend = new Backend();

describe('use', function() {
it('returns itself to allow chaining', function() {
var response = this.backend.use('submit', function(request, next) {});
var response = this.backend.use('submit', function() {});
expect(response).equal(this.backend);
});
it('accepts an array of action names', function() {
var response = this.backend.use(['submit', 'connect'], function() {});
expect(response).equal(this.backend);
});
});
describe('connect', function() {
it('passes the agent on connect', function(done) {

@@ -62,3 +62,2 @@ var clientId;

});
});

@@ -142,3 +141,2 @@

var doneAfter = util.callAfter(2, done);
var i = 0;
backend.use('doc', function(request, next) {

@@ -228,5 +226,73 @@ doneAfter();

describe('reply', function() {
beforeEach(function(done) {
this.snapshot = {v: 1, type: 'json0', data: {age: 3}};
this.backend.db.commit('dogs', 'fido', {v: 0, create: {}}, this.snapshot, null, done);
});
it('context has request and reply objects', function(done) {
var snapshot = this.snapshot;
this.backend.use('reply', function(replyContext, next) {
expect(replyContext).to.have.property('action', 'reply');
expect(replyContext.request).to.eql({a: 'qf', id: 1, c: 'dogs', q: {age: 3}});
expect(replyContext.reply).to.eql({
data: [{v: 1, data: snapshot.data, d: 'fido'}],
extra: undefined,
a: 'qf',
id: 1
});
expect(replyContext).to.have.property('agent');
expect(replyContext).to.have.property('backend');
next();
});
var connection = this.backend.connect();
connection.createFetchQuery('dogs', {age: 3}, null, function(err, results) {
if (err) {
return done(err);
}
expect(results).to.have.length(1);
expect(results[0].data).to.eql(snapshot.data);
done();
});
});
it('can produce errors that get sent back to client', function(done) {
var errorMessage = 'This is an error from reply middleware';
this.backend.use('reply', function(_replyContext, next) {
next(errorMessage);
});
var connection = this.backend.connect();
var doc = connection.get('dogs', 'fido');
doc.fetch(function(err) {
expect(err).to.have.property('message', errorMessage);
done();
});
});
it('can make raw additions to query reply extra', function(done) {
var snapshot = this.snapshot;
this.backend.use('reply', function(replyContext, next) {
expect(replyContext.request.a === 'qf');
replyContext.reply.extra = replyContext.reply.extra || {};
replyContext.reply.extra.replyMiddlewareValue = 'some value';
next();
});
var connection = this.backend.connect();
connection.createFetchQuery('dogs', {age: 3}, null, function(err, results, extra) {
if (err) {
return done(err);
}
expect(results).to.have.length(1);
expect(results[0].data).to.eql(snapshot.data);
expect(extra).to.eql({replyMiddlewareValue: 'some value'});
done();
});
});
});
describe('submit lifecycle', function() {
// DEPRECATED: 'after submit' is a synonym for 'afterSubmit'
['submit', 'apply', 'commit', 'afterSubmit', 'after submit'].forEach(function(action) {
// DEPRECATED: 'after submit' and 'afterSubmit' are synonyms for 'afterWrite'
['submit', 'apply', 'commit', 'afterWrite', 'afterSubmit', 'after submit'].forEach(function(action) {
it(action + ' gets options passed to backend.submit', function(done) {

@@ -246,2 +312,86 @@ var doneAfter = util.callAfter(1, done);

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();
});
});
});
});
});
});

@@ -1,2 +0,2 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var Backend = require('../lib/backend');

@@ -8,12 +8,12 @@ var MilestoneDB = require('../lib/milestone-db');

describe('Base class', function () {
describe('Base class', function() {
var db;
beforeEach(function () {
beforeEach(function() {
db = new MilestoneDB();
});
it('calls back with an error when trying to get a snapshot', function (done) {
db.getMilestoneSnapshot('books', '123', 1, function (error) {
expect(error.code).to.be(5019);
it('calls back with an error when trying to get a snapshot', function(done) {
db.getMilestoneSnapshot('books', '123', 1, function(error) {
expect(error.code).to.equal(5019);
done();

@@ -23,5 +23,5 @@ });

it('emits an error when trying to get a snapshot', function (done) {
db.on('error', function (error) {
expect(error.code).to.be(5019);
it('emits an error when trying to get a snapshot', function(done) {
db.on('error', function(error) {
expect(error.code).to.equal(5019);
done();

@@ -33,5 +33,5 @@ });

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);
it('calls back with an error when trying to save a snapshot', function(done) {
db.saveMilestoneSnapshot('books', {}, function(error) {
expect(error.code).to.equal(5020);
done();

@@ -41,5 +41,5 @@ });

it('emits an error when trying to save a snapshot', function (done) {
db.on('error', function (error) {
expect(error.code).to.be(5020);
it('emits an error when trying to save a snapshot', function(done) {
db.on('error', function(error) {
expect(error.code).to.equal(5020);
done();

@@ -51,5 +51,5 @@ });

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);
it('calls back with an error when trying to get a snapshot before a time', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime('books', '123', 1000, function(error) {
expect(error.code).to.equal(5021);
done();

@@ -59,5 +59,5 @@ });

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);
it('calls back with an error when trying to get a snapshot after a time', function(done) {
db.getMilestoneSnapshotAtOrAfterTime('books', '123', 1000, function(error) {
expect(error.code).to.equal(5022);
done();

@@ -68,10 +68,10 @@ });

describe('NoOpMilestoneDB', function () {
describe('NoOpMilestoneDB', function() {
var db;
beforeEach(function () {
beforeEach(function() {
db = new NoOpMilestoneDB();
});
it('does not error when trying to save and fetch a snapshot', function (done) {
it('does not error when trying to save and fetch a snapshot', function(done) {
var snapshot = new Snapshot(

@@ -81,3 +81,3 @@ 'catcher-in-the-rye',

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye' },
{title: 'Catcher in the Rye'},
null

@@ -87,10 +87,10 @@ );

util.callInSeries([
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot, next);
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();

@@ -102,4 +102,4 @@ },

it('emits an event when saving without a callback', function (done) {
db.on('save', function () {
it('emits an event when saving without a callback', function(done) {
db.on('save', function() {
done();

@@ -112,14 +112,14 @@ });

module.exports = function (options) {
module.exports = function(options) {
var create = options.create;
describe('Milestone Database', function () {
describe('Milestone Database', function() {
var db;
var backend;
beforeEach(function (done) {
create(function (error, createdDb) {
beforeEach(function(done) {
create(function(error, createdDb) {
if (error) return done(error);
db = createdDb;
backend = new Backend({ milestoneDb: db });
backend = new Backend({milestoneDb: db});
done();

@@ -129,8 +129,8 @@ });

afterEach(function (done) {
afterEach(function(done) {
db.close(done);
});
it('can call close() without a callback', function (done) {
create(function (error, db) {
it('can call close() without a callback', function(done) {
create(function(error, db) {
if (error) return done(error);

@@ -142,3 +142,3 @@ db.close();

it('stores and fetches a milestone snapshot', function (done) {
it('stores and fetches a milestone snapshot', function(done) {
var snapshot = new Snapshot(

@@ -148,3 +148,3 @@ 'catcher-in-the-rye',

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye' },
{title: 'Catcher in the Rye'},
null

@@ -154,9 +154,9 @@ );

util.callInSeries([
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot, next);
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
},
function (retrievedSnapshot, next) {
function(retrievedSnapshot, next) {
expect(retrievedSnapshot).to.eql(snapshot);

@@ -169,3 +169,3 @@ next();

it('fetches the most recent snapshot before the requested version', function (done) {
it('fetches the most recent snapshot before the requested version', function(done) {
var snapshot1 = new Snapshot(

@@ -175,3 +175,3 @@ 'catcher-in-the-rye',

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye' },
{title: 'Catcher in the Rye'},
null

@@ -184,3 +184,3 @@ );

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye', author: 'J.D. Salinger' },
{title: 'Catcher in the Rye', author: 'J.D. Salinger'},
null

@@ -193,3 +193,3 @@ );

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye', author: 'J.D. Salinger', publicationDate: '1951-07-16' },
{title: 'Catcher in the Rye', author: 'J.D. Salinger', publicationDate: '1951-07-16'},
null

@@ -199,15 +199,15 @@ );

util.callInSeries([
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot1, next);
},
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot2, next);
},
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot10, next);
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot2);

@@ -220,3 +220,3 @@ next();

it('fetches the most recent snapshot even if they are inserted in the wrong order', function (done) {
it('fetches the most recent snapshot even if they are inserted in the wrong order', function(done) {
var snapshot1 = new Snapshot(

@@ -226,3 +226,3 @@ 'catcher-in-the-rye',

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye' },
{title: 'Catcher in the Rye'},
null

@@ -235,3 +235,3 @@ );

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye', author: 'J.D. Salinger' },
{title: 'Catcher in the Rye', author: 'J.D. Salinger'},
null

@@ -241,12 +241,12 @@ );

util.callInSeries([
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot2, next);
},
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot1, next);
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot2);

@@ -259,3 +259,3 @@ next();

it('fetches the most recent snapshot when the version is null', function (done) {
it('fetches the most recent snapshot when the version is null', function(done) {
var snapshot1 = new Snapshot(

@@ -265,3 +265,3 @@ 'catcher-in-the-rye',

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye' },
{title: 'Catcher in the Rye'},
null

@@ -274,3 +274,3 @@ );

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye', author: 'J.D. Salinger' },
{title: 'Catcher in the Rye', author: 'J.D. Salinger'},
null

@@ -280,12 +280,12 @@ );

util.callInSeries([
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot1, next);
},
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot2, next);
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot2);

@@ -298,5 +298,5 @@ next();

it('errors when fetching an undefined version', function (done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function (error) {
expect(error).to.be.ok();
it('errors when fetching an undefined version', function(done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', undefined, function(error) {
expect(error).instanceOf(Error);
done();

@@ -306,5 +306,5 @@ });

it('errors when fetching version -1', function (done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', -1, function (error) {
expect(error).to.be.ok();
it('errors when fetching version -1', function(done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', -1, function(error) {
expect(error).instanceOf(Error);
done();

@@ -314,5 +314,5 @@ });

it('errors when fetching version "foo"', function (done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 'foo', function (error) {
expect(error).to.be.ok();
it('errors when fetching version "foo"', function(done) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 'foo', function(error) {
expect(error).instanceOf(Error);
done();

@@ -322,5 +322,5 @@ });

it('errors when fetching a null collection', function (done) {
db.getMilestoneSnapshot(null, 'catcher-in-the-rye', 1, function (error) {
expect(error).to.be.ok();
it('errors when fetching a null collection', function(done) {
db.getMilestoneSnapshot(null, 'catcher-in-the-rye', 1, function(error) {
expect(error).instanceOf(Error);
done();

@@ -330,5 +330,5 @@ });

it('errors when fetching a null ID', function (done) {
db.getMilestoneSnapshot('books', null, 1, function (error) {
expect(error).to.be.ok();
it('errors when fetching a null ID', function(done) {
db.getMilestoneSnapshot('books', null, 1, function(error) {
expect(error).instanceOf(Error);
done();

@@ -338,3 +338,3 @@ });

it('errors when saving a null collection', function (done) {
it('errors when saving a null collection', function(done) {
var snapshot = new Snapshot(

@@ -344,8 +344,8 @@ 'catcher-in-the-rye',

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye' },
{title: 'Catcher in the Rye'},
null
);
db.saveMilestoneSnapshot(null, snapshot, function (error) {
expect(error).to.be.ok();
db.saveMilestoneSnapshot(null, snapshot, function(error) {
expect(error).instanceOf(Error);
done();

@@ -355,9 +355,9 @@ });

it('returns undefined if no snapshot exists', function (done) {
it('returns undefined if no snapshot exists', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();

@@ -369,13 +369,13 @@ },

it('does not store a milestone snapshot on commit', function (done) {
it('does not store a milestone snapshot on commit', function(done) {
util.callInSeries([
function (next) {
function(next) {
var doc = backend.connect().get('books', 'catcher-in-the-rye');
doc.create({ title: 'Catcher in the Rye' }, next);
doc.create({title: 'Catcher in the Rye'}, next);
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', null, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();

@@ -387,3 +387,3 @@ },

it('can save without a callback', function (done) {
it('can save without a callback', function(done) {
var snapshot = new Snapshot(

@@ -393,8 +393,8 @@ 'catcher-in-the-rye',

'http://sharejs.org/types/JSONv0',
{ title: 'Catcher in the Rye' },
{title: 'Catcher in the Rye'},
null
);
db.on('save', function (collection, snapshot) {
expect(collection).to.be('books');
db.on('save', function(collection, snapshot) {
expect(collection).to.equal('books');
expect(snapshot).to.eql(snapshot);

@@ -407,5 +407,5 @@ done();

it('errors when the snapshot is undefined', function (done) {
db.saveMilestoneSnapshot('books', undefined, function (error) {
expect(error).to.be.ok();
it('errors when the snapshot is undefined', function(done) {
db.saveMilestoneSnapshot('books', undefined, function(error) {
expect(error).instanceOf(Error);
done();

@@ -415,3 +415,3 @@ });

describe('snapshots with timestamps', function () {
describe('snapshots with timestamps', function() {
var snapshot1 = new Snapshot(

@@ -458,11 +458,11 @@ 'catcher-in-the-rye',

beforeEach(function (done) {
beforeEach(function(done) {
util.callInSeries([
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot1, next);
},
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot2, next);
},
function (next) {
function(next) {
db.saveMilestoneSnapshot('books', snapshot3, next);

@@ -474,9 +474,9 @@ },

describe('fetching a snapshot before or at a time', function () {
it('fetches a snapshot before a given time', function (done) {
describe('fetching a snapshot before or at a time', function() {
it('fetches a snapshot before a given time', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2500, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot2);

@@ -489,8 +489,8 @@ next();

it('fetches a snapshot at an exact time', function (done) {
it('fetches a snapshot at an exact time', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 2000, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot2);

@@ -503,8 +503,8 @@ next();

it('fetches the first snapshot for a null timestamp', function (done) {
it('fetches the first snapshot for a null timestamp', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', null, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot1);

@@ -517,5 +517,5 @@ next();

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();
it('returns an error for a string timestamp', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) {
expect(error).instanceOf(Error);
done();

@@ -525,5 +525,5 @@ });

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();
it('returns an error for a negative timestamp', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', -1, function(error) {
expect(error).instanceOf(Error);
done();

@@ -533,9 +533,9 @@ });

it('returns undefined if there are no snapshots before a time', function (done) {
it('returns undefined if there are no snapshots before a time', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrBeforeTime('books', 'catcher-in-the-rye', 0, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();

@@ -547,5 +547,5 @@ },

it('errors if no collection is provided', function (done) {
db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function (error) {
expect(error).to.be.ok();
it('errors if no collection is provided', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime(undefined, 'catcher-in-the-rye', 0, function(error) {
expect(error).instanceOf(Error);
done();

@@ -555,5 +555,5 @@ });

it('errors if no ID is provided', function (done) {
db.getMilestoneSnapshotAtOrBeforeTime('books', undefined, 0, function (error) {
expect(error).to.be.ok();
it('errors if no ID is provided', function(done) {
db.getMilestoneSnapshotAtOrBeforeTime('books', undefined, 0, function(error) {
expect(error).instanceOf(Error);
done();

@@ -564,9 +564,9 @@ });

describe('fetching a snapshot after or at a time', function () {
it('fetches a snapshot after a given time', function (done) {
describe('fetching a snapshot after or at a time', function() {
it('fetches a snapshot after a given time', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2500, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot3);

@@ -579,8 +579,8 @@ next();

it('fetches a snapshot at an exact time', function (done) {
it('fetches a snapshot at an exact time', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 2000, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot2);

@@ -593,8 +593,8 @@ next();

it('fetches the last snapshot for a null timestamp', function (done) {
it('fetches the last snapshot for a null timestamp', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', null, next);
},
function (snapshot, next) {
function(snapshot, next) {
expect(snapshot).to.eql(snapshot3);

@@ -607,5 +607,5 @@ next();

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();
it('returns an error for a string timestamp', function(done) {
db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 'not-a-timestamp', function(error) {
expect(error).instanceOf(Error);
done();

@@ -615,5 +615,5 @@ });

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();
it('returns an error for a negative timestamp', function(done) {
db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', -1, function(error) {
expect(error).instanceOf(Error);
done();

@@ -623,9 +623,9 @@ });

it('returns undefined if there are no snapshots after a time', function (done) {
it('returns undefined if there are no snapshots after a time', function(done) {
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshotAtOrAfterTime('books', 'catcher-in-the-rye', 4000, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();

@@ -637,5 +637,5 @@ },

it('errors if no collection is provided', function (done) {
db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function (error) {
expect(error).to.be.ok();
it('errors if no collection is provided', function(done) {
db.getMilestoneSnapshotAtOrAfterTime(undefined, 'catcher-in-the-rye', 0, function(error) {
expect(error).instanceOf(Error);
done();

@@ -645,5 +645,5 @@ });

it('errors if no ID is provided', function (done) {
db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function (error) {
expect(error).to.be.ok();
it('errors if no ID is provided', function(done) {
db.getMilestoneSnapshotAtOrAfterTime('books', undefined, 0, function(error) {
expect(error).instanceOf(Error);
done();

@@ -655,10 +655,10 @@ });

describe('milestones enabled for every version', function () {
beforeEach(function (done) {
var options = { interval: 1 };
describe('milestones enabled for every version', function() {
beforeEach(function(done) {
var options = {interval: 1};
create(options, function (error, createdDb) {
create(options, function(error, createdDb) {
if (error) return done(error);
db = createdDb;
backend = new Backend({ milestoneDb: db });
backend = new Backend({milestoneDb: db});
done();

@@ -668,6 +668,6 @@ });

it('stores a milestone snapshot on commit', function (done) {
db.on('save', function (collection, snapshot) {
expect(collection).to.be('books');
expect(snapshot.data).to.eql({ title: 'Catcher in the Rye' });
it('stores a milestone snapshot on commit', function(done) {
db.on('save', function(collection, snapshot) {
expect(collection).to.equal('books');
expect(snapshot.data).to.eql({title: 'Catcher in the Rye'});
done();

@@ -677,14 +677,14 @@ });

var doc = backend.connect().get('books', 'catcher-in-the-rye');
doc.create({ title: 'Catcher in the Rye' });
doc.create({title: 'Catcher in the Rye'});
});
});
describe('milestones enabled for every other version', function () {
beforeEach(function (done) {
var options = { interval: 2 };
describe('milestones enabled for every other version', function() {
beforeEach(function(done) {
var options = {interval: 2};
create(options, function (error, createdDb) {
create(options, function(error, createdDb) {
if (error) return done(error);
db = createdDb;
backend = new Backend({ milestoneDb: db });
backend = new Backend({milestoneDb: db});
done();

@@ -694,33 +694,33 @@ });

it('only stores even-numbered versions', function (done) {
db.on('save', function (collection, snapshot) {
it('only stores even-numbered versions', function(done) {
db.on('save', function(collection, snapshot) {
if (snapshot.v !== 4) return;
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
},
function (snapshot, next) {
expect(snapshot.v).to.be(2);
function(snapshot, next) {
expect(snapshot.v).to.equal(2);
next();
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next);
},
function (snapshot, next) {
expect(snapshot.v).to.be(2);
function(snapshot, next) {
expect(snapshot.v).to.equal(2);
next();
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
},
function (snapshot, next) {
expect(snapshot.v).to.be(4);
function(snapshot, next) {
expect(snapshot.v).to.equal(4);
next();

@@ -735,13 +735,13 @@ },

util.callInSeries([
function (next) {
doc.create({ title: 'Catcher in the Rye' }, next);
function(next) {
doc.create({title: 'Catcher in the Rye'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], oi: 'J.F.Salinger' }, next);
function(next) {
doc.submitOp({p: ['author'], oi: 'J.F.Salinger'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger' }, next);
function(next) {
doc.submitOp({p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger' }, next);
function(next) {
doc.submitOp({p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger'}, next);
}

@@ -751,4 +751,4 @@ ]);

it('can have the saving logic overridden in middleware', function (done) {
backend.use('commit', function (request, callback) {
it('can have the saving logic overridden in middleware', function(done) {
backend.use('commit', function(request, callback) {
request.saveMilestoneSnapshot = request.snapshot.v >= 3;

@@ -758,32 +758,32 @@ callback();

db.on('save', function (collection, snapshot) {
db.on('save', function(collection, snapshot) {
if (snapshot.v !== 4) return;
util.callInSeries([
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 1, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 2, next);
},
function (snapshot, next) {
expect(snapshot).to.be(undefined);
function(snapshot, next) {
expect(snapshot).to.equal(undefined);
next();
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 3, next);
},
function (snapshot, next) {
expect(snapshot.v).to.be(3);
function(snapshot, next) {
expect(snapshot.v).to.equal(3);
next();
},
function (next) {
function(next) {
db.getMilestoneSnapshot('books', 'catcher-in-the-rye', 4, next);
},
function (snapshot, next) {
expect(snapshot.v).to.be(4);
function(snapshot, next) {
expect(snapshot.v).to.equal(4);
next();

@@ -798,13 +798,13 @@ },

util.callInSeries([
function (next) {
doc.create({ title: 'Catcher in the Rye' }, next);
function(next) {
doc.create({title: 'Catcher in the Rye'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], oi: 'J.F.Salinger' }, next);
function(next) {
doc.submitOp({p: ['author'], oi: 'J.F.Salinger'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger' }, next);
function(next) {
doc.submitOp({p: ['author'], od: 'J.F.Salinger', oi: 'J.D.Salinger'}, next);
},
function (next) {
doc.submitOp({ p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger' }, next);
function(next) {
doc.submitOp({p: ['author'], od: 'J.D.Salinger', oi: 'J.D. Salinger'}, next);
}

@@ -811,0 +811,0 @@ ]);

@@ -1,2 +0,2 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var ot = require('../lib/ot');

@@ -6,28 +6,27 @@ 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;
});

@@ -41,7 +40,7 @@

it('accepts valid delete operations', function() {
expect(ot.checkOp({del:true})).equal();
expect(ot.checkOp({del: true})).equal();
});
it('accepts valid ops', function() {
expect(ot.checkOp({op:[1,2,3]})).equal();
expect(ot.checkOp({op: [1, 2, 3]})).equal();
});

@@ -60,8 +59,8 @@ });

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;
});

@@ -78,3 +77,3 @@

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

@@ -113,7 +112,7 @@ expect(doc).eql({v: 6, create: {type: type.uri}});

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;
});

@@ -147,3 +146,3 @@

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'}]});

@@ -154,11 +153,11 @@ });

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;
});

@@ -173,3 +172,3 @@

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;
});

@@ -210,7 +209,7 @@

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;
});

@@ -250,3 +249,2 @@

});
});

@@ -1,2 +0,2 @@

var expect = require('expect.js');
var expect = require('chai').expect;
var projections = require('../lib/projections');

@@ -6,3 +6,2 @@ var type = require('../lib/types').defaultType.uri;

describe('projection utility methods', function() {
describe('projectSnapshot', function() {

@@ -17,3 +16,3 @@ function test(fields, snapshot, expected) {

projections.projectSnapshot({}, {type: 'other', data: 123});
}).throwException();
}).throw(Error);
});

@@ -96,4 +95,4 @@

{x: true},
{type: type, data: {x: [1,2,3]}},
{type: type, data: {x: [1,2,3]}}
{type: type, data: {x: [1, 2, 3]}},
{type: type, data: {x: [1, 2, 3]}}
);

@@ -192,3 +191,3 @@ test(

{},
{op: [{p: [], od: {a:1, x: 2}, oi: {x: 3}}]},
{op: [{p: [], od: {a: 1, x: 2}, oi: {x: 3}}]},
{op: [{p: [], od: {}, oi: {}}]}

@@ -198,3 +197,3 @@ );

{x: true},
{op: [{p: [], od: {a:1, x: 2}, oi: {x: 3}}]},
{op: [{p: [], od: {a: 1, x: 2}, oi: {x: 3}}]},
{op: [{p: [], od: {x: 2}, oi: {x: 3}}]}

@@ -204,9 +203,9 @@ );

{x: true},
{op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]},
{op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]},
{op: [{p: [], od: {x: 2}, oi: {}}]}
);
test(
{x: true, a:true, z:true},
{op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]},
{op: [{p: [], od: {a:1, x: 2}, oi: {z:3}}]}
{x: true, a: true, z: true},
{op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]},
{op: [{p: [], od: {a: 1, x: 2}, oi: {z: 3}}]}
);

@@ -221,3 +220,3 @@ test(

{x: true},
{op: [{p: [], od: {a:2, x: 5}, oi: []}]},
{op: [{p: [], od: {a: 2, x: 5}, oi: []}]},
{op: [{p: [], od: {x: 5}, oi: null}]}

@@ -255,3 +254,3 @@ );

projections.projectOp({}, {create: {type: 'other', data: 123}});
}).throwException();
}).throw(Error);
});

@@ -370,3 +369,3 @@

);
expect(projections.isOpAllowed(null, {}, {del:true})).equal(true);
expect(projections.isOpAllowed(null, {}, {del: true})).equal(true);
});

@@ -373,0 +372,0 @@

var MemoryPubSub = require('../lib/pubsub/memory');
var PubSub = require('../lib/pubsub');
var expect = require('expect.js');
var expect = require('chai').expect;

@@ -16,3 +16,3 @@ require('./pubsub')(function(callback) {

pubsub.subscribe('x', function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
expect(err.code).to.equal(5015);

@@ -26,3 +26,3 @@ done();

pubsub.on('error', function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
expect(err.code).to.equal(5015);

@@ -42,3 +42,3 @@ done();

pubsub.on('error', function(err) {
expect(err).to.be.an(Error);
expect(err).instanceOf(Error);
expect(err.code).to.equal(5016);

@@ -55,3 +55,3 @@ done();

pubsub.publish(['x', 'y'], {test: true}, function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
expect(err.code).to.equal(5017);

@@ -65,3 +65,3 @@ done();

pubsub.on('error', function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
expect(err.code).to.equal(5017);

@@ -76,3 +76,3 @@ done();

pubsub.on('error', function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
expect(err.message).equal('test error');

@@ -79,0 +79,0 @@ done();

@@ -1,2 +0,2 @@

var expect = require('expect.js');
var expect = require('chai').expect;

@@ -41,3 +41,3 @@ module.exports = function(create) {

var pubsub = this.pubsub;
pubsub.subscribe('x', function(err, stream) {
pubsub.subscribe('x', function(err) {
if (err) done(err);

@@ -50,6 +50,6 @@ pubsub.publish(['x'], {test: true}, done);

var pubsub = this.pubsub;
pubsub.subscribe('y', function(err, stream) {
pubsub.subscribe('y', function(err) {
if (err) done(err);
pubsub.subscribe('y', function(err, stream) {
if (err) done(err);
var emitted;
stream.on('data', function(data) {

@@ -83,3 +83,3 @@ expect(data).eql({test: true});

this.pubsub.on('error', function(err) {
expect(err).an(Error);
expect(err).instanceOf(Error);
expect(err.message).equal('test error');

@@ -86,0 +86,0 @@ done();

@@ -55,3 +55,3 @@

if (callbacks.length) {
args.push(function () {
args.push(function() {
var args = Array.from(arguments);

@@ -58,0 +58,0 @@ exports.callInSeries(callbacks, args);

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc