recoder-code
Version:
Complete AI-powered development platform with ML model training, plugin registry, real-time collaboration, monitoring, infrastructure automation, and enterprise deployment capabilities
1,033 lines (901 loc) • 33.8 kB
JavaScript
var Backend = require('../../../lib/backend');
var expect = require('chai').expect;
var async = require('async');
var types = require('../../../lib/types');
var presenceTestType = require('./presence-test-type');
var errorHandler = require('../../util').errorHandler;
var PresencePauser = require('./presence-pauser');
types.register(presenceTestType.type);
describe('DocPresence', function() {
var backend;
var connection1;
var connection2;
var doc1;
var doc2;
var presence1;
var presence2;
var presencePauser;
beforeEach(function(done) {
backend = new Backend({presence: true});
connection1 = backend.connect();
connection2 = backend.connect();
presencePauser = new PresencePauser();
backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(request, callback) {
presencePauser.sendPresence(request, callback);
});
doc1 = connection1.get('books', 'northern-lights');
doc2 = connection2.get('books', 'northern-lights');
async.series([
doc1.create.bind(doc1, 'North Lights', presenceTestType.type.name),
doc1.subscribe.bind(doc1),
doc2.subscribe.bind(doc2),
function(next) {
presence1 = connection1.getDocPresence('books', 'northern-lights');
presence2 = connection2.getDocPresence('books', 'northern-lights');
next();
}
], done);
});
afterEach(function(done) {
delete presenceTestType.type.invert;
connection1.close();
connection2.close();
backend.close(done);
});
it('subscribes to presence from another client', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 1}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1');
expect(presence).to.eql({index: 1});
next();
});
}
], done);
});
it('transforms existing remote presence when a new local op is applied', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 7}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(presence).to.eql({index: 7});
next();
});
},
function(next) {
presence2.once('receive', function(id, presence) {
expect(doc2.data).to.eql('Northern Lights');
expect(presence).to.eql({index: 10});
expect(presence2.remotePresences).to.eql({
'presence-1': {index: 10}
});
next();
});
doc2.submitOp({index: 5, value: 'ern'});
}
], done);
});
it('transforms existing local presence when a new local op is applied', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
localPresence1.submit.bind(localPresence1, {index: 7}),
doc1.submitOp.bind(doc1, {index: 5, value: 'ern'}),
function(next) {
expect(localPresence1.value).to.eql({index: 10});
next();
}
], done);
});
it('progresses another client\'s presence when they send an op at their index', function(done) {
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
function(next) {
localPresence2.submit({index: 5}, errorHandler(done));
presence1.once('receive', function() {
next();
});
},
function(next) {
doc2.submitOp({index: 5, value: 'ern'}, errorHandler(done));
presence1.once('receive', function(id, presence) {
expect(presence).to.eql({index: 8});
next();
});
}
], done);
});
it('does not progress another client\'s index when inserting a local op at their index', function(done) {
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
function(next) {
localPresence2.submit({index: 5}, errorHandler(done));
presence1.once('receive', function() {
next();
});
},
function(next) {
presence1.once('receive', function(id, presence) {
expect(presence).to.eql({index: 5});
next();
});
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
}
], done);
});
it('waits for pending ops before submitting presence', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
doc1.submitOp({index: 12, value: ': His Dark Materials'}, errorHandler(done));
localPresence1.submit({index: 20}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(presence).to.eql({index: 20});
expect(doc2.version).to.eql(2);
next();
});
}
], done);
});
it('queues two updates immediately after one another', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 4}, errorHandler(done));
localPresence1.submit({index: 5}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(presence).to.eql({index: 4});
presence2.once('receive', function(id, presence) {
expect(presence).to.eql({index: 5});
next();
});
});
}
], done);
});
it('transforms pending presence by another op submitted before a flush', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
doc1.submitOp({index: 12, value: ': His Dark Materials'}, errorHandler(done));
localPresence1.submit({index: 20}, errorHandler(done));
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(doc2.version).to.eql(3);
expect(doc2.data).to.eql('Northern Lights: His Dark Materials');
expect(presence).to.eql({index: 23});
next();
});
}
], done);
});
it('updates the document when the presence version is ahead', function(done) {
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
doc1.unsubscribe.bind(doc1),
doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
function(next) {
expect(doc1.version).to.eql(1);
expect(doc2.version).to.eql(2);
localPresence2.submit({index: 12}, errorHandler(done));
presence1.once('receive', function(id, presence) {
expect(doc1.version).to.eql(2);
expect(presence).to.eql({index: 12});
next();
});
}
], done);
});
it('transforms old presence when its version is behind the latest doc', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
doc1.unsubscribe.bind(doc1),
doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
function(next) {
expect(doc1.version).to.eql(1);
expect(doc2.version).to.eql(2);
localPresence1.submit({index: 12}, errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(doc2.version).to.eql(2);
expect(presence).to.eql({index: 15});
next();
});
}
], done);
});
it('returns errors when failing to transform old presence to the latest doc', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
doc1.unsubscribe.bind(doc1),
doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
function(next) {
expect(doc1.version).to.eql(1);
expect(doc2.version).to.eql(2);
localPresence1.submit({badProp: 'foo'}, function(error) {
expect(error.code).to.equal('ERR_PRESENCE_TRANSFORM_FAILED');
next();
});
}
], done);
});
it('transforms old presence when it arrives later than a new op', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
presencePauser.pause();
presencePauser.onPause = function() {
next();
};
localPresence1.submit({index: 12}, errorHandler(done));
},
function(next) {
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
doc2.once('op', function() {
presencePauser.resume();
});
presence2.once('receive', function(id, presence) {
expect(doc2.version).to.eql(2);
expect(presence).to.eql({index: 15});
next();
});
}
], done);
});
// This test case attempts to force us into a tight race condition corner case:
// 1. doc1 sends presence, as well as submits an op
// 2. doc2 receives the op first, followed by the presence, which is now out-of-date
// 3. doc2 re-requests doc1's presence again
// 4. doc1 sends *another* op, which *again* beats the presence update (this could
// in theory happen many times in a row)
it('transforms old presence when new ops keep beating the presence responses', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
// Pause presence just before sending it back to the clients. It's already been
// transformed by the server to what the server knows as the latest version
presencePauser.pause();
presencePauser.onPause = function() {
next();
};
localPresence1.submit({index: 12}, errorHandler(done));
},
function(next) {
// Now we submit another op, while the presence is still paused. We wait until
// doc2 has received this op, so we know that when we finally receive our
// presence, it will be stale
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
doc2.once('op', function() {
next();
});
},
function(next) {
// At this point in the test, both docs are up-to-date on v2, but doc2 still
// hasn't received doc1's v1 presence
expect(doc1.version).to.eql(2);
expect(doc2.version).to.eql(2);
// Resume presence broadcasts so that doc2 receives v1's stale presence
presencePauser.resume();
// However, now immediately pause again. Set a conditional pause, which
// will allow doc2 to request presence from doc1, but will pause doc1's
// presence response, making it stale again
presencePauser.pause(function(request) {
return request.presence.id === 'presence-1';
});
presencePauser.onPause = function() {
presencePauser.onPause = null;
// When we capture doc1's response, doc1 also submits some ops, which
// will make its response stale again.
doc1.submitOp({index: 0, value: 'The'}, function(error) {
if (error) return done(error);
doc1.submitOp({index: 3, value: ' '}, errorHandler(done));
doc2.on('op', function() {
// This will get fired for v3 and then v4, so check for the later one
if (doc1.version === 4 && doc2.version === 4) {
// Only once doc2 has received the ops, should we resume our
// broadcasts, ensuring that the update is stale again.
presencePauser.resume();
// Despite the second reply being stale, we expect to have transformed it
// up to the current version.
presence2.once('receive', function(id, presence) {
expect(doc2.version).to.eql(4);
expect(presence).to.eql({index: 19});
next();
});
}
});
});
};
}
], done);
});
// This test is for a similar case to the above test case, but ensures that our
// op cache correctly handles deletion and creation ops
it('transforms old presence when a doc is deleted and then created', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 3}, errorHandler(done));
presence2.once('receive', function() {
next();
});
},
function(next) {
localPresence1.submit({index: 12}, errorHandler(done));
presencePauser.pause();
presencePauser.onPause = function() {
presencePauser.onPause = null;
next();
};
},
function(next) {
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
doc2.once('op', function() {
next();
});
},
function(next) {
expect(doc1.version).to.eql(2);
expect(doc2.version).to.eql(2);
presencePauser.resume();
presencePauser.pause(function(request) {
return request.presence.id === 'presence-1';
});
presencePauser.onPause = function() {
presencePauser.onPause = null;
async.series([
doc1.del.bind(doc1),
doc1.create.bind(doc1, 'Subtle Knife', presenceTestType.type.name),
doc1.submitOp.bind(doc1, {index: 0, value: 'The '})
], errorHandler(done));
};
doc2.on('op', function() {
if (doc2.version !== 5) return;
presencePauser.resume();
presence2.once('receive', function(id, presence) {
expect(doc2.version).to.eql(5);
expect(presence).to.be.null;
next();
});
});
}
], done);
});
it('transforms local presence when a doc is deleted and created locally', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
localPresence1.submit.bind(localPresence1, {index: 3}),
doc1.del.bind(doc1),
doc1.create.bind(doc1, 'Subtle Knife', presenceTestType.type.uri),
function(next) {
expect(localPresence1.value).to.be.null;
next();
}
], done);
});
it('transforms pending presence by a re-creation submitted before a flush', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 2}, errorHandler(done));
presence2.once('receive', function() {
next();
});
},
function(next) {
doc1.submitOp({index: 12, value: ': His Dark Materials'}, errorHandler(done));
localPresence1.submit({index: 20}, errorHandler(done));
doc1.del(errorHandler(done));
doc1.create('Subtle Knife', presenceTestType.type.uri, errorHandler(done));
presence2.on('receive', function(id, presence) {
if (doc2.version !== 4) return;
expect(doc2.data).to.eql('Subtle Knife');
expect(presence).to.be.null;
next();
});
}
], done);
});
it('ignores presence that arrives out of order', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
var hasPaused = false;
// Catch the first presence update, but then allow later ones
presencePauser.pause(function() {
if (hasPaused) return false;
hasPaused = true;
return true;
});
localPresence1.submit({index: 2}, next);
},
function(next) {
presence2.once('receive', function(id, presence) {
expect(presence).to.eql({index: 3});
presence2.once('receive', function() {
done(new Error('should not get another presence event'));
});
presencePauser.resume();
next();
});
localPresence1.submit({index: 3}, errorHandler(done));
}
], done);
});
it('ignores pending presence that arrives out of order', function(done) {
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
doc1.unsubscribe.bind(doc1),
doc2.submitOp.bind(doc2, {index: 5, value: 'ern'}),
function(next) {
var pauseCount = 0;
presencePauser.pause();
presencePauser.onPause = function() {
pauseCount++;
if (pauseCount === 2) {
expect(this._pendingBroadcasts[0][0].presence.p).to.eql({index: 2});
expect(this._pendingBroadcasts[1][0].presence.p).to.eql({index: 4});
expect(this._pendingBroadcasts[0][0].presence.pv)
.to.be.lessThan(this._pendingBroadcasts[1][0].presence.pv);
// Fire the broadcasts in the reverse order
this._pendingBroadcasts[1][1]();
this._pendingBroadcasts[0][1]();
}
};
presence1.once('receive', function(id, presence) {
expect(presence).to.eql({index: 4});
next();
});
localPresence2.submit({index: 2}, errorHandler(done));
localPresence2.submit({index: 4}, errorHandler(done));
}
], done);
});
it('rejects a presence message with a numeric collection', function(done) {
var localPresence1 = presence1.create('presence-1');
localPresence1.on('error', function(error) {
expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
done();
});
var message = localPresence1._message();
message.c = 1;
message.v = 1;
message.t = presenceTestType.type.uri;
connection1.send(message);
});
it('rejects a presence message with an invalid version', function(done) {
var localPresence1 = presence1.create('presence-1');
localPresence1.on('error', function(error) {
expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
done();
});
var message = localPresence1._message();
message.v = -1;
message.t = presenceTestType.type.uri;
connection1.send(message);
});
it('rejects a presence message without an ID', function(done) {
var localPresence1 = presence1.create('presence-1');
// Have to catch the error on the Presence instance, because obviously
// we won't find the LocalPresence without the ID
presence1.on('error', function(error) {
expect(error.code).to.eql('ERR_MESSAGE_BADLY_FORMED');
done();
});
var message = localPresence1._message();
message.id = null;
message.v = 1;
message.t = presenceTestType.type.uri;
connection1.send(message);
});
it('only sends presence responses for the associated doc', function(done) {
var localPresence1 = presence1.create('presence-1');
var localPresence2 = presence2.create('presence-2');
var otherDoc1 = connection1.get('books', 'subtle-knife');
var otherDoc2 = connection2.get('books', 'subtle-knife');
var otherPresence1 = connection1.getDocPresence('books', 'subtle-knife');
var otherPresence2 = connection2.getDocPresence('books', 'subtle-knife');
var localOtherPresence1 = otherPresence1.create('other-presence-1');
async.series([
otherDoc1.create.bind(otherDoc1, 'Subtle Knife', presenceTestType.type.uri),
otherDoc2.subscribe.bind(otherDoc2),
otherPresence2.subscribe.bind(otherPresence2),
function(next) {
localOtherPresence1.submit({index: 0}, errorHandler(done));
otherPresence2.once('receive', function() {
next();
});
},
localPresence1.submit.bind(localPresence1, {index: 3}),
function(next) {
localPresence2.submit({index: 5}, next);
otherPresence1.on('receive', function() {
done(new Error('Other document should not have had presence sent'));
});
otherPresence2.on('receive', function() {
done(new Error('Other document should not have had presence sent'));
});
}
], done);
});
it('sends the presence data once the connection can send', function(done) {
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
function(next) {
connection2._setState('disconnected');
localPresence2.submit({index: 1}, errorHandler(done));
doc2.whenNothingPending(function() {
// The connection tests whether we can send just before sending on
// nothing pending, so let's also wait to reset the connection.
connection2._setState('connecting');
connection2._setState('connected');
});
presence1.once('receive', function(id, presence) {
expect(presence).to.eql({index: 1});
next();
});
}
], done);
});
it('re-requests presence when reconnecting', function(done) {
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
connection1.once('closed', function() {
next();
});
connection1.close();
},
localPresence2.submit.bind(localPresence2, {index: 0}),
function(next) {
backend.connect(connection1);
presence1.once('receive', function(id, presence) {
expect(presence).to.eql({index: 0});
next();
});
}
], done);
});
it('un-transforms presence after a soft rollback', function(done) {
// Mock invert so that we can trigger a soft rollback instead of a hard rollback
presenceTestType.type.invert = function() {
return {index: 5, del: 3};
};
var localPresence1 = presence1.create('presence-1');
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
localPresence1.submit.bind(localPresence1, {index: 7}),
function(next) {
localPresence2.submit({index: 8}, errorHandler(done));
presence1.once('receive', function() {
next();
});
},
function(next) {
backend.use(backend.MIDDLEWARE_ACTIONS.apply, function(request, callback) {
callback({code: 'ERR_OP_SUBMIT_REJECTED'});
});
presence1.once('receive', function() {
expect(localPresence1.value).to.eql({index: 10});
expect(presence1.remotePresences).to.eql({
'presence-2': {index: 11}
});
presence1.once('receive', function() {
expect(localPresence1.value).to.eql({index: 7});
expect(presence1.remotePresences).to.eql({
'presence-2': {index: 8}
});
next();
});
});
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
}
], done);
});
it('performs a hard reset on presence when the doc is hard rolled back', function(done) {
var localPresence1 = presence1.create('presence-1');
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
localPresence1.submit.bind(localPresence1, {index: 7}),
function(next) {
localPresence2.submit({index: 8}, errorHandler(done));
presence1.once('receive', function() {
next();
});
},
function(next) {
backend.use(backend.MIDDLEWARE_ACTIONS.apply, function(request, callback) {
callback({code: 'ERR_OP_SUBMIT_REJECTED'});
});
presence1.once('receive', function() {
expect(localPresence1.value).to.eql({index: 10});
expect(presence1.remotePresences).to.eql({
'presence-2': {index: 11}
});
presence1.once('receive', function() {
expect(localPresence1.value).to.be.null;
expect(presence1.remotePresences).to.eql({});
next();
});
});
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
}
], done);
});
it('can receive presence before performing the first fetch on a document', function(done) {
var connection3 = backend.connect();
var doc3 = connection3.get('books', 'northern-lights');
var presence3 = connection3.getDocPresence('books', 'northern-lights');
var localPresence3 = presence3.create('presence-3');
async.series([
presence1.subscribe.bind(presence1),
doc3.fetch.bind(doc3),
function(next) {
localPresence3.submit({index: 1}, errorHandler(done));
presence1.once('receive', function(id, presence) {
expect(id).to.eql('presence-3');
expect(presence).to.eql({index: 1});
next();
});
}
], done);
});
it('errors when submitting presence on a document that has not been created', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
doc1.del.bind(doc1),
function(next) {
localPresence1.submit({index: 2}, function(error) {
expect(error.code).to.eql('ERR_DOC_DOES_NOT_EXIST');
next();
});
}
], done);
});
it('errors when trying to submit presence on a type that does not support it', function(done) {
var jsonDoc = connection1.get('books', 'snuff');
var jsonPresence = connection1.getDocPresence('books', 'snuff');
var localJsonPresence = jsonPresence.create('json-presence');
async.series([
jsonDoc.create.bind(jsonDoc, {title: 'Snuff'}, 'json0'),
function(next) {
localJsonPresence.submit({index: 1}, function(error) {
expect(error.code).to.eql('ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE');
next();
});
}
], done);
});
it('errors local presence when listening to ops on a type that does not support presence', function(done) {
var jsonDoc = connection1.get('books', 'emma');
var jsonPresence = connection1.getDocPresence('books', 'emma');
var localJsonPresence = jsonPresence.create('json-presence');
localJsonPresence.submit({index: 1}, function() {
// Swallow error, which is expected since presence is unsupported
});
async.series([
jsonDoc.create.bind(jsonDoc, {title: 'Emma'}, 'json0'),
function(next) {
localJsonPresence.once('error', function(error) {
expect(error.code).to.eql('ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE');
next();
});
jsonDoc.submitOp({p: ['author'], oi: 'Jane Austen'});
}
], done);
});
it('returns errors sent from the middleware', function(done) {
backend.use(backend.MIDDLEWARE_ACTIONS.sendPresence, function(request, callback) {
callback('some error');
});
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
function(next) {
localPresence2.submit({index: 0}, errorHandler(done));
presence1.once('error', function(error) {
expect(error.message).to.equal('some error');
next();
});
}
], done);
});
it('removes doc event listeners when destroying presence', function(done) {
var localPresence1 = presence1.create('presence-1');
var localPresence2 = presence2.create('presence-2');
async.series([
presence2.subscribe.bind(presence2),
localPresence2.submit.bind(localPresence2, {index: 2}),
function(next) {
localPresence1.submit({index: 1}, errorHandler(done));
presence2.once('receive', function() {
next();
});
},
presence2.destroy.bind(presence2),
function(next) {
expect(doc2._eventsCount).to.equal(0);
next();
}
], done);
});
it('destroys remote presence when it is updated with null', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 1}, errorHandler(done)),
presence2.once('receive', function() {
next();
});
},
function(next) {
localPresence1.submit(null, errorHandler(done)),
presence2.once('receive', function(id, presence) {
expect(presence).to.be.null;
expect(doc2._eventsCount).to.equal(0);
next();
});
}
], done);
});
it('waits for local pending ops before accepting remote presence', function(done) {
var localPresence2 = presence2.create('presence-2');
var triggerApply;
async.series([
presence1.subscribe.bind(presence1),
presence2.subscribe.bind(presence2),
function(next) {
backend.use(backend.MIDDLEWARE_ACTIONS.apply, function(request, callback) {
triggerApply = callback;
expect(doc1.inflightOp).to.be.ok;
expect(doc1.pendingOps).to.be.empty;
next();
});
doc1.submitOp({index: 5, value: 'ern'}, errorHandler(done));
},
localPresence2.submit.bind(localPresence2, {index: 10}),
function(next) {
triggerApply();
presence1.once('receive', function(id, presence) {
expect(presence).to.eql({index: 13});
next();
});
}
], done);
});
it('emits an error when trying to transform bad local presence against an op', function(done) {
var localPresence1 = presence1.create('presence-1');
localPresence1.submit({badProp: 'foo'}, function(error) {
expect(error).to.be.ok;
});
localPresence1.once('error', function() {
done();
});
doc1.submitOp({index: 5, value: 'ern'});
});
it('emits an error when trying to transform bad remote presence against an op', function(done) {
var localPresence2 = presence2.create('presence-2');
async.series([
presence1.subscribe.bind(presence1),
function(next) {
localPresence2.submit({badProp: 'foo'}, errorHandler(done));
presence1.once('receive', function(id, presence) {
expect(presence).to.eql({badProp: 'foo'});
next();
});
},
function(next) {
localPresence2.once('error', function() {
// Ignore the local error
});
presence1.once('error', function() {
next();
});
doc1.submitOp({index: 5, value: 'ern'});
}
], done);
});
it('sends null presence when the doc is destroyed', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
localPresence1.submit({index: 2}, errorHandler(done));
presence2.once('receive', function() {
next();
});
},
function(next) {
doc1.destroy(errorHandler(done));
presence2.once('receive', function(id, presence) {
expect(id).to.equal('presence-1');
expect(presence).to.be.null;
next();
});
}
], done);
});
it('does not error when destroying presence for a deleted doc', function(done) {
var localPresence1 = presence1.create('presence-1');
async.series([
doc1.del.bind(doc1),
localPresence1.destroy.bind(localPresence1),
function(next) {
expect(Object.keys(presence1.localPresences)).to.be.empty;
next();
}
], done);
});
it('does not transform presence submitted in an op event when the presence was created late', function(done) {
var localPresence1;
doc1.on('op', function() {
localPresence1.submit({index: 7});
});
localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
doc1.submitOp({index: 5, value: 'ern'});
presence2.on('receive', function(id, value) {
expect(value).to.eql({index: 7});
next();
});
}
], done);
});
it('does transform late-created presence submitted in an op event by a deep second op', function(done) {
var localPresence1;
var submitted = false;
doc1.on('op', function() {
if (submitted) return;
submitted = true;
localPresence1.submit({index: 7});
doc1.submitOp({index: 5, value: 'akg'});
});
localPresence1 = presence1.create('presence-1');
async.series([
presence2.subscribe.bind(presence2),
function(next) {
doc1.submitOp({index: 5, value: 'ern'});
presence2.on('receive', function(id, value) {
expect(value).to.eql({index: 10});
next();
});
}
], done);
});
it('does not trigger EventEmitter memory leak warnings', function() {
for (var i = 0; i < 100; i++) {
presence1.create();
}
expect(doc1._events.op.warned).not.to.be.ok;
var emitter = connection1._docPresenceEmitter._emitters[doc1.collection][doc1.id];
expect(emitter._events.op.warned).not.to.be.ok;
});
});