@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/index.cjs CHANGED
@@ -36,6 +36,7 @@ __export(src_exports, {
36
36
  CAPTION_LANGUAGES: () => CAPTION_LANGUAGES,
37
37
  CONSENT_NOTICE_ATTRIBUTE: () => CONSENT_NOTICE_ATTRIBUTE,
38
38
  DATA_TOPICS: () => DATA_TOPICS,
39
+ DEFAULT_CONNECTION_POLICY: () => DEFAULT_CONNECTION_POLICY,
39
40
  DEFAULT_EFFECTS: () => DEFAULT_EFFECTS,
40
41
  DEFAULT_PAGE_SIZE: () => DEFAULT_PAGE_SIZE,
41
42
  EMPTY_COLLAB: () => EMPTY_COLLAB,
@@ -50,6 +51,7 @@ __export(src_exports, {
50
51
  IDLE_TRACK: () => IDLE_TRACK,
51
52
  JOIN_REFUSAL_REASONS: () => JOIN_REFUSAL_REASONS,
52
53
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
54
+ LIVEKIT_BASE_DELAYS_MS: () => LIVEKIT_BASE_DELAYS_MS,
53
55
  LIVE_PHASES: () => LIVE_PHASES,
54
56
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
55
57
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
@@ -73,6 +75,7 @@ __export(src_exports, {
73
75
  PARTICIPANT_STANDING_ATTRIBUTE: () => PARTICIPANT_STANDING_ATTRIBUTE,
74
76
  REACTION_TTL_MS: () => REACTION_TTL_MS,
75
77
  REFUSED_ABSENCE: () => REFUSED_ABSENCE,
78
+ RESUME_WINDOW_MS: () => RESUME_WINDOW_MS,
76
79
  ROLE_RANK: () => ROLE_RANK,
77
80
  ROOM_EVENTS: () => ROOM_EVENTS,
78
81
  ROOM_EVENT_PREFIX: () => ROOM_EVENT_PREFIX,
@@ -102,6 +105,7 @@ __export(src_exports, {
102
105
  attendanceTotals: () => attendanceTotals,
103
106
  attendeeKey: () => attendeeKey,
104
107
  autoAssign: () => autoAssign,
108
+ autoRejoinDelayMs: () => autoRejoinDelayMs,
105
109
  availableActivities: () => availableActivities,
106
110
  breakoutRemainingMs: () => breakoutRemainingMs,
107
111
  callInviteAction: () => callInviteAction,
@@ -116,9 +120,11 @@ __export(src_exports, {
116
120
  clockPosition: () => clockPosition,
117
121
  collabFromState: () => collabFromState,
118
122
  computeStage: () => computeStage,
123
+ connectionPolicyFrom: () => connectionPolicyFrom,
119
124
  countLabel: () => countLabel,
120
125
  createCallCenter: () => createCallCenter,
121
126
  createCallInviteHandler: () => createCallInviteHandler,
127
+ createConnectionSupervisor: () => createConnectionSupervisor,
122
128
  createDeviceManager: () => createDeviceManager,
123
129
  createInRoomClient: () => createInRoomClient,
124
130
  createLiveKitDriver: () => createLiveKitDriver,
@@ -130,11 +136,15 @@ __export(src_exports, {
130
136
  createMeetingRecordCache: () => createMeetingRecordCache,
131
137
  createMeetingSession: () => createMeetingSession,
132
138
  createMemoryDeviceStorage: () => createMemoryDeviceStorage,
139
+ createReachabilityProbe: () => createReachabilityProbe,
140
+ createReconnectPolicy: () => createReconnectPolicy,
133
141
  createRecordingController: () => createRecordingController,
134
142
  createRoomEngine: () => createRoomEngine,
135
143
  createTokenClient: () => createTokenClient,
136
144
  createWebDeviceStorage: () => createWebDeviceStorage,
137
145
  createWebEffectsStorage: () => createWebEffectsStorage,
146
+ createWebNetworkPort: () => createWebNetworkPort,
147
+ createWebResumeStorage: () => createWebResumeStorage,
138
148
  createWebRoomTokenStorage: () => createWebRoomTokenStorage,
139
149
  defaultCaptionLanguage: () => defaultCaptionLanguage,
140
150
  displayNameFor: () => displayNameFor,
@@ -147,6 +157,7 @@ __export(src_exports, {
147
157
  guestRecordAccessSetting: () => guestRecordAccessSetting,
148
158
  hasEnded: () => hasEnded,
149
159
  hostUserIds: () => hostUserIds,
160
+ httpUrlOf: () => httpUrlOf,
150
161
  invalidResponse: () => invalidResponse,
151
162
  isGuest: () => isGuest,
152
163
  isGuestIdentity: () => isGuestIdentity,
@@ -160,6 +171,7 @@ __export(src_exports, {
160
171
  isRealName: () => isRealName,
161
172
  isRefusedPhase: () => isRefusedPhase,
162
173
  isRoomEventTopic: () => isRoomEventTopic,
174
+ isUnannouncedDeparture: () => isUnannouncedDeparture,
163
175
  isWaitingPhase: () => isWaitingPhase,
164
176
  learnName: () => learnName,
165
177
  learnNames: () => learnNames,
@@ -181,6 +193,7 @@ __export(src_exports, {
181
193
  meetingTopic: () => meetingTopic,
182
194
  meetingWhen: () => meetingWhen,
183
195
  mergeCaption: () => mergeCaption,
196
+ mergeGhosts: () => mergeGhosts,
184
197
  mergeRecord: () => mergeRecord,
185
198
  microphoneNoticeCode: () => microphoneNoticeCode,
186
199
  moveToRoom: () => moveToRoom,
@@ -456,6 +469,352 @@ function invalidResponse(operation, detail) {
456
469
  );
457
470
  }
458
471
 
472
+ // src/core/connection-supervisor.ts
473
+ var DEFAULT_CONNECTION_POLICY = {
474
+ bannerDelayMs: 2e3,
475
+ autoRejoin: true,
476
+ autoRejoinMaxAttempts: 5,
477
+ reconnectGraceSeconds: 30
478
+ };
479
+ function clampInt(value, min, max, fallback) {
480
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
481
+ return Math.min(max, Math.max(min, Math.round(value)));
482
+ }
483
+ function connectionPolicyFrom(raw) {
484
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
485
+ const row = raw;
486
+ const d = DEFAULT_CONNECTION_POLICY;
487
+ return {
488
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
489
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
490
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
491
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
492
+ };
493
+ }
494
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
495
+ function createReconnectPolicy(options = {}) {
496
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
497
+ const giveUpMs = options.giveUpMs ?? 45e3;
498
+ const maxAttempts = options.maxAttempts ?? 12;
499
+ const random = options.random ?? Math.random;
500
+ return {
501
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
502
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
503
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
504
+ const spread = Math.min(2e3, Math.max(500, base / 2));
505
+ return Math.round(base + random() * spread);
506
+ }
507
+ };
508
+ }
509
+ function createWebNetworkPort() {
510
+ const scope = globalThis;
511
+ return {
512
+ online: () => scope.navigator?.onLine !== false,
513
+ subscribe(listener) {
514
+ if (typeof scope.addEventListener !== "function") return () => void 0;
515
+ const on = () => listener(true);
516
+ const off = () => listener(false);
517
+ scope.addEventListener("online", on);
518
+ scope.addEventListener("offline", off);
519
+ return () => {
520
+ scope.removeEventListener?.("online", on);
521
+ scope.removeEventListener?.("offline", off);
522
+ };
523
+ }
524
+ };
525
+ }
526
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
527
+ return async () => {
528
+ const targets = urls().filter((url) => url.length > 0);
529
+ if (targets.length === 0 || typeof fetch !== "function") return true;
530
+ const one = async (url) => {
531
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
532
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
533
+ try {
534
+ await fetch(url, {
535
+ method: "GET",
536
+ mode: "no-cors",
537
+ cache: "no-store",
538
+ credentials: "omit",
539
+ ...controller !== null ? { signal: controller.signal } : {}
540
+ });
541
+ return true;
542
+ } catch {
543
+ return false;
544
+ } finally {
545
+ clearTimeout(timer);
546
+ }
547
+ };
548
+ const answers = await Promise.all(targets.map(one));
549
+ return answers.every(Boolean);
550
+ };
551
+ }
552
+ function httpUrlOf(serverUrl) {
553
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
554
+ }
555
+ var RESUME_WINDOW_MS = 15 * 6e4;
556
+ var RESUME_KEY = "mx-meet:resume:";
557
+ function createWebResumeStorage() {
558
+ const storage = globalThis.sessionStorage;
559
+ if (storage === void 0) return void 0;
560
+ return {
561
+ load(roomName) {
562
+ try {
563
+ const raw = storage.getItem(RESUME_KEY + roomName);
564
+ if (raw === null) return null;
565
+ const parsed = JSON.parse(raw);
566
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
567
+ } catch {
568
+ return null;
569
+ }
570
+ },
571
+ save(roomName, marker) {
572
+ try {
573
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
574
+ } catch {
575
+ }
576
+ },
577
+ clear(roomName) {
578
+ try {
579
+ storage.removeItem(RESUME_KEY + roomName);
580
+ } catch {
581
+ }
582
+ }
583
+ };
584
+ }
585
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
586
+ function autoRejoinDelayMs(attempt, random = Math.random) {
587
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
588
+ return Math.round(base * (0.5 + random()));
589
+ }
590
+ function createConnectionSupervisor(options) {
591
+ const { store, network } = options;
592
+ const now = options.now ?? (() => Date.now());
593
+ const random = options.random ?? Math.random;
594
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
595
+ let driverRegion = null;
596
+ let probeTimer = null;
597
+ let probing = false;
598
+ let bannerTimer = null;
599
+ let rejoinTimer = null;
600
+ let rejoinAttempts = 0;
601
+ let lastPhase = store.snapshot().phase;
602
+ let lastSavedAt = 0;
603
+ let disposed = false;
604
+ const inCall = () => store.snapshot().phase === "in_call";
605
+ const dropped = () => store.snapshot().phase === "disconnected";
606
+ function setRegion(next) {
607
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
608
+ }
609
+ function regionFor(reachable) {
610
+ if (!network.online()) return "offline";
611
+ if (reachable === false) return "server_unreachable";
612
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
613
+ return driverRegion ?? "stable";
614
+ }
615
+ function needsWatch() {
616
+ const snap = store.snapshot();
617
+ if (dropped()) return true;
618
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
619
+ }
620
+ function stopProbe() {
621
+ if (probeTimer !== null) clearTimeout(probeTimer);
622
+ probeTimer = null;
623
+ }
624
+ function scheduleProbe(delayMs) {
625
+ if (disposed || probeTimer !== null) return;
626
+ probeTimer = setTimeout(() => {
627
+ probeTimer = null;
628
+ void runProbe();
629
+ }, delayMs);
630
+ }
631
+ async function runProbe() {
632
+ if (disposed || probing) return;
633
+ if (!needsWatch()) return;
634
+ if (!network.online()) {
635
+ setRegion("offline");
636
+ scheduleProbe(probeEveryMs);
637
+ return;
638
+ }
639
+ probing = true;
640
+ let reachable = false;
641
+ try {
642
+ reachable = await options.probe();
643
+ } catch {
644
+ reachable = false;
645
+ } finally {
646
+ probing = false;
647
+ }
648
+ if (disposed) return;
649
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
650
+ if (dropped() && reachable) maybeRejoin();
651
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
652
+ }
653
+ function syncBanner() {
654
+ const snap = store.snapshot();
655
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
656
+ if (!unstable) {
657
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
658
+ bannerTimer = null;
659
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
660
+ return;
661
+ }
662
+ if (snap.connectionBanner || bannerTimer !== null) return;
663
+ const delay = options.policy().bannerDelayMs;
664
+ if (delay <= 0) {
665
+ store.patch({ connectionBanner: true });
666
+ return;
667
+ }
668
+ bannerTimer = setTimeout(() => {
669
+ bannerTimer = null;
670
+ const latest = store.snapshot();
671
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
672
+ store.patch({ connectionBanner: true });
673
+ }
674
+ }, delay);
675
+ }
676
+ function stopRejoin() {
677
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
678
+ rejoinTimer = null;
679
+ }
680
+ function maybeRejoin() {
681
+ if (disposed || rejoinTimer !== null || !dropped()) return;
682
+ const policy = options.policy();
683
+ if (!policy.autoRejoin) return;
684
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
685
+ if (!network.online()) return;
686
+ const attempt = rejoinAttempts;
687
+ rejoinTimer = setTimeout(() => {
688
+ rejoinTimer = null;
689
+ if (disposed || !dropped() || !network.online()) return;
690
+ rejoinAttempts = attempt + 1;
691
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
692
+ void options.autoRejoin().catch(() => void 0);
693
+ }, autoRejoinDelayMs(attempt, random));
694
+ }
695
+ function saveMarker(force) {
696
+ const resume = options.resume;
697
+ const snap = store.snapshot();
698
+ if (resume === void 0 || snap.roomName === null) return;
699
+ const at = now();
700
+ if (!force && at - lastSavedAt < 1e4) return;
701
+ const request = options.currentRequest();
702
+ if (request === null) return;
703
+ lastSavedAt = at;
704
+ resume.save(snap.roomName, { request, at });
705
+ }
706
+ const unsubscribeStore = store.subscribe(() => {
707
+ if (disposed) return;
708
+ const snap = store.snapshot();
709
+ const phase = snap.phase;
710
+ const changed = phase !== lastPhase;
711
+ lastPhase = phase;
712
+ if (changed) {
713
+ if (phase === "in_call") {
714
+ rejoinAttempts = 0;
715
+ stopRejoin();
716
+ saveMarker(true);
717
+ } else if (phase === "disconnected") {
718
+ driverRegion = null;
719
+ saveMarker(true);
720
+ stopProbe();
721
+ scheduleProbe(0);
722
+ } else {
723
+ stopRejoin();
724
+ if (phase !== "joining") driverRegion = null;
725
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
726
+ }
727
+ } else if (phase === "in_call" || phase === "disconnected") {
728
+ saveMarker(false);
729
+ }
730
+ syncBanner();
731
+ });
732
+ const unsubscribeNetwork = network.subscribe((online) => {
733
+ if (disposed) return;
734
+ if (!online) {
735
+ if (inCall() || dropped()) setRegion("offline");
736
+ stopRejoin();
737
+ stopProbe();
738
+ if (needsWatch()) scheduleProbe(probeEveryMs);
739
+ return;
740
+ }
741
+ stopProbe();
742
+ if (needsWatch()) scheduleProbe(0);
743
+ });
744
+ return {
745
+ driverState(state) {
746
+ if (disposed) return;
747
+ if (state === "connected") {
748
+ driverRegion = null;
749
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
750
+ if (!needsWatch()) stopProbe();
751
+ return;
752
+ }
753
+ if (state === "reconnecting" || state === "signal_reconnecting") {
754
+ driverRegion = state;
755
+ if (inCall()) {
756
+ setRegion(network.online() ? state : "offline");
757
+ stopProbe();
758
+ scheduleProbe(0);
759
+ }
760
+ }
761
+ },
762
+ exitConnection() {
763
+ return network.online() ? "server_unreachable" : "offline";
764
+ },
765
+ resumable(roomName) {
766
+ const resume = options.resume;
767
+ if (resume === void 0) return null;
768
+ const marker = resume.load(roomName);
769
+ if (marker === null) return null;
770
+ if (now() - marker.at > RESUME_WINDOW_MS) {
771
+ resume.clear(roomName);
772
+ return null;
773
+ }
774
+ return marker;
775
+ },
776
+ dispose() {
777
+ disposed = true;
778
+ stopProbe();
779
+ stopRejoin();
780
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
781
+ bannerTimer = null;
782
+ unsubscribeStore();
783
+ unsubscribeNetwork();
784
+ }
785
+ };
786
+ }
787
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
788
+ "CLIENT_INITIATED",
789
+ "PARTICIPANT_REMOVED",
790
+ "ROOM_DELETED",
791
+ "ROOM_CLOSED",
792
+ "DUPLICATE_IDENTITY",
793
+ "USER_REJECTED",
794
+ "USER_UNAVAILABLE",
795
+ "SIP_TRUNK_FAILURE"
796
+ ]);
797
+ function isUnannouncedDeparture(reason) {
798
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
799
+ }
800
+ function mergeGhosts(live, ghosts, at) {
801
+ const present = new Set(live.map((p) => p.identity));
802
+ const kept = /* @__PURE__ */ new Map();
803
+ const extra = [];
804
+ for (const [identity, ghost] of ghosts) {
805
+ if (present.has(identity) || ghost.until <= at) continue;
806
+ kept.set(identity, ghost);
807
+ extra.push({
808
+ ...ghost.participant,
809
+ micEnabled: false,
810
+ cameraEnabled: false,
811
+ screenSharing: false,
812
+ reconnecting: true
813
+ });
814
+ }
815
+ return { roster: [...live, ...extra], kept };
816
+ }
817
+
459
818
  // src/core/tokens.ts
460
819
  function createWebRoomTokenStorage(onDiagnostic) {
461
820
  const store = globalThis.localStorage;
@@ -641,7 +1000,8 @@ function parseRoomToken(body, roomName) {
641
1000
  admissionState: readString(payload, "admission_state") ?? void 0,
642
1001
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
643
1002
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
644
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
1003
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
1004
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
645
1005
  };
646
1006
  }
647
1007
  function createTokenClient(options) {
@@ -1371,6 +1731,10 @@ function nextPhase(from, event) {
1371
1731
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1372
1732
  case "join":
1373
1733
  return JOINABLE_FROM.has(from) ? "joining" : null;
1734
+ case "auto_rejoin":
1735
+ return from === "disconnected" ? "joining" : null;
1736
+ case "resumed":
1737
+ return from === "resolving" ? "joining" : null;
1374
1738
  case "back_to_prejoin":
1375
1739
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1376
1740
  case "retry":
@@ -3284,6 +3648,7 @@ var EMPTY = {
3284
3648
  phaseNotice: null,
3285
3649
  roomName: null,
3286
3650
  connection: "stable",
3651
+ connectionBanner: false,
3287
3652
  share: NO_SHARE,
3288
3653
  call: null,
3289
3654
  waiting: null,
@@ -4238,8 +4603,11 @@ function mapConnectionState(state) {
4238
4603
  case "connecting":
4239
4604
  return "connecting";
4240
4605
  case "reconnecting":
4241
- case "signalReconnecting":
4242
4606
  return "reconnecting";
4607
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
4608
+ // signal blip from a full media reconnect.
4609
+ case "signalReconnecting":
4610
+ return "signal_reconnecting";
4243
4611
  default:
4244
4612
  return "disconnected";
4245
4613
  }
@@ -4320,8 +4688,12 @@ function createLiveKitDriver(handlers, options = {}) {
4320
4688
  // R5: large meetings degrade gracefully instead of melting a laptop.
4321
4689
  adaptiveStream: true,
4322
4690
  dynacast: true,
4691
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
4692
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
4693
+ reconnectPolicy: createReconnectPolicy(),
4323
4694
  ...options.roomOptions
4324
4695
  });
4696
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module2.DisconnectReason?.[reason] ?? String(reason);
4325
4697
  const roster = () => {
4326
4698
  const local = snapshotParticipant(created.localParticipant, true);
4327
4699
  const remotes = [...created.remoteParticipants.values()].map(
@@ -4339,7 +4711,10 @@ function createLiveKitDriver(handlers, options = {}) {
4339
4711
  if (track.kind !== Track.Kind.Audio) return;
4340
4712
  for (const element of track.detach()) element.remove();
4341
4713
  });
4342
- 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) => {
4714
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4715
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
4716
+ roster();
4717
+ }).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) => {
4343
4718
  handlers.roomMetadata?.(metadata ?? "");
4344
4719
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
4345
4720
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -4356,8 +4731,7 @@ function createLiveKitDriver(handlers, options = {}) {
4356
4731
  }).on(RoomEvent.Disconnected, (reason) => {
4357
4732
  audioContainer?.remove();
4358
4733
  audioContainer = null;
4359
- const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
4360
- handlers.disconnected(named);
4734
+ handlers.disconnected(reasonName(reason));
4361
4735
  });
4362
4736
  try {
4363
4737
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5730,6 +6104,18 @@ function createRoomEngine(options) {
5730
6104
  let disposed = false;
5731
6105
  let moving = false;
5732
6106
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
6107
+ let lastServerUrl = null;
6108
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
6109
+ let ghosts = /* @__PURE__ */ new Map();
6110
+ let ghostTimer = null;
6111
+ let liveRoster = [];
6112
+ const departures = /* @__PURE__ */ new Map();
6113
+ function requestAsNow() {
6114
+ if (lastRequest === null) return null;
6115
+ const media = store.snapshot().media;
6116
+ if (store.snapshot().phase !== "in_call") return lastRequest;
6117
+ return { ...lastRequest, mode: media.camera.enabled ? "video" : "audio", microphone: media.microphone.enabled };
6118
+ }
5733
6119
  async function dropRoom() {
5734
6120
  const target = driver;
5735
6121
  driver = null;
@@ -5740,6 +6126,7 @@ function createRoomEngine(options) {
5740
6126
  store.setLocalTrack("microphone", IDLE_TRACK);
5741
6127
  store.setLocalTrack("camera", IDLE_TRACK);
5742
6128
  store.patch({ share: NO_SHARE, connection: "stable" });
6129
+ clearGhosts();
5743
6130
  await target?.disconnect();
5744
6131
  }
5745
6132
  async function leaveRoom() {
@@ -5807,7 +6194,7 @@ function createRoomEngine(options) {
5807
6194
  participants(list) {
5808
6195
  const joinedAt = new Date(now()).toISOString();
5809
6196
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
5810
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
6197
+ projectRoster(list, joinedAt);
5811
6198
  const snapshot = store.snapshot();
5812
6199
  const newcomer = snapshot.participants.some(
5813
6200
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -5819,6 +6206,9 @@ function createRoomEngine(options) {
5819
6206
  );
5820
6207
  }
5821
6208
  },
6209
+ participantLeft(who, reason) {
6210
+ departures.set(who, reason);
6211
+ },
5822
6212
  roomMetadata(metadata) {
5823
6213
  let spotlight = null;
5824
6214
  if (metadata.length > 0) {
@@ -5845,12 +6235,8 @@ function createRoomEngine(options) {
5845
6235
  store.setQuality(asParticipantIdentity(who), value);
5846
6236
  },
5847
6237
  connectionState(state) {
5848
- if (state === "connected") {
5849
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5850
- store.dispatch({ type: "connected" });
5851
- } else if (state === "reconnecting") {
5852
- store.patch({ connection: "reconnecting" });
5853
- }
6238
+ supervisor.driverState(state);
6239
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
5854
6240
  },
5855
6241
  transcription(event) {
5856
6242
  const snapshot = store.snapshot();
@@ -5983,6 +6369,7 @@ function createRoomEngine(options) {
5983
6369
  };
5984
6370
  async function resolveExitAndApply(hint) {
5985
6371
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6372
+ lastRequest = requestAsNow();
5986
6373
  await dropRoom();
5987
6374
  let server;
5988
6375
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5996,7 +6383,7 @@ function createRoomEngine(options) {
5996
6383
  }
5997
6384
  if (disposed) return;
5998
6385
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5999
- store.patch({ connection: resolved.connection });
6386
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
6000
6387
  if (resolved.to === "waiting:knocking") {
6001
6388
  store.patch({
6002
6389
  waiting: {
@@ -6028,6 +6415,8 @@ function createRoomEngine(options) {
6028
6415
  ...request.observe === true ? { observe: true } : {}
6029
6416
  });
6030
6417
  current = token;
6418
+ lastServerUrl = token.serverUrl;
6419
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6031
6420
  store.patch({
6032
6421
  localIdentity: token.identity,
6033
6422
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6132,7 +6521,59 @@ function createRoomEngine(options) {
6132
6521
  throw normalized;
6133
6522
  }
6134
6523
  }
6135
- return {
6524
+ function projectRoster(list, joinedAt) {
6525
+ const at = now();
6526
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
6527
+ const present = new Set(list.map((p) => p.identity));
6528
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
6529
+ for (const [who, reason] of departures) {
6530
+ const last = previous.get(who);
6531
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
6532
+ ghosts.set(who, { participant: last, until: at + graceMs });
6533
+ }
6534
+ }
6535
+ departures.clear();
6536
+ liveRoster = list;
6537
+ const merged = mergeGhosts(list, ghosts, at);
6538
+ ghosts = merged.kept;
6539
+ store.setParticipants(
6540
+ merged.roster.map((entry) => {
6541
+ const projected = projectParticipant(entry, joinedAt);
6542
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6543
+ })
6544
+ );
6545
+ scheduleGhostExpiry();
6546
+ }
6547
+ function scheduleGhostExpiry() {
6548
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6549
+ ghostTimer = null;
6550
+ if (ghosts.size === 0 || disposed) return;
6551
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
6552
+ ghostTimer = setTimeout(() => {
6553
+ ghostTimer = null;
6554
+ if (disposed || driver === null) return;
6555
+ projectRoster(liveRoster, new Date(now()).toISOString());
6556
+ }, Math.max(0, next - now()) + 50);
6557
+ }
6558
+ function clearGhosts() {
6559
+ ghosts = /* @__PURE__ */ new Map();
6560
+ departures.clear();
6561
+ liveRoster = [];
6562
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6563
+ ghostTimer = null;
6564
+ }
6565
+ const supervisor = createConnectionSupervisor({
6566
+ store,
6567
+ network: options.connection?.network ?? createWebNetworkPort(),
6568
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
6569
+ policy: () => connectionPolicy,
6570
+ autoRejoin: () => engine.autoRejoin(),
6571
+ currentRequest: requestAsNow,
6572
+ resume: options.connection?.resume,
6573
+ now,
6574
+ report: (level, message) => report(level, message)
6575
+ });
6576
+ const engine = {
6136
6577
  join(request) {
6137
6578
  if (joining !== null) return joining;
6138
6579
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -6177,6 +6618,35 @@ function createRoomEngine(options) {
6177
6618
  });
6178
6619
  return joining;
6179
6620
  },
6621
+ async autoRejoin() {
6622
+ const request = lastRequest;
6623
+ if (request === null || joining !== null) return joining ?? void 0;
6624
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
6625
+ tokens.invalidate(request.roomName);
6626
+ current = null;
6627
+ joining = connect(request).catch((error) => {
6628
+ throw failJoin(error, "meet.autoRejoin");
6629
+ }).finally(() => {
6630
+ joining = null;
6631
+ });
6632
+ return joining;
6633
+ },
6634
+ resume(roomName) {
6635
+ const marker = supervisor.resumable(roomName);
6636
+ if (marker === null || joining !== null) return false;
6637
+ if (!store.dispatch({ type: "resumed" })) return false;
6638
+ lastRequest = { ...marker.request, roomName };
6639
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
6640
+ tokens.invalidate(roomName);
6641
+ current = null;
6642
+ joining = connect(lastRequest).catch((error) => {
6643
+ throw failJoin(error, "meet.resume");
6644
+ }).finally(() => {
6645
+ joining = null;
6646
+ });
6647
+ joining.catch(() => void 0);
6648
+ return true;
6649
+ },
6180
6650
  async moveToBreakout(roomId) {
6181
6651
  const request = lastRequest;
6182
6652
  if (request === null || driver === null) {
@@ -6344,6 +6814,8 @@ function createRoomEngine(options) {
6344
6814
  token: () => current,
6345
6815
  async dispose() {
6346
6816
  disposed = true;
6817
+ supervisor.dispose();
6818
+ clearGhosts();
6347
6819
  if (driver !== null) {
6348
6820
  const target = driver;
6349
6821
  driver = null;
@@ -6352,6 +6824,7 @@ function createRoomEngine(options) {
6352
6824
  store.reset();
6353
6825
  }
6354
6826
  };
6827
+ return engine;
6355
6828
  }
6356
6829
  function canModerate(participant) {
6357
6830
  return participant !== void 0 && isHostRole(participant.role);
@@ -7437,7 +7910,7 @@ function participantAttributes(p) {
7437
7910
  "data-meet-participant-role": observedRole(p.role, p.standing),
7438
7911
  "data-meet-participant-self": String(p.isLocal)
7439
7912
  };
7440
- const quality = observedQuality(p.quality);
7913
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
7441
7914
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
7442
7915
  return attrs;
7443
7916
  }