@afterrealism/dendri-client 2.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +141 -0
- package/dist/chunk-MJW5M75V.js +7785 -0
- package/dist/chunk-MJW5M75V.js.map +1 -0
- package/dist/dendri.browser.global.js +6565 -0
- package/dist/dendri.browser.global.js.map +1 -0
- package/dist/dendri.cjs +9725 -0
- package/dist/dendri.cjs.map +1 -0
- package/dist/dendri.d.cts +340 -0
- package/dist/dendri.d.ts +340 -0
- package/dist/dendri.js +1907 -0
- package/dist/dendri.js.map +1 -0
- package/dist/dendri.min.global.js +56 -0
- package/dist/dendri.min.global.js.map +1 -0
- package/dist/serializer.msgpack.cjs +4 -0
- package/dist/serializer.msgpack.cjs.map +1 -0
- package/dist/serializer.msgpack.d.cts +936 -0
- package/dist/serializer.msgpack.d.ts +936 -0
- package/dist/serializer.msgpack.js +4 -0
- package/dist/serializer.msgpack.js.map +1 -0
- package/dist/store-RTivRmUW.d.cts +1378 -0
- package/dist/store-RTivRmUW.d.ts +1378 -0
- package/dist/store.cjs +7685 -0
- package/dist/store.cjs.map +1 -0
- package/dist/store.d.cts +2 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +3 -0
- package/dist/store.js.map +1 -0
- package/package.json +100 -0
|
@@ -0,0 +1,1378 @@
|
|
|
1
|
+
import { EventEmitter, ValidEventTypes } from 'eventemitter3';
|
|
2
|
+
|
|
3
|
+
interface EventsWithError<ErrorType extends string> {
|
|
4
|
+
error: (error: DendriError<`${ErrorType}`>) => void;
|
|
5
|
+
}
|
|
6
|
+
declare class EventEmitterWithError<ErrorType extends string, Events extends EventsWithError<ErrorType>> extends EventEmitter<Events, never> {
|
|
7
|
+
/**
|
|
8
|
+
* Emits a typed error message.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
emitError(type: ErrorType, err: string | Error, retryable?: boolean): void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A DendriError is emitted whenever an error occurs.
|
|
16
|
+
* It always has a `.type`, which can be used to identify the error.
|
|
17
|
+
*/
|
|
18
|
+
declare class DendriError<T extends string> extends Error {
|
|
19
|
+
type: T;
|
|
20
|
+
/** Whether the operation that caused this error can be retried. */
|
|
21
|
+
retryable: boolean;
|
|
22
|
+
/** Structured context about the error. */
|
|
23
|
+
details?: Record<string, unknown>;
|
|
24
|
+
/**
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
constructor(type: T, err: Error | string);
|
|
28
|
+
/** Set the retryable flag (fluent API). */
|
|
29
|
+
setRetryable(retryable: boolean): this;
|
|
30
|
+
/** Set structured details (fluent API). */
|
|
31
|
+
setDetails(details: Record<string, unknown>): this;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare enum ConnectionType {
|
|
35
|
+
Data = "data",
|
|
36
|
+
Media = "media"
|
|
37
|
+
}
|
|
38
|
+
declare enum DendriErrorType {
|
|
39
|
+
/**
|
|
40
|
+
* The client's browser does not support some or all WebRTC features that you are trying to use.
|
|
41
|
+
*/
|
|
42
|
+
BrowserIncompatible = "browser-incompatible",
|
|
43
|
+
/**
|
|
44
|
+
* You've already disconnected this peer from the server and can no longer make any new connections on it.
|
|
45
|
+
*/
|
|
46
|
+
Disconnected = "disconnected",
|
|
47
|
+
/**
|
|
48
|
+
* The ID passed into the Peer constructor contains illegal characters.
|
|
49
|
+
*/
|
|
50
|
+
InvalidID = "invalid-id",
|
|
51
|
+
/**
|
|
52
|
+
* The API key passed into the Peer constructor contains illegal characters or is not in the system (cloud server only).
|
|
53
|
+
*/
|
|
54
|
+
InvalidKey = "invalid-key",
|
|
55
|
+
/**
|
|
56
|
+
* Lost or cannot establish a connection to the signalling server.
|
|
57
|
+
*/
|
|
58
|
+
Network = "network",
|
|
59
|
+
/**
|
|
60
|
+
* The peer you're trying to connect to does not exist.
|
|
61
|
+
*/
|
|
62
|
+
PeerUnavailable = "peer-unavailable",
|
|
63
|
+
/**
|
|
64
|
+
* Dendri is being used securely, but the cloud server does not support SSL. Use a custom DendriServer.
|
|
65
|
+
*/
|
|
66
|
+
SslUnavailable = "ssl-unavailable",
|
|
67
|
+
/**
|
|
68
|
+
* Unable to reach the server.
|
|
69
|
+
*/
|
|
70
|
+
ServerError = "server-error",
|
|
71
|
+
/**
|
|
72
|
+
* An error from the underlying socket.
|
|
73
|
+
*/
|
|
74
|
+
SocketError = "socket-error",
|
|
75
|
+
/**
|
|
76
|
+
* The underlying socket closed unexpectedly.
|
|
77
|
+
*/
|
|
78
|
+
SocketClosed = "socket-closed",
|
|
79
|
+
/**
|
|
80
|
+
* The ID passed into the Peer constructor is already taken.
|
|
81
|
+
*
|
|
82
|
+
* :::caution
|
|
83
|
+
* This error is not fatal if your peer has open peer-to-peer connections.
|
|
84
|
+
* This can happen if you attempt to {@apilink Dendri.reconnect} a peer that has been disconnected from the server,
|
|
85
|
+
* but its old ID has now been taken.
|
|
86
|
+
* :::
|
|
87
|
+
*/
|
|
88
|
+
UnavailableID = "unavailable-id",
|
|
89
|
+
/**
|
|
90
|
+
* Native WebRTC errors.
|
|
91
|
+
*/
|
|
92
|
+
WebRTC = "webrtc"
|
|
93
|
+
}
|
|
94
|
+
declare enum BaseConnectionErrorType {
|
|
95
|
+
NegotiationFailed = "negotiation-failed",
|
|
96
|
+
ConnectionClosed = "connection-closed"
|
|
97
|
+
}
|
|
98
|
+
declare enum DataConnectionErrorType {
|
|
99
|
+
NotOpenYet = "not-open-yet",
|
|
100
|
+
MessageTooBig = "message-too-big",
|
|
101
|
+
/** @deprecated Use {@link MessageTooBig} instead */
|
|
102
|
+
MessageToBig = "message-too-big"
|
|
103
|
+
}
|
|
104
|
+
declare enum SerializationType {
|
|
105
|
+
Binary = "binary",
|
|
106
|
+
BinaryUTF8 = "binary-utf8",
|
|
107
|
+
JSON = "json",
|
|
108
|
+
None = "raw"
|
|
109
|
+
}
|
|
110
|
+
declare enum SocketEventType {
|
|
111
|
+
Message = "message",
|
|
112
|
+
Disconnected = "disconnected",
|
|
113
|
+
Error = "error",
|
|
114
|
+
Close = "close",
|
|
115
|
+
Reconnected = "reconnected",
|
|
116
|
+
ReconnectAttempt = "reconnect-attempt"
|
|
117
|
+
}
|
|
118
|
+
declare enum ServerMessageType {
|
|
119
|
+
Heartbeat = "HEARTBEAT",
|
|
120
|
+
Candidate = "CANDIDATE",
|
|
121
|
+
Offer = "OFFER",
|
|
122
|
+
Answer = "ANSWER",
|
|
123
|
+
Open = "OPEN",// The connection to the server is open.
|
|
124
|
+
Error = "ERROR",// Server error.
|
|
125
|
+
IdTaken = "ID-TAKEN",// The selected ID is taken.
|
|
126
|
+
InvalidKey = "INVALID-KEY",// The given API key cannot be found.
|
|
127
|
+
Leave = "LEAVE",// Another peer has closed its connection to this peer.
|
|
128
|
+
Expire = "EXPIRE",// The offer sent to a peer has expired without response.
|
|
129
|
+
Data = "DATA",// Relayed data message via WebSocket.
|
|
130
|
+
Ack = "ACK",// Server acknowledgement with sequence number.
|
|
131
|
+
RoomJoin = "ROOM-JOIN",// A peer joined a room.
|
|
132
|
+
RoomLeave = "ROOM-LEAVE",// A peer left a room.
|
|
133
|
+
RoomPeers = "ROOM-PEERS",// List of peers in a room.
|
|
134
|
+
HostMigrate = "HOST-MIGRATE",// Host migration notification.
|
|
135
|
+
PresenceUpdate = "PRESENCE-UPDATE",// Presence data broadcast.
|
|
136
|
+
KeyExchange = "KEY-EXCHANGE"
|
|
137
|
+
}
|
|
138
|
+
declare enum TransportMode {
|
|
139
|
+
WebRTC = "webrtc",
|
|
140
|
+
WebSocketRelay = "ws-relay",
|
|
141
|
+
Reconnecting = "reconnecting"
|
|
142
|
+
}
|
|
143
|
+
declare enum ConnectionState {
|
|
144
|
+
/** Initial state, not yet connected */
|
|
145
|
+
Initialized = "initialized",
|
|
146
|
+
/** Attempting to connect to signaling server */
|
|
147
|
+
Connecting = "connecting",
|
|
148
|
+
/** Connected and ready */
|
|
149
|
+
Connected = "connected",
|
|
150
|
+
/** Temporarily disconnected, will auto-reconnect */
|
|
151
|
+
Disconnected = "disconnected",
|
|
152
|
+
/** Extended offline, slower retry schedule */
|
|
153
|
+
Suspended = "suspended",
|
|
154
|
+
/** Explicitly closed by user */
|
|
155
|
+
Closed = "closed",
|
|
156
|
+
/** Unrecoverable error */
|
|
157
|
+
Failed = "failed"
|
|
158
|
+
}
|
|
159
|
+
declare enum ConnectionQuality {
|
|
160
|
+
Excellent = "excellent",
|
|
161
|
+
Good = "good",
|
|
162
|
+
Poor = "poor",
|
|
163
|
+
Unknown = "unknown"
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
interface ServerMessage {
|
|
167
|
+
type: ServerMessageType;
|
|
168
|
+
payload: any;
|
|
169
|
+
src: string;
|
|
170
|
+
/** Server-assigned sequence number for reliable ordering / replay. */
|
|
171
|
+
seq?: number;
|
|
172
|
+
/** Room name associated with this message. */
|
|
173
|
+
room?: string;
|
|
174
|
+
/** Server-assigned timestamp (epoch ms). */
|
|
175
|
+
timestamp?: number;
|
|
176
|
+
/** ACK identifier for delivery confirmation. */
|
|
177
|
+
ackId?: string;
|
|
178
|
+
/** Optional topic for data multiplexing / filtering. */
|
|
179
|
+
topic?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
interface BaseConnectionEvents<ErrorType extends string = BaseConnectionErrorType> extends EventsWithError<ErrorType> {
|
|
183
|
+
/**
|
|
184
|
+
* Emitted when either you or the remote peer closes the connection.
|
|
185
|
+
*
|
|
186
|
+
* ```ts
|
|
187
|
+
* connection.on('close', () => { ... });
|
|
188
|
+
* ```
|
|
189
|
+
*/
|
|
190
|
+
close: () => void;
|
|
191
|
+
/**
|
|
192
|
+
* ```ts
|
|
193
|
+
* connection.on('error', (error) => { ... });
|
|
194
|
+
* ```
|
|
195
|
+
*/
|
|
196
|
+
error: (error: DendriError<`${ErrorType}`>) => void;
|
|
197
|
+
iceStateChanged: (state: RTCIceConnectionState) => void;
|
|
198
|
+
}
|
|
199
|
+
declare abstract class BaseConnection<SubClassEvents extends ValidEventTypes, ErrorType extends string = never> extends EventEmitterWithError<ErrorType | BaseConnectionErrorType, SubClassEvents & BaseConnectionEvents<BaseConnectionErrorType | ErrorType>> {
|
|
200
|
+
/**
|
|
201
|
+
* The ID of the peer on the other end of this connection.
|
|
202
|
+
*/
|
|
203
|
+
readonly peer: string;
|
|
204
|
+
provider: Dendri | null;
|
|
205
|
+
readonly options: any;
|
|
206
|
+
protected _open: boolean;
|
|
207
|
+
/**
|
|
208
|
+
* Any type of metadata associated with the connection,
|
|
209
|
+
* passed in by whoever initiated the connection.
|
|
210
|
+
*/
|
|
211
|
+
readonly metadata: any;
|
|
212
|
+
connectionId: string;
|
|
213
|
+
peerConnection: RTCPeerConnection | null;
|
|
214
|
+
dataChannel: RTCDataChannel | null;
|
|
215
|
+
abstract get type(): ConnectionType;
|
|
216
|
+
/**
|
|
217
|
+
* The optional label passed in or assigned by Dendri when the connection was initiated.
|
|
218
|
+
*/
|
|
219
|
+
label: string;
|
|
220
|
+
/**
|
|
221
|
+
* Whether the media connection is active (e.g. your call has been answered).
|
|
222
|
+
* You can check this if you want to set a maximum wait time for a one-sided call.
|
|
223
|
+
*/
|
|
224
|
+
get open(): boolean;
|
|
225
|
+
protected constructor(
|
|
226
|
+
/**
|
|
227
|
+
* The ID of the peer on the other end of this connection.
|
|
228
|
+
*/
|
|
229
|
+
peer: string, provider: Dendri | null, options: any);
|
|
230
|
+
abstract close(): void;
|
|
231
|
+
/**
|
|
232
|
+
* @internal
|
|
233
|
+
*/
|
|
234
|
+
abstract handleMessage(message: ServerMessage): void;
|
|
235
|
+
/**
|
|
236
|
+
* Called by the Negotiator when the DataChannel is ready.
|
|
237
|
+
* @internal
|
|
238
|
+
* */
|
|
239
|
+
abstract _initializeDataChannel(dc: RTCDataChannel): void;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
interface DataConnectionEvents extends EventsWithError<DataConnectionErrorType | BaseConnectionErrorType>, BaseConnectionEvents<DataConnectionErrorType | BaseConnectionErrorType> {
|
|
243
|
+
/**
|
|
244
|
+
* Emitted when data is received from the remote peer.
|
|
245
|
+
*/
|
|
246
|
+
data: (data: unknown) => void;
|
|
247
|
+
/**
|
|
248
|
+
* Emitted when the connection is established and ready-to-use.
|
|
249
|
+
*/
|
|
250
|
+
open: () => void;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Wraps a DataChannel between two Peers.
|
|
254
|
+
*/
|
|
255
|
+
declare abstract class DataConnection extends BaseConnection<DataConnectionEvents, DataConnectionErrorType> {
|
|
256
|
+
protected static readonly ID_PREFIX = "dc_";
|
|
257
|
+
protected static readonly MAX_BUFFERED_AMOUNT: number;
|
|
258
|
+
private _negotiator;
|
|
259
|
+
abstract readonly serialization: string;
|
|
260
|
+
readonly reliable: boolean;
|
|
261
|
+
get type(): ConnectionType;
|
|
262
|
+
constructor(peerId: string, provider: Dendri, options: any);
|
|
263
|
+
/** Called by the Negotiator when the DataChannel is ready. */
|
|
264
|
+
_initializeDataChannel(dc: RTCDataChannel): void;
|
|
265
|
+
/**
|
|
266
|
+
* Exposed functionality for users.
|
|
267
|
+
*/
|
|
268
|
+
private _flushCloseTimeout;
|
|
269
|
+
/** Allows user to close connection. */
|
|
270
|
+
close(options?: {
|
|
271
|
+
flush?: boolean;
|
|
272
|
+
}): void;
|
|
273
|
+
protected abstract _send(data: any, chunked: boolean): void | Promise<void>;
|
|
274
|
+
/** Allows user to send data. */
|
|
275
|
+
send(data: any, chunked?: boolean): void | Promise<void>;
|
|
276
|
+
handleMessage(message: ServerMessage): Promise<void>;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* ACK-based message delivery confirmation manager.
|
|
281
|
+
*
|
|
282
|
+
* Tracks outgoing messages that expect an acknowledgement and resolves/rejects
|
|
283
|
+
* the corresponding promise when the ACK arrives or the timeout fires.
|
|
284
|
+
*/
|
|
285
|
+
declare class AckManager {
|
|
286
|
+
private readonly _pending;
|
|
287
|
+
private _counter;
|
|
288
|
+
/** Generate a unique ack ID. */
|
|
289
|
+
nextId(): string;
|
|
290
|
+
/**
|
|
291
|
+
* Register a pending ACK with timeout. When `peerId` is supplied, the
|
|
292
|
+
* ACK can be rejected early by {@link rejectAllForPeer} when that peer
|
|
293
|
+
* disconnects, so callers don't wait out the full timeout after the
|
|
294
|
+
* transport layer already knows the target is gone.
|
|
295
|
+
*/
|
|
296
|
+
waitForAck(ackId: string, timeoutMs?: number, peerId?: string): Promise<void>;
|
|
297
|
+
/** Handle an incoming ACK message. Returns true if the ackId was pending. */
|
|
298
|
+
handleAck(ackId: string): boolean;
|
|
299
|
+
/**
|
|
300
|
+
* Reject every pending ACK whose `peerId` matches. Called from the
|
|
301
|
+
* close path of a per-peer connection so broadcastWithAck promises
|
|
302
|
+
* resolve with a clear error immediately instead of stalling until
|
|
303
|
+
* the timeout.
|
|
304
|
+
*/
|
|
305
|
+
rejectAllForPeer(peerId: string): number;
|
|
306
|
+
/** Clean up all pending ACKs (e.g. on disconnect). */
|
|
307
|
+
clear(): void;
|
|
308
|
+
/** Number of ACKs currently awaiting confirmation. */
|
|
309
|
+
get pendingCount(): number;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
interface AnswerOption {
|
|
313
|
+
/**
|
|
314
|
+
* Function which runs before create answer to modify sdp answer message.
|
|
315
|
+
*/
|
|
316
|
+
sdpTransform?: (sdp: string) => string;
|
|
317
|
+
}
|
|
318
|
+
interface DendriOption {
|
|
319
|
+
key?: string;
|
|
320
|
+
host?: string;
|
|
321
|
+
port?: number;
|
|
322
|
+
path?: string;
|
|
323
|
+
secure?: boolean;
|
|
324
|
+
token?: string;
|
|
325
|
+
config?: RTCConfiguration;
|
|
326
|
+
debug?: number;
|
|
327
|
+
referrerPolicy?: ReferrerPolicy;
|
|
328
|
+
/** Auto-fetch TURN credentials from the signaling server's GET /turn endpoint. */
|
|
329
|
+
fetchTurnCredentials?: boolean;
|
|
330
|
+
/** Optional JWT for authenticated connections. */
|
|
331
|
+
jwt?: string;
|
|
332
|
+
/** Enable WebSocket relay fallback when WebRTC is unavailable. */
|
|
333
|
+
enableRelay?: boolean;
|
|
334
|
+
/** Optional function to validate connection metadata before accepting. */
|
|
335
|
+
validateMetadata?: (metadata: unknown) => boolean;
|
|
336
|
+
/** Signaling transport: 'websocket' (default), 'sse', 'polling', or 'auto' (tries WS then SSE then polling) */
|
|
337
|
+
signalingTransport?: "websocket" | "sse" | "polling" | "auto";
|
|
338
|
+
}
|
|
339
|
+
interface DendriConnectOption {
|
|
340
|
+
/**
|
|
341
|
+
* A unique label by which you want to identify this data connection.
|
|
342
|
+
* If left unspecified, a label will be generated at random.
|
|
343
|
+
*
|
|
344
|
+
* Can be accessed with {@apilink DataConnection.label}
|
|
345
|
+
*/
|
|
346
|
+
label?: string;
|
|
347
|
+
/**
|
|
348
|
+
* Metadata associated with the connection, passed in by whoever initiated the connection.
|
|
349
|
+
*
|
|
350
|
+
* Can be accessed with {@apilink DataConnection.metadata}.
|
|
351
|
+
* Can be any serializable type.
|
|
352
|
+
*/
|
|
353
|
+
metadata?: any;
|
|
354
|
+
serialization?: string;
|
|
355
|
+
reliable?: boolean;
|
|
356
|
+
}
|
|
357
|
+
interface HybridConnectionOption {
|
|
358
|
+
/** Milliseconds before falling back to WebSocket relay (default 10000). */
|
|
359
|
+
iceTimeout?: number;
|
|
360
|
+
/** Periodically retry WebRTC when on relay (default true). */
|
|
361
|
+
autoUpgrade?: boolean;
|
|
362
|
+
/** Milliseconds between upgrade attempts (default 60000). */
|
|
363
|
+
upgradeInterval?: number;
|
|
364
|
+
/** Max consecutive upgrade failures before stopping (default 5). */
|
|
365
|
+
maxUpgradeAttempts?: number;
|
|
366
|
+
/** Poll getStats() for connection quality metrics (default false). */
|
|
367
|
+
enableMetrics?: boolean;
|
|
368
|
+
/** Encrypt relay (WebSocket) messages with ECDH + AES-256-GCM (default true). */
|
|
369
|
+
encryptRelay?: boolean;
|
|
370
|
+
}
|
|
371
|
+
interface CallOption {
|
|
372
|
+
/**
|
|
373
|
+
* Metadata associated with the connection, passed in by whoever initiated the connection.
|
|
374
|
+
*
|
|
375
|
+
* Can be accessed with {@apilink MediaConnection.metadata}.
|
|
376
|
+
* Can be any serializable type.
|
|
377
|
+
*/
|
|
378
|
+
metadata?: any;
|
|
379
|
+
/**
|
|
380
|
+
* Function which runs before create offer to modify sdp offer message.
|
|
381
|
+
*/
|
|
382
|
+
sdpTransform?: (sdp: string) => string;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** Options accepted by {@link HybridConnection.send}. */
|
|
386
|
+
interface HybridSendOptions {
|
|
387
|
+
/** When set, the message is tagged with this topic string. */
|
|
388
|
+
readonly topic?: string;
|
|
389
|
+
}
|
|
390
|
+
interface HybridConnectionEvents {
|
|
391
|
+
open: () => void;
|
|
392
|
+
data: (data: unknown) => void;
|
|
393
|
+
close: () => void;
|
|
394
|
+
error: (err: Error) => void;
|
|
395
|
+
transportChanged: (mode: TransportMode) => void;
|
|
396
|
+
qualityChanged: (quality: ConnectionQuality) => void;
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Wraps both a WebRTC DataConnection and a WebSocket relay,
|
|
400
|
+
* switching transparently between them.
|
|
401
|
+
*
|
|
402
|
+
* - Attempts WebRTC first; falls back to WS relay on ICE timeout.
|
|
403
|
+
* - Periodically retries WebRTC upgrade when on relay.
|
|
404
|
+
* - Emits `transportChanged` whenever the active transport switches.
|
|
405
|
+
*/
|
|
406
|
+
declare class HybridConnection extends EventEmitter<HybridConnectionEvents> {
|
|
407
|
+
readonly peer: string;
|
|
408
|
+
private readonly _provider;
|
|
409
|
+
private readonly _options;
|
|
410
|
+
private _dataConnection;
|
|
411
|
+
private _mode;
|
|
412
|
+
private _iceTimer;
|
|
413
|
+
private _upgradeTimer;
|
|
414
|
+
private _upgradeAttempts;
|
|
415
|
+
private _open;
|
|
416
|
+
private _closed;
|
|
417
|
+
private readonly _ackManager;
|
|
418
|
+
private readonly _topics;
|
|
419
|
+
private readonly _encryption;
|
|
420
|
+
private readonly _encryptRelay;
|
|
421
|
+
private _keyExchangeSent;
|
|
422
|
+
private _pendingRelayQueue;
|
|
423
|
+
private _expectedSeq;
|
|
424
|
+
private _reorderBuffer;
|
|
425
|
+
private readonly _reorderTimeout;
|
|
426
|
+
private _reorderTimers;
|
|
427
|
+
constructor(peer: string, provider: Dendri, options?: HybridConnectionOption);
|
|
428
|
+
get mode(): TransportMode;
|
|
429
|
+
get open(): boolean;
|
|
430
|
+
/** Start connection: try WebRTC first, fall back to WS relay on timeout. */
|
|
431
|
+
start(): void;
|
|
432
|
+
/** Send data through the best available transport, optionally tagged with a topic. */
|
|
433
|
+
send(data: unknown, options?: HybridSendOptions): void;
|
|
434
|
+
/**
|
|
435
|
+
* Send data with delivery confirmation (at-least-once guarantee).
|
|
436
|
+
* Resolves when the remote peer ACKs, rejects on timeout.
|
|
437
|
+
*/
|
|
438
|
+
sendWithAck(data: unknown, timeoutMs?: number): Promise<void>;
|
|
439
|
+
/**
|
|
440
|
+
* @internal
|
|
441
|
+
* Expose the AckManager so the Dendri instance can route incoming ACKs.
|
|
442
|
+
*/
|
|
443
|
+
get ackManager(): AckManager;
|
|
444
|
+
/**
|
|
445
|
+
* Subscribe to messages on a specific topic.
|
|
446
|
+
* Returns an unsubscribe function.
|
|
447
|
+
*/
|
|
448
|
+
subscribe(topic: string, handler: (data: unknown, peerId: string) => void): () => void;
|
|
449
|
+
/**
|
|
450
|
+
* Subscribe to all incoming data regardless of topic.
|
|
451
|
+
* Returns an unsubscribe function.
|
|
452
|
+
*/
|
|
453
|
+
onData(handler: (data: unknown, peerId: string) => void): () => void;
|
|
454
|
+
/**
|
|
455
|
+
* Handle a DATA message received via WebSocket relay.
|
|
456
|
+
* Called by Dendri._handleMessage when routing relay data.
|
|
457
|
+
*
|
|
458
|
+
* @param payload - The message payload.
|
|
459
|
+
* @param seq - Optional server-assigned sequence number for ordering.
|
|
460
|
+
*/
|
|
461
|
+
handleRelayData(payload: unknown, seq?: number): void;
|
|
462
|
+
/** Close connection and clean up all resources. */
|
|
463
|
+
close(): void;
|
|
464
|
+
/**
|
|
465
|
+
* Initiate the ECDH key exchange by generating a key pair and sending
|
|
466
|
+
* the public key to the remote peer via signaling.
|
|
467
|
+
*/
|
|
468
|
+
initiateKeyExchange(): void;
|
|
469
|
+
/**
|
|
470
|
+
* Handle an incoming KEY_EXCHANGE message from the remote peer.
|
|
471
|
+
* Derives the shared secret and sends our public key back if we haven't yet.
|
|
472
|
+
*/
|
|
473
|
+
handleKeyExchange(payload: {
|
|
474
|
+
publicKey: JsonWebKey;
|
|
475
|
+
}): void;
|
|
476
|
+
/**
|
|
477
|
+
* Send a payload via WebSocket relay, encrypting it if encryption is ready.
|
|
478
|
+
* If encryption is enabled but not yet ready, the message is queued.
|
|
479
|
+
*/
|
|
480
|
+
private _sendRelay;
|
|
481
|
+
/** Flush messages that were queued while waiting for key exchange. */
|
|
482
|
+
private _flushPendingRelayQueue;
|
|
483
|
+
/**
|
|
484
|
+
* Enforce ordered delivery for sequenced relay messages.
|
|
485
|
+
* Messages without a seq or when not in relay mode are delivered immediately.
|
|
486
|
+
*/
|
|
487
|
+
private _deliverInOrder;
|
|
488
|
+
/** Flush consecutive messages from the reorder buffer. */
|
|
489
|
+
private _flushReorderBuffer;
|
|
490
|
+
/**
|
|
491
|
+
* Force-flush the reorder buffer when a timeout fires.
|
|
492
|
+
* Skips past any gaps by advancing _expectedSeq to the lowest buffered
|
|
493
|
+
* sequence, then delivers all consecutive messages from there.
|
|
494
|
+
*/
|
|
495
|
+
private _forceFlushReorderBuffer;
|
|
496
|
+
/**
|
|
497
|
+
* Route an incoming payload through the TopicManager (if applicable)
|
|
498
|
+
* and emit the generic `data` event.
|
|
499
|
+
*
|
|
500
|
+
* If the payload is a topic envelope, the inner data is extracted and
|
|
501
|
+
* dispatched to topic-specific handlers. The raw `data` event always fires
|
|
502
|
+
* with the unwrapped payload so legacy listeners still work.
|
|
503
|
+
*/
|
|
504
|
+
private _dispatchIncoming;
|
|
505
|
+
/** Attempt a WebRTC connection with ICE timeout. */
|
|
506
|
+
private _attemptWebRTC;
|
|
507
|
+
/** Switch to WebSocket relay and optionally schedule periodic upgrade attempts. */
|
|
508
|
+
private _fallbackToRelay;
|
|
509
|
+
/** Update the transport mode and emit if changed. */
|
|
510
|
+
private _setMode;
|
|
511
|
+
private _clearIceTimer;
|
|
512
|
+
private _clearUpgradeTimer;
|
|
513
|
+
/**
|
|
514
|
+
* Periodically retry WebRTC when on relay.
|
|
515
|
+
* Respects `autoUpgrade`, `upgradeInterval`, and `maxUpgradeAttempts` options.
|
|
516
|
+
*/
|
|
517
|
+
private _scheduleUpgrade;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
declare enum LogLevel {
|
|
521
|
+
/**
|
|
522
|
+
* Prints no logs.
|
|
523
|
+
*/
|
|
524
|
+
Disabled = 0,
|
|
525
|
+
/**
|
|
526
|
+
* Prints only errors.
|
|
527
|
+
*/
|
|
528
|
+
Errors = 1,
|
|
529
|
+
/**
|
|
530
|
+
* Prints errors and warnings.
|
|
531
|
+
*/
|
|
532
|
+
Warnings = 2,
|
|
533
|
+
/**
|
|
534
|
+
* Prints all logs.
|
|
535
|
+
*/
|
|
536
|
+
All = 3
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
interface MediaConnectionEvents extends BaseConnectionEvents<never> {
|
|
540
|
+
/**
|
|
541
|
+
* Emitted when a connection to the Dendri server is established.
|
|
542
|
+
*
|
|
543
|
+
* ```ts
|
|
544
|
+
* mediaConnection.on('stream', (stream) => { ... });
|
|
545
|
+
* ```
|
|
546
|
+
*/
|
|
547
|
+
stream: (stream: MediaStream) => void;
|
|
548
|
+
/**
|
|
549
|
+
* Emitted when the auxiliary data channel is established.
|
|
550
|
+
* After this event, hanging up will close the connection cleanly on the remote peer.
|
|
551
|
+
* @beta
|
|
552
|
+
*/
|
|
553
|
+
willCloseOnRemote: () => void;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Wraps WebRTC's media streams.
|
|
557
|
+
* To get one, use {@apilink Dendri.call} or listen for the {@apilink DendriEvents | `call`} event.
|
|
558
|
+
*/
|
|
559
|
+
declare class MediaConnection extends BaseConnection<MediaConnectionEvents> {
|
|
560
|
+
private static readonly ID_PREFIX;
|
|
561
|
+
readonly label: string;
|
|
562
|
+
private _negotiator;
|
|
563
|
+
private _localStream;
|
|
564
|
+
private _remoteStream;
|
|
565
|
+
/**
|
|
566
|
+
* For media connections, this is always 'media'.
|
|
567
|
+
*/
|
|
568
|
+
get type(): ConnectionType;
|
|
569
|
+
get localStream(): MediaStream | null | undefined;
|
|
570
|
+
get remoteStream(): MediaStream | null;
|
|
571
|
+
constructor(peerId: string, provider: Dendri, options: any);
|
|
572
|
+
/** Called by the Negotiator when the DataChannel is ready. */
|
|
573
|
+
_initializeDataChannel(dc: RTCDataChannel): void;
|
|
574
|
+
addStream(remoteStream: MediaStream): void;
|
|
575
|
+
/**
|
|
576
|
+
* @internal
|
|
577
|
+
*/
|
|
578
|
+
handleMessage(message: ServerMessage): void;
|
|
579
|
+
/**
|
|
580
|
+
* When receiving a {@apilink DendriEvents | `call`} event on a peer, you can call
|
|
581
|
+
* `answer` on the media connection provided by the callback to accept the call
|
|
582
|
+
* and optionally send your own media stream.
|
|
583
|
+
|
|
584
|
+
*
|
|
585
|
+
* @param stream A WebRTC media stream.
|
|
586
|
+
* @param options
|
|
587
|
+
* @returns
|
|
588
|
+
*/
|
|
589
|
+
answer(stream?: MediaStream, options?: AnswerOption): void;
|
|
590
|
+
/**
|
|
591
|
+
* Exposed functionality for users.
|
|
592
|
+
*/
|
|
593
|
+
/**
|
|
594
|
+
* Closes the media connection.
|
|
595
|
+
*/
|
|
596
|
+
close(): void;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Events emitted by all signaling transports.
|
|
601
|
+
*/
|
|
602
|
+
interface TransportEvents {
|
|
603
|
+
[SocketEventType.Message]: (data: any) => void;
|
|
604
|
+
[SocketEventType.Disconnected]: () => void;
|
|
605
|
+
[SocketEventType.Error]: (error: string) => void;
|
|
606
|
+
[SocketEventType.Reconnected]: () => void;
|
|
607
|
+
[SocketEventType.ReconnectAttempt]: (attempt: number) => void;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Abstract signaling transport. WebSocket, SSE+POST, and Long Polling
|
|
611
|
+
* all implement this interface so the Dendri class is transport-agnostic.
|
|
612
|
+
*/
|
|
613
|
+
declare abstract class SignalingTransport extends EventEmitter<TransportEvents> {
|
|
614
|
+
abstract start(id: string, token: string): void;
|
|
615
|
+
abstract send(data: any): void;
|
|
616
|
+
abstract close(): void;
|
|
617
|
+
abstract get reconnectAttempt(): number;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
declare class DendriOptions implements DendriOption {
|
|
621
|
+
/**
|
|
622
|
+
* Prints log messages depending on the debug level passed in.
|
|
623
|
+
*/
|
|
624
|
+
debug?: LogLevel;
|
|
625
|
+
/**
|
|
626
|
+
* Server host for your Dendri signaling server.
|
|
627
|
+
* Also accepts `'/'` to signify relative hostname.
|
|
628
|
+
*/
|
|
629
|
+
host: string;
|
|
630
|
+
/**
|
|
631
|
+
* Server port. Defaults to `443`.
|
|
632
|
+
*/
|
|
633
|
+
port?: number;
|
|
634
|
+
/**
|
|
635
|
+
* The path where your self-hosted Dendri server is running. Defaults to `'/'`
|
|
636
|
+
*/
|
|
637
|
+
path?: string;
|
|
638
|
+
/**
|
|
639
|
+
* API key for the Dendri server.
|
|
640
|
+
* This is not used anymore.
|
|
641
|
+
* @deprecated
|
|
642
|
+
*/
|
|
643
|
+
key?: string;
|
|
644
|
+
token?: string;
|
|
645
|
+
/**
|
|
646
|
+
* Configuration hash passed to RTCPeerConnection.
|
|
647
|
+
* This hash contains any custom ICE/TURN server configuration.
|
|
648
|
+
*
|
|
649
|
+
* Defaults to {@apilink util.defaultConfig}
|
|
650
|
+
*/
|
|
651
|
+
config?: any;
|
|
652
|
+
/**
|
|
653
|
+
* Set to true `true` if you're using TLS.
|
|
654
|
+
* :::danger
|
|
655
|
+
* If possible *always use TLS*
|
|
656
|
+
* :::
|
|
657
|
+
*/
|
|
658
|
+
secure?: boolean;
|
|
659
|
+
pingInterval?: number;
|
|
660
|
+
referrerPolicy?: ReferrerPolicy;
|
|
661
|
+
logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
|
|
662
|
+
serializers?: SerializerMapping;
|
|
663
|
+
/** Auto-fetch TURN credentials from the signaling server's GET /turn endpoint. */
|
|
664
|
+
fetchTurnCredentials?: boolean;
|
|
665
|
+
/** Optional JWT for authenticated connections. */
|
|
666
|
+
jwt?: string;
|
|
667
|
+
/** Enable WebSocket relay fallback when WebRTC is unavailable. */
|
|
668
|
+
enableRelay?: boolean;
|
|
669
|
+
/** Optional function to validate connection metadata before accepting. */
|
|
670
|
+
validateMetadata?: (metadata: unknown) => boolean;
|
|
671
|
+
/** Signaling transport: 'websocket' (default), 'sse', 'polling', or 'auto' (tries WS then SSE then polling) */
|
|
672
|
+
signalingTransport?: "websocket" | "sse" | "polling" | "auto";
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
interface SerializerMapping {
|
|
676
|
+
[key: string]: new (peerId: string, provider: Dendri, options: any) => DataConnection;
|
|
677
|
+
}
|
|
678
|
+
interface DendriEvents {
|
|
679
|
+
/**
|
|
680
|
+
* Emitted when a connection to the Dendri server is established.
|
|
681
|
+
*
|
|
682
|
+
* You may use the peer before this is emitted, but messages to the server will be queued. <code>id</code> is the brokering ID of the peer (which was either provided in the constructor or assigned by the server).<span class='tip'>You should not wait for this event before connecting to other peers if connection speed is important.</span>
|
|
683
|
+
*/
|
|
684
|
+
open: (id: string) => void;
|
|
685
|
+
/**
|
|
686
|
+
* Emitted when a new data connection is established from a remote peer.
|
|
687
|
+
*/
|
|
688
|
+
connection: (dataConnection: DataConnection) => void;
|
|
689
|
+
/**
|
|
690
|
+
* Emitted when a remote peer attempts to call you.
|
|
691
|
+
*/
|
|
692
|
+
call: (mediaConnection: MediaConnection) => void;
|
|
693
|
+
/**
|
|
694
|
+
* Emitted when the peer is destroyed and can no longer accept or create any new connections.
|
|
695
|
+
*/
|
|
696
|
+
close: () => void;
|
|
697
|
+
/**
|
|
698
|
+
* Emitted when the peer is disconnected from the signalling server
|
|
699
|
+
*/
|
|
700
|
+
disconnected: (currentId: string) => void;
|
|
701
|
+
/**
|
|
702
|
+
* Errors on the peer are almost always fatal and will destroy the peer.
|
|
703
|
+
*
|
|
704
|
+
* Errors from the underlying socket and PeerConnections are forwarded here.
|
|
705
|
+
*/
|
|
706
|
+
error: (error: DendriError<`${DendriErrorType}`>) => void;
|
|
707
|
+
/**
|
|
708
|
+
* Emitted when the server sends a ROOM_PEERS message with the current
|
|
709
|
+
* list of peers in a room.
|
|
710
|
+
*/
|
|
711
|
+
roomPeers: (room: string, peers: string[]) => void;
|
|
712
|
+
/**
|
|
713
|
+
* Emitted when the connection state machine transitions to a new state.
|
|
714
|
+
*/
|
|
715
|
+
connectionStateChanged: (state: ConnectionState, previousState: ConnectionState) => void;
|
|
716
|
+
/**
|
|
717
|
+
* Emitted when a PRESENCE_UPDATE message is received from the server.
|
|
718
|
+
*/
|
|
719
|
+
presenceUpdate: (peerId: string, room: string, data: unknown) => void;
|
|
720
|
+
/**
|
|
721
|
+
* Emitted when the underlying signaling socket finishes a successful
|
|
722
|
+
* reconnection. Consumers (e.g. Room) should re-send server-side
|
|
723
|
+
* state (joinRoom, presence) so it survives server-side state loss
|
|
724
|
+
* such as a Durable Object hibernation + wake-up cycle.
|
|
725
|
+
*/
|
|
726
|
+
reconnected: () => void;
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* A peer who can initiate connections with other peers.
|
|
730
|
+
*/
|
|
731
|
+
declare class Dendri extends EventEmitterWithError<DendriErrorType, DendriEvents> {
|
|
732
|
+
private static readonly DEFAULT_KEY;
|
|
733
|
+
protected readonly _serializers: SerializerMapping;
|
|
734
|
+
private readonly _options;
|
|
735
|
+
private readonly _api;
|
|
736
|
+
private readonly _socket;
|
|
737
|
+
private _id;
|
|
738
|
+
private _lastServerId;
|
|
739
|
+
private _connectionState;
|
|
740
|
+
/** Number of consecutive reconnection failures that triggers the Suspended state. */
|
|
741
|
+
private static readonly SUSPEND_THRESHOLD;
|
|
742
|
+
private readonly _connections;
|
|
743
|
+
private readonly _lostMessages;
|
|
744
|
+
private readonly _lostMessageGeneration;
|
|
745
|
+
private readonly _hybridConnections;
|
|
746
|
+
/**
|
|
747
|
+
* The brokering ID of this peer
|
|
748
|
+
*
|
|
749
|
+
* If no ID was specified in {@apilink Dendri | the constructor},
|
|
750
|
+
* this will be `undefined` until the {@apilink DendriEvents | `open`} event is emitted.
|
|
751
|
+
*/
|
|
752
|
+
get id(): string | null;
|
|
753
|
+
get options(): DendriOptions;
|
|
754
|
+
/**
|
|
755
|
+
* The current connection state of this peer.
|
|
756
|
+
*/
|
|
757
|
+
get connectionState(): ConnectionState;
|
|
758
|
+
get open(): boolean;
|
|
759
|
+
/**
|
|
760
|
+
* @internal
|
|
761
|
+
*/
|
|
762
|
+
get socket(): SignalingTransport;
|
|
763
|
+
/**
|
|
764
|
+
* A hash of all connections associated with this peer, keyed by the remote peer's ID.
|
|
765
|
+
* @deprecated
|
|
766
|
+
* Return type will change from Object to Map<string,[]>
|
|
767
|
+
*/
|
|
768
|
+
get connections(): Object;
|
|
769
|
+
/**
|
|
770
|
+
* true if this peer and all of its connections can no longer be used.
|
|
771
|
+
*/
|
|
772
|
+
get destroyed(): boolean;
|
|
773
|
+
/**
|
|
774
|
+
* false if there is an active connection to the Dendri server.
|
|
775
|
+
*
|
|
776
|
+
* Also returns true for terminal states (Closed/Failed) to preserve
|
|
777
|
+
* backward compatibility: the original boolean was set during
|
|
778
|
+
* disconnect() and never cleared by destroy().
|
|
779
|
+
*/
|
|
780
|
+
get disconnected(): boolean;
|
|
781
|
+
/**
|
|
782
|
+
* A peer can connect to other peers and listen for connections.
|
|
783
|
+
*/
|
|
784
|
+
constructor(options: DendriOptions);
|
|
785
|
+
/**
|
|
786
|
+
* A peer can connect to other peers and listen for connections.
|
|
787
|
+
* @param id Other peers can connect to this peer using the provided ID.
|
|
788
|
+
* If no ID is given, one will be generated by the brokering server.
|
|
789
|
+
* The ID must start and end with an alphanumeric character (lower or upper case character or a digit). In the middle of the ID spaces, dashes (-) and underscores (_) are allowed. Use {@apilink DendriOptions.metadata } to send identifying information.
|
|
790
|
+
* @param options for specifying details about Dendri server
|
|
791
|
+
*/
|
|
792
|
+
constructor(id: string, options?: DendriOptions);
|
|
793
|
+
/**
|
|
794
|
+
* Transition to a new connection state. Invalid transitions are logged and ignored.
|
|
795
|
+
*/
|
|
796
|
+
private _setState;
|
|
797
|
+
private static _isValidTransition;
|
|
798
|
+
private _createServerConnection;
|
|
799
|
+
/** Initialize a connection with the server. */
|
|
800
|
+
private _initialize;
|
|
801
|
+
/** Handles messages from the server. */
|
|
802
|
+
private _handleMessage;
|
|
803
|
+
private static readonly MAX_LOST_MESSAGES;
|
|
804
|
+
private static readonly LOST_MESSAGE_TTL;
|
|
805
|
+
/** Stores messages without a set up connection, to be claimed later. */
|
|
806
|
+
private _storeMessage;
|
|
807
|
+
/**
|
|
808
|
+
* Retrieve messages from lost message store
|
|
809
|
+
* @internal
|
|
810
|
+
*/
|
|
811
|
+
_getMessages(connectionId: string): ServerMessage[];
|
|
812
|
+
/**
|
|
813
|
+
* Connects to the remote peer specified by id and returns a data connection.
|
|
814
|
+
* @param peer The brokering ID of the remote peer (their {@apilink Dendri.id}).
|
|
815
|
+
* @param options for specifying details about the data connection
|
|
816
|
+
*/
|
|
817
|
+
connect(peer: string, options?: DendriConnectOption): DataConnection | undefined;
|
|
818
|
+
/**
|
|
819
|
+
* Calls the remote peer specified by id and returns a media connection.
|
|
820
|
+
* @param peer The brokering ID of the remote peer (their peer.id).
|
|
821
|
+
* @param stream The caller's media stream
|
|
822
|
+
* @param options Metadata associated with the connection, passed in by whoever initiated the connection.
|
|
823
|
+
*/
|
|
824
|
+
call(peer: string, stream: MediaStream, options?: CallOption): MediaConnection | undefined;
|
|
825
|
+
/**
|
|
826
|
+
* Create a hybrid connection that uses WebRTC when possible
|
|
827
|
+
* and transparently falls back to WebSocket relay.
|
|
828
|
+
*
|
|
829
|
+
* @param peer The brokering ID of the remote peer.
|
|
830
|
+
* @param options HybridConnection-specific options (ICE timeout, upgrade behaviour, etc.)
|
|
831
|
+
*/
|
|
832
|
+
connectHybrid(peer: string, options?: HybridConnectionOption): HybridConnection | undefined;
|
|
833
|
+
/**
|
|
834
|
+
* Join a room on the signaling server.
|
|
835
|
+
* The server will respond with a ROOM_PEERS message listing current members.
|
|
836
|
+
*/
|
|
837
|
+
joinRoom(roomName: string): void;
|
|
838
|
+
/**
|
|
839
|
+
* Leave a room on the signaling server.
|
|
840
|
+
*/
|
|
841
|
+
leaveRoom(roomName: string): void;
|
|
842
|
+
/** Auto-fetch TURN credentials when the option is enabled. */
|
|
843
|
+
private _fetchTurnIfEnabled;
|
|
844
|
+
/** Add a data/media connection to this peer. */
|
|
845
|
+
private _addConnection;
|
|
846
|
+
_removeConnection(connection: DataConnection | MediaConnection): void;
|
|
847
|
+
/** Retrieve a data/media connection for this peer. */
|
|
848
|
+
getConnection(peerId: string, connectionId: string): null | DataConnection | MediaConnection;
|
|
849
|
+
/** Error types that indicate an unrecoverable problem. */
|
|
850
|
+
private static readonly FATAL_ERROR_TYPES;
|
|
851
|
+
private _delayedAbort;
|
|
852
|
+
/**
|
|
853
|
+
* Emits an error message and destroys the Dendri instance.
|
|
854
|
+
* The Dendri instance is not destroyed if it's in a disconnected state, in which case
|
|
855
|
+
* it retains its disconnected state and its existing connections.
|
|
856
|
+
*/
|
|
857
|
+
private _abort;
|
|
858
|
+
/**
|
|
859
|
+
* Destroys the Dendri instance: closes all active connections as well as the connection
|
|
860
|
+
* to the server.
|
|
861
|
+
*
|
|
862
|
+
* :::caution
|
|
863
|
+
* This cannot be undone; the respective peer object will no longer be able
|
|
864
|
+
* to create or receive any connections, its ID will be forfeited on the server,
|
|
865
|
+
* and all of its data and media connections will be closed.
|
|
866
|
+
* :::
|
|
867
|
+
*/
|
|
868
|
+
destroy(): void;
|
|
869
|
+
/** Internal destroy that transitions to the given terminal state. */
|
|
870
|
+
private _destroyWithState;
|
|
871
|
+
/** Disconnects every connection on this peer. */
|
|
872
|
+
private _cleanup;
|
|
873
|
+
/** Closes all connections to this peer. */
|
|
874
|
+
private _cleanupPeer;
|
|
875
|
+
/**
|
|
876
|
+
* Disconnects the Dendri instance's connection to the Dendri server. Does not close any
|
|
877
|
+
* active connections.
|
|
878
|
+
* Warning: The peer can no longer create or accept connections after being
|
|
879
|
+
* disconnected. It also cannot reconnect to the server.
|
|
880
|
+
*/
|
|
881
|
+
disconnect(): void;
|
|
882
|
+
/** Attempts to reconnect with the same ID.
|
|
883
|
+
*
|
|
884
|
+
* Only {@apilink Dendri.disconnect | disconnected peers} can be reconnected.
|
|
885
|
+
* Destroyed peers cannot be reconnected.
|
|
886
|
+
* If the connection fails (as an example, if the peer's old ID is now taken),
|
|
887
|
+
* the peer's existing connections will not close, but any associated errors events will fire.
|
|
888
|
+
*/
|
|
889
|
+
reconnect(): void;
|
|
890
|
+
/**
|
|
891
|
+
* Get a list of available peer IDs. If you're running your own server, you'll
|
|
892
|
+
* want to set allow_discovery: true in the Dendri server options.
|
|
893
|
+
*/
|
|
894
|
+
listAllPeers(cb?: (_: any[]) => void): void;
|
|
895
|
+
/** Returns a snapshot of the current connection state for debugging. */
|
|
896
|
+
diagnostics(): {
|
|
897
|
+
peerId: string | null;
|
|
898
|
+
connectionState: ConnectionState;
|
|
899
|
+
serverConnected: boolean;
|
|
900
|
+
connections: Array<{
|
|
901
|
+
peerId: string;
|
|
902
|
+
type: string;
|
|
903
|
+
open: boolean;
|
|
904
|
+
}>;
|
|
905
|
+
hybridConnections: Array<{
|
|
906
|
+
peerId: string;
|
|
907
|
+
mode: TransportMode;
|
|
908
|
+
open: boolean;
|
|
909
|
+
}>;
|
|
910
|
+
socket: {
|
|
911
|
+
connected: boolean;
|
|
912
|
+
autoReconnect: boolean;
|
|
913
|
+
reconnectAttempt: number;
|
|
914
|
+
lastSeq: number;
|
|
915
|
+
};
|
|
916
|
+
pendingMessages: number;
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
interface PresenceEvents<T> {
|
|
921
|
+
update: (peerId: string, data: T) => void;
|
|
922
|
+
join: (peerId: string, data: T) => void;
|
|
923
|
+
leave: (peerId: string) => void;
|
|
924
|
+
sync: (presences: ReadonlyMap<string, T>) => void;
|
|
925
|
+
}
|
|
926
|
+
/**
|
|
927
|
+
* Manages per-peer presence state for a room.
|
|
928
|
+
*
|
|
929
|
+
* Each peer can set arbitrary typed state that is broadcast to all room
|
|
930
|
+
* members. Incoming updates are stored and exposed via getters; lifecycle
|
|
931
|
+
* events (join, update, leave, sync) are emitted for subscribers.
|
|
932
|
+
*/
|
|
933
|
+
declare class PresenceManager<T = Record<string, unknown>> extends EventEmitter<PresenceEvents<T>> {
|
|
934
|
+
private readonly _presences;
|
|
935
|
+
private _myPresence;
|
|
936
|
+
private _myPeerId;
|
|
937
|
+
/** Set (or merge) my own presence data. */
|
|
938
|
+
setMyPresence(data: Partial<T>): void;
|
|
939
|
+
/** Get a specific peer's presence. */
|
|
940
|
+
getPresence(peerId: string): T | undefined;
|
|
941
|
+
/** Get all presences excluding this peer. */
|
|
942
|
+
getOthers(): Map<string, T>;
|
|
943
|
+
/** Get all presences (including self). */
|
|
944
|
+
getAll(): Map<string, T>;
|
|
945
|
+
/** Handle an incoming presence update from a remote peer. */
|
|
946
|
+
handleUpdate(peerId: string, data: T): void;
|
|
947
|
+
/** Handle a peer leaving — remove their presence. */
|
|
948
|
+
handleLeave(peerId: string): void;
|
|
949
|
+
/** Set this peer's ID (called by Room on join). */
|
|
950
|
+
setMyPeerId(id: string): void;
|
|
951
|
+
/** Clear all presences and local state. */
|
|
952
|
+
clear(): void;
|
|
953
|
+
/** The local peer's current presence, or null if not yet set. */
|
|
954
|
+
get myPresence(): T | null;
|
|
955
|
+
/** Number of tracked presences (including self if set). */
|
|
956
|
+
get size(): number;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
/**
|
|
960
|
+
* RPC (Remote Procedure Call) layer for request-response patterns over P2P connections.
|
|
961
|
+
*
|
|
962
|
+
* Inspired by LiveKit's `performRpc`/`registerRpcMethod`, this module enables
|
|
963
|
+
* peers to expose named methods that other peers can call and await results.
|
|
964
|
+
*
|
|
965
|
+
* Usage (via Room):
|
|
966
|
+
* // Responder
|
|
967
|
+
* room.registerRpcMethod("greet", (payload) => `Hello, ${payload}!`);
|
|
968
|
+
*
|
|
969
|
+
* // Caller
|
|
970
|
+
* const result = await room.performRpc("greet", "world");
|
|
971
|
+
* // result === "Hello, world!"
|
|
972
|
+
*/
|
|
973
|
+
interface RpcRequest {
|
|
974
|
+
readonly __rpc: true;
|
|
975
|
+
readonly id: string;
|
|
976
|
+
readonly method: string;
|
|
977
|
+
readonly payload: unknown;
|
|
978
|
+
readonly sender: string;
|
|
979
|
+
}
|
|
980
|
+
interface RpcResponse {
|
|
981
|
+
readonly __rpc_response: true;
|
|
982
|
+
readonly id: string;
|
|
983
|
+
readonly result?: unknown;
|
|
984
|
+
readonly error?: {
|
|
985
|
+
readonly code: number;
|
|
986
|
+
readonly message: string;
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
type RpcHandler = (payload: unknown, sender: string) => unknown | Promise<unknown>;
|
|
990
|
+
/** Standard RPC error codes (inspired by LiveKit). */
|
|
991
|
+
declare enum RpcErrorCode {
|
|
992
|
+
APPLICATION_ERROR = 1500,
|
|
993
|
+
CONNECTION_TIMEOUT = 1501,
|
|
994
|
+
RESPONSE_TIMEOUT = 1502,
|
|
995
|
+
RECIPIENT_DISCONNECTED = 1503,
|
|
996
|
+
RESPONSE_PAYLOAD_TOO_LARGE = 1504,
|
|
997
|
+
METHOD_NOT_FOUND = 1505
|
|
998
|
+
}
|
|
999
|
+
declare class RpcError extends Error {
|
|
1000
|
+
readonly code: number;
|
|
1001
|
+
constructor(code: number, message: string);
|
|
1002
|
+
}
|
|
1003
|
+
declare class RpcManager {
|
|
1004
|
+
private readonly _handlers;
|
|
1005
|
+
private readonly _pending;
|
|
1006
|
+
private _counter;
|
|
1007
|
+
/** Register a method handler. Returns an unregister function. */
|
|
1008
|
+
registerMethod(name: string, handler: RpcHandler): () => void;
|
|
1009
|
+
/** Perform an RPC call -- returns a promise that resolves with the response. */
|
|
1010
|
+
performRpc(method: string, payload: unknown, sendFn: (data: RpcRequest) => void, options?: {
|
|
1011
|
+
readonly timeout?: number;
|
|
1012
|
+
}): Promise<unknown>;
|
|
1013
|
+
/** Handle an incoming RPC request -- execute handler and return response. */
|
|
1014
|
+
handleRequest(request: RpcRequest): Promise<RpcResponse>;
|
|
1015
|
+
/** Handle an incoming RPC response -- resolve the pending promise. */
|
|
1016
|
+
handleResponse(response: RpcResponse): boolean;
|
|
1017
|
+
/** Clear all pending RPCs, rejecting them with CONNECTION_TIMEOUT. */
|
|
1018
|
+
clear(): void;
|
|
1019
|
+
/** Number of RPCs currently awaiting a response. */
|
|
1020
|
+
get pendingCount(): number;
|
|
1021
|
+
/** Names of all registered RPC methods. */
|
|
1022
|
+
get registeredMethods(): string[];
|
|
1023
|
+
}
|
|
1024
|
+
/** Type guard for RPC request messages. */
|
|
1025
|
+
declare function isRpcRequest(data: unknown): data is RpcRequest;
|
|
1026
|
+
/** Type guard for RPC response messages. */
|
|
1027
|
+
declare function isRpcResponse(data: unknown): data is RpcResponse;
|
|
1028
|
+
|
|
1029
|
+
/**
|
|
1030
|
+
* High-level Room abstraction for star-topology P2P rooms with host migration.
|
|
1031
|
+
*
|
|
1032
|
+
* Usage pattern:
|
|
1033
|
+
* const room = new Room("my-room");
|
|
1034
|
+
* room.join({ host: "localhost", port: 9000, secure: false, path: "/" });
|
|
1035
|
+
* room.on("joined", (peerId, isHost) => { ... });
|
|
1036
|
+
* room.on("data", (peerId, data) => { ... });
|
|
1037
|
+
* room.broadcast({ cursor: [10, 20] });
|
|
1038
|
+
*
|
|
1039
|
+
* The Room class is opt-in. Existing Dendri usage patterns are unaffected.
|
|
1040
|
+
*/
|
|
1041
|
+
|
|
1042
|
+
interface RoomEvents {
|
|
1043
|
+
/** Fired when this peer has successfully joined the room. */
|
|
1044
|
+
joined: (peerId: string, isHost: boolean) => void;
|
|
1045
|
+
/** Fired when a remote peer joins the room. */
|
|
1046
|
+
peerJoined: (peerId: string) => void;
|
|
1047
|
+
/** Fired when a remote peer leaves the room. */
|
|
1048
|
+
peerLeft: (peerId: string) => void;
|
|
1049
|
+
/** Fired when data is received from any peer. */
|
|
1050
|
+
data: (peerId: string, data: unknown) => void;
|
|
1051
|
+
/** Fired when the host changes (either via initial join or migration). */
|
|
1052
|
+
hostChanged: (newHostId: string) => void;
|
|
1053
|
+
/** Fired on errors. */
|
|
1054
|
+
error: (err: Error) => void;
|
|
1055
|
+
}
|
|
1056
|
+
interface RoomOptions {
|
|
1057
|
+
/** Milliseconds to wait for a new host to become reachable (default 10000). */
|
|
1058
|
+
readonly migrationTimeout?: number;
|
|
1059
|
+
}
|
|
1060
|
+
/**
|
|
1061
|
+
* A Room manages the lifecycle of a star-topology P2P room:
|
|
1062
|
+
* - First peer to join becomes the host (claims the room ID as its peer ID).
|
|
1063
|
+
* - Subsequent peers connect to the host.
|
|
1064
|
+
* - If the host disconnects, the remaining peer with the lowest alphabetical ID
|
|
1065
|
+
* takes over as host (deterministic election).
|
|
1066
|
+
*/
|
|
1067
|
+
declare class Room extends EventEmitter<RoomEvents> {
|
|
1068
|
+
private _peer;
|
|
1069
|
+
private _isHost;
|
|
1070
|
+
private _hostId;
|
|
1071
|
+
private readonly _connections;
|
|
1072
|
+
private readonly _knownPeers;
|
|
1073
|
+
private readonly _topics;
|
|
1074
|
+
private readonly _ackManager;
|
|
1075
|
+
private readonly _rpc;
|
|
1076
|
+
private readonly _presence;
|
|
1077
|
+
private readonly _roomId;
|
|
1078
|
+
private readonly _migrationTimeout;
|
|
1079
|
+
private _dendriOptions;
|
|
1080
|
+
private _DendriCtor;
|
|
1081
|
+
private _migrationTimer;
|
|
1082
|
+
private _roomJoinInterval;
|
|
1083
|
+
private _joined;
|
|
1084
|
+
constructor(roomId: string, options?: RoomOptions);
|
|
1085
|
+
/** Whether this peer is the current host. */
|
|
1086
|
+
get isHost(): boolean;
|
|
1087
|
+
/** The peer ID of the current host, or null if not yet joined. */
|
|
1088
|
+
get hostId(): string | null;
|
|
1089
|
+
/** All known peer IDs in the room (excluding self). */
|
|
1090
|
+
get peers(): string[];
|
|
1091
|
+
/** Number of known peers (excluding self). */
|
|
1092
|
+
get peerCount(): number;
|
|
1093
|
+
/** The local peer's ID, or null if not joined. */
|
|
1094
|
+
get peerId(): string | null;
|
|
1095
|
+
/**
|
|
1096
|
+
* Join the room. Tries to become host first (claims roomId as peer ID).
|
|
1097
|
+
* If the ID is already taken, joins as a client.
|
|
1098
|
+
*
|
|
1099
|
+
* @param DendriCtor - The Dendri constructor to use for creating peer instances.
|
|
1100
|
+
* @param dendriOptions - Options forwarded to the Dendri constructor.
|
|
1101
|
+
*/
|
|
1102
|
+
join(DendriCtor: new (id: string, opts?: DendriOptions) => Dendri, dendriOptions: DendriOptions): void;
|
|
1103
|
+
/**
|
|
1104
|
+
* Send data to all peers in the room, optionally tagged with a topic.
|
|
1105
|
+
* Tries WebRTC first; falls back to signaling server DATA relay when
|
|
1106
|
+
* no WebRTC connections are open (e.g. behind a VPN).
|
|
1107
|
+
*/
|
|
1108
|
+
broadcast(data: unknown, options?: {
|
|
1109
|
+
readonly topic?: string;
|
|
1110
|
+
}): void;
|
|
1111
|
+
/**
|
|
1112
|
+
* Broadcast a binary payload tagged with a topic. Wraps bytes in a
|
|
1113
|
+
* base64 topic envelope so they survive both WebRTC fanout and the
|
|
1114
|
+
* JSON-only signaling-relay fallback. Receivers see a Uint8Array.
|
|
1115
|
+
*/
|
|
1116
|
+
broadcastBinary(bytes: Uint8Array, options: {
|
|
1117
|
+
readonly topic: string;
|
|
1118
|
+
}): void;
|
|
1119
|
+
/**
|
|
1120
|
+
* Broadcast data with delivery confirmation (at-least-once guarantee).
|
|
1121
|
+
* Resolves when all currently connected peers have ACKed.
|
|
1122
|
+
*/
|
|
1123
|
+
broadcastWithAck(data: unknown, timeoutMs?: number): Promise<void>;
|
|
1124
|
+
/**
|
|
1125
|
+
* Subscribe to messages on a specific topic.
|
|
1126
|
+
* Returns an unsubscribe function.
|
|
1127
|
+
*/
|
|
1128
|
+
subscribe(topic: string, handler: (data: unknown, peerId: string) => void): () => void;
|
|
1129
|
+
/**
|
|
1130
|
+
* Subscribe to all incoming data regardless of topic.
|
|
1131
|
+
* Returns an unsubscribe function.
|
|
1132
|
+
*/
|
|
1133
|
+
onData(handler: (data: unknown, peerId: string) => void): () => void;
|
|
1134
|
+
/**
|
|
1135
|
+
* Register an RPC method that other peers can call.
|
|
1136
|
+
* Returns an unregister function.
|
|
1137
|
+
*/
|
|
1138
|
+
registerRpcMethod(name: string, handler: RpcHandler): () => void;
|
|
1139
|
+
/**
|
|
1140
|
+
* Call an RPC method on a remote peer (or broadcast to all).
|
|
1141
|
+
*
|
|
1142
|
+
* When `peerId` is specified the request is sent only to that peer.
|
|
1143
|
+
* Otherwise it is broadcast to every connected peer (useful when any
|
|
1144
|
+
* peer may handle the method).
|
|
1145
|
+
*/
|
|
1146
|
+
performRpc(method: string, payload: unknown, options?: {
|
|
1147
|
+
readonly timeout?: number;
|
|
1148
|
+
readonly peerId?: string;
|
|
1149
|
+
}): Promise<unknown>;
|
|
1150
|
+
/**
|
|
1151
|
+
* Set my presence data -- automatically broadcast to all room peers
|
|
1152
|
+
* via the signaling server's PRESENCE_UPDATE message.
|
|
1153
|
+
*/
|
|
1154
|
+
setPresence<P extends Record<string, unknown>>(data: P): void;
|
|
1155
|
+
/** Get all other peers' presence data. */
|
|
1156
|
+
getOthers(): Map<string, unknown>;
|
|
1157
|
+
/** Get a specific peer's presence data. */
|
|
1158
|
+
getPresence(peerId: string): unknown;
|
|
1159
|
+
/** Get the presence manager for direct event listening. */
|
|
1160
|
+
get presence(): PresenceManager;
|
|
1161
|
+
/**
|
|
1162
|
+
* Leave the room and clean up all connections.
|
|
1163
|
+
*/
|
|
1164
|
+
leave(): void;
|
|
1165
|
+
private _tryBecomeHost;
|
|
1166
|
+
/**
|
|
1167
|
+
* Wire up all listeners and register with the server-side room.
|
|
1168
|
+
* Used by every entry path (_tryBecomeHost, _joinAsClient, _becomeHost)
|
|
1169
|
+
* so the three can't drift out of sync. This is the single source of
|
|
1170
|
+
* truth for "a peer has just entered the room".
|
|
1171
|
+
*/
|
|
1172
|
+
private _postJoinSetup;
|
|
1173
|
+
private readonly _onSignalingReconnected;
|
|
1174
|
+
private _setupHostListeners;
|
|
1175
|
+
private _joinAsClient;
|
|
1176
|
+
/**
|
|
1177
|
+
* Called when the client detects the host has disconnected.
|
|
1178
|
+
* Elects a new host deterministically (lowest alphabetical peer ID).
|
|
1179
|
+
*/
|
|
1180
|
+
private _handleHostDisconnect;
|
|
1181
|
+
/**
|
|
1182
|
+
* This peer won the election. Tear down the old peer and create a new
|
|
1183
|
+
* one using the room ID so other peers can find us.
|
|
1184
|
+
*/
|
|
1185
|
+
private _becomeHost;
|
|
1186
|
+
/**
|
|
1187
|
+
* Another peer won the election. Wait for it to claim the room ID,
|
|
1188
|
+
* then connect.
|
|
1189
|
+
*/
|
|
1190
|
+
private _connectToNewHost;
|
|
1191
|
+
private _retryConnect;
|
|
1192
|
+
private _handleIncomingData;
|
|
1193
|
+
private _isAckRequest;
|
|
1194
|
+
private _isAckResponse;
|
|
1195
|
+
private _isRoomProtocol;
|
|
1196
|
+
private _handleRoomProtocol;
|
|
1197
|
+
/**
|
|
1198
|
+
* Hook up the underlying Dendri peer's presenceUpdate event so incoming
|
|
1199
|
+
* server-side presence broadcasts are routed to the PresenceManager.
|
|
1200
|
+
*/
|
|
1201
|
+
private _setupPresenceListener;
|
|
1202
|
+
/**
|
|
1203
|
+
* Listen for relay DATA messages from the signaling server (room fan-out).
|
|
1204
|
+
* This is the fallback path when WebRTC connections can't be established
|
|
1205
|
+
* (e.g. behind a VPN or restrictive firewall).
|
|
1206
|
+
*/
|
|
1207
|
+
private _setupRelayDataListener;
|
|
1208
|
+
/**
|
|
1209
|
+
* Listen for ROOM-PEERS messages from the signaling server and merge them
|
|
1210
|
+
* into `_knownPeers`. This ensures the peer count stays accurate even when
|
|
1211
|
+
* WebRTC connections fail (e.g. behind a VPN or restrictive firewall).
|
|
1212
|
+
*
|
|
1213
|
+
* WebRTC-connected peers are always trusted. Peers that only appear in the
|
|
1214
|
+
* signaling list are added as "signaling-only" so the UI reflects the
|
|
1215
|
+
* correct room membership.
|
|
1216
|
+
*/
|
|
1217
|
+
private _setupSignalingPeerTracking;
|
|
1218
|
+
/**
|
|
1219
|
+
* Send data directly to a specific peer.
|
|
1220
|
+
* If host: send via the direct connection.
|
|
1221
|
+
* If client: send via the host connection (host relays).
|
|
1222
|
+
*/
|
|
1223
|
+
private _sendToPeer;
|
|
1224
|
+
private _clearMigrationTimer;
|
|
1225
|
+
/**
|
|
1226
|
+
* Periodically re-send ROOM-JOIN to the signaling server so room
|
|
1227
|
+
* membership survives server-side state loss (e.g. DO hibernation).
|
|
1228
|
+
*/
|
|
1229
|
+
private _startRoomJoinHeartbeat;
|
|
1230
|
+
private _stopRoomJoinHeartbeat;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Framework-agnostic reactive store factory for Dendri Room.
|
|
1235
|
+
*
|
|
1236
|
+
* Works with any UI framework. Provides a subscribe-based reactivity model
|
|
1237
|
+
* (similar to Svelte stores or Zustand) that can be consumed by:
|
|
1238
|
+
*
|
|
1239
|
+
* - **Svelte 5**: Wrap getters in `$derived` or read in `$effect`
|
|
1240
|
+
* - **React**: Use `useSyncExternalStore` with `store.subscribe`
|
|
1241
|
+
* - **Vue**: Use `watchEffect` over store getters
|
|
1242
|
+
* - **Vanilla JS**: Call `store.subscribe` directly
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* ```ts
|
|
1246
|
+
* import { Dendri } from "dendri";
|
|
1247
|
+
* import { createDendriStore } from "dendri/store";
|
|
1248
|
+
*
|
|
1249
|
+
* const store = createDendriStore({
|
|
1250
|
+
* DendriCtor: Dendri,
|
|
1251
|
+
* dendriOptions: { host: "localhost", port: 9000, secure: false, path: "/" },
|
|
1252
|
+
* });
|
|
1253
|
+
*
|
|
1254
|
+
* store.join("my-room");
|
|
1255
|
+
* store.subscribe(() => console.log("peers:", store.peers));
|
|
1256
|
+
* ```
|
|
1257
|
+
*/
|
|
1258
|
+
|
|
1259
|
+
/** Options passed to {@link createDendriStore} in advanced form. */
|
|
1260
|
+
interface DendriStoreOptions {
|
|
1261
|
+
/**
|
|
1262
|
+
* The Dendri constructor to use for creating peer instances. When
|
|
1263
|
+
* omitted the default {@link Dendri} class is used. Overriding is
|
|
1264
|
+
* useful for injecting a mock in tests.
|
|
1265
|
+
*/
|
|
1266
|
+
readonly DendriCtor?: new (id: string, opts?: DendriOptions) => Dendri;
|
|
1267
|
+
/** Options forwarded to the Dendri constructor on each join. */
|
|
1268
|
+
readonly dendriOptions?: DendriOptions;
|
|
1269
|
+
/** Optional Room-level options (e.g. migration timeout). */
|
|
1270
|
+
readonly roomOptions?: RoomOptions;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* `createDendriStore` accepts either the advanced {@link DendriStoreOptions}
|
|
1274
|
+
* shape or a flat {@link DendriOptions} object. Pass an explicit signaling
|
|
1275
|
+
* server host so open-source/self-hosted apps connect to their own server.
|
|
1276
|
+
*/
|
|
1277
|
+
type CreateDendriStoreInput = DendriStoreOptions | DendriOptions;
|
|
1278
|
+
/** Snapshot of the store's reactive state at a given point in time. */
|
|
1279
|
+
interface DendriStoreSnapshot {
|
|
1280
|
+
readonly connectionState: ConnectionState;
|
|
1281
|
+
readonly isHost: boolean;
|
|
1282
|
+
readonly myPeerId: string | null;
|
|
1283
|
+
readonly hostId: string | null;
|
|
1284
|
+
readonly peers: readonly string[];
|
|
1285
|
+
readonly peerCount: number;
|
|
1286
|
+
readonly presences: ReadonlyMap<string, unknown>;
|
|
1287
|
+
}
|
|
1288
|
+
/** Listener callback for store state changes. */
|
|
1289
|
+
type StoreListener = () => void;
|
|
1290
|
+
/**
|
|
1291
|
+
* A framework-agnostic reactive wrapper around a Dendri {@link Room}.
|
|
1292
|
+
*
|
|
1293
|
+
* State is accessed via getters. External frameworks react to changes
|
|
1294
|
+
* by subscribing with {@link DendriStore.subscribe}.
|
|
1295
|
+
*/
|
|
1296
|
+
interface DendriStore {
|
|
1297
|
+
/** Current connection lifecycle state. */
|
|
1298
|
+
readonly connectionState: ConnectionState;
|
|
1299
|
+
/** Whether this peer is the current room host. */
|
|
1300
|
+
readonly isHost: boolean;
|
|
1301
|
+
/** The local peer's ID, or `null` before joining. */
|
|
1302
|
+
readonly myPeerId: string | null;
|
|
1303
|
+
/** The current host's peer ID, or `null` before joining. */
|
|
1304
|
+
readonly hostId: string | null;
|
|
1305
|
+
/** All known remote peer IDs in the room. */
|
|
1306
|
+
readonly peers: readonly string[];
|
|
1307
|
+
/** Number of known remote peers. */
|
|
1308
|
+
readonly peerCount: number;
|
|
1309
|
+
/** Map of remote peer presences. */
|
|
1310
|
+
readonly presences: ReadonlyMap<string, unknown>;
|
|
1311
|
+
/** Join a room by ID. Creates a new Room internally. */
|
|
1312
|
+
join(roomId: string): void;
|
|
1313
|
+
/** Leave the current room and clean up. */
|
|
1314
|
+
leave(): void;
|
|
1315
|
+
/** Broadcast data to all room peers, optionally on a topic. */
|
|
1316
|
+
broadcast(data: unknown, options?: {
|
|
1317
|
+
readonly topic?: string;
|
|
1318
|
+
}): void;
|
|
1319
|
+
/** Broadcast a binary payload tagged with a topic. Receivers see a Uint8Array. */
|
|
1320
|
+
broadcastBinary(bytes: Uint8Array, options: {
|
|
1321
|
+
readonly topic: string;
|
|
1322
|
+
}): void;
|
|
1323
|
+
/** Broadcast with delivery confirmation. */
|
|
1324
|
+
broadcastWithAck(data: unknown, timeout?: number): Promise<void>;
|
|
1325
|
+
/** Set this peer's presence data. */
|
|
1326
|
+
setPresence(data: Record<string, unknown>): void;
|
|
1327
|
+
/** Register a callback for when a remote peer joins. Returns unsubscribe fn. */
|
|
1328
|
+
onPeerJoin(handler: (peerId: string) => void): () => void;
|
|
1329
|
+
/** Register a callback for when a remote peer leaves. Returns unsubscribe fn. */
|
|
1330
|
+
onPeerLeave(handler: (peerId: string) => void): () => void;
|
|
1331
|
+
/** Register a callback for all incoming data. Returns unsubscribe fn. */
|
|
1332
|
+
onData(handler: (data: unknown, peerId: string) => void): () => void;
|
|
1333
|
+
/** Register a callback for presence updates. Returns unsubscribe fn. */
|
|
1334
|
+
onPresenceUpdate(handler: PresenceEvents<Record<string, unknown>>["update"]): () => void;
|
|
1335
|
+
/** Register a callback for host changes. Returns unsubscribe fn. */
|
|
1336
|
+
onHostChanged(handler: (hostId: string) => void): () => void;
|
|
1337
|
+
/**
|
|
1338
|
+
* Subscribe to state changes. The listener is called whenever reactive
|
|
1339
|
+
* state changes. Returns an unsubscribe function.
|
|
1340
|
+
*
|
|
1341
|
+
* Compatible with React's `useSyncExternalStore`, Svelte's `$effect`,
|
|
1342
|
+
* and any callback-based reactivity system.
|
|
1343
|
+
*/
|
|
1344
|
+
subscribe: {
|
|
1345
|
+
(topic: string, handler: (data: unknown, peerId: string) => void): () => void;
|
|
1346
|
+
(listener: StoreListener): () => void;
|
|
1347
|
+
};
|
|
1348
|
+
/**
|
|
1349
|
+
* Get an immutable snapshot of the current state.
|
|
1350
|
+
* Compatible with React's `useSyncExternalStore` (getSnapshot).
|
|
1351
|
+
*/
|
|
1352
|
+
getSnapshot(): DendriStoreSnapshot;
|
|
1353
|
+
/** Leave the room and remove all listeners. */
|
|
1354
|
+
destroy(): void;
|
|
1355
|
+
}
|
|
1356
|
+
/**
|
|
1357
|
+
* Create a framework-agnostic reactive store wrapping a Dendri Room.
|
|
1358
|
+
*
|
|
1359
|
+
* Accepts three calling shapes:
|
|
1360
|
+
*
|
|
1361
|
+
* ```ts
|
|
1362
|
+
* // 1. Flat DendriOptions (passthrough to the Dendri constructor)
|
|
1363
|
+
* const store = createDendriStore({ host: "localhost", port: 9000, secure: false });
|
|
1364
|
+
*
|
|
1365
|
+
* // 2. Advanced — inject a custom Dendri class (for tests / mocks)
|
|
1366
|
+
* const store = createDendriStore({
|
|
1367
|
+
* DendriCtor: MyDendri,
|
|
1368
|
+
* dendriOptions: { secure: true },
|
|
1369
|
+
* roomOptions: { migrationTimeout: 5000 },
|
|
1370
|
+
* });
|
|
1371
|
+
* ```
|
|
1372
|
+
*
|
|
1373
|
+
* @param input - Configuration containing an explicit signaling server host.
|
|
1374
|
+
* @returns A {@link DendriStore} instance.
|
|
1375
|
+
*/
|
|
1376
|
+
declare function createDendriStore(input?: CreateDendriStoreInput): DendriStore;
|
|
1377
|
+
|
|
1378
|
+
export { AckManager as A, BaseConnectionErrorType as B, type CallOption as C, Dendri as D, type RpcResponse as E, SerializationType as F, ServerMessageType as G, HybridConnection as H, SocketEventType as I, type StoreListener as J, TransportMode as K, LogLevel as L, MediaConnection as M, createDendriStore as N, isRpcRequest as O, type PresenceEvents as P, isRpcResponse as Q, Room as R, type SerializerMapping as S, type TransportEvents as T, type CreateDendriStoreInput as U, DataConnection as a, SignalingTransport as b, type AnswerOption as c, ConnectionQuality as d, ConnectionState as e, ConnectionType as f, DataConnectionErrorType as g, type DendriConnectOption as h, DendriError as i, DendriErrorType as j, type DendriEvents as k, type DendriOption as l, DendriOptions as m, type DendriStore as n, type DendriStoreOptions as o, type DendriStoreSnapshot as p, type HybridConnectionOption as q, type HybridSendOptions as r, PresenceManager as s, type RoomEvents as t, type RoomOptions as u, RpcError as v, RpcErrorCode as w, type RpcHandler as x, RpcManager as y, type RpcRequest as z };
|