limu
Version:
A fast js lib of immutable data, based on shallow copy on read and mark modified on write mechanism
523 lines (472 loc) • 18 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Licensed under the MIT License.
*
* @Author: fantasticsoul
*--------------------------------------------------------------------------------------------*/
import type { DraftMeta, IApiCtx, IExecOnOptions, IInnerCreateDraftOptions, ObjectLike, Op } from '../inner-types';
import {
ARRAY,
CAREFUL_FNKEYS,
CAREFUL_TYPES,
CHANGE_FNKEYS,
IMMUT_BASE,
JS_SYM_KEYS,
MAP,
META_VER,
OP_DEL,
OP_GET,
OP_SET,
SET,
} from '../support/consts';
import { conf, genMetaVer } from '../support/inner-data';
import { canBeNum, has, isFn, isPrevVerDraft, isPrimitive } from '../support/util';
import { handleDataNode } from './data-node-processor';
import { deepFreeze } from './freeze';
import { createScopedMeta, getMayProxiedVal, getUnProxyValue } from './helper';
import {
getDraftMetaByCtx,
getPrivateMeta,
getSafeDraftMeta,
getSourceId,
isDraft,
mayRelinkPath,
replaceMetaPartial,
ROOT_CTX,
setSourceId,
} from './meta';
import { getVal, getValByKeyPaths, pushKeyPath, setValByKeyPaths } from './path-util';
import { extractFinalData, isInSameScope, recordVerScope } from './scope';
// 可直接返回的属性
// 避免 Cannot set property size of #<Map> which has only a getter
// 避免 Cannot set property size of #<Set> which has only a getter
const PROPERTIES_BLACK_LIST = ['length', 'constructor', 'asymmetricMatch', 'nodeType', 'size'] as const;
const PBL_DICT: Record<string, number> = {}; // for perf
PROPERTIES_BLACK_LIST.forEach((item) => (PBL_DICT[item] = 1));
const TYPE_BLACK_DICT: Record<string, number> = { [ARRAY]: 1, [SET]: 1, [MAP]: 1 }; // for perf
export const FINISH_HANDLER_MAP = new Map();
export function buildLimuApis(options?: IInnerCreateDraftOptions) {
const opts = options || {};
const onOperate = opts.onOperate;
const hasOnOperate = !!onOperate;
const customKeys = opts.customKeys || [];
// @ts-ignore
const immutBase = opts[IMMUT_BASE] ?? false;
const readOnly = opts.readOnly ?? false;
const disableWarn = opts.disableWarn;
const compareVer = opts.compareVer ?? false;
// 调用那一刻起,确定 autoFreeze 值
// allow user overwrite autoFreeze setting in current call process
const autoFreeze = opts.autoFreeze ?? conf.autoFreeze;
const disableProxy = opts.disableProxy ?? false;
let metaVer = '';
let isDraftFinished = false;
const apiCtx: IApiCtx = { metaMap: new Map(), newNodeMap: new Map(), metaVer };
if (!disableProxy) {
metaVer = genMetaVer();
apiCtx.metaVer = metaVer;
ROOT_CTX.set(metaVer, apiCtx);
}
const autoRevoke = opts.autoRevoke ?? conf.autoRevoke;
const silenceSetTrapErr = opts.silenceSetTrapErr ?? true;
const logChangeFailed = (op: string, key: string) => {
console.warn(`${op} failed, cuase draft root has been finised! key:`, key);
return silenceSetTrapErr;
};
const logSetExpiredValFailed = (op: string, key: string) => {
console.warn(`${op} failed, cuase the value is an expired limu proxy data! key:`, key);
return silenceSetTrapErr;
};
const warnReadOnly = () => {
if (!disableWarn) {
console.warn('can not mutate state at readOnly mode!');
}
return true;
};
const execOnOperate = (op: Op, key: any, options: IExecOnOptions) => {
const { mayProxyVal, parentMeta: inputPMeta, value, isCustom = false } = options;
let isChanged = false;
const isNotGet = op !== OP_GET;
// set del 时,replacedValue 初始值指向 value
// get 时,replacedValue 初始值指向 mayProxyVal
let replacedValue: any = isNotGet ? value : mayProxyVal;
if (!onOperate) return { isChanged, replacedValue };
const parentMeta = (inputPMeta || {}) as DraftMeta;
const {
selfType = '',
keyPath = [],
copy,
self,
modified,
proxyVal: parentProxy,
arrKeyPath = [],
keyPaths = [],
keyStrPaths = [],
arrKeyPaths = [],
} = parentMeta;
// console.log('execOnOperate parentMeta', parentMeta);
let isBuiltInFnKey = false;
// 优先采用显式传递的 isChange
if (options.isChanged !== undefined) {
isChanged = options.isChanged;
} else {
const fnKeys = CAREFUL_FNKEYS[selfType] || [];
if (fnKeys.includes(key)) {
isBuiltInFnKey = true;
const changeFnKeys = CHANGE_FNKEYS[selfType] || [];
isChanged = changeFnKeys.includes(key);
} else if (isNotGet) {
// 变化之后取 copy 比较
const node = modified ? copy : self;
isChanged = inputPMeta ? node[key] !== value : true;
}
}
let isReplaced = false;
const replaceValue = (newValue: any) => {
isReplaced = true;
replacedValue = newValue;
};
const getReplaced = () => ({ isReplaced, replacedValue });
onOperate({
immutBase,
parent: self,
parentType: selfType,
parentProxy,
op,
replaceValue,
getReplaced,
isBuiltInFnKey,
isChanged,
isCustom,
key,
keyPath,
keyPaths,
keyStrPaths,
fullKeyPath: keyPath.concat(key),
arrKeyPath,
arrKeyPaths,
value,
// 写操作时,proxyValue 是 undefined
proxyValue: mayProxyVal,
});
return {
replacedValue,
isChanged,
};
};
const limuApis = (() => {
// let revoke: null | (() => void) = null;
/**
* 为了和下面这个 immer case 保持行为一致
* https://github.com/immerjs/immer/issues/960
* 如果数据节点上人工赋值了其他 draft 的话,当前 draft 结束后不能够被冻结( 见set逻辑 )
*/
let canFreezeDraft = true;
// >= 3.0+ ver, shadow copy on read, mark modified on write
const limuTraps = {
// parent指向的是代理之前的对象
get: (parent: any, key: any) => {
if (META_VER === key) {
return metaVer;
}
/** current child value, it may been replaced to a proxy value later */
const currentVal = parent[key];
if (JS_SYM_KEYS.includes(key)) {
// 避免报错 Method xx.yy called on incompatible receiver
// 例如 Array.from(draft)
if (isFn(currentVal)) {
// 执行 for(const item of list){ ... } 语句
if (Symbol.iterator === key && Array.isArray(parent)) {
let idx = 0;
// 模拟迭代器
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol
const iter = () => ({
next: () => {
const len = parent.length;
if (len === 0) {
return { done: true, value: undefined };
}
// 前一次迭代已到达最后一个元素时,idx === len
const done = idx === len;
// key 格式统一为字符串,故这里包一层 String
const value = done ? undefined : limuTraps.get(parent, String(idx));
idx++;
return { done, value };
},
[Symbol.iterator]: () => {
return iter;
},
});
return iter;
}
return currentVal.bind(parent);
}
return currentVal;
}
// 判断 toJSON 是为了兼容 JSON.stringify 调用, https://javascript.info/json#custom-tojson
if (key === '__proto__' || (key === 'toJSON' && !has(parent, key))) {
return currentVal;
}
let mayProxyVal = currentVal;
const parentMeta = getSafeDraftMeta(parent, apiCtx);
// const keyStr = getKeyStrByPath(parentMeta.keyStrPath.concat(key));
// const paths = getMultiRefPaths(parentMeta.sourceId, keyStr);
// if (paths.length > 1) {
// }
// if (key === 'currentUser') {
// console.trace(`get sourceId ${parentMeta.sourceId}`, metaVer);
// const keyStr = getKeyStrByPath(parentMeta.keyStrPath.concat(key));
// console.log(getMultiRefPaths(parentMeta.sourceId, keyStr));
// }
/**
* 当执行类似代码时:
* draft.a = draft.info.a;
* 需重记录多引用关系
*/
const valueMeta = getDraftMetaByCtx(mayProxyVal, apiCtx);
const proxyVal = mayRelinkPath(key, parentMeta, valueMeta);
if (proxyVal) {
return proxyVal;
}
if (customKeys.includes(key)) {
const ret = execOnOperate(OP_GET, key, { parentMeta, mayProxyVal, value: currentVal, isChanged: false, isCustom: true });
return ret.replacedValue;
}
const parentType = parentMeta?.selfType;
// copyWithin、sort 、valueOf... will hit the keys of 'asymmetricMatch', 'nodeType',
// PROPERTIES_BLACK_LIST 里 'length', 'constructor', 'asymmetricMatch', 'nodeType'
if (TYPE_BLACK_DICT[parentType] && PBL_DICT[key]) {
if (key === 'length' || key === 'size') {
execOnOperate(OP_GET, key, { parentMeta, mayProxyVal, value: currentVal });
}
return parentMeta.copy[key];
}
// 可能会指向代理对象
mayProxyVal = getMayProxiedVal(currentVal, {
key,
compareVer,
parentMeta,
parentType,
ver: metaVer,
traps: limuTraps,
parent,
immutBase,
readOnly,
apiCtx,
hasOnOperate,
autoRevoke,
});
// 用下标取数组时,可直接返回
// 例如数组操作: arrDraft[0].xxx = 'new', 此时 arrDraft[0] 需要操作的是代理对象
if (parentType === ARRAY && canBeNum(key)) {
const ret = execOnOperate(OP_GET, key, { parentMeta, mayProxyVal, value: currentVal });
return ret.replacedValue;
}
// @ts-ignore
if (CAREFUL_TYPES[parentType]) {
mayProxyVal = handleDataNode(parent, {
op: key,
key,
value: currentVal,
metaVer,
calledBy: OP_GET,
parentType,
parentMeta,
apiCtx,
});
const ret = execOnOperate(OP_GET, key, { parentMeta, mayProxyVal, value: currentVal });
return ret.replacedValue;
}
const ret = execOnOperate(OP_GET, key, { parentMeta, mayProxyVal, value: currentVal });
return ret.replacedValue;
},
// parent 指向的是代理之前的对象
set: (parent: any, key: any, inputValue: any) => {
if (isDraftFinished) {
return logChangeFailed(OP_SET, key);
}
let mayNewNode = true;
const parentMeta = getSafeDraftMeta(parent, apiCtx);
// fix issue https://github.com/tnfe/limu/issues/12
let isValueDraft = false;
let value = inputValue;
// is a draft proxy node
if (isDraft(inputValue)) {
isValueDraft = true;
// see case debug/complex/set-draft-node
if (isInSameScope(inputValue, metaVer)) {
const rawValue = getUnProxyValue(inputValue, apiCtx);
// 相同路径赋相同的值,不做任何处理
if (rawValue === parent[key]) {
return true;
}
const valueMeta = getSafeDraftMeta(inputValue, apiCtx);
// 对某个对象做了2次赋值,例如:
// draft.current = draft.list[0];
mayRelinkPath(key, parentMeta, valueMeta);
const newKeyPath = parentMeta.keyPath.concat(key);
pushKeyPath(valueMeta, newKeyPath);
} else {
// TODO: judge value must be a root draft node
// assign another scope draft node to current scope
canFreezeDraft = false;
}
} else if (isPrevVerDraft(inputValue, metaVer)) {
const { proxyVal: rootProxy, self: rootSelf } = parentMeta.rootMeta;
// 是一个之前版本的 limu 草稿对象,尝试根据之前的路径获取当前 scope 对应的代理对象来赋值
const prevVerMeta = getPrivateMeta(inputValue);
const { isGetted, val } = getValByKeyPaths(rootProxy, prevVerMeta.keyPaths);
if (!isGetted) {
return logSetExpiredValFailed(OP_SET, key);
}
const curVerMeta = getPrivateMeta(val);
replaceMetaPartial(prevVerMeta, curVerMeta, key);
const newKeyPath = parentMeta.keyPath.concat(key);
// 所有路径上的值提替换掉
curVerMeta.keyPaths.forEach((keyPath) => {
const { isGetted, val } = getVal(rootProxy, keyPath);
if (isGetted) {
const curVerPathMeta = getPrivateMeta(val);
replaceMetaPartial(prevVerMeta, curVerPathMeta, key);
}
});
pushKeyPath(curVerMeta, newKeyPath);
setValByKeyPaths(rootSelf, curVerMeta.keyPaths, curVerMeta.self);
mayNewNode = curVerMeta.keyPaths.length === 1;
apiCtx.metaMap.set(curVerMeta.copy, curVerMeta);
value = val;
}
if (readOnly) {
execOnOperate(OP_SET, key, { parentMeta, isChanged: false, value });
return warnReadOnly();
}
// speed up array operation
if (parentMeta && parentMeta.selfType === ARRAY) {
// @ts-ignore
if (parentMeta.copy && parentMeta.__callSet && canBeNum(key)) {
const ret = execOnOperate(OP_SET, key, { parentMeta, value });
value = ret.replacedValue;
parentMeta.copy[key] = value;
return true;
}
// @ts-ignore, mark is set called on parent node
parentMeta.__callSet = true;
}
let isChanged = false;
if (!onOperate) {
// 变化之后取 copy 比较
const node = parentMeta.modified ? parentMeta.copy : parentMeta.self;
isChanged = node[key] !== value;
} else {
const ret = execOnOperate(OP_SET, key, { parentMeta, value });
value = ret.replacedValue;
isChanged = ret.isChanged;
}
if (isChanged) {
handleDataNode(parent, {
parentMeta,
key,
value,
metaVer,
calledBy: OP_SET,
apiCtx,
isValueDraft,
mayNewNode,
});
}
return true;
},
// delete or Reflect.deleteProperty will trigger this trap
deleteProperty: (parent: any, key: any) => {
if (isDraftFinished) {
return logChangeFailed(OP_DEL, key);
}
const parentMeta = getSafeDraftMeta(parent, apiCtx);
const value = parent[key];
if (readOnly) {
execOnOperate(OP_DEL, key, { parentMeta, isChanged: false, value });
return warnReadOnly();
}
execOnOperate(OP_DEL, key, { parentMeta, isChanged: true, value });
handleDataNode(parent, {
parentMeta,
op: OP_DEL,
key,
value: '',
metaVer,
calledBy: 'deleteProperty',
apiCtx,
});
return true;
},
// trap function call
apply: function (target: any, thisArg: any, args: any[]) {
return target.apply(thisArg, args);
},
};
return {
createDraft: <T = ObjectLike>(mayDraft: T): T => {
if (isPrimitive(mayDraft)) {
throw new Error('base state can not be primitive');
}
if (disableProxy) {
FINISH_HANDLER_MAP.set(mayDraft, limuApis.finishDraft);
return mayDraft;
}
let oriBase = mayDraft;
const sourceId = opts.sourceId || getSourceId(mayDraft);
const draftMeta = getSafeDraftMeta(mayDraft, apiCtx);
if (draftMeta) {
// 总是返回同一个 immutBase 代理对象
if (immutBase && draftMeta.isImmutBase) {
return draftMeta.proxyVal as T;
}
oriBase = draftMeta.self;
}
const meta = createScopedMeta('', oriBase, {
ver: metaVer,
traps: limuTraps,
immutBase,
readOnly,
compareVer,
apiCtx,
hasOnOperate,
autoRevoke,
sourceId,
});
recordVerScope(meta);
meta.execOnOperate = execOnOperate;
FINISH_HANDLER_MAP.set(meta.proxyVal, limuApis.finishDraft);
return meta.proxyVal as T;
},
finishDraft: (proxyDraft: any, clearImmut?: boolean) => {
if (disableProxy) {
FINISH_HANDLER_MAP.delete(proxyDraft);
return proxyDraft;
}
// attention: if pass a revoked proxyDraft
// it will throw: Cannot perform 'set' on a proxy that has been revoked
const rootMeta = getSafeDraftMeta(proxyDraft, apiCtx);
// immutBase 是一个一直可用的代理对象(不会被revoke)
// 对 immut() 返回的对象调用 finishDraft 则总是返回 immutBase 自身代理
// 将 immut() 返回结果传给 finishDraft 是无意义的
// 彻底清理 immut 需调用 finishImmut 接口
if (rootMeta.isImmutBase && !clearImmut) {
return proxyDraft;
}
let final = extractFinalData(rootMeta, apiCtx);
if (autoFreeze && canFreezeDraft) {
// TODO deep pruning
// see https://github.com/immerjs/immer/issues/687
// let cachedFrozenOriginalBase = frozenOriginalBaseMap.get(rootMeta.originalSelf);
final = deepFreeze(final);
}
ROOT_CTX.delete(metaVer);
FINISH_HANDLER_MAP.delete(proxyDraft);
setSourceId(final, rootMeta.sourceId);
isDraftFinished = true;
return final;
},
};
})();
return limuApis;
}