dropspace 0.0.0-stage → 0.1.1

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.
Files changed (67) hide show
  1. package/README.md +17 -2
  2. package/dist/automerge/index.d.ts +108 -0
  3. package/dist/automerge/index.d.ts.map +1 -0
  4. package/dist/automerge/index.js +437 -0
  5. package/dist/automerge/index.js.map +1 -0
  6. package/dist/cli.d.ts +3 -0
  7. package/dist/cli.d.ts.map +1 -0
  8. package/dist/cli.js +372 -0
  9. package/dist/cli.js.map +1 -0
  10. package/dist/client/channel.d.ts +153 -0
  11. package/dist/client/channel.d.ts.map +1 -0
  12. package/dist/client/channel.js +704 -0
  13. package/dist/client/channel.js.map +1 -0
  14. package/dist/client/client.d.ts +376 -0
  15. package/dist/client/client.d.ts.map +1 -0
  16. package/dist/client/client.js +1421 -0
  17. package/dist/client/client.js.map +1 -0
  18. package/dist/client/events.d.ts +14 -0
  19. package/dist/client/events.d.ts.map +1 -0
  20. package/dist/client/events.js +71 -0
  21. package/dist/client/events.js.map +1 -0
  22. package/dist/client/identity.d.ts +89 -0
  23. package/dist/client/identity.d.ts.map +1 -0
  24. package/dist/client/identity.js +274 -0
  25. package/dist/client/identity.js.map +1 -0
  26. package/dist/client/index.d.ts +8 -0
  27. package/dist/client/index.d.ts.map +1 -0
  28. package/dist/client/index.js +8 -0
  29. package/dist/client/index.js.map +1 -0
  30. package/dist/client/peer-connection.d.ts +190 -0
  31. package/dist/client/peer-connection.d.ts.map +1 -0
  32. package/dist/client/peer-connection.js +647 -0
  33. package/dist/client/peer-connection.js.map +1 -0
  34. package/dist/client/peer-selection.d.ts +70 -0
  35. package/dist/client/peer-selection.d.ts.map +1 -0
  36. package/dist/client/peer-selection.js +193 -0
  37. package/dist/client/peer-selection.js.map +1 -0
  38. package/dist/client/storage.d.ts +118 -0
  39. package/dist/client/storage.d.ts.map +1 -0
  40. package/dist/client/storage.js +463 -0
  41. package/dist/client/storage.js.map +1 -0
  42. package/dist/files/index.d.ts +140 -0
  43. package/dist/files/index.d.ts.map +1 -0
  44. package/dist/files/index.js +751 -0
  45. package/dist/files/index.js.map +1 -0
  46. package/dist/files/sha256.d.ts +24 -0
  47. package/dist/files/sha256.d.ts.map +1 -0
  48. package/dist/files/sha256.js +147 -0
  49. package/dist/files/sha256.js.map +1 -0
  50. package/dist/index.d.ts +4 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +4 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/protocol/envelope.d.ts +61 -0
  55. package/dist/protocol/envelope.d.ts.map +1 -0
  56. package/dist/protocol/envelope.js +268 -0
  57. package/dist/protocol/envelope.js.map +1 -0
  58. package/dist/protocol/framing.d.ts +69 -0
  59. package/dist/protocol/framing.d.ts.map +1 -0
  60. package/dist/protocol/framing.js +145 -0
  61. package/dist/protocol/framing.js.map +1 -0
  62. package/dist/protocol/index.d.ts +3 -0
  63. package/dist/protocol/index.d.ts.map +1 -0
  64. package/dist/protocol/index.js +3 -0
  65. package/dist/protocol/index.js.map +1 -0
  66. package/fixtures/envelopes.json +132 -0
  67. package/package.json +51 -4
@@ -0,0 +1,647 @@
1
+ import { TypedEventEmitter } from "./events.js";
2
+ import { PeerChannelMultiplexer } from "./channel.js";
3
+ let connSeq = 0;
4
+ function nextConnectionId() {
5
+ connSeq = (connSeq + 1) % 1_000_000_000;
6
+ return `conn_${Date.now().toString(36)}_${connSeq.toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
7
+ }
8
+ /**
9
+ * Manages the WebRTC RTCPeerConnection & RTCDataChannel lifecycle for a single remote peer,
10
+ * including Perfect Negotiation (offer collision resolution), generation-scoped signaling,
11
+ * transient disconnect grace period, ICE restart, and coalesced exponential-backoff reconnection.
12
+ */
13
+ export class ManagedPeerSession {
14
+ options;
15
+ events = new TypedEventEmitter();
16
+ multiplexer;
17
+ state = "idle";
18
+ pc = null;
19
+ dc = null;
20
+ generation = 0;
21
+ connectionId = "";
22
+ makingOffer = false;
23
+ ignoreOffer = false;
24
+ isSettingRemoteAnswerPending = false;
25
+ pendingRemoteCandidates = [];
26
+ manuallyClosed = false;
27
+ reconnectAttempts = 0;
28
+ reconnectPromise = null;
29
+ graceTimer = null;
30
+ iceRestartTimer = null;
31
+ connectTimeoutTimer = null;
32
+ reconnectBackoffTimer = null;
33
+ boundIceCandidateListener = null;
34
+ boundConnectionStateListener = null;
35
+ boundDataChannelListener = null;
36
+ boundDcOpenListener = null;
37
+ boundDcCloseListener = null;
38
+ connectPromise = null;
39
+ staleSignalsDropped = 0;
40
+ offerCollisionsHandled = 0;
41
+ totalReconnectCount = 0;
42
+ selectedRouteType = "unknown";
43
+ constructor(options) {
44
+ this.options = options;
45
+ this.multiplexer = new PeerChannelMultiplexer({
46
+ peerDeviceId: options.remoteDeviceId,
47
+ localDeviceId: options.localDeviceId,
48
+ });
49
+ }
50
+ getState() {
51
+ return this.state;
52
+ }
53
+ getGeneration() {
54
+ return this.generation;
55
+ }
56
+ getConnectionId() {
57
+ return this.connectionId;
58
+ }
59
+ isManuallyClosed() {
60
+ return this.manuallyClosed;
61
+ }
62
+ transitionState(next, reason) {
63
+ if (this.state === next)
64
+ return;
65
+ const prev = this.state;
66
+ this.state = next;
67
+ this.events.emit("stateChange", {
68
+ peerDeviceId: this.options.remoteDeviceId,
69
+ previousState: prev,
70
+ state: next,
71
+ generation: this.generation,
72
+ connectionId: this.connectionId,
73
+ reason,
74
+ });
75
+ }
76
+ /**
77
+ * Initiates or re-initiates a connection to the peer.
78
+ * Coalesces concurrent calls while connecting or reconnecting.
79
+ */
80
+ async connect(reason = "explicit_connect") {
81
+ if (this.manuallyClosed) {
82
+ this.manuallyClosed = false;
83
+ }
84
+ if (this.state === "connected" && this.dc?.readyState === "open") {
85
+ return;
86
+ }
87
+ if (this.reconnectPromise) {
88
+ return this.reconnectPromise;
89
+ }
90
+ if (this.connectPromise) {
91
+ return this.connectPromise;
92
+ }
93
+ if (this.state === "connecting" && this.pc) {
94
+ return;
95
+ }
96
+ this.generation += 1;
97
+ this.connectionId = nextConnectionId();
98
+ this.transitionState("connecting", reason);
99
+ this.armConnectTimeout();
100
+ this.connectPromise = this.setupNewPeerConnection(true).finally(() => {
101
+ this.connectPromise = null;
102
+ });
103
+ return this.connectPromise;
104
+ }
105
+ /**
106
+ * Tears down any existing (potentially stale) RTCPeerConnection and immediately
107
+ * starts a fresh connection generation.
108
+ */
109
+ async forceReconnect(reason = "force_reconnect") {
110
+ this.manuallyClosed = false;
111
+ this.clearTimers();
112
+ this.teardownPeerConnectionOnly();
113
+ this.connectPromise = null;
114
+ this.reconnectPromise = null;
115
+ this.generation += 1;
116
+ this.connectionId = nextConnectionId();
117
+ this.transitionState("connecting", reason);
118
+ this.armConnectTimeout();
119
+ this.connectPromise = this.setupNewPeerConnection(true).finally(() => {
120
+ this.connectPromise = null;
121
+ });
122
+ return this.connectPromise;
123
+ }
124
+ armConnectTimeout() {
125
+ if (this.connectTimeoutTimer) {
126
+ clearTimeout(this.connectTimeoutTimer);
127
+ this.connectTimeoutTimer = null;
128
+ }
129
+ const connectTimeoutMs = this.options.connectTimeoutMs ?? 8000;
130
+ this.connectTimeoutTimer = setTimeout(() => {
131
+ this.connectTimeoutTimer = null;
132
+ if (!this.manuallyClosed && this.state === "connecting") {
133
+ this.teardownPeerConnectionOnly();
134
+ void this.scheduleReconnect("connect_timeout");
135
+ }
136
+ }, connectTimeoutMs);
137
+ }
138
+ setWaitingForPeer(reason = "peer_offline") {
139
+ if (this.manuallyClosed || this.state === "connected")
140
+ return;
141
+ this.clearTimers();
142
+ this.transitionState("waiting_for_peer", reason);
143
+ }
144
+ async setupNewPeerConnection(initiateOffer) {
145
+ this.teardownPeerConnectionOnly();
146
+ this.pendingRemoteCandidates = [];
147
+ const iceServers = await this.options.getIceServers();
148
+ const pc = this.options.createPeerConnection({
149
+ iceServers,
150
+ iceTransportPolicy: this.options.iceTransportPolicy ?? "all",
151
+ });
152
+ this.pc = pc;
153
+ const currentGen = this.generation;
154
+ this.boundIceCandidateListener = (ev) => {
155
+ if (this.manuallyClosed || this.generation !== currentGen)
156
+ return;
157
+ if (ev.candidate && ev.candidate.candidate) {
158
+ void this.options
159
+ .sendSignaling("webrtc.ice_candidate", this.connectionId, currentGen, {
160
+ candidate: ev.candidate.candidate,
161
+ sdpMid: ev.candidate.sdpMid ?? "0",
162
+ sdpMLineIndex: ev.candidate.sdpMLineIndex ?? 0,
163
+ usernameFragment: ev.candidate.usernameFragment ?? null,
164
+ })
165
+ .catch(() => { });
166
+ }
167
+ };
168
+ this.boundConnectionStateListener = () => {
169
+ if (this.manuallyClosed || this.generation !== currentGen)
170
+ return;
171
+ this.handleTransportStateChange();
172
+ };
173
+ this.boundDataChannelListener = (ev) => {
174
+ if (this.manuallyClosed || this.generation !== currentGen)
175
+ return;
176
+ if (ev.channel) {
177
+ this.bindDataChannel(ev.channel);
178
+ }
179
+ };
180
+ pc.addEventListener("icecandidate", this.boundIceCandidateListener);
181
+ pc.addEventListener("connectionstatechange", this.boundConnectionStateListener);
182
+ pc.addEventListener("iceconnectionstatechange", this.boundConnectionStateListener);
183
+ pc.addEventListener("datachannel", this.boundDataChannelListener);
184
+ const connectTimeoutMs = this.options.connectTimeoutMs ?? 15_000;
185
+ if (this.connectTimeoutTimer)
186
+ clearTimeout(this.connectTimeoutTimer);
187
+ this.connectTimeoutTimer = setTimeout(() => {
188
+ if (!this.manuallyClosed &&
189
+ this.generation === currentGen &&
190
+ this.state !== "connected") {
191
+ void this.scheduleReconnect("connect_timeout");
192
+ }
193
+ }, connectTimeoutMs);
194
+ if (initiateOffer) {
195
+ const dc = pc.createDataChannel("dropspace-v1", { ordered: true });
196
+ this.bindDataChannel(dc);
197
+ await this.performOffer(false);
198
+ }
199
+ }
200
+ bindDataChannel(dc) {
201
+ if (this.dc && this.dc !== dc) {
202
+ if (this.boundDcOpenListener) {
203
+ this.dc.removeEventListener("open", this.boundDcOpenListener);
204
+ }
205
+ if (this.boundDcCloseListener) {
206
+ this.dc.removeEventListener("close", this.boundDcCloseListener);
207
+ }
208
+ }
209
+ this.dc = dc;
210
+ this.multiplexer.attachDataChannel(dc);
211
+ this.boundDcOpenListener = () => {
212
+ if (this.manuallyClosed)
213
+ return;
214
+ this.clearTimers();
215
+ this.reconnectAttempts = 0;
216
+ this.transitionState("connected", "datachannel_open");
217
+ void this.inspectSelectedRoute();
218
+ };
219
+ this.boundDcCloseListener = () => {
220
+ if (this.manuallyClosed)
221
+ return;
222
+ if (this.state === "connected") {
223
+ void this.scheduleReconnect("datachannel_closed");
224
+ }
225
+ };
226
+ dc.addEventListener("open", this.boundDcOpenListener);
227
+ dc.addEventListener("close", this.boundDcCloseListener);
228
+ if (dc.readyState === "open") {
229
+ this.boundDcOpenListener();
230
+ }
231
+ }
232
+ async performOffer(iceRestart) {
233
+ if (!this.pc || this.manuallyClosed)
234
+ return;
235
+ const currentGen = this.generation;
236
+ const currentConnId = this.connectionId;
237
+ try {
238
+ this.makingOffer = true;
239
+ if (iceRestart && typeof this.pc.restartIce === "function") {
240
+ this.pc.restartIce();
241
+ }
242
+ const offer = await this.pc.createOffer(iceRestart ? { iceRestart: true } : undefined);
243
+ if (this.generation !== currentGen || !this.pc)
244
+ return;
245
+ await this.pc.setLocalDescription({
246
+ type: "offer",
247
+ sdp: offer.sdp,
248
+ });
249
+ await this.options.sendSignaling("webrtc.offer", currentConnId, currentGen, {
250
+ sdp: this.pc.localDescription?.sdp ?? offer.sdp ?? "",
251
+ iceRestart,
252
+ });
253
+ }
254
+ finally {
255
+ this.makingOffer = false;
256
+ }
257
+ }
258
+ /**
259
+ * Handles incoming WebRTC signaling envelopes (`webrtc.offer`, `webrtc.answer`, `webrtc.ice_candidate`, `webrtc.close`).
260
+ * Enforces monotonic generation and Perfect Negotiation offer-collision resolution.
261
+ */
262
+ async handleSignalingEnvelope(envelope) {
263
+ if (this.manuallyClosed && envelope.type !== "webrtc.offer")
264
+ return;
265
+ const msgGen = envelope.generation ?? 0;
266
+ const msgConnId = envelope.connectionId ?? "";
267
+ const readyForOffer = !this.makingOffer &&
268
+ (!this.pc ||
269
+ (this.pc.signalingState === "stable" && !this.isSettingRemoteAnswerPending));
270
+ const isNewOfferSession = envelope.type === "webrtc.offer" &&
271
+ msgConnId !== "" &&
272
+ msgConnId !== this.connectionId &&
273
+ readyForOffer;
274
+ // Reject stale generations (Section 9 & 10), but allow a peer that restarted
275
+ // with a brand-new connectionId (even if our local PC still appears connected).
276
+ if (msgGen < this.generation && !isNewOfferSession) {
277
+ this.staleSignalsDropped += 1;
278
+ this.events.emit("staleSignalDropped", {
279
+ peerDeviceId: this.options.remoteDeviceId,
280
+ signalType: envelope.type,
281
+ signalGeneration: msgGen,
282
+ currentGeneration: this.generation,
283
+ signalConnectionId: msgConnId,
284
+ currentConnectionId: this.connectionId,
285
+ });
286
+ return;
287
+ }
288
+ if (envelope.type === "webrtc.offer") {
289
+ this.manuallyClosed = false;
290
+ const sdp = String(envelope.payload.sdp ?? "");
291
+ if (isNewOfferSession && this.pc) {
292
+ this.clearTimers();
293
+ this.teardownPeerConnectionOnly();
294
+ }
295
+ const offerCollision = !readyForOffer && this.pc !== null;
296
+ if (offerCollision) {
297
+ this.offerCollisionsHandled += 1;
298
+ this.ignoreOffer = !this.options.polite;
299
+ if (this.ignoreOffer) {
300
+ this.events.emit("offerCollisionHandled", {
301
+ peerDeviceId: this.options.remoteDeviceId,
302
+ polite: false,
303
+ action: "ignored_incoming",
304
+ generation: msgGen,
305
+ });
306
+ return;
307
+ }
308
+ this.events.emit("offerCollisionHandled", {
309
+ peerDeviceId: this.options.remoteDeviceId,
310
+ polite: true,
311
+ action: "rolled_back",
312
+ generation: msgGen,
313
+ });
314
+ }
315
+ else {
316
+ this.ignoreOffer = false;
317
+ }
318
+ // Adopt incoming generation & connectionId if newer, restarted session, or if no PC exists yet
319
+ if (!this.pc || msgGen > this.generation || offerCollision || isNewOfferSession) {
320
+ this.generation = msgGen;
321
+ this.connectionId = msgConnId;
322
+ }
323
+ if (!this.pc) {
324
+ this.transitionState("connecting", "incoming_offer");
325
+ this.armConnectTimeout();
326
+ await this.setupNewPeerConnection(false);
327
+ }
328
+ else if (offerCollision && this.pc.signalingState !== "stable") {
329
+ // Recreate PC cleanly on initial-connect glare so no rolled-back local DataChannel remains
330
+ await this.setupNewPeerConnection(false);
331
+ }
332
+ if (!this.pc)
333
+ return;
334
+ await this.pc.setRemoteDescription({ type: "offer", sdp });
335
+ await this.flushPendingCandidates();
336
+ const answer = await this.pc.createAnswer();
337
+ await this.pc.setLocalDescription({ type: "answer", sdp: answer.sdp });
338
+ await this.options.sendSignaling("webrtc.answer", this.connectionId, this.generation, {
339
+ sdp: this.pc.localDescription?.sdp ?? answer.sdp ?? "",
340
+ });
341
+ return;
342
+ }
343
+ if (envelope.type === "webrtc.answer") {
344
+ if (!this.pc || msgGen !== this.generation || msgConnId !== this.connectionId) {
345
+ this.staleSignalsDropped += 1;
346
+ this.events.emit("staleSignalDropped", {
347
+ peerDeviceId: this.options.remoteDeviceId,
348
+ signalType: envelope.type,
349
+ signalGeneration: msgGen,
350
+ currentGeneration: this.generation,
351
+ signalConnectionId: msgConnId,
352
+ currentConnectionId: this.connectionId,
353
+ });
354
+ return;
355
+ }
356
+ this.ignoreOffer = false;
357
+ const sdp = String(envelope.payload.sdp ?? "");
358
+ this.isSettingRemoteAnswerPending = true;
359
+ try {
360
+ await this.pc.setRemoteDescription({ type: "answer", sdp });
361
+ }
362
+ finally {
363
+ this.isSettingRemoteAnswerPending = false;
364
+ }
365
+ await this.flushPendingCandidates();
366
+ return;
367
+ }
368
+ if (envelope.type === "webrtc.ice_candidate") {
369
+ // Drop candidate if it belongs to a stale generation or mismatched connectionId
370
+ if (msgGen !== this.generation ||
371
+ (this.connectionId !== "" && msgConnId !== this.connectionId)) {
372
+ this.staleSignalsDropped += 1;
373
+ this.events.emit("staleSignalDropped", {
374
+ peerDeviceId: this.options.remoteDeviceId,
375
+ signalType: envelope.type,
376
+ signalGeneration: msgGen,
377
+ currentGeneration: this.generation,
378
+ signalConnectionId: msgConnId,
379
+ currentConnectionId: this.connectionId,
380
+ });
381
+ return;
382
+ }
383
+ if (this.ignoreOffer)
384
+ return;
385
+ const candidateInit = {
386
+ candidate: String(envelope.payload.candidate ?? ""),
387
+ sdpMid: typeof envelope.payload.sdpMid === "string" ? envelope.payload.sdpMid : "0",
388
+ sdpMLineIndex: typeof envelope.payload.sdpMLineIndex === "number"
389
+ ? envelope.payload.sdpMLineIndex
390
+ : 0,
391
+ usernameFragment: typeof envelope.payload.usernameFragment === "string"
392
+ ? envelope.payload.usernameFragment
393
+ : null,
394
+ };
395
+ if (!this.pc || !this.pc.remoteDescription) {
396
+ this.pendingRemoteCandidates.push(candidateInit);
397
+ return;
398
+ }
399
+ try {
400
+ await this.pc.addIceCandidate(candidateInit);
401
+ }
402
+ catch (err) {
403
+ if (!this.ignoreOffer) {
404
+ this.events.emit("error", err instanceof Error ? err : new Error(String(err)));
405
+ }
406
+ }
407
+ return;
408
+ }
409
+ if (envelope.type === "webrtc.close") {
410
+ this.manuallyClosed = false;
411
+ this.clearTimers();
412
+ this.teardownPeerConnectionOnly();
413
+ this.transitionState("waiting_for_peer", "remote_webrtc_close");
414
+ }
415
+ }
416
+ async flushPendingCandidates() {
417
+ if (!this.pc || !this.pc.remoteDescription)
418
+ return;
419
+ while (this.pendingRemoteCandidates.length > 0) {
420
+ const cand = this.pendingRemoteCandidates.shift();
421
+ try {
422
+ await this.pc.addIceCandidate(cand);
423
+ }
424
+ catch {
425
+ // Ignore candidate failure during flush
426
+ }
427
+ }
428
+ }
429
+ handleTransportStateChange() {
430
+ if (!this.pc || this.manuallyClosed)
431
+ return;
432
+ const connState = this.pc.connectionState || this.pc.iceConnectionState;
433
+ if (connState === "connected" || connState === "completed") {
434
+ this.clearTimers();
435
+ this.reconnectAttempts = 0;
436
+ if (this.dc?.readyState === "open") {
437
+ this.transitionState("connected", "ice_connected");
438
+ void this.inspectSelectedRoute();
439
+ }
440
+ return;
441
+ }
442
+ if (connState === "disconnected") {
443
+ // Transient disconnect: wait grace period before starting ICE restart
444
+ if (!this.graceTimer) {
445
+ const graceMs = this.options.disconnectGracePeriodMs ?? 2500;
446
+ this.graceTimer = setTimeout(() => {
447
+ this.graceTimer = null;
448
+ if (!this.manuallyClosed &&
449
+ this.pc &&
450
+ (this.pc.connectionState === "disconnected" ||
451
+ this.pc.iceConnectionState === "disconnected")) {
452
+ void this.scheduleReconnect("ice_disconnected_grace_expired");
453
+ }
454
+ }, graceMs);
455
+ }
456
+ return;
457
+ }
458
+ if (connState === "failed") {
459
+ void this.scheduleReconnect("transport_failed");
460
+ }
461
+ }
462
+ /**
463
+ * Schedules a coalesced reconnect attempt with exponential backoff + jitter.
464
+ * First attempts an in-place ICE restart if a PC exists; otherwise creates a new RTCPeerConnection.
465
+ */
466
+ async scheduleReconnect(reason) {
467
+ if (this.manuallyClosed)
468
+ return;
469
+ if (this.reconnectPromise) {
470
+ return this.reconnectPromise;
471
+ }
472
+ const maxAttempts = this.options.maxReconnectAttempts ?? 5;
473
+ if (this.reconnectAttempts >= maxAttempts) {
474
+ this.clearTimers();
475
+ this.transitionState("failed", `reconnect_budget_exhausted (${reason})`);
476
+ return;
477
+ }
478
+ this.reconnectAttempts += 1;
479
+ this.totalReconnectCount += 1;
480
+ const attempt = this.reconnectAttempts;
481
+ const baseMs = this.options.baseBackoffMs ?? 200;
482
+ const maxMs = this.options.maxBackoffMs ?? 5000;
483
+ const expDelay = Math.min(maxMs, baseMs * Math.pow(2, attempt - 1));
484
+ const jitter = Math.floor(Math.random() * Math.max(20, Math.floor(expDelay * 0.25)));
485
+ const delayMs = expDelay + jitter;
486
+ const strategy = attempt === 1 && this.pc ? "ice_restart" : "new_pc";
487
+ this.transitionState("reconnecting", `${reason} (attempt ${attempt}, ${strategy})`);
488
+ this.events.emit("reconnectAttempt", {
489
+ peerDeviceId: this.options.remoteDeviceId,
490
+ attempt,
491
+ strategy,
492
+ delayMs,
493
+ });
494
+ this.reconnectPromise = (async () => {
495
+ try {
496
+ await new Promise((resolve) => {
497
+ this.reconnectBackoffTimer = setTimeout(() => {
498
+ this.reconnectBackoffTimer = null;
499
+ resolve();
500
+ }, delayMs);
501
+ });
502
+ if (this.manuallyClosed || this.state === "connected")
503
+ return;
504
+ if (strategy === "ice_restart" && this.pc) {
505
+ await this.performOffer(true);
506
+ const iceTimeoutMs = this.options.iceRestartTimeoutMs ?? 3000;
507
+ await new Promise((resolve) => {
508
+ this.iceRestartTimer = setTimeout(() => {
509
+ this.iceRestartTimer = null;
510
+ resolve();
511
+ }, iceTimeoutMs);
512
+ });
513
+ if (!this.manuallyClosed && this.state !== "connected") {
514
+ // Escalate to full new RTCPeerConnection
515
+ this.generation += 1;
516
+ this.connectionId = nextConnectionId();
517
+ await this.setupNewPeerConnection(true);
518
+ }
519
+ }
520
+ else {
521
+ this.generation += 1;
522
+ this.connectionId = nextConnectionId();
523
+ await this.setupNewPeerConnection(true);
524
+ }
525
+ }
526
+ catch (err) {
527
+ this.events.emit("error", err instanceof Error ? err : new Error(String(err)));
528
+ }
529
+ finally {
530
+ this.reconnectPromise = null;
531
+ }
532
+ })();
533
+ return this.reconnectPromise;
534
+ }
535
+ async inspectSelectedRoute() {
536
+ if (!this.pc || typeof this.pc.getStats !== "function")
537
+ return;
538
+ try {
539
+ const stats = await this.pc.getStats();
540
+ let candidateType = "unknown";
541
+ let rttMs;
542
+ let protocol;
543
+ const entries = stats instanceof Map ? Array.from(stats.values()) : Array.isArray(stats) ? stats : [];
544
+ for (const report of entries) {
545
+ if (report.type === "candidate-pair" && (report.nominated || report.state === "succeeded")) {
546
+ if (typeof report.currentRoundTripTime === "number") {
547
+ rttMs = Math.round(report.currentRoundTripTime * 1000);
548
+ }
549
+ const localCand = entries.find((r) => r.id === report.localCandidateId);
550
+ const remoteCand = entries.find((r) => r.id === report.remoteCandidateId);
551
+ if (localCand?.candidateType === "relay" ||
552
+ remoteCand?.candidateType === "relay") {
553
+ candidateType = "relay";
554
+ }
555
+ else if (localCand?.candidateType || remoteCand?.candidateType) {
556
+ candidateType = "direct";
557
+ }
558
+ protocol = localCand?.protocol;
559
+ }
560
+ }
561
+ if (candidateType === "unknown") {
562
+ candidateType =
563
+ this.options.iceTransportPolicy === "relay" ? "relay" : "direct";
564
+ }
565
+ this.selectedRouteType = candidateType;
566
+ this.events.emit("routeSelected", {
567
+ peerDeviceId: this.options.remoteDeviceId,
568
+ candidateType,
569
+ protocol,
570
+ rttMs,
571
+ });
572
+ }
573
+ catch {
574
+ // Ignore stats failure
575
+ }
576
+ }
577
+ clearTimers() {
578
+ if (this.graceTimer) {
579
+ clearTimeout(this.graceTimer);
580
+ this.graceTimer = null;
581
+ }
582
+ if (this.iceRestartTimer) {
583
+ clearTimeout(this.iceRestartTimer);
584
+ this.iceRestartTimer = null;
585
+ }
586
+ if (this.connectTimeoutTimer) {
587
+ clearTimeout(this.connectTimeoutTimer);
588
+ this.connectTimeoutTimer = null;
589
+ }
590
+ if (this.reconnectBackoffTimer) {
591
+ clearTimeout(this.reconnectBackoffTimer);
592
+ this.reconnectBackoffTimer = null;
593
+ }
594
+ }
595
+ teardownPeerConnectionOnly() {
596
+ if (this.dc) {
597
+ if (this.boundDcOpenListener) {
598
+ this.dc.removeEventListener("open", this.boundDcOpenListener);
599
+ }
600
+ if (this.boundDcCloseListener) {
601
+ this.dc.removeEventListener("close", this.boundDcCloseListener);
602
+ }
603
+ try {
604
+ this.dc.close();
605
+ }
606
+ catch { }
607
+ this.dc = null;
608
+ }
609
+ if (this.pc) {
610
+ if (this.boundIceCandidateListener) {
611
+ this.pc.removeEventListener("icecandidate", this.boundIceCandidateListener);
612
+ }
613
+ if (this.boundConnectionStateListener) {
614
+ this.pc.removeEventListener("connectionstatechange", this.boundConnectionStateListener);
615
+ this.pc.removeEventListener("iceconnectionstatechange", this.boundConnectionStateListener);
616
+ }
617
+ if (this.boundDataChannelListener) {
618
+ this.pc.removeEventListener("datachannel", this.boundDataChannelListener);
619
+ }
620
+ try {
621
+ this.pc.close();
622
+ }
623
+ catch { }
624
+ this.pc = null;
625
+ }
626
+ }
627
+ /**
628
+ * Explicitly closes the peer connection and disables automatic reconnection.
629
+ */
630
+ close(notifyRemote = true) {
631
+ if (this.manuallyClosed && this.state === "closed")
632
+ return;
633
+ this.manuallyClosed = true;
634
+ this.clearTimers();
635
+ if (notifyRemote && this.connectionId) {
636
+ void this.options
637
+ .sendSignaling("webrtc.close", this.connectionId, this.generation, {
638
+ reason: "manual_close",
639
+ })
640
+ .catch(() => { });
641
+ }
642
+ this.teardownPeerConnectionOnly();
643
+ this.multiplexer.close();
644
+ this.transitionState("closed", "manual_close");
645
+ }
646
+ }
647
+ //# sourceMappingURL=peer-connection.js.map