deep signals fix proxy object delete

feat/orm-diffs
Laurin Weger 1 day ago
parent 5d86f69c79
commit ead22c0c3c
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 41
      sdk/js/alien-deepsignals/src/deepSignal.ts
  2. 94
      sdk/js/alien-deepsignals/src/test/deepSignalOptions.test.ts

@ -258,6 +258,8 @@ export function getDeepSignalRootId(obj: any): symbol | undefined {
const proxyToSignals = new WeakMap();
// Raw object/array/Set -> stable proxy
const objToProxy = new WeakMap();
// Proxy -> raw object/array/Set (reverse lookup)
const proxyToRaw = new WeakMap();
// Raw array -> `$` meta proxy with index signals
const arrayToArrayOfSignals = new WeakMap();
// Objects already proxied or marked shallow
@ -381,22 +383,25 @@ export function setSetEntrySyntheticId(obj: object, id: string | number) {
}
const getSetEntryKey = (val: any): string | number => {
if (val && typeof val === "object") {
// If val is a proxy, get the raw object first
const rawVal = proxyToRaw.get(val) || val;
// First check for explicitly assigned synthetic ID
if (setObjectIds.has(val)) return setObjectIds.get(val)!;
if (setObjectIds.has(rawVal)) return setObjectIds.get(rawVal)!;
// Then check for @id property (primary identifier)
if (
typeof (val as any)["@id"] === "string" ||
typeof (val as any)["@id"] === "number"
typeof (rawVal as any)["@id"] === "string" ||
typeof (rawVal as any)["@id"] === "number"
)
return (val as any)["@id"];
return (rawVal as any)["@id"];
// Then check for id property (backward compatibility)
if (
typeof (val as any).id === "string" ||
typeof (val as any).id === "number"
typeof (rawVal as any).id === "string" ||
typeof (rawVal as any).id === "number"
)
return (val as any).id;
return (rawVal as any).id;
// Fall back to generating a blank node ID
return assignBlankNodeId(val);
return assignBlankNodeId(rawVal);
}
return val as any;
};
@ -451,6 +456,7 @@ export const deepSignal = <T extends object>(
// Pre-register an empty signals map so isDeepSignal() is true before any property access.
if (!proxyToSignals.has(proxy)) proxyToSignals.set(proxy, new Map());
objToProxy.set(obj, proxy);
proxyToRaw.set(proxy, obj);
}
return objToProxy.get(obj);
};
@ -523,6 +529,7 @@ function getFromSet(
childMeta.parent = receiver;
childMeta.key = synthetic;
objToProxy.set(entry, childProxy);
proxyToRaw.set(childProxy, entry);
return childProxy;
}
if (objToProxy.has(entry)) return objToProxy.get(entry);
@ -534,6 +541,16 @@ function getFromSet(
if (key === "add" || key === "delete" || key === "clear") {
const fn: Function = (raw as any)[key];
return function (this: any, ...args: any[]) {
// For delete, keep track of the original entry for patch emission
const originalEntry = key === "delete" ? args[0] : undefined;
// For delete, if the argument is a proxy, get the raw object for the actual Set operation
if (key === "delete" && args[0] && typeof args[0] === "object") {
const rawArg = proxyToRaw.get(args[0]);
if (rawArg) {
args = [rawArg];
}
}
const sizeBefore = raw.size;
const result = fn.apply(raw, args);
if (raw.size !== sizeBefore) {
@ -599,6 +616,7 @@ function getFromSet(
childMeta.parent = receiver;
childMeta.key = synthetic;
objToProxy.set(entryVal, childProxy);
proxyToRaw.set(childProxy, entryVal);
entryVal = childProxy;
}
// Set entry add: emit object vs primitive variant.
@ -621,7 +639,8 @@ function getFromSet(
});
}
} else if (key === "delete") {
const entry = args[0];
// Use the original entry (before proxy-to-raw conversion) for getting the synthetic key
const entry = originalEntry;
const synthetic = getSetEntryKey(entry);
// Check if entry is primitive or object
if (entry && typeof entry === "object") {
@ -828,6 +847,10 @@ const get =
if (target instanceof Set) {
return getFromSet(target as Set<any>, fullKey as any, receiver);
}
// Special case: accessing `$` on a non-array object returns the raw target
if (fullKey === "$" && !Array.isArray(target)) {
return target;
}
const norm = normalizeKey(target, fullKey, isArrayMeta, receiver);
if ((norm as any).shortCircuit) return (norm as any).shortCircuit; // returned meta proxy
const { key, returnSignal } = norm as {

@ -336,5 +336,99 @@ describe("deepSignal options", () => {
stop();
});
it("emits delete patch when removing objects with @id from Sets", async () => {
const options: DeepSignalOptions = {
addIdToObjects: true,
};
const state = deepSignal({ s: new Set<any>() }, options);
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
// Add objects with @id
const obj1 = { "@id": "obj-1", value: 1 };
const obj2 = { "@id": "obj-2", value: 2 };
const obj3 = { "@id": "obj-3", value: 3 };
state.s.add(obj1);
state.s.add(obj2);
state.s.add(obj3);
await Promise.resolve();
// Get the proxied objects from the Set
const proxiedObjs = Array.from(state.s);
const proxiedObj2 = proxiedObjs.find(
(o: any) => o["@id"] === "obj-2"
);
// Clear patches from additions
patches.length = 0;
// Delete one object using the proxied object
state.s.delete(proxiedObj2);
await Promise.resolve();
// Check that delete patch was emitted with correct path
const deletePaths = patches
.flat()
.filter((p) => p.op === "remove")
.map((p) => p.path.join("."));
expect(deletePaths).toContain("s.obj-2");
expect(deletePaths).not.toContain("s.obj-1");
expect(deletePaths).not.toContain("s.obj-3");
stop();
});
it("emits delete patches when removing objects without explicit @id from Sets", async () => {
const options: DeepSignalOptions = {
idGenerator: () =>
`gen-${Math.random().toString(36).substr(2, 9)}`,
addIdToObjects: true,
};
const state = deepSignal({ s: new Set<any>() }, options);
// Add objects without @id - they should get generated IDs
const obj1 = { value: 1 };
const obj2 = { value: 2 };
state.s.add(obj1);
state.s.add(obj2);
// Get the proxied objects and their generated IDs
const proxiedObjs = Array.from(state.s);
const proxiedObj1 = proxiedObjs[0];
const proxiedObj2 = proxiedObjs[1];
const id1 = (proxiedObj1 as any)["@id"];
const id2 = (proxiedObj2 as any)["@id"];
expect(id1).toBeDefined();
expect(id2).toBeDefined();
const patches: DeepPatch[][] = [];
const { stopListening: stop } = watch(state, ({ patches: batch }) =>
patches.push(batch)
);
// Delete one object using the proxied object
state.s.delete(proxiedObj1);
await Promise.resolve();
// Check that delete patch was emitted with the generated ID
const deletePaths = patches
.flat()
.filter((p) => p.op === "remove")
.map((p) => p.path.join("."));
expect(deletePaths).toContain(`s.${id1}`);
expect(deletePaths).not.toContain(`s.${id2}`);
stop();
});
});
});

Loading…
Cancel
Save