realtime
Every export of @kitr/realtime, @kitr/realtime/client, @kitr/realtime/routes, @kitr/realtime/server, @kitr/realtime/socket.io, @kitr/realtime/socket.io/server, @kitr/realtime/sse, @kitr/realtime/sse/server, @kitr/realtime/react, read from the published type definitions. The guide shows them in use.
@kitr/realtime
channel
function channel<C extends ChannelDefinition<any, any>>(transport: SocketTransport, definition: C): ChannelHandle<C>;
Bind a channel definition to a transport. Events validated by their schema
(when given) are handed to effects, which useSubscription applies to the cache.
createRealtimeClient
function createRealtimeClient(config?: RealtimeConfig): SocketTransport;
Creates the client transport config/realtime.ts names: websocket (default) or sse here; socket.io loads its
client from @kitr/realtime/socket.io so the dependency stays optional.
let transport: SocketTransport | undefined
export const realtime = () => (transport ??= createRealtimeClient(realtimeConfig))
Call it lazily, as above, so nothing connects during SSR.
createSocketClient
function createSocketClient(options: SocketClientOptions): SocketTransport;
The native WebSocket transport (partysocket underneath: reconnects, buffers while offline). Subscriptions are re-sent after every reconnect.
createSseTransport
function createSseTransport(options: SseTransportOptions): SocketTransport;
Server-Sent Events as a realtime transport: the server pushes over one EventSource,
the client subscribes and sends with plain fetch POSTs. Works through any HTTP host
and proxy, no upgrade needed. Same channels, same useSubscription.
defineChannel
function defineChannel<TParams = void, const TData extends Record<string, unknown> = Record<string, unknown>>(definition: Omit<ChannelDefinition<TParams, any>, 'events'> & {
events: {
[K in keyof TData]: ChannelEvent<TData[K], TParams>;
};
}): ChannelDefinition<TParams, {
[K in keyof TData]: ChannelEvent<TData[K], TParams>;
}>;
One channel definition serves both ends: the client applies effects to the
cache, the server checks authorize. Switching WebSocket ⇄ Socket.IO is a config change.
export const projectChannel = defineChannel({
name: 'projects',
params: z.object({ id: z.string() }),
events: {
updated: { effects: (project, { id }) => ({ set: [{ key: ['projects', 'detail', id], data: project }] }) },
deleted: { effects: () => ({ invalidate: [['projects', 'list']] }) },
},
})
Import the same definition on the server (to serve and emit) and on the client (to subscribe).
parseMessage
function parseMessage<T>(raw: unknown): T | null;
Parses a frame from a string, an ArrayBuffer or an object; returns null for anything that is not valid JSON.
stableStringify
function stableStringify(value: unknown): string;
JSON.stringify with object keys sorted, so equal params always produce the same topic.
topicFor
function topicFor(channel: string, params: unknown): string;
The topic string for a channel and params (chat:{"room":"general"}); key order in params does not matter.
createWebSocketTransport
const createWebSocketTransport: typeof createSocketClient;
The native WebSocket transport (same as createSocketClient).
defaultIdentity
const defaultIdentity: ConnectionIdentity;
The default identify: userId, user.id or id of the connection context.
defineConfig
const defineConfig: <const C extends RealtimeConfig>(config: C) => C;
Typed identity helper for config/realtime.ts.
ownedBy
const ownedBy: (identify: ConnectionIdentity | undefined, ctx: unknown, userId: string | number) => boolean;
Whether a connection with this context belongs to userId.
ChannelDefinition
interface ChannelDefinition<TParams = void, TEvents extends Record<string, ChannelEvent<any, TParams>> = Record<string, ChannelEvent<any, TParams>>> {
/** Unique channel name; it is the first part of every topic and must match on both ends. */
name: string;
/** Schema for the subscription params. The server validates (and may transform) them before `authorize`. */
params?: StandardSchemaV1<unknown, TParams>;
/** The events this channel carries, by name. */
events: TEvents;
/** Server side: may this connection subscribe? `ctx` is what `authorize` on the server returned. */
authorize?: (params: TParams, ctx: unknown) => boolean | Promise<boolean>;
/**
* Server side: handle a client-sent message on this channel. `realtime` is the server that
* received it, to emit back: `realtime.use(channel, params).emit('typing', data)`.
*/
onMessage?: (event: string, data: unknown, params: TParams, ctx: unknown, realtime: RealtimeEmitter) => void | Promise<void>;
}
A channel as defineChannel returns it: its name, params schema, events and server-side hooks.
ChannelEvent
interface ChannelEvent<TData = unknown, TParams = unknown> {
/** Optional schema for the payload (validated on receive). */
schema?: StandardSchemaV1<unknown, TData>;
/**
* What this event does to the query cache on the client. Method syntax on purpose: a channel
* without a payload schema can still annotate `data` (`effects(message: Message) {...}`).
*/
effects?(data: TData, params: TParams): CacheEffect | void;
}
One event a channel carries: an optional payload schema and the cache effects it applies on the client.
ChannelHandle
interface ChannelHandle<C extends ChannelDefinition<any, any>> {
/** A subscription contract for `useSubscription` (@kitr/query) or `subscriptionFrom`. */
subscription(): SubscriptionContract<ChannelParams<C>, ChannelEventUnion<C>>;
/** Send a client event up the channel. */
send<E extends keyof C['events'] & string>(params: ChannelParams<C>, event: E, data: unknown): void;
/** The query key of a subscription: `['realtime', name, params]`. */
key(params: ChannelParams<C>): QueryKey;
}
A channel bound to a transport, as channel() returns it.
RealtimeConfig
interface RealtimeConfig {
/** `websocket` (default, crossws/partysocket), `socket.io` (rooms, adapters) or `sse` (one-way events over HTTP, any host). */
transport?: RealtimeTransport;
/** Client URL. Default: same origin at `path`. */
url?: string;
/** Server path of the upgrade route (websocket), the SSE stream, or the Socket.IO path. Default `/ws`, `/events` for sse. Read by both `createRealtimeClient` and `createRealtimeServer`. */
path?: string;
/** WebSocket only: client ping interval in ms; a ping left unanswered by the next one reconnects. Default 25 000; 0 disables. */
heartbeat?: number;
}
The shape of config/realtime.ts: which transport both ends use and where they meet.
RealtimeEmitter
interface RealtimeEmitter {
use(channel: ChannelDefinition<any, any>, params: any): {
emit(event: any, data?: any): void;
};
}
The least every realtime server (WebSocket, SSE, Socket.IO) offers a channel's onMessage.
SocketClientOptions
interface SocketClientOptions {
/** `ws://` or `wss://` URL, or a function (e.g. to append a token). */
url: string | (() => string | Promise<string>);
/** WebSocket subprotocols, passed to the WebSocket constructor. */
protocols?: string | string[];
/** Reconnect settings passed to partysocket. */
reconnect?: {
maxRetries?: number;
minReconnectionDelay?: number;
maxReconnectionDelay?: number;
};
/** Called for every server error that isn't tied to a subscription. */
onError?: (error: KitrErrorShape) => void;
/**
* Ping interval in ms while open. A connection that has not answered the previous ping by the
* next one is treated as dead and reconnected. Default 25 000; 0 disables.
*/
heartbeat?: number;
}
Options for createSocketClient, the native WebSocket transport.
SocketTransport
interface SocketTransport {
/** Subscribe to a channel topic. Returns a function that unsubscribes. */
subscribe(channel: string, params: unknown, onEvent: (event: string, data: unknown) => void, onError?: (e: KitrErrorShape) => void): () => void;
/** Send a client event up a channel; the server hands it to the channel's `onMessage`. */
send(channel: string, params: unknown, event: string, data: unknown): void;
/** Close the connection and stop reconnecting. */
close(): void;
/** The current connection state. */
readonly status: SocketStatus;
/** Listen for status changes. Returns a function that removes the listener. */
onStatus(listener: (status: SocketStatus) => void): () => void;
}
What every realtime client implements: WebSocket, Socket.IO, SSE, or your own.
channel() and useChannel() only talk to this interface.
SseTransportOptions
interface SseTransportOptions {
/** The stream URL (`/events`). Control messages are POSTed to the same URL. */
url: string | (() => string);
/** Extra headers for control requests (the stream itself cannot carry headers; use cookies or a `?token=`). */
headers?: Record<string, string> | (() => Record<string, string>);
/** Called for server errors not tied to a subscription, and for failed control requests (`NETWORK`). */
onError?: (error: KitrErrorShape) => void;
}
Options for createSseTransport, the Server-Sent Events client.
ChannelEventUnion
type ChannelEventUnion<C extends ChannelDefinition<any, any>> = {
[K in keyof C['events'] & string]: {
event: K;
data: EventData<C['events'][K]>;
};
}[keyof C['events'] & string];
Every event of a channel as a { event, data } union, which useChannel returns as lastEvent.
ChannelParams
type ChannelParams<C> = C extends ChannelDefinition<infer P, any> ? P : never;
The params type a channel subscribes with.
ClientMessage
type ClientMessage = {
t: 'sub';
id: string;
ch: string;
p: unknown;
} | {
t: 'unsub';
id: string;
} | {
t: 'msg';
ch: string;
p: unknown;
ev: string;
d: unknown;
} | {
t: 'ping';
};
A frame the client sends: subscribe (sub), unsubscribe (unsub), a client event (msg) or a ping.
The wire protocol shared by the WebSocket, Socket.IO and SSE transports.
ConnectionIdentity
type ConnectionIdentity = (ctx: unknown) => string | number | null | undefined;
Who a connection belongs to, for closeSessions(userId). identify reads it from the context
authorize returned; the default takes ctx.userId, ctx.user.id or ctx.id, as a string.
EventData
type EventData<E> = E extends ChannelEvent<infer D, any> ? D : never;
The payload type of one channel event, inferred from its schema or effects annotation.
RealtimeStatus
type RealtimeStatus = SocketStatus;
Alias of SocketStatus.
RealtimeTransport
type RealtimeTransport = SocketTransport;
Alias: the transport interface every realtime client implements.
RealtimeTransportName
type RealtimeTransportName = 'websocket' | 'socket.io' | 'sse';
Which transport carries channel traffic: native WebSocket, Socket.IO, or Server-Sent Events.
ServerMessage
type ServerMessage = {
t: 'ev';
ch: string;
p: unknown;
ev: string;
d: unknown;
} |
/** `p` is the params after the channel's schema ran; events are routed by them. */
{
t: 'ok';
id: string;
p?: unknown;
} | {
t: 'err';
id?: string;
e: KitrErrorShape;
} | {
t: 'pong';
};
A frame the server sends: an event (ev), a subscription ack (ok), an error (err) or a pong.
SocketStatus
type SocketStatus = 'connecting' | 'open' | 'closed';
The connection state a transport reports: connecting, open or closed.
@kitr/realtime/client
channel
function channel<C extends ChannelDefinition<any, any>>(transport: SocketTransport, definition: C): ChannelHandle<C>;
Bind a channel definition to a transport. Events validated by their schema
(when given) are handed to effects, which useSubscription applies to the cache.
createSocketClient
function createSocketClient(options: SocketClientOptions): SocketTransport;
The native WebSocket transport (partysocket underneath: reconnects, buffers while offline). Subscriptions are re-sent after every reconnect.
createWebSocketTransport
const createWebSocketTransport: typeof createSocketClient;
The native WebSocket transport (same as createSocketClient).
ChannelHandle
interface ChannelHandle<C extends ChannelDefinition<any, any>> {
/** A subscription contract for `useSubscription` (@kitr/query) or `subscriptionFrom`. */
subscription(): SubscriptionContract<ChannelParams<C>, ChannelEventUnion<C>>;
/** Send a client event up the channel. */
send<E extends keyof C['events'] & string>(params: ChannelParams<C>, event: E, data: unknown): void;
/** The query key of a subscription: `['realtime', name, params]`. */
key(params: ChannelParams<C>): QueryKey;
}
A channel bound to a transport, as channel() returns it.
SocketClientOptions
interface SocketClientOptions {
/** `ws://` or `wss://` URL, or a function (e.g. to append a token). */
url: string | (() => string | Promise<string>);
/** WebSocket subprotocols, passed to the WebSocket constructor. */
protocols?: string | string[];
/** Reconnect settings passed to partysocket. */
reconnect?: {
maxRetries?: number;
minReconnectionDelay?: number;
maxReconnectionDelay?: number;
};
/** Called for every server error that isn't tied to a subscription. */
onError?: (error: KitrErrorShape) => void;
/**
* Ping interval in ms while open. A connection that has not answered the previous ping by the
* next one is treated as dead and reconnected. Default 25 000; 0 disables.
*/
heartbeat?: number;
}
Options for createSocketClient, the native WebSocket transport.
SocketTransport
interface SocketTransport {
/** Subscribe to a channel topic. Returns a function that unsubscribes. */
subscribe(channel: string, params: unknown, onEvent: (event: string, data: unknown) => void, onError?: (e: KitrErrorShape) => void): () => void;
/** Send a client event up a channel; the server hands it to the channel's `onMessage`. */
send(channel: string, params: unknown, event: string, data: unknown): void;
/** Close the connection and stop reconnecting. */
close(): void;
/** The current connection state. */
readonly status: SocketStatus;
/** Listen for status changes. Returns a function that removes the listener. */
onStatus(listener: (status: SocketStatus) => void): () => void;
}
What every realtime client implements: WebSocket, Socket.IO, SSE, or your own.
channel() and useChannel() only talk to this interface.
RealtimeStatus
type RealtimeStatus = SocketStatus;
Alias of SocketStatus.
RealtimeTransport
type RealtimeTransport = SocketTransport;
Alias: the transport interface every realtime client implements.
SocketStatus
type SocketStatus = 'connecting' | 'open' | 'closed';
The connection state a transport reports: connecting, open or closed.
@kitr/realtime/routes
ws
const ws: {
readonly key: "ws";
readonly kinds: readonly [
"ws"
];
readonly methods: {
readonly ws: (this: AnyRouteTable, path: string, options: SocketServerOptions) => AnyRouteTable;
readonly sse: (this: AnyRouteTable, path: string, options: SseServerOptions) => AnyRouteTable;
};
readonly targets: {
readonly h3: (entries: RouteEntry[], server: KitrServer) => MountedSockets;
};
};
The realtime extension: .ws and .sse on the route table, mounted by
createServer({ routes }). No React: safe in a pure backend.
import { defineRoutes } from '@kitr/kitr/app'
import ws from '@kitr/realtime/routes'
export const routes = defineRoutes([ws], (r) => r.ws('/ws', { channels: [chatChannel] }))
SocketRouteType
interface SocketRouteType<Transport extends 'websocket' | 'sse' = 'websocket' | 'sse', Path extends string = string, C extends ChannelDefinition<any, any> = ChannelDefinition<any, any>> {
kind: 'ws';
transport: Transport;
path: Path;
/** The union of channel definitions served on this path. */
channels: C;
}
The type carried per socket route into $types.
SseRouteOptions
interface SseRouteOptions<C extends readonly ChannelDefinition<any, any>[]> extends Omit<SseServerOptions, 'channels'> {
channels: C;
}
Options for .sse(path, options): the channels, the stream authorizer and the keep-alive.
WsRouteOptions
interface WsRouteOptions<C extends readonly ChannelDefinition<any, any>[]> extends Omit<SocketServerOptions, 'channels'> {
channels: C;
}
Options for .ws(path, options): the channels and the upgrade authorizer.
MountedSockets
type MountedSockets = Record<string, SocketServer | SseServer>;
What the h3 target returns: the realtime servers by path.
@kitr/realtime/server
createMemoryRealtimeAdapter
function createMemoryRealtimeAdapter(): RealtimeAdapter;
Every handler for every topic, in this process only. The default; right for one instance.
createRealtimeServer
function createRealtimeServer(options: RealtimeServerOptions): RealtimeServer;
Creates the realtime server for the transport in config/realtime.ts, so switching transports
there changes both ends. Pass the config straight in:
import realtimeConfig from '../config/realtime'
export const realtime = createRealtimeServer({ ...realtimeConfig, channels: [chatChannel] })
realtime.attach(server) // at realtimeConfig.path
Socket.IO needs a Socket.IO Server, so it throws here: use attachSocketIo from
@kitr/realtime/socket.io/server instead.
createRedisRealtimeAdapter
function createRedisRealtimeAdapter(redis: RedisPubSubClient, options?: RedisRealtimeAdapterOptions): RealtimeAdapter;
A realtime adapter over Redis pub/sub, so an emit on any instance reaches the clients on
every instance. Pass an ioredis client; the adapter opens one more connection for subscribing.
const adapter = createRedisRealtimeAdapter(new Redis(process.env.REDIS_URL)) // Redis from ioredis
r.ws('/ws', { channels: [projectChannel], adapter })
Each instance subscribes only to the topics its own clients listen on, so traffic grows with what is watched, not with the number of instances.
createSocketServer
function createSocketServer(options: SocketServerOptions): SocketServer;
Creates the native WebSocket server on crossws (via h3). Runs on Node, Bun, Deno and Cloudflare.
Attach it to a kitr server with attach(server, path); emit with use(channel, params).emit().
realtimeProvider
function realtimeProvider(options: RealtimeServerOptions): KitrProvider;
A provider that binds the realtime server as realtime in the container, for the transport and
path in config/realtime.ts (options given here win). It does not register the route, because
the provider has no server: call app.container.use('realtime').attach(server) after boot.
ChannelEmitter
interface ChannelEmitter<C extends ChannelDefinition<any, any>> {
/** Send an event to every connection subscribed to this topic, on every instance the adapter reaches. A topic with no subscribers is a no-op. */
emit<E extends keyof C['events'] & string>(event: E, data: C['events'][E] extends {
schema: infer S;
} ? S extends {
'~standard': {
types?: {
output: infer O;
};
};
} ? O : unknown : unknown): void;
/** Subscriber count for this topic on this instance. */
readonly size: number;
}
Publishes events to every subscriber of one channel topic, as server.use(channel, params) returns it.
RealtimeAdapter
interface RealtimeAdapter {
/** Shown in logs, e.g. `memory` or `redis`. */
readonly name: string;
/**
* Start receiving messages published on `topic`, in this process. Several handlers may listen
* to one topic; the adapter subscribes upstream once and fans out. Returns (or resolves with,
* once the upstream subscription is live) the function that removes this handler again.
*/
subscribe(topic: string, handler: (payload: string) => void): (() => void) | Promise<() => void>;
/**
* Deliver `payload` to every handler of `topic` on every instance, this one included.
* Resolves with the number of receivers when the backend reports it (Redis counts subscribed
* connections, the memory adapter counts local handlers), so `0` means nobody is listening.
*/
publish(topic: string, payload: string): number | Promise<number>;
/** Close the adapter's connections. Called by you on shutdown, never by the server. */
close?(): Promise<void>;
}
How realtime servers on several instances reach each other. Each server keeps its own connections; the adapter carries a published message to every instance that has a subscriber for its topic, and each instance writes it to its own connections.
The memory adapter (the default) only reaches this process. createRedisRealtimeAdapter
reaches every process connected to the same Redis.
RealtimeServer
interface RealtimeServer {
/** Which transport this server speaks. */
readonly transport: Exclude<RealtimeTransport, 'socket.io'>;
/** Publish on a channel from an action, controller or job. */
use<C extends ChannelDefinition<any, any>>(channel: C, params: ChannelParams<C>): ChannelEmitter<C>;
/** Register the route on a kitr server: at `path`, else the `path` option, else `/ws` (websocket) or `/events` (sse). */
attach(server: KitrServer, path?: string): RealtimeServer;
/** Number of open connections or streams. */
readonly connections: number;
/**
* Close every connection or stream of `userId` on this instance and return how many. Call it when
* the user's access ends (sign-out everywhere, password reset, a ban); see `SocketServer.closeSessions`.
*/
closeSessions(userId: string | number): number;
}
The realtime server for whichever transport config/realtime.ts names: websocket or sse.
RealtimeServerOptions
interface RealtimeServerOptions extends SocketServerOptions, RealtimeConfig {
/** SSE only: keep-alive comment interval in ms. Default 20 000. */
keepAlive?: number;
}
Options for createRealtimeServer: the channels, plus the transport and path from config/realtime.ts.
RedisPubSubClient
interface RedisPubSubClient {
/** `PUBLISH`: resolves with the number of subscribed connections that received it. */
publish(channel: string, message: string): Promise<number>;
/** `SUBSCRIBE`; puts the connection into subscriber mode. */
subscribe(...channels: string[]): Promise<unknown>;
/** `UNSUBSCRIBE`. */
unsubscribe(...channels: string[]): Promise<unknown>;
/** ioredis emits `message` with the channel and the payload. */
on(event: 'message', listener: (channel: string, message: string) => void): unknown;
/** A second connection with the same options, for subscriber mode. */
duplicate(): RedisPubSubClient;
/** ioredis: `'ready'` once connected and past its ready check. */
status?: string;
/** ioredis emits `ready` when the connection can take commands. */
once?(event: 'ready', listener: () => void): unknown;
/** Close the connection. */
quit(): Promise<unknown>;
}
The part of an ioredis client the realtime adapter uses.
An ioredis Redis or Cluster fits as it is.
RedisRealtimeAdapterOptions
interface RedisRealtimeAdapterOptions {
/**
* The connection that subscribes. A Redis connection in subscriber mode can run no other
* command, so it must not be the one you pass first. Default: `redis.duplicate()`.
*/
subscriber?: RedisPubSubClient;
/** Prepended to every Redis channel, so apps sharing a Redis stay apart. Default `kitr:rt:`. */
prefix?: string;
/** Called when Redis rejects a publish or subscribe. Default: `console.error`. */
onError?: (error: unknown) => void;
}
Options for createRedisRealtimeAdapter.
SocketServer
interface SocketServer {
/** The h3 handler for the upgrade route: `server.h3.get('/ws', socket.handler)`. */
handler: ReturnType<typeof defineWebSocketHandler>;
/** Publish on a channel from an action, controller or job. */
use<C extends ChannelDefinition<any, any>>(channel: C, params: ChannelParams<C>): ChannelEmitter<C>;
/** Attach to a kitr server: registers the route and enables the upgrade. */
attach(server: KitrServer, path?: string): SocketServer;
/** Number of open connections on this instance. */
readonly connections: number;
/**
* Close every connection of `userId` on this instance (code 4401, reason `Session revoked`) and
* return how many. Call it when the user's access ends: sign-out everywhere, a password reset, a
* ban. Authorisation happens when a connection opens and when it subscribes, so without this a
* revoked session keeps receiving events until its socket drops. Other instances are not reached:
* call it on each, or from a handler every instance runs.
*/
closeSessions(userId: string | number): number;
/** The adapter emits travel through. */
readonly adapter: RealtimeAdapter;
}
The realtime server for the native WebSocket transport, as createRealtimeServer returns it.
SocketServerOptions
interface SocketServerOptions {
/** Every channel clients may subscribe to. Subscribing to any other name fails with `NOT_FOUND`. */
channels: ChannelDefinition<any, any>[];
/** Authenticate the upgrade. Return per-connection context (e.g. the user) or throw/return false to reject. */
authorize?: (request: Request) => unknown | Promise<unknown>;
/**
* Who a connection belongs to, read from the context `authorize` returned, so
* `closeSessions(userId)` can find it. Default: the context's `userId`, `user.id` or `id`.
*/
identify?: ConnectionIdentity;
/**
* Carries emits between server instances. Default: the memory adapter, which only reaches
* clients connected to this process. Use `createRedisRealtimeAdapter` when you run more than one.
*/
adapter?: RealtimeAdapter;
}
Options for createSocketServer: the channels it serves and how it authenticates connections.
@kitr/realtime/socket.io
channel
function channel<C extends ChannelDefinition<any, any>>(transport: SocketTransport, definition: C): ChannelHandle<C>;
Bind a channel definition to a transport. Events validated by their schema
(when given) are handed to effects, which useSubscription applies to the cache.
createSocketIoClient
function createSocketIoClient(options?: SocketIoClientOptions): SocketTransport & {
socket: Socket;
};
Creates the same SocketTransport as
defineChannel
function defineChannel<TParams = void, const TData extends Record<string, unknown> = Record<string, unknown>>(definition: Omit<ChannelDefinition<TParams, any>, 'events'> & {
events: {
[K in keyof TData]: ChannelEvent<TData[K], TParams>;
};
}): ChannelDefinition<TParams, {
[K in keyof TData]: ChannelEvent<TData[K], TParams>;
}>;
One channel definition serves both ends: the client applies effects to the
cache, the server checks authorize. Switching WebSocket ⇄ Socket.IO is a config change.
export const projectChannel = defineChannel({
name: 'projects',
params: z.object({ id: z.string() }),
events: {
updated: { effects: (project, { id }) => ({ set: [{ key: ['projects', 'detail', id], data: project }] }) },
deleted: { effects: () => ({ invalidate: [['projects', 'list']] }) },
},
})
Import the same definition on the server (to serve and emit) and on the client (to subscribe).
SocketIoClientOptions
interface SocketIoClientOptions extends Partial<ManagerOptions & SocketOptions> {
/** Server URL. Default: the page's origin. */
url?: string;
/** Called for every server error that isn't tied to a subscription. */
onError?: (error: KitrErrorShape) => void;
}
Options for createSocketIoClient: any socket.io-client option plus the server URL and an error handler.
@kitr/realtime/socket.io/server
attachSocketIo
function attachSocketIo(io: Server, options: SocketIoServerOptions): SocketIoServer;
Serves kitr channels over an existing Socket.IO server. Rooms are channel topics, so the Redis adapter and sticky sessions work as they do for any Socket.IO app.
const io = new Server(nodeServer)
const sockets = attachSocketIo(io, { channels: [projectChannel] })
sockets.use(projectChannel, { id }).emit('updated', project)
Connections are authenticated once in a namespace middleware; each subscription runs the channel's authorize.
SocketIoServer
interface SocketIoServer {
/** The Socket.IO server you passed in. */
io: Server;
/** Publish on a channel topic: `use(channel, params).emit(event, data)` emits to that topic's room. */
use<C extends ChannelDefinition<any, any>>(channel: C, params: ChannelParams<C>): {
emit(event: keyof C['events'] & string, data: unknown): void;
};
/** Disconnect every socket of `userId` on this instance and return how many; see `SocketServer.closeSessions`. */
closeSessions(userId: string | number): number;
}
Channels served over Socket.IO, as attachSocketIo returns them.
SocketIoServerOptions
interface SocketIoServerOptions {
/** Every channel clients may subscribe to. */
channels: ChannelDefinition<any, any>[];
/** Authenticate a connection (`socket.handshake.auth`). Return context or throw. */
authorize?: (socket: Socket) => unknown | Promise<unknown>;
/** Who a socket belongs to, for `closeSessions(userId)`. Default: the context's `userId`, `user.id` or `id`. */
identify?: ConnectionIdentity;
/** Serve channels on this Socket.IO namespace instead of the default `/`. */
namespace?: string;
}
Options for attachSocketIo: the channels to serve, connection auth and an optional namespace.
@kitr/realtime/sse
createSseTransport
function createSseTransport(options: SseTransportOptions): SocketTransport;
Server-Sent Events as a realtime transport: the server pushes over one EventSource,
the client subscribes and sends with plain fetch POSTs. Works through any HTTP host
and proxy, no upgrade needed. Same channels, same useSubscription.
SseTransportOptions
interface SseTransportOptions {
/** The stream URL (`/events`). Control messages are POSTed to the same URL. */
url: string | (() => string);
/** Extra headers for control requests (the stream itself cannot carry headers; use cookies or a `?token=`). */
headers?: Record<string, string> | (() => Record<string, string>);
/** Called for server errors not tied to a subscription, and for failed control requests (`NETWORK`). */
onError?: (error: KitrErrorShape) => void;
}
Options for createSseTransport, the Server-Sent Events client.
@kitr/realtime/sse/server
createSseServer
function createSseServer(options: SseServerOptions): SseServer;
Realtime channels over Server-Sent Events: one streaming GET per client, control messages over POST. Runs anywhere h3 runs, including hosts without WebSocket upgrades.
sseProvider
function sseProvider(options: SseServerOptions): KitrProvider;
A provider that binds an SSE realtime server as realtime in the container. It does not attach the route.
SseServer
interface SseServer {
/** GET opens the stream, POST carries sub/unsub/msg control messages. Mount both on one path. */
handler: ReturnType<typeof defineHandler>;
/** Publish on a channel from an action, controller or job. */
use<C extends ChannelDefinition<any, any>>(channel: C, params: ChannelParams<C>): ChannelEmitter<C>;
/** Register GET and POST on `path` (default `/events`) of a kitr server. */
attach(server: KitrServer, path?: string): SseServer;
/** Number of open streams on this instance. */
readonly connections: number;
/** Close every stream of `userId` on this instance and return how many; see `SocketServer.closeSessions`. A client that reconnects is authorised again. */
closeSessions(userId: string | number): number;
/** The adapter emits and forwarded control messages travel through. */
readonly adapter: RealtimeAdapter;
}
The realtime server for the SSE transport, as createSseServer returns it.
SseServerOptions
interface SseServerOptions {
/** Every channel clients may subscribe to. */
channels: ChannelDefinition<any, any>[];
/** Authenticate the stream request. Return per-connection context or throw/return false to reject. */
authorize?: (request: Request) => unknown | Promise<unknown>;
/** Who a stream belongs to, for `closeSessions(userId)`. Default: the context's `userId`, `user.id` or `id`. */
identify?: ConnectionIdentity;
/** Keep-alive comment interval in ms. Default 20 000. */
keepAlive?: number;
/**
* Carries emits, and control POSTs that land on another instance than their stream, between
* server instances. Default: the memory adapter (this process only). Use
* `createRedisRealtimeAdapter` when you run more than one.
*/
adapter?: RealtimeAdapter;
}
Options for createSseServer: the channels to serve, stream auth and the keep-alive interval.
@kitr/realtime/react
useChannel
function useChannel<C extends ChannelDefinition<any, any>>(transport: SocketTransport, definition: C, params: ChannelParams<C>, options?: UseSubscriptionOptions<ChannelEventUnion<C>>): {
status: "idle" | "open" | "error";
lastEvent: ChannelEventUnion<C> | undefined;
error: import("@kitr/core").KitrErrorShape | undefined;
};
Subscribe a component to a channel: useChannel(realtime(), chatChannel, { room }).
Events run the channel's cache effects; the hook also returns the last event and status.
useRealtimeStatus
function useRealtimeStatus(transport: SocketTransport | undefined): SocketStatus;
'connecting' | 'open' | 'closed' for a connection indicator.