actions/socket.svelte.ts

Frontend WebSocket client — portable, Svelte-reactive, implements WebsocketConnection.

Plain class with $state runes (no Cell inheritance, no app coupling). Drop into any SvelteKit frontend as the underlying connection for FrontendWebsocketTransport. Handles auto-reconnect with exponential backoff, respects WS_CLOSE_SESSION_REVOKED (no reconnect loop after the server revokes auth), exposes reactive status for UI indicators, and ships three correctness primitives default-on:

  • FrontendWebsocketClient.request — promise-based JSON-RPC with auto-assigned ids and a pending-id map. Intercepts responses on the message path so request/response correlation is transport-level rather than re-invented per consumer.
  • Durable queuerequest() calls made while disconnected buffer up to DEFAULT_QUEUE_MAX_SIZE requests and flush on reopen. Overflow rejects with queue_overflow. Raw FrontendWebsocketClient.send is drop-on-disconnect (fire-and-forget notifications want that).
  • Activity-aware heartbeat — idles fire a shared heartbeat request; receive-silence past DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT closes with WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT and lets auto-reconnect pick back up.
view source

Declarations
#

16 declarations

DEFAULT_BACKOFF_FACTOR
#

actions/socket.svelte.ts view source

1.5 import {DEFAULT_BACKOFF_FACTOR} from '@fuzdev/fuz_app/actions/socket.svelte.js';

Exponential backoff factor: delay = base * factor^(attempt-1).

DEFAULT_CLOSE_CODE
#

DEFAULT_HEARTBEAT_INTERVAL
#

actions/socket.svelte.ts view source

30000 import {DEFAULT_HEARTBEAT_INTERVAL} from '@fuzdev/fuz_app/actions/socket.svelte.js';

Idle interval before sending a heartbeat (ms).

DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT
#

DEFAULT_QUEUE_MAX_SIZE
#

actions/socket.svelte.ts view source

100 import {DEFAULT_QUEUE_MAX_SIZE} from '@fuzdev/fuz_app/actions/socket.svelte.js';

Default bound on buffered requests while disconnected. Overflow rejects.

DEFAULT_RECONNECT_DELAY
#

DEFAULT_RECONNECT_DELAY_MAX
#

actions/socket.svelte.ts view source

10000 import {DEFAULT_RECONNECT_DELAY_MAX} from '@fuzdev/fuz_app/actions/socket.svelte.js';

Max reconnect delay in ms (cap on exponential backoff).

FrontendWebsocketClient
#

actions/socket.svelte.ts view source

import {FrontendWebsocketClient} from '@fuzdev/fuz_app/actions/socket.svelte.js';

Reactive WebSocket client implementing WebsocketConnection.

Construct with a URL and optional config; call connect() to open the socket and begin auto-reconnect. Register message/error handlers via add_message_handler / add_error_handler — both return unsubscribe functions. FrontendWebsocketTransport consumes this as its connection.

Session-revocation close codes (WS_CLOSE_SESSION_REVOKED) put the client in a permanently-closed state; reconnecting would just loop on 401.

inheritance

implements: WebsocketConnection,Disposable

ws

type WebSocket | null

$state.raw

status

type SocketStatus

$state.raw

reconnect_count

type number

$state.raw

current_reconnect_delay

type number

$state.raw

last_connect_time

Epoch ms of the most recent successful open. Never cleared on close.

type number | null

$state.raw

last_close_time

Epoch ms of the most recent close event or client-initiated close.

type number | null

$state.raw

last_close_code

Close code from the most recent close. Initial null means "never closed."

type number | null

$state.raw

last_close_reason

Reason string from the most recent close event (may be empty).

type string | null

$state.raw

last_send_error

The error thrown by the most recent attempted send(), or null if the most recent attempt succeeded or none has been attempted yet. Populated when the underlying ws.send throws (e.g., buffer full, serialization error); reset to null on the next successful send. Not touched when send() short-circuits because the socket is not connected — consult connected for that case. Wrappers surfacing per-message failure reasons can read this after a false return from send().

type Error | null

$state.raw

connected

type boolean

readonly $derived

constructor

type new (url: string, options?: FrontendWebsocketClientOptions): FrontendWebsocketClient

url

type string

options

default {}

set_reconnect

Swap the auto-reconnect policy in place. Accepts the same shape as the constructor's reconnect option: false disables reconnect, true or null/omitted restores the defaults, or a config object customizes specific fields (missing fields fall back to defaults, not "keep current" — each call defines the whole policy atomically, same as the constructor).

In-flight reconnect schedules are monotonically shortened: the effective total wait from arm-time never exceeds what the new policy prescribes. If the new target is already past the time already elapsed, the reconnect fires immediately (on the next tick). The wait is never extended.

Turning reconnect off while a reconnect timer is pending cancels that timer and transitions status to closed (since the lie of 'reconnecting' would be visible to UI indicators). Turning it back on does not synthesize a reconnect — wait for the next close.

type (reconnect?: boolean | FrontendWebsocketReconnectOptions | null): void

reconnect

type boolean | FrontendWebsocketReconnectOptions | null
default null
returns void

set_heartbeat

Swap the heartbeat policy in place. Accepts the same shape as the constructor's heartbeat option: false disables the timer, true or null/omitted restores the defaults, or a config object customizes specific fields (missing fields fall back to defaults, not "keep current" — each call defines the whole policy atomically, same as the constructor and set_reconnect).

When connected, the live timer is restarted immediately so the new interval / receive_timeout take effect without a reconnect; when disconnected, just stashes the policy for the next open.

type (heartbeat?: boolean | FrontendWebsocketHeartbeatOptions | null): void

heartbeat

type boolean | FrontendWebsocketHeartbeatOptions | null
default null
returns void

cancel_reconnect

Cancel a scheduled reconnect without closing the client or disabling auto-reconnect. Transitions status from reconnectingclosed and resets the backoff counters — the next close still triggers a fresh reconnect cycle under the current policy. No-op when no reconnect is pending.

Use this when UI state asks "stop trying for now" without the finality of disconnect (which also rejects pending/queued requests and clears heartbeat) or the policy change of set_reconnect(false) (which disables future reconnects). The queue stays intact so that calling connect later flushes buffered work.

type (): void

returns void

connect

Open the WebSocket. No-op on SSR, or if the session has been revoked. Cancels any pending reconnect and tears down any existing connection first; an open prior socket is closed with a normal-closure code.

type (): void

returns void

disconnect

Close the WebSocket, cancel any pending reconnect, and reset the reconnect backoff counters. Puts the client in closed status; call connect() to reopen. Safe to call more than once.

type (code?: number): void

code

type number
default DEFAULT_CLOSE_CODE
returns void

[Symbol.dispose]

Explicit-resource-management hook — supports using client = new FrontendWebsocketClient(url).

type (): void

returns void

send

type (data: object): boolean

data

type object
returns boolean

request

Promise-based JSON-RPC over the socket. Auto-assigns a monotonic request id (or uses an explicit one supplied via options.id — used by FrontendWebsocketTransport which delegates to this method and has its own peer-minted UUID), tracks the pending promise, and resolves when the server sends a matching response.

Callers supplying an explicit options.id are responsible for uniqueness — the pending map is keyed by id, and a duplicate silently overwrites the prior entry. Auto-minted ids are monotonic and never collide with themselves or with peer-minted UUIDs (the types differ: integer vs string).

While the socket is disconnected, the request is buffered in a bounded queue (default-on, DEFAULT_QUEUE_MAX_SIZE) and flushed on reopen. Pass {queue: false} to reject immediately when disconnected — used internally by the heartbeat, which must not fight the queue for the disconnect-detection slot.

On AbortSignal fire: rejects the local promise *and* sends the shared cancel notification (cancel_action_spec.method) so the server-side dispatcher can abort the matching handler's ctx.signal. Suppressed for queued-but-never-sent (server doesn't know about it) and response-beat-cancel races.

type <R = unknown>(method: string, params?: unknown, options?: { signal?: AbortSignal | undefined; queue?: boolean | undefined; id?: string | number | undefined; }): Promise<R>

method

type string

params

type unknown
default {}

options

type { signal?: AbortSignal | undefined; queue?: boolean | undefined; id?: string | number | undefined; }
default {}
returns Promise<R>

throws

  • ThrownJsonrpcError - on the returned promise — never thrown

add_message_handler

type (handler: SocketMessageHandler): () => void

handler

returns () => void

add_error_handler

type (handler: SocketErrorHandler): () => void

handler

returns () => void

url

type string

getter

revoked

Whether the server has permanently closed the session. Once true, all connect() calls are no-ops. Distinct from status:'closed', which reflects any closed state (incl. user-initiated disconnect()).

type boolean

getter

FrontendWebsocketClientOptions
#

actions/socket.svelte.ts view source

FrontendWebsocketClientOptions import type {FrontendWebsocketClientOptions} from '@fuzdev/fuz_app/actions/socket.svelte.js';

reconnect?

Auto-reconnect policy. false disables reconnect entirely; true or omit for default timing; pass an object to customize.

type boolean | FrontendWebsocketReconnectOptions | null

heartbeat?

Activity-aware heartbeat. true/null/omit for defaults; false disables the timer entirely (only do this if the server side is also running without heartbeat); pass an object to tune interval / receive_timeout.

type boolean | FrontendWebsocketHeartbeatOptions | null

queue?

Durable queue for FrontendWebsocketClient.request. true or omit for defaults; false disables buffering (requests while disconnected reject immediately). Raw FrontendWebsocketClient.send is never queued — use request() for RPC semantics.

type boolean | FrontendWebsocketQueueOptions

log?

Optional logger for diagnostic messages.

type Logger | null

FrontendWebsocketHeartbeatOptions
#

actions/socket.svelte.ts view source

FrontendWebsocketHeartbeatOptions import type {FrontendWebsocketHeartbeatOptions} from '@fuzdev/fuz_app/actions/socket.svelte.js';

interval?

Idle duration (ms) after which a heartbeat is sent. Reset by any send or receive — chatty clients never emit extras. Defaults to DEFAULT_HEARTBEAT_INTERVAL.

type number

receive_timeout?

Receive-silence (ms) after which the client closes the socket with WS_CLOSE_CLIENT_HEARTBEAT_TIMEOUT, letting auto-reconnect kick in. Should be a comfortable multiple of interval. Defaults to DEFAULT_HEARTBEAT_RECEIVE_TIMEOUT.

type number

FrontendWebsocketQueueOptions
#

actions/socket.svelte.ts view source

FrontendWebsocketQueueOptions import type {FrontendWebsocketQueueOptions} from '@fuzdev/fuz_app/actions/socket.svelte.js';

max_size?

Maximum number of requests held while the socket is disconnected. Enqueue past this rejects the new call with a queue_overflow error. Defaults to DEFAULT_QUEUE_MAX_SIZE.

type number

FrontendWebsocketReconnectOptions
#

actions/socket.svelte.ts view source

FrontendWebsocketReconnectOptions import type {FrontendWebsocketReconnectOptions} from '@fuzdev/fuz_app/actions/socket.svelte.js';

delay?

Base reconnect delay in ms. Defaults to 1000.

type number

delay_max?

Max reconnect delay in ms (cap on exponential backoff). Defaults to 10000.

type number

factor?

Exponential backoff factor. Defaults to 1.5.

type number

socket_status_to_async_status
#

actions/socket.svelte.ts view source

(status: SocketStatus, revoked: boolean): AsyncStatus import {socket_status_to_async_status} from '@fuzdev/fuz_app/actions/socket.svelte.js';

Project SocketStatus onto fuz_util's AsyncStatus — the 5-way → 4-way mapping every consumer re-derives to surface connection state to UI (loading indicators, retry banners). Collapses reconnecting into failure (UI shows "lost, retrying") and splits closed by revoked so a terminal session-revocation read as failure while a clean client- initiated close reads as initial (the "not connected, not trying" state).

status

revoked

whether the session has been permanently revoked (typically FrontendWebsocketClient.revoked)

type boolean

returns

AsyncStatus

SocketErrorHandler
#

actions/socket.svelte.ts view source

SocketErrorHandler import type {SocketErrorHandler} from '@fuzdev/fuz_app/actions/socket.svelte.js';

(call)

type (event: Event): void

event

type Event
returns void

SocketMessageHandler
#

actions/socket.svelte.ts view source

SocketMessageHandler import type {SocketMessageHandler} from '@fuzdev/fuz_app/actions/socket.svelte.js';

(call)

type (event: MessageEvent<any>): void

event

type MessageEvent<any>
returns void

SocketStatus
#

actions/socket.svelte.ts view source

SocketStatus import type {SocketStatus} from '@fuzdev/fuz_app/actions/socket.svelte.js';

Client-side WebSocket status.

  • initial — never connected; connect() has not been called.
  • connecting — WebSocket readyState === CONNECTING.
  • connected — WebSocket readyState === OPEN.
  • reconnecting — close fired; waiting out backoff before next attempt.
  • closed — socket is not open. Terminal only when revoked is true or auto-reconnect is disabled; otherwise connect() reopens.

Depends on
#