trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
98 lines • 4.02 kB
TypeScript
/**
* RealtimeRoom — presence + broadcast over a {@link RealtimeTransport}.
*
* const room = RealtimeRoom.join({
* transport: new BroadcastChannelTransport({ id, channel: 'demo' }),
* initialPresence: { name: 'Ada', color: '#6d5bfa' },
* });
*
* room.onPresence((peers) => renderAvatars(peers));
* room.setPresence({ cursor: { x, y } });
* room.on('chat', (e) => appendMessage(e.payload));
* room.broadcast('chat', 'message', { text: 'hi' });
*
* Presence is heartbeat-based: peers re-announce on an interval and are pruned
* after a timeout. Transports with explicit teardown (MemoryHub) also emit an
* immediate `bye` on {@link RealtimeRoom.leave}.
*/
import { Signal } from '../client/reactive.js';
import type { BroadcastEvent, PresencePeer, PresenceState, RealtimeMessage, RealtimeTransport } from './types.js';
export interface RealtimeRoomOptions<P extends PresenceState> {
transport: RealtimeTransport;
/** Initial presence for the local peer. */
initialPresence?: P;
/** Heartbeat interval (ms). Default 2000. Set 0 to disable timers. */
heartbeatMs?: number;
/** Peer expiry after no heartbeat (ms). Default 6000. */
timeoutMs?: number;
/** Injectable clock for tests. Default `Date.now`. */
now?: () => number;
}
type BroadcastHandler = (event: BroadcastEvent) => void;
export declare class RealtimeRoom<P extends PresenceState = PresenceState> {
readonly id: string;
private transport;
private myState;
private peers;
/** Sender timestamps from presence messages (not local lastSeen). */
private presenceTsByPeer;
private channelHandlers;
private _presence;
private now;
private heartbeatMs;
private timeoutMs;
private heartbeatTimer?;
private unsubscribe;
private closed;
private pendingReplay;
/** Ids of broadcasts already delivered — keeps replay idempotent (G-Set). */
private seenMsgIds;
private constructor();
/** Join a room and announce presence. */
static join<P extends PresenceState = PresenceState>(opts: RealtimeRoomOptions<P>): RealtimeRoom<P>;
/** The local peer id. */
get selfId(): string;
/** Current local presence state. */
getSelfState(): P;
/** Merge a partial update into local presence and broadcast it. */
setPresence(partial: Partial<P>): void;
/** Replace local presence wholesale and broadcast it. */
replacePresence(state: P): void;
/** All peers including self (self first). */
getPresence(): PresencePeer<P>[];
/** Peers excluding self. */
getOthers(): PresencePeer<P>[];
/** Subscribe to presence changes. Called immediately with current peers. */
onPresence(cb: (peers: PresencePeer<P>[]) => void): () => void;
/** Reactive presence signal (for framework adapters). */
get presenceSignal(): Signal<PresencePeer<P>[]>;
/**
* Fire-and-forget broadcast to all other peers on a channel. Returns the
* stable message id assigned to this broadcast — persist it alongside an
* optimistic local render so an echoed copy (relay replay, reconnect) is
* deduplicated by id rather than re-rendered.
*/
broadcast(channel: string, event: string, payload: unknown): string;
private newMsgId;
/** Subscribe to broadcasts on a channel. Returns an unsubscribe fn. */
on(channel: string, handler: BroadcastHandler): () => void;
/**
* Apply a relay replay batch (chat history, text snapshot, presence).
* Used when reconnecting to a hub with {@link RelayPersistence}.
*/
replay(messages: RealtimeMessage[]): void;
/** Announce departure and tear down. */
leave(): void;
private handle;
private flushPendingReplay;
private hasActiveSubscribers;
private integrateRemote;
private upsertPeer;
private announceHello;
private announcePresence;
private startHeartbeat;
private pruneExpired;
private recomputePresence;
}
export {};
//# sourceMappingURL=room.d.ts.map