UNPKG

@tanstack/optimistic

Version:

Core optimistic updates library

655 lines (654 loc) 23.5 kB
"use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); function debugLog(...args) { const isBrowser = typeof window !== `undefined` && typeof localStorage !== `undefined`; if (isBrowser && localStorage.getItem(`DEBUG`) === `true`) { console.log(`[proxy]`, ...args); } else if (!isBrowser && typeof process !== `undefined` && process.env.DEBUG === `true`) { console.log(`[proxy]`, ...args); } } function deepClone(obj, visited = /* @__PURE__ */ new WeakMap()) { if (obj === null || obj === void 0) { return obj; } if (typeof obj !== `object`) { return obj; } if (visited.has(obj)) { return visited.get(obj); } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof RegExp) { return new RegExp(obj.source, obj.flags); } if (Array.isArray(obj)) { const arrayClone = []; visited.set(obj, arrayClone); obj.forEach((item, index) => { arrayClone[index] = deepClone(item, visited); }); return arrayClone; } if (ArrayBuffer.isView(obj) && !(obj instanceof DataView)) { const TypedArrayConstructor = Object.getPrototypeOf(obj).constructor; const clone2 = new TypedArrayConstructor( obj.length ); visited.set(obj, clone2); for (let i = 0; i < obj.length; i++) { clone2[i] = obj[i]; } return clone2; } if (obj instanceof Map) { const clone2 = /* @__PURE__ */ new Map(); visited.set(obj, clone2); obj.forEach((value, key) => { clone2.set(key, deepClone(value, visited)); }); return clone2; } if (obj instanceof Set) { const clone2 = /* @__PURE__ */ new Set(); visited.set(obj, clone2); obj.forEach((value) => { clone2.add(deepClone(value, visited)); }); return clone2; } const clone = {}; visited.set(obj, clone); for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { clone[key] = deepClone( obj[key], visited ); } } const symbolProps = Object.getOwnPropertySymbols(obj); for (const sym of symbolProps) { clone[sym] = deepClone( obj[sym], visited ); } return clone; } function deepEqual(a, b) { if (a === b) return true; if (a === null || b === null || typeof a !== `object` || typeof b !== `object`) { return false; } if (a instanceof Date && b instanceof Date) { return a.getTime() === b.getTime(); } if (a instanceof RegExp && b instanceof RegExp) { return a.source === b.source && a.flags === b.flags; } if (a instanceof Map && b instanceof Map) { if (a.size !== b.size) return false; const entries = Array.from(a.entries()); for (const [key, val] of entries) { if (!b.has(key) || !deepEqual(val, b.get(key))) { return false; } } return true; } if (a instanceof Set && b instanceof Set) { if (a.size !== b.size) return false; const aValues = Array.from(a); const bValues = Array.from(b); if (aValues.every((val) => typeof val !== `object`)) { return aValues.every((val) => b.has(val)); } return aValues.length === bValues.length; } if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) { if (!deepEqual(a[i], b[i])) return false; } return true; } if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b) && !(a instanceof DataView) && !(b instanceof DataView)) { const typedA = a; const typedB = b; if (typedA.length !== typedB.length) return false; for (let i = 0; i < typedA.length; i++) { if (typedA[i] !== typedB[i]) return false; } return true; } const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every( (key) => Object.prototype.hasOwnProperty.call(b, key) && deepEqual(a[key], b[key]) ); } function createChangeProxy(target, parent) { const proxyCache = /* @__PURE__ */ new WeakMap(); const changeTracker = { changes: {}, originalObject: deepClone(target), // Create a deep clone to preserve the original state modified: false, assigned_: {}, parent, target // Store reference to the target object }; function markChanged(state) { if (!state.modified) { state.modified = true; if (state.parent) { markChanged(state.parent.tracker); } } } function checkIfReverted(state) { debugLog( `checkIfReverted called with assigned keys:`, Object.keys(state.assigned_) ); if (Object.keys(state.assigned_).length === 0 && Object.getOwnPropertySymbols(state.assigned_).length === 0) { debugLog(`No assigned properties, returning true`); return true; } for (const prop in state.assigned_) { if (state.assigned_[prop] === true) { const currentValue = state.copy_ ? state.copy_[prop] : null; const originalValue = state.originalObject[prop]; debugLog( `Checking property ${String(prop)}, current:`, currentValue, `original:`, originalValue ); if (!deepEqual(currentValue, originalValue)) { debugLog(`Property ${String(prop)} is different, returning false`); return false; } } else if (state.assigned_[prop] === false) { debugLog(`Property ${String(prop)} was deleted, returning false`); return false; } } const symbolProps = Object.getOwnPropertySymbols(state.assigned_); for (const sym of symbolProps) { if (state.assigned_[sym.toString()] === true) { const currentValue = state.copy_ ? state.copy_[sym] : null; const originalValue = state.originalObject[sym]; if (!deepEqual(currentValue, originalValue)) { debugLog(`Symbol property is different, returning false`); return false; } } else if (state.assigned_[sym.toString()] === false) { debugLog(`Symbol property was deleted, returning false`); return false; } } debugLog(`All properties match original values, returning true`); return true; } function updateModifiedStatus(state) { debugLog( `updateModifiedStatus called, assigned keys:`, Object.keys(state.assigned_) ); if (Object.keys(state.assigned_).length === 0 && Object.getOwnPropertySymbols(state.assigned_).length === 0) { debugLog(`No assigned properties, returning false`); return false; } const isReverted = checkIfReverted(state); debugLog(`checkIfReverted returned:`, isReverted); if (!isReverted) { debugLog(`Object has changes that aren't reverted, returning true`); return true; } debugLog(`All changes reverted, clearing tracking`); state.modified = false; state.changes = {}; state.assigned_ = {}; if (state.parent) { debugLog(`Checking parent status for prop:`, state.parent.prop); checkParentStatus(state.parent.tracker, state.parent.prop); } return false; } function checkParentStatus(parentState, childProp) { debugLog(`checkParentStatus called for child prop:`, childProp); const isReverted = checkIfReverted(parentState); debugLog(`Parent checkIfReverted returned:`, isReverted); if (isReverted) { debugLog(`Parent is fully reverted, clearing tracking`); parentState.modified = false; parentState.changes = {}; parentState.assigned_ = {}; if (parentState.parent) { debugLog(`Continuing up the parent chain`); checkParentStatus(parentState.parent.tracker, parentState.parent.prop); } } } function createObjectProxy(obj) { if (proxyCache.has(obj)) { return proxyCache.get(obj); } const proxy2 = new Proxy(obj, { get(ptarget, prop) { const value = ptarget[prop]; const desc = Object.getOwnPropertyDescriptor(ptarget, prop); if (desc == null ? void 0 : desc.get) { return value; } if (typeof value === `function`) { if (ptarget instanceof Map || ptarget instanceof Set) { const methodName = prop.toString(); const modifyingMethods = /* @__PURE__ */ new Set([ `set`, `delete`, `clear`, `add`, `pop`, `push`, `shift`, `unshift`, `splice`, `sort`, `reverse` ]); if (modifyingMethods.has(methodName)) { return function(...args) { const result = value.apply(ptarget, args); markChanged(changeTracker); return result; }; } const iteratorMethods = /* @__PURE__ */ new Set([ `entries`, `keys`, `values`, `forEach`, Symbol.iterator ]); if (iteratorMethods.has(methodName) || prop === Symbol.iterator) { return function(...args) { const result = value.apply(ptarget, args); if (methodName === `forEach`) { const callback = args[0]; if (typeof callback === `function`) { const wrappedCallback = function(value2, key, collection) { const cbresult = callback.call( this, value2, key, collection ); markChanged(changeTracker); return cbresult; }; return value.apply(ptarget, [ wrappedCallback, ...args.slice(1) ]); } } if (methodName === `entries` || methodName === `values` || methodName === Symbol.iterator.toString() || prop === Symbol.iterator) { const originalIterator = result; return { next() { const nextResult = originalIterator.next(); if (!nextResult.done && nextResult.value && typeof nextResult.value === `object`) { if (methodName === `entries` && Array.isArray(nextResult.value) && nextResult.value.length === 2) { if (nextResult.value[1] && typeof nextResult.value[1] === `object`) { const { proxy: valueProxy } = createChangeProxy( nextResult.value[1], { tracker: changeTracker, prop: typeof nextResult.value[0] === `symbol` ? nextResult.value[0] : String(nextResult.value[0]) } ); nextResult.value[1] = valueProxy; } } else if (methodName === `values` || methodName === Symbol.iterator.toString() || prop === Symbol.iterator) { if (typeof nextResult.value === `object` && nextResult.value !== null) { const tempKey = Symbol(`iterator-value`); const { proxy: valueProxy } = createChangeProxy( nextResult.value, { tracker: changeTracker, prop: tempKey } ); nextResult.value = valueProxy; } } } return nextResult; }, [Symbol.iterator]() { return this; } }; } return result; }; } } return value.bind(ptarget); } if (value && typeof value === `object` && !(value instanceof Date) && !(value instanceof RegExp)) { const nestedParent = { tracker: changeTracker, prop: String(prop) }; const { proxy: nestedProxy } = createChangeProxy(value, nestedParent); proxyCache.set(value, nestedProxy); return nestedProxy; } return value; }, set(sobj, prop, value) { const currentValue = sobj[prop]; debugLog( `set called for property ${String(prop)}, current:`, currentValue, `new:`, value ); if (Array.isArray(sobj) && prop === `length`) { const newLength = Number(value); const oldLength = sobj.length; const newArray = Array.from( { length: newLength }, (_, i) => i < oldLength ? sobj[i] : void 0 ); if (parent) { parent.tracker.changes[parent.prop] = newArray; parent.tracker.assigned_[parent.prop] = true; markChanged(parent.tracker); } sobj.length = newLength; return true; } if (!deepEqual(currentValue, value)) { const originalValue = changeTracker.originalObject[prop]; const isRevertToOriginal = deepEqual(value, originalValue); debugLog( `Value different, original:`, originalValue, `isRevertToOriginal:`, isRevertToOriginal ); if (isRevertToOriginal) { debugLog(`Reverting property ${String(prop)} to original value`); delete changeTracker.changes[prop.toString()]; delete changeTracker.assigned_[prop.toString()]; if (changeTracker.copy_) { debugLog(`Updating copy with original value for ${String(prop)}`); changeTracker.copy_[prop] = deepClone(originalValue); } debugLog(`Checking if all properties reverted`); const allReverted = checkIfReverted(changeTracker); debugLog(`All reverted:`, allReverted); if (allReverted) { debugLog(`All properties reverted, clearing tracking`); changeTracker.modified = false; changeTracker.changes = {}; changeTracker.assigned_ = {}; if (parent) { debugLog(`Updating parent for property:`, parent.prop); checkParentStatus(parent.tracker, parent.prop); } } else { debugLog(`Some properties still changed, keeping modified flag`); changeTracker.modified = true; } } else { debugLog(`Setting new value for property ${String(prop)}`); prepareCopy(changeTracker); if (changeTracker.copy_) { changeTracker.copy_[prop] = value; } obj[prop] = value; changeTracker.assigned_[prop.toString()] = true; changeTracker.changes[prop.toString()] = deepClone(value); debugLog(`Marking object and ancestors as modified`); markChanged(changeTracker); } } else { debugLog(`Value unchanged, not tracking`); } return true; }, defineProperty(ptarget, prop, descriptor) { const result = Reflect.defineProperty(ptarget, prop, descriptor); if (result) { if (`value` in descriptor) { changeTracker.changes[prop.toString()] = deepClone(descriptor.value); changeTracker.assigned_[prop.toString()] = true; markChanged(changeTracker); } } return result; }, setPrototypeOf(ptarget, proto) { return Object.setPrototypeOf(ptarget, proto); }, deleteProperty(dobj, prop) { const stringProp = typeof prop === `symbol` ? prop.toString() : prop; if (stringProp in dobj) { const hadPropertyInOriginal = stringProp in changeTracker.originalObject; prepareCopy(changeTracker); if (changeTracker.copy_) { delete changeTracker.copy_[prop]; } delete dobj[prop]; if (!hadPropertyInOriginal) { delete changeTracker.changes[stringProp]; delete changeTracker.assigned_[stringProp]; if (Object.keys(changeTracker.assigned_).length === 0 && Object.getOwnPropertySymbols(changeTracker.assigned_).length === 0) { changeTracker.modified = false; } else { changeTracker.modified = true; } } else { changeTracker.assigned_[stringProp] = false; changeTracker.changes[stringProp] = void 0; markChanged(changeTracker); } } return true; } }); proxyCache.set(obj, proxy2); return proxy2; } const proxy = createObjectProxy(target); return { proxy, getChanges: () => { debugLog( `getChanges called, modified:`, changeTracker.modified, `assigned keys:`, Object.keys(changeTracker.assigned_) ); if (!changeTracker.modified) { debugLog(`Object not modified, returning empty object`); return {}; } if (Object.keys(changeTracker.assigned_).length === 0 && Object.getOwnPropertySymbols(changeTracker.assigned_).length === 0) { debugLog(`No assigned properties, checking deep equality`); if (changeTracker.copy_) { debugLog(`Comparing copy with original`); if (deepEqual(changeTracker.copy_, changeTracker.originalObject)) { debugLog(`Copy equals original, returning empty object`); changeTracker.modified = false; return {}; } } else if (deepEqual(target, changeTracker.originalObject)) { debugLog(`Target equals original, returning empty object`); changeTracker.modified = false; changeTracker.changes = {}; changeTracker.assigned_ = {}; return {}; } } debugLog(`Forcing full check for reverted state`); updateModifiedStatus(changeTracker); if (!changeTracker.modified) { debugLog(`No longer modified after check, returning empty object`); return {}; } if (changeTracker.modified) { const objToCheck = changeTracker.copy_ || target; debugLog( `Checking if object is equal to original:`, objToCheck, changeTracker.originalObject ); if (deepEqual(objToCheck, changeTracker.originalObject)) { debugLog(`Object equals original, returning empty object`); changeTracker.modified = false; changeTracker.changes = {}; changeTracker.assigned_ = {}; return {}; } } if (Object.keys(changeTracker.assigned_).length > 0 || Object.getOwnPropertySymbols(changeTracker.assigned_).length > 0) { if (changeTracker.copy_) { const changes = {}; for (const key in changeTracker.assigned_) { if (changeTracker.assigned_[key] === true) { changes[key] = deepClone(changeTracker.copy_[key]); } else if (changeTracker.assigned_[key] === false) { changes[key] = void 0; } } const symbolProps = Object.getOwnPropertySymbols( changeTracker.assigned_ ); for (const sym of symbolProps) { if (changeTracker.assigned_[sym.toString()] === true) { const value = changeTracker.copy_[sym]; changes[sym.toString()] = deepClone(value); } } return changes; } return changeTracker.changes; } if (changeTracker.modified && !parent) { debugLog(`Root object with nested changes, checking deep equality`); const currentState = changeTracker.copy_ || target; debugLog( `Comparing current state with original:`, currentState, changeTracker.originalObject ); if (deepEqual(currentState, changeTracker.originalObject)) { debugLog(`Current state equals original, returning empty object`); changeTracker.modified = false; return {}; } debugLog( `Comparing target with original:`, target, changeTracker.originalObject ); if (deepEqual(target, changeTracker.originalObject)) { debugLog(`Target equals original, returning empty object`); changeTracker.modified = false; changeTracker.changes = {}; changeTracker.assigned_ = {}; return {}; } if (typeof target === `object` && target !== null) { let allNestedReverted = true; for (const key in target) { if (Object.prototype.hasOwnProperty.call(target, key)) { const currentValue = target[key]; const originalValue = changeTracker.originalObject[key]; if (!deepEqual(currentValue, originalValue)) { allNestedReverted = false; break; } } } if (allNestedReverted) { debugLog( `All nested properties match original values, returning empty object` ); changeTracker.modified = false; changeTracker.changes = {}; changeTracker.assigned_ = {}; return {}; } } debugLog( `Changes detected, returning full object:`, changeTracker.copy_ || target ); const result = changeTracker.copy_ || target; return result; } debugLog(`No changes detected, returning empty object`); return {}; } }; } function createArrayChangeProxy(targets) { const proxiesWithChanges = targets.map((target) => createChangeProxy(target)); return { proxies: proxiesWithChanges.map((p) => p.proxy), getChanges: () => proxiesWithChanges.map((p) => p.getChanges()) }; } function withChangeTracking(target, callback) { const { proxy, getChanges } = createChangeProxy(target); callback(proxy); return getChanges(); } function withArrayChangeTracking(targets, callback) { const { proxies, getChanges } = createArrayChangeProxy(targets); callback(proxies); return getChanges(); } function prepareCopy(state) { if (!state.copy_) { state.copy_ = shallowCopy(state.originalObject); } } function shallowCopy(obj) { if (Array.isArray(obj)) { return [...obj]; } if (obj instanceof Map) { return new Map(obj); } if (obj instanceof Set) { return new Set(obj); } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof RegExp) { return new RegExp(obj.source, obj.flags); } if (obj !== null && typeof obj === `object`) { return { ...obj }; } return obj; } exports.createArrayChangeProxy = createArrayChangeProxy; exports.createChangeProxy = createChangeProxy; exports.withArrayChangeTracking = withArrayChangeTracking; exports.withChangeTracking = withChangeTracking; //# sourceMappingURL=proxy.cjs.map