UNPKG

@konfirm/decoy

Version:

Proxy objects, keeping track of mutations to commit/rollback

1,276 lines (1,237 loc) 37.8 kB
'use strict'; var crypto = require('crypto'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; /** * Create a type check based on a list of valid values * * @template T * @param {unknown} name * @param {() => Array<unknown>} valid * @returns {(value: unknown) => T} */ function check(name, ...values) { return (value) => { if (values.includes(value)) { return value; } throw new Error(`Unknown ${name}: "${value}"`); }; } // Algorithm type check const asAlgorithm = check('algorithm', ...crypto.getHashes()); // Digest type check const asDigest = check('digest', 'hex', 'base64'); // Type specific Hash/Hmac updaters const types$2 = { array: (h, values) => update(h, ...values), object: (h, value) => keys$1(value) .reduce((carry, key) => update(update(carry, key), value[key]), h), null: (h) => update(h, 'NULL'), boolean: (h, value) => h.update(value ? 'true' : 'false'), }; /** * Get all object keys from its descriptors * * @param {object} target * @returns {Array<string>} */ function descriptors(target) { const { constructor: { prototype } } = target; const descriptors = Object.getOwnPropertyDescriptors(prototype); return Object.keys(descriptors); } /** * Find all possible keys on an object which may contain state * * @param {object} target * @returns {Array<string>} */ function keys$1(target) { return [...new Set(descriptors(target).concat(Object.keys(target)))] .filter((key) => key !== '__proto__' && typeof target[key] !== 'function') .sort((one, two) => -Number(one < two) || Number(one > two)); } /** * Obtain the updater for the type of value * * @param {unknown} value * @returns {Updater} */ function getTypeUpdater(value) { const type = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value; return type in types$2 ? types$2[type] : (h, value) => h.update(String(value)); } /** * Update the Hmac/Hash using the method appropriate for the types given * * @param {Updatable} h * @param {...Array<any>} args * @returns {Updatable} */ function update(h, ...args) { return args.reduce((carry, value) => { const update = getTypeUpdater(value); return update(carry, value); }, h); } /** * Calculate the Hash checksum for the given value using the provided algorithm * and digest method * * @export * @param {unknown} value * @param {string} [algorithm='sha256'] * @param {string} [digest='hex'] * @returns {string} */ function hash(value, algorithm = 'sha256', digest = 'hex') { return update(crypto.createHash(asAlgorithm(algorithm)), value).digest(asDigest(digest)); } let TypeMapper$1 = class TypeMapper { constructor(mapping = {}) { this.mapping = Object.keys(mapping) .map((key) => (value) => mapping[key](value) ? key : undefined) .concat((value) => typeof value); } map(value) { return this.mapping.reduce((carry, map) => carry || map(value), undefined); } }; let ValueMapper$1 = class ValueMapper { constructor(types = new TypeMapper$1(), mapping = {}) { this.types = types; this.mapping = mapping; } map(value) { const type = this.types.map(value); const map = (value) => this.map(value); return type in this.mapping ? this.mapping[type](value, map) : (value === null || value === void 0 ? void 0 : value.toString()) || 'undefined'; } }; const types$1 = new TypeMapper$1({ date: (value) => value instanceof Date, regexp: (value) => value instanceof RegExp, array: (value) => Array.isArray(value), null: (value) => value === null, }); const stringifier = new ValueMapper$1(types$1, { string: (value) => value ? `"${value}"` : 'EmptyString', date: (value, map) => map(value.toISOString()), object: (value, map) => { const string = String(value); if (/^\[([a-z]+) \1\]$/i.test(string)) { const mapped = Object.keys(value) .map((key) => `${key}:${map(value[key])}`); return `{${mapped.join(',')}}`; } return string; }, array: (value, map) => `[${value.map(map).join(',')}]`, null: () => 'NULL', undefined: () => 'Undefined', function: (value) => { const { constructor: { name: cname }, name } = value; const [, async, generator, func] = cname.match(/^(async)?(generator)?(function)$/i); const [, stype, sname] = value.toString().match(/^(?:async)?(?:[\s\*]+)?(class|function)?(?:[\s\*]+)?([a-z_]\w*)?\s*[\(\{]?/i); const parts = [] .concat(stype && !(name || sname) ? 'anonymous' : []) .concat(name && !stype && sname ? 'shorthand' : []) .concat(async || [], generator || []) .concat(!(stype || sname) ? 'arrow' : []) .concat(stype || func) .map((key) => key[0].toUpperCase() + key.slice(1)); return [parts.join('')].concat(name || []).join(' '); }, }); const stringify = (value) => stringifier.map(value); class AssertionError extends Error { constructor(message, value, trigger) { super(message); this.value = value; this.trigger = trigger; Object.setPrototypeOf(this, new.target.prototype); } get reasons() { const { message, value, trigger } = this; return ((trigger === null || trigger === void 0 ? void 0 : trigger.reasons) || []).concat(`${stringify(value)} ${message}`); } get cause() { const { message, value, trigger } = this; return [{ value, message }].concat((trigger === null || trigger === void 0 ? void 0 : trigger.cause) || []); } get reason() { return String(this); } toString(separator = '\u200b\u2190\u200b') { return this.reasons .reverse() .join(separator); } } /** * Create a guard verifying the given value matches the specified type */ function is$1(type) { return (value) => typeof value === type; } /** * Create a guard verifying the given value matches any of the validators */ function any$1(...checks) { return (value) => checks.some((check) => check(value)); } /** * Create a guard verifying the given value matches all of the validators */ function all$1(...checks) { return (value) => checks.every((check) => check(value)); } /** * Create a guard verifying the given value matches none of the validators */ function not$1(...checks) { return (value) => checks.every((check) => !check(value)); } /** * Guard verifying the value is a array */ const isArray$1 = (value) => Array.isArray(value); /** * Guard verifying the value is a function */ const isFunction$1 = is$1('function'); /** * Guard verifying the value is a null */ const isNULL$1 = (value) => value === null; /** * Guard verifying the value is a object */ const isObject$1 = all$1(not$1(isArray$1), not$1(isNULL$1), is$1('object')); /** * Guard verifying the value is a string */ const isString = is$1('string'); /** * Guard verifying the value is a symbol */ const isSymbol = is$1('symbol'); /** * Guard verifying the value is a undefined */ const isUndefined$1 = is$1('undefined'); /** * Create an assertion guard, throwing an AssertionError with the provided message if any validator failes */ function assertion(message, ...rules) { const valid = all$1(...rules); // create an extend of AssertionError which is a unique value for each assertion // this allows for easy recognition whether the AssertionError was thrown from this // or other assertions class InnerAssertionError extends AssertionError { } return (value) => { try { // validity may throw an error and end up in the catch block if (!valid(value)) { // if not valid (and not thrown) throw an InnerAssertionError throw new InnerAssertionError(message, value); } } catch (error) { // the caught error is passed on as origin to a new AssertionError only // if it was thrown from the valid checker function const rest = error instanceof InnerAssertionError || !(error instanceof AssertionError) ? [] : [error]; throw new AssertionError(message, value, ...rest); } return true; }; } /** * Creates a guard verifying the value is an object and has the given key */ function isKey$1(key) { return all$1(isObject$1, (value) => key in value); } /** * Creates a guard verifying the value is an object and has the given key matching the validators */ function isKeyOfType$1(key, ...validators) { return all$1(isKey$1(key), (value) => validators.every((check) => check(value[key]))); } /** * Creates a guard verifying the value is an object and doesn't have the key or has the given key matching the validators */ function isOptionalKeyOfType$1(key, ...validators) { return any$1(all$1(isObject$1, not$1(isKey$1(key))), isKeyOfType$1(key, ...validators)); } /** * Creates a guard verifying the value is an object matching at least the structure */ function isStructure$1(struct, ...options) { const optional = options.reduce((carry, key) => carry.concat(key), []); const validators = Object.keys(struct) .map((key) => (optional.includes(key) ? isOptionalKeyOfType$1 : isKeyOfType$1)(key, struct[key])); return all$1(isObject$1, ...validators); } /** * Creates a guard verifying the value is an object matching exactly the structure */ function isStrictStructure$1(struct, ...options) { const keys = Object.keys(struct); return all$1( // do the keys present match the given structure conditions isStructure$1(struct, ...options), // every key should be part of the structure (value) => Object.keys(value).every((key) => keys.includes(key))); } const isMutationOptions = isStructure$1({ target: any$1(isObject$1, isFunction$1, isArray$1), key: any$1(isString, isSymbol), value: () => true, }, 'value'); const assertMutationOptions = assertion('Invalid MutationOptions', isMutationOptions); const store = new WeakMap(); /** * Abstract Mutation * * @export * @abstract * @class AbstractMutation * @implements {MutationInterface<T>} * @template T */ class AbstractMutation { /** * Creates an instance of AbstractMutation * * @param {T} options * @memberof AbstractMutation */ constructor(options) { assertMutationOptions(options); store.set(this, options); } /** * the name * * @readonly * @type {string} * @memberof AbstractMutation */ get name() { const { constructor: { name } } = Object.getPrototypeOf(this); return name.replace(/([^A-Z]+)([A-Z]+)/g, "$1-$2").toLowerCase(); } /** * the target * * @readonly * @type {T['target']} * @memberof AbstractMutation */ get target() { const { target } = store.get(this); return target; } /** * the key * * @readonly * @type {T['key']} * @memberof AbstractMutation */ get key() { const { key } = store.get(this); return key; } /** * the value * * @readonly * @type {T['value']} * @memberof AbstractMutation */ get value() { const { value } = store.get(this); return value; } /** * The (property) descriptor * * @readonly * @type {(PropertyDescriptor | undefined)} * @memberof AbstractMutation */ get descriptor() { return undefined; } /** * Apply the mutation on its target * * @memberof AbstractMutation */ apply() { throw new Error('Not implemented'); } /** * toString implementation * * @param {string} [template='{name}: {key} = {value}'] * @return {*} {string} * @memberof AbstractMutation */ toString(template = '{name}: {key} = {value}') { return template.replace(/\{([^\}]+)\}/g, (_, key) => this[key]); } /** * toJSON implementation * * @return {*} {{ [key: string]: unknown }} * @memberof AbstractMutation */ toJSON() { const { name, key, value } = this; return { name, key, value }; } } /** * Deletion Mutation * * @export * @class DeletionMutation * @extends {AbstractMutation<T>} * @template T */ class DeletionMutation extends AbstractMutation { /** * get name * * @readonly * @type {string} * @memberof DeletionMutation */ get name() { return 'deletion-mutation'; } /** * get value (always undefined) * * @readonly * @type {T['value']} * @memberof DeletionMutation */ get value() { return undefined; } /** * apply the property mutation to the target * * @memberof DeletionMutation */ apply() { const { target, key } = this; delete target[key]; } /** * return the string representation of the deletion mutation * * @param {string} [template='{name}: {key}'] * @return {*} {string} * @memberof DeletionMutation */ toString(template = '{name}: {key}') { return super.toString(template); } } class TypeMapper { constructor(mapping = {}) { this.mapping = Object.keys(mapping) .map((key) => (value) => mapping[key](value) ? key : undefined) .concat((value) => typeof value); } map(value) { return this.mapping.reduce((carry, map) => carry || map(value), undefined); } } class ValueMapper { constructor(types = new TypeMapper(), mapping = {}) { this.types = types; this.mapping = mapping; } map(value) { const type = this.types.map(value); const map = (value) => this.map(value); return type in this.mapping ? this.mapping[type](value, map) : (value === null || value === void 0 ? void 0 : value.toString()) || 'undefined'; } } const types = new TypeMapper({ date: (value) => value instanceof Date, regexp: (value) => value instanceof RegExp, array: (value) => Array.isArray(value), null: (value) => value === null, }); new ValueMapper(types, { string: (value) => value ? `"${value}"` : 'EmptyString', date: (value, map) => map(value.toISOString()), object: (value, map) => { const string = String(value); if (/^\[([a-z]+) \1\]$/i.test(string)) { const mapped = Object.keys(value) .map((key) => `${key}:${map(value[key])}`); return `{${mapped.join(',')}}`; } return string; }, array: (value, map) => `[${value.map(map).join(',')}]`, null: () => 'NULL', undefined: () => 'Undefined', function: (value) => { const { constructor: { name: cname }, name } = value; const [, async, generator, func] = cname.match(/^(async)?(generator)?(function)$/i); const [, stype, sname] = value.toString().match(/^(?:async)?(?:[\s\*]+)?(class|function)?(?:[\s\*]+)?([a-z_]\w*)?\s*[\(\{]?/i); const parts = [] .concat(stype && !(name || sname) ? 'anonymous' : []) .concat(name && !stype && sname ? 'shorthand' : []) .concat(async || [], generator || []) .concat(!(stype || sname) ? 'arrow' : []) .concat(stype || func) .map((key) => key[0].toUpperCase() + key.slice(1)); return [parts.join('')].concat(name || []).join(' '); }, }); /** * Create a guard verifying the given value matches the specified type */ function is(type) { return (value) => typeof value === type; } /** * Create a guard verifying the given value matches any of the validators */ function any(...checks) { return (value) => checks.some((check) => check(value)); } /** * Create a guard verifying the given value matches all of the validators */ function all(...checks) { return (value) => checks.every((check) => check(value)); } /** * Create a guard verifying the given value matches none of the validators */ function not(...checks) { return (value) => checks.every((check) => !check(value)); } /** * Guard verifying the value is a array */ const isArray = (value) => Array.isArray(value); /** * Guard verifying the value is a boolean */ const isBoolean = is('boolean'); /** * Guard verifying the value is a function */ const isFunction = is('function'); /** * Guard verifying the value is a null */ const isNULL = (value) => value === null; /** * Guard verifying the value is a object */ const isObject = all(not(isArray), not(isNULL), is('object')); /** * Guard verifying the value is a undefined */ const isUndefined = is('undefined'); /** * Creates a guard verifying the value is an object and has the given key */ function isKey(key) { return all(isObject, (value) => key in value); } /** * Creates a guard verifying the value is an object and has the given key matching the validators */ function isKeyOfType(key, ...validators) { return all(isKey(key), (value) => validators.every((check) => check(value[key]))); } /** * Creates a guard verifying the value is an object and doesn't have the key or has the given key matching the validators */ function isOptionalKeyOfType(key, ...validators) { return any(all(isObject, not(isKey(key))), isKeyOfType(key, ...validators)); } /** * Creates a guard verifying the value is an object matching at least the structure */ function isStructure(struct, ...options) { const optional = options.reduce((carry, key) => carry.concat(key), []); const validators = Object.keys(struct) .map((key) => (optional.includes(key) ? isOptionalKeyOfType : isKeyOfType)(key, struct[key])); return all(isObject, ...validators); } /** * Creates a guard verifying the value is an object matching exactly the structure */ function isStrictStructure(struct, ...options) { const keys = Object.keys(struct); return all( // do the keys present match the given structure conditions isStructure(struct, ...options), // every key should be part of the structure (value) => Object.keys(value).every((key) => keys.includes(key))); } const structure$2 = { enumerable: isBoolean, configurable: isBoolean, }; const isDataDescriptor = isStrictStructure(structure$2, Object.keys(structure$2)); const structure$1 = { configurable: isBoolean, enumerable: isBoolean, get: isFunction, set: isFunction, }; const optional$1 = Object.keys(structure$1).filter((key) => key !== 'get'); const isGetterAccessorDescriptor = isStrictStructure(structure$1, ...optional$1); const structure = { configurable: isBoolean, enumerable: isBoolean, value: () => true, writable: isBoolean, }; const optional = Object.keys(structure).filter((key) => key !== 'value'); const isValueAccessorDescriptor = isStrictStructure(structure, optional); const isDescriptor = any(isDataDescriptor, isValueAccessorDescriptor, isGetterAccessorDescriptor); const keys = { VALUE: 'value', GET: 'get', SET: 'set', CONFIGURABLE: 'configurable', ENUMERABLE: 'enumerable', WRITABLE: 'writable', }; class DescriptorMapper { static get(target, key) { const descriptor = Object.getOwnPropertyDescriptor(target, key); return descriptor ? descriptor : { value: undefined, writable: !Object.isFrozen(target), enumerable: true, configurable: Object.isExtensible(target), }; } static only(target, ...keys) { return keys .filter((key) => key in target && !isUndefined(target[key])) .reduce((carry, key) => Object.assign(carry, { [key]: target[key] }), {}); } static omit(target, ...keys) { return this.only(target, ...Object.keys(target).filter((key) => !keys.includes(key))); } static merge(...descriptors) { return descriptors.reduce((carry, desc) => this.combine(carry, desc), undefined); } static combine(one, two) { if (!(one && two)) { return two; } let input = one; if (keys.GET in two || keys.SET in two) { input = this.omit(input, keys.VALUE, keys.WRITABLE); } else if (keys.VALUE in two || keys.WRITABLE in two) { input = this.omit(input, keys.GET, keys.SET); } const output = Object.assign({}, input, two); // one can't exist without the other if (keys.SET in output && !(keys.GET in output)) { output[keys.GET] = () => { }; } if (keys.WRITABLE in output && !(keys.VALUE in output)) { output[keys.VALUE] = undefined; } return output; } } /** * Property Mutation * * @export * @class PropertyMutation * @extends {AbstractMutation<T>} * @template T */ class PropertyMutation extends AbstractMutation { /** * Creates an instance of PropertyMutation * * @param {T} options * @memberof PropertyMutation */ constructor(options) { if (!(isUndefined$1(options.value) || isDescriptor(options.value))) { throw new Error('Not a valid PropertyDescriptor'); } super(options); } /** * get name * * @readonly * @type {string} * @memberof PropertyMutation */ get name() { return 'property-mutation'; } /** * get descriptor * * @readonly * @type {PropertyDescriptor} * @memberof PropertyMutation */ get descriptor() { const { target, key } = this; return DescriptorMapper.merge(Object.getOwnPropertyDescriptor(target, key), super.value); } /** * get value * * @readonly * @type {T['value']} * @memberof PropertyMutation */ get value() { const { value, get } = super.value; return get ? get() : value; } /** * apply the property mutation to the target * * @memberof PropertyMutation */ apply() { const { target, key, descriptor } = this; Object.defineProperty(target, key, descriptor); } } /** * Value Mutation * * @export * @class ValueMutation * @extends {AbstractMutation<T>} * @template T */ class ValueMutation extends AbstractMutation { /** * get name * * @readonly * @type {string} * @memberof ValueMutation */ get name() { return 'value-mutation'; } /** * get descriptor * * @readonly * @type {PropertyDescriptor} * @memberof ValueMutation */ get descriptor() { const { target, key, value } = this; const defaults = Object.getOwnPropertyDescriptor(target, key) || { writable: true, enumerable: true, configurable: true }; return DescriptorMapper.merge(defaults, { value }); } /** * apply the property mutation to the target * * @memberof ValueMutation */ apply() { const { target, key, value } = this; target[key] = value; } } const storage$2 = new WeakMap(); /** * Collection for objects * * @export * @class Collection * @template T */ class Collection { /** * Creates an instance of Collection * * @param {...Array<T>} items * @memberof Collection */ constructor(...items) { this.items = []; this.push(...items); } /** * Push items into the Collection * * @param {...Array<T>} items * @return {*} {number} * @memberof Collection */ push(...items) { const { length } = this.items; this.items.push(...items.filter((item) => !this.items.includes(item))); return this.items.length - length; } /** * Pull items from the Collection * * @param {...Array<T>} items * @return {*} {Array<T>} * @memberof Collection */ pull(...items) { return items .reduce((carry, item) => carry.concat(this.items.splice(this.items.indexOf(item), 1)), []); } /** * Determine the number of items in the collection, optionally reduced to items matching the provided structure * * @param {Partial<T>} [seek] * @return {*} {number} * @memberof Collection */ count(seek) { const list = seek ? this.items.filter(this.seeker(seek)) : this.items; return list.length; } /** * Find the first item in the collection based on a (partial) structure * * @param {Partial<T>} seek * @return {*} {(T | undefined)} * @memberof Collection */ find(seek) { return this.items.find(this.seeker(seek)); } /** * Find all items in the collection matching a (partial) structure * * @param {Partial<T>} seek * @return {*} {Array<T>} * @memberof Collection */ findAll(seek) { return this.items.filter(this.seeker(seek)); } /** * Create a filter function for matching the given structure * * @private * @param {Partial<T>} seek * @return {*} {(item: T) => boolean} * @memberof Collection */ seeker(seek) { const keys = Object.keys(seek); return (item) => keys.every((key) => item[key] === seek[key]); } /** * Factory a Collection for given reference * * @static * @template T * @param {object} ref * @return {*} {Collection<T>} * @memberof Collection */ static for(ref) { if (!storage$2.has(ref)) { storage$2.set(ref, new Collection()); } return storage$2.get(ref); } } /** * ProxyHandler implementation * * @export * @class Trap * @template T * @template O */ class Trap { /** * Creates an instance of Trap * * @param {boolean} [onlyLastKeyMutation=false] * @memberof Trap */ constructor(onlyLastKeyMutation = false) { this.onlyLastKeyMutation = onlyLastKeyMutation; this.mutations = Collection.for(this); } /** * trap for property definition * * @param {T} target * @param {(string | symbol)} key * @param {PropertyDescriptor} descriptor * @return {*} {boolean} * @memberof Trap */ defineProperty(target, key, descriptor) { const { enumerable = Array.from(this.ownKeys(target)).indexOf(key) >= 0 } = descriptor || {}; const value = descriptor && Object.assign({ enumerable }, descriptor); return this.insert(new PropertyMutation({ target, key, value })); } /** * trap for property deletion * * @param {T} target * @param {(string | symbol)} key * @return {*} {boolean} * @memberof Trap */ deleteProperty(target, key) { const mutation = new DeletionMutation({ target, key }); return (this.onlyLastKeyMutation && this.purge(mutation) >= 0 && !Object.getOwnPropertyDescriptor(target, key)) || this.insert(mutation); } /** * trap for property reading * * @param {T} target * @param {(string | symbol)} key * @return {*} {unknown} * @memberof Trap */ get(target, key) { const { [key]: initial } = target; return this.mutations.findAll({ target, key }) .reduce((_, { value }) => value, initial); } /** * trap for reading property definition * * @param {T} target * @param {(string | symbol)} key * @return {*} {(PropertyDescriptor | undefined)} * @memberof Trap */ getOwnPropertyDescriptor(target, key) { return this.mutations.findAll({ target, key }) .reduce((_, { descriptor }) => descriptor, Object.getOwnPropertyDescriptor(target, key)); } /** * trap for testing whether a property exists * * @param {T} target * @param {(string | symbol)} key * @return {*} {boolean} * @memberof Trap */ has(target, key) { return this.mutations.findAll({ target, key }) .reduce((_, mutation) => !(mutation instanceof DeletionMutation), key in target); } /** * trap for reading all (enumerable) properties * * @param {T} target * @return {*} {(ArrayLike<string | symbol>)} * @memberof Trap */ ownKeys(target) { return this.mutations.findAll({ target }) .reduce((carry, mutation) => { var _a; return mutation instanceof DeletionMutation || (mutation instanceof PropertyMutation && !((_a = mutation.descriptor) === null || _a === void 0 ? void 0 : _a.enumerable)) ? carry.filter((key) => key !== mutation.key) : carry.concat(mutation.key); }, Reflect.ownKeys(target)) .filter((v, i, a) => a.indexOf(v) === i); } /** * trap for writing property values * * @param {T} target * @param {(string | symbol)} key * @param {unknown} value * @return {*} {boolean} * @memberof Trap */ set(target, key, value) { const mutation = new ValueMutation({ target, key, value }); return (this.onlyLastKeyMutation && this.purge(mutation) >= 0 && target[key] === value) || this.insert(mutation); } /** * Determine the number of mutations, optionally reduced to mutations matching the provided structure * * @param {Partial<O>} [seek] * @return {*} {number} * @memberof Trap */ count(seek) { return this.mutations.count(seek); } /** * commit all or a subset of collected mutations * * @param {Partial<O>} [seek={}] * @memberof Trap */ commit(seek = {}) { this.eject(seek) .forEach((mutation) => mutation.apply()); } /** * rolls back all or a subset of collected mutations * * @param {Partial<O>} [seek={}] * @memberof Trap */ rollback(seek = {}) { this.eject(seek); } /** * insert a mutation, cleaning up any mutation that is no longer needed * * @private * @param {(DeletionMutation<O> | PropertyMutation<O> | ValueMutation<O>)} mutation * @return {*} {boolean} * @memberof Trap */ insert(mutation) { this.purge(mutation); return Boolean(this.mutations.push(mutation)); } /** * remove all mutations matching the search argument * * @private * @param {Partial<O>} seek * @return {*} {Array<MutationInterface<O>>} * @memberof Trap */ eject(seek) { return this.mutations.pull(...this.mutations.findAll(seek)); } /** * remove mutations with the same target and key if only the last mutation shoudl be tracked * * @private * @param {(DeletionMutation<O> | PropertyMutation<O> | ValueMutation<O>)} { target, key } * @return {*} {number} * @memberof Trap */ purge({ target, key }) { return this.onlyLastKeyMutation ? this.eject({ target, key }).length : 0; } } isStrictStructure$1({ name: isString, target: any$1(isObject$1, isFunction$1), key: any$1(isString, isSymbol), value: () => true, descriptor: any$1(isUndefined$1, isDescriptor), apply: isFunction$1, }, ['value']); const storage$1 = new WeakMap(); class DecoyTrap extends Trap { constructor(delegate, onlyLastKeyMutation = false) { super(onlyLastKeyMutation); storage$1.set(this, { delegate, cache: new WeakMap() }); } delegate(value) { const { delegate } = storage$1.get(this); const result = delegate(value); Object.setPrototypeOf(result, Object.getPrototypeOf(value)); return result; } cache(target, value) { const { cache } = storage$1.get(this); if (!cache.has(value)) { cache.set(value, new Map()); } const map = cache.get(value); if (!map.has(target)) { map.set(target, this.delegate(value)); } return map.get(target); } get(target, key) { if (key === Symbol.toPrimitive && key in target) { return target[Symbol.toPrimitive].bind(target); } const value = super.get(target, key); return (typeof value === 'object' ? this.cache(target, value) : value); } } const storage = new WeakMap(); /** * Create a new Decoy * * @export * @template T * @param {T} target * @param {boolean} [onlyLastKeyMutation] * @return {*} {Decoy<T>} */ function create(target, onlyLastKeyMutation) { const linked = []; const trap = new DecoyTrap((t) => linked[linked.push(create(t, onlyLastKeyMutation)) - 1], onlyLastKeyMutation); const proxy = new Proxy(target, trap); storage.set(proxy, { target, trap, linked }); return proxy; } /** * Calculate the checksum of given Decoy * * @export * @param {Decoy<object>} proxy * @return {*} {string} */ function checksum(proxy) { return hash(proxy); } function isDecoy(input) { return storage.has(input); } /** * Traverse the Decoy and its linked Decoys executing the provided action * * @template T * @param {Decoy<T>} decoy * @param {((decoyed: Decoyed<object>, filter?: { [key: string | symbol]: string | symbol }) => void | Promise<void>)} action * @param {Array<keyof T>} [keys] * @return {*} {Promise<T>} */ function traverse(decoy, action, keys) { return __awaiter(this, void 0, void 0, function* () { if (!isDecoy(decoy)) { throw new Error(`Not a known Decoy: ${decoy}`); } const decoyed = storage.get(decoy); if (keys === null || keys === void 0 ? void 0 : keys.length) { const mutated = keys.filter((key) => hasMutations(decoy, key)); yield Promise.all(mutated.map((key) => isDecoy(decoy[key]) ? traverse(decoy[key], action) : action(decoyed, { [key]: key }))); return decoyed.target; } yield action(decoyed); const { linked, target } = decoyed; yield Promise.all(linked.map((linked) => traverse(linked, action))); return target; }); } /** * Purge a Decoy and its linked Decoys * * @export * @template T * @param {Decoy<T>} decoy * @return {*} {Promise<T>} */ function purge(decoy) { return traverse(decoy, (decoyed) => { storage.delete(decoy); decoyed.linked.length = 0; }); } /** * Commit the mutations to the decoyed object * * @export * @template T * @param {Decoy<T>} decoy * @param {...Array<keyof T>} keys * @return {*} {Promise<T>} */ function commit(decoy, ...keys) { return traverse(decoy, ({ trap }) => trap.commit(), keys); } /** * Roll back the mutations on the Decoy * * @export * @template T * @param {Decoy<T>} decoy * @param {...Array<keyof T>} keys * @return {*} {Promise<T>} */ function rollback(decoy, ...keys) { return traverse(decoy, ({ trap }) => trap.rollback(), keys); } /** * Check whether the Decoy has mutations * * @export * @template T * @param {Decoy<T>} decoy * @param {...Array<keyof T>} keys * @return {*} {boolean} */ function hasMutations(decoy, ...keys) { if (isDecoy(decoy)) { const { trap, linked } = storage.get(decoy); return keys.length ? keys.some((key) => trap.count({ key }) > 0 || (isDecoy(decoy[key]) && hasMutations(decoy[key]))) : trap.count() > 0 || linked.some((linked) => hasMutations(linked)); } return false; } exports.checksum = checksum; exports.commit = commit; exports.create = create; exports.hasMutations = hasMutations; exports.isDecoy = isDecoy; exports.purge = purge; exports.rollback = rollback;