backend/backend.js (93 lines of code) (raw):

const { encodeChange } = require('./columnar') const { BackendDoc } = require('./new') const { backendState } = require('./util') /** * Returns an empty node state. */ function init() { return {state: new BackendDoc(), heads: []} } function clone(backend) { return {state: backendState(backend).clone(), heads: backend.heads} } function free(backend) { backend.state = null backend.frozen = true } /** * Applies a list of `changes` from remote nodes to the node state `backend`. * Returns a two-element array `[state, patch]` where `state` is the updated * node state, and `patch` describes the modifications that need to be made * to the document objects to reflect these changes. */ function applyChanges(backend, changes) { const state = backendState(backend) const patch = state.applyChanges(changes) backend.frozen = true return [{state, heads: state.heads}, patch] } function hashByActor(state, actorId, index) { if (state.hashesByActor[actorId] && state.hashesByActor[actorId][index]) { return state.hashesByActor[actorId][index] } if (!state.haveHashGraph) { state.computeHashGraph() if (state.hashesByActor[actorId] && state.hashesByActor[actorId][index]) { return state.hashesByActor[actorId][index] } } throw new RangeError(`Unknown change: actorId = ${actorId}, seq = ${index + 1}`) } /** * Takes a single change request `request` made by the local user, and applies * it to the node state `backend`. Returns a three-element array `[backend, patch, binaryChange]` * where `backend` is the updated node state,`patch` confirms the * modifications to the document objects, and `binaryChange` is a binary-encoded form of * the change submitted. */ function applyLocalChange(backend, change) { const state = backendState(backend) if (change.seq <= state.clock[change.actor] || 0) { throw new RangeError('Change request has already been applied') } // Add the local actor's last change hash to deps. We do this because when frontend // and backend are on separate threads, the frontend may fire off several local // changes in sequence before getting a response from the backend; since the binary // encoding and hashing is done by the backend, the frontend does not know the hash // of its own last change in this case. Rather than handle this situation as a // special case, we say that the frontend includes only specifies other actors' // deps in changes it generates, and the dependency from the local actor's last // change is always added here in the backend. // // Strictly speaking, we should check whether the local actor's last change is // indirectly reachable through a different actor's change; in that case, it is not // necessary to add this dependency. However, it doesn't do any harm either (only // using a few extra bytes of storage). if (change.seq > 1) { const lastHash = hashByActor(state, change.actor, change.seq - 2) if (!lastHash) { throw new RangeError(`Cannot find hash of localChange before seq=${change.seq}`) } let deps = {[lastHash]: true} for (let hash of change.deps) deps[hash] = true change.deps = Object.keys(deps).sort() } const binaryChange = encodeChange(change) const patch = state.applyChanges([binaryChange], true) backend.frozen = true // On the patch we send out, omit the last local change hash const lastHash = hashByActor(state, change.actor, change.seq - 1) patch.deps = patch.deps.filter(head => head !== lastHash) return [{state, heads: state.heads}, patch, binaryChange] } /** * Returns the state of the document serialised to an Uint8Array. */ function save(backend) { return backendState(backend).save() } /** * Loads the document and/or changes contained in an Uint8Array, and returns a * backend initialised with this state. */ function load(data) { const state = new BackendDoc(data) return {state, heads: state.heads} } /** * Applies a list of `changes` to the node state `backend`, and returns the updated * state with those changes incorporated. Unlike `applyChanges()`, this function * does not produce a patch describing the incremental modifications, making it * a little faster when loading a document from disk. When all the changes have * been loaded, you can use `getPatch()` to construct the latest document state. */ function loadChanges(backend, changes) { const state = backendState(backend) state.applyChanges(changes) backend.frozen = true return {state, heads: state.heads} } /** * Returns a patch that, when applied to an empty document, constructs the * document tree in the state described by the node state `backend`. */ function getPatch(backend) { return backendState(backend).getPatch() } /** * Returns an array of hashes of the current "head" changes (i.e. those changes * that no other change depends on). */ function getHeads(backend) { return backend.heads } /** * Returns the full history of changes that have been applied to a document. */ function getAllChanges(backend) { return getChanges(backend, []) } /** * Returns all changes that are newer than or concurrent to the changes * identified by the hashes in `haveDeps`. If `haveDeps` is an empty array, all * changes are returned. Throws an exception if any of the given hashes is unknown. */ function getChanges(backend, haveDeps) { if (!Array.isArray(haveDeps)) { throw new TypeError('Pass an array of hashes to Backend.getChanges()') } return backendState(backend).getChanges(haveDeps) } /** * Returns all changes that are present in `backend2` but not in `backend1`. * Intended for use in situations where the two backends are for different actors. * To get the changes added between an older and a newer document state of the same * actor, use `getChanges()` instead. `getChangesAdded()` throws an exception if * one of the backend states is frozen (i.e. if it is not the latest state of that * backend instance; this distinction matters when the backend is mutable). */ function getChangesAdded(backend1, backend2) { return backendState(backend2).getChangesAdded(backendState(backend1)) } /** * If the backend has applied a change with the given `hash` (given as a * hexadecimal string), returns that change (as a byte array). Returns undefined * if no change with that hash has been applied. A change with missing * dependencies does not count as having been applied. */ function getChangeByHash(backend, hash) { return backendState(backend).getChangeByHash(hash) } /** * Returns the hashes of any missing dependencies, i.e. where we have applied a * change that has a dependency on a change we have not seen. * * If the argument `heads` is given (an array of hexadecimal strings representing * hashes as returned by `getHeads()`), this function also ensures that all of * those hashes resolve to either a change that has been applied to the document, * or that has been enqueued for later application once missing dependencies have * arrived. Any missing heads hashes are included in the returned array. */ function getMissingDeps(backend, heads = []) { return backendState(backend).getMissingDeps(heads) } module.exports = { init, clone, free, applyChanges, applyLocalChange, save, load, loadChanges, getPatch, getHeads, getAllChanges, getChanges, getChangesAdded, getChangeByHash, getMissingDeps }