@ai-matrx/meet 0.11.27 → 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) {
@@ -1367,8 +1727,14 @@ function nextPhase(from, event) {
1367
1727
  return from === "resolving" ? "prejoin" : null;
1368
1728
  case "unsupported":
1369
1729
  return from === "resolving" ? "unsupported" : null;
1730
+ case "link_state":
1731
+ return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1370
1732
  case "join":
1371
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;
1372
1738
  case "back_to_prejoin":
1373
1739
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1374
1740
  case "retry":
@@ -1775,6 +2141,8 @@ var RPCS = {
1775
2141
  getOrCreateMeeting: "meet_get_or_create_meeting",
1776
2142
  /** Resolve a durable public link, before/during/after its scheduled time. */
1777
2143
  meetingBySlug: "meet_meeting_by_slug",
2144
+ /** The current run of a durable link (open / not started / ended + next start), no pass needed. */
2145
+ linkState: "meet_link_state",
1778
2146
  /*
1779
2147
  * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1780
2148
  * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
@@ -2083,6 +2451,14 @@ function createMeetRepository(options) {
2083
2451
  if (response.error !== null) throw normalizeMeetError(response.error, operation);
2084
2452
  }
2085
2453
  }
2454
+ async function readLinkRun(slug) {
2455
+ try {
2456
+ const response = await db.rpc(RPCS.linkState, { p_slug: slug });
2457
+ return projectLinkRun(response.error === null ? response.data : null);
2458
+ } catch {
2459
+ return void 0;
2460
+ }
2461
+ }
2086
2462
  async function guard(operation, run) {
2087
2463
  let response = await run();
2088
2464
  if (response.error !== null) {
@@ -2484,11 +2860,12 @@ function createMeetRepository(options) {
2484
2860
  return projectMeeting(row);
2485
2861
  },
2486
2862
  async meetingBySlug(slug) {
2487
- const row = await guard(
2488
- "meet.meetingBySlug",
2489
- () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })
2490
- );
2491
- return withRunNoteTaker(projectMeeting(row));
2863
+ const [row, linkRun] = await Promise.all([
2864
+ guard("meet.meetingBySlug", () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })),
2865
+ readLinkRun(slug)
2866
+ ]);
2867
+ const record = withRunNoteTaker(projectMeeting(row));
2868
+ return linkRun === void 0 ? record : { ...record, linkRun };
2492
2869
  },
2493
2870
  async meeting(id) {
2494
2871
  const response = await db.from(TABLES.meetings).select(MEETING_COLUMNS).eq("id", id).single();
@@ -2677,6 +3054,15 @@ function snippetRuns(snippet) {
2677
3054
  if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2678
3055
  return runs;
2679
3056
  }
3057
+ function projectLinkRun(value) {
3058
+ if (typeof value !== "object" || value === null) return void 0;
3059
+ const row = value;
3060
+ const state = row["state"];
3061
+ if (state !== "open" && state !== "not_started" && state !== "ended" && state !== "cancelled") return void 0;
3062
+ const reason = row["reason"] === "ended" || row["reason"] === "cancelled" ? row["reason"] : null;
3063
+ const nextStart = typeof row["next_start"] === "string" ? row["next_start"] : null;
3064
+ return { state, reason, nextStart };
3065
+ }
2680
3066
 
2681
3067
  // src/core/app-panel.ts
2682
3068
  function appPanelOf(meeting) {
@@ -3262,6 +3648,7 @@ var EMPTY = {
3262
3648
  phaseNotice: null,
3263
3649
  roomName: null,
3264
3650
  connection: "stable",
3651
+ connectionBanner: false,
3265
3652
  share: NO_SHARE,
3266
3653
  call: null,
3267
3654
  waiting: null,
@@ -4216,8 +4603,11 @@ function mapConnectionState(state) {
4216
4603
  case "connecting":
4217
4604
  return "connecting";
4218
4605
  case "reconnecting":
4219
- case "signalReconnecting":
4220
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";
4221
4611
  default:
4222
4612
  return "disconnected";
4223
4613
  }
@@ -4298,8 +4688,12 @@ function createLiveKitDriver(handlers, options = {}) {
4298
4688
  // R5: large meetings degrade gracefully instead of melting a laptop.
4299
4689
  adaptiveStream: true,
4300
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(),
4301
4694
  ...options.roomOptions
4302
4695
  });
4696
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module2.DisconnectReason?.[reason] ?? String(reason);
4303
4697
  const roster = () => {
4304
4698
  const local = snapshotParticipant(created.localParticipant, true);
4305
4699
  const remotes = [...created.remoteParticipants.values()].map(
@@ -4317,7 +4711,10 @@ function createLiveKitDriver(handlers, options = {}) {
4317
4711
  if (track.kind !== Track.Kind.Audio) return;
4318
4712
  for (const element of track.detach()) element.remove();
4319
4713
  });
4320
- 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) => {
4321
4718
  handlers.roomMetadata?.(metadata ?? "");
4322
4719
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
4323
4720
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -4334,8 +4731,7 @@ function createLiveKitDriver(handlers, options = {}) {
4334
4731
  }).on(RoomEvent.Disconnected, (reason) => {
4335
4732
  audioContainer?.remove();
4336
4733
  audioContainer = null;
4337
- const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
4338
- handlers.disconnected(named);
4734
+ handlers.disconnected(reasonName(reason));
4339
4735
  });
4340
4736
  try {
4341
4737
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5708,6 +6104,18 @@ function createRoomEngine(options) {
5708
6104
  let disposed = false;
5709
6105
  let moving = false;
5710
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
+ }
5711
6119
  async function dropRoom() {
5712
6120
  const target = driver;
5713
6121
  driver = null;
@@ -5718,6 +6126,7 @@ function createRoomEngine(options) {
5718
6126
  store.setLocalTrack("microphone", IDLE_TRACK);
5719
6127
  store.setLocalTrack("camera", IDLE_TRACK);
5720
6128
  store.patch({ share: NO_SHARE, connection: "stable" });
6129
+ clearGhosts();
5721
6130
  await target?.disconnect();
5722
6131
  }
5723
6132
  async function leaveRoom() {
@@ -5785,7 +6194,7 @@ function createRoomEngine(options) {
5785
6194
  participants(list) {
5786
6195
  const joinedAt = new Date(now()).toISOString();
5787
6196
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
5788
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
6197
+ projectRoster(list, joinedAt);
5789
6198
  const snapshot = store.snapshot();
5790
6199
  const newcomer = snapshot.participants.some(
5791
6200
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -5797,6 +6206,9 @@ function createRoomEngine(options) {
5797
6206
  );
5798
6207
  }
5799
6208
  },
6209
+ participantLeft(who, reason) {
6210
+ departures.set(who, reason);
6211
+ },
5800
6212
  roomMetadata(metadata) {
5801
6213
  let spotlight = null;
5802
6214
  if (metadata.length > 0) {
@@ -5823,12 +6235,8 @@ function createRoomEngine(options) {
5823
6235
  store.setQuality(asParticipantIdentity(who), value);
5824
6236
  },
5825
6237
  connectionState(state) {
5826
- if (state === "connected") {
5827
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5828
- store.dispatch({ type: "connected" });
5829
- } else if (state === "reconnecting") {
5830
- store.patch({ connection: "reconnecting" });
5831
- }
6238
+ supervisor.driverState(state);
6239
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
5832
6240
  },
5833
6241
  transcription(event) {
5834
6242
  const snapshot = store.snapshot();
@@ -5961,6 +6369,7 @@ function createRoomEngine(options) {
5961
6369
  };
5962
6370
  async function resolveExitAndApply(hint) {
5963
6371
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6372
+ lastRequest = requestAsNow();
5964
6373
  await dropRoom();
5965
6374
  let server;
5966
6375
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5974,7 +6383,7 @@ function createRoomEngine(options) {
5974
6383
  }
5975
6384
  if (disposed) return;
5976
6385
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5977
- store.patch({ connection: resolved.connection });
6386
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
5978
6387
  if (resolved.to === "waiting:knocking") {
5979
6388
  store.patch({
5980
6389
  waiting: {
@@ -6006,6 +6415,8 @@ function createRoomEngine(options) {
6006
6415
  ...request.observe === true ? { observe: true } : {}
6007
6416
  });
6008
6417
  current = token;
6418
+ lastServerUrl = token.serverUrl;
6419
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
6009
6420
  store.patch({
6010
6421
  localIdentity: token.identity,
6011
6422
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -6110,7 +6521,59 @@ function createRoomEngine(options) {
6110
6521
  throw normalized;
6111
6522
  }
6112
6523
  }
6113
- 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 = {
6114
6577
  join(request) {
6115
6578
  if (joining !== null) return joining;
6116
6579
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -6155,6 +6618,35 @@ function createRoomEngine(options) {
6155
6618
  });
6156
6619
  return joining;
6157
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
+ },
6158
6650
  async moveToBreakout(roomId) {
6159
6651
  const request = lastRequest;
6160
6652
  if (request === null || driver === null) {
@@ -6322,6 +6814,8 @@ function createRoomEngine(options) {
6322
6814
  token: () => current,
6323
6815
  async dispose() {
6324
6816
  disposed = true;
6817
+ supervisor.dispose();
6818
+ clearGhosts();
6325
6819
  if (driver !== null) {
6326
6820
  const target = driver;
6327
6821
  driver = null;
@@ -6330,6 +6824,7 @@ function createRoomEngine(options) {
6330
6824
  store.reset();
6331
6825
  }
6332
6826
  };
6827
+ return engine;
6333
6828
  }
6334
6829
  function canModerate(participant) {
6335
6830
  return participant !== void 0 && isHostRole(participant.role);
@@ -7415,7 +7910,7 @@ function participantAttributes(p) {
7415
7910
  "data-meet-participant-role": observedRole(p.role, p.standing),
7416
7911
  "data-meet-participant-self": String(p.isLocal)
7417
7912
  };
7418
- const quality = observedQuality(p.quality);
7913
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
7419
7914
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
7420
7915
  return attrs;
7421
7916
  }