Documentation

Client API

Full reference for @useceleris/client: createClient, Channel, Segment, payload helpers, types and error codes.

npm install @useceleris/client

Runs in browsers, Node.js 22.15+, Bun 1.3+ and Deno 2.5+. 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
presenceQueryTimeoutMsnumber10000Deadline for one presenceList()

Performs no network work. Known options are validated; unknown option keys are stripped at runtime. TypeScript callers should use only the documented options.

Both timeout options must be positive safe-integer JavaScript numbers in milliseconds. 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 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 if one is already running, 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.
segment(id)Side-effect free. The id is required.
defaultSegment()The "default" segment every connection joins automatically; the same as segment("default").

failed is recoverable by calling connect() again. closed is not. Initial connection failures are not automatically retried. 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/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.
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; your plan's payload cap is enforced by the server afterwards. Empty is valid. Auto-joins the segment server-side.
subscribePresence()Also joins the segment for messages; cancelling presence does not leave it.
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

function createPayloadCodec<T>(codec: {
  encode(value: T): Uint8Array;
  decode(bytes: Uint8Array): T;
}): {
  encodePayload(value: T): Uint8Array;
  readPayload(payload: Uint8Array): 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 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, .presenceQueryTimeoutMsnumber: positive safe integers, 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 may 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. PresenceEvent.joined is exposed as a boolean, not a numeric flag.

Presence request values are validated, not coerced or clamped: use { page: 1, perPage: 25 }, not strings or bigint literals. Returned page figures do not need conversion before arithmetic. Empty pages may have from > to.

JSON.stringify() handles number fields directly but throws on bigint values. Convert timestamps and any bigint error-resource values to decimal strings using the serialization examples. Avoid blanket conversion to Number(), which 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;
}

Each returns its own disposer. Listeners run synchronously in registration order; a synchronous failure is contained and reported through onError, whose own callback failures are guarded 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.

onNotice delivers raw server prose — subscription acknowledgements and refusals. It carries no segment id and no correlation id. Do not parse it.

Errors

Four classes. Three are raised by the SDK itself — ConfigurationError (code: "Configuration"), ConnectionError (a code from the table below) and ProtocolError (with field and offset) — and 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), but never carry your input, credentials or server text. A ProtocolError message also names its field and byte offset. The fourth, ServerError, carries an error the server sent, described below. 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 deadline elapsed, or a presence query timed out
CancelledYour abort signal fired, or close() cancelled a pending query
TransportHandshake or socket failure, or a provider that threw
NotConnectedPublishing or querying while not connected, or acting on a closed channel
Backpressure64 publishes are already waiting for room in the writer
OperationInProgressA concurrent connect(), 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 one message could not be decoded. That message is dropped, the error is reported, and the connection stays up. A command this SDK version does not recognise is skipped silently, so a newer server cannot break a deployed client.

There is no Authentication code: no runtime exposes the WebSocket handshake status to script, so a rejected handshake is reported as Transport rather than guessing.

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
}

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 reading 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 using 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 publish exceeded your plan's payload cap, 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
Publish payloadPer plan — 64 KiB free, 128 KiB standard, 512 KiB pro, 1024 KiB prime; enforced by the server
Writer bounds64 pending commands, 2 MiB buffered; up to 64 more publishes wait in a queue, behind subscription changes
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 filtering1024 message ids per channel
Reconnect10 failed attempts per budget, full-jitter delays up to 30 s; budget resets at a drop after at least 60 s connected
Connect deadline15 s, covering credentials and handshake
Presence query10 s, one in flight per channel
Close5 s graceful, then forced

Received messages are never size-checked: the platform has already buffered a message by the time it arrives, so the SDK processes whatever the server sends. A publish rejected by the server for its plan payload limit still counts toward your usage; a local encoded-command size rejection sends nothing.

What this SDK will not do

  • Confirm delivery. There are no receipts anywhere in the protocol.
  • Queue while offline. Publishes are resent 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 Analytics cookies to understand how people use Celeris, only if you allow it. See our Cookie Policy.