Java Client API
Full reference for celeris-client: CelerisClient, Channel, Segment, payload helpers, types and errors.
com.useceleris:celeris-client:1.0.0
The SDK requires Java 17 or newer. The module and package are com.useceleris.client. The SDK has no runtime dependencies. The WebSocket is the JDK's own java.net.http client, which Android does not have. Clients, channels, segments and subscriptions are safe for concurrent use.
CelerisClient.create()
public static CelerisClient create(ClientOptions options)
public final class ClientOptions {
public static Builder builder(CredentialProvider credentialProvider);
public static final class Builder {
public Builder baseUrl(String baseUrl);
public Builder allowInsecureLoopback(boolean allowInsecureLoopback);
public Builder connectTimeout(Duration connectTimeout);
public Builder presenceQueryTimeout(Duration presenceQueryTimeout);
public Builder reconnectTimeout(Duration reconnectTimeout);
public Builder publishQueueSize(int publishQueueSize);
public Builder deduplicationWindowSize(int deduplicationWindowSize);
public Builder maximumReconnectAttempts(int maximumReconnectAttempts);
public ClientOptions build();
}
}
| Option | Type | Default | Description |
|---|---|---|---|
credentialProvider | CredentialProvider | — | Required, as builder's argument. Called for every connection attempt. |
baseUrl | String | production endpoint | Only for a local or self-hosted stack |
allowInsecureLoopback | boolean | false | Permits ws:// for loopback hosts. Never set in production. |
connectTimeout | Duration | 15 s | Covers credential acquisition and handshake together. From 1 ms to 15 minutes |
presenceQueryTimeout | Duration | 10 s | Deadline for one presenceList. From 1 ms to 15 minutes |
reconnectTimeout | Duration | connectTimeout | The same combined deadline, per reconnect attempt. From 1 ms to 15 minutes |
publishQueueSize | int | 64 | Publishes that can wait for writer room before BACKPRESSURE |
deduplicationWindowSize | int | 1024 | Message ids remembered per channel for duplicate filtering |
maximumReconnectAttempts | int | 10 | Failed reconnect attempts in one budget before the channel stops in failed. From 1 to 100 |
create performs no network work. The client's threads, all daemon threads, start when first needed. Create one client per application. build() throws CONFIGURATION for a missing provider, an invalid base URL, a queue or window size under 1, a maximumReconnectAttempts value that is not from 1 to 100, or an invalid timeout. A timeout is invalid when it is missing, under a millisecond or over 15 minutes. baseUrl is the realtime socket endpoint, not your HTTP credential endpoint. The value must be wss://, or ws:// to localhost, [::1] or a dotted 127.x.x.x address with allowInsecureLoopback. The value must carry no user info, query string or fragment. The client always verifies TLS against the JVM's default trust store, through its own SSL context. An application that replaces the JVM's default SSL context therefore cannot weaken verification. The client never follows redirects. The JVM's default proxy selector applies.
CelerisClient
public Channel channel(String reference)
channel is side-effect free and returns a new connection handle on every call. A reference is 1 to 255 ASCII letters, digits, - or _. Anything else throws CONFIGURATION.
Channel
public final class Channel implements AutoCloseable {
public ChannelState state();
public CompletableFuture<Void> connect();
public void close();
public CompletableFuture<Void> closeAsync();
public Segment segment(String segmentId);
public Segment defaultSegment(); // segment("default")
public ChannelEventHandler events();
}
public enum ChannelState {
IDLE, CONNECTING, CONNECTED, RECONNECTING, FAILED, CLOSING, CLOSED
// toString() gives "idle", "connecting" and so on, as every Celeris SDK spells them
}
| Member | Notes |
|---|---|
state | The current state. It can change as soon as you read it |
connect | Completes once the socket is up and held subscriptions are queued to be restored. Fails with OPERATION_IN_PROGRESS if connecting, connected or reconnecting, NOT_CONNECTED once closing or closed. An initial failure both fails the future and sets failed. Canceling the future, or an orTimeout deadline, abandons the attempt and sets failed. |
close | Idempotent and terminal. Returns within about 5 s, and every call waits for the same close. Flushes publishes already handed to the socket, and fails waiting publishes and queries with CANCELLED. Ends every segment handle with the channel. |
closeAsync | close that does not block. Every call returns a fresh copy of the same future |
segment(id) | Side-effect free. The id is required: non-empty, without CR, LF or unpaired surrogates. Anything else throws CONFIGURATION. |
defaultSegment() | The "default" segment that every connection joins automatically. |
A failed channel recovers when you call connect() again. A closed channel does not recover. The SDK does not retry initial connection failures automatically. See Reconnection and Recovery for triggers, deadlines, retry budgets and restored subscriptions.
A successful connect() joins default. defaultSegment().onMessage(listener) receives its messages without subscribe(). Read and write permissions still apply. Default membership includes no other segment and no presence subscription. The automatic join also happens on reconnect.
Segment
public final class Segment {
public String segmentId();
public Subscription subscribe();
public Subscription subscribePresence();
public Registration onMessage(MessageListener listener);
public Registration onPresence(Consumer<PresenceEvent> listener);
public CompletableFuture<Void> publish(byte[] payload);
public CompletableFuture<Void> publish(byte[] payload, String messageId);
public CompletableFuture<PresencePage> presenceList(int page, int perPage);
}
public interface MessageListener {
void onMessage(byte[] payload, MessageMetadata metadata);
}
public final class Subscription implements AutoCloseable {
public void cancel(); // idempotent; close() does the same
}
public final class Registration implements AutoCloseable {
public void close(); // removes exactly that listener; idempotent
}
| Method | Notes |
|---|---|
subscribe | A real server operation, restored after every reconnect. Never sent for "default", which the SDK joins automatically. Throws NOT_CONNECTED once the channel is closing or closed. No acknowledgment exists: a denial arrives later through onError. |
onMessage / onPresence | Return a Registration that removes that listener. Handles for the same segment share one listener set. A null listener throws NullPointerException. Each message listener receives its own copy of the payload. |
publish(payload) | Completes on local acceptance only, with a generated message ID. An empty payload is valid, and it is copied before publish returns. Fails with CONFIGURATION for a command over 2 MiB encoded. The server enforces your plan's size limit afterwards. Does not join the connection to the segment. To receive messages, call subscribe. Canceling the future withdraws a publish that the socket did not start to write. |
publish(payload, messageId) | publish with your own ID: non-empty, without CR, LF or unpaired surrogates. Receivers drop a repeated ID within their deduplication window. |
subscribePresence | Tells the server to send join and leave events. You get these events only while you hold a presence subscription. It does not join the segment for messages, and it does not keep the segment. |
presenceList | One in flight per channel. A second fails with OPERATION_IN_PROGRESS. page is at least 1, perPage 1 to 100, both validated rather than clamped. A query that the server refuses fails with a ServerErrorException with subType() "PRES_LIST" at once. A timeout or cancellation frees the slot and leaves the connection up. A listener that waits for its result always times out. |
Payload helpers
public final class Payloads {
public static byte[] text(String text);
public static String readText(byte[] payload);
}
public final class PayloadCodec<T> {
public static <T> PayloadCodec<T> of(
Function<? super T, byte[]> encode, Function<byte[], ? extends T> decode);
public byte[] encodePayload(T value);
public T readPayload(byte[] payload); // decodes; does not validate
}
Payloads.text encodes UTF-8 and replaces an unpaired surrogate with U+FFFD. Payloads.readText throws CONFIGURATION for bytes that are not valid UTF-8. There is no JSON helper, because the JDK has no JSON library and the SDK bundles none. JSON goes through a PayloadCodec built from Jackson, Gson or another library. See Payload formats. PayloadCodec.of throws CONFIGURATION for a null function. Exceptions from your own encode or decode propagate unchanged. No size check happens here. publish enforces the encoded-command bound.
Types
public record MessageMetadata(
String tokenReference,
String segmentId,
String messageId, // always present: the publisher's, or one the server assigned
long timestamp) {} // Unix milliseconds
public record PresenceEvent(
String segmentId,
String tokenReference,
String connectionId,
boolean joined, // false: the connection left
long timestamp) {}
public record PresencePage(
String segmentId,
int total,
int perPage,
int currentPage,
int from, // from > to past the last page
int to,
List<PresenceConnection> connections) {} // unmodifiable
public record PresenceConnection(String tokenReference, String connectionId, long timestamp) {}
public record ServerNotice(long timestamp, byte[] payload) {}
public record RecoveryEvent(
int retryIndex,
boolean possibleGaps, // always true
boolean possibleDuplicates) {} // always true
public record CredentialRequest(
String channelReference,
boolean reconnect, // false for the attempt connect() starts
Optional<Instant> disconnectedAt, // empty unless reconnect
Optional<Duration> replayLookback) {} // the outage plus 5 s; empty unless reconnect
public record Credentials(String payload, String signature) {}
public interface CredentialProvider {
CompletionStage<Credentials> provide(CredentialRequest request);
}
Presence figures are int values as received. Timestamps are long Unix milliseconds. Convert a timestamp with Instant.ofEpochMilli. replayLookback is whole milliseconds, at most 4294967295 ms. Each notice listener receives its own copy of the payload. ServerNotice.toString shows only its size. Credentials reads and writes as {"payload": ..., "signature": ...} with Jackson or Gson. Its toString returns Credentials[redacted].
The SDK calls the provider on its own threads, once per attempt, reconnects included. When its deadline or close() abandons an attempt, the SDK cancels the returned stage and discards a later result. Null, empty or ill-formed values fail the attempt with CONFIGURATION, which also ends recovery. The SDK reports a provider that throws, or whose stage fails, as TRANSPORT. The SDK never passes on the provider's text.
ChannelEventHandler
public final class ChannelEventHandler {
public Registration onStateChange(Consumer<ChannelState> listener);
public Registration onRecovery(Consumer<RecoveryEvent> listener);
public Registration onNotice(Consumer<ServerNotice> listener);
public Registration onError(Consumer<RuntimeException> listener);
public Registration onMessage(MessageListener listener);
}
Each registration returns a Registration that removes it. Listeners run one at a time on the client's threads, in the order events happened, never under an SDK lock. A slow listener delays delivery and does not grow a backlog. A listener can call back into the channel. The events that its calls cause follow once it returns. The SDK catches a listener that throws and reports it through onError as TRANSPORT. The SDK does not report an error listener that throws. A recovery event follows the connected state change. It does not signal replay completion.
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. Each listener gets a different copy of the payload. The Registration that onMessage returns removes only this listener. The other listeners and the subscriptions do not change, and when you call close() again, it has no effect.
onError receives failures that no caller waits for. Examples are a ServerErrorException, or a CelerisException such as an undecodable message, a reconnect that gave up, or a listener that threw. onNotice delivers raw server prose, such as greetings and subscription acknowledgments and refusals. It carries no segment id and no correlation id. Do not parse it.
Futures
connect, publish and presenceList return a CompletableFuture. They never throw, except NullPointerException for a null argument. Every failure completes the future. join() rethrows a failure wrapped in a CompletionException.
Futures complete on the client's worker threads, each as its own task, independently of event delivery and of one another. A listener can wait for connect or publish, never for presenceList, because the SDK routes its reply only once listeners return. A stage you attach can wait for any of the channel's futures. A stage attached without an executor to a pending future runs on that worker. Cancel the future that the SDK returned. A cancel of a derived stage does not reach the operation. An orTimeout deadline abandons the operation in the same way as a cancel. A future that you complete yourself does the same. A canceled future completes with CancellationException. An orTimeout deadline completes the future with TimeoutException.
Errors
public final class CelerisException extends RuntimeException {
public CelerisException(ErrorCode code, String message);
public ErrorCode code();
public Optional<String> field(); // present only for PROTOCOL
public OptionalInt offset(); // zero-based byte offset, present only for PROTOCOL
}
public enum ErrorCode {
CONFIGURATION("Configuration"),
TIMEOUT("Timeout"),
CANCELLED("Cancelled"),
TRANSPORT("Transport"),
NOT_CONNECTED("NotConnected"),
BACKPRESSURE("Backpressure"),
OPERATION_IN_PROGRESS("OperationInProgress"),
DELIVERY_UNKNOWN("DeliveryUnknown"),
PROTOCOL("ProtocolError");
public String code(); // the name every Celeris SDK uses
}
Every failure the SDK detects is a CelerisException, which is unchecked. Its message says what failed and which rule or limit it broke, such as Invalid channel reference. Must not be empty.. The message never carries your input, credentials or server bytes. The exception never wraps a cause. Match on code(). Errors that the server sends are a ServerErrorException instead. Never match on message text.
| Code | Raised when |
|---|---|
CONFIGURATION | The call is wrong, or a credential provider returned invalid credentials. Fix the code. Do not retry. |
TIMEOUT | The connect deadline elapsed, or a presence query timed out |
CANCELLED | The channel closed during a connect, a queued publish or a query, or a newer attempt replaced a connection attempt |
TRANSPORT | Handshake or socket failure, a credential provider that failed, a connection lost during a presence query, or a listener that threw |
NOT_CONNECTED | A publish when the channel is not connected and does not reconnect, a presence query while not connected, or an action on a closed channel |
BACKPRESSURE | The publish queue (publishQueueSize, 64 by default) is full, with the message "The publish queue is full (size N). Retry once some publishes have gone out." Or a presence query while the writer is full or paused after a rate limit |
OPERATION_IN_PROGRESS | A concurrent connect(), or a second presenceList |
DELIVERY_UNKNOWN | The socket failed, or you canceled, during the write of a publish: acceptance genuinely unknown |
PROTOCOL | One received message could not be decoded. field() and offset() locate the failure |
PROTOCOL means that 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. The SDK reports a refused handshake as TRANSPORT, whatever its HTTP status, and does not guess a more specific code.
ServerErrorException
public final class ServerErrorException extends RuntimeException {
public static final String PARSER_ERROR = "ParserError";
public static final String SEND_ERROR = "SendError";
public static final String PERMISSION_DENIED_ERROR = "PermissionDeniedError";
public static final String RATE_LIMIT_ERROR = "RateLimitError";
public static final String MESSAGE_SIZE_LIMIT_ERROR = "MessageSizeLimitError";
public static final String INTERNAL_ERROR = "InternalError";
public String type(); // one of the above, or a type a newer server adds
public Optional<String> subType(); // the command it answers, e.g. "PUB"; empty when none
public String getMessage(); // the server's own text
public Optional<Object> resource(); // String, Integer, Long or an unmodifiable List of these
}
ServerErrorException carries an error that the server sent, with every field as sent. The connection stays up. It has no code(). Match its type(). It arrives through onError, after the call that caused it, except for a presence query error, which fails presenceList's future instead. Keep a default branch for types that a newer server adds. Check resource()'s type before you use it. The message is the server's text decoded as UTF-8, with any invalid bytes replaced.
| Error | subType() | resource() |
|---|---|---|
| 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 else | empty | empty |
type() | Sent when |
|---|---|
PermissionDeniedError | The token lacks access to what the command tried |
MessageSizeLimitError | A command exceeded your plan's size limit |
RateLimitError | A per-second, per-hour, per-month or per-connection message limit was exceeded. The SDK pauses, then resends recent commands |
ParserError | The server could not parse a command |
SendError | The server failed to send |
InternalError | A fault inside the server, such as a failed presence read |
Limits
| Limit | Value |
|---|---|
| Outbound command | 2 MiB encoded, rejected before any write |
| Encoded command, plan limit | Set by your plan (Limits). Counts the framing, the identifiers and the payload. Enforced by the server |
| Writer bounds | 64 commands and 2 MiB not yet written. Up to publishQueueSize more publishes (64 by default), plus resends, wait in a queue, behind subscription changes |
| Rate-limit recovery | Pause of 1 s plus growing jitter (at most 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 filtering | deduplicationWindowSize message ids per channel, 1024 by default |
| Reconnect | maximumReconnectAttempts 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 deadline | connectTimeout, 15 s by default, covers credentials and handshake. A reconnect attempt uses reconnectTimeout, which defaults to it |
| Presence query | presenceQueryTimeout, 10 s by default, one in flight per channel. A timeout never drops the connection |
| Close | About 5 s graceful, then forced |
| Heartbeat | The server pings every 30 s. The client pings after 15 s of silence from the server, including while a slow listener holds the read side, so slow listeners never cost the connection After each delivered message, when the server stays quiet for 1 s, the SDK sends one more ping, and with no answer in 5 s the connection recovers. This finds a closed connection that the JDK can miss. |
| Dead connection | A ping unanswered for 15 s of reading time, which excludes time your listeners hold, then recovered like any drop |
The SDK never checks the size of received messages. The messages are already in memory when they arrive, so the SDK processes whatever the server sends. A publish rejected for its plan payload limit still counts toward your usage. A local encoded-command rejection sends nothing.
What this SDK will not do
- The SDK does not confirm delivery. There are no receipts anywhere in the protocol.
- The SDK does not keep a durable offline queue. Publishes wait in an in-memory queue only while the channel is
reconnecting. The SDK resends publishes only after a rate limit, once each, with their original ID. - The SDK does not provide durable history or global ordering. Replay is a bounded window, not a cursor.
- The SDK does not sign anything. The signer lives in celeris-server.
- The SDK does not run on Android, which has no
java.net.http.