authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 17:08:54-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 17:08:54-07:00
log9c9ae3dc0e94a957d9c4a61bb211caf914828054
treedf8cc90df72f78bb64fffe9e2e4df1489c992e4b
parentc0ebc882b88dbcffd5a712e403b3c3c517ba4ea6
signature Signed by PGP key B5690EEEBB952194

fix(server): keep events during thread subscription startup (#9521)


2 files changed, 11 insertions(+), 5 deletions(-)

apps/server/src/server.test.ts+8-4
...@@ -7304,7 +7304,6 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -7304,7 +7304,6 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
7304 projectionSnapshotQuery: {7304 projectionSnapshotQuery: {
7305 getThreadDetailSnapshot: () =>7305 getThreadDetailSnapshot: () =>
7306 Effect.gen(function* () {7306 Effect.gen(function* () {
7307 yield* Effect.sleep("25 millis");
7308 yield* PubSub.publish(liveEvents, messageEvent);7307 yield* PubSub.publish(liveEvents, messageEvent);
7309 return Option.some({ snapshotSequence: 1, thread });7308 return Option.some({ snapshotSequence: 1, thread });
7310 }),7309 }),
...@@ -7317,14 +7316,19 @@ it.layer(NodeServices.layer)("server router seam", (it) => {...@@ -7317,14 +7316,19 @@ it.layer(NodeServices.layer)("server router seam", (it) => {
7317 withWsRpcClient(wsUrl, (client) =>7316 withWsRpcClient(wsUrl, (client) =>
7318 client[ORCHESTRATION_WS_METHODS.subscribeThread]({7317 client[ORCHESTRATION_WS_METHODS.subscribeThread]({
7319 threadId: defaultThreadId,7318 threadId: defaultThreadId,
7320 }).pipe(Stream.take(2), Stream.runCollect),7319 requestCompletionMarker: true,
7320 }).pipe(
7321 Stream.takeUntil((item) => item.kind === "synchronized"),
7322 Stream.runCollect,
7323 ),
7321 ),7324 ),
7322 ).pipe(Effect.timeout("2 seconds"));7325 );
73237326
7324 assert.equal(items[0]?.kind, "snapshot");7327 assert.equal(items[0]?.kind, "snapshot");
7325 assert.equal(items[1]?.kind, "event");7328 assert.equal(items[1]?.kind, "event");
7326 assert.equal(items[1]?.kind === "event" ? items[1].event.sequence : null, 2);7329 assert.equal(items[1]?.kind === "event" ? items[1].event.sequence : null, 2);
7327 }).pipe(Effect.provide(NodeHttpServer.layerTest), TestClock.withLive),7330 assert.equal(items[2]?.kind, "synchronized");
7331 }).pipe(Effect.provide(NodeHttpServer.layerTest)),
7328 );7332 );
73297333
7330 it.effect("coalesces buffered live tool updates to the latest state", () =>7334 it.effect("coalesces buffered live tool updates to the latest state", () =>
apps/server/src/ws.ts+3-1
...@@ -1549,7 +1549,9 @@ const makeWsRpcLayer = (...@@ -1549,7 +1549,9 @@ const makeWsRpcLayer = (
1549 // Attach live delivery before reading either replay or snapshot state.1549 // Attach live delivery before reading either replay or snapshot state.
1550 // Otherwise an event published while the snapshot is loading is lost.1550 // Otherwise an event published while the snapshot is loading is lost.
1551 const liveBuffer = yield* makeThreadLiveEventCoalescer();1551 const liveBuffer = yield* makeThreadLiveEventCoalescer();
1552 yield* Effect.forkScoped(liveStream.pipe(Stream.runForEach(liveBuffer.offer)));1552 yield* Effect.forkScoped(liveStream.pipe(Stream.runForEach(liveBuffer.offer)), {
1553 startImmediately: true,
1554 });
1553 const bufferedLiveStream = liveBuffer.stream;1555 const bufferedLiveStream = liveBuffer.stream;
15541556
1555 // When the client already loaded the snapshot over HTTP it passes1557 // When the client already loaded the snapshot over HTTP it passes