Documentation

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();
  }
}
OptionTypeDefaultDescription
credentialProviderCredentialProvider—Required, as builder's argument. Called for every connection attempt.
baseUrlStringproduction endpointOnly for a local or self-hosted stack
allowInsecureLoopbackbooleanfalsePermits ws:// for loopback hosts. Never set in production.
connectTimeoutDuration15 sCovers credential acquisition and handshake together. From 1 ms to 15 minutes
presenceQueryTimeoutDuration10 sDeadline for one presenceList. From 1 ms to 15 minutes
reconnectTimeoutDurationconnectTimeoutThe same combined deadline, per reconnect attempt. From 1 ms to 15 minutes
publishQueueSizeint64Publishes that can wait for writer room before BACKPRESSURE
deduplicationWindowSizeint1024Message ids remembered per channel for duplicate filtering
maximumReconnectAttemptsint10Failed 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
}
MemberNotes
stateThe current state. It can change as soon as you read it
connectCompletes 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.
closeIdempotent 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.
closeAsyncclose 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
}
MethodNotes
subscribeA 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 / onPresenceReturn 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.
subscribePresenceTells 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.
presenceListOne 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.

CodeRaised when
CONFIGURATIONThe call is wrong, or a credential provider returned invalid credentials. Fix the code. Do not retry.
TIMEOUTThe connect deadline elapsed, or a presence query timed out
CANCELLEDThe channel closed during a connect, a queued publish or a query, or a newer attempt replaced a connection attempt
TRANSPORTHandshake or socket failure, a credential provider that failed, a connection lost during a presence query, or a listener that threw
NOT_CONNECTEDA publish when the channel is not connected and does not reconnect, a presence query while not connected, or an action on a closed channel
BACKPRESSUREThe 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_PROGRESSA concurrent connect(), or a second presenceList
DELIVERY_UNKNOWNThe socket failed, or you canceled, during the write of a publish: acceptance genuinely unknown
PROTOCOLOne 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.

ErrorsubType()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 elseemptyempty
type()Sent when
PermissionDeniedErrorThe token lacks access to what the command tried
MessageSizeLimitErrorA command exceeded your plan's size limit
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 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 recoveryPause 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 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 deadlineconnectTimeout, 15 s by default, covers credentials and handshake. A reconnect attempt uses reconnectTimeout, which defaults to it
Presence querypresenceQueryTimeout, 10 s by default, one in flight per channel. A timeout never drops the connection
CloseAbout 5 s graceful, then forced
HeartbeatThe 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 connectionA 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.

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.