@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/react.cjs CHANGED
@@ -38,6 +38,7 @@ __export(react_exports, {
38
38
  CAPTION_LANGUAGES: () => CAPTION_LANGUAGES,
39
39
  CONSENT_NOTICE_ATTRIBUTE: () => CONSENT_NOTICE_ATTRIBUTE,
40
40
  DATA_TOPICS: () => DATA_TOPICS,
41
+ DEFAULT_CONNECTION_POLICY: () => DEFAULT_CONNECTION_POLICY,
41
42
  DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
42
43
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
43
44
  EMPTY_COLLAB: () => EMPTY_COLLAB,
@@ -55,6 +56,7 @@ __export(react_exports, {
55
56
  IncomingCallHost: () => IncomingCallHost,
56
57
  JOIN_REFUSAL_REASONS: () => JOIN_REFUSAL_REASONS,
57
58
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
59
+ LIVEKIT_BASE_DELAYS_MS: () => LIVEKIT_BASE_DELAYS_MS,
58
60
  LIVE_PHASES: () => LIVE_PHASES,
59
61
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
60
62
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
@@ -84,6 +86,7 @@ __export(react_exports, {
84
86
  PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
85
87
  REACTION_TTL_MS: () => REACTION_TTL_MS,
86
88
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
89
+ RESUME_WINDOW_MS: () => RESUME_WINDOW_MS,
87
90
  ROLE_RANK: () => ROLE_RANK,
88
91
  ROOM_EVENTS: () => ROOM_EVENTS,
89
92
  ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
@@ -114,6 +117,7 @@ __export(react_exports, {
114
117
  attendanceTotals: () => attendanceTotals,
115
118
  attendeeKey: () => attendeeKey,
116
119
  autoAssign: () => autoAssign,
120
+ autoRejoinDelayMs: () => autoRejoinDelayMs,
117
121
  availableActivities: () => availableActivities,
118
122
  breakoutRemainingMs: () => breakoutRemainingMs,
119
123
  callInviteAction: () => callInviteAction,
@@ -128,9 +132,11 @@ __export(react_exports, {
128
132
  clockPosition: () => clockPosition,
129
133
  collabFromState: () => collabFromState,
130
134
  computeStage: () => computeStage,
135
+ connectionPolicyFrom: () => connectionPolicyFrom,
131
136
  countLabel: () => countLabel,
132
137
  createCallCenter: () => createCallCenter,
133
138
  createCallInviteHandler: () => createCallInviteHandler,
139
+ createConnectionSupervisor: () => createConnectionSupervisor,
134
140
  createDeviceManager: () => createDeviceManager,
135
141
  createInRoomClient: () => createInRoomClient,
136
142
  createLiveKitDriver: () => createLiveKitDriver,
@@ -142,11 +148,15 @@ __export(react_exports, {
142
148
  createMeetingRecordCache: () => createMeetingRecordCache,
143
149
  createMeetingSession: () => createMeetingSession,
144
150
  createMemoryDeviceStorage: () => createMemoryDeviceStorage,
151
+ createReachabilityProbe: () => createReachabilityProbe,
152
+ createReconnectPolicy: () => createReconnectPolicy,
145
153
  createRecordingController: () => createRecordingController,
146
154
  createRoomEngine: () => createRoomEngine,
147
155
  createTokenClient: () => createTokenClient,
148
156
  createWebDeviceStorage: () => createWebDeviceStorage,
149
157
  createWebEffectsStorage: () => createWebEffectsStorage,
158
+ createWebNetworkPort: () => createWebNetworkPort,
159
+ createWebResumeStorage: () => createWebResumeStorage,
150
160
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
151
161
  defaultCaptionLanguage: () => defaultCaptionLanguage,
152
162
  displayNameFor: () => displayNameFor,
@@ -159,6 +169,7 @@ __export(react_exports, {
159
169
  guestRecordAccessSetting: () => guestRecordAccessSetting,
160
170
  hasEnded: () => hasEnded,
161
171
  hostUserIds: () => hostUserIds,
172
+ httpUrlOf: () => httpUrlOf,
162
173
  invalidResponse: () => invalidResponse,
163
174
  isGuest: () => isGuest,
164
175
  isGuestIdentity: () => isGuestIdentity,
@@ -172,6 +183,7 @@ __export(react_exports, {
172
183
  isRealName: () => isRealName,
173
184
  isRefusedPhase: () => isRefusedPhase,
174
185
  isRoomEventTopic: () => isRoomEventTopic,
186
+ isUnannouncedDeparture: () => isUnannouncedDeparture,
175
187
  isWaitingPhase: () => isWaitingPhase,
176
188
  learnName: () => learnName,
177
189
  learnNames: () => learnNames,
@@ -194,6 +206,7 @@ __export(react_exports, {
194
206
  meetingTopic: () => meetingTopic,
195
207
  meetingWhen: () => meetingWhen,
196
208
  mergeCaption: () => mergeCaption,
209
+ mergeGhosts: () => mergeGhosts,
197
210
  mergeRecord: () => mergeRecord,
198
211
  microphoneNoticeCode: () => microphoneNoticeCode,
199
212
  moveToRoom: () => moveToRoom,
@@ -307,6 +320,352 @@ __export(react_exports, {
307
320
  });
308
321
  module.exports = __toCommonJS(react_exports);
309
322
 
323
+ // src/core/connection-supervisor.ts
324
+ var DEFAULT_CONNECTION_POLICY = {
325
+ bannerDelayMs: 2e3,
326
+ autoRejoin: true,
327
+ autoRejoinMaxAttempts: 5,
328
+ reconnectGraceSeconds: 30
329
+ };
330
+ function clampInt(value, min, max, fallback) {
331
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
332
+ return Math.min(max, Math.max(min, Math.round(value)));
333
+ }
334
+ function connectionPolicyFrom(raw) {
335
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
336
+ const row = raw;
337
+ const d = DEFAULT_CONNECTION_POLICY;
338
+ return {
339
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
340
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
341
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
342
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
343
+ };
344
+ }
345
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
346
+ function createReconnectPolicy(options = {}) {
347
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
348
+ const giveUpMs = options.giveUpMs ?? 45e3;
349
+ const maxAttempts = options.maxAttempts ?? 12;
350
+ const random = options.random ?? Math.random;
351
+ return {
352
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
353
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
354
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
355
+ const spread = Math.min(2e3, Math.max(500, base / 2));
356
+ return Math.round(base + random() * spread);
357
+ }
358
+ };
359
+ }
360
+ function createWebNetworkPort() {
361
+ const scope = globalThis;
362
+ return {
363
+ online: () => scope.navigator?.onLine !== false,
364
+ subscribe(listener) {
365
+ if (typeof scope.addEventListener !== "function") return () => void 0;
366
+ const on = () => listener(true);
367
+ const off = () => listener(false);
368
+ scope.addEventListener("online", on);
369
+ scope.addEventListener("offline", off);
370
+ return () => {
371
+ scope.removeEventListener?.("online", on);
372
+ scope.removeEventListener?.("offline", off);
373
+ };
374
+ }
375
+ };
376
+ }
377
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
378
+ return async () => {
379
+ const targets = urls().filter((url) => url.length > 0);
380
+ if (targets.length === 0 || typeof fetch !== "function") return true;
381
+ const one = async (url) => {
382
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
383
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
384
+ try {
385
+ await fetch(url, {
386
+ method: "GET",
387
+ mode: "no-cors",
388
+ cache: "no-store",
389
+ credentials: "omit",
390
+ ...controller !== null ? { signal: controller.signal } : {}
391
+ });
392
+ return true;
393
+ } catch {
394
+ return false;
395
+ } finally {
396
+ clearTimeout(timer);
397
+ }
398
+ };
399
+ const answers = await Promise.all(targets.map(one));
400
+ return answers.every(Boolean);
401
+ };
402
+ }
403
+ function httpUrlOf(serverUrl) {
404
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
405
+ }
406
+ var RESUME_WINDOW_MS = 15 * 6e4;
407
+ var RESUME_KEY = "mx-meet:resume:";
408
+ function createWebResumeStorage() {
409
+ const storage = globalThis.sessionStorage;
410
+ if (storage === void 0) return void 0;
411
+ return {
412
+ load(roomName) {
413
+ try {
414
+ const raw = storage.getItem(RESUME_KEY + roomName);
415
+ if (raw === null) return null;
416
+ const parsed = JSON.parse(raw);
417
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
418
+ } catch {
419
+ return null;
420
+ }
421
+ },
422
+ save(roomName, marker) {
423
+ try {
424
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
425
+ } catch {
426
+ }
427
+ },
428
+ clear(roomName) {
429
+ try {
430
+ storage.removeItem(RESUME_KEY + roomName);
431
+ } catch {
432
+ }
433
+ }
434
+ };
435
+ }
436
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
437
+ function autoRejoinDelayMs(attempt, random = Math.random) {
438
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
439
+ return Math.round(base * (0.5 + random()));
440
+ }
441
+ function createConnectionSupervisor(options) {
442
+ const { store, network } = options;
443
+ const now = options.now ?? (() => Date.now());
444
+ const random = options.random ?? Math.random;
445
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
446
+ let driverRegion = null;
447
+ let probeTimer = null;
448
+ let probing = false;
449
+ let bannerTimer = null;
450
+ let rejoinTimer = null;
451
+ let rejoinAttempts = 0;
452
+ let lastPhase = store.snapshot().phase;
453
+ let lastSavedAt = 0;
454
+ let disposed = false;
455
+ const inCall = () => store.snapshot().phase === "in_call";
456
+ const dropped = () => store.snapshot().phase === "disconnected";
457
+ function setRegion(next) {
458
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
459
+ }
460
+ function regionFor(reachable) {
461
+ if (!network.online()) return "offline";
462
+ if (reachable === false) return "server_unreachable";
463
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
464
+ return driverRegion ?? "stable";
465
+ }
466
+ function needsWatch() {
467
+ const snap = store.snapshot();
468
+ if (dropped()) return true;
469
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
470
+ }
471
+ function stopProbe() {
472
+ if (probeTimer !== null) clearTimeout(probeTimer);
473
+ probeTimer = null;
474
+ }
475
+ function scheduleProbe(delayMs) {
476
+ if (disposed || probeTimer !== null) return;
477
+ probeTimer = setTimeout(() => {
478
+ probeTimer = null;
479
+ void runProbe();
480
+ }, delayMs);
481
+ }
482
+ async function runProbe() {
483
+ if (disposed || probing) return;
484
+ if (!needsWatch()) return;
485
+ if (!network.online()) {
486
+ setRegion("offline");
487
+ scheduleProbe(probeEveryMs);
488
+ return;
489
+ }
490
+ probing = true;
491
+ let reachable = false;
492
+ try {
493
+ reachable = await options.probe();
494
+ } catch {
495
+ reachable = false;
496
+ } finally {
497
+ probing = false;
498
+ }
499
+ if (disposed) return;
500
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
501
+ if (dropped() && reachable) maybeRejoin();
502
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
503
+ }
504
+ function syncBanner() {
505
+ const snap = store.snapshot();
506
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
507
+ if (!unstable) {
508
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
509
+ bannerTimer = null;
510
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
511
+ return;
512
+ }
513
+ if (snap.connectionBanner || bannerTimer !== null) return;
514
+ const delay = options.policy().bannerDelayMs;
515
+ if (delay <= 0) {
516
+ store.patch({ connectionBanner: true });
517
+ return;
518
+ }
519
+ bannerTimer = setTimeout(() => {
520
+ bannerTimer = null;
521
+ const latest = store.snapshot();
522
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
523
+ store.patch({ connectionBanner: true });
524
+ }
525
+ }, delay);
526
+ }
527
+ function stopRejoin() {
528
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
529
+ rejoinTimer = null;
530
+ }
531
+ function maybeRejoin() {
532
+ if (disposed || rejoinTimer !== null || !dropped()) return;
533
+ const policy = options.policy();
534
+ if (!policy.autoRejoin) return;
535
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
536
+ if (!network.online()) return;
537
+ const attempt = rejoinAttempts;
538
+ rejoinTimer = setTimeout(() => {
539
+ rejoinTimer = null;
540
+ if (disposed || !dropped() || !network.online()) return;
541
+ rejoinAttempts = attempt + 1;
542
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
543
+ void options.autoRejoin().catch(() => void 0);
544
+ }, autoRejoinDelayMs(attempt, random));
545
+ }
546
+ function saveMarker(force) {
547
+ const resume = options.resume;
548
+ const snap = store.snapshot();
549
+ if (resume === void 0 || snap.roomName === null) return;
550
+ const at = now();
551
+ if (!force && at - lastSavedAt < 1e4) return;
552
+ const request = options.currentRequest();
553
+ if (request === null) return;
554
+ lastSavedAt = at;
555
+ resume.save(snap.roomName, { request, at });
556
+ }
557
+ const unsubscribeStore = store.subscribe(() => {
558
+ if (disposed) return;
559
+ const snap = store.snapshot();
560
+ const phase = snap.phase;
561
+ const changed = phase !== lastPhase;
562
+ lastPhase = phase;
563
+ if (changed) {
564
+ if (phase === "in_call") {
565
+ rejoinAttempts = 0;
566
+ stopRejoin();
567
+ saveMarker(true);
568
+ } else if (phase === "disconnected") {
569
+ driverRegion = null;
570
+ saveMarker(true);
571
+ stopProbe();
572
+ scheduleProbe(0);
573
+ } else {
574
+ stopRejoin();
575
+ if (phase !== "joining") driverRegion = null;
576
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
577
+ }
578
+ } else if (phase === "in_call" || phase === "disconnected") {
579
+ saveMarker(false);
580
+ }
581
+ syncBanner();
582
+ });
583
+ const unsubscribeNetwork = network.subscribe((online) => {
584
+ if (disposed) return;
585
+ if (!online) {
586
+ if (inCall() || dropped()) setRegion("offline");
587
+ stopRejoin();
588
+ stopProbe();
589
+ if (needsWatch()) scheduleProbe(probeEveryMs);
590
+ return;
591
+ }
592
+ stopProbe();
593
+ if (needsWatch()) scheduleProbe(0);
594
+ });
595
+ return {
596
+ driverState(state) {
597
+ if (disposed) return;
598
+ if (state === "connected") {
599
+ driverRegion = null;
600
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
601
+ if (!needsWatch()) stopProbe();
602
+ return;
603
+ }
604
+ if (state === "reconnecting" || state === "signal_reconnecting") {
605
+ driverRegion = state;
606
+ if (inCall()) {
607
+ setRegion(network.online() ? state : "offline");
608
+ stopProbe();
609
+ scheduleProbe(0);
610
+ }
611
+ }
612
+ },
613
+ exitConnection() {
614
+ return network.online() ? "server_unreachable" : "offline";
615
+ },
616
+ resumable(roomName) {
617
+ const resume = options.resume;
618
+ if (resume === void 0) return null;
619
+ const marker = resume.load(roomName);
620
+ if (marker === null) return null;
621
+ if (now() - marker.at > RESUME_WINDOW_MS) {
622
+ resume.clear(roomName);
623
+ return null;
624
+ }
625
+ return marker;
626
+ },
627
+ dispose() {
628
+ disposed = true;
629
+ stopProbe();
630
+ stopRejoin();
631
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
632
+ bannerTimer = null;
633
+ unsubscribeStore();
634
+ unsubscribeNetwork();
635
+ }
636
+ };
637
+ }
638
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
639
+ "CLIENT_INITIATED",
640
+ "PARTICIPANT_REMOVED",
641
+ "ROOM_DELETED",
642
+ "ROOM_CLOSED",
643
+ "DUPLICATE_IDENTITY",
644
+ "USER_REJECTED",
645
+ "USER_UNAVAILABLE",
646
+ "SIP_TRUNK_FAILURE"
647
+ ]);
648
+ function isUnannouncedDeparture(reason) {
649
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
650
+ }
651
+ function mergeGhosts(live, ghosts, at) {
652
+ const present = new Set(live.map((p) => p.identity));
653
+ const kept = /* @__PURE__ */ new Map();
654
+ const extra = [];
655
+ for (const [identity, ghost] of ghosts) {
656
+ if (present.has(identity) || ghost.until <= at) continue;
657
+ kept.set(identity, ghost);
658
+ extra.push({
659
+ ...ghost.participant,
660
+ micEnabled: false,
661
+ cameraEnabled: false,
662
+ screenSharing: false,
663
+ reconnecting: true
664
+ });
665
+ }
666
+ return { roster: [...live, ...extra], kept };
667
+ }
668
+
310
669
  // src/react/provider.tsx
311
670
  var import_global_slot2 = require("@ai-matrx/kit/global-slot");
312
671
  var import_react = require("react");
@@ -703,7 +1062,8 @@ function parseRoomToken(body, roomName) {
703
1062
  admissionState: readString(payload, "admission_state") ?? void 0,
704
1063
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
705
1064
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
706
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
1065
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1066
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
707
1067
  };
708
1068
  }
709
1069
  function createTokenClient(options) {
@@ -1408,6 +1768,10 @@ function nextPhase(from, event) {
1408
1768
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1409
1769
  case "join":
1410
1770
  return JOINABLE_FROM.has(from) ? "joining" : null;
1771
+ case "auto_rejoin":
1772
+ return from === "disconnected" ? "joining" : null;
1773
+ case "resumed":
1774
+ return from === "resolving" ? "joining" : null;
1411
1775
  case "back_to_prejoin":
1412
1776
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1413
1777
  case "retry":
@@ -3547,6 +3911,7 @@ var EMPTY = {
3547
3911
  phaseNotice: null,
3548
3912
  roomName: null,
3549
3913
  connection: "stable",
3914
+ connectionBanner: false,
3550
3915
  share: NO_SHARE,
3551
3916
  call: null,
3552
3917
  waiting: null,
@@ -4826,6 +5191,18 @@ function createRoomEngine(options) {
4826
5191
  let disposed = false;
4827
5192
  let moving = false;
4828
5193
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5194
+ let lastServerUrl = null;
5195
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
5196
+ let ghosts = /* @__PURE__ */ new Map();
5197
+ let ghostTimer = null;
5198
+ let liveRoster = [];
5199
+ const departures = /* @__PURE__ */ new Map();
5200
+ function requestAsNow() {
5201
+ if (lastRequest === null) return null;
5202
+ const media = store.snapshot().media;
5203
+ if (store.snapshot().phase !== "in_call") return lastRequest;
5204
+ return { ...lastRequest, mode: media.camera.enabled ? "video" : "audio", microphone: media.microphone.enabled };
5205
+ }
4829
5206
  async function dropRoom() {
4830
5207
  const target = driver;
4831
5208
  driver = null;
@@ -4836,6 +5213,7 @@ function createRoomEngine(options) {
4836
5213
  store.setLocalTrack("microphone", IDLE_TRACK);
4837
5214
  store.setLocalTrack("camera", IDLE_TRACK);
4838
5215
  store.patch({ share: NO_SHARE, connection: "stable" });
5216
+ clearGhosts();
4839
5217
  await target?.disconnect();
4840
5218
  }
4841
5219
  async function leaveRoom() {
@@ -4903,7 +5281,7 @@ function createRoomEngine(options) {
4903
5281
  participants(list) {
4904
5282
  const joinedAt = new Date(now()).toISOString();
4905
5283
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
4906
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
5284
+ projectRoster(list, joinedAt);
4907
5285
  const snapshot = store.snapshot();
4908
5286
  const newcomer = snapshot.participants.some(
4909
5287
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -4915,6 +5293,9 @@ function createRoomEngine(options) {
4915
5293
  );
4916
5294
  }
4917
5295
  },
5296
+ participantLeft(who, reason) {
5297
+ departures.set(who, reason);
5298
+ },
4918
5299
  roomMetadata(metadata) {
4919
5300
  let spotlight = null;
4920
5301
  if (metadata.length > 0) {
@@ -4941,12 +5322,8 @@ function createRoomEngine(options) {
4941
5322
  store.setQuality(asParticipantIdentity(who), value);
4942
5323
  },
4943
5324
  connectionState(state) {
4944
- if (state === "connected") {
4945
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4946
- store.dispatch({ type: "connected" });
4947
- } else if (state === "reconnecting") {
4948
- store.patch({ connection: "reconnecting" });
4949
- }
5325
+ supervisor.driverState(state);
5326
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
4950
5327
  },
4951
5328
  transcription(event) {
4952
5329
  const snapshot = store.snapshot();
@@ -5079,6 +5456,7 @@ function createRoomEngine(options) {
5079
5456
  };
5080
5457
  async function resolveExitAndApply(hint) {
5081
5458
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5459
+ lastRequest = requestAsNow();
5082
5460
  await dropRoom();
5083
5461
  let server;
5084
5462
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5092,7 +5470,7 @@ function createRoomEngine(options) {
5092
5470
  }
5093
5471
  if (disposed) return;
5094
5472
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5095
- store.patch({ connection: resolved.connection });
5473
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
5096
5474
  if (resolved.to === "waiting:knocking") {
5097
5475
  store.patch({
5098
5476
  waiting: {
@@ -5124,6 +5502,8 @@ function createRoomEngine(options) {
5124
5502
  ...request.observe === true ? { observe: true } : {}
5125
5503
  });
5126
5504
  current = token;
5505
+ lastServerUrl = token.serverUrl;
5506
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5127
5507
  store.patch({
5128
5508
  localIdentity: token.identity,
5129
5509
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -5228,7 +5608,59 @@ function createRoomEngine(options) {
5228
5608
  throw normalized;
5229
5609
  }
5230
5610
  }
5231
- return {
5611
+ function projectRoster(list, joinedAt) {
5612
+ const at = now();
5613
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
5614
+ const present = new Set(list.map((p) => p.identity));
5615
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
5616
+ for (const [who, reason] of departures) {
5617
+ const last = previous.get(who);
5618
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
5619
+ ghosts.set(who, { participant: last, until: at + graceMs });
5620
+ }
5621
+ }
5622
+ departures.clear();
5623
+ liveRoster = list;
5624
+ const merged = mergeGhosts(list, ghosts, at);
5625
+ ghosts = merged.kept;
5626
+ store.setParticipants(
5627
+ merged.roster.map((entry) => {
5628
+ const projected = projectParticipant(entry, joinedAt);
5629
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
5630
+ })
5631
+ );
5632
+ scheduleGhostExpiry();
5633
+ }
5634
+ function scheduleGhostExpiry() {
5635
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
5636
+ ghostTimer = null;
5637
+ if (ghosts.size === 0 || disposed) return;
5638
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
5639
+ ghostTimer = setTimeout(() => {
5640
+ ghostTimer = null;
5641
+ if (disposed || driver === null) return;
5642
+ projectRoster(liveRoster, new Date(now()).toISOString());
5643
+ }, Math.max(0, next - now()) + 50);
5644
+ }
5645
+ function clearGhosts() {
5646
+ ghosts = /* @__PURE__ */ new Map();
5647
+ departures.clear();
5648
+ liveRoster = [];
5649
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
5650
+ ghostTimer = null;
5651
+ }
5652
+ const supervisor = createConnectionSupervisor({
5653
+ store,
5654
+ network: options.connection?.network ?? createWebNetworkPort(),
5655
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
5656
+ policy: () => connectionPolicy,
5657
+ autoRejoin: () => engine.autoRejoin(),
5658
+ currentRequest: requestAsNow,
5659
+ resume: options.connection?.resume,
5660
+ now,
5661
+ report: (level, message) => report(level, message)
5662
+ });
5663
+ const engine = {
5232
5664
  join(request) {
5233
5665
  if (joining !== null) return joining;
5234
5666
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -5273,6 +5705,35 @@ function createRoomEngine(options) {
5273
5705
  });
5274
5706
  return joining;
5275
5707
  },
5708
+ async autoRejoin() {
5709
+ const request = lastRequest;
5710
+ if (request === null || joining !== null) return joining ?? void 0;
5711
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
5712
+ tokens.invalidate(request.roomName);
5713
+ current = null;
5714
+ joining = connect(request).catch((error) => {
5715
+ throw failJoin(error, "meet.autoRejoin");
5716
+ }).finally(() => {
5717
+ joining = null;
5718
+ });
5719
+ return joining;
5720
+ },
5721
+ resume(roomName) {
5722
+ const marker = supervisor.resumable(roomName);
5723
+ if (marker === null || joining !== null) return false;
5724
+ if (!store.dispatch({ type: "resumed" })) return false;
5725
+ lastRequest = { ...marker.request, roomName };
5726
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
5727
+ tokens.invalidate(roomName);
5728
+ current = null;
5729
+ joining = connect(lastRequest).catch((error) => {
5730
+ throw failJoin(error, "meet.resume");
5731
+ }).finally(() => {
5732
+ joining = null;
5733
+ });
5734
+ joining.catch(() => void 0);
5735
+ return true;
5736
+ },
5276
5737
  async moveToBreakout(roomId) {
5277
5738
  const request = lastRequest;
5278
5739
  if (request === null || driver === null) {
@@ -5440,6 +5901,8 @@ function createRoomEngine(options) {
5440
5901
  token: () => current,
5441
5902
  async dispose() {
5442
5903
  disposed = true;
5904
+ supervisor.dispose();
5905
+ clearGhosts();
5443
5906
  if (driver !== null) {
5444
5907
  const target = driver;
5445
5908
  driver = null;
@@ -5448,6 +5911,7 @@ function createRoomEngine(options) {
5448
5911
  store.reset();
5449
5912
  }
5450
5913
  };
5914
+ return engine;
5451
5915
  }
5452
5916
  function canModerate(participant) {
5453
5917
  return participant !== void 0 && isHostRole(participant.role);
@@ -5758,8 +6222,11 @@ function mapConnectionState(state) {
5758
6222
  case "connecting":
5759
6223
  return "connecting";
5760
6224
  case "reconnecting":
5761
- case "signalReconnecting":
5762
6225
  return "reconnecting";
6226
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
6227
+ // signal blip from a full media reconnect.
6228
+ case "signalReconnecting":
6229
+ return "signal_reconnecting";
5763
6230
  default:
5764
6231
  return "disconnected";
5765
6232
  }
@@ -5840,8 +6307,12 @@ function createLiveKitDriver(handlers, options = {}) {
5840
6307
  // R5: large meetings degrade gracefully instead of melting a laptop.
5841
6308
  adaptiveStream: true,
5842
6309
  dynacast: true,
6310
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
6311
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
6312
+ reconnectPolicy: createReconnectPolicy(),
5843
6313
  ...options.roomOptions
5844
6314
  });
6315
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module2.DisconnectReason?.[reason] ?? String(reason);
5845
6316
  const roster = () => {
5846
6317
  const local = snapshotParticipant(created.localParticipant, true);
5847
6318
  const remotes = [...created.remoteParticipants.values()].map(
@@ -5859,7 +6330,10 @@ function createLiveKitDriver(handlers, options = {}) {
5859
6330
  if (track.kind !== Track.Kind.Audio) return;
5860
6331
  for (const element of track.detach()) element.remove();
5861
6332
  });
5862
- 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) => {
6333
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
6334
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
6335
+ roster();
6336
+ }).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) => {
5863
6337
  handlers.roomMetadata?.(metadata ?? "");
5864
6338
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
5865
6339
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -5876,8 +6350,7 @@ function createLiveKitDriver(handlers, options = {}) {
5876
6350
  }).on(RoomEvent.Disconnected, (reason) => {
5877
6351
  audioContainer?.remove();
5878
6352
  audioContainer = null;
5879
- const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
5880
- handlers.disconnected(named);
6353
+ handlers.disconnected(reasonName(reason));
5881
6354
  });
5882
6355
  try {
5883
6356
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -6267,7 +6740,14 @@ function MeetRuntime(props) {
6267
6740
  effectsStorage,
6268
6741
  resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
6269
6742
  // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
6270
- readExit: (connection) => meetings.readExit(connection)
6743
+ readExit: (connection) => meetings.readExit(connection),
6744
+ // S6b: the connection supervisor's ports — the device's network, whether OUR servers (the
6745
+ // API and the media server) answer, and the per-tab reload-after-offline marker.
6746
+ connection: {
6747
+ network: createWebNetworkPort(),
6748
+ resume: createWebResumeStorage(),
6749
+ probe: (serverUrl) => createReachabilityProbe(() => [baseUrl, ...serverUrl !== null ? [httpUrlOf(serverUrl)] : []])()
6750
+ }
6271
6751
  });
6272
6752
  const meetings = createMeetingSession({
6273
6753
  manager,
@@ -6595,7 +7075,7 @@ function participantAttributes(p) {
6595
7075
  "data-meet-participant-role": observedRole(p.role, p.standing),
6596
7076
  "data-meet-participant-self": String(p.isLocal)
6597
7077
  };
6598
- const quality = observedQuality(p.quality);
7078
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
6599
7079
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
6600
7080
  return attrs;
6601
7081
  }
@@ -8814,6 +9294,7 @@ function useMeetCore() {
8814
9294
  reason: snapshot?.reason ?? null,
8815
9295
  phaseNotice: snapshot?.phaseNotice ?? null,
8816
9296
  connection: snapshot?.connection ?? "stable",
9297
+ connectionBanner: snapshot?.connectionBanner ?? false,
8817
9298
  share: snapshot?.share ?? NO_SHARE,
8818
9299
  call: snapshot?.call ?? null,
8819
9300
  actions
@@ -10080,7 +10561,8 @@ function ParticipantTile({
10080
10561
  participant.displayName,
10081
10562
  participant.isLocal ? " (you)" : "",
10082
10563
  participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
10083
- /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RoleBadge, { role: participant.role, standing: participant.standing })
10564
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(RoleBadge, { role: participant.role, standing: participant.standing }),
10565
+ participant.reconnecting === true ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null
10084
10566
  ] }),
10085
10567
  !participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
10086
10568
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QualityBars, { quality: participant.quality })
@@ -10123,6 +10605,12 @@ function QualityBars({ quality }) {
10123
10605
  const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
10124
10606
  return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SignalIcon, { bars, width: 14, height: 14 }) });
10125
10607
  }
10608
+ var CONNECTION_BANNER = {
10609
+ signal_reconnecting: "Reconnecting\u2026",
10610
+ reconnecting: "Reconnecting\u2026",
10611
+ offline: "You're offline. Reconnecting when your internet is back\u2026",
10612
+ server_unreachable: "Can't reach the meeting server. Retrying\u2026"
10613
+ };
10126
10614
  function Stage({ pageSize }) {
10127
10615
  const stage = useStage(pageSize);
10128
10616
  const columns = gridColumns(stage.tiles.length);
@@ -11830,7 +12318,7 @@ function MeetSkinRoom({
11830
12318
  headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
11831
12319
  isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HostMenu, {}) : null
11832
12320
  ] }),
11833
- core.connection !== "stable" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
12321
+ core.connectionBanner && core.connection !== "stable" ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": `connection-${core.connection}`, children: CONNECTION_BANNER[core.connection] }) : null,
11834
12322
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ShareNotice, {}),
11835
12323
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Stage, {}),
11836
12324
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Captions, {}),
@@ -11864,7 +12352,7 @@ var REFUSED_TITLES = {
11864
12352
  registration_required: "Register to join this meeting"
11865
12353
  };
11866
12354
  function ExitScreen({ onLeave }) {
11867
- const { phase, reason, host, snapshot, actions, call } = useMeetCore();
12355
+ const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
11868
12356
  const [busy, setBusy] = (0, import_react10.useState)(false);
11869
12357
  const failure = snapshot?.failure ?? null;
11870
12358
  const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -11920,7 +12408,7 @@ function ExitScreen({ onLeave }) {
11920
12408
  break;
11921
12409
  case "disconnected":
11922
12410
  title = "You lost your connection to the meeting";
11923
- line = failure?.remedy ?? "Check your internet connection, then rejoin.";
12411
+ line = connection === "offline" ? "You're offline. You'll rejoin automatically when your internet is back." : connection === "server_unreachable" ? "Can't reach the meeting server. You'll rejoin automatically when it answers." : failure?.remedy ?? "Check your internet connection, then rejoin.";
11924
12412
  notice = "disconnected";
11925
12413
  actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11926
12414
  rejoin,
@@ -12529,6 +13017,7 @@ function MeetingSkinRootBody({
12529
13017
  host.store.dispatch({ type: "reset" });
12530
13018
  }
12531
13019
  if (host.store.snapshot().phase !== "resolving") return;
13020
+ if (given?.linkRun?.reason == null && host.engine.resume(roomName)) return;
12532
13021
  const reason = given?.linkRun?.reason ?? null;
12533
13022
  if (reason !== null && given?.linkRun !== void 0) {
12534
13023
  const words = linkRunWords(given.linkRun);