@geekmidas/constructs 10.0.0-alpha.125 → 10.0.0-alpha.127

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-api.cjs","names":["servicesOf","idsOf","randomBytes","HttpError","runWithRequestContext","ServiceDiscovery","canonicalId","provideKey","DEFAULT_LOGGER","envParserFor","serviceKey","edgeTo"],"sources":["../src/websocket/transport.ts","../src/websocket/publisher.ts","../src/websocket/route.ts","../src/websocket/server.ts","../src/websocket-api.ts"],"sourcesContent":["/**\n * How a send reaches the process holding the socket.\n *\n * A connection id names the process it lives on — `<instance>.<n>` — so a send\n * needs no registry: it is published to that instance's channel and nowhere\n * else. A broadcast goes to every instance. On a stack the channels are Redis\n * pub/sub, so an endpoint in another container reaches a socket the same way a\n * route beside it does; in one process (a test, a single dev server) they are\n * a map.\n */\n\n/** What one process is asked to do with the sockets it holds. */\nexport type Delivery =\n\t| { kind: 'send'; ids: readonly string[]; payload: string }\n\t| { kind: 'broadcast'; payload: string; except?: readonly string[] }\n\t| { kind: 'close'; ids: readonly string[]; code: number; reason: string };\n\nexport interface WebSocketTransport {\n\t/** Deliver to one instance, or to every instance when it is `'*'`. */\n\tpublish(instance: string, delivery: Delivery): Promise<void>;\n\t/** Receive what is published to `instance`, and every broadcast. */\n\tsubscribe(\n\t\tinstance: string,\n\t\treceive: (delivery: Delivery) => void,\n\t): Promise<void>;\n\tclose(): Promise<void>;\n}\n\n/** Every instance — what a broadcast is published to. */\nexport const EVERY_INSTANCE = '*';\n\n/** A connection string names no transport this package knows. */\nexport class UnsupportedWebSocketTransport extends Error {\n\tconstructor(readonly connectionString: string) {\n\t\tsuper(\n\t\t\t`'${redact(connectionString)}' is not a WebSocket publisher connection string. ` +\n\t\t\t\t'Use redis://, rediss:// or memory:// — gkm composes it as <ID>_PUBLISHER_CONNECTION_STRING.',\n\t\t);\n\t\tthis.name = 'UnsupportedWebSocketTransport';\n\t}\n}\n\n/** The scheme alone, so an error never prints a password. */\nfunction redact(connectionString: string): string {\n\tconst scheme = connectionString.split('://')[0];\n\treturn scheme && scheme !== connectionString\n\t\t? `${scheme}://…`\n\t\t: connectionString.slice(0, 12);\n}\n\n// On globalThis, as the shutdown hooks are: a package loaded as ESM and CJS\n// would otherwise hold two maps, and a publisher would talk to the empty one.\nconst key = Symbol.for('@geekmidas/constructs/websocket/memory');\nconst registry = globalThis as Record<\n\tsymbol,\n\tMap<string, Map<string, (delivery: Delivery) => void>> | undefined\n>;\nregistry[key] ??= new Map();\nconst channels = registry[key];\n\n/**\n * In one process: the server and whoever sends meet in a map, keyed by the\n * channel the connection string names.\n */\nexport class MemoryTransport implements WebSocketTransport {\n\tprivate readonly subscribed = new Set<string>();\n\n\tconstructor(private readonly channel: string) {}\n\n\tasync publish(instance: string, delivery: Delivery): Promise<void> {\n\t\tconst instances = channels.get(this.channel);\n\t\tif (!instances) return;\n\t\tif (instance === EVERY_INSTANCE) {\n\t\t\tfor (const receive of instances.values()) receive(delivery);\n\t\t\treturn;\n\t\t}\n\t\tinstances.get(instance)?.(delivery);\n\t}\n\n\tasync subscribe(\n\t\tinstance: string,\n\t\treceive: (delivery: Delivery) => void,\n\t): Promise<void> {\n\t\tlet instances = channels.get(this.channel);\n\t\tif (!instances) {\n\t\t\tinstances = new Map();\n\t\t\tchannels.set(this.channel, instances);\n\t\t}\n\t\tinstances.set(instance, receive);\n\t\tthis.subscribed.add(instance);\n\t}\n\n\tasync close(): Promise<void> {\n\t\tconst instances = channels.get(this.channel);\n\t\tfor (const instance of this.subscribed) instances?.delete(instance);\n\t\tthis.subscribed.clear();\n\t}\n}\n\n/** The subset of an ioredis client the transport uses. */\ninterface RedisClient {\n\tpublish(channel: string, message: string): Promise<number>;\n\tsubscribe(...channels: string[]): Promise<unknown>;\n\ton(\n\t\tevent: 'message',\n\t\tlistener: (channel: string, message: string) => void,\n\t): unknown;\n\tduplicate(): RedisClient;\n\tquit(): Promise<unknown>;\n}\n\n/**\n * Over Redis pub/sub: one channel per instance and one for broadcasts, under\n * a prefix that keeps two surfaces — or two projects on one Redis — apart.\n * Channels are not scoped by database index, so the prefix is what does it.\n *\n * Publishing and subscribing take separate connections, because a connection\n * that has subscribed can do nothing else.\n */\nexport class RedisTransport implements WebSocketTransport {\n\tprivate subscriber?: RedisClient;\n\tprivate closed?: Promise<void>;\n\n\tconstructor(\n\t\tprivate readonly client: RedisClient,\n\t\tprivate readonly prefix: string,\n\t) {}\n\n\tprivate channel(instance: string): string {\n\t\treturn `${this.prefix}:${instance}`;\n\t}\n\n\tasync publish(instance: string, delivery: Delivery): Promise<void> {\n\t\tawait this.client.publish(this.channel(instance), JSON.stringify(delivery));\n\t}\n\n\tasync subscribe(\n\t\tinstance: string,\n\t\treceive: (delivery: Delivery) => void,\n\t): Promise<void> {\n\t\tconst subscriber = this.client.duplicate();\n\t\tthis.subscriber = subscriber;\n\t\tconst own = this.channel(instance);\n\t\tconst every = this.channel(EVERY_INSTANCE);\n\t\tsubscriber.on('message', (channel, message) => {\n\t\t\tif (channel !== own && channel !== every) return;\n\t\t\treceive(JSON.parse(message) as Delivery);\n\t\t});\n\t\tawait subscriber.subscribe(own, every);\n\t}\n\n\t/** Idempotent: the server and a shutdown hook may both close it. */\n\tclose(): Promise<void> {\n\t\tthis.closed ??= Promise.all([\n\t\t\tthis.subscriber?.quit(),\n\t\t\tthis.client.quit(),\n\t\t]).then(() => undefined);\n\t\treturn this.closed;\n\t}\n}\n\n/**\n * The transport a connection string names.\n *\n * `redis://` and `rediss://` take an optional `channel` query parameter, the\n * prefix every channel is named under; `memory://<channel>` is in-process.\n * Absent, the channel is `fallback` — the surface's own.\n */\nexport async function transportFromConnectionString(\n\tconnectionString: string,\n\tfallback: string,\n): Promise<WebSocketTransport> {\n\t// Not parsed as a URL: a channel is `gkm:ws:<id>`, colons and all.\n\tif (connectionString.startsWith('memory://')) {\n\t\treturn new MemoryTransport(\n\t\t\tconnectionString.slice('memory://'.length) || fallback,\n\t\t);\n\t}\n\n\tconst url = URL.canParse(connectionString)\n\t\t? new URL(connectionString)\n\t\t: undefined;\n\tif (!url) throw new UnsupportedWebSocketTransport(connectionString);\n\n\tif (url.protocol === 'redis:' || url.protocol === 'rediss:') {\n\t\tconst channel = url.searchParams.get('channel') || fallback;\n\t\turl.searchParams.delete('channel');\n\t\tconst { Redis } = await import('ioredis');\n\t\tconst client = new Redis(url.toString(), {\n\t\t\t// A publish while Redis restarts waits for it rather than failing the\n\t\t\t// request that sent it.\n\t\t\tmaxRetriesPerRequest: null,\n\t\t\tlazyConnect: false,\n\t\t});\n\t\treturn new RedisTransport(client as unknown as RedisClient, channel);\n\t}\n\n\tthrow new UnsupportedWebSocketTransport(connectionString);\n}\n","import type { StandardSchemaV1 } from '@standard-schema/spec';\nimport {\n\ttype Delivery,\n\tEVERY_INSTANCE,\n\ttype WebSocketTransport,\n} from './transport';\n\n/** The instance a connection id was issued by: everything before its dot. */\nexport function instanceOf(connectionId: string): string | undefined {\n\tconst dot = connectionId.indexOf('.');\n\treturn dot > 0 ? connectionId.slice(0, dot) : undefined;\n}\n\n/**\n * What a surface sends its sockets, by action — `WebSocketApiConfig.messages`.\n */\nexport type WebSocketMessages = Record<string, StandardSchemaV1>;\n\n/** A surface that declares no messages: any action, any data. */\nexport type NoMessages = Record<never, never>;\n\n/** The actions a publisher sends: the declared ones, or any. */\nexport type MessageAction<TMessages extends WebSocketMessages> = [\n\tkeyof TMessages,\n] extends [never]\n\t? string\n\t: keyof TMessages & string;\n\n/** What `action`'s data is, as its schema takes it. */\nexport type MessageData<\n\tTMessages extends WebSocketMessages,\n\tTAction extends string,\n> = TAction extends keyof TMessages\n\t? StandardSchemaV1.InferInput<TMessages[TAction]>\n\t: unknown;\n\n/** A message sent on an action the surface does not declare. */\nexport class UnknownWebSocketMessage extends Error {\n\tconstructor(\n\t\treadonly surface: string,\n\t\treadonly action: string,\n\t\treadonly declared: readonly string[],\n\t) {\n\t\tsuper(\n\t\t\t`${surface} sends no '${action}' message. It declares ${declared\n\t\t\t\t.map((a) => `'${a}'`)\n\t\t\t\t.join(', ')} — send one of them, or add '${action}' to its messages.`,\n\t\t);\n\t\tthis.name = 'UnknownWebSocketMessage';\n\t}\n}\n\n/** A message whose data its action's schema refused. */\nexport class InvalidWebSocketMessage extends Error {\n\tconstructor(\n\t\treadonly surface: string,\n\t\treadonly action: string,\n\t\treadonly issues: readonly StandardSchemaV1.Issue[],\n\t) {\n\t\tsuper(\n\t\t\t`${surface}'s '${action}' message was refused by its schema: ${issues\n\t\t\t\t.map((issue) => issue.message)\n\t\t\t\t.join('; ')}. Nothing was sent.`,\n\t\t);\n\t\tthis.name = 'InvalidWebSocketMessage';\n\t}\n}\n\n/**\n * How anything reaches a socket — `services.realtime` in an endpoint, and\n * `sockets` in a route of the surface itself.\n *\n * A message is `{ action, data }`, the shape a client's frames arrive in.\n * Where the surface declares its messages, `action` is one of them and\n * `data` is typed and checked by its schema before anything is sent; where it\n * declares none, any action and any JSON value go. A message is serialised\n * once, whatever the number of sockets it goes to. An id whose process has\n * gone is dropped silently, as API Gateway's `postToConnection` to a closed\n * connection is a no-op the sender rarely wants to handle.\n */\nexport class WebSocketPublisher<\n\tTMessages extends WebSocketMessages = NoMessages,\n> {\n\tconstructor(\n\t\tprivate readonly transport: WebSocketTransport,\n\t\t/**\n\t\t * Delivers to this process's own sockets without a round trip through\n\t\t * the transport — set on the server's own publisher.\n\t\t */\n\t\tprivate readonly local?: {\n\t\t\tinstance: string;\n\t\t\tdeliver(delivery: Delivery): void;\n\t\t},\n\t\t/** The surface's declared messages, which every send is checked by. */\n\t\tprivate readonly declared?: { surface: string; messages: TMessages },\n\t) {}\n\n\t/** Send `action`'s message to each connection. */\n\tasync send<TAction extends MessageAction<TMessages>>(\n\t\tids: string | readonly string[],\n\t\taction: TAction,\n\t\tdata: MessageData<TMessages, TAction>,\n\t): Promise<void> {\n\t\tconst payload = await this.payload(action, data);\n\t\tawait this.each(ids, (group) => ({ kind: 'send', ids: group, payload }));\n\t}\n\n\t/** Send `action`'s message to every open connection, on every process. */\n\tasync broadcast<TAction extends MessageAction<TMessages>>(\n\t\taction: TAction,\n\t\tdata: MessageData<TMessages, TAction>,\n\t\toptions: { except?: readonly string[] } = {},\n\t): Promise<void> {\n\t\tconst payload = await this.payload(action, data);\n\t\tawait this.transport.publish(EVERY_INSTANCE, {\n\t\t\tkind: 'broadcast',\n\t\t\tpayload,\n\t\t\t...(options.except?.length ? { except: options.except } : {}),\n\t\t});\n\t}\n\n\t/** Close each connection, with a close code and reason its client sees. */\n\tasync close(\n\t\tids: string | readonly string[],\n\t\tcode = 1000,\n\t\treason = '',\n\t): Promise<void> {\n\t\tawait this.each(ids, (group) => ({\n\t\t\tkind: 'close',\n\t\t\tids: group,\n\t\t\tcode,\n\t\t\treason,\n\t\t}));\n\t}\n\n\t/**\n\t * The frame, checked by the action's schema where the surface declares\n\t * its messages.\n\t *\n\t * @throws {UnknownWebSocketMessage}, {InvalidWebSocketMessage}\n\t */\n\tprivate async payload(action: string, data: unknown): Promise<string> {\n\t\tif (!this.declared) return JSON.stringify({ action, data });\n\n\t\tconst { surface, messages } = this.declared;\n\t\tconst schema = messages[action];\n\t\tif (!schema) {\n\t\t\tthrow new UnknownWebSocketMessage(surface, action, Object.keys(messages));\n\t\t}\n\t\tconst result = await schema['~standard'].validate(data);\n\t\tif (result.issues) {\n\t\t\tthrow new InvalidWebSocketMessage(surface, action, result.issues);\n\t\t}\n\t\treturn JSON.stringify({ action, data: result.value });\n\t}\n\n\t/** One delivery per process that holds any of `ids`. */\n\tprivate async each(\n\t\tids: string | readonly string[],\n\t\tdelivery: (group: string[]) => Delivery,\n\t): Promise<void> {\n\t\tconst byInstance = new Map<string, string[]>();\n\t\tfor (const id of typeof ids === 'string' ? [ids] : ids) {\n\t\t\tconst instance = instanceOf(id);\n\t\t\tif (!instance) continue;\n\t\t\tconst group = byInstance.get(instance) ?? [];\n\t\t\tgroup.push(id);\n\t\t\tbyInstance.set(instance, group);\n\t\t}\n\n\t\tawait Promise.all(\n\t\t\t[...byInstance].map(([instance, group]) =>\n\t\t\t\tthis.local?.instance === instance\n\t\t\t\t\t? this.local.deliver(delivery(group))\n\t\t\t\t\t: this.transport.publish(instance, delivery(group)),\n\t\t\t),\n\t\t);\n\t}\n}\n","import type { Logger } from '@geekmidas/logger';\nimport type { InferStandardSchema } from '@geekmidas/schema';\nimport type { Service, ServiceRecord } from '@geekmidas/services';\nimport type { StandardSchemaV1 } from '@standard-schema/spec';\nimport {\n\ttype Consumable,\n\tidsOf,\n\ttype ServicesOf,\n\tservicesOf,\n} from '../construct-interface';\nimport type {\n\tNoMessages,\n\tWebSocketMessages,\n\tWebSocketPublisher,\n} from './publisher';\n\n/** One open connection, as a handler sees it. */\nexport interface Connection {\n\treadonly id: string;\n\t/**\n\t * What the connect handler returned — who the connection is for, the room\n\t * it joined. `{}` when it returned nothing.\n\t */\n\treadonly data: Record<string, unknown>;\n}\n\ninterface BaseContext<\n\tTServices extends Service[],\n\tTMessages extends WebSocketMessages,\n> {\n\tservices: ServiceRecord<TServices>;\n\tlogger: Logger;\n\t/** Every socket this surface holds, on whichever process holds it. */\n\tsockets: WebSocketPublisher<TMessages>;\n}\n\nexport interface ConnectContext<\n\tTServices extends Service[],\n\tTMessages extends WebSocketMessages = NoMessages,\n> extends BaseContext<TServices, TMessages> {\n\tconnectionId: string;\n\theaders: Headers;\n\tquery: URLSearchParams;\n\t/**\n\t * Whose connection it is, from the surface's `.auth()` — `null` when the\n\t * upgrade carried no session, and when the surface names no authenticator.\n\t * Throw an `UnauthorizedError` to refuse it.\n\t */\n\tsession: unknown;\n}\n\nexport interface MessageContext<\n\tTBody,\n\tTServices extends Service[],\n\tTMessages extends WebSocketMessages = NoMessages,\n> extends BaseContext<TServices, TMessages> {\n\tconnection: Connection;\n\tbody: TBody;\n\t/** Send to this connection alone. */\n\treply(message: unknown): void;\n}\n\nexport interface DisconnectContext<\n\tTServices extends Service[],\n\tTMessages extends WebSocketMessages = NoMessages,\n> extends BaseContext<TServices, TMessages> {\n\tconnection: Connection;\n\tcode: number;\n\treason: string;\n}\n\n/** What a frame is dispatched on: an action, or the connection's lifecycle. */\nexport type RouteKind = 'connect' | 'disconnect' | 'message';\n\n/**\n * A handler bound to a WebSocket surface — what `realtime.route('chat.send')\n * …handle()` returns, and what a file exports.\n *\n * Carries `surface` and `constructs` the way an endpoint does, so discovery\n * records what its process reaches without knowing what a route is.\n */\nexport class WebSocketRoute {\n\treadonly __IS_WEBSOCKET_ROUTE__ = true;\n\n\tstatic isRoute(value: unknown): value is WebSocketRoute {\n\t\treturn Boolean(\n\t\t\tvalue &&\n\t\t\t\t(value as { __IS_WEBSOCKET_ROUTE__?: unknown })\n\t\t\t\t\t.__IS_WEBSOCKET_ROUTE__ === true,\n\t\t);\n\t}\n\n\tconstructor(\n\t\treadonly kind: RouteKind,\n\t\t/** The `action` it answers; `$connect` and `$disconnect` for the lifecycle. */\n\t\treadonly action: string,\n\t\treadonly surface: { id: string },\n\t\treadonly services: readonly Service[],\n\t\t/** Construct ids it depends on — its edges in the manifest. */\n\t\treadonly constructs: readonly string[],\n\t\treadonly handler: (context: never) => unknown,\n\t\treadonly schema?: StandardSchemaV1,\n\t) {}\n}\n\n/** The builder `connect()`, `disconnect()` and `route()` start. */\nexport class WebSocketRouteBuilder<\n\tTKind extends RouteKind,\n\tTSchema extends StandardSchemaV1 | undefined = undefined,\n\tTServices extends Service[] = [],\n\t/** The surface's declared messages, which `sockets` sends. */\n\tTMessages extends WebSocketMessages = NoMessages,\n> {\n\tconstructor(\n\t\tprivate readonly kind: TKind,\n\t\tprivate readonly action: string,\n\t\tprivate readonly surface: { id: string },\n\t\tprivate readonly _services: readonly Service[] = [],\n\t\tprivate readonly _constructs: readonly string[] = [],\n\t\tprivate readonly schema?: TSchema,\n\t) {}\n\n\t/** The schema a frame's `data` must satisfy before the handler runs. */\n\tbody<S extends StandardSchemaV1>(\n\t\tschema: S,\n\t): WebSocketRouteBuilder<TKind, S, TServices, TMessages> {\n\t\treturn new WebSocketRouteBuilder(\n\t\t\tthis.kind,\n\t\t\tthis.action,\n\t\t\tthis.surface,\n\t\t\tthis._services,\n\t\t\tthis._constructs,\n\t\t\tschema,\n\t\t);\n\t}\n\n\t/** What the handler reaches, as `services.<id>` — per route, as an endpoint's. */\n\tdependsOn<const T extends readonly Consumable[]>(\n\t\tconstructs: T,\n\t): WebSocketRouteBuilder<\n\t\tTKind,\n\t\tTSchema,\n\t\t[...TServices, ...(ServicesOf<T> extends Service[] ? ServicesOf<T> : [])],\n\t\tTMessages\n\t> {\n\t\treturn new WebSocketRouteBuilder(\n\t\t\tthis.kind,\n\t\t\tthis.action,\n\t\t\tthis.surface,\n\t\t\t[...this._services, ...(servicesOf(constructs) as Service[])],\n\t\t\tidsOf(constructs, this._constructs),\n\t\t\tthis.schema,\n\t\t) as never;\n\t}\n\n\t/**\n\t * The handler. A message handler's return value, when there is one, is\n\t * sent back to the connection as the reply to that frame; a connect\n\t * handler's is kept as the connection's `data`.\n\t */\n\thandle(\n\t\thandler: TKind extends 'connect'\n\t\t\t? (\n\t\t\t\t\tcontext: ConnectContext<TServices, TMessages>,\n\t\t\t\t) =>\n\t\t\t\t\t| Record<string, unknown>\n\t\t\t\t\t| undefined\n\t\t\t\t\t| Promise<Record<string, unknown> | undefined>\n\t\t\t: TKind extends 'disconnect'\n\t\t\t\t? (context: DisconnectContext<TServices, TMessages>) => unknown\n\t\t\t\t: (\n\t\t\t\t\t\tcontext: MessageContext<\n\t\t\t\t\t\t\tTSchema extends StandardSchemaV1\n\t\t\t\t\t\t\t\t? InferStandardSchema<TSchema>\n\t\t\t\t\t\t\t\t: unknown,\n\t\t\t\t\t\t\tTServices,\n\t\t\t\t\t\t\tTMessages\n\t\t\t\t\t\t>,\n\t\t\t\t\t) => unknown,\n\t): WebSocketRoute {\n\t\treturn new WebSocketRoute(\n\t\t\tthis.kind,\n\t\t\tthis.action,\n\t\t\tthis.surface,\n\t\t\tthis._services,\n\t\t\tthis._constructs,\n\t\t\thandler as (context: never) => unknown,\n\t\t\tthis.schema,\n\t\t);\n\t}\n}\n","/**\n * The process that holds a WebSocket surface's sockets.\n *\n * Node's own `http` server and `ws`, nothing between them: a frame is parsed,\n * validated and handed to its route, and that is the whole hot path. The HTTP\n * server is whatever the entry started — the generated one serves `/health`\n * on the same port — and this attaches to its `upgrade` event.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport type { IncomingMessage, Server } from 'node:http';\nimport type { Duplex } from 'node:stream';\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport { HttpError } from '@geekmidas/errors';\nimport type { Logger } from '@geekmidas/logger';\nimport { runWithRequestContext, ServiceDiscovery } from '@geekmidas/services';\nimport type { WebSocket, WebSocketServer } from 'ws';\nimport type { Authenticator } from '../construct-interface';\nimport {\n\ttype NoMessages,\n\ttype WebSocketMessages,\n\tWebSocketPublisher,\n} from './publisher';\nimport type { Connection, WebSocketRoute } from './route';\nimport type { Delivery, WebSocketTransport } from './transport';\n\n/** Close codes a client sees. */\nexport const CLOSE = {\n\tnormal: 1000,\n\t/** The process is stopping; reconnect and land on another. */\n\tserviceRestart: 1012,\n\t/** It stopped reading, and what was queued for it passed the limit. */\n\ttooSlow: 1013,\n} as const;\n\n/** A frame a client sends: `{ \"action\": \"chat.send\", \"id\": 1, \"data\": … }`. */\ninterface InboundFrame {\n\taction: string;\n\t/** Echoed on the reply, so a client can pair them. */\n\tid?: string | number;\n\tdata?: unknown;\n}\n\n/** Two routes answer one action. */\nexport class DuplicateWebSocketRoute extends Error {\n\tconstructor(\n\t\treadonly surface: string,\n\t\treadonly action: string,\n\t) {\n\t\tsuper(\n\t\t\t`Two routes on ${surface} answer '${action}'. Each action has one handler — rename one of them.`,\n\t\t);\n\t\tthis.name = 'DuplicateWebSocketRoute';\n\t}\n}\n\nexport interface WebSocketServerOptions<\n\tTMessages extends WebSocketMessages = NoMessages,\n> {\n\tsurface: string;\n\t/** What the surface sends its sockets — see `WebSocketApiConfig.messages`. */\n\tmessages?: TMessages;\n\troutes: readonly WebSocketRoute[];\n\ttransport: WebSocketTransport;\n\tenvParser: EnvironmentParser<{}>;\n\tlogger: Logger;\n\tauthenticator?: Authenticator;\n\t/**\n\t * Origins a browser may open a socket from. A request with an `Origin`\n\t * not in the list is refused with 403; one with none — a server, a load\n\t * generator — is not a browser, and is let through.\n\t */\n\ttrustedOrigins: readonly string[];\n\t/** How often each socket is pinged; one that missed the last is dropped. */\n\theartbeatMs?: number;\n\t/** The largest frame accepted, in bytes. */\n\tmaxPayload?: number;\n\t/** Queued bytes a socket may hold before it is closed as too slow. */\n\tmaxBufferedBytes?: number;\n}\n\ninterface Held {\n\tsocket: WebSocket;\n\tconnection: Connection;\n\talive: boolean;\n}\n\nexport class WebSocketSurfaceServer<\n\tTMessages extends WebSocketMessages = NoMessages,\n> {\n\t/** This process, as connection ids name it. */\n\treadonly instance = randomBytes(6).toString('hex');\n\t/** `sockets` in a route: local sends skip the transport. */\n\treadonly publisher: WebSocketPublisher<TMessages>;\n\n\tprivate readonly held = new Map<string, Held>();\n\tprivate readonly routes = new Map<string, WebSocketRoute>();\n\tprivate readonly lifecycle: {\n\t\tconnect?: WebSocketRoute;\n\t\tdisconnect?: WebSocketRoute;\n\t} = {};\n\tprivate readonly heartbeat: NodeJS.Timeout;\n\tprivate next = 0;\n\tprivate closing = false;\n\n\tprivate constructor(\n\t\tprivate readonly wss: WebSocketServer,\n\t\tprivate readonly options: WebSocketServerOptions<TMessages>,\n\t) {\n\t\tfor (const route of options.routes) {\n\t\t\tif (route.surface.id !== options.surface) continue;\n\t\t\tif (route.kind === 'message') {\n\t\t\t\tif (this.routes.has(route.action)) {\n\t\t\t\t\tthrow new DuplicateWebSocketRoute(options.surface, route.action);\n\t\t\t\t}\n\t\t\t\tthis.routes.set(route.action, route);\n\t\t\t} else {\n\t\t\t\tif (this.lifecycle[route.kind]) {\n\t\t\t\t\tthrow new DuplicateWebSocketRoute(options.surface, route.action);\n\t\t\t\t}\n\t\t\t\tthis.lifecycle[route.kind] = route;\n\t\t\t}\n\t\t}\n\n\t\tthis.publisher = new WebSocketPublisher<TMessages>(\n\t\t\toptions.transport,\n\t\t\t{\n\t\t\t\tinstance: this.instance,\n\t\t\t\tdeliver: (delivery) => this.deliver(delivery),\n\t\t\t},\n\t\t\toptions.messages\n\t\t\t\t? { surface: options.surface, messages: options.messages }\n\t\t\t\t: undefined,\n\t\t);\n\n\t\tthis.heartbeat = setInterval(\n\t\t\t() => this.beat(),\n\t\t\toptions.heartbeatMs ?? 30_000,\n\t\t);\n\t\tthis.heartbeat.unref();\n\t}\n\n\tstatic async start<TMessages extends WebSocketMessages = NoMessages>(\n\t\toptions: WebSocketServerOptions<TMessages>,\n\t): Promise<WebSocketSurfaceServer<TMessages>> {\n\t\tconst { WebSocketServer } = await import('ws');\n\t\tconst wss = new WebSocketServer({\n\t\t\tnoServer: true,\n\t\t\tmaxPayload: options.maxPayload ?? 1024 * 1024,\n\t\t\t// Compression costs a zlib context per socket — tens of KB each,\n\t\t\t// the largest share of a connection's memory — for frames that are\n\t\t\t// mostly small JSON.\n\t\t\tperMessageDeflate: false,\n\t\t\tclientTracking: false,\n\t\t});\n\t\tconst server = new WebSocketSurfaceServer<TMessages>(wss, options);\n\t\tawait options.transport.subscribe(server.instance, (delivery) =>\n\t\t\tserver.deliver(delivery),\n\t\t);\n\t\treturn server;\n\t}\n\n\t/** How many sockets this process holds. */\n\tget size(): number {\n\t\treturn this.held.size;\n\t}\n\n\t/** Take over `server`'s upgrades. */\n\tattach(server: Server): void {\n\t\tserver.on('upgrade', (request, socket, head) => {\n\t\t\tvoid this.upgrade(request, socket, head);\n\t\t});\n\t}\n\n\t/** Upgrade one request, or refuse it with a status line. */\n\tasync upgrade(\n\t\trequest: IncomingMessage,\n\t\tsocket: Duplex,\n\t\thead: Buffer,\n\t): Promise<void> {\n\t\tif (this.closing) return refuse(socket, 503, 'Service Unavailable');\n\n\t\tconst origin = request.headers.origin;\n\t\tif (origin && !this.options.trustedOrigins.includes(origin)) {\n\t\t\treturn refuse(socket, 403, 'Forbidden');\n\t\t}\n\n\t\tconst connectionId = `${this.instance}.${(this.next++).toString(36)}`;\n\t\tconst headers = headersOf(request);\n\t\tlet data: Record<string, unknown> = {};\n\n\t\tconst connect = this.lifecycle.connect;\n\t\tif (connect) {\n\t\t\ttry {\n\t\t\t\tconst session = this.options.authenticator\n\t\t\t\t\t? await this.options.authenticator.verify(\n\t\t\t\t\t\t\theaders,\n\t\t\t\t\t\t\tthis.options.envParser,\n\t\t\t\t\t\t)\n\t\t\t\t\t: null;\n\t\t\t\tconst returned = await this.run(connect, connectionId, (base) => ({\n\t\t\t\t\t...base,\n\t\t\t\t\tconnectionId,\n\t\t\t\t\theaders,\n\t\t\t\t\tquery: new URL(request.url ?? '/', 'http://localhost').searchParams,\n\t\t\t\t\tsession,\n\t\t\t\t}));\n\t\t\t\tif (returned && typeof returned === 'object') {\n\t\t\t\t\tdata = returned as Record<string, unknown>;\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tif (error instanceof HttpError) {\n\t\t\t\t\treturn refuse(socket, error.statusCode, error.statusMessage);\n\t\t\t\t}\n\t\t\t\tthis.options.logger.error(\n\t\t\t\t\t{ err: error, connectionId },\n\t\t\t\t\t'WebSocket connect handler failed',\n\t\t\t\t);\n\t\t\t\treturn refuse(socket, 500, 'Internal Server Error');\n\t\t\t}\n\t\t}\n\n\t\t// The client may have gone while the connect handler ran.\n\t\tif (socket.destroyed) return;\n\n\t\tthis.wss.handleUpgrade(request, socket, head, (ws) =>\n\t\t\tthis.open(ws, { id: connectionId, data }),\n\t\t);\n\t}\n\n\tprivate open(socket: WebSocket, connection: Connection): void {\n\t\tconst held: Held = { socket, connection, alive: true };\n\t\tthis.held.set(connection.id, held);\n\n\t\tsocket.on('pong', () => {\n\t\t\theld.alive = true;\n\t\t});\n\t\tsocket.on('message', (raw, isBinary) => {\n\t\t\theld.alive = true;\n\t\t\tif (isBinary) {\n\t\t\t\treturn this.error(held, undefined, 'Frames are JSON text.');\n\t\t\t}\n\t\t\tvoid this.dispatch(held, raw.toString());\n\t\t});\n\t\tsocket.on('error', () => socket.terminate());\n\t\tsocket.on('close', (code, reason) => {\n\t\t\tthis.held.delete(connection.id);\n\t\t\tconst disconnect = this.lifecycle.disconnect;\n\t\t\tif (!disconnect) return;\n\t\t\tvoid this.run(disconnect, connection.id, (base) => ({\n\t\t\t\t...base,\n\t\t\t\tconnection,\n\t\t\t\tcode,\n\t\t\t\treason: reason.toString(),\n\t\t\t})).catch((error) =>\n\t\t\t\tthis.options.logger.error(\n\t\t\t\t\t{ err: error, connectionId: connection.id },\n\t\t\t\t\t'WebSocket disconnect handler failed',\n\t\t\t\t),\n\t\t\t);\n\t\t});\n\t}\n\n\t/** Parse, validate and run one frame. Errors go back as frames, not closes. */\n\tprivate async dispatch(held: Held, text: string): Promise<void> {\n\t\tlet frame: InboundFrame;\n\t\ttry {\n\t\t\tframe = JSON.parse(text);\n\t\t} catch {\n\t\t\treturn this.error(held, undefined, 'Frames are JSON.');\n\t\t}\n\t\tif (!frame || typeof frame.action !== 'string') {\n\t\t\treturn this.error(held, frame?.id, 'A frame names its `action`.');\n\t\t}\n\n\t\tconst route = this.routes.get(frame.action) ?? this.routes.get('$default');\n\t\tif (!route) {\n\t\t\treturn this.error(held, frame.id, `No route answers '${frame.action}'.`);\n\t\t}\n\n\t\tlet body = frame.data;\n\t\tif (route.schema) {\n\t\t\tconst result = await route.schema['~standard'].validate(frame.data);\n\t\t\tif (result.issues) {\n\t\t\t\treturn this.error(\n\t\t\t\t\theld,\n\t\t\t\t\tframe.id,\n\t\t\t\t\t'The frame was invalid.',\n\t\t\t\t\tresult.issues,\n\t\t\t\t);\n\t\t\t}\n\t\t\tbody = result.value;\n\t\t}\n\n\t\ttry {\n\t\t\tconst returned = await this.run(route, held.connection.id, (base) => ({\n\t\t\t\t...base,\n\t\t\t\tconnection: held.connection,\n\t\t\t\tbody,\n\t\t\t\treply: (message: unknown) => this.write(held, JSON.stringify(message)),\n\t\t\t}));\n\t\t\tif (returned !== undefined) {\n\t\t\t\tthis.write(\n\t\t\t\t\theld,\n\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\taction: frame.action,\n\t\t\t\t\t\t...(frame.id !== undefined ? { id: frame.id } : {}),\n\t\t\t\t\t\tdata: returned,\n\t\t\t\t\t}),\n\t\t\t\t);\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tif (error instanceof HttpError) {\n\t\t\t\treturn this.error(held, frame.id, error.message);\n\t\t\t}\n\t\t\tthis.options.logger.error(\n\t\t\t\t{ err: error, connectionId: held.connection.id, action: frame.action },\n\t\t\t\t'WebSocket route failed',\n\t\t\t);\n\t\t\tthis.error(held, frame.id, 'The route failed.');\n\t\t}\n\t}\n\n\t/** Run a handler with its services, inside a request context of its own. */\n\tprivate async run(\n\t\troute: WebSocketRoute,\n\t\tconnectionId: string,\n\t\tcontext: (base: {\n\t\t\tservices: unknown;\n\t\t\tlogger: Logger;\n\t\t\tsockets: WebSocketPublisher<TMessages>;\n\t\t}) => object,\n\t): Promise<unknown> {\n\t\tconst logger = this.options.logger.child({\n\t\t\tconnectionId,\n\t\t\taction: route.action,\n\t\t});\n\t\treturn runWithRequestContext(\n\t\t\t{\n\t\t\t\tlogger,\n\t\t\t\trequestId: connectionId,\n\t\t\t\tstartTime: Date.now(),\n\t\t\t\toperation: `ws ${route.action}`,\n\t\t\t},\n\t\t\tasync () => {\n\t\t\t\tconst services = route.services.length\n\t\t\t\t\t? await ServiceDiscovery.getInstance(this.options.envParser).register(\n\t\t\t\t\t\t\t[...route.services],\n\t\t\t\t\t\t)\n\t\t\t\t\t: {};\n\t\t\t\treturn (route.handler as (context: object) => unknown)(\n\t\t\t\t\tcontext({ services, logger, sockets: this.publisher }),\n\t\t\t\t);\n\t\t\t},\n\t\t);\n\t}\n\n\tprivate error(\n\t\theld: Held,\n\t\tid: InboundFrame['id'],\n\t\tmessage: string,\n\t\tissues?: unknown,\n\t): void {\n\t\tthis.write(\n\t\t\theld,\n\t\t\tJSON.stringify({\n\t\t\t\taction: '$error',\n\t\t\t\t...(id !== undefined ? { id } : {}),\n\t\t\t\tdata: { message, ...(issues ? { issues } : {}) },\n\t\t\t}),\n\t\t);\n\t}\n\n\t/** Write, unless the socket has stopped reading and its queue is full. */\n\tprivate write(held: Held, payload: string): void {\n\t\tconst { socket } = held;\n\t\tif (socket.readyState !== socket.OPEN) return;\n\t\tif (\n\t\t\tsocket.bufferedAmount > (this.options.maxBufferedBytes ?? 4 * 1024 * 1024)\n\t\t) {\n\t\t\tsocket.close(CLOSE.tooSlow, 'Too slow');\n\t\t\treturn;\n\t\t}\n\t\tsocket.send(payload);\n\t}\n\n\t/** What the transport, or a local publisher, asked of this process. */\n\tprivate deliver(delivery: Delivery): void {\n\t\tswitch (delivery.kind) {\n\t\t\tcase 'send':\n\t\t\t\tfor (const id of delivery.ids) {\n\t\t\t\t\tconst held = this.held.get(id);\n\t\t\t\t\tif (held) this.write(held, delivery.payload);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\tcase 'broadcast': {\n\t\t\t\tconst except = delivery.except ? new Set(delivery.except) : undefined;\n\t\t\t\tfor (const [id, held] of this.held) {\n\t\t\t\t\tif (!except?.has(id)) this.write(held, delivery.payload);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tcase 'close':\n\t\t\t\tfor (const id of delivery.ids) {\n\t\t\t\t\tthis.held.get(id)?.socket.close(delivery.code, delivery.reason);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t}\n\t}\n\n\t/** Drop every socket that missed the last ping, and ping the rest. */\n\tprivate beat(): void {\n\t\tfor (const [id, held] of this.held) {\n\t\t\tif (!held.alive) {\n\t\t\t\theld.socket.terminate();\n\t\t\t\tthis.held.delete(id);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\theld.alive = false;\n\t\t\theld.socket.ping();\n\t\t}\n\t}\n\n\t/**\n\t * Stop: refuse new upgrades, close every socket with `code` — 1012 tells\n\t * a client to reconnect, and it lands on a process that is still up — and\n\t * leave the transport.\n\t */\n\tasync close(\n\t\tcode: number = CLOSE.serviceRestart,\n\t\treason = 'Restarting',\n\t): Promise<void> {\n\t\tif (this.closing) return;\n\t\tthis.closing = true;\n\t\tclearInterval(this.heartbeat);\n\n\t\tconst closed = [...this.held.values()].map(\n\t\t\t({ socket }) =>\n\t\t\t\tnew Promise<void>((resolve) => {\n\t\t\t\t\tif (socket.readyState === socket.CLOSED) return resolve();\n\t\t\t\t\tsocket.once('close', () => resolve());\n\t\t\t\t\tsocket.close(code, reason);\n\t\t\t\t\t// A client that never answers the close is cut off.\n\t\t\t\t\tsetTimeout(() => {\n\t\t\t\t\t\tsocket.terminate();\n\t\t\t\t\t\tresolve();\n\t\t\t\t\t}, 2_000).unref();\n\t\t\t\t}),\n\t\t);\n\t\tawait Promise.all(closed);\n\t\tawait this.options.transport.close();\n\t\tthis.wss.close();\n\t}\n}\n\nfunction headersOf(request: IncomingMessage): Headers {\n\tconst headers = new Headers();\n\tfor (const [name, value] of Object.entries(request.headers)) {\n\t\tif (value === undefined) continue;\n\t\tfor (const one of Array.isArray(value) ? value : [value]) {\n\t\t\theaders.append(name, one);\n\t\t}\n\t}\n\treturn headers;\n}\n\nfunction refuse(socket: Duplex, status: number, message: string): void {\n\tif (!socket.destroyed) {\n\t\tsocket.end(\n\t\t\t`HTTP/1.1 ${status} ${message}\\r\\nConnection: close\\r\\nContent-Length: 0\\r\\n\\r\\n`,\n\t\t);\n\t}\n}\n","/**\n * `WebSocketApi` — a surface that holds long-lived connections.\n *\n * A process of its own, like a `RestApi`: it owns an address, its routes are\n * built from it, and they run in its container and nowhere else. A frame is\n * routed on its `action` — `{ \"action\": \"chat.send\", \"data\": … }`, API\n * Gateway's shape — and the connection's lifecycle on `$connect` and\n * `$disconnect`.\n *\n * Anything else reaches a socket through the surface's publisher:\n * `.dependsOn([realtime])` puts `services.realtime` in an endpoint, a cron or\n * a subscriber, and whichever replica holds the socket delivers. What it\n * sends is declared in `messages`, which the publisher is typed by.\n *\n * ```ts\n * export const realtime = new WebSocketApi('Realtime', {\n * path: 'apps/realtime',\n * messages: { 'chat.sent': z.object({ text: z.string() }) },\n * logger,\n * });\n *\n * export const send = realtime\n * .route('chat.send')\n * .body(z.object({ text: z.string() }))\n * .handle(async ({ body, sockets }) => sockets.broadcast('chat.sent', body));\n * ```\n */\n\nimport type { Server } from 'node:http';\nimport type { EnvironmentParser } from '@geekmidas/envkit';\nimport type { Logger } from '@geekmidas/logger';\nimport { DEFAULT_LOGGER } from '@geekmidas/logger/console';\nimport {\n\ttype ConstructName,\n\tcanonicalId,\n\ttype Declaration,\n\ttype Dependency,\n\tprovideKey,\n\tserviceKey,\n} from '@geekmidas/manifest';\nimport type { Service, ServiceRegisterOptions } from '@geekmidas/services';\nimport type { Hono } from 'hono';\nimport {\n\ttype Authenticator,\n\ttype Declarable,\n\tedgeTo,\n} from './construct-interface';\nimport { envParserFor } from './endpoints/surfaceEnv';\nimport { onShutdown } from './shutdown';\nimport type { Telemetry } from './telemetry';\nimport {\n\ttype NoMessages,\n\ttype WebSocketMessages,\n\tWebSocketPublisher,\n} from './websocket/publisher';\nimport { WebSocketRoute, WebSocketRouteBuilder } from './websocket/route';\nimport { WebSocketSurfaceServer } from './websocket/server';\nimport { transportFromConnectionString } from './websocket/transport';\n\nexport {\n\tInvalidWebSocketMessage,\n\ttype MessageAction,\n\ttype MessageData,\n\ttype NoMessages,\n\tUnknownWebSocketMessage,\n\ttype WebSocketMessages,\n\tWebSocketPublisher,\n} from './websocket/publisher';\nexport type {\n\tConnectContext,\n\tConnection,\n\tDisconnectContext,\n\tMessageContext,\n\tRouteKind,\n} from './websocket/route';\nexport { WebSocketRoute, WebSocketRouteBuilder } from './websocket/route';\nexport {\n\tCLOSE,\n\tDuplicateWebSocketRoute,\n\ttype WebSocketServerOptions,\n\tWebSocketSurfaceServer,\n} from './websocket/server';\nexport {\n\ttype Delivery,\n\tMemoryTransport,\n\tRedisTransport,\n\ttransportFromConnectionString,\n\tUnsupportedWebSocketTransport,\n\ttype WebSocketTransport,\n} from './websocket/transport';\n\nexport interface WebSocketApiConfig<\n\tTMessages extends WebSocketMessages = NoMessages,\n> {\n\t/** The app that serves it, relative to the workspace root. */\n\tpath: string;\n\t/**\n\t * What it sends its sockets, by action — each with the schema its `data`\n\t * is checked by before it is sent.\n\t *\n\t * ```ts\n\t * messages: {\n\t * announcement: z.object({ text: z.string() }),\n\t * 'chat.said': z.object({ from: z.string(), text: z.string() }),\n\t * }\n\t * ```\n\t *\n\t * The publisher is typed by it — `services.realtime.broadcast(\n\t * 'announcement', { text })` — in an endpoint and in a route's `sockets`\n\t * alike. Absent, any action and any data go, unchecked.\n\t */\n\tmessages?: TMessages;\n\t/** The label it answers on. Absent, its id kebab-cased. */\n\tsubdomain?: string;\n\t/** How many processes serve it on one machine. Default 1. */\n\treplicas?: number;\n\tlogger?: Logger;\n\tenvParser?: EnvironmentParser<{}>;\n\ttelemetry?: Telemetry;\n\t/** Seconds between pings; a socket that misses one is dropped. Default 30. */\n\theartbeat?: number;\n\t/** The largest frame accepted, in bytes. Default 1 MiB. */\n\tmaxPayload?: number;\n}\n\n/** A replica count that is not a whole number of at least one. */\nexport class InvalidReplicaCount extends Error {\n\tconstructor(\n\t\treadonly id: string,\n\t\treadonly replicas: number,\n\t) {\n\t\tsuper(\n\t\t\t`${id} asks for ${replicas} replicas. A surface runs on at least one process — give a whole number of 1 or more.`,\n\t\t);\n\t\tthis.name = 'InvalidReplicaCount';\n\t}\n}\n\nexport class WebSocketApi<\n\tTName extends string = string,\n\tTMessages extends WebSocketMessages = NoMessages,\n> implements Declarable<TName>\n{\n\treadonly id: TName;\n\n\treadonly keys: {\n\t\turl: string;\n\t\ttrustedOrigins: string;\n\t\tpublisherConnectionString: string;\n\t};\n\n\treadonly logger: Logger;\n\treadonly envParser: EnvironmentParser<{}>;\n\treadonly telemetry?: Telemetry;\n\n\t/**\n\t * The surface as a dependency: `services.<id>` is its publisher, so an\n\t * endpoint sends to a socket the way a route beside it does.\n\t */\n\treadonly service: Service<Uncapitalize<TName>, WebSocketPublisher<TMessages>>;\n\n\tconstructor(\n\t\tid: ConstructName<TName>,\n\t\tprivate readonly config: WebSocketApiConfig<TMessages>,\n\t\t/** Internal: how `.calls()` carries edges into the copy it returns. */\n\t\tprivate readonly dependencies: readonly Dependency[] = [],\n\t\t/** Internal: how `.auth()` carries the authenticator into its copy. */\n\t\treadonly authenticator?: Authenticator,\n\t) {\n\t\tconst canonical = canonicalId(id as string);\n\t\tthis.id = canonical as TName;\n\n\t\tconst replicas = config.replicas ?? 1;\n\t\tif (!Number.isInteger(replicas) || replicas < 1) {\n\t\t\tthrow new InvalidReplicaCount(canonical, replicas);\n\t\t}\n\n\t\tthis.keys = {\n\t\t\turl: provideKey(canonical, 'url'),\n\t\t\ttrustedOrigins: provideKey(canonical, 'trustedOrigins'),\n\t\t\tpublisherConnectionString: provideKey(\n\t\t\t\tcanonical,\n\t\t\t\t'publisherConnectionString',\n\t\t\t),\n\t\t};\n\t\tthis.logger = config.logger ?? DEFAULT_LOGGER;\n\t\tthis.telemetry = config.telemetry;\n\t\tthis.envParser = envParserFor(\n\t\t\tconfig.envParser\n\t\t\t\t? { id: canonical, envParser: config.envParser }\n\t\t\t\t: undefined,\n\t\t);\n\n\t\tconst envVar = this.keys.publisherConnectionString;\n\t\tconst channel = channelOf(canonical);\n\t\tconst declared = this.declared;\n\t\tthis.service = {\n\t\t\tserviceName: serviceKey(canonical) as Uncapitalize<TName>,\n\t\t\tasync register({ envParser }) {\n\t\t\t\tconst { connectionString } = envParser\n\t\t\t\t\t.create((get) => ({ connectionString: get(envVar).string() }))\n\t\t\t\t\t.parse();\n\t\t\t\tconst transport = await transportFromConnectionString(\n\t\t\t\t\tconnectionString,\n\t\t\t\t\tchannel,\n\t\t\t\t);\n\t\t\t\tonShutdown(() => transport.close());\n\t\t\t\treturn new WebSocketPublisher<TMessages>(\n\t\t\t\t\ttransport,\n\t\t\t\t\tundefined,\n\t\t\t\t\tdeclared,\n\t\t\t\t);\n\t\t\t},\n\t\t};\n\t}\n\n\t/** The handler a connection's upgrade runs: refuse it, or say who it is. */\n\tconnect(): WebSocketRouteBuilder<'connect', undefined, [], TMessages> {\n\t\treturn new WebSocketRouteBuilder('connect', '$connect', { id: this.id });\n\t}\n\n\t/** The handler a closed connection runs. */\n\tdisconnect(): WebSocketRouteBuilder<'disconnect', undefined, [], TMessages> {\n\t\treturn new WebSocketRouteBuilder('disconnect', '$disconnect', {\n\t\t\tid: this.id,\n\t\t});\n\t}\n\n\t/**\n\t * The handler for frames whose `action` is `action`. `$default` answers\n\t * any action nothing else does.\n\t */\n\troute(\n\t\taction: string,\n\t): WebSocketRouteBuilder<'message', undefined, [], TMessages> {\n\t\treturn new WebSocketRouteBuilder('message', action, { id: this.id });\n\t}\n\n\t/** What authenticates a connection — its session reaches `connect()`. */\n\tauth(construct: Authenticator): WebSocketApi<TName, TMessages> {\n\t\treturn new WebSocketApi<TName, TMessages>(\n\t\t\tthis.id as ConstructName<TName>,\n\t\t\tthis.config,\n\t\t\t[...this.dependencies, edgeTo(construct)],\n\t\t\tconstruct,\n\t\t);\n\t}\n\n\t/** Other surfaces this one calls. See `RestApi.calls`. */\n\tcalls(constructs: readonly Declarable[]): WebSocketApi<TName, TMessages> {\n\t\treturn new WebSocketApi<TName, TMessages>(\n\t\t\tthis.id as ConstructName<TName>,\n\t\t\tthis.config,\n\t\t\t[...this.dependencies, ...constructs.map(edgeTo)],\n\t\t\tthis.authenticator,\n\t\t);\n\t}\n\n\t/**\n\t * Serve it: the routes exported by `modules` that were built from this\n\t * surface, on whatever HTTP server `attach` is handed.\n\t *\n\t * `app` answers plain HTTP — the generated entry puts `/health` in front\n\t * of it — and `attach` takes over the server's upgrades. The sockets are\n\t * closed with 1012 when the server shuts down.\n\t */\n\tasync server(\n\t\toptions: ServiceRegisterOptions & { modules?: readonly object[] },\n\t): Promise<{\n\t\tapp: Hono;\n\t\tattach(server: Server): void;\n\t\t/** Close every socket with 1012, so the HTTP server can close. */\n\t\trelease(): Promise<void>;\n\t\tserver: WebSocketSurfaceServer<TMessages>;\n\t}> {\n\t\tconst { Hono } = await import('hono');\n\t\tconst routes = (options.modules ?? []).flatMap((module) =>\n\t\t\tObject.values(module).filter(WebSocketRoute.isRoute),\n\t\t);\n\n\t\tconst { connectionString, origins } = options.envParser\n\t\t\t.create((get) => ({\n\t\t\t\tconnectionString: get(this.keys.publisherConnectionString)\n\t\t\t\t\t.string()\n\t\t\t\t\t// One process with nobody else sending: the sockets and the\n\t\t\t\t\t// routes meet in memory.\n\t\t\t\t\t.default(`memory://${channelOf(this.id)}`),\n\t\t\t\torigins: get(this.keys.trustedOrigins)\n\t\t\t\t\t.string()\n\t\t\t\t\t.default('')\n\t\t\t\t\t.transform((value: string) =>\n\t\t\t\t\t\tvalue\n\t\t\t\t\t\t\t.split(',')\n\t\t\t\t\t\t\t.map((origin) => origin.trim())\n\t\t\t\t\t\t\t.filter(Boolean),\n\t\t\t\t\t),\n\t\t\t}))\n\t\t\t.parse();\n\n\t\tconst server = await WebSocketSurfaceServer.start<TMessages>({\n\t\t\tsurface: this.id,\n\t\t\troutes,\n\t\t\ttransport: await transportFromConnectionString(\n\t\t\t\tconnectionString,\n\t\t\t\tchannelOf(this.id),\n\t\t\t),\n\t\t\tenvParser: options.envParser,\n\t\t\tlogger: this.logger,\n\t\t\ttrustedOrigins: origins,\n\t\t\t...(this.declared ? { messages: this.declared.messages } : {}),\n\t\t\t...(this.authenticator ? { authenticator: this.authenticator } : {}),\n\t\t\t...(this.config.heartbeat\n\t\t\t\t? { heartbeatMs: this.config.heartbeat * 1000 }\n\t\t\t\t: {}),\n\t\t\t...(this.config.maxPayload ? { maxPayload: this.config.maxPayload } : {}),\n\t\t});\n\t\tonShutdown(() => server.close());\n\n\t\tconst app = new Hono();\n\t\t// Its own, so a process with nothing in front of it — `gkm dev`'s —\n\t\t// answers a probe too. A production entry puts one before it anyway.\n\t\tapp.get('/health', (c) =>\n\t\t\tc.json({ status: 'ok', connections: server.size }),\n\t\t);\n\t\tapp.get('/', (c) =>\n\t\t\tc.json({ surface: this.id, connections: server.size }, 426, {\n\t\t\t\tUpgrade: 'websocket',\n\t\t\t}),\n\t\t);\n\n\t\treturn {\n\t\t\tapp,\n\t\t\tattach: (http) => server.attach(http),\n\t\t\trelease: () => server.close(),\n\t\t\tserver,\n\t\t};\n\t}\n\n\t/** The declared messages, with the surface they are checked for. */\n\tprivate get declared(): { surface: string; messages: TMessages } | undefined {\n\t\treturn this.config.messages\n\t\t\t? { surface: this.id, messages: this.config.messages }\n\t\t\t: undefined;\n\t}\n\n\tdeclare(): Declaration[] {\n\t\treturn [\n\t\t\t{\n\t\t\t\tkind: 'websocket-api',\n\t\t\t\tid: this.id,\n\t\t\t\tpath: this.config.path,\n\t\t\t\t...(this.config.subdomain ? { subdomain: this.config.subdomain } : {}),\n\t\t\t\t...(this.authenticator ? { auth: this.authenticator.id } : {}),\n\t\t\t\t...(this.dependencies.length ? { calls: this.dependencies } : {}),\n\t\t\t\t...(this.config.telemetry\n\t\t\t\t\t? { telemetry: this.config.telemetry.id }\n\t\t\t\t\t: {}),\n\t\t\t\t...(this.config.replicas && this.config.replicas > 1\n\t\t\t\t\t? { replicas: this.config.replicas }\n\t\t\t\t\t: {}),\n\t\t\t\tprovides: [\n\t\t\t\t\tthis.keys.url,\n\t\t\t\t\tthis.keys.trustedOrigins,\n\t\t\t\t\tthis.keys.publisherConnectionString,\n\t\t\t\t],\n\t\t\t},\n\t\t];\n\t}\n}\n\n/** The prefix a surface's channels are named under. */\nfunction channelOf(id: string): string {\n\treturn `gkm:ws:${id}`;\n}\n"],"mappings":"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"}
@@ -0,0 +1,449 @@
1
+ import { n as StandardSchemaV1 } from "./index-DdRjYhiJ.cjs";
2
+ import { c as ServicesOf, i as Declarable, r as Consumable, t as Authenticator } from "./construct-interface-B1AfqRl4.cjs";
3
+ import { n as Telemetry } from "./telemetry-B48RLjyk.cjs";
4
+ import { EnvironmentParser } from "@geekmidas/envkit";
5
+ import { Logger } from "@geekmidas/logger";
6
+ import { InferStandardSchema } from "@geekmidas/schema";
7
+ import { Service, ServiceRecord, ServiceRegisterOptions } from "@geekmidas/services";
8
+ import { ConstructName, Declaration, Dependency } from "@geekmidas/manifest";
9
+ import { Hono } from "hono";
10
+ import { IncomingMessage, Server } from "node:http";
11
+ import { Duplex } from "node:stream";
12
+ //#region src/websocket/transport.d.ts
13
+ /**
14
+ * How a send reaches the process holding the socket.
15
+ *
16
+ * A connection id names the process it lives on — `<instance>.<n>` — so a send
17
+ * needs no registry: it is published to that instance's channel and nowhere
18
+ * else. A broadcast goes to every instance. On a stack the channels are Redis
19
+ * pub/sub, so an endpoint in another container reaches a socket the same way a
20
+ * route beside it does; in one process (a test, a single dev server) they are
21
+ * a map.
22
+ */
23
+ /** What one process is asked to do with the sockets it holds. */
24
+ type Delivery = {
25
+ kind: 'send';
26
+ ids: readonly string[];
27
+ payload: string;
28
+ } | {
29
+ kind: 'broadcast';
30
+ payload: string;
31
+ except?: readonly string[];
32
+ } | {
33
+ kind: 'close';
34
+ ids: readonly string[];
35
+ code: number;
36
+ reason: string;
37
+ };
38
+ interface WebSocketTransport {
39
+ /** Deliver to one instance, or to every instance when it is `'*'`. */
40
+ publish(instance: string, delivery: Delivery): Promise<void>;
41
+ /** Receive what is published to `instance`, and every broadcast. */
42
+ subscribe(instance: string, receive: (delivery: Delivery) => void): Promise<void>;
43
+ close(): Promise<void>;
44
+ }
45
+ /** A connection string names no transport this package knows. */
46
+ export declare class UnsupportedWebSocketTransport extends Error {
47
+ readonly connectionString: string;
48
+ constructor(connectionString: string);
49
+ }
50
+ /**
51
+ * In one process: the server and whoever sends meet in a map, keyed by the
52
+ * channel the connection string names.
53
+ */
54
+ export declare class MemoryTransport implements WebSocketTransport {
55
+ private readonly channel;
56
+ private readonly subscribed;
57
+ constructor(channel: string);
58
+ publish(instance: string, delivery: Delivery): Promise<void>;
59
+ subscribe(instance: string, receive: (delivery: Delivery) => void): Promise<void>;
60
+ close(): Promise<void>;
61
+ }
62
+ /** The subset of an ioredis client the transport uses. */
63
+ interface RedisClient {
64
+ publish(channel: string, message: string): Promise<number>;
65
+ subscribe(...channels: string[]): Promise<unknown>;
66
+ on(event: 'message', listener: (channel: string, message: string) => void): unknown;
67
+ duplicate(): RedisClient;
68
+ quit(): Promise<unknown>;
69
+ }
70
+ /**
71
+ * Over Redis pub/sub: one channel per instance and one for broadcasts, under
72
+ * a prefix that keeps two surfaces — or two projects on one Redis — apart.
73
+ * Channels are not scoped by database index, so the prefix is what does it.
74
+ *
75
+ * Publishing and subscribing take separate connections, because a connection
76
+ * that has subscribed can do nothing else.
77
+ */
78
+ export declare class RedisTransport implements WebSocketTransport {
79
+ private readonly client;
80
+ private readonly prefix;
81
+ private subscriber?;
82
+ private closed?;
83
+ constructor(client: RedisClient, prefix: string);
84
+ private channel;
85
+ publish(instance: string, delivery: Delivery): Promise<void>;
86
+ subscribe(instance: string, receive: (delivery: Delivery) => void): Promise<void>;
87
+ /** Idempotent: the server and a shutdown hook may both close it. */
88
+ close(): Promise<void>;
89
+ }
90
+ /**
91
+ * The transport a connection string names.
92
+ *
93
+ * `redis://` and `rediss://` take an optional `channel` query parameter, the
94
+ * prefix every channel is named under; `memory://<channel>` is in-process.
95
+ * Absent, the channel is `fallback` — the surface's own.
96
+ */
97
+ export declare function transportFromConnectionString(connectionString: string, fallback: string): Promise<WebSocketTransport>;
98
+ //#endregion
99
+ //#region src/websocket/publisher.d.ts
100
+ /**
101
+ * What a surface sends its sockets, by action — `WebSocketApiConfig.messages`.
102
+ */
103
+ type WebSocketMessages = Record<string, StandardSchemaV1>;
104
+ /** A surface that declares no messages: any action, any data. */
105
+ type NoMessages = Record<never, never>;
106
+ /** The actions a publisher sends: the declared ones, or any. */
107
+ type MessageAction<TMessages extends WebSocketMessages> = [keyof TMessages] extends [never] ? string : keyof TMessages & string;
108
+ /** What `action`'s data is, as its schema takes it. */
109
+ type MessageData<TMessages extends WebSocketMessages, TAction extends string> = TAction extends keyof TMessages ? StandardSchemaV1.InferInput<TMessages[TAction]> : unknown;
110
+ /** A message sent on an action the surface does not declare. */
111
+ export declare class UnknownWebSocketMessage extends Error {
112
+ readonly surface: string;
113
+ readonly action: string;
114
+ readonly declared: readonly string[];
115
+ constructor(surface: string, action: string, declared: readonly string[]);
116
+ }
117
+ /** A message whose data its action's schema refused. */
118
+ export declare class InvalidWebSocketMessage extends Error {
119
+ readonly surface: string;
120
+ readonly action: string;
121
+ readonly issues: readonly StandardSchemaV1.Issue[];
122
+ constructor(surface: string, action: string, issues: readonly StandardSchemaV1.Issue[]);
123
+ }
124
+ /**
125
+ * How anything reaches a socket — `services.realtime` in an endpoint, and
126
+ * `sockets` in a route of the surface itself.
127
+ *
128
+ * A message is `{ action, data }`, the shape a client's frames arrive in.
129
+ * Where the surface declares its messages, `action` is one of them and
130
+ * `data` is typed and checked by its schema before anything is sent; where it
131
+ * declares none, any action and any JSON value go. A message is serialised
132
+ * once, whatever the number of sockets it goes to. An id whose process has
133
+ * gone is dropped silently, as API Gateway's `postToConnection` to a closed
134
+ * connection is a no-op the sender rarely wants to handle.
135
+ */
136
+ export declare class WebSocketPublisher<TMessages extends WebSocketMessages = NoMessages> {
137
+ private readonly transport;
138
+ /**
139
+ * Delivers to this process's own sockets without a round trip through
140
+ * the transport — set on the server's own publisher.
141
+ */
142
+ private readonly local?;
143
+ /** The surface's declared messages, which every send is checked by. */
144
+ private readonly declared?;
145
+ constructor(transport: WebSocketTransport,
146
+ /**
147
+ * Delivers to this process's own sockets without a round trip through
148
+ * the transport — set on the server's own publisher.
149
+ */
150
+ local?: {
151
+ instance: string;
152
+ deliver(delivery: Delivery): void;
153
+ } | undefined,
154
+ /** The surface's declared messages, which every send is checked by. */
155
+ declared?: {
156
+ surface: string;
157
+ messages: TMessages;
158
+ } | undefined);
159
+ /** Send `action`'s message to each connection. */
160
+ send<TAction extends MessageAction<TMessages>>(ids: string | readonly string[], action: TAction, data: MessageData<TMessages, TAction>): Promise<void>;
161
+ /** Send `action`'s message to every open connection, on every process. */
162
+ broadcast<TAction extends MessageAction<TMessages>>(action: TAction, data: MessageData<TMessages, TAction>, options?: {
163
+ except?: readonly string[];
164
+ }): Promise<void>;
165
+ /** Close each connection, with a close code and reason its client sees. */
166
+ close(ids: string | readonly string[], code?: number, reason?: string): Promise<void>;
167
+ /**
168
+ * The frame, checked by the action's schema where the surface declares
169
+ * its messages.
170
+ *
171
+ * @throws {UnknownWebSocketMessage}, {InvalidWebSocketMessage}
172
+ */
173
+ private payload;
174
+ /** One delivery per process that holds any of `ids`. */
175
+ private each;
176
+ }
177
+ //#endregion
178
+ //#region src/websocket/route.d.ts
179
+ /** One open connection, as a handler sees it. */
180
+ interface Connection {
181
+ readonly id: string;
182
+ /**
183
+ * What the connect handler returned — who the connection is for, the room
184
+ * it joined. `{}` when it returned nothing.
185
+ */
186
+ readonly data: Record<string, unknown>;
187
+ }
188
+ interface BaseContext<TServices extends Service[], TMessages extends WebSocketMessages> {
189
+ services: ServiceRecord<TServices>;
190
+ logger: Logger;
191
+ /** Every socket this surface holds, on whichever process holds it. */
192
+ sockets: WebSocketPublisher<TMessages>;
193
+ }
194
+ interface ConnectContext<TServices extends Service[], TMessages extends WebSocketMessages = NoMessages> extends BaseContext<TServices, TMessages> {
195
+ connectionId: string;
196
+ headers: Headers;
197
+ query: URLSearchParams;
198
+ /**
199
+ * Whose connection it is, from the surface's `.auth()` — `null` when the
200
+ * upgrade carried no session, and when the surface names no authenticator.
201
+ * Throw an `UnauthorizedError` to refuse it.
202
+ */
203
+ session: unknown;
204
+ }
205
+ interface MessageContext<TBody, TServices extends Service[], TMessages extends WebSocketMessages = NoMessages> extends BaseContext<TServices, TMessages> {
206
+ connection: Connection;
207
+ body: TBody;
208
+ /** Send to this connection alone. */
209
+ reply(message: unknown): void;
210
+ }
211
+ interface DisconnectContext<TServices extends Service[], TMessages extends WebSocketMessages = NoMessages> extends BaseContext<TServices, TMessages> {
212
+ connection: Connection;
213
+ code: number;
214
+ reason: string;
215
+ }
216
+ /** What a frame is dispatched on: an action, or the connection's lifecycle. */
217
+ type RouteKind = 'connect' | 'disconnect' | 'message';
218
+ /**
219
+ * A handler bound to a WebSocket surface — what `realtime.route('chat.send')
220
+ * …handle()` returns, and what a file exports.
221
+ *
222
+ * Carries `surface` and `constructs` the way an endpoint does, so discovery
223
+ * records what its process reaches without knowing what a route is.
224
+ */
225
+ export declare class WebSocketRoute {
226
+ readonly kind: RouteKind;
227
+ /** The `action` it answers; `$connect` and `$disconnect` for the lifecycle. */
228
+ readonly action: string;
229
+ readonly surface: {
230
+ id: string;
231
+ };
232
+ readonly services: readonly Service[];
233
+ /** Construct ids it depends on — its edges in the manifest. */
234
+ readonly constructs: readonly string[];
235
+ readonly handler: (context: never) => unknown;
236
+ readonly schema?: StandardSchemaV1 | undefined;
237
+ readonly __IS_WEBSOCKET_ROUTE__ = true;
238
+ static isRoute(value: unknown): value is WebSocketRoute;
239
+ constructor(kind: RouteKind,
240
+ /** The `action` it answers; `$connect` and `$disconnect` for the lifecycle. */
241
+ action: string, surface: {
242
+ id: string;
243
+ }, services: readonly Service[],
244
+ /** Construct ids it depends on — its edges in the manifest. */
245
+ constructs: readonly string[], handler: (context: never) => unknown, schema?: StandardSchemaV1 | undefined);
246
+ }
247
+ /** The builder `connect()`, `disconnect()` and `route()` start. */
248
+ export declare class WebSocketRouteBuilder<TKind extends RouteKind, TSchema extends StandardSchemaV1 | undefined = undefined, TServices extends Service[] = [],
249
+ /** The surface's declared messages, which `sockets` sends. */
250
+ TMessages extends WebSocketMessages = NoMessages> {
251
+ private readonly kind;
252
+ private readonly action;
253
+ private readonly surface;
254
+ private readonly _services;
255
+ private readonly _constructs;
256
+ private readonly schema?;
257
+ constructor(kind: TKind, action: string, surface: {
258
+ id: string;
259
+ }, _services?: readonly Service[], _constructs?: readonly string[], schema?: TSchema | undefined);
260
+ /** The schema a frame's `data` must satisfy before the handler runs. */
261
+ body<S extends StandardSchemaV1>(schema: S): WebSocketRouteBuilder<TKind, S, TServices, TMessages>;
262
+ /** What the handler reaches, as `services.<id>` — per route, as an endpoint's. */
263
+ dependsOn<const T extends readonly Consumable[]>(constructs: T): WebSocketRouteBuilder<TKind, TSchema, [...TServices, ...(ServicesOf<T> extends Service[] ? ServicesOf<T> : [])], TMessages>;
264
+ /**
265
+ * The handler. A message handler's return value, when there is one, is
266
+ * sent back to the connection as the reply to that frame; a connect
267
+ * handler's is kept as the connection's `data`.
268
+ */
269
+ handle(handler: TKind extends 'connect' ? (context: ConnectContext<TServices, TMessages>) => Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> : TKind extends 'disconnect' ? (context: DisconnectContext<TServices, TMessages>) => unknown : (context: MessageContext<TSchema extends StandardSchemaV1 ? InferStandardSchema<TSchema> : unknown, TServices, TMessages>) => unknown): WebSocketRoute;
270
+ }
271
+ //#endregion
272
+ //#region src/websocket/server.d.ts
273
+ /** Close codes a client sees. */
274
+ export declare const CLOSE: {
275
+ readonly normal: 1000;
276
+ /** The process is stopping; reconnect and land on another. */
277
+ readonly serviceRestart: 1012;
278
+ /** It stopped reading, and what was queued for it passed the limit. */
279
+ readonly tooSlow: 1013;
280
+ };
281
+ /** Two routes answer one action. */
282
+ export declare class DuplicateWebSocketRoute extends Error {
283
+ readonly surface: string;
284
+ readonly action: string;
285
+ constructor(surface: string, action: string);
286
+ }
287
+ interface WebSocketServerOptions<TMessages extends WebSocketMessages = NoMessages> {
288
+ surface: string;
289
+ /** What the surface sends its sockets — see `WebSocketApiConfig.messages`. */
290
+ messages?: TMessages;
291
+ routes: readonly WebSocketRoute[];
292
+ transport: WebSocketTransport;
293
+ envParser: EnvironmentParser<{}>;
294
+ logger: Logger;
295
+ authenticator?: Authenticator;
296
+ /**
297
+ * Origins a browser may open a socket from. A request with an `Origin`
298
+ * not in the list is refused with 403; one with none — a server, a load
299
+ * generator — is not a browser, and is let through.
300
+ */
301
+ trustedOrigins: readonly string[];
302
+ /** How often each socket is pinged; one that missed the last is dropped. */
303
+ heartbeatMs?: number;
304
+ /** The largest frame accepted, in bytes. */
305
+ maxPayload?: number;
306
+ /** Queued bytes a socket may hold before it is closed as too slow. */
307
+ maxBufferedBytes?: number;
308
+ }
309
+ export declare class WebSocketSurfaceServer<TMessages extends WebSocketMessages = NoMessages> {
310
+ private readonly wss;
311
+ private readonly options;
312
+ /** This process, as connection ids name it. */
313
+ readonly instance: string;
314
+ /** `sockets` in a route: local sends skip the transport. */
315
+ readonly publisher: WebSocketPublisher<TMessages>;
316
+ private readonly held;
317
+ private readonly routes;
318
+ private readonly lifecycle;
319
+ private readonly heartbeat;
320
+ private next;
321
+ private closing;
322
+ private constructor();
323
+ static start<TMessages extends WebSocketMessages = NoMessages>(options: WebSocketServerOptions<TMessages>): Promise<WebSocketSurfaceServer<TMessages>>;
324
+ /** How many sockets this process holds. */
325
+ get size(): number;
326
+ /** Take over `server`'s upgrades. */
327
+ attach(server: Server): void;
328
+ /** Upgrade one request, or refuse it with a status line. */
329
+ upgrade(request: IncomingMessage, socket: Duplex, head: Buffer): Promise<void>;
330
+ private open;
331
+ /** Parse, validate and run one frame. Errors go back as frames, not closes. */
332
+ private dispatch;
333
+ /** Run a handler with its services, inside a request context of its own. */
334
+ private run;
335
+ private error;
336
+ /** Write, unless the socket has stopped reading and its queue is full. */
337
+ private write;
338
+ /** What the transport, or a local publisher, asked of this process. */
339
+ private deliver;
340
+ /** Drop every socket that missed the last ping, and ping the rest. */
341
+ private beat;
342
+ /**
343
+ * Stop: refuse new upgrades, close every socket with `code` — 1012 tells
344
+ * a client to reconnect, and it lands on a process that is still up — and
345
+ * leave the transport.
346
+ */
347
+ close(code?: number, reason?: string): Promise<void>;
348
+ }
349
+ //#endregion
350
+ //#region src/websocket-api.d.ts
351
+ export interface WebSocketApiConfig<TMessages extends WebSocketMessages = NoMessages> {
352
+ /** The app that serves it, relative to the workspace root. */
353
+ path: string;
354
+ /**
355
+ * What it sends its sockets, by action — each with the schema its `data`
356
+ * is checked by before it is sent.
357
+ *
358
+ * ```ts
359
+ * messages: {
360
+ * announcement: z.object({ text: z.string() }),
361
+ * 'chat.said': z.object({ from: z.string(), text: z.string() }),
362
+ * }
363
+ * ```
364
+ *
365
+ * The publisher is typed by it — `services.realtime.broadcast(
366
+ * 'announcement', { text })` — in an endpoint and in a route's `sockets`
367
+ * alike. Absent, any action and any data go, unchecked.
368
+ */
369
+ messages?: TMessages;
370
+ /** The label it answers on. Absent, its id kebab-cased. */
371
+ subdomain?: string;
372
+ /** How many processes serve it on one machine. Default 1. */
373
+ replicas?: number;
374
+ logger?: Logger;
375
+ envParser?: EnvironmentParser<{}>;
376
+ telemetry?: Telemetry;
377
+ /** Seconds between pings; a socket that misses one is dropped. Default 30. */
378
+ heartbeat?: number;
379
+ /** The largest frame accepted, in bytes. Default 1 MiB. */
380
+ maxPayload?: number;
381
+ }
382
+ /** A replica count that is not a whole number of at least one. */
383
+ export declare class InvalidReplicaCount extends Error {
384
+ readonly id: string;
385
+ readonly replicas: number;
386
+ constructor(id: string, replicas: number);
387
+ }
388
+ export declare class WebSocketApi<TName extends string = string, TMessages extends WebSocketMessages = NoMessages> implements Declarable<TName> {
389
+ private readonly config;
390
+ /** Internal: how `.calls()` carries edges into the copy it returns. */
391
+ private readonly dependencies;
392
+ /** Internal: how `.auth()` carries the authenticator into its copy. */
393
+ readonly authenticator?: Authenticator | undefined;
394
+ readonly id: TName;
395
+ readonly keys: {
396
+ url: string;
397
+ trustedOrigins: string;
398
+ publisherConnectionString: string;
399
+ };
400
+ readonly logger: Logger;
401
+ readonly envParser: EnvironmentParser<{}>;
402
+ readonly telemetry?: Telemetry;
403
+ /**
404
+ * The surface as a dependency: `services.<id>` is its publisher, so an
405
+ * endpoint sends to a socket the way a route beside it does.
406
+ */
407
+ readonly service: Service<Uncapitalize<TName>, WebSocketPublisher<TMessages>>;
408
+ constructor(id: ConstructName<TName>, config: WebSocketApiConfig<TMessages>,
409
+ /** Internal: how `.calls()` carries edges into the copy it returns. */
410
+ dependencies?: readonly Dependency[],
411
+ /** Internal: how `.auth()` carries the authenticator into its copy. */
412
+ authenticator?: Authenticator | undefined);
413
+ /** The handler a connection's upgrade runs: refuse it, or say who it is. */
414
+ connect(): WebSocketRouteBuilder<'connect', undefined, [], TMessages>;
415
+ /** The handler a closed connection runs. */
416
+ disconnect(): WebSocketRouteBuilder<'disconnect', undefined, [], TMessages>;
417
+ /**
418
+ * The handler for frames whose `action` is `action`. `$default` answers
419
+ * any action nothing else does.
420
+ */
421
+ route(action: string): WebSocketRouteBuilder<'message', undefined, [], TMessages>;
422
+ /** What authenticates a connection — its session reaches `connect()`. */
423
+ auth(construct: Authenticator): WebSocketApi<TName, TMessages>;
424
+ /** Other surfaces this one calls. See `RestApi.calls`. */
425
+ calls(constructs: readonly Declarable[]): WebSocketApi<TName, TMessages>;
426
+ /**
427
+ * Serve it: the routes exported by `modules` that were built from this
428
+ * surface, on whatever HTTP server `attach` is handed.
429
+ *
430
+ * `app` answers plain HTTP — the generated entry puts `/health` in front
431
+ * of it — and `attach` takes over the server's upgrades. The sockets are
432
+ * closed with 1012 when the server shuts down.
433
+ */
434
+ server(options: ServiceRegisterOptions & {
435
+ modules?: readonly object[];
436
+ }): Promise<{
437
+ app: Hono;
438
+ attach(server: Server): void;
439
+ /** Close every socket with 1012, so the HTTP server can close. */
440
+ release(): Promise<void>;
441
+ server: WebSocketSurfaceServer<TMessages>;
442
+ }>;
443
+ /** The declared messages, with the surface they are checked for. */
444
+ private get declared();
445
+ declare(): Declaration[];
446
+ }
447
+ //#endregion
448
+ export type { ConnectContext, Connection, Delivery, DisconnectContext, MessageAction, MessageContext, MessageData, NoMessages, RouteKind, WebSocketMessages, WebSocketServerOptions, WebSocketTransport };
449
+ //# sourceMappingURL=websocket-api.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"websocket-api.d.cts","names":[],"sources":["../src/websocket/transport.ts","../src/websocket/publisher.ts","../src/websocket/route.ts","../src/websocket/server.ts","../src/websocket-api.ts"],"mappings":"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"}