UNPKG

@legendapp/state

Version:
1,367 lines (1,359 loc) 67.5 kB
'use strict'; const hasOwnProperty = Object.prototype.hasOwnProperty; function isArray(obj) { return Array.isArray(obj); } function isString(obj) { return typeof obj === 'string'; } function isObject(obj) { return !!obj && typeof obj === 'object' && !isArray(obj); } function isFunction(obj) { return typeof obj === 'function'; } function isPrimitive(arg) { const type = typeof arg; return arg !== undefined && (isDate(arg) || (type !== 'object' && type !== 'function')); } function isDate(obj) { return obj instanceof Date; } function isSymbol(obj) { return typeof obj === 'symbol'; } function isBoolean(obj) { return typeof obj === 'boolean'; } function isPromise(obj) { return obj instanceof Promise; } function isEmpty(obj) { // Looping and returning false on the first property is faster than Object.keys(obj).length === 0 // https://jsbench.me/qfkqv692c8 if (!obj) return false; if (isArray(obj)) return obj.length === 0; for (const key in obj) { if (hasOwnProperty.call(obj, key)) { return false; } } return true; } const setPrimitives = new Set(['boolean', 'string', 'number']); /** @internal */ function isActualPrimitive(arg) { return setPrimitives.has(typeof arg); } /** @internal */ function isChildNodeValue(node) { return !!node.parent; } const symbolToPrimitive = Symbol.toPrimitive; const symbolGetNode = Symbol('getNode'); const symbolDelete = /* @__PURE__ */ Symbol('delete'); const symbolOpaque = Symbol('opaque'); const optimized = Symbol('optimized'); // TODOV3 Remove these const extraPrimitiveActivators = new Map(); const extraPrimitiveProps = new Map(); const globalState = { isLoadingLocal: false, isLoadingRemote: false, isMerging: false, }; function checkActivate(node) { var _a; const root = node.root; (_a = root.activate) === null || _a === void 0 ? void 0 : _a.call(root); if (root.toActivate) { root.toActivate.forEach(checkActivate); delete root.toActivate; } } function getNode(obs) { return obs && obs[symbolGetNode]; } function setNodeValue(node, newValue) { var _a; const parentNode = (_a = node.parent) !== null && _a !== void 0 ? _a : node; const key = node.parent ? node.key : '_'; const isDelete = newValue === symbolDelete; if (isDelete) newValue = undefined; // Get the value of the parent // const parentValue = isRoot ? node.root : ensureNodeValue(node); const parentValue = node.parent ? ensureNodeValue(parentNode) : parentNode.root; // Save the previous value first const prevValue = parentValue[key]; const isFunc = isFunction(newValue); // Compute newValue if newValue is a function or an observable newValue = !parentNode.isAssigning && isFunc ? newValue(prevValue) : isObject(newValue) && (newValue === null || newValue === void 0 ? void 0 : newValue[symbolGetNode]) ? newValue.peek() : newValue; try { parentNode.isSetting = (parentNode.isSetting || 0) + 1; // Save the new value if (isDelete) { delete parentValue[key]; } else { parentValue[key] = newValue; } } finally { parentNode.isSetting--; } if (parentNode.root.locked && parentNode.root.set) { parentNode.root.set(parentNode.root._); } return { prevValue, newValue, parentValue }; } const arrNodeKeys = []; function getNodeValue(node) { let count = 0; let n = node; while (isChildNodeValue(n)) { arrNodeKeys[count++] = n.key; n = n.parent; } let child = node.root._; for (let i = count - 1; child && i >= 0; i--) { const key = arrNodeKeys[i]; child = key !== 'size' && (child instanceof Map || child instanceof WeakMap) ? child.get(key) : child[key]; } return child; } function getChildNode(node, key) { var _a; // Get the child by key let child = (_a = node.children) === null || _a === void 0 ? void 0 : _a.get(key); // Create the child node if it doesn't already exist if (!child) { child = { root: node.root, parent: node, key, lazy: true, }; if (!node.children) { node.children = new Map(); } node.children.set(key, child); } return child; } function ensureNodeValue(node) { let value = getNodeValue(node); if (!value) { if (isChildNodeValue(node)) { const parent = ensureNodeValue(node.parent); value = parent[node.key] = {}; } else { value = node.root._ = {}; } } return value; } function findIDKey(obj, node) { let idKey = isObject(obj) ? 'id' in obj ? 'id' : 'key' in obj ? 'key' : '_id' in obj ? '_id' : '__id' in obj ? '__id' : undefined : undefined; if (!idKey && node.parent) { const keyExtractor = getNodeValue(node.parent)[node.key + '_keyExtractor']; if (keyExtractor && isFunction(keyExtractor)) { idKey = keyExtractor; } } return idKey; } function extractFunction(node, key, fnOrComputed, computedChildNode) { if (!node.functions) { node.functions = new Map(); } node.functions.set(key, fnOrComputed); if (computedChildNode) { computedChildNode.parentOther = getChildNode(node, key); if (!node.root.toActivate) { node.root.toActivate = []; } node.root.toActivate.push(computedChildNode); } } function isObservable(obs) { return obs && !!obs[symbolGetNode]; } function isEvent(obs) { var _a; return obs && ((_a = obs[symbolGetNode]) === null || _a === void 0 ? void 0 : _a.isEvent); } function computeSelector(selector, e, retainObservable) { let c = selector; if (isFunction(c)) { c = e ? c(e) : c(); } return isObservable(c) && !retainObservable ? c.get() : c; } function getObservableIndex(obs) { const node = getNode(obs); const n = +node.key; return n - n < 1 ? +n : -1; } function opaqueObject(value) { if (value) { value[symbolOpaque] = true; } return value; } function lockObservable(obs, value) { var _a; const root = (_a = getNode(obs)) === null || _a === void 0 ? void 0 : _a.root; if (root) { root.locked = value; } } function setAtPath(obj, path, pathTypes, value, fullObj, restore) { let o = obj; let oFull = fullObj; if (path.length > 0) { for (let i = 0; i < path.length; i++) { const p = path[i]; if (i === path.length - 1) { // Don't set if the value is the same. This prevents creating a new key // when setting undefined on an object without this key if (o[p] !== value) { o[p] = value; } } else if (o[p] === symbolDelete) { // If this was previously deleted, restore it if (oFull) { o[p] = oFull[p]; restore === null || restore === void 0 ? void 0 : restore(path.slice(0, i + 1), o[p]); } break; } else if (o[p] === undefined || o[p] === null) { o[p] = initializePathType(pathTypes[i]); } o = o[p]; if (oFull) { oFull = oFull[p]; } } } else { obj = value; } return obj; } function setInObservableAtPath(obs, path, pathTypes, value, mode) { let o = obs; let v = value; for (let i = 0; i < path.length; i++) { const p = path[i]; if (!o.peek()[p]) { o[p].set(initializePathType(pathTypes[i])); } o = o[p]; v = v[p]; } if (v === symbolDelete) { o.delete(); } // Assign if possible, or set otherwise else if (mode === 'assign' && o.assign && isObject(o.peek())) { o.assign(v); } else { o.set(v); } } function mergeIntoObservable(target, ...sources) { beginBatch(); globalState.isMerging = true; for (let i = 0; i < sources.length; i++) { target = _mergeIntoObservable(target, sources[i]); } globalState.isMerging = false; endBatch(); return target; } function _mergeIntoObservable(target, source) { var _a; const needsSet = isObservable(target); const targetValue = needsSet ? target.peek() : target; const isTargetArr = isArray(targetValue); const isTargetObj = !isTargetArr && isObject(targetValue); if ((isTargetObj && isObject(source) && !isEmpty(targetValue)) || (isTargetArr && isArray(source) && targetValue.length > 0)) { const keys = Object.keys(source); for (let i = 0; i < keys.length; i++) { const key = keys[i]; const sourceValue = source[key]; if (sourceValue === symbolDelete) { needsSet && ((_a = target[key]) === null || _a === void 0 ? void 0 : _a.delete) ? target[key].delete() : delete target[key]; } else { const isObj = isObject(sourceValue); const isArr = !isObj && isArray(sourceValue); const targetChild = target[key]; if ((isObj || isArr) && targetChild && (needsSet || !isEmpty(targetChild))) { if (!needsSet && (!targetChild || (isObj ? !isObject(targetChild) : !isArray(targetChild)))) { target[key] = sourceValue; } else { _mergeIntoObservable(targetChild, sourceValue); } } else { needsSet ? targetChild.set(sourceValue) : (target[key] = sourceValue); } } } } else if (source !== undefined) { needsSet ? target.set(source) : (target = source); } return target; } function constructObjectWithPath(path, pathTypes, value) { let out; if (path.length > 0) { let o = (out = {}); for (let i = 0; i < path.length; i++) { const p = path[i]; o[p] = i === path.length - 1 ? value : initializePathType(pathTypes[i]); o = o[p]; } } else { out = value; } return out; } function deconstructObjectWithPath(path, pathTypes, value) { let o = value; for (let i = 0; i < path.length; i++) { const p = path[i]; o = o ? o[p] : initializePathType(pathTypes[i]); } return o; } function isObservableValueReady(value) { return !!value && ((!isObject(value) && !isArray(value)) || !isEmpty(value)); } function setSilently(obs, newValue) { const node = getNode(obs); return setNodeValue(node, newValue).newValue; } function getPathType(value) { return isArray(value) ? 'array' : value instanceof Map ? 'map' : value instanceof Set ? 'set' : 'object'; } function initializePathType(pathType) { switch (pathType) { case 'array': return []; case 'object': return {}; case 'map': return new Map(); case 'set': return new Set(); } } function replacer(_, value) { if (value instanceof Map) { return { __LSType: 'Map', value: Array.from(value.entries()), // or with spread: value: [...value] }; } else if (value instanceof Set) { return { __LSType: 'Set', value: Array.from(value), // or with spread: value: [...value] }; } else { return value; } } function reviver(_, value) { if (typeof value === 'object' && value) { if (value.__LSType === 'Map') { return new Map(value.value); } else if (value.__LSType === 'Set') { return new Set(value.value); } } return value; } function safeStringify(value) { return JSON.stringify(value, replacer); } function safeParse(value) { return JSON.parse(value, reviver); } function clone(value) { return safeParse(safeStringify(value)); } let timeout; let numInBatch = 0; let isRunningBatch = false; let didDelayEndBatch = false; let _afterBatch = []; let _queuedBatches = []; let _batchMap = new Map(); function onActionTimeout() { if (_batchMap.size > 0) { if (process.env.NODE_ENV === 'development') { console.error('Forcibly completing observableBatcher because end() was never called. This may be due to an uncaught error between begin() and end().'); } endBatch(/*force*/ true); } } function isArraySubset(mainArr, subsetArr) { for (let i = 0; i < mainArr.length; i++) { if (mainArr[i] !== subsetArr[i]) { return false; } } return true; } function createPreviousHandlerInner(value, changes) { // Clones the current state and inject the previous data at the changed path let cloned = value ? clone(value) : {}; for (let i = 0; i < changes.length; i++) { const { path, prevAtPath } = changes[i]; let o = cloned; if (path.length > 0) { let i; for (i = 0; i < path.length - 1; i++) { o = o[path[i]]; } const key = path[i]; if (o instanceof Map) { o.set(key, prevAtPath); } else { o[key] = prevAtPath; } } else { cloned = prevAtPath; } } return cloned; } function createPreviousHandler(value, changes) { // Create a function that generates the previous state // We don't want to always do this because cloning is expensive // so it's better to run on demand. return function () { return createPreviousHandlerInner(value, changes); }; } function notify(node, value, prev, level, whenOptimizedOnlyIf) { // Run immediate listeners if there are any const changesInBatch = new Map(); computeChangesRecursive(changesInBatch, node, value, [], [], value, prev, /*immediate*/ true, level, whenOptimizedOnlyIf); batchNotifyChanges(changesInBatch, /*immediate*/ true); // Update the current batch const existing = _batchMap.get(node); if (existing) { existing.value = value; // TODO: level, whenOptimizedOnlyIf } else { _batchMap.set(node, { value, prev, level, whenOptimizedOnlyIf }); } // If not in a batch run it immediately if (numInBatch <= 0) { runBatch(); } } function computeChangesAtNode(changesInBatch, node, value, path, pathTypes, valueAtPath, prevAtPath, immediate, level, whenOptimizedOnlyIf) { // If there are listeners at this node compute the changes that need to be run if (immediate ? node.listenersImmediate : node.listeners) { const change = { path, pathTypes, valueAtPath, prevAtPath, }; const changeInBatch = changesInBatch.get(node); // If the node itself has been changed then we can ignore all the child changes if (changeInBatch && path.length > 0) { const { changes } = changeInBatch; if (!isArraySubset(changes[0].path, change.path)) { changes.push(change); } } else { changesInBatch.set(node, { level, value, whenOptimizedOnlyIf, changes: [change], }); } } } function computeChangesRecursive(changesInBatch, node, value, path, pathTypes, valueAtPath, prevAtPath, immediate, level, whenOptimizedOnlyIf) { // Do the compute at this node computeChangesAtNode(changesInBatch, node, value, path, pathTypes, valueAtPath, prevAtPath, immediate, level, whenOptimizedOnlyIf); if (node.linkedFromNodes) { for (const linkedFromNode of node.linkedFromNodes) { computeChangesAtNode(changesInBatch, linkedFromNode, value, path, pathTypes, valueAtPath, prevAtPath, immediate, level, whenOptimizedOnlyIf); } } // If not root notify up through parents if (node.parent) { const parent = node.parent; if (parent) { const parentValue = getNodeValue(parent); computeChangesRecursive(changesInBatch, parent, parentValue, [node.key].concat(path), [getPathType(value)].concat(pathTypes), valueAtPath, prevAtPath, immediate, level + 1, whenOptimizedOnlyIf); } } } function batchNotifyChanges(changesInBatch, immediate) { const listenersNotified = new Set(); // For each change in the batch, notify all of the listeners changesInBatch.forEach(({ changes, level, value, whenOptimizedOnlyIf }, node) => { const listeners = immediate ? node.listenersImmediate : node.listeners; if (listeners) { let listenerParams; // Need to convert to an array here instead of using a for...of loop because listeners can change while iterating const arr = Array.from(listeners); for (let i = 0; i < arr.length; i++) { const listenerFn = arr[i]; const { track, noArgs, listener } = listenerFn; if (!listenersNotified.has(listener)) { const ok = track === true ? level <= 0 : track === optimized ? whenOptimizedOnlyIf && level <= 0 : true; // Notify if listener is not shallow or if this is the first level if (ok) { // Create listenerParams if not already created if (!noArgs && !listenerParams) { listenerParams = { value, getPrevious: createPreviousHandler(value, changes), changes, }; } if (!track) { listenersNotified.add(listener); } listener(listenerParams); } } } } }); } function runBatch() { // Save batch locally and reset _batchMap first because a new batch could begin while looping over callbacks. // This can happen with observableComputed for example. const map = _batchMap; _batchMap = new Map(); const changesInBatch = new Map(); // First compute all of the changes at each node. It's important to do this first before // running all the notifications because createPreviousHandler depends on knowing // all of the changes happening at the node. map.forEach(({ value, prev, level, whenOptimizedOnlyIf }, node) => { computeChangesRecursive(changesInBatch, node, value, [], [], value, prev, false, level, whenOptimizedOnlyIf); }); // Once all changes are computed, notify all listeners for each node with the computed changes. batchNotifyChanges(changesInBatch, false); } function batch(fn, onComplete) { if (onComplete) { // If there's an onComplete we need a batch that's fully isolated from others to ensure it wraps only the given changes. // So if already batching, push this batch onto a queue and run it after the current batch is fully done. if (isRunningBatch) { _queuedBatches.push([fn, onComplete]); return; } else { _afterBatch.push(onComplete); } } beginBatch(); try { fn(); } finally { endBatch(); } } function beginBatch() { numInBatch++; if (!timeout) { timeout = setTimeout(onActionTimeout, 0); } } function endBatch(force) { numInBatch--; if (numInBatch <= 0 || force) { if (isRunningBatch) { // Don't want to run multiple endBatches recursively, so just note that an endBatch // was delayed so that the top level endBatch will run endBatch again after it's done. didDelayEndBatch = true; } else { if (timeout) { clearTimeout(timeout); timeout = undefined; } numInBatch = 0; // Save batch locally and reset _batch first because a new batch could begin while looping over callbacks. // This can happen with observableComputed for example. const after = _afterBatch; if (after.length) { _afterBatch = []; } isRunningBatch = true; runBatch(); isRunningBatch = false; // Run after functions at the end of this batch before running the next batch. // This needs to run before the delayed endBatch because the after functions need // to run before any side effects of the batch for (let i = 0; i < after.length; i++) { after[i](); } // If an endBatch was delayed run it now if (didDelayEndBatch) { didDelayEndBatch = false; endBatch(true); } const queued = _queuedBatches; if (queued.length) { _queuedBatches = []; for (let i = 0; i < queued.length; i++) { const [fn, onComplete] = queued[i]; batch(fn, onComplete); } } } } } function createObservable(value, makePrimitive, createObject, createPrimitive) { const valueIsPromise = isPromise(value); const root = { _: value, }; const node = { root, lazy: true, }; const prim = makePrimitive || isActualPrimitive(value); const obs = prim ? new createPrimitive(node) : createObject(node); if (valueIsPromise) { extractPromise(node, value); } return obs; } function onChange(node, callback, options = {}) { const { initial, immediate, noArgs } = options; const { trackingType } = options; let listeners = immediate ? node.listenersImmediate : node.listeners; if (!listeners) { listeners = new Set(); if (immediate) { node.listenersImmediate = listeners; } else { node.listeners = listeners; } } checkActivate(node); const listener = { listener: callback, track: trackingType, noArgs, }; listeners.add(listener); if (initial) { const value = getNodeValue(node); callback({ value, changes: [ { path: [], pathTypes: [], prevAtPath: value, valueAtPath: value, }, ], getPrevious: () => undefined, }); } return () => listeners.delete(listener); } let trackCount = 0; const trackingQueue = []; const tracking = { current: undefined, }; function beginTracking() { // Keep a copy of the previous tracking context so it can be restored // when this context is complete trackingQueue.push(tracking.current); trackCount++; tracking.current = {}; } function endTracking() { // Restore the previous tracking context trackCount--; if (trackCount < 0) { trackCount = 0; } tracking.current = trackingQueue.pop(); } function updateTracking(node, track) { if (trackCount) { const tracker = tracking.current; if (tracker) { if (!tracker.nodes) { tracker.nodes = new Map(); } const existing = tracker.nodes.get(node); if (existing) { existing.track = existing.track || track; existing.num++; } else { tracker.nodes.set(node, { node, track, num: 1 }); } } } } const ArrayModifiers = new Set([ 'copyWithin', 'fill', 'from', 'pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift', ]); const ArrayLoopers = new Set([ 'every', 'filter', 'find', 'findIndex', 'forEach', 'join', 'map', 'some', ]); const ArrayLoopersReturn = new Set(['filter', 'find']); const observableProperties = new Map(); const observableFns = new Map([ ['get', get], ['set', set], ['peek', peek], ['onChange', onChange], ['assign', assign], ['delete', deleteFn], ['toggle', toggle], ]); if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { // eslint-disable-next-line no-var var __devUpdateNodes = new Set(); } function collectionSetter(node, target, prop, ...args) { var _a; const prevValue = (isArray(target) && target.slice()) || target; const ret = target[prop].apply(target, args); if (node) { const hasParent = isChildNodeValue(node); const key = hasParent ? node.key : '_'; const parentValue = hasParent ? getNodeValue(node.parent) : node.root; // Set the object to the previous value first parentValue[key] = prevValue; // Then set with the new value so it notifies with the correct prevValue setKey((_a = node.parent) !== null && _a !== void 0 ? _a : node, key, target); } // Return the original value return ret; } function getKeys(obj, isArr, isMap) { return isArr ? undefined : obj ? (isMap ? Array.from(obj.keys()) : Object.keys(obj)) : []; } function updateNodes(parent, obj, prevValue) { var _a, _b; if ((process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && typeof __devUpdateNodes !== 'undefined' && isObject(obj)) { if (__devUpdateNodes.has(obj)) { console.error('[legend-state] Circular reference detected in object. You may want to use opaqueObject to stop traversing child nodes.', obj); return false; } __devUpdateNodes.add(obj); } if ((isObject(obj) && obj[symbolOpaque]) || (isObject(prevValue) && prevValue[symbolOpaque])) { const isDiff = obj !== prevValue; if (isDiff) { if (parent.listeners || parent.listenersImmediate) { notify(parent, obj, prevValue, 0); } } if ((process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && typeof __devUpdateNodes !== 'undefined' && obj !== undefined) { __devUpdateNodes.delete(obj); } return isDiff; } const isArr = isArray(obj); let prevChildrenById; let moved; const isMap = obj instanceof Map; const keys = getKeys(obj, isArr, isMap); const keysPrev = getKeys(prevValue, isArr, isMap); const length = ((_a = (keys || obj)) === null || _a === void 0 ? void 0 : _a.length) || 0; const lengthPrev = ((_b = (keysPrev || prevValue)) === null || _b === void 0 ? void 0 : _b.length) || 0; let idField; let isIdFieldFunction; let hasADiff = false; let retValue; if (isArr && isArray(prevValue)) { // Construct a map of previous indices for computing move if (prevValue.length > 0) { const firstPrevValue = prevValue[0]; if (firstPrevValue !== undefined) { idField = findIDKey(firstPrevValue, parent); if (idField) { isIdFieldFunction = isFunction(idField); prevChildrenById = new Map(); moved = []; const keysSeen = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test' ? new Set() : undefined; if (parent.children) { for (let i = 0; i < prevValue.length; i++) { const p = prevValue[i]; if (p) { const child = parent.children.get(i + ''); if (child) { const key = isIdFieldFunction ? idField(p) : p[idField]; if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { if (keysSeen.has(key)) { console.warn(`[legend-state] Warning: Multiple elements in array have the same ID. Key field: ${idField}, Array:`, prevValue); } keysSeen.add(key); } prevChildrenById.set(key, child); } } } } } } } } else if (prevValue && (!obj || isObject(obj))) { // For keys that have been removed from object, notify and update children recursively const lengthPrev = keysPrev.length; for (let i = 0; i < lengthPrev; i++) { const key = keysPrev[i]; if (!keys.includes(key)) { hasADiff = true; const child = getChildNode(parent, key); const prev = isMap ? prevValue.get(key) : prevValue[key]; if (prev !== undefined) { if (!isPrimitive(prev)) { updateNodes(child, undefined, prev); } if (child.listeners || child.listenersImmediate) { notify(child, undefined, prev, 0); } } } } } if (obj && !isPrimitive(obj)) { hasADiff = hasADiff || length !== lengthPrev; const isArrDiff = hasADiff; let didMove = false; for (let i = 0; i < length; i++) { const key = isArr ? i + '' : keys[i]; const value = isMap ? obj.get(key) : obj[key]; const prev = isMap ? prevValue === null || prevValue === void 0 ? void 0 : prevValue.get(key) : prevValue === null || prevValue === void 0 ? void 0 : prevValue[key]; let isDiff = value !== prev; if (isDiff) { const id = idField && value ? isIdFieldFunction ? idField(value) : value[idField] : undefined; let child = getChildNode(parent, key); // Detect moves within an array. Need to move the original proxy to the new position to keep // the proxy stable, so that listeners to this node will be unaffected by the array shift. if (isArr && id !== undefined) { // Find the previous position of this element in the array const prevChild = id !== undefined ? prevChildrenById === null || prevChildrenById === void 0 ? void 0 : prevChildrenById.get(id) : undefined; if (!prevChild) { // This id was not in the array before so it does not need to notify children isDiff = false; hasADiff = true; } else if (prevChild !== undefined && prevChild.key !== key) { const valuePrevChild = prevValue[prevChild.key]; // If array length changed then move the original node to the current position. // That should be faster than notifying every single element that // it's in a new position. if (isArrDiff) { child = prevChild; parent.children.delete(child.key); child.key = key; moved.push([key, child]); } didMove = true; // And check for diff against the previous value in the previous position isDiff = valuePrevChild !== value; } } if (isDiff) { // Array has a new / modified element // If object iterate through its children if (isPrimitive(value)) { hasADiff = true; } else { // Always need to updateNodes so we notify through all children const updatedNodes = updateNodes(child, value, prev); hasADiff = hasADiff || updatedNodes; } } if (isDiff || !isArrDiff) { // Notify for this child if this element is different and it has listeners // Or if the position changed in an array whose length did not change // But do not notify child if the parent is an array with changing length - // the array's listener will cover it if (child.listeners || child.listenersImmediate) { notify(child, value, prev, 0, !isArrDiff); } } } } if (moved) { for (let i = 0; i < moved.length; i++) { const [key, child] = moved[i]; parent.children.set(key, child); } } // The full array does not need to re-render if the length is the same // So don't notify shallow listeners retValue = hasADiff || didMove; } else if (prevValue !== undefined) { // If value got set to undefined, it has a diff retValue = true; } if ((process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && typeof __devUpdateNodes !== 'undefined' && obj !== undefined) { __devUpdateNodes.delete(obj); } return retValue !== null && retValue !== void 0 ? retValue : false; } function getProxy(node, p) { // Get the child node if p prop if (p !== undefined) node = getChildNode(node, p); // Create a proxy if not already cached and return it return (node.proxy || (node.proxy = new Proxy(node, proxyHandler))); } const proxyHandler = { get(node, p, receiver) { var _a; if (p === symbolToPrimitive) { throw new Error(process.env.NODE_ENV === 'development' ? '[legend-state] observable should not be used as a primitive. You may have forgotten to use .get() or .peek() to get the value of the observable.' : '[legend-state] observable is not a primitive.'); } if (p === symbolGetNode) { return node; } const value = peek(node); // If this node is linked to another observable then forward to the target's handler. // The exception is onChange because it needs to listen to this node for changes. // This needs to be below peek because it activates there. if (node.linkedToNode && p !== 'onChange') { return proxyHandler.get(node.linkedToNode, p, receiver); } if (value instanceof Map || value instanceof WeakMap || value instanceof Set || value instanceof WeakSet) { const ret = handlerMapSet(node, p, value); if (ret !== undefined) { return ret; } } const fn = observableFns.get(p); // If this is an observable function, call it if (fn) { return function (a, b, c) { const l = arguments.length; // Array call and apply are slow so micro-optimize this hot path. // The observable functions depends on the number of arguments so we have to // call it with the correct arguments, not just undefined switch (l) { case 0: return fn(node); case 1: return fn(node, a); case 2: return fn(node, a, b); default: return fn(node, a, b, c); } }; } if (node.isComputed) { if (node.proxyFn && !fn) { return node.proxyFn(p); } else { checkActivate(node); } } const property = observableProperties.get(p); if (property) { return property.get(node); } // TODOV3 Remove this const isValuePrimitive = isPrimitive(value); // If accessing a key that doesn't already exist, and this node has been activated with extra keys // then return the values that were set. This is used by enableLegendStateReact for example. if (value === undefined || value === null || isValuePrimitive) { if (extraPrimitiveProps.size && (node.isActivatedPrimitive || extraPrimitiveActivators.has(p))) { node.isActivatedPrimitive = true; const vPrim = extraPrimitiveProps.get(p); if (vPrim !== undefined) { return isFunction(vPrim) ? vPrim(getProxy(node)) : vPrim; } } } // /TODOV3 Remove this const vProp = value === null || value === void 0 ? void 0 : value[p]; if (isObject(value) && value[symbolOpaque]) { return vProp; } // Handle function calls if (isFunction(vProp)) { if (isArray(value)) { if (ArrayModifiers.has(p)) { // Call the wrapped modifier function return (...args) => collectionSetter(node, value, p, ...args); } else if (ArrayLoopers.has(p)) { // Update that this node was accessed for observers updateTracking(node); return function (cbOrig, thisArg) { // If callback needs to run on the observable proxies, use a wrapped callback function cbWrapped(_, index, array) { return cbOrig(getProxy(node, index + ''), index, array); } // If return value needs to be observable proxies, use our own looping logic and return the proxy when found if (ArrayLoopersReturn.has(p)) { const isFind = p === 'find'; const out = []; for (let i = 0; i < value.length; i++) { if (cbWrapped(value[i], i, value)) { const proxy = getProxy(node, i + ''); if (isFind) { return proxy; } else { out.push(proxy); } } } return isFind ? undefined : out; } else { return value[p](cbWrapped, thisArg); } }; } } // Return the function bound to the value return vProp.bind(value); } // Accessing primitive returns the raw value if (isPrimitive(vProp)) { // Update that this primitive node was accessed for observers if (isArray(value) && p === 'length') { updateTracking(node, true); // } else if (!isPrimitive(value)) { // updateTracking(getChildNode(node, p)); return vProp; } } const fnOrComputed = (_a = node.functions) === null || _a === void 0 ? void 0 : _a.get(p); if (fnOrComputed) { return fnOrComputed; } // TODOV3: Remove "state" if (vProp === undefined && (p === 'state' || p === '_state') && node.state) { return node.state; } // Return an observable proxy to the property return getProxy(node, p); }, // Forward all proxy properties to the target's value getPrototypeOf(node) { const value = getNodeValue(node); return value !== null && typeof value === 'object' ? Reflect.getPrototypeOf(value) : null; }, ownKeys(node) { const value = getNodeValue(node); if (isPrimitive(value)) return []; const keys = value ? Reflect.ownKeys(value) : []; // Update that this node was accessed for observers updateTracking(node, true); // This is required to fix this error: // TypeError: 'getOwnPropertyDescriptor' on proxy: trap reported non-configurability for // property 'length' which is either non-existent or configurable in the proxy node if (isArray(value) && keys[keys.length - 1] === 'length') { keys.splice(keys.length - 1, 1); } return keys; }, getOwnPropertyDescriptor(node, prop) { const value = getNodeValue(node); return !isPrimitive(value) ? Reflect.getOwnPropertyDescriptor(value, prop) : undefined; }, set(node, prop, value) { // If this assignment comes from within an observable function it's allowed if (node.isSetting) { return Reflect.set(node, prop, value); } if (node.isAssigning) { setKey(node, prop, value); return true; } const property = observableProperties.get(prop); if (property) { property.set(node, value); return true; } if (process.env.NODE_ENV === 'development') { console.warn('[legend-state]: Error: Cannot set a value directly:', prop, value); } return false; }, deleteProperty(node, prop) { // If this delete comes from within an observable function it's allowed if (node.isSetting) { return Reflect.deleteProperty(node, prop); } else { if (process.env.NODE_ENV === 'development') { console.warn('[legend-state]: Error: Cannot delete a value directly:', prop); } return false; } }, has(node, prop) { const value = getNodeValue(node); return Reflect.has(value, prop); }, }; function set(node, newValue) { if (node.parent) { return setKey(node.parent, node.key, newValue); } else { return setKey(node, '_', newValue); } } function toggle(node) { const value = getNodeValue(node); if (value === undefined || isBoolean(value)) { set(node, !value); return !value; } else if (process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') { throw new Error('[legend-state] Cannot toggle a non-boolean value'); } } function setKey(node, key, newValue, level) { if (process.env.NODE_ENV === 'development') { if (typeof HTMLElement !== 'undefined' && newValue instanceof HTMLElement) { console.warn(`[legend-state] Set an HTMLElement into state. You probably don't want to do that.`); } } if (node.root.locked && !node.root.set) { // This happens when modifying a locked observable such as a computed. // If merging this could be happening deep in a hierarchy so we don't want to throw errors so we'll just do nothing. // This could happen during persistence local load for example. if (globalState.isMerging) { return; } else { throw new Error(process.env.NODE_ENV === 'development' ? '[legend-state] Cannot modify an observable while it is locked. Please make sure that you unlock the observable before making changes.' : '[legend-state] Modified locked observable'); } } const isRoot = !node.parent && key === '_'; // Get the child node for updating and notifying const childNode = isRoot ? node : getChildNode(node, key); // Set the raw value on the parent object const { newValue: savedValue, prevValue, parentValue } = setNodeValue(childNode, newValue); const isFunc = isFunction(savedValue); const isPrim = isPrimitive(savedValue) || savedValue instanceof Date; if (savedValue !== prevValue) { updateNodesAndNotify(node, savedValue, prevValue, childNode, isPrim, isRoot, level); } extractFunctionOrComputed(node, parentValue, key, savedValue); return isFunc ? savedValue : isRoot ? getProxy(node) : getProxy(node, key); } function assign(node, value) { const proxy = getProxy(node); beginBatch(); if (isPrimitive(node.root._)) { node.root._ = {}; } // Set inAssign to allow setting on safe observables node.isAssigning = (node.isAssigning || 0) + 1; try { Object.assign(proxy, value); } finally { node.isAssigning--; } endBatch(); return proxy; } function deleteFn(node, key) { // If called without a key, delete by key from the parent node if (key === undefined && isChildNodeValue(node)) { key = node.key; node = node.parent; } setKey(node, key !== null && key !== void 0 ? key : '_', symbolDelete, /*level*/ -1); } function handlerMapSet(node, p, value) { const vProp = value === null || value === void 0 ? void 0 : value[p]; if (p === 'size') { return getProxy(node, p); } else if (isFunction(vProp)) { return function (a, b, c) { const l = arguments.length; const valueMap = value; if (p === 'get') { if (l > 0 && typeof a !== 'boolean' && a !== optimized) { return getProxy(node, a); } } else if (p === 'set') { if (l === 2) { const prev = valueMap.get(a); const ret = valueMap.set(a, b); if (prev !== b) { updateNodesAndNotify(getChildNode(node, a), b, prev); } return ret; } } else if (p === 'delete') { if (l > 0) { // Support Set by just returning a if it doesn't have get, meaning it's not a Map const prev = value.get ? valueMap.get(a) : a; const ret = value.delete(a); if (ret) { updateNodesAndNotify(getChildNode(node, a), undefined, prev); } return ret; } } else if (p === 'clear') { const prev = new Map(valueMap); const size = valueMap.size; valueMap.clear(); if (size) { updateNodesAndNotify(node, value, prev); } return; } else if (p === 'add') { const prev = new Set(value); const ret = value.add(a); if (!value.has(p)) { notify(node, ret, prev, 0); } return ret; } // TODO: This is duplicated from proxy handler, how to dedupe with best performance? const fn = observableFns.get(p); if (fn) { // Array call and apply are slow so micro-optimize this hot path. // The observable functions depends on the number of arguments so we have to // call it with the correct arguments, not just undefined switch (l) { case 0: return fn(node); case 1: return fn(node, a); case 2: return fn(node, a, b); default: return fn(node, a, b, c); } } else { return value[p](a, b); } }; } } function updateNodesAndNotify(node, newValue, prevValue, childNode, isPrim, isRoot, level) { if (!childNode) childNode = node; // Make sure we don't call too many listeners for ever property set beginBatch(); if (isPrim === undefined) { isPrim = isPrimitive(newValue); } let hasADiff = isPrim; let whenOptimizedOnlyIf = false; // If new value is an object or array update notify down the tree if (!isPrim || (prevValue && !isPrimitive(prevValue))) { if ((process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test') && typeof __devUpdateNodes !== 'undefined') { __devUpdateNodes.clear(); } hasADiff = updateNodes(childNode, newValue, prevValue); if (isArray(newValue)) { whenOptimizedOnlyIf = (newValue === null || newValue === void 0 ? void 0 : newValue.length) !== (prevValue ===