@ai-matrx/meet 0.11.28 → 0.11.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/react.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) {
@@ -1115,6 +1462,10 @@ function nextPhase(from, event) {
1115
1462
  return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1116
1463
  case "join":
1117
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;
1118
1469
  case "back_to_prejoin":
1119
1470
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1120
1471
  case "retry":
@@ -3254,6 +3605,7 @@ var EMPTY = {
3254
3605
  phaseNotice: null,
3255
3606
  roomName: null,
3256
3607
  connection: "stable",
3608
+ connectionBanner: false,
3257
3609
  share: NO_SHARE,
3258
3610
  call: null,
3259
3611
  waiting: null,
@@ -4535,6 +4887,18 @@ function createRoomEngine(options) {
4535
4887
  let disposed = false;
4536
4888
  let moving = false;
4537
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
+ }
4538
4902
  async function dropRoom() {
4539
4903
  const target = driver;
4540
4904
  driver = null;
@@ -4545,6 +4909,7 @@ function createRoomEngine(options) {
4545
4909
  store.setLocalTrack("microphone", IDLE_TRACK);
4546
4910
  store.setLocalTrack("camera", IDLE_TRACK);
4547
4911
  store.patch({ share: NO_SHARE, connection: "stable" });
4912
+ clearGhosts();
4548
4913
  await target?.disconnect();
4549
4914
  }
4550
4915
  async function leaveRoom() {
@@ -4612,7 +4977,7 @@ function createRoomEngine(options) {
4612
4977
  participants(list) {
4613
4978
  const joinedAt = new Date(now()).toISOString();
4614
4979
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
4615
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
4980
+ projectRoster(list, joinedAt);
4616
4981
  const snapshot = store.snapshot();
4617
4982
  const newcomer = snapshot.participants.some(
4618
4983
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -4624,6 +4989,9 @@ function createRoomEngine(options) {
4624
4989
  );
4625
4990
  }
4626
4991
  },
4992
+ participantLeft(who, reason) {
4993
+ departures.set(who, reason);
4994
+ },
4627
4995
  roomMetadata(metadata) {
4628
4996
  let spotlight = null;
4629
4997
  if (metadata.length > 0) {
@@ -4650,12 +5018,8 @@ function createRoomEngine(options) {
4650
5018
  store.setQuality(asParticipantIdentity(who), value);
4651
5019
  },
4652
5020
  connectionState(state) {
4653
- if (state === "connected") {
4654
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
4655
- store.dispatch({ type: "connected" });
4656
- } else if (state === "reconnecting") {
4657
- store.patch({ connection: "reconnecting" });
4658
- }
5021
+ supervisor.driverState(state);
5022
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
4659
5023
  },
4660
5024
  transcription(event) {
4661
5025
  const snapshot = store.snapshot();
@@ -4788,6 +5152,7 @@ function createRoomEngine(options) {
4788
5152
  };
4789
5153
  async function resolveExitAndApply(hint) {
4790
5154
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5155
+ lastRequest = requestAsNow();
4791
5156
  await dropRoom();
4792
5157
  let server;
4793
5158
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -4801,7 +5166,7 @@ function createRoomEngine(options) {
4801
5166
  }
4802
5167
  if (disposed) return;
4803
5168
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
4804
- store.patch({ connection: resolved.connection });
5169
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
4805
5170
  if (resolved.to === "waiting:knocking") {
4806
5171
  store.patch({
4807
5172
  waiting: {
@@ -4833,6 +5198,8 @@ function createRoomEngine(options) {
4833
5198
  ...request.observe === true ? { observe: true } : {}
4834
5199
  });
4835
5200
  current = token;
5201
+ lastServerUrl = token.serverUrl;
5202
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
4836
5203
  store.patch({
4837
5204
  localIdentity: token.identity,
4838
5205
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -4937,7 +5304,59 @@ function createRoomEngine(options) {
4937
5304
  throw normalized;
4938
5305
  }
4939
5306
  }
4940
- 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 = {
4941
5360
  join(request) {
4942
5361
  if (joining !== null) return joining;
4943
5362
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -4982,6 +5401,35 @@ function createRoomEngine(options) {
4982
5401
  });
4983
5402
  return joining;
4984
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
+ },
4985
5433
  async moveToBreakout(roomId) {
4986
5434
  const request = lastRequest;
4987
5435
  if (request === null || driver === null) {
@@ -5149,6 +5597,8 @@ function createRoomEngine(options) {
5149
5597
  token: () => current,
5150
5598
  async dispose() {
5151
5599
  disposed = true;
5600
+ supervisor.dispose();
5601
+ clearGhosts();
5152
5602
  if (driver !== null) {
5153
5603
  const target = driver;
5154
5604
  driver = null;
@@ -5157,6 +5607,7 @@ function createRoomEngine(options) {
5157
5607
  store.reset();
5158
5608
  }
5159
5609
  };
5610
+ return engine;
5160
5611
  }
5161
5612
  function canModerate(participant) {
5162
5613
  return participant !== void 0 && isHostRole(participant.role);
@@ -5467,8 +5918,11 @@ function mapConnectionState(state) {
5467
5918
  case "connecting":
5468
5919
  return "connecting";
5469
5920
  case "reconnecting":
5470
- case "signalReconnecting":
5471
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";
5472
5926
  default:
5473
5927
  return "disconnected";
5474
5928
  }
@@ -5549,8 +6003,12 @@ function createLiveKitDriver(handlers, options = {}) {
5549
6003
  // R5: large meetings degrade gracefully instead of melting a laptop.
5550
6004
  adaptiveStream: true,
5551
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(),
5552
6009
  ...options.roomOptions
5553
6010
  });
6011
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module.DisconnectReason?.[reason] ?? String(reason);
5554
6012
  const roster = () => {
5555
6013
  const local = snapshotParticipant(created.localParticipant, true);
5556
6014
  const remotes = [...created.remoteParticipants.values()].map(
@@ -5568,7 +6026,10 @@ function createLiveKitDriver(handlers, options = {}) {
5568
6026
  if (track.kind !== Track.Kind.Audio) return;
5569
6027
  for (const element of track.detach()) element.remove();
5570
6028
  });
5571
- 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) => {
5572
6033
  handlers.roomMetadata?.(metadata ?? "");
5573
6034
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
5574
6035
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -5585,8 +6046,7 @@ function createLiveKitDriver(handlers, options = {}) {
5585
6046
  }).on(RoomEvent.Disconnected, (reason) => {
5586
6047
  audioContainer?.remove();
5587
6048
  audioContainer = null;
5588
- const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
5589
- handlers.disconnected(named);
6049
+ handlers.disconnected(reasonName(reason));
5590
6050
  });
5591
6051
  try {
5592
6052
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5976,7 +6436,14 @@ function MeetRuntime(props) {
5976
6436
  effectsStorage,
5977
6437
  resolveDevice: (kind) => devicesHolder.current?.resolve(kind) ?? null,
5978
6438
  // §3.4: on every LiveKit disconnect, ask the server what happened to this connection.
5979
- 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
+ }
5980
6447
  });
5981
6448
  const meetings = createMeetingSession({
5982
6449
  manager,
@@ -6304,7 +6771,7 @@ function participantAttributes(p) {
6304
6771
  "data-meet-participant-role": observedRole(p.role, p.standing),
6305
6772
  "data-meet-participant-self": String(p.isLocal)
6306
6773
  };
6307
- const quality = observedQuality(p.quality);
6774
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
6308
6775
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
6309
6776
  return attrs;
6310
6777
  }
@@ -8523,6 +8990,7 @@ function useMeetCore() {
8523
8990
  reason: snapshot?.reason ?? null,
8524
8991
  phaseNotice: snapshot?.phaseNotice ?? null,
8525
8992
  connection: snapshot?.connection ?? "stable",
8993
+ connectionBanner: snapshot?.connectionBanner ?? false,
8526
8994
  share: snapshot?.share ?? NO_SHARE,
8527
8995
  call: snapshot?.call ?? null,
8528
8996
  actions
@@ -9789,7 +10257,8 @@ function ParticipantTile({
9789
10257
  participant.displayName,
9790
10258
  participant.isLocal ? " (you)" : "",
9791
10259
  participant.isAgent ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__badge", children: "AI" }) : null,
9792
- /* @__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
9793
10262
  ] }),
9794
10263
  !participant.micEnabled ? /* @__PURE__ */ jsx6(MicOffIcon, { width: 14, height: 14 }) : null,
9795
10264
  /* @__PURE__ */ jsx6(QualityBars, { quality: participant.quality })
@@ -9832,6 +10301,12 @@ function QualityBars({ quality }) {
9832
10301
  const label = quality === "lost" ? "Connection lost" : quality === "poor" ? "Poor connection" : `Connection ${quality}`;
9833
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 }) });
9834
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
+ };
9835
10310
  function Stage({ pageSize }) {
9836
10311
  const stage = useStage(pageSize);
9837
10312
  const columns = gridColumns(stage.tiles.length);
@@ -11539,7 +12014,7 @@ function MeetSkinRoom({
11539
12014
  headerControls !== void 0 && headerControls !== null ? /* @__PURE__ */ jsx6("span", { className: "mx-meet__header-controls", "data-meet-header-controls": "", children: headerControls }) : null,
11540
12015
  isHost ? /* @__PURE__ */ jsx6(HostMenu, {}) : null
11541
12016
  ] }),
11542
- 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,
11543
12018
  /* @__PURE__ */ jsx6(ShareNotice, {}),
11544
12019
  /* @__PURE__ */ jsx6(Stage, {}),
11545
12020
  /* @__PURE__ */ jsx6(Captions, {}),
@@ -11573,7 +12048,7 @@ var REFUSED_TITLES = {
11573
12048
  registration_required: "Register to join this meeting"
11574
12049
  };
11575
12050
  function ExitScreen({ onLeave }) {
11576
- const { phase, reason, host, snapshot, actions, call } = useMeetCore();
12051
+ const { phase, reason, host, snapshot, actions, call, connection } = useMeetCore();
11577
12052
  const [busy, setBusy] = useState4(false);
11578
12053
  const failure = snapshot?.failure ?? null;
11579
12054
  const rejoin = /* @__PURE__ */ jsx6(
@@ -11629,7 +12104,7 @@ function ExitScreen({ onLeave }) {
11629
12104
  break;
11630
12105
  case "disconnected":
11631
12106
  title = "You lost your connection to the meeting";
11632
- 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.";
11633
12108
  notice = "disconnected";
11634
12109
  actionsNode = /* @__PURE__ */ jsxs3(Fragment, { children: [
11635
12110
  rejoin,
@@ -12238,6 +12713,7 @@ function MeetingSkinRootBody({
12238
12713
  host.store.dispatch({ type: "reset" });
12239
12714
  }
12240
12715
  if (host.store.snapshot().phase !== "resolving") return;
12716
+ if (given?.linkRun?.reason == null && host.engine.resume(roomName)) return;
12241
12717
  const reason = given?.linkRun?.reason ?? null;
12242
12718
  if (reason !== null && given?.linkRun !== void 0) {
12243
12719
  const words = linkRunWords(given.linkRun);
@@ -12523,6 +12999,7 @@ export {
12523
12999
  CAPTION_LANGUAGES,
12524
13000
  CONSENT_NOTICE_ATTRIBUTE,
12525
13001
  DATA_TOPICS,
13002
+ DEFAULT_CONNECTION_POLICY,
12526
13003
  DEFAULT_EFFECTS,
12527
13004
  DEFAULT_PAGE_SIZE,
12528
13005
  EMPTY_COLLAB,
@@ -12540,6 +13017,7 @@ export {
12540
13017
  IncomingCallHost,
12541
13018
  JOIN_REFUSAL_REASONS,
12542
13019
  LISTEN_ONLY_NOTICE,
13020
+ LIVEKIT_BASE_DELAYS_MS,
12543
13021
  LIVE_PHASES,
12544
13022
  MEETING_INVITE_KIND,
12545
13023
  MEETING_INVITE_VERSION,
@@ -12569,6 +13047,7 @@ export {
12569
13047
  PARTICIPANT_STANDING_ATTRIBUTE,
12570
13048
  REACTION_TTL_MS,
12571
13049
  REFUSED_ABSENCE,
13050
+ RESUME_WINDOW_MS,
12572
13051
  ROLE_RANK,
12573
13052
  ROOM_EVENTS,
12574
13053
  ROOM_EVENT_PREFIX,
@@ -12599,6 +13078,7 @@ export {
12599
13078
  attendanceTotals,
12600
13079
  attendeeKey,
12601
13080
  autoAssign,
13081
+ autoRejoinDelayMs,
12602
13082
  availableActivities,
12603
13083
  breakoutRemainingMs,
12604
13084
  callInviteAction,
@@ -12613,9 +13093,11 @@ export {
12613
13093
  clockPosition,
12614
13094
  collabFromState,
12615
13095
  computeStage,
13096
+ connectionPolicyFrom,
12616
13097
  countLabel,
12617
13098
  createCallCenter,
12618
13099
  createCallInviteHandler,
13100
+ createConnectionSupervisor,
12619
13101
  createDeviceManager,
12620
13102
  createInRoomClient,
12621
13103
  createLiveKitDriver,
@@ -12627,11 +13109,15 @@ export {
12627
13109
  createMeetingRecordCache,
12628
13110
  createMeetingSession,
12629
13111
  createMemoryDeviceStorage,
13112
+ createReachabilityProbe,
13113
+ createReconnectPolicy,
12630
13114
  createRecordingController,
12631
13115
  createRoomEngine,
12632
13116
  createTokenClient,
12633
13117
  createWebDeviceStorage,
12634
13118
  createWebEffectsStorage,
13119
+ createWebNetworkPort,
13120
+ createWebResumeStorage,
12635
13121
  createWebRoomTokenStorage,
12636
13122
  defaultCaptionLanguage,
12637
13123
  displayNameFor,
@@ -12644,6 +13130,7 @@ export {
12644
13130
  guestRecordAccessSetting,
12645
13131
  hasEnded,
12646
13132
  hostUserIds,
13133
+ httpUrlOf,
12647
13134
  invalidResponse,
12648
13135
  isGuest,
12649
13136
  isGuestIdentity,
@@ -12657,6 +13144,7 @@ export {
12657
13144
  isRealName,
12658
13145
  isRefusedPhase,
12659
13146
  isRoomEventTopic,
13147
+ isUnannouncedDeparture,
12660
13148
  isWaitingPhase,
12661
13149
  learnName,
12662
13150
  learnNames,
@@ -12679,6 +13167,7 @@ export {
12679
13167
  meetingTopic,
12680
13168
  meetingWhen,
12681
13169
  mergeCaption,
13170
+ mergeGhosts,
12682
13171
  mergeRecord,
12683
13172
  microphoneNoticeCode,
12684
13173
  moveToRoom,