Skip to content

Commit 2eb19be

Browse files
committed
fix(webapp): never resolve realtime streams v2 without S2 configured
The default-version branch returned `REALTIME_STREAMS_DEFAULT_VERSION` verbatim while the explicit branch checked that a basin and credentials were present. A deployment that set the default to v2 without configuring S2 therefore stamped runs v2, and every read and write against those runs' streams threw for the life of the run. Both paths now go through the same check, and an unsatisfiable v2 degrades to v1, which is a working backend.
1 parent 337dda1 commit 2eb19be

3 files changed

Lines changed: 87 additions & 4 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
Stop creating runs against a realtime streams backend the deployment cannot serve.

apps/webapp/app/services/realtime/v1StreamsGlobal.server.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,20 @@ function streamPrefixFor(environment: AuthenticatedEnvironment, basin: string):
9696
return segments.join("/");
9797
}
9898

99+
/**
100+
* Resolve the streams version to stamp on a run, falling back to
101+
* `REALTIME_STREAMS_DEFAULT_VERSION` when the caller expresses no preference.
102+
*
103+
* v2 is only ever returned when S2 is actually configured. A run stamped v2 on
104+
* a deployment without S2 is unusable: `getRealtimeStreamInstance` throws for
105+
* the life of the run, and no read or write against its streams can succeed.
106+
* v1 is a working backend, so an unsatisfiable v2 degrades to it.
107+
*/
99108
export function determineRealtimeStreamsVersion(streamVersion?: string): "v1" | "v2" {
100-
if (!streamVersion) {
101-
return env.REALTIME_STREAMS_DEFAULT_VERSION;
102-
}
109+
const requested = streamVersion ?? env.REALTIME_STREAMS_DEFAULT_VERSION;
103110

104111
if (
105-
streamVersion === "v2" &&
112+
requested === "v2" &&
106113
env.REALTIME_STREAMS_S2_BASIN &&
107114
(env.REALTIME_STREAMS_S2_ACCESS_TOKEN || env.REALTIME_STREAMS_S2_SKIP_ACCESS_TOKENS === "true")
108115
) {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const envMock = vi.hoisted(() => ({
4+
REALTIME_STREAMS_DEFAULT_VERSION: "v1" as "v1" | "v2",
5+
REALTIME_STREAMS_S2_BASIN: undefined as string | undefined,
6+
REALTIME_STREAMS_S2_ACCESS_TOKEN: undefined as string | undefined,
7+
REALTIME_STREAMS_S2_SKIP_ACCESS_TOKENS: "false",
8+
}));
9+
10+
vi.mock("~/env.server", () => ({ env: envMock }));
11+
12+
import { determineRealtimeStreamsVersion } from "~/services/realtime/v1StreamsGlobal.server";
13+
14+
function configureS2() {
15+
envMock.REALTIME_STREAMS_S2_BASIN = "a-basin";
16+
envMock.REALTIME_STREAMS_S2_ACCESS_TOKEN = "a-token";
17+
}
18+
19+
beforeEach(() => {
20+
envMock.REALTIME_STREAMS_DEFAULT_VERSION = "v1";
21+
envMock.REALTIME_STREAMS_S2_BASIN = undefined;
22+
envMock.REALTIME_STREAMS_S2_ACCESS_TOKEN = undefined;
23+
envMock.REALTIME_STREAMS_S2_SKIP_ACCESS_TOKENS = "false";
24+
});
25+
26+
describe("determineRealtimeStreamsVersion", () => {
27+
it("honours an explicit v2 when S2 is configured", () => {
28+
configureS2();
29+
expect(determineRealtimeStreamsVersion("v2")).toBe("v2");
30+
});
31+
32+
it("accepts a skip-tokens deployment as configured", () => {
33+
envMock.REALTIME_STREAMS_S2_BASIN = "a-basin";
34+
envMock.REALTIME_STREAMS_S2_SKIP_ACCESS_TOKENS = "true";
35+
expect(determineRealtimeStreamsVersion("v2")).toBe("v2");
36+
});
37+
38+
it("degrades an explicit v2 to v1 when S2 is not configured", () => {
39+
expect(determineRealtimeStreamsVersion("v2")).toBe("v1");
40+
});
41+
42+
it("falls back to the default version when the caller expresses no preference", () => {
43+
configureS2();
44+
envMock.REALTIME_STREAMS_DEFAULT_VERSION = "v2";
45+
expect(determineRealtimeStreamsVersion()).toBe("v2");
46+
});
47+
48+
it("degrades a v2 default to v1 when S2 is not configured", () => {
49+
envMock.REALTIME_STREAMS_DEFAULT_VERSION = "v2";
50+
expect(determineRealtimeStreamsVersion()).toBe("v1");
51+
});
52+
53+
it("requires a basin, not just a token", () => {
54+
envMock.REALTIME_STREAMS_S2_ACCESS_TOKEN = "a-token";
55+
envMock.REALTIME_STREAMS_DEFAULT_VERSION = "v2";
56+
expect(determineRealtimeStreamsVersion()).toBe("v1");
57+
expect(determineRealtimeStreamsVersion("v2")).toBe("v1");
58+
});
59+
60+
it("keeps an explicit v1 on v1 even where S2 is available", () => {
61+
configureS2();
62+
envMock.REALTIME_STREAMS_DEFAULT_VERSION = "v2";
63+
expect(determineRealtimeStreamsVersion("v1")).toBe("v1");
64+
});
65+
66+
it("treats an unrecognised version as v1", () => {
67+
configureS2();
68+
expect(determineRealtimeStreamsVersion("v3")).toBe("v1");
69+
});
70+
});

0 commit comments

Comments
 (0)