UNPKG

sharedb

Version:
265 lines (231 loc) 9.45 kB
// This contains the master OT functions for the database. They look like // ot-types style operational transform functions, but they're a bit different. // These functions understand versions and can deal with out of bound create & // delete operations. var types = require('./types'); var ShareDBError = require('./error'); var util = require('./util'); var ERROR_CODE = ShareDBError.CODES; // Returns an error string on failure. Rockin' it C style. exports.checkOp = function(op) { if (op == null || typeof op !== 'object') { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Op must be an object'); } if (op.create != null) { if (typeof op.create !== 'object') { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Create data must be an object'); } var typeName = op.create.type; if (typeof typeName !== 'string') { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Missing create type'); } var type = types.map[typeName]; if (type == null || typeof type !== 'object') { return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); } } else if (op.del != null) { if (op.del !== true) return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'del value must be true'); } else if (!('op' in op)) { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Missing op, create, or del'); } if (op.src != null && typeof op.src !== 'string') { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'src must be a string'); } if (op.seq != null && typeof op.seq !== 'number') { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'seq must be a number'); } if ( (op.src == null && op.seq != null) || (op.src != null && op.seq == null) ) { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'Both src and seq must be set together'); } if (op.m != null && typeof op.m !== 'object') { return new ShareDBError(ERROR_CODE.ERR_OT_OP_BADLY_FORMED, 'op.m must be an object or null'); } }; // Takes in a string (type name or URI) and returns the normalized name (uri) exports.normalizeType = function(typeName) { return types.map[typeName] && types.map[typeName].uri; }; // This is the super apply function that takes in snapshot data (including the // type) and edits it in-place. Returns an error or null for success. exports.apply = function(snapshot, op) { if (typeof snapshot !== 'object') { return new ShareDBError(ERROR_CODE.ERR_APPLY_SNAPSHOT_NOT_PROVIDED, 'Missing snapshot'); } if (snapshot.v != null && op.v != null && snapshot.v !== op.v) { return new ShareDBError(ERROR_CODE.ERR_APPLY_OP_VERSION_DOES_NOT_MATCH_SNAPSHOT, 'Version mismatch'); } // Create operation if (op.create) { if (snapshot.type) return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document already exists'); // The document doesn't exist, although it might have once existed var create = op.create; var type = types.map[create.type]; if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); try { snapshot.data = type.create(create.data); snapshot.type = type.uri; snapshot.v++; } catch (err) { return err; } // Delete operation } else if (op.del) { snapshot.data = undefined; snapshot.type = null; snapshot.v++; // Edit operation } else if ('op' in op) { var err = applyOpEdit(snapshot, op.op); if (err) return err; snapshot.v++; // No-op, and we don't have to do anything } else { snapshot.v++; } }; function applyOpEdit(snapshot, edit) { if (!snapshot.type) return new ShareDBError(ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Document does not exist'); if (edit === undefined) return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_PROVIDED, 'Missing op'); var type = types.map[snapshot.type]; if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); if (type.name === 'json0' && Array.isArray(edit)) { for (var i = 0; i < edit.length; i++) { var opComponent = edit[i]; if (Array.isArray(opComponent.p)) { for (var j = 0; j < opComponent.p.length; j++) { var pathSegment = opComponent.p[j]; if (util.isDangerousProperty(pathSegment)) { return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_APPLIED, 'Invalid path segment'); } } } } } try { snapshot.data = type.apply(snapshot.data, edit); } catch (err) { return new ShareDBError(ERROR_CODE.ERR_OT_OP_NOT_APPLIED, err.message); } } exports.transform = function(type, op, appliedOp) { // There are 16 cases this function needs to deal with - which are all the // combinations of create/delete/op/noop from both op and appliedOp if (op.v != null && op.v !== appliedOp.v) { return new ShareDBError(ERROR_CODE.ERR_OP_VERSION_MISMATCH_DURING_TRANSFORM, 'Version mismatch'); } if (appliedOp.del) { if (op.create || 'op' in op) { return new ShareDBError(ERROR_CODE.ERR_DOC_WAS_DELETED, 'Document was deleted'); } } else if ( (appliedOp.create && ('op' in op || op.create || op.del)) || ('op' in appliedOp && op.create) ) { // If appliedOp.create is not true, appliedOp contains an op - which // also means the document exists remotely. return new ShareDBError(ERROR_CODE.ERR_DOC_ALREADY_CREATED, 'Document was created remotely'); } else if ('op' in appliedOp && 'op' in op) { // If we reach here, they both have a .op property. if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_DOES_NOT_EXIST, 'Document does not exist'); if (typeof type === 'string') { type = types.map[type]; if (!type) return new ShareDBError(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, 'Unknown type'); } try { op.op = type.transform(op.op, appliedOp.op, 'left'); } catch (err) { return err; } } if (op.v != null) op.v++; }; /** * Apply an array of ops to the provided snapshot. * * @param snapshot - a Snapshot object which will be mutated by the provided ops * @param ops - an array of ops to apply to the snapshot * @param options - options (currently for internal use only) * @return an error object if applicable */ exports.applyOps = function(snapshot, ops, options) { options = options || {}; for (var index = 0; index < ops.length; index++) { var op = ops[index]; if (options._normalizeLegacyJson0Ops) { try { normalizeLegacyJson0Ops(snapshot, op); } catch (error) { return new ShareDBError( ERROR_CODE.ERR_OT_LEGACY_JSON0_OP_CANNOT_BE_NORMALIZED, 'Cannot normalize legacy json0 op' ); } } snapshot.v = op.v; var error = exports.apply(snapshot, op); if (error) return error; } }; exports.transformPresence = function(presence, op, isOwnOp) { var opError = this.checkOp(op); if (opError) return opError; var type = presence.t; if (typeof type === 'string') { type = types.map[type]; } if (!type) return {code: ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED, message: 'Unknown type'}; if (!util.supportsPresence(type)) { return {code: ERROR_CODE.ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE, message: 'Type does not support presence'}; } if (op.create || op.del) { presence.p = null; presence.v++; return; } try { presence.p = presence.p === null ? null : type.transformPresence(presence.p, op.op, isOwnOp); } catch (error) { return {code: ERROR_CODE.ERR_PRESENCE_TRANSFORM_FAILED, message: error.message || error}; } presence.v++; }; /** * json0 had a breaking change in https://github.com/ottypes/json0/pull/40 * The change added stricter type checking, which breaks fetchSnapshot() * when trying to rebuild a snapshot from old, committed ops that didn't * have this stricter validation. This method fixes up legacy ops to * pass the stricter validation */ function normalizeLegacyJson0Ops(snapshot, json0Op) { if (snapshot.type !== types.defaultType.uri) return; var components = json0Op.op; if (!components) return; var data = snapshot.data; // type.apply() makes no guarantees about mutating the original data, so // we need to clone. However, we only need to apply() if we have multiple // components, so avoid cloning if we don't have to. if (components.length > 1) data = util.clone(data); for (var i = 0; i < components.length; i++) { var component = components[i]; if (typeof component.lm === 'string') component.lm = +component.lm; var path = component.p; var element = data; for (var j = 0; j < path.length; j++) { var key = path[j]; // https://github.com/ottypes/json0/blob/73db17e86adc5d801951d1a69453b01382e66c7d/lib/json0.js#L21 if (Object.prototype.toString.call(element) == '[object Array]') path[j] = +key; // https://github.com/ottypes/json0/blob/73db17e86adc5d801951d1a69453b01382e66c7d/lib/json0.js#L32 else if (element.constructor === Object) path[j] = key.toString(); element = element[key]; } // Apply to update the snapshot, so we can correctly check the path for // the next component. We don't need to do this on the final iteration, // since there's no more ops. if (i < components.length - 1) data = types.defaultType.apply(data, [component]); } }