e-bon
e-bon.ro
Realtime

Reconnect and lifecycle

The auto-reconnect schedule, the close / suspend / resume controls, and the CursorStore hook for future at-least-once delivery.

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:

ConstantValueMeaning
DEFAULT_RECONNECT_DELAY1000 msFirst retry happens 1 second after disconnect.
RECONNECT_BACKOFF_FACTOR2Each subsequent retry doubles the delay.
MAX_RECONNECT_DELAY30000 msCap — 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 until resume() 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>;
}

Where to next

  • Events — the eight partner-facing payload shapes you can listen for.
  • Schemas — server→device and device→server wire frames.