Skip to content

Commit 7a4722a

Browse files
committed
fix(webapp): use getRequestAbortSignal() for dashboard stream routes
Three dashboard-scoped stream routes were passing request.signal into realtimeStream.streamResponse. That signal is broken under Remix+Express (see apps/webapp/CLAUDE.md, nodejs/node#55428 — the chain is severed when Remix internally clones the Request), so when a user closes their dashboard tab the signal never fires. The underlying RedisRealtimeStreams.streamResponse loops while(!signal.aborted) over XREAD BLOCK and only exits on its 15s inactivity timeout; the S2 path keeps the upstream fetch open for up to its 60s wait window. Thread getRequestAbortSignal() through: - resources/orgs/.../runs/$runParam/realtime/v1/streams/$runId/$streamId - resources/orgs/.../runs/$runParam/realtime/v1/streams/$runId/input/$streamId - resources/orgs/.../playground/realtime/v1/streams/$runId/$streamId Each picks up the Express res.on('close')-backed signal that fires reliably when the downstream client disconnects.
1 parent 8a1e9e9 commit 7a4722a

3 files changed

Lines changed: 31 additions & 9 deletions

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.playground.realtime.v1.streams.$runId.$streamId.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { $replica } from "~/db.server";
4+
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
45
import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server";
56
import { findProjectBySlug } from "~/models/project.server";
67
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
@@ -54,8 +55,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
5455

5556
const realtimeStream = getRealtimeStreamInstance(environment, run.realtimeStreamsVersion);
5657

57-
return realtimeStream.streamResponse(request, run.friendlyId, streamId, request.signal, {
58-
lastEventId,
59-
timeoutInSeconds,
60-
});
58+
// `request.signal` is severed by Remix's Request.clone() + Node undici GC bug
59+
// (see apps/webapp/CLAUDE.md). Use the Express res.on('close')-backed signal.
60+
return realtimeStream.streamResponse(
61+
request,
62+
run.friendlyId,
63+
streamId,
64+
getRequestAbortSignal(),
65+
{
66+
lastEventId,
67+
timeoutInSeconds,
68+
}
69+
);
6170
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.streams.$runId.$streamId.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { $replica } from "~/db.server";
4+
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
45
import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server";
56
import { findProjectBySlug } from "~/models/project.server";
67
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
@@ -72,8 +73,17 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
7273

7374
const realtimeStream = getRealtimeStreamInstance(environment, run.realtimeStreamsVersion);
7475

75-
return realtimeStream.streamResponse(request, run.friendlyId, streamId, request.signal, {
76-
lastEventId,
77-
timeoutInSeconds,
78-
});
76+
// `request.signal` is severed by Remix's Request.clone() + Node undici GC bug
77+
// (see apps/webapp/CLAUDE.md). Use the Express res.on('close')-backed signal so
78+
// the upstream stream fetch actually aborts when the user closes the tab.
79+
return realtimeStream.streamResponse(
80+
request,
81+
run.friendlyId,
82+
streamId,
83+
getRequestAbortSignal(),
84+
{
85+
lastEventId,
86+
timeoutInSeconds,
87+
}
88+
);
7989
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.realtime.v1.streams.$runId.input.$streamId.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
22
import { z } from "zod";
33
import { $replica } from "~/db.server";
4+
import { getRequestAbortSignal } from "~/services/httpAsyncStorage.server";
45
import { getRealtimeStreamInstance } from "~/services/realtime/v1StreamsGlobal.server";
56
import { findProjectBySlug } from "~/models/project.server";
67
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
@@ -74,11 +75,13 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
7475

7576
const realtimeStream = getRealtimeStreamInstance(environment, run.realtimeStreamsVersion);
7677

78+
// `request.signal` is severed by Remix's Request.clone() + Node undici GC bug
79+
// (see apps/webapp/CLAUDE.md). Use the Express res.on('close')-backed signal.
7780
return realtimeStream.streamResponse(
7881
request,
7982
run.friendlyId,
8083
`$trigger.input:${streamId}`,
81-
request.signal,
84+
getRequestAbortSignal(),
8285
{
8386
lastEventId,
8487
timeoutInSeconds,

0 commit comments

Comments
 (0)