Files
triggerdotdev--trigger.dev/apps/webapp/test/realtime/nativeRealtimeClient.test.ts
T
2026-07-13 13:32:57 +08:00

111 lines
3.8 KiB
TypeScript

import { CURRENT_API_VERSION } from "~/api/versions";
import {
NativeRealtimeClient,
type RealtimeListEnvironment,
} from "~/services/realtime/nativeRealtimeClient.server";
import { type RealtimeRunRow } from "~/services/realtime/electricStreamProtocol.server";
import { EnvChangeRouter } from "~/services/realtime/envChangeRouter.server";
import { describe, expect, it } from "vitest";
function sampleRow(): RealtimeRunRow {
return {
id: "run_1",
taskIdentifier: "t",
createdAt: new Date("2026-06-07T10:00:00.000Z"),
updatedAt: new Date("2026-06-07T10:00:01.000Z"),
startedAt: null,
delayUntil: null,
queuedAt: null,
expiredAt: null,
completedAt: null,
friendlyId: "run_friendly_1",
number: 1,
isTest: false,
status: "EXECUTING",
usageDurationMs: 0,
costInCents: 0,
baseCostInCents: 0,
ttl: null,
payload: "{}",
payloadType: "application/json",
metadata: null,
metadataType: "application/json",
output: null,
outputType: "application/json",
runTags: [],
error: null,
realtimeStreams: [],
};
}
// Only the initial-snapshot path is exercised here, which touches the shared
// #buildResponse — enough to lock the response-header contract.
function makeClient(row: RealtimeRunRow | null) {
return new NativeRealtimeClient({
runReader: {
getRunById: async () => row,
hydrateByIds: async () => (row ? [row] : []),
} as any,
runListResolver: { resolveMatchingRunIds: async () => [] } as any,
// Snapshot path only; the router (over a no-op source) is never invoked here.
router: new EnvChangeRouter({
source: { subscribeToEnv: () => () => {} },
hydrator: { hydrateByIds: async () => (row ? [row] : []) },
replayWindowMs: 0,
unsubscribeLingerMs: 0,
}),
limiter: { incrementAndCheck: async () => true, decrement: async () => {} } as any,
cachedLimitProvider: { getCachedLimit: async () => 100 },
maximumCreatedAtFilterAgeMs: 24 * 60 * 60 * 1000,
});
}
const ENV: RealtimeListEnvironment = {
id: "env_1",
organizationId: "org_1",
projectId: "proj_1",
};
describe("NativeRealtimeClient response headers", () => {
it("exposes electric headers cross-origin so browser hooks can read them", async () => {
const client = makeClient(sampleRow());
const res = await client.streamRun(
"http://localhost:3030/realtime/v1/runs/run_1?offset=-1",
ENV,
"run_1",
CURRENT_API_VERSION,
undefined,
"1.0.0-beta.1" // modern client => lowercase electric-* headers
);
// Without these the deployed @electric-sql/client throws MissingHeadersError
// (it can't read the electric-* headers across origins). This regressed once.
expect(res.headers.get("access-control-allow-origin")).toBe("*");
expect(res.headers.get("access-control-expose-headers")).toBe("*");
// Initial (non-live) snapshot requires offset + handle + schema.
expect(res.headers.get("electric-offset")).toBeTruthy();
expect(res.headers.get("electric-handle")).toBeTruthy();
expect(res.headers.get("electric-schema")).toBeTruthy();
expect(res.headers.get("content-type")).toBe("application/json");
});
it("renames headers for legacy (0.4.0) clients", async () => {
const client = makeClient(sampleRow());
const res = await client.streamRun(
"http://localhost:3030/realtime/v1/runs/run_1?offset=-1",
ENV,
"run_1",
CURRENT_API_VERSION,
undefined,
undefined // no client version => legacy header names
);
expect(res.headers.get("electric-chunk-last-offset")).toBeTruthy();
expect(res.headers.get("electric-shape-id")).toBeTruthy();
expect(res.headers.get("electric-offset")).toBeNull();
expect(res.headers.get("electric-handle")).toBeNull();
expect(res.headers.get("access-control-expose-headers")).toBe("*");
});
});