UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

588 lines (587 loc) 21.8 kB
function debugLog(...args) { const isBrowser = typeof window !== `undefined` && typeof localStorage !== `undefined`; if (isBrowser && localStorage.getItem(`DEBUG`) === `true`) { console.log(`[proxy]`, ...args); } else if ( // true !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]) ); } let count = 0; function getProxyCount() { count += 1; return count; } function createChangeProxy(target, parent) { const changeProxyCache = /* @__PURE__ */ new Map(); function memoizedCreateChangeProxy(innerTarget, innerParent) { debugLog(`Object ID:`, innerTarget.constructor.name); if (changeProxyCache.has(innerTarget)) { return changeProxyCache.get(innerTarget); } else { const changeProxy = createChangeProxy(innerTarget, innerParent); changeProxyCache.set(innerTarget, changeProxy); return changeProxy; } } const proxyCache = /* @__PURE__ */ new Map(); const changeTracker = { copy_: deepClone(target), originalObject: deepClone(target), proxyCount: getProxyCount(), modified: false, assigned_: {}, parent, target // Store reference to the target object }; debugLog( `createChangeProxy called for target`, target, changeTracker.proxyCount ); function markChanged(state) { if (!state.modified) { state.modified = true; } if (state.parent) { debugLog(`propagating change to parent`); if (`updateMap` in state.parent) { state.parent.updateMap(state.copy_); } else if (`updateSet` in state.parent) { state.parent.updateSet(state.copy_); } else { state.parent.tracker.copy_[state.parent.prop] = state.copy_; state.parent.tracker.assigned_[state.parent.prop] = true; } 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_[prop]; 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] === true) { const currentValue = state.copy_[sym]; const originalValue = state.originalObject[sym]; if (!deepEqual(currentValue, originalValue)) { debugLog(`Symbol property is different, returning false`); return false; } } else if (state.assigned_[sym] === false) { debugLog(`Symbol property was deleted, returning false`); return false; } } debugLog(`All properties match original values, returning true`); return true; } 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.assigned_ = {}; if (parentState.parent) { debugLog(`Continuing up the parent chain`); checkParentStatus(parentState.parent.tracker, parentState.parent.prop); } } } function createObjectProxy(obj) { debugLog(`createObjectProxy`, obj); if (proxyCache.has(obj)) { debugLog(`proxyCache found match`); return proxyCache.get(obj); } const proxy2 = new Proxy(obj, { get(ptarget, prop) { debugLog(`get`, ptarget, prop); const value = changeTracker.copy_[prop] ?? changeTracker.originalObject[prop]; const originalValue = changeTracker.originalObject[prop]; debugLog(`value (at top of proxy get)`, value); const desc = Object.getOwnPropertyDescriptor(ptarget, prop); if (desc == null ? void 0 : desc.get) { return value; } if (typeof value === `function`) { if (Array.isArray(ptarget)) { const methodName = prop.toString(); const modifyingMethods = /* @__PURE__ */ new Set([ `pop`, `push`, `shift`, `unshift`, `splice`, `sort`, `reverse`, `fill`, `copyWithin` ]); if (modifyingMethods.has(methodName)) { return function(...args) { const result = value.apply(changeTracker.copy_, args); markChanged(changeTracker); return result; }; } } 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(changeTracker.copy_, 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(changeTracker.copy_, 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; const valueToKeyMap = /* @__PURE__ */ new Map(); if (methodName === `values` && ptarget instanceof Map) { for (const [ key, mapValue ] of changeTracker.copy_.entries()) { valueToKeyMap.set(mapValue, key); } } const originalToModifiedMap = /* @__PURE__ */ new Map(); if (ptarget instanceof Set) { for (const setValue of changeTracker.copy_.values()) { originalToModifiedMap.set(setValue, setValue); } } 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 mapKey = nextResult.value[0]; const mapParent = { tracker: changeTracker, prop: mapKey, updateMap: (newValue) => { if (changeTracker.copy_ instanceof Map) { changeTracker.copy_.set(mapKey, newValue); } } }; const { proxy: valueProxy } = memoizedCreateChangeProxy( nextResult.value[1], mapParent ); nextResult.value[1] = valueProxy; } } else if (methodName === `values` || methodName === Symbol.iterator.toString() || prop === Symbol.iterator) { if (typeof nextResult.value === `object` && nextResult.value !== null) { if (methodName === `values` && ptarget instanceof Map) { const mapKey = valueToKeyMap.get(nextResult.value); if (mapKey !== void 0) { const mapParent = { tracker: changeTracker, prop: mapKey, updateMap: (newValue) => { if (changeTracker.copy_ instanceof Map) { changeTracker.copy_.set(mapKey, newValue); } } }; const { proxy: valueProxy } = memoizedCreateChangeProxy( nextResult.value, mapParent ); nextResult.value = valueProxy; } } else if (ptarget instanceof Set) { const setOriginalValue = nextResult.value; const setParent = { tracker: changeTracker, prop: setOriginalValue, // Use the original value as the prop updateSet: (newValue) => { if (changeTracker.copy_ instanceof Set) { changeTracker.copy_.delete(setOriginalValue); changeTracker.copy_.add(newValue); originalToModifiedMap.set( setOriginalValue, newValue ); } } }; const { proxy: valueProxy } = memoizedCreateChangeProxy( nextResult.value, setParent ); nextResult.value = valueProxy; } else { const tempKey = Symbol(`iterator-value`); const { proxy: valueProxy } = memoizedCreateChangeProxy(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 } = memoizedCreateChangeProxy( originalValue, nestedParent ); proxyCache.set(value, nestedProxy); return nestedProxy; } return value; }, set(_sobj, prop, value) { const currentValue = changeTracker.copy_[prop]; debugLog( `set called for property ${String(prop)}, current:`, currentValue, `new:`, value ); if (!deepEqual(currentValue, value)) { const originalValue = changeTracker.originalObject[prop]; const isRevertToOriginal = deepEqual(value, originalValue); debugLog( `value:`, value, `original:`, originalValue, `isRevertToOriginal:`, isRevertToOriginal ); if (isRevertToOriginal) { debugLog(`Reverting property ${String(prop)} to original value`); delete changeTracker.assigned_[prop.toString()]; 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.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)}`); changeTracker.copy_[prop] = value; changeTracker.assigned_[prop.toString()] = true; debugLog(`Marking object and ancestors as modified`, changeTracker); markChanged(changeTracker); } } else { debugLog(`Value unchanged, not tracking`); } return true; }, defineProperty(_ptarget, prop, descriptor) { if (`value` in descriptor) { changeTracker.copy_[prop] = deepClone(descriptor.value); changeTracker.assigned_[prop.toString()] = true; markChanged(changeTracker); } return true; }, deleteProperty(dobj, prop) { debugLog(`deleteProperty`, dobj, prop); const stringProp = typeof prop === `symbol` ? prop.toString() : prop; if (stringProp in dobj) { const hadPropertyInOriginal = stringProp in changeTracker.originalObject; delete changeTracker.copy_[prop]; if (!hadPropertyInOriginal) { delete changeTracker.copy_[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.copy_[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); debugLog(changeTracker); if (!changeTracker.modified) { debugLog(`Object not modified, returning empty object`); return {}; } if (typeof changeTracker.copy_ !== `object` || Array.isArray(changeTracker.copy_)) { return changeTracker.copy_; } if (Object.keys(changeTracker.assigned_).length === 0) { return changeTracker.copy_; } const result = {}; for (const key in changeTracker.copy_) { if (changeTracker.assigned_[key] === true && key in changeTracker.copy_) { result[key] = changeTracker.copy_[key]; } } debugLog(`Returning copy:`, result); return result; } }; } 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(); } export { createArrayChangeProxy, createChangeProxy, withArrayChangeTracking, withChangeTracking }; //# sourceMappingURL=proxy.js.map