Documentation

Client API

The full reference for @useceleris/client covers createClient, Channel, Segment, the payload helpers, the types and the error codes.

npm install @useceleris/client

The package runs in browsers, Node.js 22.15+, Bun 1.3+ and Deno 2.5+. It ships as ESM with a tested CommonJS entry point. The only runtime dependency is Zod.

createClient(options)

function createClient(options: ClientOptions): Client;
OptionTypeDefaultDescription
credentialProviderCredentialProvider—Required. Called for every connection attempt.
baseUrlstringproduction endpointOnly for a local or self-hosted stack
allowInsecureLoopbackbooleanfalsePermits ws:// for loopback hosts. Never set in production.
connectTimeoutMsnumber15000Covers credential acquisition and handshake together. From 1 to 900000 (15 minutes)
presenceQueryTimeoutMsnumber10000Deadline for one presenceList(). From 1 to 900000 (15 minutes)
reconnectTimeoutMsnumberconnectTimeoutMsThe same combined deadline, per reconnect attempt. From 1 to 900000 (15 minutes)
publishQueueSizenumber64Publishes that can wait for writer room before Backpressure
deduplicationWindowSizenumber1024Message ids remembered per channel for duplicate filtering
maximumReconnectAttemptsnumber10Failed reconnect attempts in one budget before the channel stops in failed. From 1 to 100

createClient performs no network work. The SDK validates known options and strips unknown option keys at runtime. TypeScript callers should use only the documented options.

The timeout options must be integers from 1 to 900000, as JavaScript numbers in milliseconds. A value larger than 15 minutes gives a ConfigurationError. The two size options must also be positive safe integers. maximumReconnectAttempts must be an integer from 1 to 100. Any other value gives a ConfigurationError. Strings, bigint, fractional values, and non-finite values are invalid. The default URL is wss://realtime.useceleris.com. baseUrl is the realtime socket endpoint, not your HTTP credential endpoint.

Client

class Client {
  channel(reference: string): Channel;
}

channel() is side-effect free and returns a new connection handle on every call. A reference is 1–255 characters of [a-zA-Z0-9_-].

Channel

class Channel {
  readonly state: ChannelState;
  connect(options?: { signal?: AbortSignal }): Promise<void>;
  close(): Promise<void>;
  segment(segmentId: string): Segment;
  defaultSegment(): Segment; // segment("default")
  events(): ChannelEventHandler;
}
MemberNotes
stateOne of idle, connecting, connected, reconnecting, failed, closing, closed
connect()Rejects OperationInProgress while the channel is connecting, connected or reconnecting, and NotConnected once closed. An initial failure both rejects and sets failed.
close()Idempotent and terminal, with a 5 s graceful budget. Closes every segment handle with it. Queued publishes and a pending presence query reject with Cancelled.
segment(id)Side-effect free. The id is required.
defaultSegment()The "default" segment every connection joins automatically. The same as segment("default").

You can recover a failed channel when you call connect() again. You cannot recover a closed channel. The SDK does not automatically retry an initial connection failure. See Reconnection and Recovery for triggers, deadlines, retry budgets, and restored subscriptions.

A successful connect() automatically joins default. Use defaultSegment().onMessage(listener) to receive its messages. defaultSegment().subscribe() is not required. Read and write permissions still apply, and default membership does not include other segments or presence subscriptions. This automatic join also happens on reconnect.

Segment

class Segment {
  readonly segmentId: string;
  subscribe(): Subscription;
  onMessage(listener: MessageListener): () => void;
  onPresence(listener: (event: PresenceEvent) => void): () => void;
  publish(options: {
    payload: Uint8Array;
    messageId?: string;
    signal?: AbortSignal;
  }): Promise<void>;
  subscribePresence(): Subscription;
  presenceList(options: {
    page: number;
    perPage: number;
    signal?: AbortSignal;
  }): Promise<PresencePage>;
}
MethodNotes
subscribe()A real server operation. Never sent for "default", which is joined automatically. Throws NotConnected on a closing or closed channel.
onMessage() / onPresence()Return a function that removes that listener. Handles for the same segment share one listener set.
publish()Resolves on local acceptance only. Rejects a command over 2 MiB encoded. The server enforces your plan's size limit afterwards. Empty is valid. Does not join the connection to the segment. To receive messages, call subscribe(). Accepted only while connected or reconnecting, otherwise rejects NotConnected. Waits in the publish queue (publishQueueSize) until the writer has room, also across a reconnect, and resolves when the socket takes it.
subscribePresence()Tells the server to send join and leave events. It does not join the segment for messages, and it does not keep the segment.
presenceList()One in flight per channel. A second rejects OperationInProgress. page is 1–2147483647, perPage 1–100, both validated rather than clamped. A query the server refuses or cannot answer rejects at once with a ServerError whose subType is "PRES_LIST". A timeout or an abort rejects the query and leaves the connection up.

Payload helpers

function textPayload(value: string): Uint8Array;
function jsonPayload(value: unknown): Uint8Array;
function readText(payload: Uint8Array): string;
function readJson<T>(payload: Uint8Array): T; // asserts, does not validate

type PayloadCodec<T> = {
  encode(value: T): Uint8Array;
  decode(bytes: Uint8Array): T;
};

type BoundPayloadCodec<T> = {
  encodePayload(value: T): Uint8Array;
  readPayload(payload: Uint8Array): T;
};

function createPayloadCodec<T>(codec: PayloadCodec<T>): BoundPayloadCodec<T>;

jsonPayload throws ConfigurationError when JSON serialization fails. readText and readJson reject invalid UTF-8 or JSON. textPayload uses the platform's UTF-8 TextEncoder. A failure thrown by your own encode or decode propagates unchanged. No size check happens here: publish() enforces the encoded-command bound.

Types

type ChannelState =
  | "idle"
  | "connecting"
  | "connected"
  | "reconnecting"
  | "failed"
  | "closing"
  | "closed";

type ChannelError =
  ConfigurationError | ConnectionError | ProtocolError | ServerError;

type MessageMetadata = {
  readonly tokenReference: string;
  readonly segmentId: string;
  readonly messageId: string; // always present; the SDK generates one if you omit it
  readonly timestamp: bigint;
};

type MessageListener = (payload: Uint8Array, metadata: MessageMetadata) => void;

type PresenceEvent = {
  readonly segmentId: string;
  readonly tokenReference: string;
  readonly connectionId: string;
  readonly joined: boolean; // false = left
  readonly timestamp: bigint;
};

type PresencePage = {
  readonly segmentId: string;
  readonly total: number;
  readonly perPage: number;
  readonly currentPage: number;
  readonly from: number; // from > to past the last page
  readonly to: number;
  readonly connections: readonly PresenceConnection[];
};

type PresenceConnection = {
  readonly tokenReference: string;
  readonly connectionId: string;
  readonly timestamp: bigint;
};

type ServerNotice = {
  readonly timestamp: bigint;
  readonly payload: Uint8Array;
};

type RecoveryEvent = {
  readonly retryIndex: number;
  readonly possibleGaps: true;
  readonly possibleDuplicates: true;
};

type CredentialRequest = {
  readonly channelReference: string;
  readonly reason: "initial" | "reconnect";
  readonly disconnectedAt?: number;
  readonly replayLookbackMs?: number;
  readonly signal: AbortSignal;
};

type Credentials = { readonly payload: string; readonly signature: string };
type CredentialProvider = (request: CredentialRequest) => Promise<Credentials>;

interface Subscription {
  cancel(): void;
}

Numeric values

FieldType and contract
presenceList({ page })number: integer from 1 through 2,147,483,647
presenceList({ perPage })number: integer from 1 through 100
PresencePage.total, .perPage, .currentPage, .from, .tonumber: decoded signed 32-bit integers, preserved as received
MessageMetadata.timestamp, ServerNotice.timestamp, PresenceEvent.timestamp, PresenceConnection.timestampbigint: exact decoded signed 64-bit integer
ClientOptions.connectTimeoutMs, .reconnectTimeoutMs, .presenceQueryTimeoutMsnumber: integers from 1 through 900,000 (15 minutes), in milliseconds
CredentialRequest.disconnectedAtOptional number: Unix time in milliseconds at the start of the outage
CredentialRequest.replayLookbackMsOptional number: suggested lookback in milliseconds, capped at 4,294,967,295
RecoveryEvent.retryIndexnumber: failed reconnect attempts already consumed in the current budget. Starts at 0, not a count of socket drops
ProtocolError.offsetnumber: byte offset associated with the decoding failure
Numeric values inside ServerError.resourcenumber or bigint: depends on the server's integer encoding. Arrays can contain either

The protocol's Integer32 values decode to number. Integer64 values decode to bigint. A timestamp remains a bigint even when its value would fit in a safe JavaScript number. The SDK exposes PresenceEvent.joined as a boolean, not a numeric flag.

The SDK validates presence request values and does not coerce or clamp them. Use { page: 1, perPage: 25 }, not strings or bigint literals. Returned page figures do not need conversion before arithmetic. Empty pages can have from > to.

JSON.stringify() handles number fields directly but throws on bigint values. Use the serialization examples to convert timestamps, and any bigint values in error resources, to decimal strings. Do not convert every value with Number(), because that can lose precision.

ChannelEventHandler

interface ChannelEventHandler {
  onStateChange(listener: (state: ChannelState) => void): () => void;
  onRecovery(listener: (event: RecoveryEvent) => void): () => void;
  onNotice(listener: (notice: ServerNotice) => void): () => void;
  onError(listener: (error: ChannelError) => void): () => void;
  onMessage(listener: MessageListener): () => void;
}

Each method returns its own disposer. Listeners run synchronously in registration order. The SDK contains a synchronous failure and reports it through onError. The SDK also guards failures in onError callbacks against recursive error dispatch. Handle promise rejections inside asynchronous listener work yourself. A recovery event follows the connected state change. It does not signal replay completion. See Reconnection and Recovery.

onMessage receives all messages from all segments of the connection. It gets each message after the segment listeners, and it gets each message ID one time. All listeners of a message get the same payload. Do not change the payload. The function that onMessage returns removes only this listener. The other listeners and the subscriptions do not change, and when you call it again, it has no effect.

onNotice delivers raw server prose, such as subscription acknowledgments and refusals. It carries no segment id and no correlation id. Do not parse it.

Errors

There are four error classes. The SDK itself raises three: ConfigurationError (code: "Configuration"), ConnectionError (a code from the table below) and ProtocolError (with field and offset). Their messages say what failed and which rule or limit it broke, for example Invalid channel reference. Must not be empty., or a field path such as connectTimeoutMs. The messages never carry your input, credentials or server text. A ProtocolError message also names its field and byte offset. The fourth class, ServerError, carries an error that the server sent. The section below describes it. Match SDK errors on code and server errors on type, never on message text.

CodeRaised when
ConfigurationThe call site is wrong. Fix the code. Do not retry.
TimeoutThe connect or reconnect deadline elapsed, or a presence query timed out
CancelledYour abort signal fired, or close() cancelled a connect, a queued publish or a pending query
TransportHandshake or socket failure, a provider that threw, a listener that threw, or a connection loss during a presence query
NotConnectedA publish while the channel is idle, connecting, failed, closing or closed, a presence query while not connected, or an action on a closed channel
BackpressureThe publish queue (publishQueueSize, 64 by default) is full, or a presence query found the writer full or paused after a rate limit. For a full queue, the message is "The publish queue is full (size N). Retry once some publishes have gone out."
OperationInProgressconnect() while connecting, connected or reconnecting, or a second presenceList()
DeliveryUnknownThe send threw after hand-off — acceptance genuinely unknown
ProtocolErrorOne received frame could not be decoded. Its error also includes field and offset

ProtocolError means the SDK could not decode one message. The SDK drops that message and reports the error, and the connection stays up. The SDK silently skips a command that this SDK version does not recognize, so a newer server cannot break a deployed client.

There is no Authentication code. No runtime exposes the WebSocket handshake status to script, so the SDK reports a rejected handshake as Transport and does not guess.

ServerError

type ServerErrorType =
  | "ParserError"
  | "SendError"
  | "PermissionDeniedError"
  | "RateLimitError"
  | "MessageSizeLimitError"
  | "InternalError";

type ServerErrorResource =
  null | string | number | bigint | readonly ServerErrorResource[];

class ServerError extends Error {
  readonly type: ServerErrorType | (string & {});
  readonly subType: string | null; // the command it answers, e.g. "PUB"
  readonly message: string; // the server's own text, unchanged
  readonly resource: ServerErrorResource; // what that command names, e.g. the segment
}

A ServerError is an error the server sent, with every field exactly as sent, and the connection stays up. It arrives through onError, after the call that caused it, except for a presence query error, which rejects presenceList() instead. type also accepts types a newer server adds, so switch on the six below and keep a default branch. ServerError has no code, so narrow with instanceof before you read code on an error from onError.

resource is not always a string. Integer32 resources are numbers, Integer64 resources are bigints, and arrays can nest these alongside strings and null. Narrow the value with typeof or Array.isArray() before you use it. Do not assume every numeric resource is a timestamp.

ErrorsubTyperesource
A permission denial for a publish, subscribe or presence subscription"PUB", "SUB", "UNSUB", "PRES_SUB" or "PRES_UNSUB"The segment id
A refused or failed presence query"PRES_LIST"The query's request id, which the SDK sets
Anything elsenullnull
typeSent when
PermissionDeniedErrorThe token lacks access to what the command tried
MessageSizeLimitErrorA command exceeded your plan's size limit, or a message exceeded 2 MiB
RateLimitErrorA per-second, per-hour, per-month or per-connection message limit was exceeded. The SDK pauses, then resends recent commands
ParserErrorThe server could not parse a command
SendErrorThe server failed to send
InternalErrorA fault inside the server, such as a failed presence read

Limits

LimitValue
Outbound command2 MiB encoded, rejected before any write
Encoded command, plan limitSet by your plan (Limits). Counts the framing, the identifiers and the payload. Enforced by the server
Writer bounds64 pending commands, 2 MiB buffered. Up to publishQueueSize more publishes (64 by default) wait in a queue, behind subscription changes. The queue stays across a reconnect
Rate-limit recoveryPause of 1 s plus growing jitter (≤31 s), then a resend of the last 2 s of commands (at most 64 publishes, each once). After 8 limits in a row, dropped subscriptions are retried after 1 min, doubling to 1 h
Duplicate filteringdeduplicationWindowSize message ids per channel, 1024 by default
ReconnectmaximumReconnectAttempts failed attempts per budget (10 by default), full-jitter delays up to 30 s. Only network failures and timeouts count. Other failures stop recovery at once. The budget resets at a drop after at least 60 s connected
Connect deadlineconnectTimeoutMs, 15 s by default, covering credentials and handshake. A reconnect attempt uses reconnectTimeoutMs, defaulting to it
Presence querypresenceQueryTimeoutMs, 10 s by default, one in flight per channel
Close5 s graceful, then forced

The SDK does not check the size of received messages. A message is already in the platform's buffer when it arrives, so the SDK processes whatever the server sends. A publish rejected by the server for your plan's size limit still counts toward your usage. A local encoded-command size rejection sends nothing.

What this SDK will not do

The SDK does not do these tasks:

  • Confirm delivery. There are no receipts anywhere in the protocol.
  • Keep a durable offline queue. The publish queue is in memory. It keeps publishes across a reconnect, but not after failed or close(). The SDK resends publishes only after a rate limit, once each, with their original ID.
  • Provide durable history or global ordering. Replay is a bounded window, not a cursor.
  • Sign anything, or expose a signing facility to the browser.

We use Google cookies for analytics and for advertising measurement, only if you allow them. Until you choose, Google sets no cookies. Refer to our Cookie Policy.