Skip to content

Commit

Permalink
✨ Allow to preserve previous snapshot version that will be available …
Browse files Browse the repository at this point in the history
…in commit middleware

Occasionally, it is necessary to reference the previous version of a snapshot to execute auxiliary operations within the commit middleware, for example:

Let's imagine we have this doc that describes user:
```typescript
{
  id: '123',
  emailConfirmed: false;
}
```
Now we would want to send notification to the admin whenever `emailConfirmed` changed from `false` to `true`

At the moment there is no simple way to check in the middlewares if something is changing, apart form trying to apply the ops manually in the `apply` middleware. Something like

```typescript
backend.use('apply', function(request, next) {
  const changedSnapshot = applyOps(clone(request.snapshot), request.op);
  if(
    request.snapshot.data.emailConfirmed === false &&
    request.snapshot.data.emailConfirmed === true
  ) {
    notifyAdmin();
  }
})
```

This approach has few drawbacks:
1. We need to apply ops twice once in this middleware and once in the sharedb itself.
2. It is quite difficult for app to make considerations and mimick the flow of `$fixOps`

Solution
---
Let's make it possible to opt-in for storing the pre-apply snapshot version in request by setting a `preservePreapplySnapshot` flag to `true` in any middleware that happens before apply. This will allow us to:
1. Have the old and new version of snapshot available in the `commit` middleware
2. We do not have much memory overhead as this is opt-in, so if user never needs it the snapshot would never be cloned.
3. We only need to apply ops once
4. The apply mechanism is using shareDb power house.
  • Loading branch information
Dawidpol committed Jun 26, 2024
1 parent 10ba551 commit 6490158
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 0 deletions.
7 changes: 7 additions & 0 deletions docs/middleware/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ copy:
`snapshot` -- [`Snapshot`]({{ site.baseurl }}{% link api/snapshot.md %})
> The snapshot
`preservePreapplySnapshot` -- boolean
> This flag can be set to true in any middleware action before `commit`. This will inform shareDb
to clone snapshot data and store it in the request object, so it is available in commit middleware as `preapplySnapshot`
`preapplySnapshot` -- [`Snapshot`]({{ site.baseurl }}{% link api/snapshot.md %})
> The snapshot captures the state prior to the application of operations. It is accessible exclusively during the `commit` action, and only when the `preservePreapplySnapshot` flag is set to `true`.
`extra` -- Object
> `extra.source` -- Object
>> The submitted source when [`doc.submitSource`]({{ site.baseurl }}{% link api/doc.md %}http://localhost:4000/api/doc#submitsource--boolean) is set to `true`
Expand Down
12 changes: 12 additions & 0 deletions lib/submit-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ var ot = require('./ot');
var projections = require('./projections');
var ShareDBError = require('./error');
var types = require('./types');
var util = require('./util');

var ERROR_CODE = ShareDBError.CODES;

Expand Down Expand Up @@ -42,6 +43,8 @@ function SubmitRequest(backend, agent, index, id, op, options) {
this.ops = [];
this.channels = null;
this._fixupOps = [];
this.preservePreapplySnapshot = false;
this.preapplySnapshot = null;
}
module.exports = SubmitRequest;

Expand Down Expand Up @@ -180,6 +183,15 @@ SubmitRequest.prototype.apply = function(callback) {
this.backend.trigger(this.backend.MIDDLEWARE_ACTIONS.apply, this.agent, this, function(err) {
if (err) return callback(err);

/**
* Middleware is able to opt in to store old snapshot
* which could be useful in cases where we want to compare the old snapshot
* (snapshot without the ops applied) and the new snapshot (with the ops applied)
*/
if (request.preservePreapplySnapshot) {
request.preapplySnapshot = util.clone(request.snapshot);
}

// Apply the submitted op to the snapshot
err = ot.apply(request.snapshot, request.op);
if (err) return callback(err);
Expand Down
165 changes: 165 additions & 0 deletions test/middleware.js
Original file line number Diff line number Diff line change
Expand Up @@ -773,4 +773,169 @@ describe('middleware', function() {
});
});
});

describe('preservePreapplySnapshot', function() {
var connection;
var backend;
var doc;

beforeEach(function() {
backend = this.backend;
connection = backend.connect();
doc = connection.get('dogs', 'fido');
});

describe('preservePreapplySnapshot = true', function() {
beforeEach(function() {
backend.use('apply', function(request, next) {
request.preservePreapplySnapshot = true;
next();
});
});

describe('create', function() {
it('has version equal to 0 and data undefined', function(done) {
backend.use('commit', function(request, next) {
expect(request.preapplySnapshot).to.not.equal(request.snapshot);

expect(request.preapplySnapshot.v).to.equal(0);
expect(request.preapplySnapshot.type).to.be.null;
expect(request.preapplySnapshot.data).to.be.undefined;

expect(request.snapshot.v).to.equal(1);
expect(request.snapshot.type).to.be.ok;
expect(request.snapshot.data).to.be.deep.equal({name: 'fido'});

next();
});

doc.create({name: 'fido'}, done);
});
});

describe('op', function() {
it('has version and data before the apply', function(done) {
backend.use('commit', function(request, next) {
if (request.op.create) return next();
expect(request.preapplySnapshot).to.not.equal(request.snapshot);

expect(request.preapplySnapshot.v).to.equal(1);
expect(request.preapplySnapshot.type).to.be.ok;
expect(request.preapplySnapshot.data).to.be.deep.equal({name: 'fido'});

expect(request.snapshot.v).to.equal(2);
expect(request.preapplySnapshot.type).to.be.ok;
expect(request.snapshot.data).to.be.deep.equal({name: 'bfooar'});

next();
});

doc.create({name: 'fido'}, function(error) {
if (error) return done(error);

doc.submitOp([
{
p: ['name'],
oi: 'bar'
},
{
p: ['name', 1],
si: 'foo'
}
], done);
});
});

it('has version and data before the apply with $fixOps set', function(done) {
backend.use('apply', function(request, next) {
if (request.op.create) return next();
request.$fixup([{p: ['tricks', 1], li: 'stay'}]);
next();
});

backend.use('commit', function(request, next) {
if (request.op.create) return next();

expect(request.preapplySnapshot).to.not.equal(request.snapshot);

expect(request.preapplySnapshot.v).to.equal(1);
expect(request.preapplySnapshot.type).to.be.ok;
expect(request.preapplySnapshot.data).to.be.deep.equal({name: 'fido'});

expect(request.snapshot.v).to.equal(2);
expect(request.preapplySnapshot.type).to.be.ok;
expect(request.snapshot.data).to.be.deep.equal({
name: 'fido',
tricks: ['fetch', 'stay']
});

next();
});

doc.create({name: 'fido'}, function(error) {
if (error) return done(error);

doc.submitOp([{p: ['tricks'], oi: ['fetch']}], done);
});
});
});

describe('del', function() {
it('has version and data before the apply', function(done) {
backend.use('commit', function(request, next) {
if (request.op.create) return next();
expect(request.preapplySnapshot).to.not.equal(request.snapshot);

expect(request.preapplySnapshot.v).to.equal(1);
expect(request.preapplySnapshot.type).to.be.ok;
expect(request.preapplySnapshot.data).to.be.deep.equal({name: 'fido'});

expect(request.snapshot.v).to.equal(2);
expect(request.snapshot.type).to.be.null;
expect(request.snapshot.data).to.be.undefined;

next();
});

doc.create({name: 'fido'}, function(error) {
if (error) return done(error);

doc.del(done);
});
});
});
});

describe('preservePreapplySnapshot = false (default)', function() {
it('does\'t store the preapplySnapshot', function(done) {
backend.use('commit', function(request, next) {
expect(request.preapplySnapshot).to.be.null;
next();
});

doc.create({name: 'fido'}, function(error) {
if (error) return done(error);

process.nextTick(function() {
doc.submitOp([
{
p: ['name'],
oi: 'bar'
},
{
p: ['name', 1],
si: 'foo'
}
], function(error) {
if (error) return done(error);

process.nextTick(function() {
doc.del(done);
});
});
});
});
});
});
});
});

0 comments on commit 6490158

Please sign in to comment.