@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.js CHANGED
@@ -1,6 +1,352 @@
1
1
  "use client";
2
2
  "use client";
3
3
 
4
+ // src/core/connection-supervisor.ts
5
+ var DEFAULT_CONNECTION_POLICY = {
6
+ bannerDelayMs: 2e3,
7
+ autoRejoin: true,
8
+ autoRejoinMaxAttempts: 5,
9
+ reconnectGraceSeconds: 30
10
+ };
11
+ function clampInt(value, min, max, fallback) {
12
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
13
+ return Math.min(max, Math.max(min, Math.round(value)));
14
+ }
15
+ function connectionPolicyFrom(raw) {
16
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
17
+ const row = raw;
18
+ const d = DEFAULT_CONNECTION_POLICY;
19
+ return {
20
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
21
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
22
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
23
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
24
+ };
25
+ }
26
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
27
+ function createReconnectPolicy(options = {}) {
28
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
29
+ const giveUpMs = options.giveUpMs ?? 45e3;
30
+ const maxAttempts = options.maxAttempts ?? 12;
31
+ const random = options.random ?? Math.random;
32
+ return {
33
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
34
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
35
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
36
+ const spread = Math.min(2e3, Math.max(500, base / 2));
37
+ return Math.round(base + random() * spread);
38
+ }
39
+ };
40
+ }
41
+ function createWebNetworkPort() {
42
+ const scope = globalThis;
43
+ return {
44
+ online: () => scope.navigator?.onLine !== false,
45
+ subscribe(listener) {
46
+ if (typeof scope.addEventListener !== "function") return () => void 0;
47
+ const on = () => listener(true);
48
+ const off = () => listener(false);
49
+ scope.addEventListener("online", on);
50
+ scope.addEventListener("offline", off);
51
+ return () => {
52
+ scope.removeEventListener?.("online", on);
53
+ scope.removeEventListener?.("offline", off);
54
+ };
55
+ }
56
+ };
57
+ }
58
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
59
+ return async () => {
60
+ const targets = urls().filter((url) => url.length > 0);
61
+ if (targets.length === 0 || typeof fetch !== "function") return true;
62
+ const one = async (url) => {
63
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
64
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
65
+ try {
66
+ await fetch(url, {
67
+ method: "GET",
68
+ mode: "no-cors",
69
+ cache: "no-store",
70
+ credentials: "omit",
71
+ ...controller !== null ? { signal: controller.signal } : {}
72
+ });
73
+ return true;
74
+ } catch {
75
+ return false;
76
+ } finally {
77
+ clearTimeout(timer);
78
+ }
79
+ };
80
+ const answers = await Promise.all(targets.map(one));
81
+ return answers.every(Boolean);
82
+ };
83
+ }
84
+ function httpUrlOf(serverUrl) {
85
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
86
+ }
87
+ var RESUME_WINDOW_MS = 15 * 6e4;
88
+ var RESUME_KEY = "mx-meet:resume:";
89
+ function createWebResumeStorage() {
90
+ const storage = globalThis.sessionStorage;
91
+ if (storage === void 0) return void 0;
92
+ return {
93
+ load(roomName) {
94
+ try {
95
+ const raw = storage.getItem(RESUME_KEY + roomName);
96
+ if (raw === null) return null;
97
+ const parsed = JSON.parse(raw);
98
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
99
+ } catch {
100
+ return null;
101
+ }
102
+ },
103
+ save(roomName, marker) {
104
+ try {
105
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
106
+ } catch {
107
+ }
108
+ },
109
+ clear(roomName) {
110
+ try {
111
+ storage.removeItem(RESUME_KEY + roomName);
112
+ } catch {
113
+ }
114
+ }
115
+ };
116
+ }
117
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
118
+ function autoRejoinDelayMs(attempt, random = Math.random) {
119
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
120
+ return Math.round(base * (0.5 + random()));
121
+ }
122
+ function createConnectionSupervisor(options) {
123
+ const { store, network } = options;
124
+ const now = options.now ?? (() => Date.now());
125
+ const random = options.random ?? Math.random;
126
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
127
+ let driverRegion = null;
128
+ let probeTimer = null;
129
+ let probing = false;
130
+ let bannerTimer = null;
131
+ let rejoinTimer = null;
132
+ let rejoinAttempts = 0;
133
+ let lastPhase = store.snapshot().phase;
134
+ let lastSavedAt = 0;
135
+ let disposed = false;
136
+ const inCall = () => store.snapshot().phase === "in_call";
137
+ const dropped = () => store.snapshot().phase === "disconnected";
138
+ function setRegion(next) {
139
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
140
+ }
141
+ function regionFor(reachable) {
142
+ if (!network.online()) return "offline";
143
+ if (reachable === false) return "server_unreachable";
144
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
145
+ return driverRegion ?? "stable";
146
+ }
147
+ function needsWatch() {
148
+ const snap = store.snapshot();
149
+ if (dropped()) return true;
150
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
151
+ }
152
+ function stopProbe() {
153
+ if (probeTimer !== null) clearTimeout(probeTimer);
154
+ probeTimer = null;
155
+ }
156
+ function scheduleProbe(delayMs) {
157
+ if (disposed || probeTimer !== null) return;
158
+ probeTimer = setTimeout(() => {
159
+ probeTimer = null;
160
+ void runProbe();
161
+ }, delayMs);
162
+ }
163
+ async function runProbe() {
164
+ if (disposed || probing) return;
165
+ if (!needsWatch()) return;
166
+ if (!network.online()) {
167
+ setRegion("offline");
168
+ scheduleProbe(probeEveryMs);
169
+ return;
170
+ }
171
+ probing = true;
172
+ let reachable = false;
173
+ try {
174
+ reachable = await options.probe();
175
+ } catch {
176
+ reachable = false;
177
+ } finally {
178
+ probing = false;
179
+ }
180
+ if (disposed) return;
181
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
182
+ if (dropped() && reachable) maybeRejoin();
183
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
184
+ }
185
+ function syncBanner() {
186
+ const snap = store.snapshot();
187
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
188
+ if (!unstable) {
189
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
190
+ bannerTimer = null;
191
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
192
+ return;
193
+ }
194
+ if (snap.connectionBanner || bannerTimer !== null) return;
195
+ const delay = options.policy().bannerDelayMs;
196
+ if (delay <= 0) {
197
+ store.patch({ connectionBanner: true });
198
+ return;
199
+ }
200
+ bannerTimer = setTimeout(() => {
201
+ bannerTimer = null;
202
+ const latest = store.snapshot();
203
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
204
+ store.patch({ connectionBanner: true });
205
+ }
206
+ }, delay);
207
+ }
208
+ function stopRejoin() {
209
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
210
+ rejoinTimer = null;
211
+ }
212
+ function maybeRejoin() {
213
+ if (disposed || rejoinTimer !== null || !dropped()) return;
214
+ const policy = options.policy();
215
+ if (!policy.autoRejoin) return;
216
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
217
+ if (!network.online()) return;
218
+ const attempt = rejoinAttempts;
219
+ rejoinTimer = setTimeout(() => {
220
+ rejoinTimer = null;
221
+ if (disposed || !dropped() || !network.online()) return;
222
+ rejoinAttempts = attempt + 1;
223
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
224
+ void options.autoRejoin().catch(() => void 0);
225
+ }, autoRejoinDelayMs(attempt, random));
226
+ }
227
+ function saveMarker(force) {
228
+ const resume = options.resume;
229
+ const snap = store.snapshot();
230
+ if (resume === void 0 || snap.roomName === null) return;
231
+ const at = now();
232
+ if (!force && at - lastSavedAt < 1e4) return;
233
+ const request = options.currentRequest();
234
+ if (request === null) return;
235
+ lastSavedAt = at;
236
+ resume.save(snap.roomName, { request, at });
237
+ }
238
+ const unsubscribeStore = store.subscribe(() => {
239
+ if (disposed) return;
240
+ const snap = store.snapshot();
241
+ const phase = snap.phase;
242
+ const changed = phase !== lastPhase;
243
+ lastPhase = phase;
244
+ if (changed) {
245
+ if (phase === "in_call") {
246
+ rejoinAttempts = 0;
247
+ stopRejoin();
248
+ saveMarker(true);
249
+ } else if (phase === "disconnected") {
250
+ driverRegion = null;
251
+ saveMarker(true);
252
+ stopProbe();
253
+ scheduleProbe(0);
254
+ } else {
255
+ stopRejoin();
256
+ if (phase !== "joining") driverRegion = null;
257
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
258
+ }
259
+ } else if (phase === "in_call" || phase === "disconnected") {
260
+ saveMarker(false);
261
+ }
262
+ syncBanner();
263
+ });
264
+ const unsubscribeNetwork = network.subscribe((online) => {
265
+ if (disposed) return;
266
+ if (!online) {
267
+ if (inCall() || dropped()) setRegion("offline");
268
+ stopRejoin();
269
+ stopProbe();
270
+ if (needsWatch()) scheduleProbe(probeEveryMs);
271
+ return;
272
+ }
273
+ stopProbe();
274
+ if (needsWatch()) scheduleProbe(0);
275
+ });
276
+ return {
277
+ driverState(state) {
278
+ if (disposed) return;
279
+ if (state === "connected") {
280
+ driverRegion = null;
281
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
282
+ if (!needsWatch()) stopProbe();
283
+ return;
284
+ }
285
+ if (state === "reconnecting" || state === "signal_reconnecting") {
286
+ driverRegion = state;
287
+ if (inCall()) {
288
+ setRegion(network.online() ? state : "offline");
289
+ stopProbe();
290
+ scheduleProbe(0);
291
+ }
292
+ }
293
+ },
294
+ exitConnection() {
295
+ return network.online() ? "server_unreachable" : "offline";
296
+ },
297
+ resumable(roomName) {
298
+ const resume = options.resume;
299
+ if (resume === void 0) return null;
300
+ const marker = resume.load(roomName);
301
+ if (marker === null) return null;
302
+ if (now() - marker.at > RESUME_WINDOW_MS) {
303
+ resume.clear(roomName);
304
+ return null;
305
+ }
306
+ return marker;
307
+ },
308
+ dispose() {
309
+ disposed = true;
310
+ stopProbe();
311
+ stopRejoin();
312
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
313
+ bannerTimer = null;
314
+ unsubscribeStore();
315
+ unsubscribeNetwork();
316
+ }
317
+ };
318
+ }
319
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
320
+ "CLIENT_INITIATED",
321
+ "PARTICIPANT_REMOVED",
322
+ "ROOM_DELETED",
323
+ "ROOM_CLOSED",
324
+ "DUPLICATE_IDENTITY",
325
+ "USER_REJECTED",
326
+ "USER_UNAVAILABLE",
327
+ "SIP_TRUNK_FAILURE"
328
+ ]);
329
+ function isUnannouncedDeparture(reason) {
330
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
331
+ }
332
+ function mergeGhosts(live, ghosts, at) {
333
+ const present = new Set(live.map((p) => p.identity));
334
+ const kept = /* @__PURE__ */ new Map();
335
+ const extra = [];
336
+ for (const [identity, ghost] of ghosts) {
337
+ if (present.has(identity) || ghost.until <= at) continue;
338
+ kept.set(identity, ghost);
339
+ extra.push({
340
+ ...ghost.participant,
341
+ micEnabled: false,
342
+ cameraEnabled: false,
343
+ screenSharing: false,
344
+ reconnecting: true
345
+ });
346
+ }
347
+ return { roster: [...live, ...extra], kept };
348
+ }
349
+
4
350
  // src/react/provider.tsx
5
351
  import { globalSlot as globalSlot2 } from "@ai-matrx/kit/global-slot";
6
352
  import {
@@ -410,7 +756,8 @@ function parseRoomToken(body, roomName) {
410
756
  admissionState: readString(payload, "admission_state") ?? void 0,
411
757
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
412
758
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
413
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
759
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
760
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
414
761
  };
415
762
  }
416
763
  function createTokenClient(options) {
@@ -1111,8 +1458,14 @@ function nextPhase(from, event) {
1111
1458
  return from === "resolving" ? "prejoin" : null;
1112
1459
  case "unsupported":
1113
1460
  return from === "resolving" ? "unsupported" : null;
1461
+ case "link_state":
1462
+ return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1114
1463
  case "join":
1115
1464
  return JOINABLE_FROM.has(from) ? "joining" : null;
1465
+ case "auto_rejoin":
1466
+ return from === "disconnected" ? "joining" : null;
1467
+ case "resumed":
1468
+ return from === "resolving" ? "joining" : null;
1116
1469
  case "back_to_prejoin":
1117
1470
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1118
1471
  case "retry":
@@ -1873,6 +2226,8 @@ var RPCS = {
1873
2226
  getOrCreateMeeting: "meet_get_or_create_meeting",
1874
2227
  /** Resolve a durable public link, before/during/after its scheduled time. */
1875
2228
  meetingBySlug: "meet_meeting_by_slug",
2229
+ /** The current run of a durable link (open / not started / ended + next start), no pass needed. */
2230
+ linkState: "meet_link_state",
1876
2231
  /*
1877
2232
  * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1878
2233
  * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
@@ -2181,6 +2536,14 @@ function createMeetRepository(options) {
2181
2536
  if (response.error !== null) throw normalizeMeetError(response.error, operation);
2182
2537
  }
2183
2538
  }
2539
+ async function readLinkRun(slug) {
2540
+ try {
2541
+ const response = await db.rpc(RPCS.linkState, { p_slug: slug });
2542
+ return projectLinkRun(response.error === null ? response.data : null);
2543
+ } catch {
2544
+ return void 0;
2545
+ }
2546
+ }
2184
2547
  async function guard(operation, run) {
2185
2548
  let response = await run();
2186
2549
  if (response.error !== null) {
@@ -2582,11 +2945,12 @@ function createMeetRepository(options) {
2582
2945
  return projectMeeting(row);
2583
2946
  },
2584
2947
  async meetingBySlug(slug) {
2585
- const row = await guard(
2586
- "meet.meetingBySlug",
2587
- () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })
2588
- );
2589
- return withRunNoteTaker(projectMeeting(row));
2948
+ const [row, linkRun] = await Promise.all([
2949
+ guard("meet.meetingBySlug", () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })),
2950
+ readLinkRun(slug)
2951
+ ]);
2952
+ const record = withRunNoteTaker(projectMeeting(row));
2953
+ return linkRun === void 0 ? record : { ...record, linkRun };
2590
2954
  },
2591
2955
  async meeting(id) {
2592
2956
  const response = await db.from(TABLES.meetings).select(MEETING_COLUMNS).eq("id", id).single();
@@ -2775,6 +3139,15 @@ function snippetRuns(snippet) {
2775
3139
  if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2776
3140
  return runs;
2777
3141
  }
3142
+ function projectLinkRun(value) {
3143
+ if (typeof value !== "object" || value === null) return void 0;
3144
+ const row = value;
3145
+ const state = row["state"];
3146
+ if (state !== "open" && state !== "not_started" && state !== "ended" && state !== "cancelled") return void 0;
3147
+ const reason = row["reason"] === "ended" || row["reason"] === "cancelled" ? row["reason"] : null;
3148
+ const nextStart = typeof row["next_start"] === "string" ? row["next_start"] : null;
3149
+ return { state, reason, nextStart };
3150
+ }
2778
3151
 
2779
3152
  // src/core/app-panel.ts
2780
3153
  function appPanelOf(meeting) {
@@ -3232,6 +3605,7 @@ var EMPTY = {
3232
3605
  phaseNotice: null,
3233
3606
  roomName: null,
3234
3607
  connection: "stable",
3608
+ connectionBanner: false,
3235
3609
  share: NO_SHARE,
3236
3610
  call: null,
3237
3611
  waiting: null,
@@ -4513,6 +4887,18 @@ function createRoomEngine(options) {
4513
4887
  let disposed = false;
4514
4888
  let moving = false;
4515
4889
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
4890
+ let lastServerUrl = null;
4891
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
4892
+ let ghosts = /* @__PURE__ */ new Map();
4893
+ let ghostTimer = null;
4894
+ let liveRoster = [];
4895
+ const departures = /* @__PURE__ */ new Map();
4896
+ function requestAsNow() {
4897
+ if (lastRequest === null) return null;
4898
+ const media = store.snapshot().media;
4899
+ if (store.snapshot().phase !== "in_call") return lastRequest;
4900
+ return { ...lastRequest, mode: media.camera.enabled ? "video" : "audio", microphone: media.microphone.enabled };
4901
+ }
4516
4902
  async function dropRoom() {
4517
4903
  const target = driver;
4518
4904
  driver = null;
@@ -4523,6 +4909,7 @@ function createRoomEngine(options) {
4523
4909
  store.setLocalTrack("microphone", IDLE_TRACK);
4524
4910
  store.setLocalTrack("camera", IDLE_TRACK);
4525
4911
  store.patch({ share: NO_SHARE, connection: "stable" });
4912
+ clearGhosts();
4526
4913
  await target?.disconnect();
4527
4914
  }
4528
4915
  async function leaveRoom() {
@@ -4590,7 +4977,7 @@ function createRoomEngine(options) {
4590
4977
  participants(list) {
4591
4978
  const joinedAt = new Date(now()).toISOString();
4592
4979
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
4593
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
4980
+ projectRoster(list, joinedAt);
4594
4981
  const snapshot = store.snapshot();
4595
4982
  const newcomer = snapshot.participants.some(
4596
4983
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -4602,6 +4989,9 @@ function createRoomEngine(options) {
4602
4989
  );
4603
4990
  }
4604
4991
  },
4992
+ participantLeft(who, reason) {
4993
+ departures.set(who, reason);
4994
+ },
4605
4995
  roomMetadata(metadata) {
4606
4996
  let spotlight = null;
4607
4997
  if (metadata.length > 0) {
@@ -4628,12 +5018,8 @@ function createRoomEngine(options) {
4628
5018
  store.setQuality(asParticipantIdentity(who), value);
4629
5019
  },
4630
5020
  connectionState(state) {
4631
- if (state === "connected") {
4632
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4633
- store.dispatch({ type: "connected" });
4634
- } else if (state === "reconnecting") {
4635
- store.patch({ connection: "reconnecting" });
4636
- }
5021
+ supervisor.driverState(state);
5022
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
4637
5023
  },
4638
5024
  transcription(event) {
4639
5025
  const snapshot = store.snapshot();
@@ -4766,6 +5152,7 @@ function createRoomEngine(options) {
4766
5152
  };
4767
5153
  async function resolveExitAndApply(hint) {
4768
5154
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5155
+ lastRequest = requestAsNow();
4769
5156
  await dropRoom();
4770
5157
  let server;
4771
5158
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -4779,7 +5166,7 @@ function createRoomEngine(options) {
4779
5166
  }
4780
5167
  if (disposed) return;
4781
5168
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
4782
- store.patch({ connection: resolved.connection });
5169
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
4783
5170
  if (resolved.to === "waiting:knocking") {
4784
5171
  store.patch({
4785
5172
  waiting: {
@@ -4811,6 +5198,8 @@ function createRoomEngine(options) {
4811
5198
  ...request.observe === true ? { observe: true } : {}
4812
5199
  });
4813
5200
  current = token;
5201
+ lastServerUrl = token.serverUrl;
5202
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
4814
5203
  store.patch({
4815
5204
  localIdentity: token.identity,
4816
5205
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -4915,7 +5304,59 @@ function createRoomEngine(options) {
4915
5304
  throw normalized;
4916
5305
  }
4917
5306
  }
4918
- return {
5307
+ function projectRoster(list, joinedAt) {
5308
+ const at = now();
5309
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
5310
+ const present = new Set(list.map((p) => p.identity));
5311
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
5312
+ for (const [who, reason] of departures) {
5313
+ const last = previous.get(who);
5314
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
5315
+ ghosts.set(who, { participant: last, until: at + graceMs });
5316
+ }
5317
+ }
5318
+ departures.clear();
5319
+ liveRoster = list;
5320
+ const merged = mergeGhosts(list, ghosts, at);
5321
+ ghosts = merged.kept;
5322
+ store.setParticipants(
5323
+ merged.roster.map((entry) => {
5324
+ const projected = projectParticipant(entry, joinedAt);
5325
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
5326
+ })
5327
+ );
5328
+ scheduleGhostExpiry();
5329
+ }
5330
+ function scheduleGhostExpiry() {
5331
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
5332
+ ghostTimer = null;
5333
+ if (ghosts.size === 0 || disposed) return;
5334
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
5335
+ ghostTimer = setTimeout(() => {
5336
+ ghostTimer = null;
5337
+ if (disposed || driver === null) return;
5338
+ projectRoster(liveRoster, new Date(now()).toISOString());
5339
+ }, Math.max(0, next - now()) + 50);
5340
+ }
5341
+ function clearGhosts() {
5342
+ ghosts = /* @__PURE__ */ new Map();
5343
+ departures.clear();
5344
+ liveRoster = [];
5345
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
5346
+ ghostTimer = null;
5347
+ }
5348
+ const supervisor = createConnectionSupervisor({
5349
+ store,
5350
+ network: options.connection?.network ?? createWebNetworkPort(),
5351
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
5352
+ policy: () => connectionPolicy,
5353
+ autoRejoin: () => engine.autoRejoin(),
5354
+ currentRequest: requestAsNow,
5355
+ resume: options.connection?.resume,
5356
+ now,
5357
+ report: (level, message) => report(level, message)
5358
+ });
5359
+ const engine = {
4919
5360
  join(request) {
4920
5361
  if (joining !== null) return joining;
4921
5362
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -4960,6 +5401,35 @@ function createRoomEngine(options) {
4960
5401
  });
4961
5402
  return joining;
4962
5403
  },
5404
+ async autoRejoin() {
5405
+ const request = lastRequest;
5406
+ if (request === null || joining !== null) return joining ?? void 0;
5407
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
5408
+ tokens.invalidate(request.roomName);
5409
+ current = null;
5410
+ joining = connect(request).catch((error) => {
5411
+ throw failJoin(error, "meet.autoRejoin");
5412
+ }).finally(() => {
5413
+ joining = null;
5414
+ });
5415
+ return joining;
5416
+ },
5417
+ resume(roomName) {
5418
+ const marker = supervisor.resumable(roomName);
5419
+ if (marker === null || joining !== null) return false;
5420
+ if (!store.dispatch({ type: "resumed" })) return false;
5421
+ lastRequest = { ...marker.request, roomName };
5422
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
5423
+ tokens.invalidate(roomName);
5424
+ current = null;
5425
+ joining = connect(lastRequest).catch((error) => {
5426
+ throw failJoin(error, "meet.resume");
5427
+ }).finally(() => {
5428
+ joining = null;
5429
+ });
5430
+ joining.catch(() => void 0);
5431
+ return true;
5432
+ },
4963
5433
  async moveToBreakout(roomId) {
4964
5434
  const request = lastRequest;
4965
5435
  if (request === null || driver === null) {
@@ -5127,6 +5597,8 @@ function createRoomEngine(options) {
5127
5597
  token: () => current,
5128
5598
  async dispose() {
5129
5599
  disposed = true;
5600
+ supervisor.dispose();
5601
+ clearGhosts();
5130
5602
  if (driver !== null) {
5131
5603
  const target = driver;
5132
5604
  driver = null;
@@ -5135,6 +5607,7 @@ function createRoomEngine(options) {
5135
5607
  store.reset();
5136
5608
  }
5137
5609
  };
5610
+ return engine;
5138
5611
  }
5139
5612
  function canModerate(participant) {
5140
5613
  return participant !== void 0 && isHostRole(participant.role);
@@ -5445,8 +5918,11 @@ function mapConnectionState(state) {
5445
5918
  case "connecting":
5446
5919
  return "connecting";
5447
5920
  case "reconnecting":
5448
- case "signalReconnecting":
5449
5921
  return "reconnecting";
5922
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
5923
+ // signal blip from a full media reconnect.
5924
+ case "signalReconnecting":
5925
+ return "signal_reconnecting";
5450
5926
  default:
5451
5927
  return "disconnected";
5452
5928
  }
@@ -5527,8 +6003,12 @@ function createLiveKitDriver(handlers, options = {}) {
5527
6003
  // R5: large meetings degrade gracefully instead of melting a laptop.
5528
6004
  adaptiveStream: true,
5529
6005
  dynacast: true,
6006
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
6007
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
6008
+ reconnectPolicy: createReconnectPolicy(),
5530
6009
  ...options.roomOptions
5531
6010
  });
6011
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module.DisconnectReason?.[reason] ?? String(reason);
5532
6012
  const roster = () => {
5533
6013
  const local = snapshotParticipant(created.localParticipant, true);
5534
6014
  const remotes = [...created.remoteParticipants.values()].map(
@@ -5546,7 +6026,10 @@ function createLiveKitDriver(handlers, options = {}) {
5546
6026
  if (track.kind !== Track.Kind.Audio) return;
5547
6027
  for (const element of track.detach()) element.remove();
5548
6028
  });
5549
- 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) => {
6029
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
6030
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
6031
+ roster();
6032
+ }).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) => {
5550
6033
  handlers.roomMetadata?.(metadata ?? "");
5551
6034
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
5552
6035
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -5563,8 +6046,7 @@ function createLiveKitDriver(handlers, options = {}) {
5563
6046
  }).on(RoomEvent.Disconnected, (reason) => {
5564
6047
  audioContainer?.remove();
5565
6048
  audioContainer = null;
5566
- const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
5567
- handlers.disconnected(named);
6049
+ handlers.disconnected(reasonName(reason));
5568
6050
  });
5569
6051
  try {
5570
6052
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5954,7 +6436,14 @@ function MeetRuntime(props) {
5954
6436
  effectsStorage,
5955
6437
  resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
5956
6438
  // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
5957
- readExit: (connection) => meetings.readExit(connection)
6439
+ readExit: (connection) => meetings.readExit(connection),
6440
+ // S6b: the connection supervisor's ports — the device's network, whether OUR servers (the
6441
+ // API and the media server) answer, and the per-tab reload-after-offline marker.
6442
+ connection: {
6443
+ network: createWebNetworkPort(),
6444
+ resume: createWebResumeStorage(),
6445
+ probe: (serverUrl) => createReachabilityProbe(() => [baseUrl, ...serverUrl !== null ? [httpUrlOf(serverUrl)] : []])()
6446
+ }
5958
6447
  });
5959
6448
  const meetings = createMeetingSession({
5960
6449
  manager,
@@ -6282,7 +6771,7 @@ function participantAttributes(p) {
6282
6771
  "data-meet-participant-role": observedRole(p.role, p.standing),
6283
6772
  "data-meet-participant-self": String(p.isLocal)
6284
6773
  };
6285
- const quality = observedQuality(p.quality);
6774
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
6286
6775
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
6287
6776
  return attrs;
6288
6777
  }
@@ -8501,6 +8990,7 @@ function useMeetCore() {
8501
8990
  reason: snapshot?.reason ?? null,
8502
8991
  phaseNotice: snapshot?.phaseNotice ?? null,
8503
8992
  connection: snapshot?.connection ?? "stable",
8993
+ connectionBanner: snapshot?.connectionBanner ?? false,
8504
8994
  share: snapshot?.share ?? NO_SHARE,
8505
8995
  call: snapshot?.call ?? null,
8506
8996
  actions
@@ -9767,7 +10257,8 @@ function ParticipantTile({
9767
10257
  participant.displayName,
9768
10258
  participant.isLocal ? " (you)" : "",
9769
10259
  participant.isAgent ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "AI" }) : null,
9770
- /* @__PURE__ */ jsx6(RoleBadge, { role: participant.role, standing: participant.standing })
10260
+ /* @__PURE__ */ jsx6(RoleBadge, { role: participant.role, standing: participant.standing }),
10261
+ participant.reconnecting === true ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "Reconnecting\u2026" }) : null
9771
10262
  ] }),
9772
10263
  !participant.micEnabled ? /* @__PURE__ */ jsx6(MicOffIcon, { width: 14, height: 14 }) : null,
9773
10264
  /* @__PURE__ */ jsx6(QualityBars, { quality: participant.quality })
@@ -9810,6 +10301,12 @@ function QualityBars({ quality }) {
9810
10301
  const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
9811
10302
  return /* @__PURE__ */ jsx6("span", { className: `mx-meet__signal mx-meet__signal--${quality}`, title: label, "aria-label": label, role: "img", children: /* @__PURE__ */ jsx6(SignalIcon, { bars, width: 14, height: 14 }) });
9812
10303
  }
10304
+ var CONNECTION_BANNER = {
10305
+ signal_reconnecting: "Reconnecting\u2026",
10306
+ reconnecting: "Reconnecting\u2026",
10307
+ offline: "You're offline. Reconnecting when your internet is back\u2026",
10308
+ server_unreachable: "Can't reach the meeting server. Retrying\u2026"
10309
+ };
9813
10310
  function Stage({ pageSize }) {
9814
10311
  const stage = useStage(pageSize);
9815
10312
  const columns = gridColumns(stage.tiles.length);
@@ -11517,7 +12014,7 @@ function MeetSkinRoom({
11517
12014
  headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
11518
12015
  isHost ? /* @__PURE__ */ jsx6(HostMenu, {}) : null
11519
12016
  ] }),
11520
- core.connection !== "stable" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", children: "Reconnecting\u2026" }) : null,
12017
+ core.connectionBanner && core.connection !== "stable" ? /* @__PURE__ */ jsx6("p", { className: "mx-meet__banner", role: "status", "data-meet-notice": `connection-${core.connection}`, children: CONNECTION_BANNER[core.connection] }) : null,
11521
12018
  /* @__PURE__ */ jsx6(ShareNotice, {}),
11522
12019
  /* @__PURE__ */ jsx6(Stage, {}),
11523
12020
  /* @__PURE__ */ jsx6(Captions, {}),
@@ -11551,7 +12048,7 @@ var REFUSED_TITLES = {
11551
12048
  registration_required: "Register to join this meeting"
11552
12049
  };
11553
12050
  function ExitScreen({ onLeave }) {
11554
- const { phase, reason, host, snapshot, actions, call } = useMeetCore();
12051
+ const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
11555
12052
  const [busy, setBusy] = useState4(false);
11556
12053
  const failure = snapshot?.failure ?? null;
11557
12054
  const rejoin = /* @__PURE__ */ jsx6(
@@ -11591,7 +12088,7 @@ function ExitScreen({ onLeave }) {
11591
12088
  if (phase.startsWith("refused:")) {
11592
12089
  const code = phase.slice("refused:".length);
11593
12090
  title = REFUSED_TITLES[code] ?? "You can't join this meeting";
11594
- line = failure?.remedy ?? null;
12091
+ line = code === "too_early" || code === "registration_required" || code === "cancelled" ? failure?.remedy ?? null : null;
11595
12092
  actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11596
12093
  /* @__PURE__ */ jsx6(Button, { type: "button", variant: "outline", onClick: actions.retry, children: "Try again" }),
11597
12094
  home
@@ -11607,7 +12104,7 @@ function ExitScreen({ onLeave }) {
11607
12104
  break;
11608
12105
  case "disconnected":
11609
12106
  title = "You lost your connection to the meeting";
11610
- line = failure?.remedy ?? "Check your internet connection, then rejoin.";
12107
+ 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.";
11611
12108
  notice = "disconnected";
11612
12109
  actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11613
12110
  rejoin,
@@ -11641,6 +12138,10 @@ function ExitScreen({ onLeave }) {
11641
12138
  case "ended":
11642
12139
  case "record":
11643
12140
  title = reason === "time_limit" ? "The meeting reached its time limit" : "The meeting has ended";
12141
+ if (failure?.reason === "ended" && failure.remedy.length > 0 && failure.message.startsWith("This run")) {
12142
+ title = failure.message.replace(/\.$/, "");
12143
+ line = failure.remedy;
12144
+ }
11644
12145
  notice = "meeting-ended";
11645
12146
  break;
11646
12147
  default:
@@ -12211,8 +12712,16 @@ function MeetingSkinRootBody({
12211
12712
  if (now.phase !== "resolving" && now.roomName !== null && now.roomName !== roomName && now.call === null) {
12212
12713
  host.store.dispatch({ type: "reset" });
12213
12714
  }
12214
- if (host.store.snapshot().phase === "resolving") host.store.dispatch({ type: "resolved" });
12215
- }, [host, roomName, phase]);
12715
+ if (host.store.snapshot().phase !== "resolving") return;
12716
+ if (given?.linkRun?.reason == null && host.engine.resume(roomName)) return;
12717
+ const reason = given?.linkRun?.reason ?? null;
12718
+ if (reason !== null && given?.linkRun !== void 0) {
12719
+ const words = linkRunWords(given.linkRun);
12720
+ host.store.patch({ failure: { message: words.message, remedy: words.remedy, reason } });
12721
+ if (host.store.dispatch({ type: "link_state", reason })) return;
12722
+ }
12723
+ host.store.dispatch({ type: "resolved" });
12724
+ }, [host, roomName, phase, given]);
12216
12725
  useEffect8(() => {
12217
12726
  if (given === void 0) return;
12218
12727
  if (host.store.snapshot().meeting !== null) return;
@@ -12239,6 +12748,31 @@ function MeetingSkinRootBody({
12239
12748
  }
12240
12749
  );
12241
12750
  }
12751
+ function linkRunWords(run) {
12752
+ if (run.reason === "cancelled") {
12753
+ return {
12754
+ message: "This occurrence of the meeting was cancelled.",
12755
+ remedy: "The series continues; open the same link at its next time."
12756
+ };
12757
+ }
12758
+ if (run.nextStart === null) {
12759
+ return {
12760
+ message: "This meeting has ended.",
12761
+ remedy: "The link still opens its record: summary, notes and transcript. Nobody can rejoin."
12762
+ };
12763
+ }
12764
+ const when2 = new Date(run.nextStart).toLocaleString(void 0, {
12765
+ weekday: "short",
12766
+ month: "short",
12767
+ day: "numeric",
12768
+ hour: "numeric",
12769
+ minute: "2-digit"
12770
+ });
12771
+ return {
12772
+ message: "This run of the meeting has ended.",
12773
+ remedy: `The next one starts ${when2}. Open the same link then.`
12774
+ };
12775
+ }
12242
12776
 
12243
12777
  // src/react/calls-ui.tsx
12244
12778
  import { useEffect as useEffect9 } from "react";
@@ -12465,6 +12999,7 @@ export {
12465
12999
  CAPTION_LANGUAGES,
12466
13000
  CONSENT_NOTICE_ATTRIBUTE,
12467
13001
  DATA_TOPICS,
13002
+ DEFAULT_CONNECTION_POLICY,
12468
13003
  DEFAULT_EFFECTS,
12469
13004
  DEFAULT_PAGE_SIZE,
12470
13005
  EMPTY_COLLAB,
@@ -12482,6 +13017,7 @@ export {
12482
13017
  IncomingCallHost,
12483
13018
  JOIN_REFUSAL_REASONS,
12484
13019
  LISTEN_ONLY_NOTICE,
13020
+ LIVEKIT_BASE_DELAYS_MS,
12485
13021
  LIVE_PHASES,
12486
13022
  MEETING_INVITE_KIND,
12487
13023
  MEETING_INVITE_VERSION,
@@ -12511,6 +13047,7 @@ export {
12511
13047
  PARTICIPANT_STANDING_ATTRIBUTE,
12512
13048
  REACTION_TTL_MS,
12513
13049
  REFUSED_ABSENCE,
13050
+ RESUME_WINDOW_MS,
12514
13051
  ROLE_RANK,
12515
13052
  ROOM_EVENTS,
12516
13053
  ROOM_EVENT_PREFIX,
@@ -12541,6 +13078,7 @@ export {
12541
13078
  attendanceTotals,
12542
13079
  attendeeKey,
12543
13080
  autoAssign,
13081
+ autoRejoinDelayMs,
12544
13082
  availableActivities,
12545
13083
  breakoutRemainingMs,
12546
13084
  callInviteAction,
@@ -12555,9 +13093,11 @@ export {
12555
13093
  clockPosition,
12556
13094
  collabFromState,
12557
13095
  computeStage,
13096
+ connectionPolicyFrom,
12558
13097
  countLabel,
12559
13098
  createCallCenter,
12560
13099
  createCallInviteHandler,
13100
+ createConnectionSupervisor,
12561
13101
  createDeviceManager,
12562
13102
  createInRoomClient,
12563
13103
  createLiveKitDriver,
@@ -12569,11 +13109,15 @@ export {
12569
13109
  createMeetingRecordCache,
12570
13110
  createMeetingSession,
12571
13111
  createMemoryDeviceStorage,
13112
+ createReachabilityProbe,
13113
+ createReconnectPolicy,
12572
13114
  createRecordingController,
12573
13115
  createRoomEngine,
12574
13116
  createTokenClient,
12575
13117
  createWebDeviceStorage,
12576
13118
  createWebEffectsStorage,
13119
+ createWebNetworkPort,
13120
+ createWebResumeStorage,
12577
13121
  createWebRoomTokenStorage,
12578
13122
  defaultCaptionLanguage,
12579
13123
  displayNameFor,
@@ -12586,6 +13130,7 @@ export {
12586
13130
  guestRecordAccessSetting,
12587
13131
  hasEnded,
12588
13132
  hostUserIds,
13133
+ httpUrlOf,
12589
13134
  invalidResponse,
12590
13135
  isGuest,
12591
13136
  isGuestIdentity,
@@ -12599,6 +13144,7 @@ export {
12599
13144
  isRealName,
12600
13145
  isRefusedPhase,
12601
13146
  isRoomEventTopic,
13147
+ isUnannouncedDeparture,
12602
13148
  isWaitingPhase,
12603
13149
  learnName,
12604
13150
  learnNames,
@@ -12621,6 +13167,7 @@ export {
12621
13167
  meetingTopic,
12622
13168
  meetingWhen,
12623
13169
  mergeCaption,
13170
+ mergeGhosts,
12624
13171
  mergeRecord,
12625
13172
  microphoneNoticeCode,
12626
13173
  moveToRoom,