@tanstack/optimistic
Version:
Core optimistic updates library
655 lines (654 loc) • 23.3 kB
JavaScript
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;
}
export {
createArrayChangeProxy,
createChangeProxy,
withArrayChangeTracking,
withChangeTracking
};
//# sourceMappingURL=proxy.js.map