space-lift
Version:
TypeScript Array, Object, Map, Set, Union, Enum utils
162 lines (161 loc) • 5.97 kB
JavaScript
;
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;