Reconnect and lifecycle
Reconnect and lifecycle
RealtimeClient is built to stay connected for the lifetime of your process. It reconnects automatically, exposes explicit suspend and resume controls for mobile / battery-aware hosts, and provides a CursorStore injection point so the future at-least-once delivery work (E-1b / E-10) can plug a persistence layer in without changing your code.
Auto-reconnect with exponential backoff
When the socket drops, the client reconnects automatically. The schedule is fixed:
| Constant | Value | Meaning |
|---|---|---|
DEFAULT_RECONNECT_DELAY | 1000 ms | First retry happens 1 second after disconnect. |
RECONNECT_BACKOFF_FACTOR | 2 | Each subsequent retry doubles the delay. |
MAX_RECONNECT_DELAY | 30000 ms | Cap — the delay never grows past 30 seconds. |
So the gaps between reconnect attempts go 1s → 2s → 4s → 8s → 16s → 30s → 30s → … until the socket comes back up, at which point the delay resets to 1 second on the next disconnect. The connected event fires every time the socket reopens; the disconnected event fires every time it drops.
realtime.on('connected', () => console.log('realtime: live'));
realtime.on('disconnected', () => console.warn('realtime: dropped — reconnecting'));
realtime.on('error', (err) => console.error('realtime: socket error', err));
Close the client
Call realtime.close() when you are done. It marks the client as closed, cancels any pending reconnect timer and tears down the underlying socket. After close() the client is permanent — construct a new one if you need to start over.
process.on('SIGTERM', () => realtime.close());
Suspend and resume
suspend() and resume() are the right primitives when you want to temporarily stop receiving events without permanently giving up the connection — for example when an app goes to the background, when the device wakes from sleep, or when a controller hands a device off to another instance.
// App went to background — drop the socket but keep the listeners.
appLifecycle.on('background', () => realtime.suspend());
// App came back to the foreground — reconnect immediately.
appLifecycle.on('foreground', () => realtime.resume());
What each call does:
suspend()— close the socket, cancel any pending reconnect timer and refuse to reconnect untilresume()is called. A no-op if already suspended.resume()— clear the suspended flag, reset the backoff delay to 1 second and connect immediately. A no-op if not currently suspended.
Listeners stay registered across suspend / resume cycles — only the underlying socket cycles.
suspend() does not mark the client as closed. After resume() the same instance is reused; you do not need to re-add your listeners.Malformed messages are silently dropped
Server-to-device wire frames (the schemas on the Schemas page) are validated strictly with zod. A frame that fails validation is dropped and logged via console.warn ([RealtimeClient] dropping invalid wire frame) so a malformed gateway message can never reach a listener as a partially-typed object. The socket stays open.
Non-JSON frames are dropped silently with no log.
Cursor store
RealtimeClientOptions.cursorStore is the persistence hook for the last-seen event cursor. The current client (E-1a) only holds the reference — the read / write paths are wired in with the at-least-once delivery work in E-1b / E-10. Implementing the interface today is forward-compatible:
import { RealtimeClient, type CursorStore } from '@e-bon/realtime';
const cursorStore: CursorStore = {
async load() {
return (await fs.readFile('cursor', 'utf8').catch(() => null)) ?? null;
},
async save(cursor: string) {
await fs.writeFile('cursor', cursor, 'utf8');
},
};
const realtime = new RealtimeClient('https://api.e-bon.ro', {
apiKey: process.env.EBON_API_KEY!,
cursorStore,
});
Implementations can be backed by AsyncStorage, localStorage, a file, or anything else that satisfies the two-method contract:
interface CursorStore {
load(): Promise<string | null>;
save(cursor: string): Promise<void>;
}