UNPKG

stateshot

Version:

💾 Non-aggressive history state management with structure sharing.

359 lines (296 loc) • 11.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); // See https://github.com/garycourt/murmurhash-js var murmurHash2 = function murmurHash2(str) { var seed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; var l = str.length; var h = seed ^ l; var i = 0; var k = void 0; while (l >= 4) { k = str.charCodeAt(i) & 0xff | (str.charCodeAt(++i) & 0xff) << 8 | (str.charCodeAt(++i) & 0xff) << 16 | (str.charCodeAt(++i) & 0xff) << 24; k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0x5bd1e995 & 0xffff) << 16); k ^= k >>> 24; k = (k & 0xffff) * 0x5bd1e995 + (((k >>> 16) * 0x5bd1e995 & 0xffff) << 16); h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16) ^ k; l -= 4; ++i; } /* eslint-disable no-fallthrough */ switch (l) { case 3: h ^= (str.charCodeAt(i + 2) & 0xff) << 16; case 2: h ^= (str.charCodeAt(i + 1) & 0xff) << 8; case 1: h ^= str.charCodeAt(i) & 0xff; h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16); } h ^= h >>> 13; h = (h & 0xffff) * 0x5bd1e995 + (((h >>> 16) * 0x5bd1e995 & 0xffff) << 16); h ^= h >>> 15; return h >>> 0; }; var hashFunc = murmurHash2; var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; function safeStringify(obj) { var indent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 2; var cache = new Set(); var retVal = JSON.stringify(obj, function (key, value) { return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null ? cache.has(value) ? undefined // Duplicate reference found, discard key : cache.add(value) && value // Store value in our collection : value; }, indent); cache = null; return retVal; } var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } var defaultRule = { // StateNode => { Chunks, Children } toRecord: function toRecord(node) { return { chunks: [_extends({}, node, { children: undefined })], children: node.children }; }, // { Chunks, Children } => StateNode fromRecord: function fromRecord(_ref) { var chunks = _ref.chunks, children = _ref.children; return _extends({}, chunks[0], { children: children }); } }; var state2Record = function state2Record(stateNode, chunkPool) { var rules = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; var prevRecord = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : null; var pickIndex = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : -1; var ruleIndex = rules.findIndex(function (_ref2) { var match = _ref2.match; return match(stateNode); }); var rule = rules[ruleIndex] || defaultRule; var _rule$toRecord = rule.toRecord(stateNode), chunks = _rule$toRecord.chunks, children = _rule$toRecord.children; var recordChildren = children; var hashes = []; for (var i = 0; i < chunks.length; i++) { var chunkStr = safeStringify(chunks[i], 0); var hashKey = hashFunc(chunkStr); hashes.push(hashKey); chunkPool[hashKey] = chunkStr; } if (pickIndex !== -1 && Array.isArray(prevRecord && prevRecord.children)) { var childrenCopy = [].concat(_toConsumableArray(prevRecord.children)); childrenCopy[pickIndex] = state2Record(recordChildren[pickIndex], chunkPool, rules); return { hashes: hashes, ruleIndex: ruleIndex, children: childrenCopy }; } else { return { hashes: hashes, ruleIndex: ruleIndex, children: children && children.map(function (node) { return state2Record(node, chunkPool, rules); }) }; } }; var record2State = function record2State(recordNode, chunkPool) { var rules = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; var hashes = recordNode.hashes, ruleIndex = recordNode.ruleIndex, children = recordNode.children; var chunks = hashes.map(function (hash) { return JSON.parse(chunkPool[hash]); }); var rule = rules[ruleIndex] || defaultRule; return rule.fromRecord({ chunks: chunks, children: children && children.map(function (node) { return record2State(node, chunkPool, rules); }) }); }; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var noop = function noop() {}; var History = function () { function History() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { initialState: undefined, rules: [], delay: 50, maxLength: 100, onChange: noop, useChunks: true }; _classCallCheck(this, History); this.rules = options.rules || []; this.delay = options.delay || 50; this.maxLength = options.maxLength || 100; this.useChunks = options.useChunks === undefined ? true : options.useChunks; this.onChange = noop; this.$index = -1; this.$records = []; this.$chunks = {}; this.$pending = { state: null, pickIndex: null, onResolves: [], timer: null }; this.$debounceTime = null; if (options.initialState !== undefined) { this.pushSync(options.initialState); } if (options.onChange) { this.onChange = options.onChange; } } // : boolean _createClass(History, [{ key: 'get', // void => State value: function get() { var currentRecord = this.$records[this.$index]; var resultState = void 0; if (!currentRecord) { resultState = null; } else if (!this.useChunks) { resultState = currentRecord; } else { resultState = record2State(currentRecord, this.$chunks, this.rules); } this.onChange(resultState); return resultState; } // (State, number?) => History }, { key: 'pushSync', value: function pushSync(state) { var _this = this; var pickIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : -1; var latestRecord = this.$records[this.$index] || null; var record = this.useChunks ? state2Record(state, this.$chunks, this.rules, latestRecord, pickIndex) : state; this.$index++; this.$records[this.$index] = record; // Clear redo records. for (var i = this.$index + 1; i < this.$records.length; i++) { this.$records[i] = null; } // Clear first valid record if max length reached. if (this.$index >= this.maxLength) { this.$records[this.$index - this.maxLength] = null; } // Clear pending state. if (this.$pending.timer) { clearTimeout(this.$pending.timer); this.$pending.state = null; this.$pending.pickIndex = null; this.$pending.timer = null; this.$debounceTime = null; this.$pending.onResolves.forEach(function (resolve) { return resolve(_this); }); this.$pending.onResolves = []; } this.onChange(state); return this; } // (State, number?) => Promise<History> }, { key: 'push', value: function push(state) { var _this2 = this; var pickIndex = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : -1; var currentTime = +new Date(); var setupPending = function setupPending() { _this2.$pending.state = state; _this2.$pending.pickIndex = pickIndex; _this2.$debounceTime = currentTime; var promise = new Promise(function (resolve, reject) { _this2.$pending.onResolves.push(resolve); _this2.$pending.timer = setTimeout(function () { _this2.pushSync(_this2.$pending.state, _this2.$pending.pickIndex); }, _this2.delay); }); return promise; }; // First time called. if (this.$pending.timer === null) { return setupPending(); } else if (currentTime - this.$debounceTime < this.delay) { // Has been called without resolved. clearTimeout(this.$pending.timer); this.$pending.timer = null; return setupPending(); } else return Promise.reject(new Error('Invalid push ops')); } // void => History }, { key: 'undo', value: function undo() { if (this.hasUndo) this.$index--; return this; } // void => History }, { key: 'redo', value: function redo() { if (this.hasRedo) this.$index++; return this; } // void => History }, { key: 'reset', value: function reset() { var _this3 = this; this.$index = -1; this.$records.forEach(function (tree) { tree = null; }); Object.keys(this.$chunks).forEach(function (key) { _this3.$chunks[key] = null; }); this.$records = []; this.$chunks = {}; clearTimeout(this.$pending.timer); this.$pending = { state: null, pickIndex: null, onResolves: [], timer: null }; this.$debounceTime = null; return this; } }, { key: 'hasRedo', get: function get() { // No redo when pointing to last record. if (this.$index === this.$records.length - 1) return false; // Only has redo if there're valid records after index. // There can be no redo even if index less than records' length, // when we undo multi records then push a new one. var hasRecordAfterIndex = false; for (var i = this.$index + 1; i < this.$records.length; i++) { if (this.$records[i] !== null) hasRecordAfterIndex = true; } return hasRecordAfterIndex; } // : boolean }, { key: 'hasUndo', get: function get() { // Only has undo if we have records before index. var lowerBound = Math.max(this.$records.length - this.maxLength, 0); return this.$index > lowerBound; } // : number }, { key: 'length', get: function get() { return Math.min(this.$records.length, this.maxLength); } }]); return History; }(); /** * Stateshot.js * (c) 2018 Yifeng Wang */ exports.History = History; //# sourceMappingURL=stateshot.js.map