@kyneta/websocket-transport 1.3.0 → 1.3.1
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.
- package/README.md +28 -15
- package/dist/browser.d.ts +58 -0
- package/dist/browser.js +15 -0
- package/dist/browser.js.map +1 -0
- package/dist/bun.d.ts +6 -6
- package/dist/bun.js.map +1 -1
- package/dist/{client.js → chunk-YZQF5RLV.js} +123 -17
- package/dist/chunk-YZQF5RLV.js.map +1 -0
- package/dist/{client.d.ts → client-transport-DUAFjVbh.d.ts} +25 -100
- package/dist/server.d.ts +41 -4
- package/dist/server.js +10 -1
- package/dist/server.js.map +1 -1
- package/dist/{types-DdNb8cAz.d.ts → types-D0lbeevu.d.ts} +50 -2
- package/package.json +13 -13
- package/src/__tests__/client-transport.test.ts +168 -0
- package/src/{client.ts → browser.ts} +15 -10
- package/src/bun-websocket.ts +5 -5
- package/src/bun.ts +1 -1
- package/src/client-transport.ts +42 -66
- package/src/server-transport.ts +3 -3
- package/src/server.ts +18 -4
- package/src/service-client.ts +52 -0
- package/src/types.ts +74 -6
- package/dist/chunk-PSG3LLT5.js +0 -111
- package/dist/chunk-PSG3LLT5.js.map +0 -1
- package/dist/client.js.map +0 -1
package/dist/client.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/client-program.ts","../src/client-transport.ts"],"sourcesContent":["// client-program — pure Mealy machine for websocket client connection lifecycle.\n//\n// The client program encodes every state transition and effect as data.\n// The imperative shell (client-transport.ts) interprets effects as I/O.\n// Tests assert on data — no sockets, no timing, never flaky.\n//\n// Algebra: Program<WsClientMsg, WebsocketClientState, WsClientEffect>\n// Interpreter: client-transport.ts executeClientEffect()\n//\n// The websocket client has a 5-state lifecycle with an extra \"ready\" state\n// compared to the unix socket client. The server sends a text \"ready\" signal\n// after the connection opens, and only then does the client create a channel\n// and start the establishment handshake.\n//\n// Race condition: the server may send \"ready\" before the client's open event\n// fires (server-ready while connecting). The program handles this by\n// transitioning directly to ready, skipping the connected state.\n\nimport type { Program } from \"@kyneta/machine\"\nimport type { ReconnectOptions } from \"@kyneta/transport\"\nimport { computeBackoffDelay, DEFAULT_RECONNECT } from \"@kyneta/transport\"\n\nimport type { DisconnectReason, WebsocketClientState } from \"./types.js\"\n\n// ---------------------------------------------------------------------------\n// Messages\n// ---------------------------------------------------------------------------\n\nexport type WsClientMsg =\n | { type: \"start\" }\n | { type: \"socket-opened\" }\n | { type: \"server-ready\" }\n | { type: \"socket-closed\"; code: number; reason: string }\n | { type: \"socket-error\"; error: Error }\n | { type: \"reconnect-timer-fired\" }\n | { type: \"stop\" }\n\n// ---------------------------------------------------------------------------\n// Effects (data — interpreted by the imperative shell)\n// ---------------------------------------------------------------------------\n\nexport type WsClientEffect =\n | { type: \"create-websocket\"; attempt: number }\n | { type: \"close-websocket\" }\n | { type: \"add-channel-and-establish\" }\n | { type: \"remove-channel\" }\n | { type: \"start-reconnect-timer\"; delayMs: number }\n | { type: \"cancel-reconnect-timer\" }\n | { type: \"start-keepalive\" }\n | { type: \"stop-keepalive\" }\n\n// ---------------------------------------------------------------------------\n// Program factory\n// ---------------------------------------------------------------------------\n\nexport interface WsClientProgramOptions {\n reconnect?: Partial<ReconnectOptions>\n /** Inject jitter source for deterministic testing. Default: () => Math.random() * 1000 */\n jitterFn?: () => number\n}\n\n/**\n * Create the websocket client connection lifecycle program — a pure Mealy machine.\n *\n * The returned `Program<WsClientMsg, WebsocketClientState, WsClientEffect>`\n * encodes every state transition and effect as inspectable data. The imperative\n * shell interprets `WsClientEffect` as actual I/O.\n */\nexport function createWsClientProgram(\n options: WsClientProgramOptions = {},\n): Program<WsClientMsg, WebsocketClientState, WsClientEffect> {\n const { jitterFn = () => Math.random() * 1000 } = options\n const reconnect: ReconnectOptions = {\n ...DEFAULT_RECONNECT,\n ...options.reconnect,\n }\n\n /**\n * Attempt to transition into reconnecting, or give up and disconnect.\n *\n * Pure — computes the next state and effects from the current attempt\n * count and reconnect configuration. Returns a tuple suitable for\n * spreading into an `update` return.\n */\n function tryReconnect(\n currentAttempt: number,\n reason: DisconnectReason,\n ...extraEffects: WsClientEffect[]\n ): [WebsocketClientState, ...WsClientEffect[]] {\n if (!reconnect.enabled) {\n return [{ status: \"disconnected\", reason }, ...extraEffects]\n }\n\n if (currentAttempt >= reconnect.maxAttempts) {\n return [\n {\n status: \"disconnected\",\n reason: { type: \"max-retries-exceeded\", attempts: currentAttempt },\n },\n ...extraEffects,\n ]\n }\n\n const delay = computeBackoffDelay(\n currentAttempt + 1,\n reconnect.baseDelay,\n reconnect.maxDelay,\n jitterFn(),\n )\n\n return [\n {\n status: \"reconnecting\",\n attempt: currentAttempt + 1,\n nextAttemptMs: delay,\n },\n ...extraEffects,\n { type: \"start-reconnect-timer\", delayMs: delay },\n ]\n }\n\n return {\n init: [{ status: \"disconnected\" }],\n\n update(msg, model): [WebsocketClientState, ...WsClientEffect[]] {\n switch (msg.type) {\n // -----------------------------------------------------------------\n // start\n // -----------------------------------------------------------------\n case \"start\": {\n if (model.status !== \"disconnected\") return [model]\n return [\n { status: \"connecting\", attempt: 1 },\n { type: \"create-websocket\", attempt: 1 },\n ]\n }\n\n // -----------------------------------------------------------------\n // socket-opened\n // -----------------------------------------------------------------\n case \"socket-opened\": {\n if (model.status !== \"connecting\") return [model]\n return [{ status: \"connected\" }, { type: \"start-keepalive\" }]\n }\n\n // -----------------------------------------------------------------\n // server-ready\n // -----------------------------------------------------------------\n case \"server-ready\": {\n // Already ready — ignore duplicate\n if (model.status === \"ready\") return [model]\n\n // Normal path: connected → ready\n if (model.status === \"connected\") {\n return [{ status: \"ready\" }, { type: \"add-channel-and-establish\" }]\n }\n\n // Race condition: server sent \"ready\" before client's open event fired.\n // Skip connected, go directly to ready with both keepalive and channel effects.\n if (model.status === \"connecting\") {\n return [\n { status: \"ready\" },\n { type: \"start-keepalive\" },\n { type: \"add-channel-and-establish\" },\n ]\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // socket-closed\n // -----------------------------------------------------------------\n case \"socket-closed\": {\n const reason: DisconnectReason = {\n type: \"closed\",\n code: msg.code,\n reason: msg.reason,\n }\n\n if (model.status === \"connected\") {\n return tryReconnect(0, reason, { type: \"stop-keepalive\" })\n }\n\n if (model.status === \"ready\") {\n return tryReconnect(\n 0,\n reason,\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // socket-error\n // -----------------------------------------------------------------\n case \"socket-error\": {\n const reason: DisconnectReason = {\n type: \"error\",\n error: msg.error,\n }\n\n if (model.status === \"connecting\") {\n return tryReconnect(model.attempt, reason)\n }\n\n if (model.status === \"connected\") {\n return tryReconnect(0, reason, { type: \"stop-keepalive\" })\n }\n\n if (model.status === \"ready\") {\n return tryReconnect(\n 0,\n reason,\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [model]\n }\n\n // -----------------------------------------------------------------\n // reconnect-timer-fired\n // -----------------------------------------------------------------\n case \"reconnect-timer-fired\": {\n if (model.status !== \"reconnecting\") return [model]\n return [\n { status: \"connecting\", attempt: model.attempt },\n { type: \"create-websocket\", attempt: model.attempt },\n ]\n }\n\n // -----------------------------------------------------------------\n // stop\n // -----------------------------------------------------------------\n case \"stop\": {\n if (model.status === \"disconnected\") return [model]\n\n const effects: WsClientEffect[] = [{ type: \"cancel-reconnect-timer\" }]\n\n if (model.status === \"connecting\") {\n effects.push({ type: \"close-websocket\" })\n }\n\n if (model.status === \"connected\") {\n effects.push(\n { type: \"close-websocket\" },\n { type: \"stop-keepalive\" },\n )\n }\n\n if (model.status === \"ready\") {\n effects.push(\n { type: \"close-websocket\" },\n { type: \"stop-keepalive\" },\n { type: \"remove-channel\" },\n )\n }\n\n return [\n { status: \"disconnected\", reason: { type: \"intentional\" } },\n ...effects,\n ]\n }\n }\n },\n }\n}\n","// client-transport — Websocket client transport for @kyneta/exchange.\n//\n// Thin imperative shell around the pure client program (client-program.ts).\n// The program produces data effects; this module interprets them as I/O.\n//\n// FC/IS design:\n// - client-program.ts: pure Mealy machine (functional core)\n// - client-transport.ts: effect executor (imperative shell)\n//\n// Uses the kyneta wire format (CBOR codec + framing + fragmentation)\n// for binary messages. Text frames carry the \"ready\" handshake and\n// keepalive ping/pong.\n\nimport type { ObservableHandle, TransitionListener } from \"@kyneta/machine\"\nimport { createObservableProgram } from \"@kyneta/machine\"\nimport type {\n Channel,\n ChannelMsg,\n GeneratedChannel,\n PeerId,\n TransportFactory,\n} from \"@kyneta/transport\"\nimport { Transport } from \"@kyneta/transport\"\nimport {\n decodeBinaryMessages,\n encodeBinaryAndSend,\n FragmentReassembler,\n} from \"@kyneta/wire\"\nimport {\n createWsClientProgram,\n type WsClientEffect,\n type WsClientMsg,\n} from \"./client-program.js\"\nimport type {\n DisconnectReason,\n WebsocketClientState,\n WebsocketClientStateTransition,\n} from \"./types.js\"\n\n// Re-export state types for convenience\nexport type {\n DisconnectReason,\n WebsocketClientState,\n WebsocketClientStateTransition,\n}\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\n/**\n * Default fragment threshold in bytes.\n * AWS API Gateway has a 128KB limit, so 100KB provides a safe margin.\n */\nexport const DEFAULT_FRAGMENT_THRESHOLD = 100 * 1024\n\n/**\n * Options for the Websocket client transport (browser connections).\n */\nexport interface WebsocketClientOptions {\n /** Websocket URL to connect to. Can be a string or a function of peerId. */\n url: string | ((peerId: PeerId) => string)\n\n /** Optional custom WebSocket implementation (for Node.js or testing). */\n WebSocket?: typeof globalThis.WebSocket\n\n /** Reconnection options. */\n reconnect?: {\n enabled?: boolean\n maxAttempts?: number\n baseDelay?: number\n maxDelay?: number\n }\n\n /** Keepalive interval in ms (default: 30000). */\n keepaliveInterval?: number\n\n /**\n * Fragment threshold in bytes. Messages larger than this are fragmented.\n * Set to 0 to disable fragmentation (not recommended for cloud deployments).\n * Default: 100KB\n */\n fragmentThreshold?: number\n\n /** Lifecycle event callbacks. */\n lifecycle?: WebsocketClientLifecycleEvents\n}\n\n/**\n * Lifecycle event callbacks for the Websocket client.\n */\nexport interface WebsocketClientLifecycleEvents {\n /** Called on every state transition (delivered async via microtask). */\n onStateChange?: (transition: WebsocketClientStateTransition) => void\n\n /** Called when the connection is lost. */\n onDisconnect?: (reason: DisconnectReason) => void\n\n /** Called when a reconnection attempt is scheduled. */\n onReconnecting?: (attempt: number, nextAttemptMs: number) => void\n\n /** Called when reconnection succeeds after a previous connection. */\n onReconnected?: () => void\n\n /** Called when the server sends the \"ready\" signal. */\n onReady?: () => void\n}\n\n/**\n * Options for service-to-service Websocket connections.\n * Extends WebsocketClientOptions with header support for authentication.\n *\n * Note: Headers are a Bun/Node-specific extension. The browser WebSocket API\n * does not support custom headers per the WHATWG spec.\n */\nexport interface ServiceWebsocketClientOptions extends WebsocketClientOptions {\n /**\n * Headers to send during Websocket upgrade.\n * Used for authentication in service-to-service communication.\n */\n headers?: Record<string, string>\n}\n\n// ---------------------------------------------------------------------------\n// WebsocketClientTransport\n// ---------------------------------------------------------------------------\n\n/**\n * Websocket client network transport for @kyneta/exchange.\n *\n * Connects to a Websocket server, sends and receives ChannelMsg via\n * the kyneta wire format (CBOR codec + framing + fragmentation).\n *\n * Internally, the connection lifecycle is a `Program<Msg, Model, Fx>` —\n * a pure Mealy machine whose transitions are deterministically testable.\n * This class is the imperative shell that interprets data effects as I/O.\n *\n * Prefer the factory functions for construction:\n * - `createWebsocketClient()` — browser-to-server\n * - `createServiceWebsocketClient()` — service-to-service (with headers)\n */\nexport class WebsocketClientTransport extends Transport<void> {\n #peerId?: PeerId\n #options: ServiceWebsocketClientOptions\n #WebSocketImpl: typeof globalThis.WebSocket\n\n // Observable program handle — created in constructor, drives all state\n #handle: ObservableHandle<WsClientMsg, WebsocketClientState>\n\n // Executor-local I/O state — not in the program model\n #socket?: WebSocket\n #serverChannel?: Channel\n #keepaliveTimer?: ReturnType<typeof setInterval>\n #reconnectTimer?: ReturnType<typeof setTimeout>\n\n // Fragmentation\n readonly #fragmentThreshold: number\n readonly #reassembler: FragmentReassembler\n\n constructor(options: ServiceWebsocketClientOptions) {\n super({ transportType: \"websocket-client\" })\n this.#options = options\n this.#WebSocketImpl = options.WebSocket ?? globalThis.WebSocket\n this.#fragmentThreshold =\n options.fragmentThreshold ?? DEFAULT_FRAGMENT_THRESHOLD\n this.#reassembler = new FragmentReassembler({\n timeoutMs: 10_000,\n })\n\n const program = createWsClientProgram({\n reconnect: options.reconnect,\n })\n\n this.#handle = createObservableProgram(program, (effect, dispatch) => {\n this.#executeEffect(effect, dispatch)\n })\n\n // Set up lifecycle event forwarding\n this.#setupLifecycleEvents()\n }\n\n // ==========================================================================\n // Effect executor — interprets data effects as I/O\n // ==========================================================================\n\n #executeEffect(\n effect: WsClientEffect,\n dispatch: (msg: WsClientMsg) => void,\n ): void {\n switch (effect.type) {\n case \"create-websocket\": {\n this.#doCreateWebsocket(dispatch)\n break\n }\n\n case \"close-websocket\": {\n if (this.#socket) {\n this.#socket.close(1000, \"Client disconnecting\")\n this.#socket = undefined\n }\n break\n }\n\n case \"add-channel-and-establish\": {\n // Clean up previous channel if it exists (e.g. after reconnect)\n if (this.#serverChannel) {\n this.removeChannel(this.#serverChannel.channelId)\n this.#serverChannel = undefined\n }\n\n this.#serverChannel = this.addChannel()\n\n // Establish immediately — the server already signaled ready\n this.establishChannel(this.#serverChannel.channelId)\n break\n }\n\n case \"remove-channel\": {\n if (this.#serverChannel) {\n this.removeChannel(this.#serverChannel.channelId)\n this.#serverChannel = undefined\n }\n break\n }\n\n case \"start-reconnect-timer\": {\n this.#reconnectTimer = setTimeout(() => {\n this.#reconnectTimer = undefined\n dispatch({ type: \"reconnect-timer-fired\" })\n }, effect.delayMs)\n break\n }\n\n case \"cancel-reconnect-timer\": {\n if (this.#reconnectTimer !== undefined) {\n clearTimeout(this.#reconnectTimer)\n this.#reconnectTimer = undefined\n }\n break\n }\n\n case \"start-keepalive\": {\n this.#startKeepalive()\n break\n }\n\n case \"stop-keepalive\": {\n this.#stopKeepalive()\n break\n }\n }\n }\n\n // ==========================================================================\n // WebSocket creation — the core I/O operation\n // ==========================================================================\n\n /**\n * Create a WebSocket and wire up event handlers to dispatch messages.\n *\n * The message handler is set up IMMEDIATELY after creation (before\n * the open event) to handle the race condition where the server sends\n * \"ready\" before the client's open promise resolves.\n */\n #doCreateWebsocket(dispatch: (msg: WsClientMsg) => void): void {\n const peerId = this.#peerId\n if (!peerId) {\n dispatch({\n type: \"socket-error\",\n error: new Error(\"Cannot connect: peerId not set\"),\n })\n return\n }\n\n // Resolve URL\n const url =\n typeof this.#options.url === \"function\"\n ? this.#options.url(peerId)\n : this.#options.url\n\n try {\n // Create WebSocket with optional headers (Bun-specific extension)\n if (\n this.#options.headers &&\n Object.keys(this.#options.headers).length > 0\n ) {\n type BunWebSocketConstructor = new (\n url: string,\n options: { headers: Record<string, string> },\n ) => WebSocket\n const BunWebSocket = this\n .#WebSocketImpl as unknown as BunWebSocketConstructor\n this.#socket = new BunWebSocket(url, {\n headers: this.#options.headers,\n })\n } else {\n this.#socket = new this.#WebSocketImpl(url)\n }\n this.#socket.binaryType = \"arraybuffer\"\n\n const socket = this.#socket\n\n // Set up message handler IMMEDIATELY to handle the \"ready\" race condition.\n // The server may send \"ready\" before the open event fires.\n socket.addEventListener(\"message\", (event: MessageEvent) => {\n this.#handleMessage(event, dispatch)\n })\n\n // Track whether we've dispatched a terminal event for this connection attempt\n let settled = false\n\n const onOpen = () => {\n cleanup()\n settled = true\n dispatch({ type: \"socket-opened\" })\n\n // After open, set up permanent close handler for post-connection closes\n socket.addEventListener(\"close\", (event: CloseEvent) => {\n dispatch({\n type: \"socket-closed\",\n code: event.code,\n reason: event.reason,\n })\n })\n }\n\n const onError = () => {\n if (settled) return\n cleanup()\n settled = true\n dispatch({\n type: \"socket-error\",\n error: new Error(\"WebSocket connection failed\"),\n })\n }\n\n const onClose = () => {\n if (settled) return\n cleanup()\n settled = true\n dispatch({\n type: \"socket-error\",\n error: new Error(\"WebSocket closed during connection\"),\n })\n }\n\n const cleanup = () => {\n socket.removeEventListener(\"open\", onOpen)\n socket.removeEventListener(\"error\", onError)\n socket.removeEventListener(\"close\", onClose)\n }\n\n socket.addEventListener(\"open\", onOpen)\n socket.addEventListener(\"error\", onError)\n socket.addEventListener(\"close\", onClose)\n } catch (error) {\n dispatch({\n type: \"socket-error\",\n error: error instanceof Error ? error : new Error(String(error)),\n })\n }\n }\n\n // ==========================================================================\n // Message handling — I/O parsing logic\n // ==========================================================================\n\n /**\n * Handle incoming Websocket messages.\n *\n * Text frames carry the \"ready\" handshake and keepalive pong.\n * Binary frames carry CBOR-encoded ChannelMsg.\n */\n #handleMessage(\n event: MessageEvent,\n dispatch: (msg: WsClientMsg) => void,\n ): void {\n const data = event.data\n\n // Handle text messages (keepalive and ready signal)\n if (typeof data === \"string\") {\n if (data === \"ready\") {\n dispatch({ type: \"server-ready\" })\n }\n // Ignore pong responses and other text\n return\n }\n\n // Handle binary messages through shared decode pipeline\n if (data instanceof ArrayBuffer) {\n try {\n const messages = decodeBinaryMessages(\n new Uint8Array(data),\n this.#reassembler,\n )\n if (messages) {\n for (const msg of messages) {\n this.#handleChannelMessage(msg)\n }\n }\n } catch (error) {\n console.error(\"Failed to decode message:\", error)\n }\n }\n }\n\n /**\n * Handle a decoded channel message.\n */\n #handleChannelMessage(msg: ChannelMsg): void {\n if (!this.#serverChannel) {\n return\n }\n\n // Deliver synchronously — the Synchronizer's receive queue prevents recursion\n this.#serverChannel.onReceive(msg)\n }\n\n // ==========================================================================\n // Keepalive\n // ==========================================================================\n\n #startKeepalive(): void {\n this.#stopKeepalive()\n\n const interval = this.#options.keepaliveInterval ?? 30_000\n\n this.#keepaliveTimer = setInterval(() => {\n if (this.#socket?.readyState === WebSocket.OPEN) {\n this.#socket.send(\"ping\")\n }\n }, interval)\n }\n\n #stopKeepalive(): void {\n if (this.#keepaliveTimer) {\n clearInterval(this.#keepaliveTimer)\n this.#keepaliveTimer = undefined\n }\n }\n\n // ==========================================================================\n // Lifecycle event forwarding\n // ==========================================================================\n\n #setupLifecycleEvents(): void {\n // wasConnectedBefore is observer-local state, not in the program model\n let wasConnectedBefore = false\n\n this.#handle.subscribeToTransitions(transition => {\n // Forward to onStateChange callback\n this.#options.lifecycle?.onStateChange?.(transition)\n\n const { from, to } = transition\n\n // onDisconnect: transitioning TO disconnected\n if (to.status === \"disconnected\" && to.reason) {\n this.#options.lifecycle?.onDisconnect?.(to.reason)\n }\n\n // onReconnecting: transitioning TO reconnecting\n if (to.status === \"reconnecting\") {\n this.#options.lifecycle?.onReconnecting?.(to.attempt, to.nextAttemptMs)\n }\n\n // onReconnected: from reconnecting/connecting TO connected/ready (after prior connection)\n if (\n wasConnectedBefore &&\n (from.status === \"reconnecting\" || from.status === \"connecting\") &&\n (to.status === \"connected\" || to.status === \"ready\")\n ) {\n this.#options.lifecycle?.onReconnected?.()\n }\n\n // onReady: transitioning TO ready\n if (to.status === \"ready\") {\n this.#options.lifecycle?.onReady?.()\n wasConnectedBefore = true\n }\n })\n }\n\n // ==========================================================================\n // State observation — delegated to the observable handle\n // ==========================================================================\n\n /**\n * Get the current connection state.\n */\n getState(): WebsocketClientState {\n return this.#handle.getState()\n }\n\n /**\n * Subscribe to state transitions.\n */\n subscribeToTransitions(\n listener: TransitionListener<WebsocketClientState>,\n ): () => void {\n return this.#handle.subscribeToTransitions(listener)\n }\n\n /**\n * Wait for a specific state.\n */\n waitForState(\n predicate: (state: WebsocketClientState) => boolean,\n options?: { timeoutMs?: number },\n ): Promise<WebsocketClientState> {\n return this.#handle.waitForState(predicate, options)\n }\n\n /**\n * Wait for a specific status.\n */\n waitForStatus(\n status: WebsocketClientState[\"status\"],\n options?: { timeoutMs?: number },\n ): Promise<WebsocketClientState> {\n return this.#handle.waitForStatus(status, options)\n }\n\n /**\n * Whether the client is ready (server ready signal received).\n */\n get isReady(): boolean {\n return this.#handle.getState().status === \"ready\"\n }\n\n // ==========================================================================\n // Transport abstract method implementations\n // ==========================================================================\n\n protected generate(): GeneratedChannel {\n return {\n transportType: this.transportType,\n send: (msg: ChannelMsg) => {\n const socket = this.#socket\n if (!socket || socket.readyState !== WebSocket.OPEN) {\n return\n }\n\n encodeBinaryAndSend(msg, this.#fragmentThreshold, data =>\n socket.send(new Uint8Array(data).buffer),\n )\n },\n stop: () => {\n // Don't call disconnect here — channel.stop() is called when\n // the channel is removed, which can happen during effect execution.\n // The actual disconnect is handled by onStop() or the program.\n },\n }\n }\n\n async onStart(): Promise<void> {\n if (!this.identity) {\n throw new Error(\n \"Transport not properly initialized — identity not available\",\n )\n }\n this.#peerId = this.identity.peerId\n this.#handle.dispatch({ type: \"start\" })\n }\n\n async onStop(): Promise<void> {\n this.#reassembler.dispose()\n this.#handle.dispatch({ type: \"stop\" })\n }\n}\n\n// ---------------------------------------------------------------------------\n// Factory functions\n// ---------------------------------------------------------------------------\n\n/**\n * Create a Websocket client transport factory for browser-to-server\n * connections.\n *\n * Returns an `TransportFactory` — a closure that creates a fresh transport\n * instance when called. Pass directly to `Exchange({ transports: [...] })`.\n *\n * @example\n * ```typescript\n * import { createWebsocketClient } from \"@kyneta/websocket-transport/client\"\n *\n * const exchange = new Exchange({\n * transports: [createWebsocketClient({\n * url: \"ws://localhost:3000/ws\",\n * reconnect: { enabled: true },\n * })],\n * })\n * ```\n */\nexport function createWebsocketClient(\n options: WebsocketClientOptions,\n): TransportFactory {\n return () => new WebsocketClientTransport(options)\n}\n\n/**\n * Create a Websocket client transport for service-to-service connections.\n *\n * This factory is for backend environments (Bun, Node.js) where you need\n * to pass authentication headers during the Websocket upgrade.\n *\n * Note: Headers are a Bun/Node-specific extension. The browser WebSocket API\n * does not support custom headers. For browser clients, use\n * `createWebsocketClient()` and authenticate via URL query parameters.\n *\n * @example\n * ```typescript\n * import { createServiceWebsocketClient } from \"@kyneta/websocket-transport/client\"\n *\n * const exchange = new Exchange({\n * transports: [createServiceWebsocketClient({\n * url: \"ws://primary-server:3000/ws\",\n * headers: { Authorization: \"Bearer token\" },\n * reconnect: { enabled: true },\n * })],\n * })\n * ```\n */\nexport function createServiceWebsocketClient(\n options: ServiceWebsocketClientOptions,\n): TransportFactory {\n return () => new WebsocketClientTransport(options)\n}\n"],"mappings":"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","names":[]}
|