Documentation

Connection and Lifecycle

Fetch credentials, connect a channel, observe recovery, and release resources.

Before you start: add com.useceleris:celeris-client (see the overview). Prepare an endpoint that signs credentials, as Authentication describes. A trusted backend can sign its own credentials instead. See Server-side usage.

1. Fetch credentials

The SDK has a built-in production endpoint. Supply a credential provider that obtains signed credentials from your application for each connection attempt. The provider returns them as a CompletionStage. The JDK has no JSON library. This example reads and writes JSON with Jackson 3 (tools.jackson.core:jackson-databind). Gson works the same way.

import com.useceleris.client.CelerisClient;
import com.useceleris.client.Channel;
import com.useceleris.client.ClientOptions;
import com.useceleris.client.CredentialRequest;
import com.useceleris.client.Credentials;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.concurrent.CompletionStage;
import tools.jackson.databind.ObjectMapper;
import tools.jackson.databind.json.JsonMapper;

public final class RealtimeApp {
  private static final ObjectMapper MAPPER = JsonMapper.builder().build();
  private static final HttpClient HTTP = HttpClient.newHttpClient();

  private RealtimeApp() {}

  static CompletionStage<Credentials> fetchCredentials(CredentialRequest request) {
    String body =
        MAPPER.writeValueAsString(
            Map.of(
                "channelReference", request.channelReference(),
                "replayLookbackMs", request.replayLookback().map(Duration::toMillis).orElse(0L)));
    HttpRequest httpRequest =
        HttpRequest.newBuilder(URI.create("https://your-app.example/api/realtime-credentials"))
            .header("Authorization", "Bearer ...")
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();

    return HTTP.sendAsync(httpRequest, HttpResponse.BodyHandlers.ofByteArray())
        .thenApply(
            response -> {
              if (response.statusCode() != 200) {
                throw new IllegalStateException(
                    "credential endpoint answered " + response.statusCode());
              }

              return MAPPER.readValue(response.body(), Credentials.class);
            });
  }

  public static void main(String[] arguments) {
    CelerisClient client =
        CelerisClient.create(ClientOptions.builder(RealtimeApp::fetchCredentials).build());

    try (Channel channel = client.channel("demo-room")) {
      // The steps below continue here.
    }
  }
}

The provider receives a CredentialRequest with channelReference() and reconnect(). The reconnect() value is false for the attempt that connect() starts and true for automatic recovery. On recovery the request also carries disconnectedAt(), an Optional<Instant>, and replayLookback(), an Optional<Duration> that covers the outage so far plus five seconds. Both values are empty on the first attempt. The SDK calls the provider on its own threads. When a deadline or close() abandons an attempt, the SDK cancels the stage that the provider returned. The SDK discards a result that arrives later.

Credentials is a record that Jackson and Gson read and write as {"payload": "...", "signature": "..."}. It decodes your endpoint's response directly, and its toString never shows either value. Return both values exactly as your backend signed them. Do not generate, cache, or inspect them. If the provider throws, or its stage fails, the SDK reports a safe TRANSPORT error. The SDK does not forward your exception's text.

When you create a client, a channel, or a segment, the SDK does not open a connection. Use baseUrl only for local or self-hosted Celeris, not your application's credential endpoint. See the Java client API for the options.

2. Observe state, then connect

ChannelEventHandler events = channel.events();
Registration stateListener =
    events.onStateChange(state -> System.out.println("Connection: " + state));
Registration errorListener =
    events.onError(error -> System.out.println("Error: " + error.getMessage()));

try {
  channel.connect().join();
} catch (CompletionException failure) {
  System.out.println("Initial connection failed: " + failure.getCause().getMessage());
}

The expected states are connecting, then connected. A failed initial attempt fails the future that connect() returned and sets failed. join() rethrows that failure inside a CompletionException. The SDK does not also report that failure to onError, and it does not retry the attempt automatically.

Once connected, the channel is automatically a member of default. Use channel.defaultSegment() to access that stream. Do not open another connection or call subscribe() just to join it.

StateWhat your application must do
IDLECall connect() when ready
CONNECTINGWait. Do not make another connect() call
CONNECTEDPublish, subscribe, and query
RECONNECTINGDisable send actions and show the connection status
FAILEDReport the failure. Call connect() again to retry
CLOSINGWait for cleanup
CLOSEDCreate a new channel if you need another connection

ChannelState prints the lowercase names that every Celeris SDK uses, such as connected. A connect() call in the connected, connecting, or reconnecting state fails with OPERATION_IN_PROGRESS.

To cancel a connection attempt, cancel its future or give it a deadline with orTimeout. The SDK abandons the attempt, and the channel becomes failed:

try {
  channel.connect().orTimeout(5, TimeUnit.SECONDS).join();
} catch (CompletionException failure) {
  if (failure.getCause() instanceof TimeoutException) {
    System.out.println("Gave up connecting after 5 seconds.");
  }
}

The deadline bounds only the attempt. An established connection outlives it. The connectTimeout option, 15 seconds by default, bounds credentials and handshake together and fails the attempt with TIMEOUT. The reconnectTimeout option bounds a reconnect attempt and defaults to connectTimeout. Each timeout option accepts a Duration from 1 ms to Duration.ofMinutes(15). For a larger value, build() throws CONFIGURATION.

3. Respond to recovery

Register a recovery listener once when you set up the channel:

Registration recoveryListener =
    events.onRecovery(
        event -> {
          System.out.println("Recovered after retry " + event.retryIndex());
          // Reload your application's authoritative state here.
        });

retryIndex() counts the failed reconnect attempts already consumed in the current budget. The budget carries over when a connection drops within 60 seconds after it connected. possibleGaps() and possibleDuplicates() are always true. Reload the data that matters from your own API. Then reconcile new messages with an application-owned version.

Listeners survive reconnects. Do not register them again on every connected state. The SDK catches a listener that throws and reports the exception through onError. Also handle exceptions inside any thread or stage that you start in a listener.

Replay sends at most the last 100 messages of each segment, for at most 2 minutes. default replays when the channel connects. Other segments replay when the connection joins them with subscribe. A publish starts no replay. If the reconnect credentials have no replay claim, the missed messages are not available, and the recovery event reports the gap. See Reconcile application data.

See Reconnection and Recovery for triggers, timing, restored interests, and replay. The recovery event is not a replay-complete signal.

4. Release resources

Each event registration returns a Registration, and its close() removes the listener. Subscription handles have cancel(). The channel owns the socket. The try-with-resources block from step 1 closes the channel. You can also close it yourself:

channel.close();
recoveryListener.close();
errorListener.close();
stateListener.close();

close() is idempotent and permanent. It returns within about five seconds even when the server does not answer. The SDK first flushes publishes already handed to the socket. Then close() fails pending publishes and presence queries with CANCELLED, cancels recovery, and closes the channel's segment handles too. closeAsync() does the same work, does not block, and returns a future.

Listeners can still receive events that the SDK queued before close() returned, such as the closed state change. Do not reuse a closed channel. Obtain a new one from the client.

Next: Messaging.

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.