Skip to content

Commit 1867bf7

Browse files
authored
fix(runner): inline data URIs before handling write redirects (#1911)
fix(runner): inline data URIs before handling write redirects Move data URI inlining logic to occur before write redirect (alias) handling in normalizeAndDiff. This ensures the correct evaluation order when a data URI contains a redirect link. Previously, when newValue was a data URI containing a redirect, the code would: 1. Check currentValue for redirects (line 290) 2. Follow currentValue redirect if present 3. Process data URI inlining later (line 318) This caused the redirect within the data URI to be written to the wrong location - it would follow the currentValue redirect first, then inline the data URI, resulting in the redirect being written to the destination of the currentValue alias instead of to the intended target. The fix reorders the logic so data URIs are inlined immediately after unwrapping proxies and cells (line 212), before any redirect handling. This preserves the invariant that redirect handling at line 290 only applies when newValue is not itself an alias. The order is now: 1. Unwrap proxies/cells 2. Inline data URIs (exposing any contained redirects) 3. Handle redirects in newValue 4. Handle redirects in currentValue This ensures redirects contained in data URIs are properly recognized and handled as part of newValue, not incorrectly written through a currentValue redirect.
1 parent 43a43e7 commit 1867bf7

File tree

2 files changed

+151
-70
lines changed

2 files changed

+151
-70
lines changed

packages/runner/src/data-updating.ts

Lines changed: 76 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ import {
2424
import { type IRuntime } from "./runtime.ts";
2525
import { toURI } from "./uri-utils.ts";
2626

27+
const diffLogger = getLogger("normalizeAndDiff", {
28+
enabled: false,
29+
level: "debug",
30+
});
31+
2732
/**
2833
* Traverses newValue and updates `current` and any relevant linked documents.
2934
*
@@ -86,34 +91,6 @@ type ChangeSet = {
8691
* @param context - The context of the change.
8792
* @returns An array of changes that should be written.
8893
*/
89-
const diffLogger = getLogger("normalizeAndDiff", {
90-
enabled: false,
91-
level: "debug",
92-
});
93-
94-
/**
95-
* Returns true if `target` is the immediate parent of `base` in the same document.
96-
*
97-
* Example:
98-
* - base.path = ["internal", "__#1", "next"]
99-
* - target.path = ["internal", "__#1"]
100-
*
101-
* This is used to decide when to collapse a self/parent link that would create
102-
* a tight self-loop (e.g., obj.next -> obj) while allowing references to
103-
* higher ancestors (like an item's `items` pointing to its containing array).
104-
*/
105-
function isImmediateParent(
106-
target: NormalizedFullLink,
107-
base: NormalizedFullLink,
108-
): boolean {
109-
return (
110-
target.id === base.id &&
111-
target.space === base.space &&
112-
target.path.length === base.path.length - 1 &&
113-
target.path.every((seg, i) => seg === base.path[i])
114-
);
115-
}
116-
11794
export function normalizeAndDiff(
11895
runtime: IRuntime,
11996
tx: IExtendedStorageTransaction,
@@ -232,6 +209,54 @@ export function normalizeAndDiff(
232209
newValue = newValue.getAsLink();
233210
}
234211

212+
// Check for links that are data: URIs and inline them, by calling
213+
// normalizeAndDiff on the contents of the link.
214+
if (isLink(newValue)) {
215+
const parsedLink = parseLink(newValue, link);
216+
if (parsedLink.id.startsWith("data:")) {
217+
diffLogger.debug(() =>
218+
`[BRANCH_CELL_LINK] Data link detected, treating as contents at path=${pathStr}`
219+
);
220+
// Use the tx code to make sure we read it the same way
221+
let dataValue: any = tx.readValueOrThrow({
222+
...parsedLink,
223+
path: [],
224+
}, options);
225+
const path = [...parsedLink.path];
226+
// If there is a link on the way to `path`, follow it, appending remaining
227+
// path to the target link.
228+
for (;;) {
229+
if (isAnyCellLink(dataValue)) {
230+
const dataLink = parseLink(dataValue, parsedLink);
231+
dataValue = createSigilLinkFromParsedLink({
232+
...dataLink,
233+
path: [...dataLink.path, ...path],
234+
});
235+
break;
236+
}
237+
if (path.length > 0) {
238+
if (isRecord(dataValue)) {
239+
dataValue = dataValue[path.shift()!];
240+
} else {
241+
dataValue = undefined;
242+
break;
243+
}
244+
} else {
245+
break;
246+
}
247+
}
248+
return normalizeAndDiff(
249+
runtime,
250+
tx,
251+
link,
252+
dataValue,
253+
context,
254+
options,
255+
seen,
256+
);
257+
}
258+
}
259+
235260
// If we're about to create a reference to ourselves, no-op
236261
if (areMaybeLinkAndNormalizedLinkSame(newValue, link)) {
237262
diffLogger.debug(() =>
@@ -318,48 +343,6 @@ export function normalizeAndDiff(
318343
seen,
319344
);
320345
}
321-
if (parsedLink.id.startsWith("data:")) {
322-
diffLogger.debug(() =>
323-
`[BRANCH_CELL_LINK] Data link detected, treating as contents at path=${pathStr}`
324-
);
325-
// If there is a data link treat it as writing it's contents instead.
326-
327-
// Use the tx code to make sure we read it the same way
328-
let dataValue: any = tx.readValueOrThrow({
329-
...parsedLink,
330-
path: [],
331-
}, options);
332-
const path = [...parsedLink.path];
333-
for (;;) {
334-
if (isAnyCellLink(dataValue)) {
335-
const dataLink = parseLink(dataValue, parsedLink);
336-
dataValue = createSigilLinkFromParsedLink({
337-
...dataLink,
338-
path: [...dataLink.path, ...path],
339-
});
340-
break;
341-
}
342-
if (path.length > 0) {
343-
if (isRecord(dataValue)) {
344-
dataValue = dataValue[path.shift()!];
345-
} else {
346-
dataValue = undefined;
347-
break;
348-
}
349-
} else {
350-
break;
351-
}
352-
}
353-
return normalizeAndDiff(
354-
runtime,
355-
tx,
356-
link,
357-
dataValue,
358-
context,
359-
options,
360-
seen,
361-
);
362-
}
363346
if (
364347
isAnyCellLink(currentValue) &&
365348
areLinksSame(newValue, currentValue, link)
@@ -634,3 +617,26 @@ export function addCommonIDfromObjectID(
634617

635618
traverse(obj);
636619
}
620+
621+
/**
622+
* Returns true if `target` is the immediate parent of `base` in the same document.
623+
*
624+
* Example:
625+
* - base.path = ["internal", "__#1", "next"]
626+
* - target.path = ["internal", "__#1"]
627+
*
628+
* This is used to decide when to collapse a self/parent link that would create
629+
* a tight self-loop (e.g., obj.next -> obj) while allowing references to
630+
* higher ancestors (like an item's `items` pointing to its containing array).
631+
*/
632+
function isImmediateParent(
633+
target: NormalizedFullLink,
634+
base: NormalizedFullLink,
635+
): boolean {
636+
return (
637+
target.id === base.id &&
638+
target.space === base.space &&
639+
target.path.length === base.path.length - 1 &&
640+
target.path.every((seg, i) => seg === base.path[i])
641+
);
642+
}

packages/runner/test/data-updating.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
areNormalizedLinksSame,
1414
createSigilLinkFromParsedLink,
1515
isAnyCellLink,
16+
isSigilLink,
1617
parseLink,
1718
} from "../src/link-utils.ts";
1819
import { type IExtendedStorageTransaction } from "../src/storage/interface.ts";
@@ -1095,6 +1096,80 @@ describe("data-updating", () => {
10951096
expect(value.result).toBe(100);
10961097
});
10971098

1099+
it("should inline data URI containing redirect without writing redirect to wrong location", () => {
1100+
// Setup: Create two separate cells - source and destination
1101+
const sourceCell = runtime.getCell<{ value: number }>(
1102+
space,
1103+
"data URI redirect source value",
1104+
undefined,
1105+
tx,
1106+
);
1107+
sourceCell.set({ value: 99 });
1108+
1109+
const destinationCell = runtime.getCell<{ value: number }>(
1110+
space,
1111+
"data URI redirect destination value",
1112+
undefined,
1113+
tx,
1114+
);
1115+
destinationCell.set({ value: 42 });
1116+
1117+
// Create a data URI that contains a redirect pointing to sourceCell
1118+
const redirectAlias = sourceCell.key("value").getAsWriteRedirectLink();
1119+
const dataCell = runtime.getImmutableCell<any>(
1120+
space,
1121+
redirectAlias,
1122+
undefined,
1123+
tx,
1124+
);
1125+
1126+
// Create a target cell that currently has an alias to destinationCell
1127+
const targetCell = runtime.getCell<{ result: any }>(
1128+
space,
1129+
"data URI redirect target cell",
1130+
undefined,
1131+
tx,
1132+
);
1133+
targetCell.setRaw({
1134+
result: destinationCell.key("value").getAsWriteRedirectLink(),
1135+
});
1136+
1137+
const current = targetCell.key("result").getAsNormalizedFullLink();
1138+
1139+
// Write the data cell (which contains a redirect to sourceCell) to the target
1140+
// Before the fix: data URI was not inlined early enough, and the redirect
1141+
// would be written to destinationCell.value instead of target.result
1142+
// After the fix: data URI is inlined first, exposing the redirect, which is
1143+
// then properly written to target.result
1144+
const changes = normalizeAndDiff(
1145+
runtime,
1146+
tx,
1147+
current,
1148+
dataCell.getAsLink(),
1149+
);
1150+
1151+
// Should write the new redirect to target Cell.result
1152+
// Note: The change writes a redirect (alias object with $alias key)
1153+
expect(changes.length).toBe(1);
1154+
expect(changes[0].location.id).toBe(
1155+
targetCell.getAsNormalizedFullLink().id,
1156+
);
1157+
expect(changes[0].location.path).toEqual(["result"]);
1158+
// The value should be the redirect link
1159+
expect(isSigilLink(changes[0].value)).toBe(true);
1160+
const parsedLink = parseLink(changes[0].value);
1161+
expect(parsedLink?.overwrite).toBe("redirect");
1162+
1163+
applyChangeSet(tx, changes);
1164+
1165+
// Verify that targetCell now points to sourceCell's value (99), not destinationCell's (42)
1166+
expect(targetCell.get().result).toBe(99);
1167+
1168+
// Verify neither source nor destination cells were modified
1169+
expect(sourceCell.get()).toEqual({ value: 99 });
1170+
expect(destinationCell.get()).toEqual({ value: 42 });
1171+
});
1172+
10981173
describe("addCommonIDfromObjectID", () => {
10991174
it("should handle arrays", () => {
11001175
const obj = { items: [{ id: "item1", name: "First Item" }] };

0 commit comments

Comments
 (0)