From 3beb0f63b897facba8fd25c75bdd36e04c4bbc7e Mon Sep 17 00:00:00 2001 From: Vance Ingalls Date: Sun, 30 Aug 2026 12:16:17 -0700 Subject: [PATCH] fix(core): mint guest-local runtime-data ids in a separate space from host ids --- packages/core/src/runtime/runtimeData.test.ts | 20 +++++++++++++++++++ packages/core/src/runtime/runtimeData.ts | 5 ++++- packages/core/src/runtime/window.d.ts | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/core/src/runtime/runtimeData.test.ts b/packages/core/src/runtime/runtimeData.test.ts index 254424d2a..856bcda7d 100644 --- a/packages/core/src/runtime/runtimeData.test.ts +++ b/packages/core/src/runtime/runtimeData.test.ts @@ -88,4 +88,24 @@ describe("runtime data registry", () => { expect(applied).toHaveBeenCalledTimes(1); }); + + it("never reports a composition-side delivery under a pending host request id", async () => { + const applied = vi.fn(); + setRuntimeDataAppliedReporter(applied); + const resolvers: Array<() => void> = []; + registerRuntimeDataHandler( + "captions", + () => new Promise((resolve) => resolvers.push(resolve)), + ); + + // The host mints id 1 and waits on it; the composition then calls the two-argument + // public form, which mints an id of its own. + setRuntimeData("captions", "first", 1); + setRuntimeData("captions", "latest"); + resolvers[1]?.(); + await vi.waitFor(() => expect(applied).toHaveBeenCalledTimes(1)); + + const [, reportedId] = applied.mock.calls[0] ?? []; + expect(reportedId).not.toBe(1); + }); }); diff --git a/packages/core/src/runtime/runtimeData.ts b/packages/core/src/runtime/runtimeData.ts index f97b21904..75b716f7b 100644 --- a/packages/core/src/runtime/runtimeData.ts +++ b/packages/core/src/runtime/runtimeData.ts @@ -28,7 +28,10 @@ function nextGeneration(channel: string): number { function resolveRequestId(requestId: number | undefined): number { if (typeof requestId === "number" && Number.isSafeInteger(requestId) && requestId > 0) return requestId; - localRequestId += 1; + // Guest-local ids count down so they can never collide with a host id, which is + // required above to be positive. A shared id space lets a composition-side call + // report `applied` under a host request's id while that host payload is still in flight. + localRequestId -= 1; return localRequestId; } diff --git a/packages/core/src/runtime/window.d.ts b/packages/core/src/runtime/window.d.ts index ce9ab530c..4c1163d85 100644 --- a/packages/core/src/runtime/window.d.ts +++ b/packages/core/src/runtime/window.d.ts @@ -35,7 +35,7 @@ declare global { channel: string, handler: (payload: unknown) => void, ) => () => void; - setRuntimeData?: (channel: string, payload: unknown) => void; + setRuntimeData?: (channel: string, payload: unknown, requestId?: number) => void; clearRuntimeData?: (channel: string) => void; [key: string]: unknown; };