Skip to content

Commit 3cc80db

Browse files
committed
Merge branch 'w1-inf-05-electric-removal-realtime-client-backend-collapse' into integration-batch-01
2 parents c9c89db + 8c0f694 commit 3cc80db

15 files changed

Lines changed: 61 additions & 2030 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
area: webapp
3+
type: improvement
4+
---
5+
6+
Collapse the realtime run feed to the native backend only and remove the Electric-SQL HTTP proxy path. `resolveRealtimeStreamClient` now returns the native realtime client unconditionally, so the per-org `realtimeBackend` flag, the `REALTIME_BACKEND_NATIVE_ENABLED` master switch, and the shadow-compare scaffolding are no longer consulted for serving runs. The Electric proxy `RealtimeClient` class, its singleton, the shadow client/comparator, and the orphaned Electric-proxy integration test are removed. The run-feed path no longer constructs Electric shape URLs or reads `ELECTRIC_ORIGIN`, so native realtime is not pinned to the soon-abandoned RDS DB via Electric shapes. Applies to self-host too: with the Electric proxy gone, self-host now serves runs over native realtime unconditionally.
7+
8+
The native wire serializer (`electricStreamProtocol.server.ts`) is retained — it emits the wire format the SDK realtime client speaks and is still used by the native client. The dashboard trace-sync routes (`sync.traces.*`) still use `ELECTRIC_ORIGIN` and are out of scope for this change, so the Electric container cannot be fully decommissioned by this change alone.
9+
10+
Note: the production shadow-compare divergence sign-off ("shadowCompare diff clean before deletion") is an operational gate handled separately from this code change.

apps/webapp/CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ const signal = getRequestAbortSignal();
7575
Access via `env` export from `app/env.server.ts`. **Never use `process.env` directly.**
7676

7777
For testable code, **never import env.server.ts** in test files. Pass configuration as options instead:
78-
- `realtimeClient.server.ts` (testable service, takes config as constructor arg)
79-
- `realtimeClientGlobal.server.ts` (creates singleton with env config)
78+
- `realtime/nativeRealtimeClient.server.ts` (testable service, takes config as constructor arg)
79+
- `realtime/nativeRealtimeClientInstance.server.ts` (creates singleton with env config)
8080

8181
## Run Engine 2.0
8282

apps/webapp/app/routes/realtime.v1.batches.$batchId.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export const loader = createLoaderApiRoute(
2929
},
3030
},
3131
async ({ authentication, request, resource: batchRun, apiVersion }) => {
32-
// Pick the Electric proxy or the native backend per org (defaults to Electric); both implement streamBatch.
32+
// Resolve the native realtime client; it implements streamBatch.
3333
const client = await resolveRealtimeStreamClient(authentication.environment);
3434

3535
return client.streamBatch(

apps/webapp/app/routes/realtime.v1.runs.$runId.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export const loader = createLoaderApiRoute(
4949
},
5050
},
5151
async ({ authentication, request, resource: run, apiVersion }) => {
52-
// Pick the Electric proxy or the native backend per org (defaults to Electric); both implement streamRun.
52+
// Resolve the native realtime client; it implements streamRun.
5353
const client = await resolveRealtimeStreamClient(authentication.environment);
5454

5555
return client.streamRun(

apps/webapp/app/routes/realtime.v1.runs.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const loader = createLoaderApiRoute(
3131
},
3232
},
3333
async ({ searchParams, authentication, request, apiVersion }) => {
34-
// Pick the Electric proxy or the native backend per org (defaults to Electric); both implement streamRuns.
34+
// Resolve the native realtime client; it implements streamRuns.
3535
const client = await resolveRealtimeStreamClient(authentication.environment);
3636

3737
return client.streamRuns(

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
2-
* Pure (no DB/Redis/env) Electric HTTP shape-stream wire serializer, byte-faithful to what the
3-
* deployed `@electric-sql/client` (1.0.14 + 0.4.0) and the SDK's `SubscribeRunRawShape` expect.
2+
* Pure (no DB/Redis/env) native realtime wire serializer. It emits the shape-stream wire format the
3+
* SDK's realtime client speaks, byte-faithful to what the deployed `@electric-sql/client` (1.0.14 + 0.4.0)
4+
* and the SDK's `SubscribeRunRawShape` expect.
45
* Each column value is wire-encoded as a string (or null) decoded via the `electric-schema` header;
56
* `up-to-date` is the only control message that makes the client emit, and re-sending a full row is idempotent.
67
*/
Lines changed: 4 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,93 +1,12 @@
1-
import { $replica } from "~/db.server";
2-
import { env } from "~/env.server";
3-
import { singleton } from "~/utils/singleton";
4-
import { FEATURE_FLAG } from "~/v3/featureFlags";
5-
import { makeFlag } from "~/v3/featureFlags.server";
6-
import { logger } from "../logger.server";
71
import { type RealtimeEnvironment } from "../realtimeClient.server";
8-
import { realtimeClient } from "../realtimeClientGlobal.server";
9-
import { BoundedTtlCache } from "./boundedTtlCache";
102
import { type RealtimeStreamClient } from "./nativeRealtimeClient.server";
113
import { getNativeRealtimeClient } from "./nativeRealtimeClientInstance.server";
12-
import { getShadowRealtimeClient } from "./shadowRealtimeClientInstance.server";
13-
14-
type RealtimeBackend = "electric" | "native" | "shadow";
15-
16-
// Two gates, both defaulting to the Electric path: the env master switch, then the
17-
// per-org `realtimeBackend` feature flag (cached so long-polls don't hit the DB per request).
18-
const nativeBackendEnabled = env.REALTIME_BACKEND_NATIVE_ENABLED === "1";
19-
20-
const flag = singleton("realtimeBackendFlag", () => makeFlag($replica));
21-
const backendCache = singleton(
22-
"realtimeBackendCache",
23-
() =>
24-
new BoundedTtlCache<RealtimeBackend>(
25-
env.REALTIME_BACKEND_FLAG_CACHE_TTL_MS,
26-
env.REALTIME_BACKEND_FLAG_CACHE_MAX_ENTRIES
27-
)
28-
);
294

5+
// The realtime run feed is served exclusively by the native backend; there is no longer
6+
// an Electric proxy path or per-org backend selection. The signature is kept async (and
7+
// still accepts the authenticated environment) so the run routes don't have to change.
308
export async function resolveRealtimeStreamClient(
319
environment: RealtimeEnvironment & { organization?: { featureFlags?: unknown } }
3210
): Promise<RealtimeStreamClient> {
33-
if (!nativeBackendEnabled) {
34-
return realtimeClient;
35-
}
36-
37-
// The authenticated environment already carries the org's feature flags; pass them
38-
// through so a cache miss doesn't need an extra organization read.
39-
const orgFeatureFlags = environment.organization
40-
? (environment.organization.featureFlags ?? {})
41-
: undefined;
42-
43-
switch (await getRealtimeBackend(environment.organizationId, orgFeatureFlags)) {
44-
case "native":
45-
return getNativeRealtimeClient();
46-
case "shadow":
47-
// The client is still served Electric; the native path is diffed in the background.
48-
return getShadowRealtimeClient();
49-
case "electric":
50-
default:
51-
return realtimeClient;
52-
}
53-
}
54-
55-
async function getRealtimeBackend(
56-
organizationId: string,
57-
orgFeatureFlags: unknown | undefined
58-
): Promise<RealtimeBackend> {
59-
const cached = backendCache.get(organizationId);
60-
if (cached !== undefined) {
61-
return cached;
62-
}
63-
64-
let backend: RealtimeBackend = "electric";
65-
66-
try {
67-
const overrides =
68-
orgFeatureFlags !== undefined
69-
? orgFeatureFlags
70-
: (
71-
await $replica.organization.findFirst({
72-
where: { id: organizationId },
73-
select: { featureFlags: true },
74-
})
75-
)?.featureFlags;
76-
77-
backend = await flag({
78-
key: FEATURE_FLAG.realtimeBackend,
79-
defaultValue: "electric",
80-
overrides: (overrides as Record<string, unknown>) ?? {},
81-
});
82-
} catch (error) {
83-
// Never let a flag lookup failure break the realtime feed.
84-
logger.error("[resolveRealtimeStreamClient] failed to resolve realtimeBackend flag", {
85-
organizationId,
86-
error,
87-
});
88-
backend = "electric";
89-
}
90-
91-
backendCache.set(organizationId, backend);
92-
return backend;
11+
return getNativeRealtimeClient();
9312
}

0 commit comments

Comments
 (0)