@ai-matrx/meet 0.11.28 → 0.11.29

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/dist/core.js CHANGED
@@ -204,6 +204,352 @@ function invalidResponse(operation, detail) {
204
204
  );
205
205
  }
206
206
 
207
+ // src/core/connection-supervisor.ts
208
+ var DEFAULT_CONNECTION_POLICY = {
209
+ bannerDelayMs: 2e3,
210
+ autoRejoin: true,
211
+ autoRejoinMaxAttempts: 5,
212
+ reconnectGraceSeconds: 30
213
+ };
214
+ function clampInt(value, min, max, fallback) {
215
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
216
+ return Math.min(max, Math.max(min, Math.round(value)));
217
+ }
218
+ function connectionPolicyFrom(raw) {
219
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
220
+ const row = raw;
221
+ const d = DEFAULT_CONNECTION_POLICY;
222
+ return {
223
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
224
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
225
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
226
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
227
+ };
228
+ }
229
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
230
+ function createReconnectPolicy(options = {}) {
231
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
232
+ const giveUpMs = options.giveUpMs ?? 45e3;
233
+ const maxAttempts = options.maxAttempts ?? 12;
234
+ const random = options.random ?? Math.random;
235
+ return {
236
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
237
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
238
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
239
+ const spread = Math.min(2e3, Math.max(500, base / 2));
240
+ return Math.round(base + random() * spread);
241
+ }
242
+ };
243
+ }
244
+ function createWebNetworkPort() {
245
+ const scope = globalThis;
246
+ return {
247
+ online: () => scope.navigator?.onLine !== false,
248
+ subscribe(listener) {
249
+ if (typeof scope.addEventListener !== "function") return () => void 0;
250
+ const on = () => listener(true);
251
+ const off = () => listener(false);
252
+ scope.addEventListener("online", on);
253
+ scope.addEventListener("offline", off);
254
+ return () => {
255
+ scope.removeEventListener?.("online", on);
256
+ scope.removeEventListener?.("offline", off);
257
+ };
258
+ }
259
+ };
260
+ }
261
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
262
+ return async () => {
263
+ const targets = urls().filter((url) => url.length > 0);
264
+ if (targets.length === 0 || typeof fetch !== "function") return true;
265
+ const one = async (url) => {
266
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
267
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
268
+ try {
269
+ await fetch(url, {
270
+ method: "GET",
271
+ mode: "no-cors",
272
+ cache: "no-store",
273
+ credentials: "omit",
274
+ ...controller !== null ? { signal: controller.signal } : {}
275
+ });
276
+ return true;
277
+ } catch {
278
+ return false;
279
+ } finally {
280
+ clearTimeout(timer);
281
+ }
282
+ };
283
+ const answers = await Promise.all(targets.map(one));
284
+ return answers.every(Boolean);
285
+ };
286
+ }
287
+ function httpUrlOf(serverUrl) {
288
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
289
+ }
290
+ var RESUME_WINDOW_MS = 15 * 6e4;
291
+ var RESUME_KEY = "mx-meet:resume:";
292
+ function createWebResumeStorage() {
293
+ const storage = globalThis.sessionStorage;
294
+ if (storage === void 0) return void 0;
295
+ return {
296
+ load(roomName) {
297
+ try {
298
+ const raw = storage.getItem(RESUME_KEY + roomName);
299
+ if (raw === null) return null;
300
+ const parsed = JSON.parse(raw);
301
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
302
+ } catch {
303
+ return null;
304
+ }
305
+ },
306
+ save(roomName, marker) {
307
+ try {
308
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
309
+ } catch {
310
+ }
311
+ },
312
+ clear(roomName) {
313
+ try {
314
+ storage.removeItem(RESUME_KEY + roomName);
315
+ } catch {
316
+ }
317
+ }
318
+ };
319
+ }
320
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
321
+ function autoRejoinDelayMs(attempt, random = Math.random) {
322
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
323
+ return Math.round(base * (0.5 + random()));
324
+ }
325
+ function createConnectionSupervisor(options) {
326
+ const { store, network } = options;
327
+ const now = options.now ?? (() => Date.now());
328
+ const random = options.random ?? Math.random;
329
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
330
+ let driverRegion = null;
331
+ let probeTimer = null;
332
+ let probing = false;
333
+ let bannerTimer = null;
334
+ let rejoinTimer = null;
335
+ let rejoinAttempts = 0;
336
+ let lastPhase = store.snapshot().phase;
337
+ let lastSavedAt = 0;
338
+ let disposed = false;
339
+ const inCall = () => store.snapshot().phase === "in_call";
340
+ const dropped = () => store.snapshot().phase === "disconnected";
341
+ function setRegion(next) {
342
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
343
+ }
344
+ function regionFor(reachable) {
345
+ if (!network.online()) return "offline";
346
+ if (reachable === false) return "server_unreachable";
347
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
348
+ return driverRegion ?? "stable";
349
+ }
350
+ function needsWatch() {
351
+ const snap = store.snapshot();
352
+ if (dropped()) return true;
353
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
354
+ }
355
+ function stopProbe() {
356
+ if (probeTimer !== null) clearTimeout(probeTimer);
357
+ probeTimer = null;
358
+ }
359
+ function scheduleProbe(delayMs) {
360
+ if (disposed || probeTimer !== null) return;
361
+ probeTimer = setTimeout(() => {
362
+ probeTimer = null;
363
+ void runProbe();
364
+ }, delayMs);
365
+ }
366
+ async function runProbe() {
367
+ if (disposed || probing) return;
368
+ if (!needsWatch()) return;
369
+ if (!network.online()) {
370
+ setRegion("offline");
371
+ scheduleProbe(probeEveryMs);
372
+ return;
373
+ }
374
+ probing = true;
375
+ let reachable = false;
376
+ try {
377
+ reachable = await options.probe();
378
+ } catch {
379
+ reachable = false;
380
+ } finally {
381
+ probing = false;
382
+ }
383
+ if (disposed) return;
384
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
385
+ if (dropped() && reachable) maybeRejoin();
386
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
387
+ }
388
+ function syncBanner() {
389
+ const snap = store.snapshot();
390
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
391
+ if (!unstable) {
392
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
393
+ bannerTimer = null;
394
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
395
+ return;
396
+ }
397
+ if (snap.connectionBanner || bannerTimer !== null) return;
398
+ const delay = options.policy().bannerDelayMs;
399
+ if (delay <= 0) {
400
+ store.patch({ connectionBanner: true });
401
+ return;
402
+ }
403
+ bannerTimer = setTimeout(() => {
404
+ bannerTimer = null;
405
+ const latest = store.snapshot();
406
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
407
+ store.patch({ connectionBanner: true });
408
+ }
409
+ }, delay);
410
+ }
411
+ function stopRejoin() {
412
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
413
+ rejoinTimer = null;
414
+ }
415
+ function maybeRejoin() {
416
+ if (disposed || rejoinTimer !== null || !dropped()) return;
417
+ const policy = options.policy();
418
+ if (!policy.autoRejoin) return;
419
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
420
+ if (!network.online()) return;
421
+ const attempt = rejoinAttempts;
422
+ rejoinTimer = setTimeout(() => {
423
+ rejoinTimer = null;
424
+ if (disposed || !dropped() || !network.online()) return;
425
+ rejoinAttempts = attempt + 1;
426
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
427
+ void options.autoRejoin().catch(() => void 0);
428
+ }, autoRejoinDelayMs(attempt, random));
429
+ }
430
+ function saveMarker(force) {
431
+ const resume = options.resume;
432
+ const snap = store.snapshot();
433
+ if (resume === void 0 || snap.roomName === null) return;
434
+ const at = now();
435
+ if (!force && at - lastSavedAt < 1e4) return;
436
+ const request = options.currentRequest();
437
+ if (request === null) return;
438
+ lastSavedAt = at;
439
+ resume.save(snap.roomName, { request, at });
440
+ }
441
+ const unsubscribeStore = store.subscribe(() => {
442
+ if (disposed) return;
443
+ const snap = store.snapshot();
444
+ const phase = snap.phase;
445
+ const changed = phase !== lastPhase;
446
+ lastPhase = phase;
447
+ if (changed) {
448
+ if (phase === "in_call") {
449
+ rejoinAttempts = 0;
450
+ stopRejoin();
451
+ saveMarker(true);
452
+ } else if (phase === "disconnected") {
453
+ driverRegion = null;
454
+ saveMarker(true);
455
+ stopProbe();
456
+ scheduleProbe(0);
457
+ } else {
458
+ stopRejoin();
459
+ if (phase !== "joining") driverRegion = null;
460
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
461
+ }
462
+ } else if (phase === "in_call" || phase === "disconnected") {
463
+ saveMarker(false);
464
+ }
465
+ syncBanner();
466
+ });
467
+ const unsubscribeNetwork = network.subscribe((online) => {
468
+ if (disposed) return;
469
+ if (!online) {
470
+ if (inCall() || dropped()) setRegion("offline");
471
+ stopRejoin();
472
+ stopProbe();
473
+ if (needsWatch()) scheduleProbe(probeEveryMs);
474
+ return;
475
+ }
476
+ stopProbe();
477
+ if (needsWatch()) scheduleProbe(0);
478
+ });
479
+ return {
480
+ driverState(state) {
481
+ if (disposed) return;
482
+ if (state === "connected") {
483
+ driverRegion = null;
484
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
485
+ if (!needsWatch()) stopProbe();
486
+ return;
487
+ }
488
+ if (state === "reconnecting" || state === "signal_reconnecting") {
489
+ driverRegion = state;
490
+ if (inCall()) {
491
+ setRegion(network.online() ? state : "offline");
492
+ stopProbe();
493
+ scheduleProbe(0);
494
+ }
495
+ }
496
+ },
497
+ exitConnection() {
498
+ return network.online() ? "server_unreachable" : "offline";
499
+ },
500
+ resumable(roomName) {
501
+ const resume = options.resume;
502
+ if (resume === void 0) return null;
503
+ const marker = resume.load(roomName);
504
+ if (marker === null) return null;
505
+ if (now() - marker.at > RESUME_WINDOW_MS) {
506
+ resume.clear(roomName);
507
+ return null;
508
+ }
509
+ return marker;
510
+ },
511
+ dispose() {
512
+ disposed = true;
513
+ stopProbe();
514
+ stopRejoin();
515
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
516
+ bannerTimer = null;
517
+ unsubscribeStore();
518
+ unsubscribeNetwork();
519
+ }
520
+ };
521
+ }
522
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
523
+ "CLIENT_INITIATED",
524
+ "PARTICIPANT_REMOVED",
525
+ "ROOM_DELETED",
526
+ "ROOM_CLOSED",
527
+ "DUPLICATE_IDENTITY",
528
+ "USER_REJECTED",
529
+ "USER_UNAVAILABLE",
530
+ "SIP_TRUNK_FAILURE"
531
+ ]);
532
+ function isUnannouncedDeparture(reason) {
533
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
534
+ }
535
+ function mergeGhosts(live, ghosts, at) {
536
+ const present = new Set(live.map((p) => p.identity));
537
+ const kept = /* @__PURE__ */ new Map();
538
+ const extra = [];
539
+ for (const [identity, ghost] of ghosts) {
540
+ if (present.has(identity) || ghost.until <= at) continue;
541
+ kept.set(identity, ghost);
542
+ extra.push({
543
+ ...ghost.participant,
544
+ micEnabled: false,
545
+ cameraEnabled: false,
546
+ screenSharing: false,
547
+ reconnecting: true
548
+ });
549
+ }
550
+ return { roster: [...live, ...extra], kept };
551
+ }
552
+
207
553
  // src/core/tokens.ts
208
554
  function createWebRoomTokenStorage(onDiagnostic) {
209
555
  const store = globalThis.localStorage;
@@ -389,7 +735,8 @@ function parseRoomToken(body, roomName) {
389
735
  admissionState: readString(payload, "admission_state") ?? void 0,
390
736
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
391
737
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
392
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
738
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
739
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
393
740
  };
394
741
  }
395
742
  function createTokenClient(options) {
@@ -1119,6 +1466,10 @@ function nextPhase(from, event) {
1119
1466
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1120
1467
  case "join":
1121
1468
  return JOINABLE_FROM.has(from) ? "joining" : null;
1469
+ case "auto_rejoin":
1470
+ return from === "disconnected" ? "joining" : null;
1471
+ case "resumed":
1472
+ return from === "resolving" ? "joining" : null;
1122
1473
  case "back_to_prejoin":
1123
1474
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1124
1475
  case "retry":
@@ -3032,6 +3383,7 @@ var EMPTY = {
3032
3383
  phaseNotice: null,
3033
3384
  roomName: null,
3034
3385
  connection: "stable",
3386
+ connectionBanner: false,
3035
3387
  share: NO_SHARE,
3036
3388
  call: null,
3037
3389
  waiting: null,
@@ -3986,8 +4338,11 @@ function mapConnectionState(state) {
3986
4338
  case "connecting":
3987
4339
  return "connecting";
3988
4340
  case "reconnecting":
3989
- case "signalReconnecting":
3990
4341
  return "reconnecting";
4342
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
4343
+ // signal blip from a full media reconnect.
4344
+ case "signalReconnecting":
4345
+ return "signal_reconnecting";
3991
4346
  default:
3992
4347
  return "disconnected";
3993
4348
  }
@@ -4068,8 +4423,12 @@ function createLiveKitDriver(handlers, options = {}) {
4068
4423
  // R5: large meetings degrade gracefully instead of melting a laptop.
4069
4424
  adaptiveStream: true,
4070
4425
  dynacast: true,
4426
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
4427
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
4428
+ reconnectPolicy: createReconnectPolicy(),
4071
4429
  ...options.roomOptions
4072
4430
  });
4431
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module.DisconnectReason?.[reason] ?? String(reason);
4073
4432
  const roster = () => {
4074
4433
  const local = snapshotParticipant(created.localParticipant, true);
4075
4434
  const remotes = [...created.remoteParticipants.values()].map(
@@ -4087,7 +4446,10 @@ function createLiveKitDriver(handlers, options = {}) {
4087
4446
  if (track.kind !== Track.Kind.Audio) return;
4088
4447
  for (const element of track.detach()) element.remove();
4089
4448
  });
4090
- created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, roster).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4449
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4450
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
4451
+ roster();
4452
+ }).on(RoomEvent.TrackSubscribed, roster).on(RoomEvent.TrackUnsubscribed, roster).on(RoomEvent.TrackMuted, roster).on(RoomEvent.TrackUnmuted, roster).on(RoomEvent.LocalTrackPublished, roster).on(RoomEvent.LocalTrackUnpublished, roster).on(RoomEvent.ParticipantNameChanged, roster).on(RoomEvent.ParticipantAttributesChanged, roster).on(RoomEvent.ParticipantMetadataChanged, roster).on(RoomEvent.RoomMetadataChanged, (metadata) => {
4091
4453
  handlers.roomMetadata?.(metadata ?? "");
4092
4454
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
4093
4455
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -4104,8 +4466,7 @@ function createLiveKitDriver(handlers, options = {}) {
4104
4466
  }).on(RoomEvent.Disconnected, (reason) => {
4105
4467
  audioContainer?.remove();
4106
4468
  audioContainer = null;
4107
- const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
4108
- handlers.disconnected(named);
4469
+ handlers.disconnected(reasonName(reason));
4109
4470
  });
4110
4471
  try {
4111
4472
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5478,6 +5839,18 @@ function createRoomEngine(options) {
5478
5839
  let disposed = false;
5479
5840
  let moving = false;
5480
5841
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5842
+ let lastServerUrl = null;
5843
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
5844
+ let ghosts = /* @__PURE__ */ new Map();
5845
+ let ghostTimer = null;
5846
+ let liveRoster = [];
5847
+ const departures = /* @__PURE__ */ new Map();
5848
+ function requestAsNow() {
5849
+ if (lastRequest === null) return null;
5850
+ const media = store.snapshot().media;
5851
+ if (store.snapshot().phase !== "in_call") return lastRequest;
5852
+ return { ...lastRequest, mode: media.camera.enabled ? "video" : "audio", microphone: media.microphone.enabled };
5853
+ }
5481
5854
  async function dropRoom() {
5482
5855
  const target = driver;
5483
5856
  driver = null;
@@ -5488,6 +5861,7 @@ function createRoomEngine(options) {
5488
5861
  store.setLocalTrack("microphone", IDLE_TRACK);
5489
5862
  store.setLocalTrack("camera", IDLE_TRACK);
5490
5863
  store.patch({ share: NO_SHARE, connection: "stable" });
5864
+ clearGhosts();
5491
5865
  await target?.disconnect();
5492
5866
  }
5493
5867
  async function leaveRoom() {
@@ -5555,7 +5929,7 @@ function createRoomEngine(options) {
5555
5929
  participants(list) {
5556
5930
  const joinedAt = new Date(now()).toISOString();
5557
5931
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
5558
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
5932
+ projectRoster(list, joinedAt);
5559
5933
  const snapshot = store.snapshot();
5560
5934
  const newcomer = snapshot.participants.some(
5561
5935
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -5567,6 +5941,9 @@ function createRoomEngine(options) {
5567
5941
  );
5568
5942
  }
5569
5943
  },
5944
+ participantLeft(who, reason) {
5945
+ departures.set(who, reason);
5946
+ },
5570
5947
  roomMetadata(metadata) {
5571
5948
  let spotlight = null;
5572
5949
  if (metadata.length > 0) {
@@ -5593,12 +5970,8 @@ function createRoomEngine(options) {
5593
5970
  store.setQuality(asParticipantIdentity(who), value);
5594
5971
  },
5595
5972
  connectionState(state) {
5596
- if (state === "connected") {
5597
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5598
- store.dispatch({ type: "connected" });
5599
- } else if (state === "reconnecting") {
5600
- store.patch({ connection: "reconnecting" });
5601
- }
5973
+ supervisor.driverState(state);
5974
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
5602
5975
  },
5603
5976
  transcription(event) {
5604
5977
  const snapshot = store.snapshot();
@@ -5731,6 +6104,7 @@ function createRoomEngine(options) {
5731
6104
  };
5732
6105
  async function resolveExitAndApply(hint) {
5733
6106
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6107
+ lastRequest = requestAsNow();
5734
6108
  await dropRoom();
5735
6109
  let server;
5736
6110
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5744,7 +6118,7 @@ function createRoomEngine(options) {
5744
6118
  }
5745
6119
  if (disposed) return;
5746
6120
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5747
- store.patch({ connection: resolved.connection });
6121
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
5748
6122
  if (resolved.to === "waiting:knocking") {
5749
6123
  store.patch({
5750
6124
  waiting: {
@@ -5776,6 +6150,8 @@ function createRoomEngine(options) {
5776
6150
  ...request.observe === true ? { observe: true } : {}
5777
6151
  });
5778
6152
  current = token;
6153
+ lastServerUrl = token.serverUrl;
6154
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5779
6155
  store.patch({
5780
6156
  localIdentity: token.identity,
5781
6157
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -5880,7 +6256,59 @@ function createRoomEngine(options) {
5880
6256
  throw normalized;
5881
6257
  }
5882
6258
  }
5883
- return {
6259
+ function projectRoster(list, joinedAt) {
6260
+ const at = now();
6261
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
6262
+ const present = new Set(list.map((p) => p.identity));
6263
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
6264
+ for (const [who, reason] of departures) {
6265
+ const last = previous.get(who);
6266
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
6267
+ ghosts.set(who, { participant: last, until: at + graceMs });
6268
+ }
6269
+ }
6270
+ departures.clear();
6271
+ liveRoster = list;
6272
+ const merged = mergeGhosts(list, ghosts, at);
6273
+ ghosts = merged.kept;
6274
+ store.setParticipants(
6275
+ merged.roster.map((entry) => {
6276
+ const projected = projectParticipant(entry, joinedAt);
6277
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6278
+ })
6279
+ );
6280
+ scheduleGhostExpiry();
6281
+ }
6282
+ function scheduleGhostExpiry() {
6283
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6284
+ ghostTimer = null;
6285
+ if (ghosts.size === 0 || disposed) return;
6286
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
6287
+ ghostTimer = setTimeout(() => {
6288
+ ghostTimer = null;
6289
+ if (disposed || driver === null) return;
6290
+ projectRoster(liveRoster, new Date(now()).toISOString());
6291
+ }, Math.max(0, next - now()) + 50);
6292
+ }
6293
+ function clearGhosts() {
6294
+ ghosts = /* @__PURE__ */ new Map();
6295
+ departures.clear();
6296
+ liveRoster = [];
6297
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6298
+ ghostTimer = null;
6299
+ }
6300
+ const supervisor = createConnectionSupervisor({
6301
+ store,
6302
+ network: options.connection?.network ?? createWebNetworkPort(),
6303
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
6304
+ policy: () => connectionPolicy,
6305
+ autoRejoin: () => engine.autoRejoin(),
6306
+ currentRequest: requestAsNow,
6307
+ resume: options.connection?.resume,
6308
+ now,
6309
+ report: (level, message) => report(level, message)
6310
+ });
6311
+ const engine = {
5884
6312
  join(request) {
5885
6313
  if (joining !== null) return joining;
5886
6314
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -5925,6 +6353,35 @@ function createRoomEngine(options) {
5925
6353
  });
5926
6354
  return joining;
5927
6355
  },
6356
+ async autoRejoin() {
6357
+ const request = lastRequest;
6358
+ if (request === null || joining !== null) return joining ?? void 0;
6359
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
6360
+ tokens.invalidate(request.roomName);
6361
+ current = null;
6362
+ joining = connect(request).catch((error) => {
6363
+ throw failJoin(error, "meet.autoRejoin");
6364
+ }).finally(() => {
6365
+ joining = null;
6366
+ });
6367
+ return joining;
6368
+ },
6369
+ resume(roomName) {
6370
+ const marker = supervisor.resumable(roomName);
6371
+ if (marker === null || joining !== null) return false;
6372
+ if (!store.dispatch({ type: "resumed" })) return false;
6373
+ lastRequest = { ...marker.request, roomName };
6374
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
6375
+ tokens.invalidate(roomName);
6376
+ current = null;
6377
+ joining = connect(lastRequest).catch((error) => {
6378
+ throw failJoin(error, "meet.resume");
6379
+ }).finally(() => {
6380
+ joining = null;
6381
+ });
6382
+ joining.catch(() => void 0);
6383
+ return true;
6384
+ },
5928
6385
  async moveToBreakout(roomId) {
5929
6386
  const request = lastRequest;
5930
6387
  if (request === null || driver === null) {
@@ -6092,6 +6549,8 @@ function createRoomEngine(options) {
6092
6549
  token: () => current,
6093
6550
  async dispose() {
6094
6551
  disposed = true;
6552
+ supervisor.dispose();
6553
+ clearGhosts();
6095
6554
  if (driver !== null) {
6096
6555
  const target = driver;
6097
6556
  driver = null;
@@ -6100,6 +6559,7 @@ function createRoomEngine(options) {
6100
6559
  store.reset();
6101
6560
  }
6102
6561
  };
6562
+ return engine;
6103
6563
  }
6104
6564
  function canModerate(participant) {
6105
6565
  return participant !== void 0 && isHostRole(participant.role);
@@ -7185,7 +7645,7 @@ function participantAttributes(p) {
7185
7645
  "data-meet-participant-role": observedRole(p.role, p.standing),
7186
7646
  "data-meet-participant-self": String(p.isLocal)
7187
7647
  };
7188
- const quality = observedQuality(p.quality);
7648
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
7189
7649
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
7190
7650
  return attrs;
7191
7651
  }
@@ -7201,6 +7661,7 @@ export {
7201
7661
  CAPTION_LANGUAGES,
7202
7662
  CONSENT_NOTICE_ATTRIBUTE,
7203
7663
  DATA_TOPICS,
7664
+ DEFAULT_CONNECTION_POLICY,
7204
7665
  DEFAULT_EFFECTS,
7205
7666
  DEFAULT_PAGE_SIZE,
7206
7667
  EMPTY_COLLAB,
@@ -7215,6 +7676,7 @@ export {
7215
7676
  IDLE_TRACK,
7216
7677
  JOIN_REFUSAL_REASONS,
7217
7678
  LISTEN_ONLY_NOTICE,
7679
+ LIVEKIT_BASE_DELAYS_MS,
7218
7680
  LIVE_PHASES,
7219
7681
  MEETING_INVITE_KIND,
7220
7682
  MEETING_INVITE_VERSION,
@@ -7238,6 +7700,7 @@ export {
7238
7700
  PARTICIPANT_STANDING_ATTRIBUTE,
7239
7701
  REACTION_TTL_MS,
7240
7702
  REFUSED_ABSENCE,
7703
+ RESUME_WINDOW_MS,
7241
7704
  ROLE_RANK,
7242
7705
  ROOM_EVENTS,
7243
7706
  ROOM_EVENT_PREFIX,
@@ -7267,6 +7730,7 @@ export {
7267
7730
  attendanceTotals,
7268
7731
  attendeeKey,
7269
7732
  autoAssign,
7733
+ autoRejoinDelayMs,
7270
7734
  availableActivities,
7271
7735
  breakoutRemainingMs,
7272
7736
  callInviteAction,
@@ -7281,9 +7745,11 @@ export {
7281
7745
  clockPosition,
7282
7746
  collabFromState,
7283
7747
  computeStage,
7748
+ connectionPolicyFrom,
7284
7749
  countLabel,
7285
7750
  createCallCenter,
7286
7751
  createCallInviteHandler,
7752
+ createConnectionSupervisor,
7287
7753
  createDeviceManager,
7288
7754
  createInRoomClient,
7289
7755
  createLiveKitDriver,
@@ -7295,11 +7761,15 @@ export {
7295
7761
  createMeetingRecordCache,
7296
7762
  createMeetingSession,
7297
7763
  createMemoryDeviceStorage,
7764
+ createReachabilityProbe,
7765
+ createReconnectPolicy,
7298
7766
  createRecordingController,
7299
7767
  createRoomEngine,
7300
7768
  createTokenClient,
7301
7769
  createWebDeviceStorage,
7302
7770
  createWebEffectsStorage,
7771
+ createWebNetworkPort,
7772
+ createWebResumeStorage,
7303
7773
  createWebRoomTokenStorage,
7304
7774
  defaultCaptionLanguage,
7305
7775
  displayNameFor,
@@ -7312,6 +7782,7 @@ export {
7312
7782
  guestRecordAccessSetting,
7313
7783
  hasEnded,
7314
7784
  hostUserIds,
7785
+ httpUrlOf,
7315
7786
  invalidResponse,
7316
7787
  isGuest,
7317
7788
  isGuestIdentity,
@@ -7325,6 +7796,7 @@ export {
7325
7796
  isRealName,
7326
7797
  isRefusedPhase,
7327
7798
  isRoomEventTopic,
7799
+ isUnannouncedDeparture,
7328
7800
  isWaitingPhase,
7329
7801
  learnName,
7330
7802
  learnNames,
@@ -7346,6 +7818,7 @@ export {
7346
7818
  meetingTopic,
7347
7819
  meetingWhen,
7348
7820
  mergeCaption,
7821
+ mergeGhosts,
7349
7822
  mergeRecord,
7350
7823
  microphoneNoticeCode,
7351
7824
  moveToRoom,