Skip to content
tRPC

WebSockets

On this page

You can use WebSockets for all or some of the communication with your server, see wsLink for how to set it up on the client.

Creating a WebSocket-server

yarn add ws
server/wsServer.ts
import { 
function applyWSSHandler<TRouter extends AnyRouter>(opts: WSSHandlerOptions<TRouter>): {
    broadcastReconnectNotification: () => void;
}
applyWSSHandler
} from '@trpc/server/adapters/ws';
import { const WebSocketServer: typeof ServerWebSocketServer } from 'ws'; import {
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
} from './routers/app';
import { const createContext: (opts: CreateWSSContextFnOptions) => {}createContext } from './trpc'; const const wss: Server<typeof WebSocket, typeof IncomingMessage>wss = new new WebSocketServer<typeof WebSocket, typeof IncomingMessage>(options?: WebSocket.ServerOptions<typeof WebSocket, typeof IncomingMessage> | undefined, callback?: () => void): Server<typeof WebSocket, typeof IncomingMessage>WebSocketServer({ WebSocket.ServerOptions<U extends typeof WebSocket = typeof WebSocket, V extends typeof IncomingMessage = typeof IncomingMessage>.port?: number | undefinedport: 3001, }); const
const handler: {
    broadcastReconnectNotification: () => void;
}
handler
=
applyWSSHandler<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: WSSHandlerOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>): {
    broadcastReconnectNotification: () => void;
}
applyWSSHandler
({
wss: ws.WebSocketServerwss,
BaseHandlerOptions<BuiltRouter<{ ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }, DecorateCreateRouterOptions<{}>>, IncomingMessage>.router: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
router
:
const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>
appRouter
,
createContext?: CreateWSSContextFn<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>> | undefined
createContext
,
// Enable heartbeat messages to keep connection open (disabled by default)
keepAlive?: {
    enabled: boolean;
    pingMs?: number;
    pongWaitMs?: number;
} | undefined
keepAlive
: {
enabled: booleanenabled: true, // server ping message interval in milliseconds pingMs?: number | undefinedpingMs: 30000, // connection is terminated if pong message is not received in this many milliseconds pongWaitMs?: number | undefinedpongWaitMs: 5000, }, }); const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.on(event: "connection", cb: (this: Server<typeof WebSocket, typeof IncomingMessage>, websocket: WebSocket, request: IncomingMessage) => void): Server<typeof WebSocket, typeof IncomingMessage> (+5 overloads)on('connection', (ws: WebSocketws) => { var console: Consoleconsole.Console.log(...data: any[]): voidlog(`++ Connection (${const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.clients: Set<WebSocket>clients.Set<WebSocket>.size: numbersize})`); ws: WebSocketws.WebSocket.once(event: "close", listener: (this: WebSocket, code: number, reason: Buffer) => void): WebSocket (+8 overloads)once('close', () => { var console: Consoleconsole.Console.log(...data: any[]): voidlog(`-- Connection (${const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.clients: Set<WebSocket>clients.Set<WebSocket>.size: numbersize})`); }); }); var console: Consoleconsole.Console.log(...data: any[]): voidlog('WebSocket Server listening on ws://localhost:3001'); var process: NodeJS.Processprocess.NodeJS.Process.on<"SIGTERM">(eventName: "SIGTERM", listener: (signal: "SIGTERM") => void): NodeJS.Process (+1 overload)on('SIGTERM', () => { var console: Consoleconsole.Console.log(...data: any[]): voidlog('SIGTERM');
const handler: {
    broadcastReconnectNotification: () => void;
}
handler
.broadcastReconnectNotification: () => voidbroadcastReconnectNotification();
const wss: Server<typeof WebSocket, typeof IncomingMessage>wss.Server<typeof WebSocket, typeof IncomingMessage>.close(cb?: (err?: Error) => void): voidclose(); });

Setting TRPCClient to use WebSockets

client.ts
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient, function wsLink<TRouter extends AnyRouter>(opts: WebSocketLinkOptions<TRouter>): TRPCLink<TRouter>wsLink } from '@trpc/client';
import type { 
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server';
// create persistent WebSocket connection const const wsClient: WsClientwsClient = function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient({ UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: `ws://localhost:3001`, }); // configure TRPCClient to use WebSockets transport const
const client: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
client
=
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
wsLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>(opts: WebSocketLinkOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{}>>>
wsLink
({
client: WsClientclient: const wsClient: WsClientwsClient, }), ], });

Authentication / connection params

In order to authenticate with WebSockets, you can define connectionParams to createWSClient. This will be sent as the first message when the client establishes a WebSocket connection.

server/context.ts
import type { 
type CreateWSSContextFnOptions = {
    req: IncomingMessage;
    res: ws.WebSocket;
    info: TRPCRequestInfo;
}
CreateWSSContextFnOptions
} from '@trpc/server/adapters/ws';
export const const createContext: (opts: CreateWSSContextFnOptions) => Promise<{}>createContext = async (opts: CreateWSSContextFnOptionsopts:
type CreateWSSContextFnOptions = {
    req: IncomingMessage;
    res: ws.WebSocket;
    info: TRPCRequestInfo;
}
CreateWSSContextFnOptions
) => {
const token = opts: CreateWSSContextFnOptionsopts.info: TRPCRequestInfoinfo.TRPCRequestInfo.connectionParams: Dict<string> | nullconnectionParams?.string | undefinedtoken;
const token: string | undefined
// [... authenticate] return {}; }; export type type Context = {}Context =
type Awaited<T> = T extends null | undefined ? T : T extends object & {
    then(onfulfilled: infer F, ...args: infer _): any;
} ? F extends (value: infer V, ...args: infer _) => any ? Awaited<V> : never : T
Awaited
<type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : anyReturnType<typeof const createContext: (opts: CreateWSSContextFnOptions) => Promise<{}>createContext>>;
client/trpc.ts
import { function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>createTRPCClient, function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient, function wsLink<TRouter extends AnyRouter>(opts: WebSocketLinkOptions<TRouter>): TRPCLink<TRouter>wsLink } from '@trpc/client';
import type { 
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
} from './server';
import class superjsonsuperjson from 'superjson'; const const wsClient: WsClientwsClient = function createWSClient(opts: WebSocketClientOptions): WsClientcreateWSClient({ UrlOptionsWithConnectionParams.url: CallbackOrValue<string>url: `ws://localhost:3000`, UrlOptionsWithConnectionParams.connectionParams?: CallbackOrValue<Dict<string> | null> | undefinedconnectionParams: async () => { return { token: stringtoken: 'supersecret', }; }, }); export const
const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
trpc
=
createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
createTRPCClient
<
type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>> & DecorateCreateRouterOptions<{}>
AppRouter
>({
links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>[]
links
: [
wsLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>(opts: WebSocketLinkOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: true;
}, DecorateCreateRouterOptions<{}>>>
wsLink
({ client: WsClientclient: const wsClient: WsClientwsClient, transformer: DataTransformerOptionstransformer: class superjsonsuperjson })],
});

If you yield an event using our tracked()-helper and include an id, the client will automatically reconnect when it gets disconnected and send the last known ID when reconnecting as part of the lastEventId-input.

You can send an initial lastEventId when initializing the subscription and it will be automatically updated as the browser receives data.

import 
class EventEmitter<T extends EventMap<T> = any>
interface EventEmitter<T extends EventMap<T> = any>
EventEmitter
, { function on(emitter: EventEmitter, eventName: string | symbol, options?: EventEmitter.OnOptions): NodeJS.AsyncIterator<any[]> (+1 overload)on } from 'events';
import { const initTRPC: TRPCBuilder<object, object>initTRPC, function tracked<TData>(id: string, data: TData): TrackedEnvelope<TData>tracked } from '@trpc/server'; import { import zz } from 'zod'; type
type Post = {
    id: string;
    title: string;
}
Post
= { id: stringid: string; title: stringtitle: string };
const
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
= const initTRPC: TRPCBuilder<object, object>initTRPC.
TRPCBuilder<object, object>.create<RuntimeConfigOptions<object, object>>(opts?: RuntimeConfigOptions<object, object> | undefined): TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
create
();
const const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure =
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>procedure;
const
const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
=
const t: TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, {
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
t
.
TRPCRootObject<object, object, RuntimeConfigOptions<object, object>, { ctx: object; meta: object; errorShape: DefaultErrorShape; transformer: false; }>.router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
router
;
const const ee: EventEmitter<any>ee = new new EventEmitter<any>(options?: EventEmitter.EventEmitterOptions): EventEmitter<any>EventEmitter(); export const
const subRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: string | null | undefined;
        } | undefined;
        output: AsyncIterable<TrackedData<Post>, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>>
subRouter
=
const router: RouterBuilder
<{
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: string | null | undefined;
        } | undefined;
        output: AsyncIterable<TrackedData<Post>, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}>(_: {
    onPostAdd: SubscriptionProcedure<{
        input: {
            lastEventId?: string | null | undefined;
        } | undefined;
        output: AsyncIterable<TrackedData<Post>, void, any>;
        meta: object;
        errorShape: DefaultErrorShape;
    }>;
}) => BuiltRouter<...>
router
({
onPostAdd: SubscriptionProcedure<{
    input: {
        lastEventId?: string | null | undefined;
    } | undefined;
    output: AsyncIterable<TrackedData<Post>, void, any>;
    meta: object;
    errorShape: DefaultErrorShape;
}>
onPostAdd
: const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>publicProcedure
.
ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.input<z.ZodOptional<z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>>>(schema: z.ZodOptional<z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>>): ProcedureBuilder<object, object, object, {
    lastEventId?: string | null | undefined;
} | undefined, {
    lastEventId?: string | null | undefined;
} | undefined, UnsetMarker, UnsetMarker, false, DefaultErrorShape>
input
(
import zz .
function object<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}>(shape?: {
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueInvalidType<unknown> | z.core.$ZodIssueUnrecognizedKeys>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>
object
({
// lastEventId is the last event id that the client has received // On the first call, it will be whatever was passed in the initial setup // If the client reconnects, it will be the last event id that the client received lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>lastEventId: import zz.function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)string().ZodType<any, any, $ZodStringInternals<string>>.nullish(): z.ZodOptional<z.ZodNullable<z.ZodString>>nullish(), }) .
ZodType<any, any, $ZodObjectInternals<{ lastEventId: ZodOptional<ZodNullable<ZodString>>; }, $strip>>.optional(): z.ZodOptional<z.ZodObject<{
    lastEventId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
}, z.core.$strip>>
optional
(),
) .
ProcedureBuilder<object, object, object, { lastEventId?: string | null | undefined; } | undefined, { lastEventId?: string | null | undefined; } | undefined, UnsetMarker, UnsetMarker, false, DefaultErrorShape>.subscription<AsyncGenerator<TrackedEnvelope<Post>, void, any>>(resolver: ProcedureResolver<object, object, object, {
    lastEventId?: string | null | undefined;
} | undefined, UnsetMarker, AsyncGenerator<TrackedEnvelope<Post>, void, any>>): SubscriptionProcedure<{
    input: {
        lastEventId?: string | null | undefined;
    } | undefined;
    output: AsyncIterable<TrackedData<Post>, void, any>;
    meta: object;
    errorShape: DefaultErrorShape;
}> (+1 overload)
subscription
(async function* (
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: string | null | undefined;
} | undefined>
opts
) {
if (
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: string | null | undefined;
} | undefined>
opts
.
ProcedureResolverOptions<object, object, object, { lastEventId?: string | null | undefined; } | undefined>.input: {
    lastEventId?: string | null | undefined;
} | undefined
input
?.lastEventId?: string | null | undefinedlastEventId) {
// [...] get the posts since the last event id and yield them } // listen for new events for await (const [const data: anydata] of function on(emitter: EventEmitter, eventName: string | symbol, options?: EventEmitter.OnOptions): NodeJS.AsyncIterator<any[]> (+1 overload)on(const ee: EventEmitter<any>ee, 'add', { // Passing the AbortSignal from the request automatically cancels the event emitter when the subscription is aborted EventEmitter<T extends EventMap<T> = any>.Abortable.signal?: AbortSignal | undefinedsignal:
opts: ProcedureResolverOptions<object, object, object, {
    lastEventId?: string | null | undefined;
} | undefined>
opts
.ProcedureResolverOptions<TContext, _TMeta, TContextOverridesIn, TInputOut>.signal: AbortSignal | undefinedsignal,
})) { const const post: Postpost = const data: anydata as
type Post = {
    id: string;
    title: string;
}
Post
;
// tracking the post id ensures the client can reconnect at any time and get the latest events since this id yield tracked<Post>(id: string, data: Post): TrackedEnvelope<Post>tracked(const post: Postpost.id: stringid, const post: Postpost); } }), });

WebSockets RPC Specification

You can read more details by drilling into the TypeScript definitions:

query / mutation

Request

interface RequestMessage {
  RequestMessage.id: string | numberid: number | string;
  RequestMessage.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
  RequestMessage.method: "query" | "mutation"method: 'query' | 'mutation';
  
RequestMessage.params: {
    path: string;
    input?: unknown;
}
params
: {
path: stringpath: string; input?: unknowninput?: unknown; // <-- pass input of procedure, serialized by transformer }; }

Response

... below, or an error.

interface ResponseMessage {
  ResponseMessage.id: string | numberid: number | string;
  ResponseMessage.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
  
ResponseMessage.result: {
    type: "data";
    data: TOutput;
}
result
: {
type: "data"type: 'data'; // always 'data' for mutation / queries data: anydata: type TOutput = anyTOutput; // output from procedure }; }

subscription / subscription.stop

Start a subscription

interface SubscriptionRequest {
  SubscriptionRequest.id: string | numberid: number | string;
  SubscriptionRequest.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
  SubscriptionRequest.method: "subscription"method: 'subscription';
  
SubscriptionRequest.params: {
    path: string;
    input?: unknown;
}
params
: {
path: stringpath: string; input?: unknowninput?: unknown; // <-- pass input of procedure, serialized by transformer }; }

To cancel a subscription, call subscription.stop

interface SubscriptionStopRequest {
  SubscriptionStopRequest.id: string | numberid: number | string; // <-- id of your created subscription
  SubscriptionStopRequest.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
  SubscriptionStopRequest.method: "subscription.stop"method: 'subscription.stop';
}

Subscription response shape

... below, or an error.

interface SubscriptionResponse {
  SubscriptionResponse.id: string | numberid: number | string;
  SubscriptionResponse.jsonrpc?: "2.0" | undefinedjsonrpc?: '2.0';
  
SubscriptionResponse.result: {
    type: "data";
    data: TData;
} | {
    type: "started";
} | {
    type: "stopped";
}
result
:
| { type: "data"type: 'data'; data: anydata: type TData = anyTData; // subscription emitted data } | { type: "started"type: 'started'; // subscription started } | { type: "stopped"type: 'stopped'; // subscription stopped }; }

Connection params

If the connection is initialized with ?connectionParams=1, the first message has to be connection params.

interface ConnectionParamsMessage {
  ConnectionParamsMessage.data: Record<string, string> | nulldata: type Record<K extends keyof any, T> = { [P in K]: T; }Record<string, string> | null;
  ConnectionParamsMessage.method: "connectionParams"method: 'connectionParams';
}

Errors

See https://www.jsonrpc.org/specification#error_object or Error Formatting.

Notifications from Server to Client

{ id: null, type: 'reconnect' }

Tells clients to reconnect before shutting down the server. Invoked by wssHandler.broadcastReconnectNotification().