Op submission
When an op is submitted, it will pass through a number of middleware hooks as it is processed, giving you opportunities to interact with both the op and the snapshot as they are manipulated by ShareDB’s backend.
Lifecycle summary
- Submit – an op has been received by the server
- Apply – an op is about to be applied to the snapshot
- Commit – an op and its updated snapshot are about to be committed to the database
- After write – an op and its updated snapshot have successfully been committed to the database
- Submit request end – an op submission has finished (this is an event, not a middleware hook)
Lifecycle
Submit
The 'submit'
hook is triggered when the op has been received by the server.
This is the earliest point in the op’s server-side lifecycle, and probably the point at which you may want to perform actions such as authenticating, validation, sanitization, etc.
backend.use('submit', (context, next) => {
// agent.custom is usually set in the 'connection' hook
const userId = context.agent.custom.userId
const id = context.id
if (!userCanChangeDoc(userId, id)) {
return next(new Error('Unauthorized'))
}
next()
})
The snapshot has not yet been fetched. If you want to make any changes or assertions involving the snapshot, that should be done in the apply or commit hooks.
Apply
The apply
hook is triggered when the snapshot has been fetched, and the op is about to be applied.
During the 'apply'
hook, the snapshot is in its “old” state – the op has not yet been applied to it.
This point in the lifecycle is the earliest you can make checks against the snapshot itself (e.g. checking against snapshot metadata).
backend.use('apply', (context, next) => {
// agent.custom is usually set in the 'connection' hook
const userId = context.agent.custom.userId
const ownerId = context.snapshot.m.ownerId
if (userId !== ownerId) {
return next(new Error('Unauthorized'))
}
next()
})
Commit
The commit
hook is triggered after the op has been applied to the snapshot in memory, and both the op and snapshot are about to be written to the database.
During the 'commit'
hook, the snapshot is in its “new” state – the op has been applied to it.
This point in the lifecycle is the point at which you can make checks against the updated snapshot (e.g. validating your updated snapshot). This is your last opportunity to prevent the op from being committed.
This is a good place to update snapshot metadata, as this is the final snapshot that will be written to the database.
backend.use('commit', (context, next) => {
const userId = context.agent.custom.userId
context.op.m.userId = userId
context.snapshot.m.lastEditBy = userId
next()
})
After write
The afterWrite
hook is triggered after the op and updated snapshot have been successfully written to the database. This is the earliest that you know the op and snapshot are canonical.
This may be a sensible place to trigger analytics, or react to finalized changes to the snapshot. For example, if you’re keeping a cache of documents, this would be the time to update the cache.
backend.use('afterWrite', (context, next) => {
cache.set(context.collection, context.id, context.snapshot)
next()
})
Submit request end
The submit request end is an event, not a middleware hook, but is mentioned here for completeness.
This event is triggered at the end of an op’s life, regardless of success or failure. This is particularly useful for cleaning up any state that was set up earlier in the middleware and needs tearing down when an op is at the end of its life.
For example, consider a simple counter, that tracks how many requests are in progress:
backend.use('submit', (context, next) => {
requestsInProgress++
next()
})
A naive approach would simply decrement this in the 'afterWrite'
hook:
// This is a BAD approach
backend.use('afterWrite', (context, next) => {
requestsInProgress--
next()
})
However, this approach will miss any op submissions that result in errors, and hence will never correctly reset to zero. The correct approach is to use the 'submitRequestEnd'
event:
// This is a GOOD approach
// Note use of .on() instead of .use()
backend.on('submitRequestEnd', () => {
requestsInProgress--
})
Since 'submitRequestEnd'
is an event – not a middleware hook – it provides no callback, and no way to return an error to the client. It is purely informational.
Mutating ops
Ops may be amended in the apply
middleware using the special request.$fixup()
method:
backend.use('apply', (request, next) => {
let error;
try {
request.$fixup([{p: ['meta'], oi: {timestamp: Date.now()}}]);
} catch (e) {
error = e;
}
next(error);
});
The request.$fixup()
method may throw an error, which should be handled appropriately, usually by passing directly to the next()
callback.
Comparing old snapshot version with new version
Frequently, it becomes necessary to verify the changes made. This can be accomplished by leveraging two hooks, apply
and commit
, and creating a snapshot clone within the apply
hook.
// Alternatively use your favourite deep-clone library
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
backend.use('apply', (request, next) => {
request.snapshotBeforeApply = deepClone(request.snapshot);
next(error);
});
backend.use('commit', (request, next) => {
// Snapshot without ops and $fixupOps applied is now available as request.snapshotBeforeApply
// Snapshot with ops and $fixupOps applied is still available as request.snapshot
console.log(request.snapshotBeforeApply)
next(error);
});