@ai-matrx/meet 0.11.27 → 0.11.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -204,6 +204,352 @@ function invalidResponse(operation, detail) {
204
204
  );
205
205
  }
206
206
 
207
+ // src/core/connection-supervisor.ts
208
+ var DEFAULT_CONNECTION_POLICY = {
209
+ bannerDelayMs: 2e3,
210
+ autoRejoin: true,
211
+ autoRejoinMaxAttempts: 5,
212
+ reconnectGraceSeconds: 30
213
+ };
214
+ function clampInt(value, min, max, fallback) {
215
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
216
+ return Math.min(max, Math.max(min, Math.round(value)));
217
+ }
218
+ function connectionPolicyFrom(raw) {
219
+ if (typeof raw !== "object" || raw === null) return DEFAULT_CONNECTION_POLICY;
220
+ const row = raw;
221
+ const d = DEFAULT_CONNECTION_POLICY;
222
+ return {
223
+ bannerDelayMs: clampInt(row["reconnect_banner_delay_ms"], 0, 1e4, d.bannerDelayMs),
224
+ autoRejoin: typeof row["auto_rejoin"] === "boolean" ? row["auto_rejoin"] : d.autoRejoin,
225
+ autoRejoinMaxAttempts: clampInt(row["auto_rejoin_max_attempts"], 0, 20, d.autoRejoinMaxAttempts),
226
+ reconnectGraceSeconds: clampInt(row["reconnect_grace_seconds"], 0, 600, d.reconnectGraceSeconds)
227
+ };
228
+ }
229
+ var LIVEKIT_BASE_DELAYS_MS = [0, 300, 1200, 2700, 4800, 7e3];
230
+ function createReconnectPolicy(options = {}) {
231
+ const delays = options.delaysMs !== void 0 && options.delaysMs.length > 0 ? options.delaysMs : LIVEKIT_BASE_DELAYS_MS;
232
+ const giveUpMs = options.giveUpMs ?? 45e3;
233
+ const maxAttempts = options.maxAttempts ?? 12;
234
+ const random = options.random ?? Math.random;
235
+ return {
236
+ nextRetryDelayInMs({ retryCount, elapsedMs }) {
237
+ if (retryCount >= maxAttempts || elapsedMs >= giveUpMs) return null;
238
+ const base = delays[Math.min(retryCount, delays.length - 1)] ?? 0;
239
+ const spread = Math.min(2e3, Math.max(500, base / 2));
240
+ return Math.round(base + random() * spread);
241
+ }
242
+ };
243
+ }
244
+ function createWebNetworkPort() {
245
+ const scope = globalThis;
246
+ return {
247
+ online: () => scope.navigator?.onLine !== false,
248
+ subscribe(listener) {
249
+ if (typeof scope.addEventListener !== "function") return () => void 0;
250
+ const on = () => listener(true);
251
+ const off = () => listener(false);
252
+ scope.addEventListener("online", on);
253
+ scope.addEventListener("offline", off);
254
+ return () => {
255
+ scope.removeEventListener?.("online", on);
256
+ scope.removeEventListener?.("offline", off);
257
+ };
258
+ }
259
+ };
260
+ }
261
+ function createReachabilityProbe(urls, timeoutMs = 4e3) {
262
+ return async () => {
263
+ const targets = urls().filter((url) => url.length > 0);
264
+ if (targets.length === 0 || typeof fetch !== "function") return true;
265
+ const one = async (url) => {
266
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
267
+ const timer = setTimeout(() => controller?.abort(), timeoutMs);
268
+ try {
269
+ await fetch(url, {
270
+ method: "GET",
271
+ mode: "no-cors",
272
+ cache: "no-store",
273
+ credentials: "omit",
274
+ ...controller !== null ? { signal: controller.signal } : {}
275
+ });
276
+ return true;
277
+ } catch {
278
+ return false;
279
+ } finally {
280
+ clearTimeout(timer);
281
+ }
282
+ };
283
+ const answers = await Promise.all(targets.map(one));
284
+ return answers.every(Boolean);
285
+ };
286
+ }
287
+ function httpUrlOf(serverUrl) {
288
+ return serverUrl.replace(/^ws(s?):\/\//i, "http$1://");
289
+ }
290
+ var RESUME_WINDOW_MS = 15 * 6e4;
291
+ var RESUME_KEY = "mx-meet:resume:";
292
+ function createWebResumeStorage() {
293
+ const storage = globalThis.sessionStorage;
294
+ if (storage === void 0) return void 0;
295
+ return {
296
+ load(roomName) {
297
+ try {
298
+ const raw = storage.getItem(RESUME_KEY + roomName);
299
+ if (raw === null) return null;
300
+ const parsed = JSON.parse(raw);
301
+ return typeof parsed?.at === "number" && typeof parsed.request?.roomName === "string" ? parsed : null;
302
+ } catch {
303
+ return null;
304
+ }
305
+ },
306
+ save(roomName, marker) {
307
+ try {
308
+ storage.setItem(RESUME_KEY + roomName, JSON.stringify(marker));
309
+ } catch {
310
+ }
311
+ },
312
+ clear(roomName) {
313
+ try {
314
+ storage.removeItem(RESUME_KEY + roomName);
315
+ } catch {
316
+ }
317
+ }
318
+ };
319
+ }
320
+ var CLEARS_RESUME = (phase) => phase === "left" || phase === "removed" || phase === "superseded" || phase === "inactive" || phase === "ended" || phase === "record" || phase === "prejoin" || phase.startsWith("refused:") || phase.startsWith("waiting:");
321
+ function autoRejoinDelayMs(attempt, random = Math.random) {
322
+ const base = Math.min(15e3, 1e3 * 2 ** Math.max(0, attempt));
323
+ return Math.round(base * (0.5 + random()));
324
+ }
325
+ function createConnectionSupervisor(options) {
326
+ const { store, network } = options;
327
+ const now = options.now ?? (() => Date.now());
328
+ const random = options.random ?? Math.random;
329
+ const probeEveryMs = options.probeEveryMs ?? 3e3;
330
+ let driverRegion = null;
331
+ let probeTimer = null;
332
+ let probing = false;
333
+ let bannerTimer = null;
334
+ let rejoinTimer = null;
335
+ let rejoinAttempts = 0;
336
+ let lastPhase = store.snapshot().phase;
337
+ let lastSavedAt = 0;
338
+ let disposed = false;
339
+ const inCall = () => store.snapshot().phase === "in_call";
340
+ const dropped = () => store.snapshot().phase === "disconnected";
341
+ function setRegion(next) {
342
+ if (store.snapshot().connection !== next) store.patch({ connection: next });
343
+ }
344
+ function regionFor(reachable) {
345
+ if (!network.online()) return "offline";
346
+ if (reachable === false) return "server_unreachable";
347
+ if (dropped()) return reachable === true ? "stable" : store.snapshot().connection;
348
+ return driverRegion ?? "stable";
349
+ }
350
+ function needsWatch() {
351
+ const snap = store.snapshot();
352
+ if (dropped()) return true;
353
+ return inCall() && (driverRegion !== null || snap.connection !== "stable");
354
+ }
355
+ function stopProbe() {
356
+ if (probeTimer !== null) clearTimeout(probeTimer);
357
+ probeTimer = null;
358
+ }
359
+ function scheduleProbe(delayMs) {
360
+ if (disposed || probeTimer !== null) return;
361
+ probeTimer = setTimeout(() => {
362
+ probeTimer = null;
363
+ void runProbe();
364
+ }, delayMs);
365
+ }
366
+ async function runProbe() {
367
+ if (disposed || probing) return;
368
+ if (!needsWatch()) return;
369
+ if (!network.online()) {
370
+ setRegion("offline");
371
+ scheduleProbe(probeEveryMs);
372
+ return;
373
+ }
374
+ probing = true;
375
+ let reachable = false;
376
+ try {
377
+ reachable = await options.probe();
378
+ } catch {
379
+ reachable = false;
380
+ } finally {
381
+ probing = false;
382
+ }
383
+ if (disposed) return;
384
+ if (inCall() || dropped()) setRegion(regionFor(reachable));
385
+ if (dropped() && reachable) maybeRejoin();
386
+ if (needsWatch()) scheduleProbe(Math.round(probeEveryMs * (0.75 + random() * 0.5)));
387
+ }
388
+ function syncBanner() {
389
+ const snap = store.snapshot();
390
+ const unstable = snap.phase === "in_call" && snap.connection !== "stable";
391
+ if (!unstable) {
392
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
393
+ bannerTimer = null;
394
+ if (snap.connectionBanner) store.patch({ connectionBanner: false });
395
+ return;
396
+ }
397
+ if (snap.connectionBanner || bannerTimer !== null) return;
398
+ const delay = options.policy().bannerDelayMs;
399
+ if (delay <= 0) {
400
+ store.patch({ connectionBanner: true });
401
+ return;
402
+ }
403
+ bannerTimer = setTimeout(() => {
404
+ bannerTimer = null;
405
+ const latest = store.snapshot();
406
+ if (latest.phase === "in_call" && latest.connection !== "stable" && !latest.connectionBanner) {
407
+ store.patch({ connectionBanner: true });
408
+ }
409
+ }, delay);
410
+ }
411
+ function stopRejoin() {
412
+ if (rejoinTimer !== null) clearTimeout(rejoinTimer);
413
+ rejoinTimer = null;
414
+ }
415
+ function maybeRejoin() {
416
+ if (disposed || rejoinTimer !== null || !dropped()) return;
417
+ const policy = options.policy();
418
+ if (!policy.autoRejoin) return;
419
+ if (rejoinAttempts >= policy.autoRejoinMaxAttempts) return;
420
+ if (!network.online()) return;
421
+ const attempt = rejoinAttempts;
422
+ rejoinTimer = setTimeout(() => {
423
+ rejoinTimer = null;
424
+ if (disposed || !dropped() || !network.online()) return;
425
+ rejoinAttempts = attempt + 1;
426
+ options.report?.("info", `meet: rejoining by itself (attempt ${rejoinAttempts} of ${policy.autoRejoinMaxAttempts})`);
427
+ void options.autoRejoin().catch(() => void 0);
428
+ }, autoRejoinDelayMs(attempt, random));
429
+ }
430
+ function saveMarker(force) {
431
+ const resume = options.resume;
432
+ const snap = store.snapshot();
433
+ if (resume === void 0 || snap.roomName === null) return;
434
+ const at = now();
435
+ if (!force && at - lastSavedAt < 1e4) return;
436
+ const request = options.currentRequest();
437
+ if (request === null) return;
438
+ lastSavedAt = at;
439
+ resume.save(snap.roomName, { request, at });
440
+ }
441
+ const unsubscribeStore = store.subscribe(() => {
442
+ if (disposed) return;
443
+ const snap = store.snapshot();
444
+ const phase = snap.phase;
445
+ const changed = phase !== lastPhase;
446
+ lastPhase = phase;
447
+ if (changed) {
448
+ if (phase === "in_call") {
449
+ rejoinAttempts = 0;
450
+ stopRejoin();
451
+ saveMarker(true);
452
+ } else if (phase === "disconnected") {
453
+ driverRegion = null;
454
+ saveMarker(true);
455
+ stopProbe();
456
+ scheduleProbe(0);
457
+ } else {
458
+ stopRejoin();
459
+ if (phase !== "joining") driverRegion = null;
460
+ if (CLEARS_RESUME(phase) && snap.roomName !== null) options.resume?.clear(snap.roomName);
461
+ }
462
+ } else if (phase === "in_call" || phase === "disconnected") {
463
+ saveMarker(false);
464
+ }
465
+ syncBanner();
466
+ });
467
+ const unsubscribeNetwork = network.subscribe((online) => {
468
+ if (disposed) return;
469
+ if (!online) {
470
+ if (inCall() || dropped()) setRegion("offline");
471
+ stopRejoin();
472
+ stopProbe();
473
+ if (needsWatch()) scheduleProbe(probeEveryMs);
474
+ return;
475
+ }
476
+ stopProbe();
477
+ if (needsWatch()) scheduleProbe(0);
478
+ });
479
+ return {
480
+ driverState(state) {
481
+ if (disposed) return;
482
+ if (state === "connected") {
483
+ driverRegion = null;
484
+ if (inCall() || store.snapshot().phase === "joining") setRegion(network.online() ? "stable" : "offline");
485
+ if (!needsWatch()) stopProbe();
486
+ return;
487
+ }
488
+ if (state === "reconnecting" || state === "signal_reconnecting") {
489
+ driverRegion = state;
490
+ if (inCall()) {
491
+ setRegion(network.online() ? state : "offline");
492
+ stopProbe();
493
+ scheduleProbe(0);
494
+ }
495
+ }
496
+ },
497
+ exitConnection() {
498
+ return network.online() ? "server_unreachable" : "offline";
499
+ },
500
+ resumable(roomName) {
501
+ const resume = options.resume;
502
+ if (resume === void 0) return null;
503
+ const marker = resume.load(roomName);
504
+ if (marker === null) return null;
505
+ if (now() - marker.at > RESUME_WINDOW_MS) {
506
+ resume.clear(roomName);
507
+ return null;
508
+ }
509
+ return marker;
510
+ },
511
+ dispose() {
512
+ disposed = true;
513
+ stopProbe();
514
+ stopRejoin();
515
+ if (bannerTimer !== null) clearTimeout(bannerTimer);
516
+ bannerTimer = null;
517
+ unsubscribeStore();
518
+ unsubscribeNetwork();
519
+ }
520
+ };
521
+ }
522
+ var DELIBERATE_DEPARTURES = /* @__PURE__ */ new Set([
523
+ "CLIENT_INITIATED",
524
+ "PARTICIPANT_REMOVED",
525
+ "ROOM_DELETED",
526
+ "ROOM_CLOSED",
527
+ "DUPLICATE_IDENTITY",
528
+ "USER_REJECTED",
529
+ "USER_UNAVAILABLE",
530
+ "SIP_TRUNK_FAILURE"
531
+ ]);
532
+ function isUnannouncedDeparture(reason) {
533
+ return reason === null || !DELIBERATE_DEPARTURES.has(reason);
534
+ }
535
+ function mergeGhosts(live, ghosts, at) {
536
+ const present = new Set(live.map((p) => p.identity));
537
+ const kept = /* @__PURE__ */ new Map();
538
+ const extra = [];
539
+ for (const [identity, ghost] of ghosts) {
540
+ if (present.has(identity) || ghost.until <= at) continue;
541
+ kept.set(identity, ghost);
542
+ extra.push({
543
+ ...ghost.participant,
544
+ micEnabled: false,
545
+ cameraEnabled: false,
546
+ screenSharing: false,
547
+ reconnecting: true
548
+ });
549
+ }
550
+ return { roster: [...live, ...extra], kept };
551
+ }
552
+
207
553
  // src/core/tokens.ts
208
554
  function createWebRoomTokenStorage(onDiagnostic) {
209
555
  const store = globalThis.localStorage;
@@ -389,7 +735,8 @@ function parseRoomToken(body, roomName) {
389
735
  admissionState: readString(payload, "admission_state") ?? void 0,
390
736
  pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
391
737
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null,
392
- observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone"
738
+ observersVisibleTo: readString(payload, "observers_visible_to") === "hosts" ? "hosts" : "everyone",
739
+ connectionPolicy: connectionPolicyFrom(payload["connection_policy"])
393
740
  };
394
741
  }
395
742
  function createTokenClient(options) {
@@ -1115,8 +1462,14 @@ function nextPhase(from, event) {
1115
1462
  return from === "resolving" ? "prejoin" : null;
1116
1463
  case "unsupported":
1117
1464
  return from === "resolving" ? "unsupported" : null;
1465
+ case "link_state":
1466
+ return from === "resolving" && event.reason !== null && isJoinRefusalReason(event.reason) ? refusalPhase(event.reason) : null;
1118
1467
  case "join":
1119
1468
  return JOINABLE_FROM.has(from) ? "joining" : null;
1469
+ case "auto_rejoin":
1470
+ return from === "disconnected" ? "joining" : null;
1471
+ case "resumed":
1472
+ return from === "resolving" ? "joining" : null;
1120
1473
  case "back_to_prejoin":
1121
1474
  return from === "left" || from === "disconnected" ? "prejoin" : null;
1122
1475
  case "retry":
@@ -1523,6 +1876,8 @@ var RPCS = {
1523
1876
  getOrCreateMeeting: "meet_get_or_create_meeting",
1524
1877
  /** Resolve a durable public link, before/during/after its scheduled time. */
1525
1878
  meetingBySlug: "meet_meeting_by_slug",
1879
+ /** The current run of a durable link (open / not started / ended + next start), no pass needed. */
1880
+ linkState: "meet_link_state",
1526
1881
  /*
1527
1882
  * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1528
1883
  * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
@@ -1831,6 +2186,14 @@ function createMeetRepository(options) {
1831
2186
  if (response.error !== null) throw normalizeMeetError(response.error, operation);
1832
2187
  }
1833
2188
  }
2189
+ async function readLinkRun(slug) {
2190
+ try {
2191
+ const response = await db.rpc(RPCS.linkState, { p_slug: slug });
2192
+ return projectLinkRun(response.error === null ? response.data : null);
2193
+ } catch {
2194
+ return void 0;
2195
+ }
2196
+ }
1834
2197
  async function guard(operation, run) {
1835
2198
  let response = await run();
1836
2199
  if (response.error !== null) {
@@ -2232,11 +2595,12 @@ function createMeetRepository(options) {
2232
2595
  return projectMeeting(row);
2233
2596
  },
2234
2597
  async meetingBySlug(slug) {
2235
- const row = await guard(
2236
- "meet.meetingBySlug",
2237
- () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })
2238
- );
2239
- return withRunNoteTaker(projectMeeting(row));
2598
+ const [row, linkRun] = await Promise.all([
2599
+ guard("meet.meetingBySlug", () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })),
2600
+ readLinkRun(slug)
2601
+ ]);
2602
+ const record = withRunNoteTaker(projectMeeting(row));
2603
+ return linkRun === void 0 ? record : { ...record, linkRun };
2240
2604
  },
2241
2605
  async meeting(id) {
2242
2606
  const response = await db.from(TABLES.meetings).select(MEETING_COLUMNS).eq("id", id).single();
@@ -2425,6 +2789,15 @@ function snippetRuns(snippet) {
2425
2789
  if (last < snippet.length) runs.push({ text: snippet.slice(last), hit: false });
2426
2790
  return runs;
2427
2791
  }
2792
+ function projectLinkRun(value) {
2793
+ if (typeof value !== "object" || value === null) return void 0;
2794
+ const row = value;
2795
+ const state = row["state"];
2796
+ if (state !== "open" && state !== "not_started" && state !== "ended" && state !== "cancelled") return void 0;
2797
+ const reason = row["reason"] === "ended" || row["reason"] === "cancelled" ? row["reason"] : null;
2798
+ const nextStart = typeof row["next_start"] === "string" ? row["next_start"] : null;
2799
+ return { state, reason, nextStart };
2800
+ }
2428
2801
 
2429
2802
  // src/core/app-panel.ts
2430
2803
  function appPanelOf(meeting) {
@@ -3010,6 +3383,7 @@ var EMPTY = {
3010
3383
  phaseNotice: null,
3011
3384
  roomName: null,
3012
3385
  connection: "stable",
3386
+ connectionBanner: false,
3013
3387
  share: NO_SHARE,
3014
3388
  call: null,
3015
3389
  waiting: null,
@@ -3964,8 +4338,11 @@ function mapConnectionState(state) {
3964
4338
  case "connecting":
3965
4339
  return "connecting";
3966
4340
  case "reconnecting":
3967
- case "signalReconnecting":
3968
4341
  return "reconnecting";
4342
+ // Only the signal socket is down (S6b): its own region value, so the supervisor can tell a
4343
+ // signal blip from a full media reconnect.
4344
+ case "signalReconnecting":
4345
+ return "signal_reconnecting";
3969
4346
  default:
3970
4347
  return "disconnected";
3971
4348
  }
@@ -4046,8 +4423,12 @@ function createLiveKitDriver(handlers, options = {}) {
4046
4423
  // R5: large meetings degrade gracefully instead of melting a laptop.
4047
4424
  adaptiveStream: true,
4048
4425
  dynacast: true,
4426
+ // S6b: OUR retry schedule — jittered from attempt 0 (the SDK's default has no jitter on its
4427
+ // first two attempts), give-up limits in one place (`connection-supervisor.ts`).
4428
+ reconnectPolicy: createReconnectPolicy(),
4049
4429
  ...options.roomOptions
4050
4430
  });
4431
+ const reasonName = (reason) => reason === void 0 || reason === null ? null : module.DisconnectReason?.[reason] ?? String(reason);
4051
4432
  const roster = () => {
4052
4433
  const local = snapshotParticipant(created.localParticipant, true);
4053
4434
  const remotes = [...created.remoteParticipants.values()].map(
@@ -4065,7 +4446,10 @@ function createLiveKitDriver(handlers, options = {}) {
4065
4446
  if (track.kind !== Track.Kind.Audio) return;
4066
4447
  for (const element of track.detach()) element.remove();
4067
4448
  });
4068
- 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) => {
4449
+ created.on(RoomEvent.ParticipantConnected, roster).on(RoomEvent.ParticipantDisconnected, (participant, reason) => {
4450
+ handlers.participantLeft?.(participant.identity, reasonName(reason));
4451
+ roster();
4452
+ }).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) => {
4069
4453
  handlers.roomMetadata?.(metadata ?? "");
4070
4454
  }).on(RoomEvent.ActiveSpeakersChanged, (speakers) => {
4071
4455
  handlers.activeSpeakers(speakers.map((speaker) => speaker.identity));
@@ -4082,8 +4466,7 @@ function createLiveKitDriver(handlers, options = {}) {
4082
4466
  }).on(RoomEvent.Disconnected, (reason) => {
4083
4467
  audioContainer?.remove();
4084
4468
  audioContainer = null;
4085
- const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
4086
- handlers.disconnected(named);
4469
+ handlers.disconnected(reasonName(reason));
4087
4470
  });
4088
4471
  try {
4089
4472
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -5456,6 +5839,18 @@ function createRoomEngine(options) {
5456
5839
  let disposed = false;
5457
5840
  let moving = false;
5458
5841
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5842
+ let lastServerUrl = null;
5843
+ let connectionPolicy = DEFAULT_CONNECTION_POLICY;
5844
+ let ghosts = /* @__PURE__ */ new Map();
5845
+ let ghostTimer = null;
5846
+ let liveRoster = [];
5847
+ const departures = /* @__PURE__ */ new Map();
5848
+ function requestAsNow() {
5849
+ if (lastRequest === null) return null;
5850
+ const media = store.snapshot().media;
5851
+ if (store.snapshot().phase !== "in_call") return lastRequest;
5852
+ return { ...lastRequest, mode: media.camera.enabled ? "video" : "audio", microphone: media.microphone.enabled };
5853
+ }
5459
5854
  async function dropRoom() {
5460
5855
  const target = driver;
5461
5856
  driver = null;
@@ -5466,6 +5861,7 @@ function createRoomEngine(options) {
5466
5861
  store.setLocalTrack("microphone", IDLE_TRACK);
5467
5862
  store.setLocalTrack("camera", IDLE_TRACK);
5468
5863
  store.patch({ share: NO_SHARE, connection: "stable" });
5864
+ clearGhosts();
5469
5865
  await target?.disconnect();
5470
5866
  }
5471
5867
  async function leaveRoom() {
@@ -5533,7 +5929,7 @@ function createRoomEngine(options) {
5533
5929
  participants(list) {
5534
5930
  const joinedAt = new Date(now()).toISOString();
5535
5931
  const before = new Set(store.snapshot().participants.map((p) => p.identity));
5536
- store.setParticipants(list.map((entry) => projectParticipant(entry, joinedAt)));
5932
+ projectRoster(list, joinedAt);
5537
5933
  const snapshot = store.snapshot();
5538
5934
  const newcomer = snapshot.participants.some(
5539
5935
  (participant) => !participant.isLocal && !before.has(participant.identity)
@@ -5545,6 +5941,9 @@ function createRoomEngine(options) {
5545
5941
  );
5546
5942
  }
5547
5943
  },
5944
+ participantLeft(who, reason) {
5945
+ departures.set(who, reason);
5946
+ },
5548
5947
  roomMetadata(metadata) {
5549
5948
  let spotlight = null;
5550
5949
  if (metadata.length > 0) {
@@ -5571,12 +5970,8 @@ function createRoomEngine(options) {
5571
5970
  store.setQuality(asParticipantIdentity(who), value);
5572
5971
  },
5573
5972
  connectionState(state) {
5574
- if (state === "connected") {
5575
- if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5576
- store.dispatch({ type: "connected" });
5577
- } else if (state === "reconnecting") {
5578
- store.patch({ connection: "reconnecting" });
5579
- }
5973
+ supervisor.driverState(state);
5974
+ if (state === "connected" && store.snapshot().phase === "joining") store.dispatch({ type: "connected" });
5580
5975
  },
5581
5976
  transcription(event) {
5582
5977
  const snapshot = store.snapshot();
@@ -5709,6 +6104,7 @@ function createRoomEngine(options) {
5709
6104
  };
5710
6105
  async function resolveExitAndApply(hint) {
5711
6106
  const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
6107
+ lastRequest = requestAsNow();
5712
6108
  await dropRoom();
5713
6109
  let server;
5714
6110
  if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
@@ -5722,7 +6118,7 @@ function createRoomEngine(options) {
5722
6118
  }
5723
6119
  if (disposed) return;
5724
6120
  const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5725
- store.patch({ connection: resolved.connection });
6121
+ store.patch({ connection: server === null ? supervisor.exitConnection() : resolved.connection });
5726
6122
  if (resolved.to === "waiting:knocking") {
5727
6123
  store.patch({
5728
6124
  waiting: {
@@ -5754,6 +6150,8 @@ function createRoomEngine(options) {
5754
6150
  ...request.observe === true ? { observe: true } : {}
5755
6151
  });
5756
6152
  current = token;
6153
+ lastServerUrl = token.serverUrl;
6154
+ connectionPolicy = token.connectionPolicy ?? DEFAULT_CONNECTION_POLICY;
5757
6155
  store.patch({
5758
6156
  localIdentity: token.identity,
5759
6157
  breakoutRoomId: token.breakoutRoomId ?? null,
@@ -5858,7 +6256,59 @@ function createRoomEngine(options) {
5858
6256
  throw normalized;
5859
6257
  }
5860
6258
  }
5861
- return {
6259
+ function projectRoster(list, joinedAt) {
6260
+ const at = now();
6261
+ const previous = new Map(liveRoster.map((p) => [p.identity, p]));
6262
+ const present = new Set(list.map((p) => p.identity));
6263
+ const graceMs = connectionPolicy.reconnectGraceSeconds * 1e3;
6264
+ for (const [who, reason] of departures) {
6265
+ const last = previous.get(who);
6266
+ if (last !== void 0 && !last.isLocal && !present.has(who) && graceMs > 0 && isUnannouncedDeparture(reason)) {
6267
+ ghosts.set(who, { participant: last, until: at + graceMs });
6268
+ }
6269
+ }
6270
+ departures.clear();
6271
+ liveRoster = list;
6272
+ const merged = mergeGhosts(list, ghosts, at);
6273
+ ghosts = merged.kept;
6274
+ store.setParticipants(
6275
+ merged.roster.map((entry) => {
6276
+ const projected = projectParticipant(entry, joinedAt);
6277
+ return entry.reconnecting === true ? { ...projected, reconnecting: true, quality: "lost" } : projected;
6278
+ })
6279
+ );
6280
+ scheduleGhostExpiry();
6281
+ }
6282
+ function scheduleGhostExpiry() {
6283
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6284
+ ghostTimer = null;
6285
+ if (ghosts.size === 0 || disposed) return;
6286
+ const next = Math.min(...[...ghosts.values()].map((g) => g.until));
6287
+ ghostTimer = setTimeout(() => {
6288
+ ghostTimer = null;
6289
+ if (disposed || driver === null) return;
6290
+ projectRoster(liveRoster, new Date(now()).toISOString());
6291
+ }, Math.max(0, next - now()) + 50);
6292
+ }
6293
+ function clearGhosts() {
6294
+ ghosts = /* @__PURE__ */ new Map();
6295
+ departures.clear();
6296
+ liveRoster = [];
6297
+ if (ghostTimer !== null) clearTimeout(ghostTimer);
6298
+ ghostTimer = null;
6299
+ }
6300
+ const supervisor = createConnectionSupervisor({
6301
+ store,
6302
+ network: options.connection?.network ?? createWebNetworkPort(),
6303
+ probe: () => options.connection?.probe?.(current?.serverUrl ?? lastServerUrl) ?? Promise.resolve(true),
6304
+ policy: () => connectionPolicy,
6305
+ autoRejoin: () => engine.autoRejoin(),
6306
+ currentRequest: requestAsNow,
6307
+ resume: options.connection?.resume,
6308
+ now,
6309
+ report: (level, message) => report(level, message)
6310
+ });
6311
+ const engine = {
5862
6312
  join(request) {
5863
6313
  if (joining !== null) return joining;
5864
6314
  if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
@@ -5903,6 +6353,35 @@ function createRoomEngine(options) {
5903
6353
  });
5904
6354
  return joining;
5905
6355
  },
6356
+ async autoRejoin() {
6357
+ const request = lastRequest;
6358
+ if (request === null || joining !== null) return joining ?? void 0;
6359
+ if (!store.dispatch({ type: "auto_rejoin" })) return;
6360
+ tokens.invalidate(request.roomName);
6361
+ current = null;
6362
+ joining = connect(request).catch((error) => {
6363
+ throw failJoin(error, "meet.autoRejoin");
6364
+ }).finally(() => {
6365
+ joining = null;
6366
+ });
6367
+ return joining;
6368
+ },
6369
+ resume(roomName) {
6370
+ const marker = supervisor.resumable(roomName);
6371
+ if (marker === null || joining !== null) return false;
6372
+ if (!store.dispatch({ type: "resumed" })) return false;
6373
+ lastRequest = { ...marker.request, roomName };
6374
+ report("info", "meet: this tab was in the call before the page reloaded \u2014 rejoining");
6375
+ tokens.invalidate(roomName);
6376
+ current = null;
6377
+ joining = connect(lastRequest).catch((error) => {
6378
+ throw failJoin(error, "meet.resume");
6379
+ }).finally(() => {
6380
+ joining = null;
6381
+ });
6382
+ joining.catch(() => void 0);
6383
+ return true;
6384
+ },
5906
6385
  async moveToBreakout(roomId) {
5907
6386
  const request = lastRequest;
5908
6387
  if (request === null || driver === null) {
@@ -6070,6 +6549,8 @@ function createRoomEngine(options) {
6070
6549
  token: () => current,
6071
6550
  async dispose() {
6072
6551
  disposed = true;
6552
+ supervisor.dispose();
6553
+ clearGhosts();
6073
6554
  if (driver !== null) {
6074
6555
  const target = driver;
6075
6556
  driver = null;
@@ -6078,6 +6559,7 @@ function createRoomEngine(options) {
6078
6559
  store.reset();
6079
6560
  }
6080
6561
  };
6562
+ return engine;
6081
6563
  }
6082
6564
  function canModerate(participant) {
6083
6565
  return participant !== void 0 && isHostRole(participant.role);
@@ -7163,7 +7645,7 @@ function participantAttributes(p) {
7163
7645
  "data-meet-participant-role": observedRole(p.role, p.standing),
7164
7646
  "data-meet-participant-self": String(p.isLocal)
7165
7647
  };
7166
- const quality = observedQuality(p.quality);
7648
+ const quality = p.reconnecting === true ? "reconnecting" : observedQuality(p.quality);
7167
7649
  if (quality !== null) attrs["data-meet-participant-connection"] = quality;
7168
7650
  return attrs;
7169
7651
  }
@@ -7179,6 +7661,7 @@ export {
7179
7661
  CAPTION_LANGUAGES,
7180
7662
  CONSENT_NOTICE_ATTRIBUTE,
7181
7663
  DATA_TOPICS,
7664
+ DEFAULT_CONNECTION_POLICY,
7182
7665
  DEFAULT_EFFECTS,
7183
7666
  DEFAULT_PAGE_SIZE,
7184
7667
  EMPTY_COLLAB,
@@ -7193,6 +7676,7 @@ export {
7193
7676
  IDLE_TRACK,
7194
7677
  JOIN_REFUSAL_REASONS,
7195
7678
  LISTEN_ONLY_NOTICE,
7679
+ LIVEKIT_BASE_DELAYS_MS,
7196
7680
  LIVE_PHASES,
7197
7681
  MEETING_INVITE_KIND,
7198
7682
  MEETING_INVITE_VERSION,
@@ -7216,6 +7700,7 @@ export {
7216
7700
  PARTICIPANT_STANDING_ATTRIBUTE,
7217
7701
  REACTION_TTL_MS,
7218
7702
  REFUSED_ABSENCE,
7703
+ RESUME_WINDOW_MS,
7219
7704
  ROLE_RANK,
7220
7705
  ROOM_EVENTS,
7221
7706
  ROOM_EVENT_PREFIX,
@@ -7245,6 +7730,7 @@ export {
7245
7730
  attendanceTotals,
7246
7731
  attendeeKey,
7247
7732
  autoAssign,
7733
+ autoRejoinDelayMs,
7248
7734
  availableActivities,
7249
7735
  breakoutRemainingMs,
7250
7736
  callInviteAction,
@@ -7259,9 +7745,11 @@ export {
7259
7745
  clockPosition,
7260
7746
  collabFromState,
7261
7747
  computeStage,
7748
+ connectionPolicyFrom,
7262
7749
  countLabel,
7263
7750
  createCallCenter,
7264
7751
  createCallInviteHandler,
7752
+ createConnectionSupervisor,
7265
7753
  createDeviceManager,
7266
7754
  createInRoomClient,
7267
7755
  createLiveKitDriver,
@@ -7273,11 +7761,15 @@ export {
7273
7761
  createMeetingRecordCache,
7274
7762
  createMeetingSession,
7275
7763
  createMemoryDeviceStorage,
7764
+ createReachabilityProbe,
7765
+ createReconnectPolicy,
7276
7766
  createRecordingController,
7277
7767
  createRoomEngine,
7278
7768
  createTokenClient,
7279
7769
  createWebDeviceStorage,
7280
7770
  createWebEffectsStorage,
7771
+ createWebNetworkPort,
7772
+ createWebResumeStorage,
7281
7773
  createWebRoomTokenStorage,
7282
7774
  defaultCaptionLanguage,
7283
7775
  displayNameFor,
@@ -7290,6 +7782,7 @@ export {
7290
7782
  guestRecordAccessSetting,
7291
7783
  hasEnded,
7292
7784
  hostUserIds,
7785
+ httpUrlOf,
7293
7786
  invalidResponse,
7294
7787
  isGuest,
7295
7788
  isGuestIdentity,
@@ -7303,6 +7796,7 @@ export {
7303
7796
  isRealName,
7304
7797
  isRefusedPhase,
7305
7798
  isRoomEventTopic,
7799
+ isUnannouncedDeparture,
7306
7800
  isWaitingPhase,
7307
7801
  learnName,
7308
7802
  learnNames,
@@ -7324,6 +7818,7 @@ export {
7324
7818
  meetingTopic,
7325
7819
  meetingWhen,
7326
7820
  mergeCaption,
7821
+ mergeGhosts,
7327
7822
  mergeRecord,
7328
7823
  microphoneNoticeCode,
7329
7824
  moveToRoom,