Op submission

  1. Lifecycle summary
  2. Lifecycle
    1. Submit
    2. Apply
    3. Commit
    4. After write
    5. Submit request end
  3. Mutating ops

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.