realtime-clients
Try it
This example runs inside the Setu-TS repository. To start a similar project:
npm create @setu-ts my-app -- --template rest
cd my-app && npm install
npm run start:dev
The commands further down run inside a clone of the repository.
A live SSE/WebSocket server driven by the SDK clients; the smoke check exercises resume,
authentication and keep-alive. The smoke check needs Bun installed and runs Wrangler through npx.
Run from the repository root after installing dependencies:
pnpm --dir apps/realtime-clients run start 3400
pnpm --dir apps/realtime-clients run smoke
Read the example deep dive for the concept and source
walkthrough.
Read, in order
setu.config.ts
import { createApplication } from '@setu-ts/kernel';
import type { IKernelApplication } from '@setu-ts/kernel';
import { RuntimePlugin } from '@setu-ts/runtime';
import { SsePlugin } from '@setu-ts/sse-plugin';
import { WebSocketPlugin } from '@setu-ts/websocket-plugin';
import { registerModules } from './src/modules/index.ts';
import { IdleSocketPlugin } from './src/plugins/idle-socket.plugin.ts';
/**
* Builds the server every realtime-client runtime check drives, without starting it.
*
* The heartbeat and idle timings are short so a check finishes in milliseconds.
*/
export function createApp(): IKernelApplication {
const app = createApplication({
plugins: [
RuntimePlugin(),
SsePlugin({ heartbeatMs: 10, retryMs: 1, scalingNotice: false }),
WebSocketPlugin({ heartbeatMs: 20, idleTimeoutMs: 60, scalingNotice: false }),
IdleSocketPlugin(),
],
});
registerModules(app.router);
return app;
}
driver.ts
// A relative source import, not `@setu-ts/sdk`: this file also runs under
// plain Node and Bun with type stripping, which do not resolve workspace paths.
import { createRealtimeClient, createSseClient } from '../../packages/sdk/src/index.ts';
import { IDLE_SOCKET_PATH, SMOKE_TOKEN } from './src/contract.ts';
const wait = (milliseconds: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
/** Checks that the SSE client reconnects with `Last-Event-ID` and skips heartbeats. */
async function checkSseClient(baseUrl: string): Promise<void> {
const received: Array<{ readonly id?: string }> = [];
let twoEventsArrived: () => void = () => {};
const complete = new Promise<void>((resolve) => {
twoEventsArrived = resolve;
});
const sse = createSseClient({
url: `${baseUrl}/events`,
headers: { authorization: `Bearer ${SMOKE_TOKEN}` },
reconnect: { delayMs: 1, maxAttempts: 3 },
onEvent: (event) => {
received.push(event);
if (received.length === 2) twoEventsArrived();
},
});
await Promise.race([
complete,
wait(2_000).then(() => Promise.reject(new Error('SSE reconnect did not deliver two events.'))),
]);
sse.close();
if (received.some((event) => event.id === undefined)) {
throw new Error('An SSE heartbeat comment leaked as an event.');
}
const { resumedWith } = (await (await fetch(`${baseUrl}/resume`)).json()) as {
resumedWith: string | null;
};
if (resumedWith !== received[0]?.id) {
throw new Error('The SSE reconnect did not send Last-Event-ID.');
}
}
/** Checks that a read-only WebSocket client survives the server's idle sweep. */
async function checkIdleSocket(baseUrl: string): Promise<void> {
let closedUnexpectedly = false;
let stopping = false;
let opened: () => void = () => {};
const open = new Promise<void>((resolve) => {
opened = resolve;
});
const socket = createRealtimeClient({
url: baseUrl.replace(/^http/, 'ws') + IDLE_SOCKET_PATH,
parse: (data) => data,
reconnect: { maxAttempts: 0 },
onMessage: () => {},
onStateChange: (state) => {
if (state === 'open') opened();
if (state === 'closed' && !stopping) closedUnexpectedly = true;
},
});
await Promise.race([
open,
wait(2_000).then(() => Promise.reject(new Error('The WebSocket client did not connect.'))),
]);
// Timed from the open, and longer than the server's idle timeout, so only
// heartbeats can keep the socket alive.
await wait(140);
stopping = true;
socket.close();
if (closedUnexpectedly) {
throw new Error('The read-only WebSocket client did not survive the idle sweep.');
}
}
/** Drives both SDK clients against the real server, from Node, Bun or workerd. */
export async function exerciseRealtimeClients(baseUrl: string): Promise<void> {
await checkSseClient(baseUrl);
await checkIdleSocket(baseUrl);
}
if (import.meta.main) {
const baseUrl = process.argv[2];
if (baseUrl === undefined) throw new Error('Expected the realtime server URL.');
await exerciseRealtimeClients(baseUrl);
}
smoke.ts
import assert from 'node:assert/strict';
import { after, before, describe, it } from 'node:test';
import { run } from '../../scripts/lib/run.ts';
import { freePort, SOURCE_HOST_ARGS, startHost, stopHost, waitForHttp } from '../smoke-helpers.ts';
import type { Host } from '../smoke-helpers.ts';
import { exerciseRealtimeClients } from './driver.ts';
/** Runs a command to completion and fails, naming it, if it does. */
async function runOrFail(command: string, args: readonly string[]): Promise<void> {
const outcome = await run(command, args, { inherit: true });
if (outcome.code !== 0) throw new Error(`${command} ${args.join(' ')} exited ${outcome.code}.`);
}
describe('realtime-clients example', () => {
let server: Host;
let baseUrl: string;
before(async () => {
const port = await freePort();
baseUrl = `http://127.0.0.1:${port}`;
server = startHost(process.execPath, [...SOURCE_HOST_ARGS, 'main.ts', String(port)]);
await waitForHttp(`${baseUrl}/resume`, { label: 'Realtime client server' });
});
after(async () => {
assert.equal(await stopHost(server), 0, 'the server did not shut down cleanly');
});
it('works from this Node process', async () => {
await exerciseRealtimeClients(baseUrl);
});
it('works from Node with type stripping', async () => {
await runOrFail('node', ['--experimental-strip-types', 'driver.ts', baseUrl]);
});
it('works from Bun', async () => {
await runOrFail('bun', ['driver.ts', baseUrl]);
});
it('works from workerd', async () => {
// `npx wrangler` downloads Wrangler on first use, which on a cold runner
// takes longer than the readiness budget below. Resolve it first.
await runOrFail('npx', ['--yes', 'wrangler', '--version']);
const workerdPort = await freePort();
const worker = startHost('npx', [
'wrangler',
'dev',
'worker.ts',
'--port',
String(workerdPort),
'--log-level',
'error',
]);
try {
const workerdUrl = `http://127.0.0.1:${workerdPort}`;
await waitForHttp(workerdUrl, { label: 'workerd client host' });
const response = await fetch(`${workerdUrl}/?baseUrl=${encodeURIComponent(baseUrl)}`);
if (!response.ok) throw new Error(`The workerd client driver returned ${response.status}.`);
} finally {
await stopHost(worker);
}
});
});