Realtime Engine & Event Model
Realtime Engine & Event Model
The RealtimeEngine is the headless, session-scoped entry point of the realtime-core package: it owns the signal connection, routes inbound events to subscribers, intercepts media signaling events for the SFU media layer, and exposes the chat/call API surface. The event model defines how wire-level SignalEvents (bigint-based) are normalized into RealtimeEvents (string-based) for application consumers.
Purpose and Scope
This page documents the Realtime Engine & Event Model — the orchestration core and event contract of realtime-core:
- The
RealtimeEngineclass: construction, lifecycle, event routing, and the full public API (connect,disconnect,sendMessage,sendSignal,getHistory,markRead,startCall,endCall,onEvent). - The event model:
RealtimeEvent(domain shape), the wireSignalEventshape (frommessages.ts), and the normalization helperstoBigIntandevtToEventinevents.ts. - The public package surface in
index.ts, including thecreateRealtimeEnginefactory and the exported types.
Intentionally left to sibling pages (referenced here but not deep-dived):
- Signal & Transport layer —
SignalClientinsignal-client.ts, the gRPC-Web transport intransport.ts, and message codecs incodec.tsimplement the actual wire I/O; this page only covers the engine’s delegation to them. - Media subsystem —
MediaManager,MediaAdapter, and media session handling undermedia/; this page covers only the engine’s interception and forwarding ofmedia_offer/media_answer/media_iceevents. - Message contracts — the exact payload shapes for
MessageInput,HistoryInput,SendSignalInputand theSignalEventwire type are defined inmessages.tsandtypes.ts.
Overview
realtime-core is a headless realtime service — one RealtimeEngine instance per app session, covering both chats (text messages, history, read receipts over a signaling channel) and calls (media rooms over an SFU, negotiated via signaling). The engine is deliberately thin: it composes two subsystems and mediates the event flow between them:
- Signaling side — a
SignalClientmaintains the connection to the realtime backend and emits raw events. - Media side — a
MediaManager(backed by an injectedMediaAdapter) handles WebRTC/SFU media sessions.
The engine’s core design decisions, visible directly in engine.ts:
- Single event funnel: every inbound event passes through the private
route()method, which decides whether the event belongs to the media subsystem or to application listeners. This keeps media internals hidden from app code while giving the app one uniformonEventsubscription surface. - Listener isolation:
emit()wraps each listener callback intry/catchso a throwing subscriber can never break the event stream for other subscribers. - Subscription as disposable:
onEvent(cb)returns an unsubscribe function instead of requiring a separate API, so listeners are self-cleaning. - Adapter injection for media: the engine accepts a
MediaAdapterthroughEngineConfig.mediaAdapter; if no adapter is available, the engine runs in chat-only mode (mediaisnull) and call APIs become no-ops.
The event model normalizes between two representations:
| Layer | Type | Field style | Defined in |
|---|---|---|---|
| Wire | SignalEvent |
snake_case, bigint (event_type, from_id, group_id, created_at, payload) |
messages.ts |
| Domain | RealtimeEvent |
camelCase, string (eventType, fromId, groupId?, payload, createdAt) |
types.ts |
The mapper evtToEvent (in events.ts) performs this conversion, and toBigInt normalizes loose scalar inputs (strings, numbers, null, '', 0) to bigint — a pattern typical for gRPC-Web payloads where int64 values arrive as strings and absent values arrive as 0.
Architecture
Component roles:
createRealtimeEngine(config)(index.ts) — the package factory. It is the only recommended construction path; it simply instantiatesRealtimeEnginewith the givenEngineConfig.RealtimeEngine(engine.ts) — the orchestrator. Owns theSignalClient, the listener set, and (optionally) theMediaManager. All application-facing chat/call methods delegate to these collaborators.SignalClient(signal-client.ts) — the signaling connection. Constructed with a callback that the engine binds toroute(), so every signal event enters the engine through one funnel.MediaManager(media/manager.ts) — the media session owner. It consumes media signaling events from the engine and drives theMediaAdapter; it reports outgoing signals back through thesendMediacallback, which the engine forwards assendSignal({ targetId: 0, signalType, payload }).MediaAdapter(media/adapter.ts) — the pluggable WebRTC/SFU backend.defaultMediaAdapter()may returnnull, which disables media entirely.events.ts— the event-model boundary:toBigIntandevtToEventconvert betweenSignalEvent(wire) andRealtimeEvent(domain).
The direction of the arrows shows the one-way dependency discipline: the engine depends on signal and media collaborators; the signal client pushes events back into the engine; the engine never calls into the application’s listeners directly except through the isolated emit() loop.
Main Content: RealtimeEngine Implementation Walkthrough
Construction and Composition
The engine is created either directly or through the createRealtimeEngine factory. The constructor wires all collaborators in one place:
/** Headless realtime service — one instance per app session (chats + calls over gRPC-Web + SFU) */
export class RealtimeEngine {
readonly signal: SignalClient;
private listeners = new Set<(e: RealtimeEvent) => void>();
private config: EngineConfig;
private media: MediaManager | null;
constructor(config: EngineConfig) {
this.config = config;
this.signal = new SignalClient(config, (e) => this.route(e));
const adapter = config.mediaAdapter ?? defaultMediaAdapter();
this.media = adapter ? new MediaManager(adapter, {
sendMedia: (signalType, payload) => this.signal.sendSignal({ targetId: 0, signalType, payload: JSON.stringify(payload) }),
onLocalStream: (s) => this.config.onLocalStream?.(s),
onRemoteStream: (s) => this.config.onRemoteStream?.(s),
}) : null;
}
Source: engine.ts
Key mechanics:
- Signal wiring:
SignalClientis constructed with the engine’s config plus an event callback bound tothis.route(e). From that point on, every event the signal layer produces is pushed through the engine’s private funnel — the engine never polls and never exposes the raw client callback. - Media resolution:
config.mediaAdapter ?? defaultMediaAdapter()— an explicit adapter in config wins; otherwise the default adapter is used. The??treats an explicitly provided adapter as authoritative, so a caller can deliberately inject a custom WebRTC backend or stub. - Null-adapter degradation:
adapter ? new MediaManager(...) : null— when no adapter exists,mediaisnulland the engine silently runs in chat-only mode.startCall/endCallbecome no-ops (guarded with optional chaining), but messaging still works. - Media callbacks are bridged back to the engine:
sendMedia(signalType, payload)→this.signal.sendSignal({ targetId: 0, signalType, payload: JSON.stringify(payload) }). Media-layer signals are re-encoded as JSON strings and sent through the same signaling channel, targeting the SFU (targetId: 0acts as the “room/server” target on the wire). This is why the app only ever deals with one outbound path.onLocalStream/onRemoteStreamare forwarded straight to the user-suppliedEngineConfigcallbacks, so the app observes media streams without touchingMediaManager.
Event Routing: The Single Funnel
Every inbound event — chat or media — enters through route():
private route(e: RealtimeEvent): void {
if (this.media && (e.eventType === 'media_offer' || e.eventType === 'media_answer' || e.eventType === 'media_ice')) {
this.media.handleEvent(e.eventType, e.payload);
} else {
this.emit(e);
}
}
Source: engine.ts
The routing decision is intentionally coarse: three event types are intercepted, everything else is forwarded. Media signaling events (media_offer, media_answer, media_ice — the classic SDP/ICE negotiation triplet) are consumed by MediaManager.handleEvent, which advances the WebRTC state machine internally. All other events (messages, presence, receipts, etc.) are emitted to application listeners. The this.media && guard means that in chat-only mode, even media events fall through to emit() — the app still sees them as opaque events rather than losing them.
Listener Management and Emission
private emit(e: RealtimeEvent): void {
for (const listener of this.listeners) {
try { listener(e); } catch { /* listener must not break the stream */ }
}
}
onEvent(cb: (e: RealtimeEvent) => void): () => void {
this.listeners.add(cb);
return () => { this.listeners.delete(cb); };
}
Source: engine.ts
Design intent:
- The listener set is a plain
Set, soonEventis idempotent — registering the same callback twice results in one subscription, and unsubscribe is O(1). emititerates the live set and isolates every listener in its owntry/catch. A subscriber that throws cannot kill the stream for others; the comment in source makes the contract explicit: “listener must not break the stream”. Note the empty catch — the error is swallowed intentionally, trading diagnosability for resilience.onEventreturns a dispose closure capturing the callback. Consumers can attach it to component teardown (e.g.,useEffectcleanup in React,onDestroyin frameworks) without holding a reference to the engine.
Public API: Delegation and Call Control
connect(): void { this.signal.connect(); }
disconnect(): void { this.signal.disconnect(); }
sendMessage(input: MessageInput): Promise<void> { return this.signal.sendMessage(input); }
sendSignal(input: SendSignalInput): Promise<void> { return this.signal.sendSignal(input); }
getHistory(input: HistoryInput) { return this.signal.getHistory(input); }
markRead(input: { peerId?: string | number; groupId?: string | number }): Promise<void> { return this.signal.markRead(input); }
/** Connect to an SFU media room and send the initial offer */
async startCall(roomId: string, kind: CallKind): Promise<void> {
if (this.media) await this.media.connect(roomId, kind);
}
/** Leave the SFU media room */
endCall(): void {
this.media?.end();
}
Source: engine.ts
Observations:
- Lifecycle (
connect/disconnect) and chat operations (sendMessage,sendSignal,getHistory,markRead) are pure pass-throughs toSignalClient. The engine’s value here is a stable, minimal public surface: the app callsengine.sendMessage(...)and never touches transport concerns. markReadaccepts eitherpeerIdorgroupId— a 1 conversation or a group thread — reflecting that read receipts apply to both chat kinds. The fields are loosely typed (string | number) because IDs may arrive as strings on the wire but be constructed as numbers in app code; normalization happens downstream inevents.ts/messages.ts.startCallis the only async call API: it awaitsMediaManager.connect(roomId, kind)because the initial SDP offer must be generated before the method resolves.endCallis synchronous — tearing down the room has nothing to await.startCallreturnsundefined-promise whenmediaisnull(chat-only mode); callers should gate the call UI on whether an adapter is present.
Public Package Surface: index.ts
export { RealtimeEngine } from './engine';
export { SignalClient } from './signal-client';
export { toBigInt } from './events';
export * from './messages';
export type {
CallKind,
EngineConfig,
RealtimeEvent,
SendSignalInput,
MessageInput,
HistoryInput,
MediaAdapter,
MediaCallbacks,
} from './types';
import { RealtimeEngine } from './engine';
import type { EngineConfig } from './types';
export function createRealtimeEngine(config: EngineConfig): RealtimeEngine {
return new RealtimeEngine(config);
}
Source: index.ts
The module deliberately exports classes, the event helper, and all message contracts — a complete SDK surface in one import. createRealtimeEngine is the sanctioned factory; exporting RealtimeEngine itself allows advanced use (subclassing, DI containers), while the factory keeps the common path one line: const engine = createRealtimeEngine(config).
Event Model: Wire Events → Domain Events
Normalization Helpers (events.ts)
export function toBigInt(v: string | number | bigint | undefined | null): bigint {
if (v === undefined || v === null || v === '' || v === 0) return 0n;
return BigInt(String(v));
}
export function evtToEvent(e: SignalEvent): RealtimeEvent {
return {
eventType: e.event_type,
fromId: e.from_id.toString(),
groupId: e.group_id === 0n ? undefined : e.group_id.toString(),
payload: e.payload,
createdAt: e.created_at.toString(),
};
}
Source: events.ts
toBigInt(v) is the canonical scalar normalizer for gRPC-Web-style int64 fields. The four-way empty check (undefined | null | '' | 0) collapses every “absent” representation to 0n, then everything else is stringified before BigInt() — safe for numeric strings, number literals, and already-bigint values alike. This matters because gRPC-Web encodes int64 as decimal strings, and JSON round-trips can degrade them to numbers or null.
evtToEvent(e) is the wire→domain mapper and defines the event contract precisely:
RealtimeEvent field |
Derived from SignalEvent |
Rule |
|---|---|---|
eventType |
event_type |
passed through verbatim (e.g., 'message', 'media_offer') |
fromId |
from_id (bigint) |
always a string — the sender’s ID, never undefined |
groupId |
group_id (bigint) |
undefined when group_id === 0n — the “no group” sentinel on the wire becomes absence in the domain model |
payload |
payload |
passed through unchanged (JSON string for media signals, structured data otherwise) |
createdAt |
created_at (bigint) |
string epoch — consumers convert as needed rather than losing precision |
The 0n → undefined rule for groupId is the key semantic: the wire uses 0 as a sentinel for “not in a group” (the same convention the engine reuses for the SFU target targetId: 0 in outbound media signals), while the domain model prefers optionality. Applications can then safely write if (event.groupId) without special-casing zero.
Event Routing Decision
The funnel shows how the this.media && guard combines with the type check: media interception requires both a live MediaManager and one of the three media event types. Otherwise the event is emitted. This is a deliberate degradation path — a chat-only engine still surfaces media events as opaque application events instead of swallowing them.
Core Flow: End-to-End Sequence
Walkthrough of the sequence:
- Construction —
createRealtimeEngine(config)builds the engine, which buildsSignalClient(bound toroute) and optionallyMediaManager(bound tosendMedia/stream callbacks). No network I/O happens yet. - Subscription —
onEvent(cb)registers the callback and returns the unsubscribe closure. Multiple subscribers are supported via theSet. - Connect —
connect()delegates toSignalClient.connect(), opening the gRPC-Web channel. - Inbound events — the signal client asynchronously pushes normalized events into
route(). Media events with a live media layer go toMediaManager.handleEvent; the manager may respond by invoking thesendMediacallback, which the engine forwards tosendSignaltargeting the SFU (targetId: 0) — completing the signaling loop for negotiation. Non-media events go toemit()and reach every listener. - Outbound chat —
sendMessage/getHistory/markReadgo straight to the signal client; the app never sees the wire encoding. - Call lifecycle —
startCall(roomId, kind)awaits the SFU room connection and initial offer;endCall()tears it down synchronously. - Disconnect —
disconnect()closes the channel; listeners are not automatically cleared, so a re-connect()reuses the same subscription set.
Usage Examples
Basic Usage — Chat Session
The canonical setup: create the engine, subscribe, connect, send a message, and clean up.
import { createRealtimeEngine } from './index';
import type { EngineConfig } from './types';
const config: EngineConfig = {
// signaling/server settings (see signal-client.ts and types.ts)
// onLocalStream / onRemoteStream are optional media callbacks
};
const engine = createRealtimeEngine(config);
const unsubscribe = engine.onEvent((e) => {
if (e.eventType === 'message') {
console.log(`message from ${e.fromId}:`, e.payload);
} else {
console.log('event', e.eventType, e);
}
});
engine.connect();
await engine.sendMessage({ /* MessageInput: peerId/groupId + content */ });
// later...
unsubscribe();
engine.disconnect();
This mirrors the engine’s own contract: onEvent returns a disposer, so teardown is symmetrical with setup. The fromId field is always a string and groupId is absent for 1 messages — exactly the normalization evtToEvent guarantees.
Call Flow — SFU Media Session
// Engine is configured with a media adapter (config.mediaAdapter or the default).
await engine.startCall('room-42', 'video'); // CallKind, e.g. 'audio' | 'video'
// ... media negotiation happens automatically via intercepted
// media_offer / media_answer / media_ice events ...
engine.endCall(); // leave the room synchronously
Source: engine.ts
The application never handles SDP or ICE manually — MediaManager consumes those events through route(), and outbound media signals flow back through sendSignal({ targetId: 0, signalType, payload }), which the media layer encodes as JSON strings. In chat-only mode (no adapter), startCall resolves without effect and endCall is a safe no-op due to optional chaining.
Normalizing IDs — BigInt to Domain String
import { toBigInt, evtToEvent } from './events';
import type { SignalEvent } from './messages';
// Wire values that all normalize to 0n:
toBigInt(undefined); // 0n
toBigInt(null); // 0n
toBigInt(''); // 0n
toBigInt(0); // 0n
toBigInt('123'); // 123n
toBigInt(123); // 123n
// Wire event → domain event:
const wire: SignalEvent = {
event_type: 'message',
from_id: 7n,
group_id: 0n, // "not in a group" sentinel
created_at: 1700000000000n,
payload: { text: 'hi' },
};
const evt = evtToEvent(wire);
// evt = { eventType: 'message', fromId: '7', groupId: undefined,
// payload: { text: 'hi' }, createdAt: '1700000000000' }
Source: events.ts
This example makes the sentinel rule visible: group_id: 0n becomes groupId: undefined, while from_id and created_at always surface as strings. Applications that read event.groupId can rely on undefined rather than '0' for group-less events.
Configuration Options
EngineConfig is defined in types.ts (also passed to SignalClient, which consumes the connection-related fields). The options the engine itself reads, as evidenced in engine.ts:
| Option | Type | Default | Description |
|---|---|---|---|
mediaAdapter |
MediaAdapter | null | undefined |
defaultMediaAdapter() (via ??) |
Pluggable WebRTC/SFU backend. null/falsy disables media → chat-only mode; media becomes null and call APIs no-op. |
onLocalStream |
(stream) => void (optional) |
— | Called by MediaManager when the local media stream is ready; forwarded from the engine’s media callbacks. |
onRemoteStream |
(stream) => void (optional) |
— | Called when a remote participant’s stream arrives. |
| (connection fields) | — | — | Additional EngineConfig fields consumed by SignalClient for the gRPC-Web channel (server endpoint, credentials, etc.). Full list in types.ts / signal-client.ts. |
Behavioral notes:
config.mediaAdapter ?? defaultMediaAdapter()means an explicit falsy adapter (null/undefinedassignment) defeats the default — the null-coalescing operator only falls back when the value isnull/undefined, and if the fallback itself isnull, media is disabled.- Media callbacks in config are optional-chained (
this.config.onLocalStream?.(s)), so an engine without stream handlers never throws when streams arrive.
API Reference
class RealtimeEngine — engine.ts
Constructor:
constructor(config: EngineConfig)— builds theSignalClient(routing events throughroute) and theMediaManager(when an adapter resolves). Throws only ifSignalClientconstruction fails on invalid config.
Members:
| Method | Signature | Behavior |
|---|---|---|
onEvent |
(cb: (e: RealtimeEvent) => void) => () => void |
Subscribes a listener; returns an unsubscribe closure. Idempotent per callback (backed by a Set). |
connect |
(): void |
Opens the signaling connection (delegates to SignalClient.connect). |
disconnect |
(): void |
Closes the signaling connection. Listeners are retained across reconnects. |
sendMessage |
(input: MessageInput) => Promise<void> |
Sends a chat message via the signal client. |
sendSignal |
(input: SendSignalInput) => Promise<void> |
Sends an arbitrary signaling message (also used internally for media with targetId: 0). |
getHistory |
(input: HistoryInput) => ... |
Fetches message history (return type from SignalClient.getHistory; see signal-client.ts). |
markRead |
(input: { peerId?: string | number; groupId?: string | number }) => Promise<void> |
Marks a 1 (peerId) or group (groupId) conversation as read. |
startCall |
(roomId: string, kind: CallKind) => Promise<void> |
Connects to an SFU media room and sends the initial offer. No-op (resolves immediately) when media is null. |
endCall |
(): void |
Leaves the SFU room. Safe no-op when media is null. |
Properties:
readonly signal: SignalClient— exposed for advanced use (direct signal access), though all normal operations route through the engine methods.
Event Types — types.ts / events.ts
RealtimeEvent (domain, consumed by listeners):
| Field | Type | Notes |
|---|---|---|
eventType |
string |
e.g. 'message', 'media_offer', 'media_answer', 'media_ice', presence, receipts… |
fromId |
string |
Sender ID, always present, stringified from wire bigint |
groupId |
string | undefined |
undefined when the wire group_id is 0n |
payload |
unknown |
Event-specific data; JSON string for media signals |
createdAt |
string |
Epoch as string (bigint precision preserved) |
Helper functions:
toBigInt(v: string | number | bigint | undefined | null): bigint— normalizes empty/absent scalars to0n; otherwiseBigInt(String(v)). ThrowsSyntaxError-styleRangeErrorfromBigInt()only ifString(v)is not a valid integer string (e.g.,'abc');'',null,undefined, and0are explicitly handled.evtToEvent(e: SignalEvent): RealtimeEvent— wire→domain mapping per the table above. Does not throw for well-formedSignalEvents.
Failure Modes, Edge Cases & Concurrency
Failure Modes
| Failure | Where handled | Behavior |
|---|---|---|
| Listener throws | emit() in engine.ts |
Swallowed per listener (try/catch with empty catch). The stream continues for other subscribers; the error is intentionally not rethrown or logged — apps that need visibility should wrap their own callbacks. |
| No media adapter | Constructor, engine.ts | media = null. Chat APIs unaffected; startCall resolves as a no-op, endCall is a safe no-op. Media events are emitted to listeners instead of being consumed. |
toBigInt on non-numeric string |
events.ts | BigInt(String(v)) throws RangeError for invalid strings (e.g., 'abc'). The four empty cases (undefined, null, '', 0) are guarded before the conversion, so only genuinely malformed payloads can throw. |
| Disconnect while subscribed | disconnect() / onEvent |
Listeners are retained in the Set across disconnects; re-connect() reuses the same subscriptions. Subscribers must call the returned unsubscribe to detach permanently. |
Edge Cases
groupId: 0nsentinel — the wire’s0nmeans “not in a group”;evtToEventmaps it toundefined, soif (event.groupId)is safe. The same zero-target convention appears outbound: media signals usetargetId: 0to address the SFU.- Media events in chat-only mode — with
media === null,media_offer/media_answer/media_icefall throughroute()toemit(). A chat-only app that wants to observe negotiation (e.g., to log or forward it) can still do so. - Duplicate subscriptions —
Setsemantics makeonEvent(cb)idempotent; calling it twice with the same function registers once, and one unsubscribe removes it. fromIdandcreatedAtas strings — the mapper stringifies bigint IDs and timestamps, so consumers never loseint64precision to JavaScriptNumbercoercion; they must parsecreatedAtexplicitly if they need aDate.
Concurrency & Consistency
- Single-threaded event dispatch —
emit()iterates the listenerSetsynchronously in the event loop. Since listeners run inline, a slow or blocking listener delays subsequent listeners and the signal client’s event processing. Thetry/catchprotects against exceptions but not against slow handlers; keep callbacks lightweight or defer heavy work withqueueMicrotask/setTimeout. - One engine per session — the design contract (“one instance per app session”) means the listener
Setand the signal connection are implicitly single-owner. Creating multiple engines per session would multiply signal connections; use one engine and fan out within the app instead. - Call state machine isolation — media negotiation state lives inside
MediaManagerand is driven only viahandleEvent; the engine never touches it, so the media state machine is not interleaved with chat event dispatch. Outbound media signals are serialized through the samesendSignalpath as chat signals. - Async boundary —
startCallawaitsMediaManager.connect(initial SDP offer), whileendCallis synchronous; concurrentstartCall/endCallordering is the caller’s responsibility.
Performance & Operational Notes
- Listener fan-out is O(n) per event with no copy —
emititerates the liveSet. With many subscribers, batch dispatch is cheap; with slow subscribers, it serializes the event loop (see concurrency note). - No event buffering — events are delivered as they arrive; there is no queue or replay in the engine. Apps that need offline replay should rely on
getHistory. - Unsubscribe hygiene — each
onEventcall allocates a closure; returning the disposer is designed for framework teardown, preventing listener leaks in long-lived sessions (SPAs, mobile runtimes). - Media signaling overhead — media signals are
JSON.stringify’d on the outbound path (sendMedia→sendSignal) and re-encoded on the wire; this is acceptable for negotiation messages (low frequency), not for media payloads themselves, which flow over the SFU data path. - Connection lifecycle —
connect/disconnectare direct delegates; reconnects and backoff are owned bySignalClient/transport.tsand are outside the engine’s scope.
Extension Points
- Pluggable
MediaAdapter— the primary extension seam. Inject a custom adapter viaEngineConfig.mediaAdapterto replace the default WebRTC/SFU backend (e.g., a different SFU protocol, a simulator, or a test stub). Returning a falsy adapter yields chat-only mode. EngineConfig.onLocalStream/onRemoteStream— media stream observation hooks; wire them to UI renderers (e.g.,<video>element binding) without touching the engine.onEventsubscription — the general-purpose extension point for any behavior: logging, analytics, forwarding to a store (Redux/Zustand), or re-emitting into an app-level event bus.sendSignalpassthrough — custom signaling payloads can be sent through the engine for app-specific handshakes alongside media negotiation.RealtimeEngineexport + factory —index.tsexports the class and thecreateRealtimeEnginefactory; the class can be subclassed or DI-wrapped while the factory covers the common path.
Related Links
- Signal Client & Transport — the signaling connection the engine delegates to; owns reconnects and the gRPC-Web channel.
- Message Contracts —
SignalEventwire type andMessageInput/HistoryInput/SendSignalInputpayload shapes. - Media Manager — the SFU/WebRTC session owner consuming intercepted media events.
- Media Adapter — the pluggable media backend (
defaultMediaAdapter) andMediaAdaptercontract. - Media Session — per-room session state driven by
media_offer/media_answer/media_ice. - Type Definitions —
EngineConfig,RealtimeEvent,CallKind, and the exported public types. - Package Entry — public exports and the
createRealtimeEnginefactory. - Package Manifest — dependency and build configuration for
realtime-core.