Messaging
Subscribe to a segment, exchange byte payloads, and understand what publish completion means.
Before you start: use the client and unconnected channel setup from Connecting and lifecycle. Your credentials must grant read/write access to chat.
Connecting to a channel automatically joins default. Steps 1 and 2 use an additional named segment, chat, so they explicitly subscribe. For messaging directly on default, skip to step 3.
1. Listen and subscribe
In web/app.js, register these interests before the existing Connect button calls connect(). The final line below shows the connection step; do not add a second simultaneous call.
import { readText, textPayload } from "@useceleris/client";
const chat = channel.segment("chat");
const stopListening = chat.onMessage((payload, metadata) => {
console.log(metadata.tokenReference, readText(payload));
console.log("Message ID:", metadata.messageId);
});
const membership = chat.subscribe();
await channel.connect();
onMessage() registers a local callback. subscribe() records interest in the segment. Calling only onMessage() does not join chat; default is different because connecting already joins it.
The listener receives payload bytes first, then metadata. All segments on this channel share one socket. Multiple handles for the same segment share listener sets and reference-counted interest.
2. Publish bytes
await chat.publish({ payload: textPayload("Hello from this connection") });
Open a second connected subscriber to observe receipt. By default, a connection does not receive its own publish. Your server can opt into echo with allowEcho: true when signing credentials.
A resolved publish means only that the local socket accepted the bytes. A server permission denial or size rejection can arrive later through channel.events().onError. There is no per-publish receipt.
Publishing also joins the segment server-side. It does not grant read permission: a write-only member receives nothing.
Optional message identifiers
await chat.publish({
messageId: crypto.randomUUID(),
payload: textPayload("An update with an application-supplied ID"),
});
If omitted, the SDK generates a random one, so every publish can be recognised if it is resent. Reusing an ID is not a guaranteed exactly-once publishing strategy. The client filters repeated IDs only within a bounded window; use your own durable idempotency for business operations.
Size and disconnected behavior
The SDK rejects an encoded command larger than 2 MiB before writing it. Framing, identifiers, and payload together count toward that bound.
The server enforces lower plan payload limits: Free 64 KiB, Standard 128 KiB, Pro 512 KiB, Prime 1024 KiB. A plan-limit rejection arrives asynchronously and still counts toward usage. Check payload size before sending; do not use rejection as a size probe.
There is no offline queue. Disconnected publishes reject with NotConnected, and so do publishes still waiting to be sent when the connection drops. A DeliveryUnknown error means acceptance is uncertain; blindly retrying may duplicate an action.
When the server rate-limits the connection, its RateLimitError does not say which command it dropped. The SDK pauses sending for at least a second, then resends what it sent in the last two seconds: subscription changes first, then up to the last 64 publishes, each at most once with its original ID, so receivers drop a copy that had already arrived. Resends count toward usage. After eight rate limits in a row the SDK treats the limit as a used-up quota: it stops resending and retries the subscriptions it dropped after a minute, doubling to at most an hour, until commands go through without a limit again. If rate limits keep arriving, publish less often.
3. Use the default segment deliberately
const defaultSegment = channel.defaultSegment(); // Same as segment("default").
You are already a member of default when the channel connects. This handle accesses that membership; it does not create a new socket or require subscribe(). Use it when one stream per channel is enough.
To try it with the Quickstart, make these changes in both tabs' shared application:
- In
server.mjs, replace thechatsegment permission with{ segmentId: "default", read: true, write: true }. Keep the channel restricted todemo-room, and restart the credential server. - In
web/app.js, replaceconst chat = channel.segment("chat");with the line below. Keeping the variable name lets the existing publish and listener code stay unchanged.
const chat = channel.defaultSegment();
- Remove
const membership = chat.subscribe();and its matchingmembership.cancel();cleanup call. Keepchat.onMessage(), its disposer, andchannel.close(). - Reload both tabs to obtain the new permissions, connect them, and send a message. The other tab receives it through
default, without either tab callingsubscribe().
The resulting message setup is:
const channel = client.channel("demo-room");
const chat = channel.defaultSegment();
const stopMessages = chat.onMessage((payload, metadata) => {
log(metadata.tokenReference + ": " + readText(payload));
});
// The existing Connect button calls channel.connect().
// The existing Send handler calls chat.publish({ payload: textPayload(message) }).
Automatic membership does not bypass authorization. Your credentials still need read permission to receive and write permission to publish. The unchanged Quickstart grants only chat, which is why the server-side permission change above is necessary.
default is a stream, not a wildcard: it does not receive messages published to chat or other named segments. Sender echo still follows allowEcho, just as it does on any other segment.
The default segment cannot be remotely unsubscribed. Automatic membership does not subscribe to presence.
4. Clean up subscriptions and listeners
For the named-segment example in steps 1 and 2:
stopListening();
membership.cancel();
await channel.close();
For the default-segment Quickstart variation in step 3, keep the existing stopMessages() and channel.close() cleanup. There is no subscription handle to cancel; closing the channel leaves default.
Removing a listener and cancelling a subscription are separate actions. One caller cancelling does not remove another caller's held subscription. On a non-default segment, remote message unsubscribe is sent only when both message and presence interest counts reach zero; cancelling presence alone does not leave message membership.
Expected: the second tab receives the text once, while the sender sees no received copy unless echo is enabled. Cleanup stops this receiver and closes its channel.
The SDK restores held interests after reconnecting, but a reconnect never resends publishes. Read Reconnection and Recovery.
Next: Payload formats.