stateshot
Version:
💾 Non-aggressive history state management with structure sharing.
359 lines (296 loc) • 11.4 kB
JavaScript
'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