mutative
Version:
A JavaScript library for efficient immutable updates
340 lines (331 loc) • 10.9 kB
text/typescript
import {
DraftType,
Finalities,
Patches,
ProxyDraft,
Options,
Operation,
} from './interface';
import { dataTypes, PROXY_DRAFT } from './constant';
import { mapHandler, mapHandlerKeys } from './map';
import { setHandler, setHandlerKeys } from './set';
import { internal } from './internal';
import {
deepFreeze,
ensureShallowCopy,
getDescriptor,
getProxyDraft,
getType,
getValue,
has,
isEqual,
isDraftable,
latest,
markChanged,
peek,
get,
set,
revokeProxy,
finalizeSetValue,
markFinalization,
finalizePatches,
} from './utils';
import { checkReadable } from './unsafe';
import { generatePatches } from './patch';
const draftsCache = new WeakSet<object>();
const proxyHandler: ProxyHandler<ProxyDraft> = {
get(target: ProxyDraft, key: string | number | symbol, receiver: any) {
const copy = target.copy?.[key];
// Improve draft reading performance by caching the draft copy.
if (copy && draftsCache.has(copy)) {
return copy;
}
if (key === PROXY_DRAFT) return target;
let markResult: any;
if (target.options.mark) {
// handle `Uncaught TypeError: Method get Map.prototype.size called on incompatible receiver #<Map>`
// or `Uncaught TypeError: Method get Set.prototype.size called on incompatible receiver #<Set>`
const value =
key === 'size' &&
(target.original instanceof Map || target.original instanceof Set)
? Reflect.get(target.original, key)
: Reflect.get(target.original, key, receiver);
markResult = target.options.mark(value, dataTypes);
if (markResult === dataTypes.mutable) {
if (target.options.strict) {
checkReadable(value, target.options, true);
}
return value;
}
}
const source = latest(target);
if (source instanceof Map && mapHandlerKeys.includes(key as any)) {
if (key === 'size') {
return Object.getOwnPropertyDescriptor(mapHandler, 'size')!.get!.call(
target.proxy
);
}
const handle = mapHandler[key as keyof typeof mapHandler] as Function;
if (handle) {
return handle.bind(target.proxy);
}
}
if (source instanceof Set && setHandlerKeys.includes(key as any)) {
if (key === 'size') {
return Object.getOwnPropertyDescriptor(setHandler, 'size')!.get!.call(
target.proxy
);
}
const handle = setHandler[key as keyof typeof setHandler] as Function;
if (handle) {
return handle.bind(target.proxy);
}
}
if (!has(source, key)) {
const desc = getDescriptor(source, key);
return desc
? `value` in desc
? desc.value
: // !case: support for getter
desc.get?.call(target.proxy)
: undefined;
}
const value = source[key];
if (target.options.strict) {
checkReadable(value, target.options);
}
if (target.finalized || !isDraftable(value, target.options)) {
return value;
}
// Ensure that the assigned values are not drafted
if (value === peek(target.original, key)) {
ensureShallowCopy(target);
target.copy![key] = createDraft({
original: target.original[key],
parentDraft: target,
key: target.type === DraftType.Array ? Number(key) : key,
finalities: target.finalities,
options: target.options,
});
// !case: support for custom shallow copy function
if (typeof markResult === 'function') {
const subProxyDraft = getProxyDraft(target.copy![key])!;
ensureShallowCopy(subProxyDraft);
// Trigger a custom shallow copy to update to a new copy
markChanged(subProxyDraft);
return subProxyDraft.copy;
}
return target.copy![key];
}
return value;
},
set(target: ProxyDraft, key: string | number | symbol, value: any) {
if (target.type === DraftType.Set || target.type === DraftType.Map) {
throw new Error(
`Map/Set draft does not support any property assignment.`
);
}
let _key: number;
if (
target.type === DraftType.Array &&
key !== 'length' &&
!(
Number.isInteger((_key = Number(key))) &&
_key >= 0 &&
(key === 0 || _key === 0 || String(_key) === String(key))
)
) {
throw new Error(
`Only supports setting array indices and the 'length' property.`
);
}
const desc = getDescriptor(latest(target), key);
if (desc?.set) {
// !case: cover the case of setter
desc.set.call(target.proxy, value);
return true;
}
const current = peek(latest(target), key);
const currentProxyDraft = getProxyDraft(current);
if (currentProxyDraft && isEqual(currentProxyDraft.original, value)) {
// !case: ignore the case of assigning the original draftable value to a draft
target.copy![key] = value;
target.assignedMap = target.assignedMap ?? new Map();
target.assignedMap.set(key, false);
return true;
}
// !case: handle new props with value 'undefined'
if (
isEqual(value, current) &&
(value !== undefined || has(target.original, key))
)
return true;
ensureShallowCopy(target);
markChanged(target);
if (has(target.original, key) && isEqual(value, target.original[key])) {
// !case: handle the case of assigning the original non-draftable value to a draft
target.assignedMap!.delete(key);
} else {
target.assignedMap!.set(key, true);
}
target.copy![key] = value;
markFinalization(target, key, value, generatePatches);
return true;
},
has(target: ProxyDraft, key: string | symbol) {
return key in latest(target);
},
ownKeys(target: ProxyDraft) {
return Reflect.ownKeys(latest(target));
},
getOwnPropertyDescriptor(target: ProxyDraft, key: string | symbol) {
const source = latest(target);
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
if (!descriptor) return descriptor;
return {
writable: true,
configurable: target.type !== DraftType.Array || key !== 'length',
enumerable: descriptor.enumerable,
value: source[key],
};
},
getPrototypeOf(target: ProxyDraft) {
return Reflect.getPrototypeOf(target.original);
},
setPrototypeOf() {
throw new Error(`Cannot call 'setPrototypeOf()' on drafts`);
},
defineProperty() {
throw new Error(`Cannot call 'defineProperty()' on drafts`);
},
deleteProperty(target: ProxyDraft, key: string | symbol) {
if (target.type === DraftType.Array) {
return proxyHandler.set!.call(this, target, key, undefined, target.proxy);
}
if (peek(target.original, key) !== undefined || key in target.original) {
// !case: delete an existing key
ensureShallowCopy(target);
markChanged(target);
target.assignedMap!.set(key, false);
} else {
target.assignedMap = target.assignedMap ?? new Map();
// The original non-existent key has been deleted
target.assignedMap.delete(key);
}
if (target.copy) delete target.copy[key];
return true;
},
};
export function createDraft<T extends object>(createDraftOptions: {
original: T;
parentDraft?: ProxyDraft | null;
key?: string | number | symbol;
finalities: Finalities;
options: Options<any, any>;
}): T {
const { original, parentDraft, key, finalities, options } =
createDraftOptions;
const type = getType(original);
const proxyDraft: ProxyDraft = {
type,
finalized: false,
parent: parentDraft,
original,
copy: null,
proxy: null,
finalities,
options,
// Mapping of draft Set items to their corresponding draft values.
setMap:
type === DraftType.Set
? new Map((original as Set<any>).entries())
: undefined,
};
// !case: undefined as a draft map key
if (key || 'key' in createDraftOptions) {
proxyDraft.key = key;
}
const { proxy, revoke } = Proxy.revocable<any>(
type === DraftType.Array ? Object.assign([], proxyDraft) : proxyDraft,
proxyHandler
);
finalities.revoke.push(revoke);
draftsCache.add(proxy);
proxyDraft.proxy = proxy;
if (parentDraft) {
const target = parentDraft;
target.finalities.draft.push((patches, inversePatches) => {
const oldProxyDraft = getProxyDraft(proxy)!;
// if target is a Set draft, `setMap` is the real Set copies proxy mapping.
let copy = target.type === DraftType.Set ? target.setMap : target.copy;
const draft = get(copy, key!);
const proxyDraft = getProxyDraft(draft);
if (proxyDraft) {
// assign the updated value to the copy object
let updatedValue = proxyDraft.original;
if (proxyDraft.operated) {
updatedValue = getValue(draft);
}
finalizeSetValue(proxyDraft);
finalizePatches(proxyDraft, generatePatches, patches, inversePatches);
if (__DEV__ && target.options.enableAutoFreeze) {
target.options.updatedValues =
target.options.updatedValues ?? new WeakMap();
target.options.updatedValues.set(updatedValue, proxyDraft.original);
}
// final update value
set(copy, key!, updatedValue);
}
// !case: handle the deleted key
oldProxyDraft.callbacks?.forEach((callback) => {
callback(patches, inversePatches);
});
});
} else {
// !case: handle the root draft
const target = getProxyDraft(proxy)!;
target.finalities.draft.push((patches, inversePatches) => {
finalizeSetValue(target);
finalizePatches(target, generatePatches, patches, inversePatches);
});
}
return proxy;
}
internal.createDraft = createDraft;
export function finalizeDraft<T>(
result: T,
returnedValue: [T] | [],
patches?: Patches,
inversePatches?: Patches,
enableAutoFreeze?: boolean
) {
const proxyDraft = getProxyDraft(result);
const original = proxyDraft?.original ?? result;
const hasReturnedValue = !!returnedValue.length;
if (proxyDraft?.operated) {
while (proxyDraft.finalities.draft.length > 0) {
const finalize = proxyDraft.finalities.draft.pop()!;
finalize(patches, inversePatches);
}
}
const state = hasReturnedValue
? returnedValue[0]
: proxyDraft
? proxyDraft.operated
? proxyDraft.copy
: proxyDraft.original
: result;
if (proxyDraft) revokeProxy(proxyDraft);
if (enableAutoFreeze) {
deepFreeze(state, state, proxyDraft?.options.updatedValues);
}
return [
state,
patches && hasReturnedValue
? [{ op: Operation.Replace, path: [], value: returnedValue[0] }]
: patches,
inversePatches && hasReturnedValue
? [{ op: Operation.Replace, path: [], value: original }]
: inversePatches,
] as [T, Patches | undefined, Patches | undefined];
}