frontend/observable.js (80 lines of code) (raw):

const { OBJECT_ID, CONFLICTS } = require('./constants') /** * Allows an application to register a callback when a particular object in * a document changes. * * NOTE: This API is experimental and may change without warning in minor releases. */ class Observable { constructor() { this.observers = {} // map from objectId to array of observers for that object } /** * Called by an Automerge document when `patch` is applied. `before` is the * state of the document before the patch, and `after` is the state after * applying it. `local` is true if the update is a result of locally calling * `Automerge.change()`, and false otherwise. `changes` is an array of * changes that were applied to the document (as Uint8Arrays). */ patchCallback(patch, before, after, local, changes) { this._objectUpdate(patch.diffs, before, after, local, changes) } /** * Recursively walks a patch and calls the callbacks for all objects that * appear in the patch. */ _objectUpdate(diff, before, after, local, changes) { if (!diff.objectId) return if (this.observers[diff.objectId]) { for (let callback of this.observers[diff.objectId]) { callback(diff, before, after, local, changes) } } if (diff.type === 'map' && diff.props) { for (const propName of Object.keys(diff.props)) { for (const opId of Object.keys(diff.props[propName])) { this._objectUpdate(diff.props[propName][opId], before && before[CONFLICTS] && before[CONFLICTS][propName] && before[CONFLICTS][propName][opId], after && after[CONFLICTS] && after[CONFLICTS][propName] && after[CONFLICTS][propName][opId], local, changes) } } } else if (diff.type === 'table' && diff.props) { for (const rowId of Object.keys(diff.props)) { for (const opId of Object.keys(diff.props[rowId])) { this._objectUpdate(diff.props[rowId][opId], before && before.byId(rowId), after && after.byId(rowId), local, changes) } } } else if (diff.type === 'list' && diff.edits) { let offset = 0 for (const edit of diff.edits) { if (edit.action === 'insert') { offset -= 1 this._objectUpdate(edit.value, undefined, after && after[CONFLICTS] && after[CONFLICTS][edit.index] && after[CONFLICTS][edit.index][edit.elemId], local, changes) } else if (edit.action === 'multi-insert') { offset -= edit.values.length } else if (edit.action === 'update') { this._objectUpdate(edit.value, before && before[CONFLICTS] && before[CONFLICTS][edit.index + offset] && before[CONFLICTS][edit.index + offset][edit.opId], after && after[CONFLICTS] && after[CONFLICTS][edit.index] && after[CONFLICTS][edit.index][edit.opId], local, changes) } else if (edit.action === 'remove') { offset += edit.count } } } else if (diff.type === 'text' && diff.edits) { let offset = 0 for (const edit of diff.edits) { if (edit.action === 'insert') { offset -= 1 this._objectUpdate(edit.value, undefined, after && after.get(edit.index), local, changes) } else if (edit.action === 'multi-insert') { offset -= edit.values.length } else if (edit.action === 'update') { this._objectUpdate(edit.value, before && before.get(edit.index + offset), after && after.get(edit.index), local, changes) } else if (edit.action === 'remove') { offset += edit.count } } } } /** * Call this to register a callback that will get called whenever a particular * object in a document changes. The callback is passed five arguments: the * part of the patch describing the update to that object, the old state of * the object, the new state of the object, a boolean that is true if the * change is the result of calling `Automerge.change()` locally, and the array * of binary changes applied to the document. */ observe(object, callback) { const objectId = object[OBJECT_ID] if (!objectId) throw new TypeError('The observed object must be part of an Automerge document') if (!this.observers[objectId]) this.observers[objectId] = [] this.observers[objectId].push(callback) } } module.exports = { Observable }