Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/op metadata support #586

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
Expand Down
18 changes: 11 additions & 7 deletions lib/client/doc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -637,24 +635,24 @@ 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;
}

// 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
// op. For locally submitted ops, this will be synchronously with
// 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;
}

Expand Down Expand Up @@ -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") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand why we're doing this? The custom object is meant to be a generic data blob for clients to use. Setting a magical property here is surprising and may clash with clients' existing usage.

Also this.connection is only ever available in setups where the client is running on the same machine as the server (as opposed to, say, a client connected remotely over websocket).

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);
Expand Down
3 changes: 2 additions & 1 deletion lib/submit-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed above, we shouldn't be using agent.custom.


// 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;
Expand Down
76 changes: 76 additions & 0 deletions test/client/submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
});
});
};