Docs

JavaScript SDK

The full API of @foony/realtime, the WebSocket client for browsers and Node.

One package covers both transports: Realtime holds a WebSocket for live subscriptions, Rest makes stateless HTTPS calls. Both ship TypeScript types.

npm install @foony/realtime

Realtime constructor

import { Realtime } from '@foony/realtime';

const realtime = new Realtime({
  authCallback: async () => fetchTokenFromYourBackend(),
});
Option Type Default Meaning
key string API key, for servers and trusted contexts
token string Static JWT, for scripts and local dev
authCallback () => Promise<string> | string Returns a fresh JWT, called on connect and re-auth
endpoint string realtime.foony.io Host or full wss:// URL
clientId string Identity attached to key-auth connections
autoReconnect boolean true Reconnect with exponential backoff, 1 s to 30 s
queueMessages boolean true Buffer publishes while disconnected
batch BatchOptions Auto-batching defaults for all channels

Exactly one of key, token, or authCallback is required.

Connection

realtime.connection.on((state, reason) => {
  console.log(state, reason?.message);
});

await realtime.connection.once('connected');
console.log(realtime.getConnectionId(), realtime.getClientId());

await realtime.close();

States move through initialized, connecting, connected, disconnected, closing, closed, and failed. On reconnect the SDK re-attaches subscribed channels, re-enters presence it had entered, flushes queued publishes, and backfills any missed messages. Resent publishes carry stable ids, so the service deduplicates them.

Channels

const channel = realtime.channels.get('chat:lobby');
realtime.channels.release('chat:lobby');

get returns a stable instance per name and accepts { cipher, batch } options. release detaches and forgets the channel. Names are 1 to 255 characters from A-Z a-z 0-9 : - _, colons for hierarchy, no leading colon.

Messages

channel.subscribe(listener);
channel.subscribe('typing', listener);
channel.subscribe(['added', 'removed'], listener);
channel.unsubscribe();

await channel.publish('chat', { text: 'hello' });
await channel.publish([{ name: 'a', data: 1 }, { name: 'b', data: 2 }]);
await channel.publish('cursor', { x, y }, { ephemeral: true });
channel.flush();

See Publish and subscribe for batching, ephemeral semantics, and the message object fields.

History

const { messages, more } = await channel.history({ limit: 50, before: oldestSerial });

Oldest first, with before as an exclusive serial cursor for paging backward. Details in History.

Presence

channel.presence.on(listener);
channel.presence.on('leave', listener);
await channel.presence.enter({ name: 'Acorn' });
await channel.presence.update({ name: 'Acorn', busy: true });
await channel.presence.leave();

Listeners receive the current members as enter events first, then live transitions. Details in Presence.

Channel lifecycle

await channel.attach();
await channel.detach();
console.log(channel.state);

channel.on((change) => {
  console.log(change.current);
});

Subscribing attaches implicitly, so attach is only for warming a channel early. States are initialized, attaching, attached, detaching, detached, suspended, and failed.

End-to-end encryption

import { generateRandomKey } from '@foony/realtime';

const key = await generateRandomKey();
const channel = realtime.channels.get('secure:room', { cipher: { key } });
await channel.publish('chat', { text: 'only we can read this' });

Payloads are AES-GCM encrypted before they leave your process and the service only ever sees ciphertext. See Encryption for keys, what stays plaintext, and reading encrypted history.

Rest client

import { Rest } from '@foony/realtime';

const rest = new Rest({ key: process.env.FOONY_API_KEY! });

await rest.channels.get('chat:lobby').publish('chat', { text: 'from a function' });
const history = await rest.channels.get('chat:lobby').history({ limit: 100 });
const presence = await rest.channels.get('chat:lobby').presence.get();
const details = await rest.auth.requestToken({ clientId: 'user-123' });
const serverTime = await rest.time();

Read methods return paginated results with items, next(), hasNext(), and isLast(). Failures throw RestError with the numeric code and HTTP statusCode from the error table.

Errors

Error codes are exported as the ErrorCode object, so handlers can compare against named constants instead of magic numbers:

import { ErrorCode } from '@foony/realtime';

channel.publish('chat', data).catch((error) => {
  if (error.code === ErrorCode.Capability) {
    showUpgradePrompt();
  }
});