UNPKG

space-lift

Version:

TypeScript Array, Object, Map, Set, Union, Enum utils

162 lines (161 loc) 5.97 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.toDraft = exports.clone = exports.update = void 0; /** * Creates an object copy by mutating a draft Object/Array/Map/Set or any of its descendants. */ function update(obj, updater) { let updatedObject = obj; const draft = makeDraft(obj, newObj => { updatedObject = newObj; }); updater(draft); return updatedObject; } exports.update = update; function makeDraft(initialObject, onChange) { let obj = initialObject; let childDraftCache = {}; const mutateObj = (mutation, key) => { if (obj === initialObject) obj = clone(obj); mutation(); if (key === allKeys) childDraftCache = {}; else if (key !== noKeys) delete childDraftCache[key]; onChange(obj); return true; }; const getObj = () => obj; const proxiedMethods = Array.isArray(obj) ? proxiedArrayMethods(getObj, mutateObj, () => draft) : obj instanceof Map ? proxiedMapMethods(getObj, mutateObj, childDraftCache) : obj instanceof Set ? proxiedSetMethods(getObj, mutateObj) : null; const draft = new Proxy(obj, { get: (_, key) => { // If we explicitely override or add this method, use it. if (proxiedMethods && proxiedMethods[key]) return proxiedMethods[key]; const value = obj[key]; // If it's a function on the host object, then return its reference (after binding it as it's now detached) if (typeof value === 'function') return value.bind(obj); // If it's a primitive, there's no point drafting it so return as is. if (isPrimitive(value)) return value; // At this point, the draft is either an Object or Array and the child being accessed is not a primitive: Draft it. let childDraft = childDraftCache[key]; if (!childDraft) { childDraft = childDraftCache[key] = makeDraft(value, newChildObject => { draft[key] = newChildObject; }); } return childDraft; }, set: (_, key, newValue) => mutateObj(() => { obj[key] = newValue; }, key), deleteProperty: (_, key) => mutateObj(() => { delete obj[key]; }, key) }); return draft; } function proxiedArrayMethods(getArray, mutateArray, getDraft) { return { prepend: (item) => mutateArray(() => getArray().unshift(item), allKeys), append: (item) => mutateArray(() => getArray().push(item), noKeys), insert: (item, index) => mutateArray(() => getArray().splice(index, 0, item), allKeys), updateIf: (predicate, updateFunction) => getArray().forEach((item, index) => { if (predicate(item, index)) updateFunction(getDraft()[index], index); }), removeIf: (predicate) => { let index = getArray().length - 1; while (index >= 0) { if (predicate(getArray()[index], index)) mutateArray(() => getArray().splice(index, 1), allKeys); index -= 1; } } }; } function proxiedMapMethods(getMap, mutateMap, draftCache) { const methods = { get: (key) => { const value = getMap().get(key); if (!value || isPrimitive(value)) return value; let childDraft = draftCache[key]; if (!childDraft) { childDraft = draftCache[key] = makeDraft(value, newChildObject => { methods.set(key, newChildObject); }); } return childDraft; }, set: (key, value) => mutateMap(() => getMap().set(key, value), key), clear: () => mutateMap(() => getMap().clear(), allKeys), delete: (key) => { mutateMap(() => getMap().delete(key), key); }, updateValue: (key, updateFunction) => { if (!getMap().has(key)) return; // .get() already creates a draft for this value, if applicable. const value = methods.get(key); mutateMap(() => { const maybeUpdatedValue = updateFunction(value); // If a value was returned, it means no draft was modified directly and we must set it. if (maybeUpdatedValue !== undefined) getMap().set(key, maybeUpdatedValue); }, key); } }; return methods; } function proxiedSetMethods(getSet, mutateSet) { return { add: (value) => mutateSet(() => getSet().add(value), noKeys), clear: () => mutateSet(() => getSet().clear(), noKeys), delete: (value) => mutateSet(() => getSet().delete(value), noKeys) }; } function isPrimitive(obj) { return obj === null || typeof obj !== 'object'; } function clone(obj) { if (isPrimitive(obj)) return obj; if (Array.isArray(obj)) return obj.slice(); if (obj instanceof Map) return new Map(obj); if (obj instanceof Set) return new Set(obj); else { const cloned = {}; Object.keys(obj).forEach(key => { cloned[key] = obj[key]; }); return cloned; } } exports.clone = clone; const allKeys = {}; const noKeys = {}; /** * Type-cast from a regular type to its draft type. * Use this if you want to assign a regular value wholesale instead of mutating an existing one. */ // We need this because Typescript doesn't yet allow one to type reads and writes differently: // https://github.com/microsoft/TypeScript/issues/43826 // If we had this ability, we could type the getter as Draft<T> and the setter as T. function toDraft(obj) { return obj; } exports.toDraft = toDraft;