UNPKG

graph-crdt

Version:
607 lines (479 loc) 16.4 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); var _regenerator = require('babel-runtime/regenerator'); var _regenerator2 = _interopRequireDefault(_regenerator); var _iterator5 = require('babel-runtime/core-js/symbol/iterator'); var _iterator6 = _interopRequireDefault(_iterator5); var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray'); var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2); var _slicedToArray2 = require('babel-runtime/helpers/slicedToArray'); var _slicedToArray3 = _interopRequireDefault(_slicedToArray2); var _getIterator2 = require('babel-runtime/core-js/get-iterator'); var _getIterator3 = _interopRequireDefault(_getIterator2); var _extends2 = require('babel-runtime/helpers/extends'); var _extends3 = _interopRequireDefault(_extends2); var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of'); var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf); var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck'); var _classCallCheck3 = _interopRequireDefault(_classCallCheck2); var _possibleConstructorReturn2 = require('babel-runtime/helpers/possibleConstructorReturn'); var _possibleConstructorReturn3 = _interopRequireDefault(_possibleConstructorReturn2); var _createClass2 = require('babel-runtime/helpers/createClass'); var _createClass3 = _interopRequireDefault(_createClass2); var _inherits2 = require('babel-runtime/helpers/inherits'); var _inherits3 = _interopRequireDefault(_inherits2); var _symbol = require('babel-runtime/core-js/symbol'); var _symbol2 = _interopRequireDefault(_symbol); var _eventemitter = require('eventemitter3'); var _eventemitter2 = _interopRequireDefault(_eventemitter); var _uuid = require('uuid'); var _union = require('../union'); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var node = (0, _symbol2.default)('source object'); /** * Increments each state given an object of key/value pairs. * @private * @param {Node} current - Current state. * @param {Object} update - Key-value update pairs. * @return {Node} - Incremented update. */ /** * @module graph-crdt.Node */ var getIncrementedState = function getIncrementedState(current, update) { var result = current.new(); for (var field in update) { if (update.hasOwnProperty(field)) { var state = current.state(field) + 1; var value = update[field]; result[node][field] = { value: value, state: state }; } } return result; }; /** * An observable object with conflict-resolution. * * @class Node * @param {Object} [config] - Instance level configuration. * @param {Object} [config.uid] - Override the randomly generated * node uid. */ var Node = function (_Emitter) { (0, _inherits3.default)(Node, _Emitter); (0, _createClass3.default)(Node, null, [{ key: 'create', /** * Creates a new Node instance without using * `new`. * * @param {Object} [config] - The constructor configuration object. * @returns {Node} - The new node instance. */ value: function create(config) { return new Node(config); } /** * Turns a normal object into a Node instance. * Properties to be imported must be enumerable * and cannot be inherited via prototype. * * @param {Object} object - Any enumerable object. * @returns {Node} - A node interface constructed from the object. */ }, { key: 'from', value: function from(object) { var instance = Node.create(); var state = 1; for (var key in object) { if (object.hasOwnProperty(key)) { var value = object[key]; instance[node][key] = { value: value, state: state }; } } return instance; } /** * Take an object and use it as the data source for a new * node. This method is used with properly formatted * objects, such as stringified, then parsed node instances. * * @param {Object} object - The preformatted object. * @returns {Node} - A new node instance. * * @example * const original = Node.create() * original.merge({ data: 'intact' }) * const serialized = JSON.stringify(original) * const parsed = JSON.parse(serialized) * * const node = Node.source(parsed) * node.value('data') // 'intact' */ }, { key: 'source', value: function source(object) { var instance = Node.create(); instance[node] = object; return instance; } }]); function Node() { var config = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; (0, _classCallCheck3.default)(this, Node); var _this = (0, _possibleConstructorReturn3.default)(this, (Node.__proto__ || (0, _getPrototypeOf2.default)(Node)).call(this)); var uid = config.uid || (0, _uuid.v4)(); _this[node] = { '@object': { uid: uid } }; return _this; } /** * Read metadata, either on a field, or * on the entire object. * * @param {String} [field] - The property to read metadata from. * @returns {Object|null} - If metadata is found, it returns the object. * Otherwise `null` is given. */ (0, _createClass3.default)(Node, [{ key: 'meta', value: function meta(field) { if (field === undefined) { /** Returns the object metadata if no field is specified. */ return this[node]['@object']; } /** Returns the field given, and null if metadata isn't found. */ return this[node][field] || null; } /** * Show the value of a node's property. * * @param {String} field - The name of the property * to retrieve. * @returns {Mixed} - The value of the property, * or undefined if it doesn't exist. Cannot be called * on reserved fields (like "@object"). */ }, { key: 'value', value: function value(field) { if (field === '@object') { return undefined; } /** Gets the field metadata. */ var subject = this.meta(field); /** * If the field exists, it returns the corresponding * value, otherwise it returns undefined. */ return subject ? subject.value : undefined; } /** * Get the current state of a property. * * @param {String} field - Property name. * @returns {Number} - Current lamport state (or 0). */ }, { key: 'state', value: function state(field) { /** Get the field metadata. */ var meta = this.meta(field); return meta ? meta.state : 0; } /** * Sets metadata for a field, bumping the current state. * @param {String} field - A field to update. * @param {Object} metadata - What metadata the field should contain. * @return {Object} - The metadata object (not the same one given). */ }, { key: 'setMetadata', value: function setMetadata(field, metadata) { var state = this.state(field) + 1; var update = this.new(); update[node][field] = (0, _extends3.default)({}, metadata, { state: state }); return this.merge(update); } /** * Takes the changes from the current node and plays them after the * changes in the target node. * Similar to git rebase, but without the conflicts. * @param {Node} target - Preceding state. * @return {Node} - A new, rebased node. */ }, { key: 'rebase', value: function rebase(target) { var rebased = this.new(); rebased.merge(target); rebased.merge(this); // Bump state for older fields. var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = (0, _getIterator3.default)(this), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { var _step$value = (0, _slicedToArray3.default)(_step.value, 1), key = _step$value[0]; if (target.state(key) >= this.state(key)) { // Avoids mutation of metadata. rebased[node][key] = (0, _extends3.default)({}, this.meta(key), { state: target.state(key) + 1 }); } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } return rebased; } /** * Calculates the intersection between two nodes. * @param {Node} target - Any other node. * @return {Node} - A collection of fields common to both nodes. */ }, { key: 'overlap', value: function overlap(target) { var shared = this.new(); var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = (0, _getIterator3.default)(this), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _step2$value = (0, _slicedToArray3.default)(_step2.value, 1), field = _step2$value[0]; if (this.state(field) && target.state(field)) { shared[node][field] = this.meta(field); } } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } return shared; } /** * Merges an update into the current node. * * @param {Node} incoming - The node to merge from. * If a plain object is passed, it will be upgraded to a node * using `Node.from`. * @returns {Object} - A collection of changes caused by the merge. */ }, { key: 'merge', value: function merge(incoming) { if (!(incoming instanceof Node)) { incoming = getIncrementedState(this, incoming); } /** Track all mutations. */ var changes = { history: this.new(), update: this.new() }; var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = (0, _getIterator3.default)(incoming), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { var _step3$value = (0, _slicedToArray3.default)(_step3.value, 1), field = _step3$value[0]; var forceUpdate = false; var current = this.meta(field); var update = incoming.meta(field); var state = { incoming: incoming.state(field), current: this.state(field) }; /** Handle conflicts. */ if (state.current === state.incoming) { var winner = (0, _union.conflict)(current, update); /** No further action needed. */ if (winner === current) { continue; } /** Replace the current value */ this.emit('conflict', update, current); forceUpdate = true; } if (state.current < state.incoming || forceUpdate) { /** Track overwritten values. */ if (current) { changes.history[node][field] = current; } changes.update[node][field] = update; /** Immediately apply updates. */ this[node][field] = update; } else { changes.history[node][field] = update; } } /** Only emit when there's a change. */ } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } var changed = [].concat((0, _toConsumableArray3.default)(changes.update)).length > 0; var overwritten = [].concat((0, _toConsumableArray3.default)(changes.history)).length > 0; if (overwritten) { this.emit('history', changes.history); } if (changed) { this.emit('update', changes.update); } return changes; } /** * Creates an empty instance with the same configuration. * @return {Node} - A new node instance with the same properties. */ }, { key: 'new', value: function _new() { var _meta = this.meta(), uid = _meta.uid; var clone = new Node({ uid: uid }); return clone; } /** * Takes a snapshot of the current state. * @return {Object} - Every key and value (currently) in the node. */ }, { key: 'snapshot', value: function snapshot() { var object = {}; var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = (0, _getIterator3.default)(this), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var _step4$value = (0, _slicedToArray3.default)(_step4.value, 2), key = _step4$value[0], value = _step4$value[1]; object[key] = value; } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } return object; } /** * Iterates over the node keys & values, ignoring metadata. * @return {Array} - Each value yielded is a [key, value] pair. */ }, { key: _iterator6.default, value: _regenerator2.default.mark(function value() { var object, meta, key, value; return _regenerator2.default.wrap(function value$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: object = this[node]; meta = '@object'; /** Iterate over the source object. */ _context.t0 = _regenerator2.default.keys(object); case 3: if ((_context.t1 = _context.t0()).done) { _context.next = 11; break; } key = _context.t1.value; if (!(object.hasOwnProperty(key) && key !== meta)) { _context.next = 9; break; } value = this.value(key); _context.next = 9; return [key, value]; case 9: _context.next = 3; break; case 11: case 'end': return _context.stop(); } } }, value, this); }) /* Coercion interfaces */ /** * Returns the node's uid. Not meant for end developers, * instead used by JavaScript for type coercion. * * @private * @returns {String} - The node's unique ID. */ }, { key: 'toString', value: function toString() { var _meta2 = this.meta(), uid = _meta2.uid; return uid; } /** * Returns the actual object, called when JSON needs something * to stringify. Obviously dangerous as it exposes the object * to untracked mutation. Please don't use it. * * @private * @returns {Object} - The actual node object. */ }, { key: 'toJSON', value: function toJSON() { return this[node]; } }]); return Node; }(_eventemitter2.default); exports.default = Node;