diff --git a/lib/agent.js b/lib/agent.js index 2ad4d383d..eadd7e27d 100644 --- a/lib/agent.js +++ b/lib/agent.js @@ -261,6 +261,7 @@ Agent.prototype._sendOp = function(collection, id, op) { if ('op' in op) message.op = op.op; if (op.create) message.create = op.create; if (op.del) message.del = true; + if (op.m) message.m = op.m; this.send(message); }; diff --git a/lib/client/doc.js b/lib/client/doc.js index cf4f08cc9..bf893a24e 100644 --- a/lib/client/doc.js +++ b/lib/client/doc.js @@ -600,9 +600,7 @@ Doc.prototype._otApply = function(op, source) { ); } - // NB: If we need to add another argument to this event, we should consider - // the fact that the 'op' event has op.src as its 3rd argument - this.emit('before op batch', op.op, source); + this.emit('before op batch', op.op, source, op.src, {op: op}); // Iteratively apply multi-component remote operations and rollback ops // (source === false) for the default JSON0 OT type. It could use @@ -637,7 +635,7 @@ Doc.prototype._otApply = function(op, source) { this._setData(this.type.apply(this.data, componentOp.op)); this.emit('op', componentOp.op, source, op.src); } - this.emit('op batch', op.op, source); + this.emit('op batch', op.op, source, op.src, {op: op}); // Pop whatever was submitted since we started applying this op this._popApplyStack(stackLength); return; @@ -645,7 +643,7 @@ Doc.prototype._otApply = function(op, source) { // The 'before op' event enables clients to pull any necessary data out of // the snapshot before it gets changed - this.emit('before op', op.op, source, op.src); + this.emit('before op', op.op, source, op.src, {op: op}); // Apply the operation to the local data, mutating it in place this._setData(this.type.apply(this.data, op.op)); // Emit an 'op' event once the local data includes the changes from the @@ -653,8 +651,8 @@ Doc.prototype._otApply = function(op, source) { // submission and before the server or other clients have received the op. // For ops from other clients, this will be after the op has been // committed to the database and published - this.emit('op', op.op, source, op.src); - this.emit('op batch', op.op, source); + this.emit('op', op.op, source, op.src, {op: op}); + this.emit('op batch', op.op, source, op.src, {op: op}); return; } @@ -866,6 +864,12 @@ Doc.prototype.submitOp = function(component, options, callback) { callback = options; options = null; } + + // If agent has metadata, append on client level so that both client and backend have access to it + if(this.connection.agent && this.connection.agent.custom && typeof this.connection.agent.custom.metadata === "object") { + component.m = Object.assign({}, component.m, this.connection.agent.custom.metadata); + } + var op = {op: component}; var source = options && options.source; this._submit(op, source, callback); diff --git a/lib/submit-request.js b/lib/submit-request.js index 04517513b..7aa3450c2 100644 --- a/lib/submit-request.js +++ b/lib/submit-request.js @@ -171,7 +171,8 @@ SubmitRequest.prototype.commit = function(callback) { var op = request.op; op.c = request.collection; op.d = request.id; - op.m = undefined; + op.m = request.agent.custom.metadata ? request.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; diff --git a/test/client/submit.js b/test/client/submit.js index 15d9e3f49..62eaad760 100644 --- a/test/client/submit.js +++ b/test/client/submit.js @@ -1210,5 +1210,81 @@ module.exports = function() { }); }); }); + + describe('op metadata', function() { + + it('metadata disabled', async function() { + let resolveTest = null; + let rejectTest = null; + const testPromise = new Promise((resolve, reject) => { resolveTest = resolve; rejectTest = reject; }); + + this.backend.use('afterWrite', function(request, next) { + expect(request.op.m).to.be.undefined; + next(); + doneAfter(); + }); + + const docs = []; + for(let i = 0; i < 2; i++) { + const doc = this.backend.connect().get('dogs', 'fido').on('error', rejectTest); + docs.push(doc); + } + + let left = docs.size; + const doneAfter = () => { if(!left--) { resolveTest(); } }; + + await new Promise((resolve, reject) => docs[0].create({ age: 1 }, err => err?reject(err):resolve())).catch(err => rejectTest(err)); + + for(let i = 0; i < docs.length; i++) { + const doc = docs[i]; + await new Promise((resolve, reject) => doc.subscribe(err => err?reject(err):resolve())).catch(err => rejectTest(err)); + } + + const doc = docs[0]; + doc.submitOp({ p: ['age'], na: 1, m: { clientMeta: 'client0' } }); + + return testPromise; + }); + + it('metadata enabled', async function() { + let resolveTest = null; + let rejectTest = null; + const testPromise = new Promise((resolve, reject) => { resolveTest = resolve; rejectTest = reject; }); + + this.backend.use('connect', function(request, next) { + expect(request.req.metadata).to.be.ok; + Object.assign(request.agent.custom, request.req); + next(); + }); + + this.backend.use('afterWrite', function(request, next) { + expect(request.op.m).to.be.ok; + next(); + doneAfter(); + }); + + const docs = []; + for(let i = 0; i < 2; i++) { + const connOptions = { metadata: { agentMeta: 'agent'+i } }; + const doc = this.backend.connect(undefined, connOptions).get('dogs', 'fido').on('error', rejectTest); + docs.push(doc); + } + + let left = docs.size; + const doneAfter = () => { if(!left--) { resolveTest(); } }; + + await new Promise((resolve, reject) => docs[0].create({ age: 1 }, err => err?reject(err):resolve())).catch(err => rejectTest(err)); + + for(let i = 0; i < docs.length; i++) { + const doc = docs[i]; + await new Promise((resolve, reject) => doc.subscribe(err => err?reject(err):resolve())).catch(err => rejectTest(err)); + } + + const doc = docs[0]; + doc.submitOp({ p: ['age'], na: 1, m: { clientMeta: 'client0' } }); + + return testPromise; + }); + }); }); };