@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.mjs","names":[],"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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekmidas/constructs",
3
- "version": "10.0.0-alpha.125",
3
+ "version": "10.0.0-alpha.127",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -244,6 +244,16 @@
244
244
  "default": "./dist/rest-api.cjs"
245
245
  }
246
246
  },
247
+ "./websocket-api": {
248
+ "import": {
249
+ "types": "./dist/websocket-api.d.mts",
250
+ "default": "./dist/websocket-api.mjs"
251
+ },
252
+ "require": {
253
+ "types": "./dist/websocket-api.d.cts",
254
+ "default": "./dist/websocket-api.cjs"
255
+ }
256
+ },
247
257
  "./site": {
248
258
  "import": {
249
259
  "types": "./dist/site.d.mts",
@@ -317,13 +327,13 @@
317
327
  "lodash.uniqby": "~4.7.0",
318
328
  "openapi-types": "~12.1.3",
319
329
  "qs": "~6.16.0",
320
- "@geekmidas/envkit": "^10.0.0-alpha.125",
321
- "@geekmidas/errors": "^10.0.0-alpha.125",
322
- "@geekmidas/events": "^10.0.0-alpha.125",
323
- "@geekmidas/logger": "^10.0.0-alpha.125",
324
- "@geekmidas/manifest": "^10.0.0-alpha.125",
325
- "@geekmidas/schema": "^10.0.0-alpha.125",
326
- "@geekmidas/services": "^10.0.0-alpha.125"
330
+ "@geekmidas/envkit": "^10.0.0-alpha.127",
331
+ "@geekmidas/errors": "^10.0.0-alpha.127",
332
+ "@geekmidas/events": "^10.0.0-alpha.127",
333
+ "@geekmidas/logger": "^10.0.0-alpha.127",
334
+ "@geekmidas/manifest": "^10.0.0-alpha.127",
335
+ "@geekmidas/schema": "^10.0.0-alpha.127",
336
+ "@geekmidas/services": "^10.0.0-alpha.127"
327
337
  },
328
338
  "devDependencies": {
329
339
  "@aws-sdk/client-kms": "~3.1136.0",
@@ -336,25 +346,28 @@
336
346
  "@types/lodash.uniqby": "~4.7.9",
337
347
  "@types/pg": "~8.23.1",
338
348
  "@types/qs": "~6.15.1",
349
+ "@types/ws": "~8.18.1",
339
350
  "better-auth": "~1.7.6",
351
+ "ioredis": "~6.0.0",
340
352
  "kysely": "~0.29.6",
341
353
  "msw": "~2.15.0",
342
354
  "pg": "~8.23.0",
355
+ "ws": "~8.22.0",
343
356
  "zod": "~4.6.5",
344
- "@geekmidas/audit": "^10.0.0-alpha.125",
345
- "@geekmidas/auth": "10.0.0-alpha.125",
346
- "@geekmidas/cache": "^10.0.0-alpha.125",
347
- "@geekmidas/db": "^10.0.0-alpha.125",
348
- "@geekmidas/emailkit": "^10.0.0-alpha.125",
349
- "@geekmidas/envkit": "^10.0.0-alpha.125",
350
- "@geekmidas/errors": "^10.0.0-alpha.125",
351
- "@geekmidas/events": "^10.0.0-alpha.125",
352
- "@geekmidas/logger": "^10.0.0-alpha.125",
353
- "@geekmidas/rate-limit": "^10.0.0-alpha.125",
354
- "@geekmidas/schema": "^10.0.0-alpha.125",
355
- "@geekmidas/services": "^10.0.0-alpha.125",
356
- "@geekmidas/telescope": "10.0.0-alpha.125",
357
- "@geekmidas/testkit": "^10.0.0-alpha.125"
357
+ "@geekmidas/audit": "^10.0.0-alpha.127",
358
+ "@geekmidas/auth": "10.0.0-alpha.127",
359
+ "@geekmidas/cache": "^10.0.0-alpha.127",
360
+ "@geekmidas/db": "^10.0.0-alpha.127",
361
+ "@geekmidas/emailkit": "^10.0.0-alpha.127",
362
+ "@geekmidas/envkit": "^10.0.0-alpha.127",
363
+ "@geekmidas/errors": "^10.0.0-alpha.127",
364
+ "@geekmidas/events": "^10.0.0-alpha.127",
365
+ "@geekmidas/logger": "^10.0.0-alpha.127",
366
+ "@geekmidas/rate-limit": "^10.0.0-alpha.127",
367
+ "@geekmidas/schema": "^10.0.0-alpha.127",
368
+ "@geekmidas/services": "^10.0.0-alpha.127",
369
+ "@geekmidas/telescope": "10.0.0-alpha.127",
370
+ "@geekmidas/testkit": "^10.0.0-alpha.127"
358
371
  },
359
372
  "peerDependencies": {
360
373
  "@aws-sdk/client-kms": ">=3.1136.0",
@@ -362,18 +375,20 @@
362
375
  "@types/aws-lambda": ">=8.10.163",
363
376
  "better-auth": ">=1.7.5",
364
377
  "hono": ">=4.13.8",
378
+ "ioredis": ">=6.0.0",
365
379
  "kysely": "~0.29.6",
366
380
  "msw": ">=2.15.0",
367
381
  "pg": ">=8.23.0",
368
- "@geekmidas/audit": "^10.0.0-alpha.125",
369
- "@geekmidas/auth": "10.0.0-alpha.125",
370
- "@geekmidas/cache": "^10.0.0-alpha.125",
371
- "@geekmidas/db": "^10.0.0-alpha.125",
372
- "@geekmidas/emailkit": "^10.0.0-alpha.125",
373
- "@geekmidas/rate-limit": "^10.0.0-alpha.125",
374
- "@geekmidas/storage": "^10.0.0-alpha.125",
375
- "@geekmidas/telescope": "10.0.0-alpha.125",
376
- "@geekmidas/testkit": "^10.0.0-alpha.125"
382
+ "ws": ">=8.22.0",
383
+ "@geekmidas/audit": "^10.0.0-alpha.127",
384
+ "@geekmidas/auth": "10.0.0-alpha.127",
385
+ "@geekmidas/cache": "^10.0.0-alpha.127",
386
+ "@geekmidas/db": "^10.0.0-alpha.127",
387
+ "@geekmidas/emailkit": "^10.0.0-alpha.127",
388
+ "@geekmidas/rate-limit": "^10.0.0-alpha.127",
389
+ "@geekmidas/storage": "^10.0.0-alpha.127",
390
+ "@geekmidas/telescope": "10.0.0-alpha.127",
391
+ "@geekmidas/testkit": "^10.0.0-alpha.127"
377
392
  },
378
393
  "peerDependenciesMeta": {
379
394
  "@aws-sdk/client-kms": {
@@ -415,6 +430,9 @@
415
430
  "hono": {
416
431
  "optional": true
417
432
  },
433
+ "ioredis": {
434
+ "optional": true
435
+ },
418
436
  "kysely": {
419
437
  "optional": true
420
438
  },
@@ -423,6 +441,9 @@
423
441
  },
424
442
  "pg": {
425
443
  "optional": true
444
+ },
445
+ "ws": {
446
+ "optional": true
426
447
  }
427
448
  },
428
449
  "scripts": {