limu
Version:
A fast js lib of immutable data, based on shallow copy on read and mark modified on write mechanism
1,460 lines (1,449 loc) • 76.3 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.limu = {}));
})(this, (function (exports) { 'use strict';
/**
* 4.0 开始,新增 keyPaths 支持多引用记录
* 4.1 开始,支持 draft 操作过程中产生新引用
*/
const VER$1 = '4.1.1';
/** 版本号key */
const META_VER = Symbol('V');
/** 标识这是一个 immut 创建的根对象 */
const IMMUT_BASE = Symbol('IMMUT_BASE');
/** markRaw 调用会给对象标记 IS_RAW 为 true */
const IS_RAW = Symbol('IS_RAW');
/** 数据节点私有数据 */
const PRIVATE_META = Symbol('P');
const MAP = 'Map';
const SET = 'Set';
const ARRAY = 'Array';
const OBJECT = 'Object';
/**
* limu 需要关心的 symbol 读取 key 列表
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol
*/
const JS_SYM_KEYS = [Symbol.iterator, Symbol.toStringTag, IS_RAW];
const CAREFUL_TYPES = { Map: MAP, Set: SET, Array: ARRAY };
const OBJ_DESC = '[object Object]';
const MAP_DESC = '[object Map]';
const SET_DESC = '[object Set]';
const ARR_DESC = '[object Array]';
const FN_DESC = '[object Function]';
const desc2dataType = {
[MAP_DESC]: MAP,
[SET_DESC]: SET,
[ARR_DESC]: ARRAY,
[OBJ_DESC]: OBJECT,
};
const SHOULD_REASSIGN_ARR_METHODS = ['push', 'pop', 'shift', 'splice', 'unshift', 'reverse', 'copyWithin', 'delete', 'fill'];
const SHOULD_REASSIGN_MAP_METHODS = ['set', 'clear', 'delete'];
const SHOULD_REASSIGN_SET_METHODS = ['add', 'clear', 'delete'];
const CHANGE_ARR_ORDER_METHODS = ['splice', 'sort', 'unshift', 'shift'];
const arrFnKeys = [
'concat',
'copyWithin',
'entries',
'every',
'fill',
'filter',
'find',
'findIndex',
'flat',
'flatMap',
'forEach',
'includes',
'indexOf',
'join',
'keys',
'lastIndexOf',
'map',
'pop',
'push',
'reduce',
'reduceRight',
'reverse',
'shift',
'unshift',
'slice',
'some',
'sort',
'splice',
'values',
'valueOf',
];
const mapFnKeys = ['clear', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', 'values'];
const setFnKeys = ['add', 'clear', 'delete', 'entries', 'forEach', 'has', 'keys', 'values'];
const CAREFUL_FNKEYS = {
[MAP]: mapFnKeys,
[SET]: setFnKeys,
[ARRAY]: arrFnKeys,
};
const CHANGE_FNKEYS = {
[MAP]: ['clear', 'set', 'delete'],
[SET]: ['clear', 'add', 'delete'],
[ARRAY]: ['pop', 'push', 'shift', 'unshift', 'splice', 'sort', 'copyWithin'],
};
const PROXYITEM_FNKEYS = {
[MAP]: ['forEach', 'get'],
[SET]: ['forEach'],
[ARRAY]: ['forEach', 'map'],
};
const OP_GET = 'get';
const OP_SET = 'set';
const OP_DEL = 'del';
/*---------------------------------------------------------------------------------------------
* Licensed under the MIT License.
*
* @Author: fantasticsoul
*--------------------------------------------------------------------------------------------*/
function getId(idWrap, customPrefix = '') {
if (idWrap.value >= Number.MAX_SAFE_INTEGER) {
idWrap.value = 1;
idWrap.prefixSeed += 1;
}
else {
idWrap.value += 1;
}
const { value, prefixSeed } = idWrap;
const metaVer = `${customPrefix}${prefixSeed}_${value}`;
return metaVer;
}
const verWrap = { value: 0, prefixSeed: 1 };
const idWrap = { value: 0, prefixSeed: 1 };
const symbolIdWrap = { value: 0, prefixSeed: 1 };
const sourceIdWrap = { value: 0, prefixSeed: 1 };
const symbolStrDict = {};
const strSymbolDict = {};
function genMetaId() {
return getId(idWrap, 'MID_');
}
function genMetaVer() {
return getId(verWrap, 'MV_');
}
function genSymbolId() {
return getId(symbolIdWrap, 'SI_');
}
function genSourceId() {
return getId(sourceIdWrap, 'SR_');
}
const conf = {
autoFreeze: false,
autoRevoke: true,
};
const toString = Object.prototype.toString;
const canUseReflect = !!Reflect;
const hasProp = Object.prototype.hasOwnProperty;
function has(obj, key) {
if (canUseReflect) {
return Reflect.has(obj, key);
}
return hasProp.call(obj, key);
}
function deepDrill(obj, parentObj, key, subObjCb) {
const list = [];
const innerDeep = (obj, parentObj, key) => {
if (isPrimitive(obj)) {
return;
}
if (list.includes(obj)) {
// 出现循环引用,及时跳出来,避免出现错误:Maximum call stack size exceeded
return;
}
list.push(obj);
subObjCb(obj, parentObj, key);
// const drillThenCb = (obj: any, parentObj: any, key: any) => {
// innerDeep(obj, parentObj, key);
// subObjCb(obj, parentObj, key);
// };
if (Array.isArray(obj)) {
obj.forEach((item, idx) => {
innerDeep(item, obj, idx);
});
}
// TODO 处理 set
// if (isSet(obj)) {
// obj.forEach((item: any) => {
// subObjCb(item, obj, idx);
// });
// }
if (isMap(obj)) {
obj.forEach((value, key) => {
innerDeep(value, obj, key);
});
}
if (isObject(obj)) {
Object.keys(obj).forEach((key) => {
innerDeep(obj[key], obj, key);
});
}
};
innerDeep(obj, parentObj, key);
}
function getValStrDesc(val) {
// return Array.isArray(val) ? ARR_DESC : toString.call(val);
return toString.call(val);
}
function noop(...args) {
return args;
}
function isObject(val) {
// attention,null desc is '[object Null]'
return getValStrDesc(val) === OBJ_DESC;
}
function isMap(val) {
return getValStrDesc(val) === MAP_DESC;
}
function isSet(val) {
return getValStrDesc(val) === SET_DESC;
}
function isFn(val) {
return getValStrDesc(val) === FN_DESC;
}
function getDataType(dataNode) {
var strDesc = getValStrDesc(dataNode);
const dataType = desc2dataType[strDesc];
return dataType;
}
function isPrimitive(val) {
const desc = getValStrDesc(val);
return ![OBJ_DESC, ARR_DESC, MAP_DESC, SET_DESC, FN_DESC].includes(desc);
}
function isPromiseFn(obj) {
return obj.constructor.name === 'AsyncFunction' || 'function' === typeof obj.then;
}
function isPromiseResult(result) {
return typeof Promise !== 'undefined' && result instanceof Promise;
}
function canBeNum(val) {
var valType = typeof val;
if (valType === 'number')
return true;
if (valType === 'string')
return /^[0-9]*$/.test(val);
return false;
}
function isSymbol(maySymbol) {
return typeof maySymbol === 'symbol';
}
/**
* 是否已被 markRaw 标记
*/
function isMardedRaw(val) {
var _a;
if (!val)
return false;
return (_a = val[IS_RAW]) !== null && _a !== void 0 ? _a : false;
}
function getProxyVer(mayProxy) {
if (!mayProxy) {
return '';
}
return mayProxy[META_VER] || '';
}
function isPrevVerDraft(mayProxy, curVer) {
const ver = getProxyVer(mayProxy);
if (!ver) {
return false;
}
return ver !== curVer;
}
function deepCopy$1(obj) {
const innerDeep = (obj) => {
if (isPrimitive(obj)) {
return obj;
}
// TODO: 或许这里通过 metaVer 能够解决多引用问题 ( see 3.4.2 )
// TODO: 传入参数 reuse,确定是否走逻辑查看 meta.modified,并复用 meta.copy or meta.self
let newNode = obj;
if (Array.isArray(obj)) {
newNode = obj.slice();
newNode.forEach((item, idx) => {
newNode[idx] = innerDeep(item);
});
}
if (isSet(obj)) {
const tmpArr = Array.from(obj);
tmpArr.forEach((item, idx) => {
tmpArr[idx] = innerDeep(item);
});
newNode = new Set(tmpArr);
}
if (isMap(obj)) {
newNode = new Map(obj);
newNode.forEach((value, key) => {
newNode.set(key, innerDeep(value));
});
}
if (isObject(obj)) {
newNode = {};
Object.keys(obj).forEach((key) => {
newNode[key] = innerDeep(obj[key]);
});
}
return newNode;
};
return innerDeep(obj);
}
/**
* 尝试生成copy
* @param val
* @returns
*/
function tryMakeCopy(val, readOnly) {
if (readOnly) {
return val;
}
if (Array.isArray(val)) {
return val.slice();
}
let copy = val;
if (val && isObject(val)) {
copy = Object.assign({}, val);
}
if (isMap(val)) {
copy = new Map(val);
}
if (isSet(val)) {
copy = new Set(val);
}
return copy;
}
// 调用处已保证 meta 不为空
function mayMakeCopy(ori, options) {
// LABEL: SEE IF IMMUT HAS MEM LEAK
if (options.immutBase) {
return ori;
}
const copy = tryMakeCopy(ori, options.readOnly);
return copy;
}
function ensureStrKey(maySymbolKey) {
const key = maySymbolKey;
if (!isSymbol(maySymbolKey)) {
return maySymbolKey;
}
let symbolStr = symbolStrDict[key];
if (!symbolStr) {
symbolStr = genSymbolId();
symbolStrDict[key] = symbolStr;
}
return symbolStr;
}
function getKeyPathIdx(keyPaths, keyPath) {
const keyPathStrs = keyPaths.map((v) => v.join('|'));
const keyPathStr = keyPath.join('|');
return keyPathStrs.indexOf(keyPathStr);
}
function getKeyStrByPath(keyPath, ensureStr) {
let target = keyPath;
if (ensureStr) {
target = toKeyStrPath(keyPath, true);
}
return target.join('|');
}
/**
* in: ['a', 'b', Symbol]
* out: ['a', 'b', 'SID_1']
*/
function toKeyStrPath(keyPath, traversal) {
let keyStrPath = [];
if (traversal) {
keyPath.forEach((v) => {
const keyStr = ensureStrKey(v);
keyStrPath.push(keyStr);
});
return keyStrPath;
}
keyStrPath = keyPath.slice();
const lastIdx = keyPath.length - 1;
const lastKey = keyPath[lastIdx];
const keyStr = ensureStrKey(lastKey);
keyStrPath[lastIdx] = keyStr;
return keyStrPath;
}
/**
* out: ['a', 'b', 'SID_1']
* in: ['a', 'b', Symbol]
*/
function toKeyPath(keyStrPath) {
return keyStrPath.map((str) => strSymbolDict[str] || str);
}
function pushKeyPath(meta, keyPath, inputKeyStrPath) {
const { keyPaths, keyStrPaths } = meta;
const keyStrPath = inputKeyStrPath || toKeyStrPath(keyPath);
const idx = getKeyPathIdx(keyStrPaths, keyStrPath);
if (idx < 0) {
keyPaths.push(keyPath);
keyStrPaths.push(keyStrPath);
}
}
/**
* 删一条路径,并将 keyPath, keyStrPath 重新指向一个存在的路径
*/
function delKeyPath(meta) {
const { keyPaths, keyStrPaths, keyStrPath } = meta;
const idx = getKeyPathIdx(keyStrPaths, keyStrPath);
keyStrPaths.splice(idx, 1);
keyPaths.splice(idx, 1);
meta.keyPath = keyPaths[0];
meta.keyStrPath = keyStrPaths[0];
}
/**
* string 获取不到,尝试转为 number 获取
*/
function getMapVal(map, key) {
const strKeyVal = map.get(key);
if (strKeyVal !== undefined) {
return strKeyVal;
}
const numKeyVal = map.get(Number(key) || key);
if (numKeyVal !== undefined) {
return numKeyVal;
}
return undefined;
}
function getVal(obj, keyPath) {
let val;
let parent = obj;
const lastIdx = keyPath.length - 1;
let isGetted = true;
for (let i = 0; i <= lastIdx; i++) {
const key = keyPath[i];
if (!parent && i < lastIdx) {
isGetted = false;
break;
}
val = isMap(parent) ? getMapVal(parent, key) : parent[key];
parent = val;
}
return { val, isGetted };
}
function getValByKeyPaths(obj, keyPaths) {
let targetVal;
let isValGetted = false;
const lastIdx = keyPaths.length - 1;
for (let i = 0; i <= lastIdx; i++) {
const keyPath = keyPaths[i];
const { isGetted, val } = getVal(obj, keyPath);
if (isGetted) {
targetVal = val;
isValGetted = true;
break;
}
}
return { isGetted: isValGetted, val: targetVal };
}
function setVal(obj, keyPath, val) {
let parent = obj;
const lastIdx = keyPath.length - 1;
for (let i = 0; i <= lastIdx; i++) {
if (!parent) {
break;
}
const key = keyPath[i];
if (i === lastIdx) {
parent[key] = val;
break;
}
parent = isMap(parent) ? getMapVal(parent, key) : parent[key];
}
}
function setValByKeyPaths(obj, keyPaths, val) {
const lastIdx = keyPaths.length - 1;
for (let i = 0; i <= lastIdx; i++) {
const keyPath = keyPaths[i];
setVal(obj, keyPath, val);
}
}
/**
* in: keyPath [a,b,0,d,0] [[a,b,0], [info,0]]
* out: [[a,b,0,d,0], [info,0,d,0]]
*/
function intersectPath(keyPath, commonKeyPaths) {
const inputKeyStr = getKeyStrByPath(keyPath);
let restStr = '';
for (const tmpKeyPath of commonKeyPaths) {
const tmpKeyStr = getKeyStrByPath(tmpKeyPath, true);
const prefix = `${tmpKeyStr}|`;
if (inputKeyStr.startsWith(prefix)) {
restStr = inputKeyStr.substring(prefix.length);
break;
}
}
const newKeyPaths = [];
if (restStr) {
const subStrPaths = restStr.split('|');
const subPaths = toKeyPath(subStrPaths);
commonKeyPaths.forEach((keyPath) => {
newKeyPaths.push(keyPath.concat(subPaths));
});
}
return newKeyPaths;
}
/** multi ref data, sourceId 2 key */
/**
* multi ref data: sourceId 2 key dict
* key: srouceId, value: { fullKeyStr: [ [path1Arr], [path2Arr] ] }
*/
const MRDSid2KeyDict = new Map();
/**
* multi ref data: sourceId 2 all multi ref keys list
* key: sourceId, value: [ [ keyPath1, keyPath2 ], [ anotherKeyPath1, anotherKeyPath2 ] ],
* 数组里每个元素代表一个相同对象的多个引用路径
*/
const MRDSid2PathsList = new Map();
const finalDataSourceId = new WeakMap();
const ROOT_CTX = new Map();
function markModified(meta) {
meta.rootMeta.modified = true;
const doMark = (meta) => {
if (meta && !meta.modified) {
meta.modified = true;
doMark(meta.parentMeta);
}
};
doMark(meta);
}
function getKeyPath(draftNode, curKey, apiCtx) {
const pathArr = [curKey];
const meta = getSafeDraftMeta(draftNode, apiCtx);
if (meta && meta.level > 0) {
const { keyPath } = meta;
return [...keyPath, curKey];
}
return pathArr;
}
function newMeta(key, baseData, options) {
const { ver, parentMeta = null, immutBase, compareVer, apiCtx, hasOnOperate } = options;
const dataType = getDataType(baseData);
let sourceId = options.sourceId;
let keyPath = [];
let keyStrPath = [];
let arrKeyPath = [];
let arrKeyPaths = [];
let keyStrPaths = [];
let keyPaths = [];
const keyStr = ensureStrKey(key);
let level = 0;
let copy = null;
if (parentMeta) {
sourceId = parentMeta.sourceId;
copy = parentMeta.copy;
level = getNextMetaLevel(copy, apiCtx);
const isParentArr = parentMeta.selfType === ARRAY;
arrKeyPath = isParentArr ? parentMeta.keyPath.concat(key) : parentMeta.arrKeyPath;
keyPath = getKeyPath(copy, key, apiCtx);
keyStrPath = toKeyStrPath(keyPath);
// 尝试查出记录的多引用关系
let paths = [];
// 当前节点本身也是可由数组下标访问到的
if (parentMeta.arrKeyPath.length) {
const keyStr = getKeyStrByPath(parentMeta.arrKeyPath, true);
const parentPaths = getMultiRefPathsByKey(sourceId, keyStr);
paths = intersectPath(keyPath, parentPaths);
}
if (!paths.length) {
const { keyStrPathStr } = parentMeta;
const fullKeyStr = keyStrPathStr ? `${keyStrPathStr}|${keyStr}` : keyStr;
paths = getMultiRefPathsByKey(sourceId, fullKeyStr);
}
if (paths.length > 1) {
// draft 结束后再次创建草稿,发现了多引用路径
const { copy: rootRaw } = parentMeta.rootMeta;
const { val: curNode } = getVal(rootRaw, keyPath);
const toDelIdxList = [];
let isNodeParentArr = false;
const mayArrKeyPaths = [];
paths.forEach((keyPath, idx) => {
const { val: tmpNode } = getVal(rootRaw, keyPath);
if (!isNodeParentArr) {
const parentKeyPath = keyPath.slice(0, keyPath.length - 1);
const { val: parentNode } = getVal(rootRaw, parentKeyPath);
if (Array.isArray(parentNode)) {
// 只有有一条路径上确认了该节点是数组下标索引到节点,即可标识 isNodeParentArr 为 true
isNodeParentArr = true;
}
}
// 当前节点还是上一个版本的节点
if (tmpNode === curNode) {
keyPaths.push(keyPath);
keyStrPaths.push(toKeyStrPath(keyPath));
mayArrKeyPaths.push(keyPath);
}
else {
toDelIdxList.push(idx);
}
});
if (isNodeParentArr) {
arrKeyPaths = mayArrKeyPaths;
}
// 移除已不再是共同引用的路径记录
toDelIdxList.forEach((idx) => paths.splice(idx, 1));
}
else if (parentMeta.keyPaths.length > 0) {
// 父节点上可能有多路径,转移到子节点的 keyPaths keyStrPaths 里
parentMeta.keyPaths.forEach((keyPath) => {
const curKeyPath = keyPath.concat(key);
keyPaths.push(curKeyPath);
keyStrPaths.push(toKeyStrPath(curKeyPath));
});
}
else {
keyPaths = [keyPath];
keyStrPaths = [keyStrPath];
}
}
if (!arrKeyPath.length && arrKeyPaths.length) {
arrKeyPath = arrKeyPaths[0];
}
if (arrKeyPath.length && !arrKeyPaths.length) {
arrKeyPaths.push(arrKeyPath);
}
const keyStrPathStr = parentMeta ? `${parentMeta.keyStrPathStr}|${keyStr}` : keyStr;
const meta = {
id: genMetaId(),
sourceId,
// @ts-ignore add later
rootMeta: null,
parentMeta,
parent: copy,
selfType: dataType,
self: baseData,
// @ts-ignore add later
copy: null,
key,
keyStr,
keyPath,
keyStrPath,
keyStrPathStr,
keyPaths,
keyStrPaths,
arrKeyPath,
arrKeyPaths,
level,
// @ts-ignore add later
/** @type any */
proxyVal: null,
proxyItems: null,
modified: false,
scopes: [],
/**
* 当前对象 是否是一个一直可用的代理对象(不会被revoke),
* 服务于 immut 接口
*/
isImmutBase: immutBase,
isDel: false,
isArrOrderChanged: false,
newNodeStats: {},
newNodeMap: new Map(),
newNodes: [],
ver,
compareVer,
revoke: noop,
hasOnOperate,
execOnOperate: noop,
};
if (level === 0) {
meta.rootMeta = meta;
}
else {
// @ts-ignore 这里 parentMeta 一定有值
meta.rootMeta = parentMeta.rootMeta;
}
return meta;
}
/**
* 是否是一个当前版本对应的草稿对象代理节点
*/
function isDraft$1(mayDraft) {
if (!mayDraft) {
return false;
}
const meta = getDraftProxyMeta(mayDraft);
if (!meta) {
return false;
}
return !meta.isImmutBase;
}
function getNextMetaLevel(mayContainMetaObj, apiCtx) {
const meta = getDraftMetaByCtx(mayContainMetaObj, apiCtx);
return meta ? meta.level + 1 : 1;
}
function getSafeDraftMeta(proxyDraft, apiCtx) {
// @ts-ignore
return apiCtx.metaMap.get(proxyDraft);
}
function getDraftMetaByCtx(mayProxyDraft, apiCtx) {
if (!mayProxyDraft) {
return null;
}
if (apiCtx) {
return apiCtx.metaMap.get(mayProxyDraft) || null;
}
return getPrivateMeta(mayProxyDraft) || null;
}
function getDraftMeta(mayProxyDraft) {
if (!mayProxyDraft) {
return null;
}
return getPrivateMeta(mayProxyDraft) || null;
}
function getMetaVer(mayDraftProxy) {
return mayDraftProxy ? mayDraftProxy[META_VER] || '' : '';
}
function getDraftProxyMeta(mayDraftProxy) {
return getPrivateMeta(mayDraftProxy) || null;
}
/**
* 判断两个值是否相同,true 表示不相等,false 表示相等
*/
function isDiff$1(val1, val2) {
const meta1 = getDraftProxyMeta(val1);
const meta2 = getDraftProxyMeta(val2);
if (!meta1 && !meta2) {
return !Object.is(val1, val2);
}
const { self: self1, modified: modified1, compareVer: cv1, ver: ver1, level: level1, } = meta1 || { self: val1, modified: false, compareVer: false, ver: '0', level: 0 };
const { self: self2, modified: modified2, compareVer: cv2, ver: ver2, level: level2, } = meta2 || { self: val2, modified: false, compareVer: false, ver: '0', level: 0 };
if (self1 !== self2) {
// self 是内部维护的值,可不用 Object.is 判断
return true;
}
if ((cv1 || cv2) && (level1 === 0 || level2 === 0) && ver1 !== ver2) {
return true;
}
return modified1 || modified2;
}
/**
* 浅比较两个对象,除了专用于比较 helux 生成的代理对象,此函数还可以比较普通对象
* ```txt
* true:两个对象一样
* false:两个对象不一样
* ```
*/
function shallowCompare$1(prevObj, nextObj, compareLimuProxyRaw = true) {
const diffFn = compareLimuProxyRaw ? isDiff$1 : Object.is;
const isObjDiff = (a, b) => {
for (let i in a)
if (!(i in b))
return true;
for (let i in b)
if (diffFn(a[i], b[i]))
return true;
return false;
};
const isEqual = !isObjDiff(prevObj, nextObj);
return isEqual;
}
/**
* 内部调用,相信 proxyData 是 limu 产生的代理数据
*/
function getPrivateMeta(proxyData) {
return proxyData[PRIVATE_META];
}
function replaceMetaPartial(oldMeta, newMeta, key) {
newMeta.copy = oldMeta.copy;
newMeta.self = oldMeta.self;
newMeta.parentMeta[key] = oldMeta.self;
}
function getSourceId(rawData) {
return finalDataSourceId.get(rawData) || genSourceId();
}
function setSourceId(rawData, sourceId) {
return finalDataSourceId.set(rawData, sourceId);
}
function getMultiRefPathsDict(sourceId) {
// get 时不创建,避免额外性能损耗
return MRDSid2KeyDict.get(sourceId);
}
function setMultiRefPaths(sourceId, key, paths) {
let dict = MRDSid2KeyDict.get(sourceId);
if (!dict) {
dict = {};
MRDSid2KeyDict.set(sourceId, dict);
}
dict[key] = paths;
}
function getMultiRefPathsByKey(sourceId, key) {
const dict = getMultiRefPathsDict(sourceId);
// 不写为 (dict || {})[key] || [] , 因为此写法性能较差一些
if (!dict) {
return [];
}
return dict[key] || [];
}
function getMultiRefPaths(sourceId) {
const paths = MRDSid2PathsList.get(sourceId) || [];
return paths;
}
function clearMultiRefData(sourceId, toClearIdxList, toClearKeyStrList) {
const dict = MRDSid2KeyDict.get(sourceId);
if (dict) {
toClearKeyStrList.forEach((keyStr) => Reflect.deleteProperty(dict, keyStr));
}
const pathsList = MRDSid2PathsList.get(sourceId) || [];
// @ts-ignore
const newPathsList = pathsList.filter((v, idx) => !toClearIdxList.includes(idx));
MRDSid2PathsList.set(sourceId, newPathsList);
}
function recordMultiRefData(meta, keyStrs) {
const { sourceId, keyPaths } = meta;
keyStrs.forEach((keyStr) => setMultiRefPaths(sourceId, keyStr, keyPaths));
// [[1,2,3],[4,5,6]], [[a,b],[x]], ...
const pathsList = MRDSid2PathsList.get(sourceId) || [];
const keyStrsOfKP = keyPaths.map((keyPath) => getKeyStrByPath(keyPath, true));
let matched = false;
out: for (const paths of pathsList) {
for (const keyPath of paths) {
const curKeyStr = getKeyStrByPath(keyPath, true);
if (keyStrsOfKP.includes(curKeyStr)) {
const keyStrsOfPaths = paths.map((keyPath) => getKeyStrByPath(keyPath, true));
keyPaths.forEach((keyPath, idx) => {
if (!keyStrsOfPaths.includes(keyStrsOfKP[idx])) {
paths.push(keyPath);
}
});
matched = true;
break out;
}
}
}
if (!matched) {
pathsList.push(keyPaths);
}
MRDSid2PathsList.set(sourceId, pathsList);
}
/**
* 将某个草稿对象(代理对象)赋值到另一个地方,例如: draft.current = draft.list[0];
* 将重建路径连接关系
*/
function mayRelinkPath(key, parentMeta, valueMeta) {
let proxyVal = null;
const shouldRelink = valueMeta && valueMeta.parentMeta !== parentMeta;
if (!shouldRelink) {
return proxyVal;
}
const prevKeyPath = valueMeta.keyPath;
const newKeyPath = parentMeta.keyPath.concat(key);
const prevKeyStrPath = toKeyStrPath(prevKeyPath);
const newKeyStrPath = toKeyStrPath(newKeyPath);
const prevKeyStr = prevKeyStrPath.join('|');
const newKeyStr = newKeyStrPath.join('|');
if (prevKeyStr !== newKeyStr) {
// 发现一条新的路径指向当前 value,说明存在多引用
pushKeyPath(valueMeta, newKeyPath, newKeyStrPath);
recordMultiRefData(valueMeta, [prevKeyStr, newKeyStr]);
// if (valueMeta.modified) {
// let curKey = key;
// let curMeta = valueMeta;
// let curPMeta = parentMeta;
// do {
// curPMeta.copy[curKey] = curMeta.copy;
// curPMeta.modified = true;
// curKey = curPMeta.key;
// curMeta = curPMeta;
// // @ts-ignore
// curPMeta = curPMeta.parentMeta;
// } while (curPMeta);
// }
const modified = valueMeta.modified;
let curKey = key;
let curMeta = valueMeta;
let curPMeta = parentMeta;
do {
curPMeta.copy[curKey] = curMeta.copy;
curPMeta.modified = modified;
curKey = curPMeta.key;
curMeta = curPMeta;
// @ts-ignore
curPMeta = curPMeta.parentMeta;
} while (curPMeta);
proxyVal = valueMeta.proxyVal;
}
return proxyVal;
}
function ressignArrayItem(listMeta, itemMeta, ctx) {
const { copy, isArrOrderChanged } = listMeta;
const { targetNode, key } = ctx;
// 数组顺序已变化
if (isArrOrderChanged) {
// fix issue https://github.com/tnfe/limu/issues/13
// 元素经过 sort 后,可能已变成 proxy 对象,所以这里需要比较 copy 和 proxyVal
const index = copy.findIndex((item) => item === itemMeta.copy || item === itemMeta.proxyVal);
if (index >= 0) {
copy[index] = targetNode;
}
return;
}
copy[key] = targetNode;
}
function isInSameScope(mayDraftProxy, callerScopeVer) {
if (!isObject(mayDraftProxy)) {
return true;
}
return getMetaVer(mayDraftProxy) === callerScopeVer;
}
function clearScopes(rootMeta, apiCtx) {
const { metaMap } = apiCtx;
// TODO 下钻有一定的性能损耗,允许用户关闭此逻辑 findDraftNodeNewRef=false
const drilledMap = new Map();
apiCtx.newNodeMap.forEach((v) => {
const { node, parent, key } = v;
const drilledNode = drilledMap.get(node);
if (drilledNode) {
// 同一个节点被多个父级引用了,只需要指向自身即可,无需再次下钻
parent[key] = drilledNode;
return;
}
const item = v;
deepDrill(node, parent, key, (obj, parentObj, key) => {
const meta = getDraftMetaByCtx(obj, apiCtx);
if (meta) {
const { modified, copy, self } = meta;
const targetNode = !modified ? self : copy;
parentObj[key] = targetNode;
}
});
item.target = parent[key]; // 此处可能已被替换为真正的目标节点
drilledMap.set(node, item.target);
});
rootMeta.scopes.forEach((meta) => {
const { modified, copy, parentMeta, key, self, revoke, proxyVal, isDel } = meta;
// LABEL: WAIT TEST(2025-05-25)
const leaveScope = () => {
metaMap.delete(self);
metaMap.delete(proxyVal);
// 这里还不能将 copy 的映射删除,否则会照成以下错误,留给 js 引擎自己清理即可
// fail at test/api/setAutoRevoke.ts -> set autoRevoke when call createDraft › read subNode
// metaMap.delete(copy);
revoke();
};
if (!copy || !parentMeta)
return leaveScope();
const targetNode = !modified ? self : copy;
// 父节点是 Map、Set 时,parent 指向的是 ProxyItems,这里取到 copy 本体后再重新赋值
const parentCopy = parentMeta.copy;
const parentType = parentMeta.selfType;
if (parentType === MAP) {
parentCopy.set(key, targetNode);
return leaveScope();
}
if (parentType === SET) {
parentCopy.delete(proxyVal);
parentCopy.add(targetNode);
return leaveScope();
}
if (parentType === ARRAY) {
ressignArrayItem(parentMeta, meta, { targetNode, key });
return leaveScope();
}
if (isDel !== true) {
parentCopy[key] = targetNode;
return leaveScope();
}
});
rootMeta.scopes.length = 0;
}
function handleMultiRef(rootMeta, final) {
const keyPathsList = getMultiRefPaths(rootMeta.sourceId);
let idx = -1;
const toClearIdxList = [];
const toClearKeyStrList = [];
for (const keyPaths of keyPathsList) {
idx += 1;
let changedMeta = null;
let fixedMeta = null;
const results = [];
for (const keyPath of keyPaths) {
const { val } = getVal(rootMeta.proxyVal, keyPath);
const valMeta = getDraftMeta(val);
if (!valMeta)
continue;
if (valMeta.modified && !changedMeta) {
changedMeta = valMeta;
}
fixedMeta = valMeta;
results.push(valMeta.self);
}
// TODO 优化为分析所有 results,做部分清理,见 handleMultiRefV2
// prev: [ r1, r2, r3, r4, r5 ] 都一样
// now: [ r1, r2, r3 ] 一样,[ r4, r5 ] 一样
// 需要按照新的比较结果来记录新的,目前先实现简单的下标1和2比较
const isEq = results[0] === results[1];
if (!isEq) {
toClearIdxList.push(idx);
keyPaths.forEach((keyPath) => toClearKeyStrList.push(getKeyStrByPath(keyPath)));
}
else if (changedMeta) {
for (const keyPath of keyPaths) {
setVal(final, keyPath, changedMeta.copy);
}
}
else if (fixedMeta) {
for (const keyPath of keyPaths) {
setVal(final, keyPath, fixedMeta.self);
}
}
}
if (toClearIdxList.length) {
clearMultiRefData(rootMeta.sourceId, toClearIdxList, toClearKeyStrList);
}
}
function extractFinalData(rootMeta, apiCtx) {
const { self, copy, modified } = rootMeta;
let final = self;
// 有 copy 不一定有修改行为,这里需做双重判断
if (copy && modified) {
final = rootMeta.copy;
}
// 这里 handleMultiRef 和 clearScopes 顺序很重要,必须先处理多引用,再清理 scopes
handleMultiRef(rootMeta, final);
// if put this on first line, fail at test/set-other/update-object-item.ts
clearScopes(rootMeta, apiCtx);
return final;
}
function recordVerScope(meta) {
meta.rootMeta.scopes.push(meta);
}
function createScopedMeta(key, baseData, options) {
const { traps, immutBase, apiCtx, autoRevoke } = options;
// new meta data for current data node
const meta = newMeta(key, baseData, options);
const copy = mayMakeCopy(baseData, options);
meta.copy = copy;
const dataNodeTraps = Object.assign(Object.assign({}, traps), { get: (parent, key) => {
if (PRIVATE_META === key) {
return meta;
}
return traps.get(parent, key);
} });
if (immutBase) {
const ret = new Proxy(copy, dataNodeTraps);
meta.proxyVal = ret;
meta.revoke = noop;
}
else {
const ret = Proxy.revocable(copy, dataNodeTraps);
meta.proxyVal = ret.proxy;
meta.revoke = autoRevoke ? ret.revoke : noop;
}
apiCtx.metaMap.set(copy, meta);
apiCtx.metaMap.set(meta.proxyVal, meta);
apiCtx.metaMap.set(meta.self, meta);
return meta;
}
function shouldGenerateProxyItems(parentType, key) {
// !!! 对于 Array,直接生成 proxyItems
if (parentType === ARRAY)
return true;
const fnKeys = PROXYITEM_FNKEYS[parentType] || [];
return fnKeys.includes(key);
}
function getMayProxiedVal(val, options) {
const { key, parentMeta, parent, parentType, apiCtx } = options;
const mayCreateProxyVal = (val, inputKey) => {
const key = inputKey || '';
if (isPrimitive(val) || !val) {
return val;
}
if (!parentMeta) {
throw new Error('[[ createMeta ]]: meta should not be null');
}
if (!isFn(val)) {
if (
// 是一个全新的节点,不必生成代理,以便提高性能
parentMeta.newNodeStats[key] ||
// 已被 markRaw 标记,不需转为代理
val[IS_RAW]) {
return val;
}
let valMeta = getSafeDraftMeta(val, apiCtx);
// 惰性生成代理对象和其元数据
if (!valMeta) {
valMeta = createScopedMeta(key, val, options);
recordVerScope(valMeta);
// child value 指向 copy
if (parentMeta.selfType === MAP) {
parent.set(key, valMeta.copy);
}
else {
parent[key] = valMeta.copy;
}
}
return valMeta.proxyVal;
}
if (!shouldGenerateProxyItems(parentType, key)) {
return val;
}
if (parentMeta.proxyItems) {
return val;
}
// 提前完成遍历,为所有 item 生成代理
let proxyItems = [];
if (parentType === SET) {
const tmp = new Set();
parent.forEach((val) => tmp.add(mayCreateProxyVal(val)));
replaceSetOrMapMethods(tmp, parentMeta, {
dataType: SET,
apiCtx,
});
proxyItems = tmp;
// 区别于 2.0.2 版本,这里提前把 copy 指回来
parentMeta.copy = proxyItems;
}
else if (parentType === MAP) {
const tmp = new Map();
parent.forEach((val, key) => tmp.set(key, mayCreateProxyVal(val, key)));
replaceSetOrMapMethods(tmp, parentMeta, {
dataType: MAP,
apiCtx,
});
proxyItems = tmp;
// 区别于 2.0.2 版本,这里提前把copy指回来
parentMeta.copy = proxyItems;
}
else if (parentType === ARRAY && key !== 'sort') {
parentMeta.copy = parentMeta.copy || parent.slice();
proxyItems = parentMeta.proxyVal;
}
parentMeta.proxyItems = proxyItems;
return val;
};
return mayCreateProxyVal(val, key);
}
function getUnProxyValue(value, apiCtx) {
if (!isObject(value)) {
return value;
}
const valueMeta = getSafeDraftMeta(value, apiCtx);
if (!valueMeta)
return value;
return valueMeta.copy;
}
/**
* 拦截 set delete clear add
* 支持用户使用 callback 的第三位参数 (val, key, mapOrSet) 的 mapOrSet 当做 draft 使用
*/
function replaceSetOrMapMethods(mapOrSet, meta, options) {
const { dataType, apiCtx } = options;
// 拦截 set delete clear add,注意 set,add 在末尾判断后添加
// 支持用户使用 callback 的第三位参数 (val, key, map) 的 map 当做 draft 使用
const oriDel = mapOrSet.delete.bind(mapOrSet);
const oriClear = mapOrSet.clear.bind(mapOrSet);
mapOrSet.delete = function limuDelete(...args) {
markModified(meta);
return oriDel(...args);
};
mapOrSet.clear = function limuClear(...args) {
markModified(meta);
return oriClear(...args);
};
if (dataType === SET) {
const oriAdd = mapOrSet.add.bind(mapOrSet);
mapOrSet.add = function limuAdd(...args) {
markModified(meta);
return oriAdd(...args);
};
}
if (dataType === MAP) {
const oriSet = mapOrSet.set.bind(mapOrSet);
const oriGet = mapOrSet.get.bind(mapOrSet);
mapOrSet.set = function limuSet(...args) {
markModified(meta);
if (meta.hasOnOperate) {
const value = args[1];
meta.rootMeta.execOnOperate('set', args[0], { mayProxyVal: value, value, parentMeta: meta });
}
// @ts-ignore
return oriSet(...args);
};
mapOrSet.get = function limuGet(...args) {
const mayProxyVal = oriGet(...args);
if (meta.hasOnOperate) {
const draftMeta = getDraftMetaByCtx(mayProxyVal, apiCtx);
const value = draftMeta ? draftMeta.copy || draftMeta.self : mayProxyVal;
meta.rootMeta.execOnOperate('get', args[0], { mayProxyVal, value, parentMeta: meta, isChanged: false });
}
return mayProxyVal;
};
}
}
function mayMarkModified(options) {
const { calledBy, parentMeta, op, parentType } = options;
// 对于由 set 陷阱触发的 handleDataNode 调用,需要替换掉爷爷数据节点 key 指向的 value
if (['deleteProperty', 'set'].includes(calledBy) ||
(calledBy === 'get' &&
((parentType === SET && SHOULD_REASSIGN_SET_METHODS.includes(op)) || // 针对 Set.add
(parentType === ARRAY && SHOULD_REASSIGN_ARR_METHODS.includes(op)) || // 针对 Array 一系列的改变操作
(parentType === MAP && SHOULD_REASSIGN_MAP_METHODS.includes(op)))) // 针对 Map 一系列的改变操作
) {
markModified(parentMeta);
}
}
function getValPathKey(parentMeta, key) {
const pathCopy = parentMeta.keyPath.slice();
pathCopy.push(key);
const valPathKey = pathCopy.join('|');
return valPathKey;
}
function handleDataNode(parentDataNode, copyCtx) {
const { op, key, value: mayProxyValue, calledBy, parentType, parentMeta, apiCtx, isValueDraft, mayNewNode } = copyCtx;
/**
* 防止 value 本身就是一个 Proxy
* var draft_a1_b = draft.a1.b;
* draft.a2 = draft_a1_b;
*/
const value = getUnProxyValue(mayProxyValue, apiCtx);
/**
* 链路断裂,此对象未被代理
* // draft = { a: { b: { c: 1 } }};
* const newData = { n1: { n2: 2 } };
* draft.a = newData;
* draft.a.n1.n2 = 888; // 此时 n2_DataNode 是未代理对象
*/
if (!parentMeta) {
parentDataNode[key] = value;
return;
}
const { self, copy: parentCopy } = parentMeta;
mayMarkModified({ calledBy, parentMeta, op, key, parentType });
// 是 Map, Set, Array 类型的方法操作或者值获取
const fnKeys = CAREFUL_FNKEYS[parentType] || [];
// 是函数调用
if (isFn(mayProxyValue) && fnKeys.includes(op)) {
// slice 操作无需使用 copy,返回自身即可
if ('slice' === op) {
return self.slice;
}
if (CHANGE_ARR_ORDER_METHODS.includes(op)) {
parentMeta.isArrOrderChanged = true;
}
if (parentCopy) {
// 因为 Map 和 Set 里的对象不能直接操作修改,是通过 set 调用来修改的
// 所以无需 bind(parentDataNodeMeta.proxyVal), 否则会以下情况出现,
// Method Map.prototype.forEach called on incompatible receiver
// Method Set.prototype.forEach called on incompatible receiver
// see https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Errors/Called_on_incompatible_type
if (parentType === SET || parentType === MAP) {
// 注意 forEach 等方法已提前生成了 proxyItems,这里 bind 的目标优先取 proxyItems
return parentCopy[op].bind(parentCopy);
}
return parentCopy[op];
}
return self[op].bind(self);
}
if (!parentCopy) {
return value;
}
const oldValue = parentCopy[key];
const tryMarkDel = () => {
const oldValueMeta = getDraftMetaByCtx(oldValue, apiCtx);
oldValueMeta && (oldValueMeta.isDel = true);
};
const tryMarkUndel = () => {
const valueMeta = getDraftMetaByCtx(mayProxyValue, apiCtx);
if (valueMeta && valueMeta.isDel) {
valueMeta.isDel = false;
valueMeta.key = key;
valueMeta.keyPath = parentMeta.keyPath.concat([key]);
valueMeta.level = parentMeta.level + 1;
valueMeta.parent = parentMeta.copy;
valueMeta.parentMeta = parentMeta;
}
};
if (OP_DEL === op) {
const valueMeta = getDraftMetaByCtx(mayProxyValue, apiCtx);
// for test/complex/data-node-change case3
if (valueMeta) {
const { keyPaths } = valueMeta;
if (keyPaths.length === 1) {
valueMeta.isDel = true;
}
else {
// 存在多个路径指到当前对象时,调用 delKeyPath 删一条路径,并将 keyPath, keyStrPath 重新指向一个存在的路径
delKeyPath(valueMeta);
}
}
else {
// for test/complex/data-node-change (node-change 2)
tryMarkDel();
}
const val = parentCopy[key];
if (!isPrimitive(val)) {
apiCtx.newNodeMap.delete(getValPathKey(parentMeta, key));
}
delete parentCopy[key];
return;
}
// set 时非原始值都当做新节点记录下来
if (OP_SET === op && mayNewNode) {
if (!isValueDraft && !isPrimitive(value)) {
parentMeta.newNodeStats[key] = true;
apiCtx.newNodeMap.set(getValPathKey(parentMeta, key), { parent: parentCopy, node: value, key, target: null });
}
}
parentCopy[key] = value;
// 谨防是 a.b = { ... } ---> a.b = 1 的变异赋值方式
tryMarkDel();
tryMarkUndel();
}
function deepFreeze$1(obj) {
if (isPrimitive(obj)) {
return obj;
}
// @ts-ignore
if (Array.isArray(obj) && obj.length > 0) {
obj.forEach(deepFreeze$1);
return Object.freeze(obj);
}
if (isSet(obj)) {
const set = obj;
// TODO: throw error 'do not mutate' ?
set.add = () => set;
set.delete = () => false;
set.clear = noop;
// @ts-ignore
for (const item of set.values()) {
Object.freeze(item);
}
return Object.freeze(obj);
}
if (isMap(obj)) {
const map = obj;
// TODO: throw error 'do not mutate' ?
map.set = () => map;
map.delete = () => false;
map.clear = noop;
// @ts-ignore
for (const item of map.values()) {
Object.freeze(item);
}
return Object.freeze(obj);
}
// get all properties
const propertyNames = Object.getOwnPropertyNames(obj);
propertyNames.forEach((name) => {
// @ts-ignore
const value = obj[name];
deepFreeze$1(value);
});
return Object.freeze(obj);
}
// 可直接返回的属性
// 避免 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'];
const PBL_DICT = {}; // for perf
PROPERTIES_BLACK_LIST.forEach((item) => (PBL_DICT[item] = 1));
const TYPE_BLACK_DICT = { [ARRAY]: 1, [SET]: 1, [MAP]: 1 }; // for perf
const FINISH_HANDLER_MAP = new Map();
function buildLimuApis(options) {
var _a, _b, _c, _d, _e, _f, _g;
const opts = options || {};
const onOperate = opts.onOperate;
const hasOnOperate = !!onOperate;
const customKeys = opts.customKeys || [];
// @ts-ignore
const immutBase = (_a = opts[IMMUT_BASE]) !== null && _a !== void 0 ? _a : false;
const readOnly = (_b = opts.readOnly) !== null && _b !== void 0 ? _b : false;
const disableWarn = opts.disableWarn;
const compareVer = (_c = opts.compareVer) !== null && _c !== void 0 ? _c : false;
// 调用那一刻起,确定 autoFreeze 值
// allow user overwrite autoFreeze setting in current call process
const autoFreeze = (_d = opts.autoFreeze) !== null && _d !== void 0 ? _d : conf.autoFreeze;
const disableProxy = (_e = opts.disableProxy) !== null && _e !== void 0 ? _e : false;
let metaVer = '';
let isDraftFinished = false;
const apiCtx = { metaMap: new Map(), newNodeMap: new Map(), metaVer };
if (!disableProxy) {
metaVer = genMetaVer();
apiCtx.metaVer = metaVer;
ROOT_CTX.set(metaVer, apiCtx);
}
const autoRevoke = (_f = opts.autoRevoke) !== null && _f !== void 0 ? _f : conf.autoRevoke;
const silenceSetTrapErr = (_g = opts.silenceSetTrapErr) !== null && _g !== void 0 ? _g : true;
const logChangeFailed = (op, key) => {
console.warn(`${op} failed, cuase draft root has been finised! key:`, key);
return silenceSetTrapErr;
};
const logSetExpiredValFailed = (op, key) => {
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, key, options) => {
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 = isNotGet ? value : mayProxyVal;
if (!onOperate)
return { isChanged, replacedValue };
const parentMeta = (inputPMeta || {});
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) => {
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 结束后不能够被冻结( 见se