From 6966636dbe69e70f5036124be3f6b31043d5b74c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 24 Sep 2026 19:33:02 +0200 Subject: [PATCH] fix(chord): allow promise probes on settled drafts --- packages/chord/src/delta/tracker.ts | 16 ++++++---- .../chord/test/delta-tracker/tracker.test.ts | 29 +++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/packages/chord/src/delta/tracker.ts b/packages/chord/src/delta/tracker.ts index 703721031..0c4182071 100644 --- a/packages/chord/src/delta/tracker.ts +++ b/packages/chord/src/delta/tracker.ts @@ -388,6 +388,10 @@ const sharedObjectHandler: ProxyHandler = { throw new TypeError("Defining overlay properties is not supported"); }, get(target, property) { + if (property === "then") { + const node = (target as Record)[NODE] as OverlayNode | undefined; + if (node === undefined || isSettledContext(node.context)) return undefined; + } const node = nodeForTarget(target); if (property === NODE) return node; return getProperty(node, property); @@ -499,15 +503,17 @@ function nodeForTarget(target: object): OverlayNode { return node; } -function assertReadable(context: OverlayContext): void { - if ( +function isSettledContext(context: OverlayContext): boolean { + return ( context.overlayReleased || context.status.value === "consumed" || context.status.value === "aborted" || context.status.value === "stale" - ) { - throw new TypeError("Cannot use a settled overlay"); - } + ); +} + +function assertReadable(context: OverlayContext): void { + if (isSettledContext(context)) throw new TypeError("Cannot use a settled overlay"); } function assertWritable(context: OverlayContext): void { diff --git a/packages/chord/test/delta-tracker/tracker.test.ts b/packages/chord/test/delta-tracker/tracker.test.ts index 7b4d7808d..672e2f620 100644 --- a/packages/chord/test/delta-tracker/tracker.test.ts +++ b/packages/chord/test/delta-tracker/tracker.test.ts @@ -86,6 +86,35 @@ describe("delta tracker transactional overlay lifecycle", () => { tracker.adopt(prepared); }); + it("allows Promise resolution to inspect a settled draft without reporting a false failure", async () => { + const initial = JSON.parse('{"then":"document-value","value":1}') as { then: string; value: number }; + const tracker = track(initial); + const change = tracker.beginChange(); + const draft = change.state; + expect(draft.then).toBe("document-value"); + draft.value = 2; + const prepared = change.prepare(); + + const resolved = await Promise.resolve(draft); + expect(resolved).toBe(draft); + expect(resolved.then).toBeUndefined(); + expect(() => resolved.value).toThrow(/settled/); + + tracker.adopt(prepared); + expect(tracker.value.then).toBe("document-value"); + expect(tracker.value.value).toBe(2); + + const largeTracker = track({ rows: Array.from({ length: 4_100 }, (_, value) => ({ value })) }); + const largeChange = largeTracker.beginChange(); + const largeDraft = largeChange.state; + for (const row of largeDraft.rows) expect(row.value).toBeGreaterThanOrEqual(0); + largeDraft.rows[0]!.value = -1; + const largePrepared = largeChange.prepare(); + expect(await Promise.resolve(largeDraft)).toBe(largeDraft); + expect(() => largeDraft.rows).toThrow(/settled/); + largeTracker.adopt(largePrepared); + }); + it("aborts idempotently and revokes held descendants", () => { const tracker = track({ child: { value: 1 } }); const change = tracker.beginChange();