@tanstack/db
Version:
A reactive client store for building super fast apps on sync
588 lines (587 loc) • 22 kB
JavaScript
;
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 (
// 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();
}
exports.createArrayChangeProxy = createArrayChangeProxy;
exports.createChangeProxy = createChangeProxy;
exports.withArrayChangeTracking = withArrayChangeTracking;
exports.withChangeTracking = withChangeTracking;
//# sourceMappingURL=proxy.cjs.map