@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/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) {
@@ -1404,8 +1764,14 @@ function nextPhase(from, event) {
1404
1764
  return from === "resolving" ? "prejoin" : null;
1405
1765
  case "unsupported":
1406
1766
  return from === "resolving" ? "unsupported" : null;
1767
+ case "link_state":
1768
+ return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1407
1769
  case "join":
1408
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;
1409
1775
  case "back_to_prejoin":
1410
1776
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1411
1777
  case "retry":
@@ -2166,6 +2532,8 @@ var RPCS = {
2166
2532
  getOrCreateMeeting: "meet_get_or_create_meeting",
2167
2533
  /** Resolve a durable public link, before/during/after its scheduled time. */
2168
2534
  meetingBySlug: "meet_meeting_by_slug",
2535
+ /** The current run of a durable link (open / not started / ended + next start), no pass needed. */
2536
+ linkState: "meet_link_state",
2169
2537
  /*
2170
2538
  * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
2171
2539
  * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
@@ -2474,6 +2842,14 @@ function createMeetRepository(options) {
2474
2842
  if (response.error !== null) throw normalizeMeetError(response.error, operation);
2475
2843
  }
2476
2844
  }
2845
+ async function readLinkRun(slug) {
2846
+ try {
2847
+ const response = await db.rpc(RPCS.linkState, { p_slug: slug });
2848
+ return projectLinkRun(response.error === null ? response.data : null);
2849
+ } catch {
2850
+ return void 0;
2851
+ }
2852
+ }
2477
2853
  async function guard(operation, run) {
2478
2854
  let response = await run();
2479
2855
  if (response.error !== null) {
@@ -2875,11 +3251,12 @@ function createMeetRepository(options) {
2875
3251
  return projectMeeting(row);
2876
3252
  },
2877
3253
  async meetingBySlug(slug) {
2878
- const row = await guard(
2879
- "meet.meetingBySlug",
2880
- () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })
2881
- );
2882
- return withRunNoteTaker(projectMeeting(row));
3254
+ const [row, linkRun] = await Promise.all([
3255
+ guard("meet.meetingBySlug", () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })),
3256
+ readLinkRun(slug)
3257
+ ]);
3258
+ const record = withRunNoteTaker(projectMeeting(row));
3259
+ return linkRun === void 0 ? record : { ...record, linkRun };
2883
3260
  },
2884
3261
  async meeting(id) {
2885
3262
  const response = await db.from(TABLES.meetings).select(MEETING_COLUMNS).eq("id", id).single();
@@ -3068,6 +3445,15 @@ function snippetRuns(snippet) {
3068
3445
  if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
3069
3446
  return runs;
3070
3447
  }
3448
+ function projectLinkRun(value) {
3449
+ if (typeof value !== "object" || value === null) return void 0;
3450
+ const row = value;
3451
+ const state = row["state"];
3452
+ if (state !== "open" && state !== "not_started" && state !== "ended" && state !== "cancelled") return void 0;
3453
+ const reason = row["reason"] === "ended" || row["reason"] === "cancelled" ? row["reason"] : null;
3454
+ const nextStart = typeof row["next_start"] === "string" ? row["next_start"] : null;
3455
+ return { state, reason, nextStart };
3456
+ }
3071
3457
 
3072
3458
  // src/core/app-panel.ts
3073
3459
  function appPanelOf(meeting) {
@@ -3525,6 +3911,7 @@ var EMPTY = {
3525
3911
  phaseNotice: null,
3526
3912
  roomName: null,
3527
3913
  connection: "stable",
3914
+ connectionBanner: false,
3528
3915
  share: NO_SHARE,
3529
3916
  call: null,
3530
3917
  waiting: null,
@@ -4804,6 +5191,18 @@ function createRoomEngine(options) {
4804
5191
  let disposed = false;
4805
5192
  let moving = false;
4806
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
+ }
4807
5206
  async function dropRoom() {
4808
5207
  const target = driver;
4809
5208
  driver = null;
@@ -4814,6 +5213,7 @@ function createRoomEngine(options) {
4814
5213
  store.setLocalTrack("microphone", IDLE_TRACK);
4815
5214
  store.setLocalTrack("camera", IDLE_TRACK);
4816
5215
  store.patch({ share: NO_SHARE, connection: "stable" });
5216
+ clearGhosts();
4817
5217
  await target?.disconnect();
4818
5218
  }
4819
5219
  async function leaveRoom() {
@@ -4881,7 +5281,7 @@ function createRoomEngine(options) {
4881
5281
  participants(list) {
4882
5282
  const joinedAt = new Date(now()).toISOString();
4883
5283
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
4884
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
5284
+ projectRoster(list, joinedAt);
4885
5285
  const snapshot = store.snapshot();
4886
5286
  const newcomer = snapshot.participants.some(
4887
5287
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -4893,6 +5293,9 @@ function createRoomEngine(options) {
4893
5293
  );
4894
5294
  }
4895
5295
  },
5296
+ participantLeft(who, reason) {
5297
+ departures.set(who, reason);
5298
+ },
4896
5299
  roomMetadata(metadata) {
4897
5300
  let spotlight = null;
4898
5301
  if (metadata.length > 0) {
@@ -4919,12 +5322,8 @@ function createRoomEngine(options) {
4919
5322
  store.setQuality(asParticipantIdentity(who), value);
4920
5323
  },
4921
5324
  connectionState(state) {
4922
- if (state === "connected") {
4923
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4924
- store.dispatch({ type: "connected" });
4925
- } else if (state === "reconnecting") {
4926
- store.patch({ connection: "reconnecting" });
4927
- }
5325
+ supervisor.driverState(state);
5326
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
4928
5327
  },
4929
5328
  transcription(event) {
4930
5329
  const snapshot = store.snapshot();
@@ -5057,6 +5456,7 @@ function createRoomEngine(options) {
5057
5456
  };
5058
5457
  async function resolveExitAndApply(hint) {
5059
5458
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5459
+ lastRequest = requestAsNow();
5060
5460
  await dropRoom();
5061
5461
  let server;
5062
5462
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5070,7 +5470,7 @@ function createRoomEngine(options) {
5070
5470
  }
5071
5471
  if (disposed) return;
5072
5472
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5073
- store.patch({ connection: resolved.connection });
5473
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
5074
5474
  if (resolved.to === "waiting:knocking") {
5075
5475
  store.patch({
5076
5476
  waiting: {
@@ -5102,6 +5502,8 @@ function createRoomEngine(options) {
5102
5502
  ...request.observe === true ? { observe: true } : {}
5103
5503
  });
5104
5504
  current = token;
5505
+ lastServerUrl = token.serverUrl;
5506
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5105
5507
  store.patch({
5106
5508
  localIdentity: token.identity,
5107
5509
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -5206,7 +5608,59 @@ function createRoomEngine(options) {
5206
5608
  throw normalized;
5207
5609
  }
5208
5610
  }
5209
- 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 = {
5210
5664
  join(request) {
5211
5665
  if (joining !== null) return joining;
5212
5666
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -5251,6 +5705,35 @@ function createRoomEngine(options) {
5251
5705
  });
5252
5706
  return joining;
5253
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
+ },
5254
5737
  async moveToBreakout(roomId) {
5255
5738
  const request = lastRequest;
5256
5739
  if (request === null || driver === null) {
@@ -5418,6 +5901,8 @@ function createRoomEngine(options) {
5418
5901
  token: () => current,
5419
5902
  async dispose() {
5420
5903
  disposed = true;
5904
+ supervisor.dispose();
5905
+ clearGhosts();
5421
5906
  if (driver !== null) {
5422
5907
  const target = driver;
5423
5908
  driver = null;
@@ -5426,6 +5911,7 @@ function createRoomEngine(options) {
5426
5911
  store.reset();
5427
5912
  }
5428
5913
  };
5914
+ return engine;
5429
5915
  }
5430
5916
  function canModerate(participant) {
5431
5917
  return participant !== void 0 && isHostRole(participant.role);
@@ -5736,8 +6222,11 @@ function mapConnectionState(state) {
5736
6222
  case "connecting":
5737
6223
  return "connecting";
5738
6224
  case "reconnecting":
5739
- case "signalReconnecting":
5740
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";
5741
6230
  default:
5742
6231
  return "disconnected";
5743
6232
  }
@@ -5818,8 +6307,12 @@ function createLiveKitDriver(handlers, options = {}) {
5818
6307
  // R5: large meetings degrade gracefully instead of melting a laptop.
5819
6308
  adaptiveStream: true,
5820
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(),
5821
6313
  ...options.roomOptions
5822
6314
  });
6315
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module2.DisconnectReason?.[reason] ?? String(reason);
5823
6316
  const roster = () => {
5824
6317
  const local = snapshotParticipant(created.localParticipant, true);
5825
6318
  const remotes = [...created.remoteParticipants.values()].map(
@@ -5837,7 +6330,10 @@ function createLiveKitDriver(handlers, options = {}) {
5837
6330
  if (track.kind !== Track.Kind.Audio) return;
5838
6331
  for (const element of track.detach()) element.remove();
5839
6332
  });
5840
- 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) => {
5841
6337
  handlers.roomMetadata?.(metadata ?? "");
5842
6338
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
5843
6339
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -5854,8 +6350,7 @@ function createLiveKitDriver(handlers, options = {}) {
5854
6350
  }).on(RoomEvent.Disconnected, (reason) => {
5855
6351
  audioContainer?.remove();
5856
6352
  audioContainer = null;
5857
- const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
5858
- handlers.disconnected(named);
6353
+ handlers.disconnected(reasonName(reason));
5859
6354
  });
5860
6355
  try {
5861
6356
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -6245,7 +6740,14 @@ function MeetRuntime(props) {
6245
6740
  effectsStorage,
6246
6741
  resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
6247
6742
  // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
6248
- 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
+ }
6249
6751
  });
6250
6752
  const meetings = createMeetingSession({
6251
6753
  manager,
@@ -6573,7 +7075,7 @@ function participantAttributes(p) {
6573
7075
  "data-meet-participant-role": observedRole(p.role, p.standing),
6574
7076
  "data-meet-participant-self": String(p.isLocal)
6575
7077
  };
6576
- const quality = observedQuality(p.quality);
7078
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
6577
7079
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
6578
7080
  return attrs;
6579
7081
  }
@@ -8792,6 +9294,7 @@ function useMeetCore() {
8792
9294
  reason: snapshot?.reason ?? null,
8793
9295
  phaseNotice: snapshot?.phaseNotice ?? null,
8794
9296
  connection: snapshot?.connection ?? "stable",
9297
+ connectionBanner: snapshot?.connectionBanner ?? false,
8795
9298
  share: snapshot?.share ?? NO_SHARE,
8796
9299
  call: snapshot?.call ?? null,
8797
9300
  actions
@@ -10058,7 +10561,8 @@ function ParticipantTile({
10058
10561
  participant.displayName,
10059
10562
  participant.isLocal ? " (you)" : "",
10060
10563
  participant.isAgent ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { className: "mx-meet__badge", children: "AI" }) : null,
10061
- /* @__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
10062
10566
  ] }),
10063
10567
  !participant.micEnabled ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(MicOffIcon, { width: 14, height: 14 }) : null,
10064
10568
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(QualityBars, { quality: participant.quality })
@@ -10101,6 +10605,12 @@ function QualityBars({ quality }) {
10101
10605
  const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
10102
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 }) });
10103
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
+ };
10104
10614
  function Stage({ pageSize }) {
10105
10615
  const stage = useStage(pageSize);
10106
10616
  const columns = gridColumns(stage.tiles.length);
@@ -11808,7 +12318,7 @@ function MeetSkinRoom({
11808
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,
11809
12319
  isHost ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(HostMenu, {}) : null
11810
12320
  ] }),
11811
- 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,
11812
12322
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(ShareNotice, {}),
11813
12323
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Stage, {}),
11814
12324
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(Captions, {}),
@@ -11842,7 +12352,7 @@ var REFUSED_TITLES = {
11842
12352
  registration_required: "Register to join this meeting"
11843
12353
  };
11844
12354
  function ExitScreen({ onLeave }) {
11845
- const { phase, reason, host, snapshot, actions, call } = useMeetCore();
12355
+ const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
11846
12356
  const [busy, setBusy] = (0, import_react10.useState)(false);
11847
12357
  const failure = snapshot?.failure ?? null;
11848
12358
  const rejoin = /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
@@ -11882,7 +12392,7 @@ function ExitScreen({ onLeave }) {
11882
12392
  if (phase.startsWith("refused:")) {
11883
12393
  const code = phase.slice("refused:".length);
11884
12394
  title = REFUSED_TITLES[code] ?? "You can't join this meeting";
11885
- line = failure?.remedy ?? null;
12395
+ line = code === "too_early" || code === "registration_required" || code === "cancelled" ? failure?.remedy ?? null : null;
11886
12396
  actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11887
12397
  /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_design_system.Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
11888
12398
  home
@@ -11898,7 +12408,7 @@ function ExitScreen({ onLeave }) {
11898
12408
  break;
11899
12409
  case "disconnected":
11900
12410
  title = "You lost your connection to the meeting";
11901
- 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.";
11902
12412
  notice = "disconnected";
11903
12413
  actionsNode = /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
11904
12414
  rejoin,
@@ -11932,6 +12442,10 @@ function ExitScreen({ onLeave }) {
11932
12442
  case "ended":
11933
12443
  case "record":
11934
12444
  title = reason === "time_limit" ? "The meeting reached its time limit" : "The meeting has ended";
12445
+ if (failure?.reason === "ended" && failure.remedy.length > 0 && failure.message.startsWith("This run")) {
12446
+ title = failure.message.replace(/\.$/, "");
12447
+ line = failure.remedy;
12448
+ }
11935
12449
  notice = "meeting-ended";
11936
12450
  break;
11937
12451
  default:
@@ -12502,8 +13016,16 @@ function MeetingSkinRootBody({
12502
13016
  if (now.phase !== "resolving" && now.roomName !== null && now.roomName !== roomName && now.call === null) {
12503
13017
  host.store.dispatch({ type: "reset" });
12504
13018
  }
12505
- if (host.store.snapshot().phase === "resolving") host.store.dispatch({ type: "resolved" });
12506
- }, [host, roomName, phase]);
13019
+ if (host.store.snapshot().phase !== "resolving") return;
13020
+ if (given?.linkRun?.reason == null && host.engine.resume(roomName)) return;
13021
+ const reason = given?.linkRun?.reason ?? null;
13022
+ if (reason !== null && given?.linkRun !== void 0) {
13023
+ const words = linkRunWords(given.linkRun);
13024
+ host.store.patch({ failure: { message: words.message, remedy: words.remedy, reason } });
13025
+ if (host.store.dispatch({ type: "link_state", reason })) return;
13026
+ }
13027
+ host.store.dispatch({ type: "resolved" });
13028
+ }, [host, roomName, phase, given]);
12507
13029
  (0, import_react12.useEffect)(() => {
12508
13030
  if (given === void 0) return;
12509
13031
  if (host.store.snapshot().meeting !== null) return;
@@ -12530,6 +13052,31 @@ function MeetingSkinRootBody({
12530
13052
  }
12531
13053
  );
12532
13054
  }
13055
+ function linkRunWords(run) {
13056
+ if (run.reason === "cancelled") {
13057
+ return {
13058
+ message: "This occurrence of the meeting was cancelled.",
13059
+ remedy: "The series continues; open the same link at its next time."
13060
+ };
13061
+ }
13062
+ if (run.nextStart === null) {
13063
+ return {
13064
+ message: "This meeting has ended.",
13065
+ remedy: "The link still opens its record: summary, notes and transcript. Nobody can rejoin."
13066
+ };
13067
+ }
13068
+ const when2 = new Date(run.nextStart).toLocaleString(void 0, {
13069
+ weekday: "short",
13070
+ month: "short",
13071
+ day: "numeric",
13072
+ hour: "numeric",
13073
+ minute: "2-digit"
13074
+ });
13075
+ return {
13076
+ message: "This run of the meeting has ended.",
13077
+ remedy: `The next one starts ${when2}. Open the same link then.`
13078
+ };
13079
+ }
12533
13080
 
12534
13081
  // src/react/calls-ui.tsx
12535
13082
  var import_react13 = require("react");