@ai-matrx/meet 0.10.6 → 0.11.0

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/core.cjs CHANGED
@@ -46,9 +46,11 @@ __export(core_exports, {
46
46
  GUEST_RECORD_ACCESS_METADATA_KEY: () => GUEST_RECORD_ACCESS_METADATA_KEY,
47
47
  GUEST_RECORD_NOT_SHARED: () => GUEST_RECORD_NOT_SHARED,
48
48
  IDLE_MEDIA: () => IDLE_MEDIA,
49
+ IDLE_PHASES: () => IDLE_PHASES,
49
50
  IDLE_TRACK: () => IDLE_TRACK,
50
51
  JOIN_REFUSAL_REASONS: () => JOIN_REFUSAL_REASONS,
51
52
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
53
+ LIVE_PHASES: () => LIVE_PHASES,
52
54
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
53
55
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
54
56
  MEETING_PASS_HEADER: () => MEETING_PASS_HEADER,
@@ -64,6 +66,7 @@ __export(core_exports, {
64
66
  NOTE_TAKER_IDENTITY_PREFIX: () => NOTE_TAKER_IDENTITY_PREFIX,
65
67
  NOTE_TAKER_METADATA_KEY: () => NOTE_TAKER_METADATA_KEY,
66
68
  NO_GRANTS: () => NO_GRANTS,
69
+ NO_SHARE: () => NO_SHARE,
67
70
  ORGANIZATION_HEADER: () => ORGANIZATION_HEADER,
68
71
  PARTICIPANT_CAN_RECORD_ATTRIBUTE: () => PARTICIPANT_CAN_RECORD_ATTRIBUTE,
69
72
  PARTICIPANT_ROLE_ATTRIBUTE: () => PARTICIPANT_ROLE_ATTRIBUTE,
@@ -77,9 +80,11 @@ __export(core_exports, {
77
80
  TABLES: () => TABLES,
78
81
  TRANSCRIPTION_ATTRIBUTES: () => TRANSCRIPTION_ATTRIBUTES,
79
82
  TRANSCRIPTION_TOPIC: () => TRANSCRIPTION_TOPIC,
83
+ WAITING_PHASES: () => WAITING_PHASES,
80
84
  absenceSentence: () => absenceSentence,
81
85
  activeWrapUpSummary: () => activeWrapUpSummary,
82
86
  activityAttention: () => activityAttention,
87
+ admissionPhase: () => admissionPhase,
83
88
  announceMeeting: () => announceMeeting,
84
89
  appPanelOf: () => appPanelOf,
85
90
  applyCollabPacket: () => applyCollabPacket,
@@ -149,10 +154,13 @@ __export(core_exports, {
149
154
  isJoinRefusalReason: () => isJoinRefusalReason,
150
155
  isLinkFollower: () => isLinkFollower,
151
156
  isListenOnly: () => isListenOnly,
157
+ isLivePhase: () => isLivePhase,
152
158
  isNoteTaker: () => isNoteTaker,
153
159
  isObserverRole: () => isObserverRole,
154
160
  isRealName: () => isRealName,
161
+ isRefusedPhase: () => isRefusedPhase,
155
162
  isRoomEventTopic: () => isRoomEventTopic,
163
+ isWaitingPhase: () => isWaitingPhase,
156
164
  learnName: () => learnName,
157
165
  learnNames: () => learnNames,
158
166
  listenOnlyNotice: () => listenOnlyNotice,
@@ -176,6 +184,7 @@ __export(core_exports, {
176
184
  mergeRecord: () => mergeRecord,
177
185
  microphoneNoticeCode: () => microphoneNoticeCode,
178
186
  moveToRoom: () => moveToRoom,
187
+ nextPhase: () => nextPhase,
179
188
  nextTranslationBatch: () => nextTranslationBatch,
180
189
  normalizeDeviceError: () => normalizeDeviceError,
181
190
  normalizeMeetError: () => normalizeMeetError,
@@ -208,10 +217,13 @@ __export(core_exports, {
208
217
  pushMeetingToGoogleCalendar: () => pushMeetingToGoogleCalendar,
209
218
  readServerRefusal: () => readServerRefusal,
210
219
  readableTranscript: () => readableTranscript,
220
+ reasonOf: () => reasonOf,
211
221
  recordingLabel: () => recordingLabel,
212
222
  recordingNotice: () => recordingNotice,
213
223
  recordingOffsetMs: () => recordingOffsetMs,
224
+ refusalPhase: () => refusalPhase,
214
225
  rememberedMeetingPass: () => rememberedMeetingPass,
226
+ resolveExit: () => resolveExit,
215
227
  roleOf: () => roleOf,
216
228
  roomTopic: () => roomTopic,
217
229
  rootAttributes: () => rootAttributes,
@@ -1254,6 +1266,166 @@ var MEET_EVENTS = {
1254
1266
  stateChanged: "mx.state.changed"
1255
1267
  };
1256
1268
 
1269
+ // src/core/reasons.ts
1270
+ var JOIN_REFUSAL_REASONS = [
1271
+ "not_found",
1272
+ "cancelled",
1273
+ "ended",
1274
+ "expired",
1275
+ "too_early",
1276
+ "sign_in_required",
1277
+ "wrong_account",
1278
+ "not_invited",
1279
+ "locked",
1280
+ "removed",
1281
+ "lobby_rejects",
1282
+ "meeting_full",
1283
+ "platform_capacity",
1284
+ "verification_required",
1285
+ "registration_required"
1286
+ ];
1287
+ function isJoinRefusalReason(value) {
1288
+ return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
1289
+ }
1290
+
1291
+ // src/core/phase.ts
1292
+ var LIVE_PHASES = /* @__PURE__ */ new Set(["joining", "in_call"]);
1293
+ var WAITING_PHASES = /* @__PURE__ */ new Set([
1294
+ "waiting:host_not_started",
1295
+ "waiting:knocking",
1296
+ "denied",
1297
+ "knock_expired"
1298
+ ]);
1299
+ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1300
+ "prejoin",
1301
+ "left",
1302
+ "disconnected",
1303
+ "superseded",
1304
+ "inactive",
1305
+ "denied",
1306
+ "knock_expired"
1307
+ ]);
1308
+ var NOT_ENDABLE = /* @__PURE__ */ new Set([
1309
+ "ended",
1310
+ "record",
1311
+ "unsupported",
1312
+ "ringing_out",
1313
+ "ringing_in",
1314
+ "call_declined",
1315
+ "call_missed",
1316
+ "call_busy",
1317
+ "answered_elsewhere"
1318
+ ]);
1319
+ var RINGING = /* @__PURE__ */ new Set(["ringing_out", "ringing_in"]);
1320
+ var IDLE_PHASES = /* @__PURE__ */ new Set([
1321
+ "resolving",
1322
+ "left",
1323
+ "call_declined",
1324
+ "call_missed",
1325
+ "call_busy",
1326
+ "answered_elsewhere"
1327
+ ]);
1328
+ function isLivePhase(phase) {
1329
+ return LIVE_PHASES.has(phase);
1330
+ }
1331
+ function isWaitingPhase(phase) {
1332
+ return WAITING_PHASES.has(phase);
1333
+ }
1334
+ function isRefusedPhase(phase) {
1335
+ return phase.startsWith("refused:");
1336
+ }
1337
+ function reasonOf(phase) {
1338
+ return isRefusedPhase(phase) ? phase.slice("refused:".length) : null;
1339
+ }
1340
+ function refusalPhase(reason) {
1341
+ if (reason === null || !isJoinRefusalReason(reason)) return "disconnected";
1342
+ return reason === "ended" ? "ended" : `refused:${reason}`;
1343
+ }
1344
+ function admissionPhase(admission) {
1345
+ switch (admission) {
1346
+ case "waiting_host":
1347
+ return "waiting:host_not_started";
1348
+ case "knocking":
1349
+ return "waiting:knocking";
1350
+ case "denied":
1351
+ return "denied";
1352
+ case "expired":
1353
+ return "knock_expired";
1354
+ case "withdrawn":
1355
+ return "left";
1356
+ case "ended":
1357
+ return "ended";
1358
+ case "removed":
1359
+ return "refused:removed";
1360
+ default:
1361
+ return null;
1362
+ }
1363
+ }
1364
+ function nextPhase(from, event) {
1365
+ switch (event.type) {
1366
+ case "resolved":
1367
+ return from === "resolving" ? "prejoin" : null;
1368
+ case "unsupported":
1369
+ return from === "resolving" ? "unsupported" : null;
1370
+ case "join":
1371
+ return JOINABLE_FROM.has(from) ? "joining" : null;
1372
+ case "back_to_prejoin":
1373
+ return from === "left" || from === "disconnected" ? "prejoin" : null;
1374
+ case "retry":
1375
+ return isRefusedPhase(from) || from === "unsupported" ? "resolving" : null;
1376
+ case "admission": {
1377
+ if (from !== "joining" && !WAITING_PHASES.has(from)) return null;
1378
+ return admissionPhase(event.admission);
1379
+ }
1380
+ case "admitted":
1381
+ return WAITING_PHASES.has(from) ? "joining" : null;
1382
+ case "connected":
1383
+ return from === "joining" ? "in_call" : null;
1384
+ case "join_failed":
1385
+ return from === "joining" ? refusalPhase(event.reason) : null;
1386
+ case "exit":
1387
+ return from === "in_call" || from === "joining" ? event.to : null;
1388
+ case "leave":
1389
+ return from === "in_call" || from === "joining" || WAITING_PHASES.has(from) ? "left" : null;
1390
+ case "ended":
1391
+ return NOT_ENDABLE.has(from) ? null : "ended";
1392
+ case "ring_out":
1393
+ return IDLE_PHASES.has(from) || from === "prejoin" ? "ringing_out" : null;
1394
+ case "ring_in":
1395
+ return IDLE_PHASES.has(from) ? "ringing_in" : null;
1396
+ case "call_answered":
1397
+ return RINGING.has(from) ? "joining" : null;
1398
+ case "call_outcome":
1399
+ return RINGING.has(from) ? event.outcome : null;
1400
+ case "reset":
1401
+ return LIVE_PHASES.has(from) ? null : "resolving";
1402
+ }
1403
+ }
1404
+ function resolveExit(input) {
1405
+ const { server, hint } = input;
1406
+ if (server === null) {
1407
+ return { to: "disconnected", reason: null, connection: "server_unreachable", notice: null };
1408
+ }
1409
+ const word = server?.exitReason ?? null;
1410
+ if (server !== void 0 && (server.sessionPhase === "ended" || server.meetingPhase === "ended" || word === "ended" || word === "time_limit")) {
1411
+ return { to: "ended", reason: word === "time_limit" ? "time_limit" : "ended", connection: "stable", notice: null };
1412
+ }
1413
+ if (word === "removed") return { to: "removed", reason: "removed", connection: "stable", notice: null };
1414
+ if (word === "displaced") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1415
+ if (word === "sent_to_waiting") {
1416
+ return { to: "waiting:knocking", reason: "sent_to_waiting", connection: "stable", notice: "sent-to-waiting" };
1417
+ }
1418
+ if (hint === "DUPLICATE_IDENTITY") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1419
+ if (hint === "PARTICIPANT_REMOVED" && word === null && server === void 0) {
1420
+ return { to: "removed", reason: "removed", connection: "stable", notice: null };
1421
+ }
1422
+ if (hint === "ROOM_DELETED" && server === void 0) {
1423
+ return { to: "ended", reason: "ended", connection: "stable", notice: null };
1424
+ }
1425
+ if (input.frozen === true) return { to: "inactive", reason: "inactive", connection: "stable", notice: null };
1426
+ return { to: "disconnected", reason: null, connection: "stable", notice: null };
1427
+ }
1428
+
1257
1429
  // src/core/types.ts
1258
1430
  var asMeetingId = (value) => value;
1259
1431
  var asCallId = (value) => value;
@@ -1345,6 +1517,39 @@ function createCallCenter(options) {
1345
1517
  clearRing(invite.id);
1346
1518
  incoming.delete(invite.id);
1347
1519
  if (outgoing !== null && outgoing.id === invite.id) outgoing = invite;
1520
+ const held = options.store.snapshot().call;
1521
+ if (held !== null && held.invite.id === invite.id && held.direction === "incoming") {
1522
+ const outcome = invite.state === "connecting" || invite.state === "active" ? "answered_elsewhere" : invite.state === "declined" ? "reset" : "call_missed";
1523
+ const moved = outcome === "reset" ? options.store.dispatch({ type: "reset" }) : options.store.dispatch({ type: "call_outcome", outcome });
1524
+ if (moved) options.store.patch({ call: outcome === "reset" ? null : { ...held, invite } });
1525
+ }
1526
+ options.onSettled?.(invite);
1527
+ }
1528
+ function settleOutgoing(invite) {
1529
+ if (outgoing === null || outgoing.id !== invite.id) return;
1530
+ clearRing(invite.id);
1531
+ outgoing = invite;
1532
+ const store = options.store;
1533
+ const held = store.snapshot().call;
1534
+ if (held !== null && held.invite.id === invite.id) store.patch({ call: { ...held, invite } });
1535
+ switch (invite.state) {
1536
+ case "connecting":
1537
+ case "active":
1538
+ if (store.dispatch({ type: "call_answered" })) options.onAnswered?.(invite);
1539
+ break;
1540
+ case "declined":
1541
+ store.dispatch({ type: "call_outcome", outcome: "call_declined" });
1542
+ break;
1543
+ case "busy":
1544
+ store.dispatch({ type: "call_outcome", outcome: "call_busy" });
1545
+ break;
1546
+ case "missed":
1547
+ case "failed":
1548
+ store.dispatch({ type: "call_outcome", outcome: "call_missed" });
1549
+ break;
1550
+ default:
1551
+ break;
1552
+ }
1348
1553
  options.onSettled?.(invite);
1349
1554
  }
1350
1555
  function ingest(invite) {
@@ -1353,7 +1558,7 @@ function createCallCenter(options) {
1353
1558
  if (identity.userId === null) return;
1354
1559
  if (invite.callerUserId === identity.userId) return;
1355
1560
  const phase = options.store.snapshot().phase;
1356
- if (phase === "connected" || phase === "connecting" || phase === "reconnecting") {
1561
+ if (isLivePhase(phase) || phase === "ringing_out") {
1357
1562
  void repository.settleCallInvite(invite.id, "busy", null).then((settled) => {
1358
1563
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
1359
1564
  }).catch((error) => {
@@ -1366,6 +1571,9 @@ function createCallCenter(options) {
1366
1571
  return;
1367
1572
  }
1368
1573
  incoming.set(invite.id, { ...invite, state: "ringing" });
1574
+ if (options.store.snapshot().call === null && options.store.dispatch({ type: "ring_in" })) {
1575
+ options.store.patch({ call: { invite: { ...invite, state: "ringing" }, direction: "incoming" } });
1576
+ }
1369
1577
  options.onIncoming?.(invite);
1370
1578
  timeouts.set(
1371
1579
  invite.id,
@@ -1403,6 +1611,7 @@ function createCallCenter(options) {
1403
1611
  const invite = message.data;
1404
1612
  const held = incoming.get(invite.id);
1405
1613
  if (held !== void 0) settleLocal(invite);
1614
+ else if (outgoing?.id === invite.id) settleOutgoing(invite);
1406
1615
  }
1407
1616
  }
1408
1617
  ],
@@ -1469,6 +1678,9 @@ function createCallCenter(options) {
1469
1678
  expiresAt: new Date(timers.now() + ringTimeoutMs).toISOString()
1470
1679
  });
1471
1680
  outgoing = invite;
1681
+ if (options.store.dispatch({ type: "ring_out" })) {
1682
+ options.store.patch({ call: { invite, direction: "outgoing" } });
1683
+ }
1472
1684
  for (const callee of people) {
1473
1685
  to(callee).send(MEET_EVENTS.ring, invite);
1474
1686
  }
@@ -1477,8 +1689,7 @@ function createCallCenter(options) {
1477
1689
  timers.setTimeout(() => {
1478
1690
  if (outgoing?.id !== invite.id) return;
1479
1691
  void repository.settleCallInvite(invite.id, "missed", null).then((settled) => {
1480
- outgoing = settled;
1481
- options.onSettled?.(settled);
1692
+ settleOutgoing(settled);
1482
1693
  }).catch(() => void 0);
1483
1694
  }, ringTimeoutMs)
1484
1695
  );
@@ -1493,7 +1704,9 @@ function createCallCenter(options) {
1493
1704
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
1494
1705
  );
1495
1706
  }
1707
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1496
1708
  const settled = await repository.settleCallInvite(id, "connecting", null);
1709
+ options.store.patch({ call: { invite: settled, direction: "incoming" } });
1497
1710
  settleLocal(settled);
1498
1711
  channel?.send(MEET_EVENTS.settled, settled);
1499
1712
  to(settled.callerUserId).send(MEET_EVENTS.settled, { ...settled, mode });
@@ -1501,6 +1714,9 @@ function createCallCenter(options) {
1501
1714
  },
1502
1715
  async decline(id, message) {
1503
1716
  const settled = await repository.settleCallInvite(id, "declined", message ?? null);
1717
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1718
+ options.store.patch({ call: null });
1719
+ }
1504
1720
  settleLocal(settled);
1505
1721
  channel?.send(MEET_EVENTS.settled, settled);
1506
1722
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
@@ -1510,6 +1726,9 @@ function createCallCenter(options) {
1510
1726
  const settled = await repository.settleCallInvite(id, "cancelled", null);
1511
1727
  clearRing(id);
1512
1728
  if (outgoing?.id === id) outgoing = settled;
1729
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1730
+ options.store.patch({ call: null });
1731
+ }
1513
1732
  for (const callee of settled.calleeUserIds) {
1514
1733
  to(callee).send(MEET_EVENTS.cancel, settled);
1515
1734
  }
@@ -1541,7 +1760,9 @@ var TABLES = {
1541
1760
  /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1542
1761
  invitees: "meet_invitees",
1543
1762
  /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1544
- occurrenceOverrides: "meet_occurrence_overrides"
1763
+ occurrenceOverrides: "meet_occurrence_overrides",
1764
+ /** S5: one row per RUN of a meeting; carries that run's note-taker state. */
1765
+ sessions: "meet_sessions"
1545
1766
  };
1546
1767
  var RPCS = {
1547
1768
  /** Atomic: create the invite row and stamp its expiry in one statement. */
@@ -3022,8 +3243,15 @@ var NO_GRANTS = {
3022
3243
  canPublish: false,
3023
3244
  canSubscribe: false
3024
3245
  };
3246
+ var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
3025
3247
  var EMPTY = {
3026
- phase: "idle",
3248
+ phase: "resolving",
3249
+ reason: null,
3250
+ phaseNotice: null,
3251
+ roomName: null,
3252
+ connection: "stable",
3253
+ share: NO_SHARE,
3254
+ call: null,
3027
3255
  waiting: null,
3028
3256
  meeting: null,
3029
3257
  localIdentity: null,
@@ -3087,8 +3315,20 @@ function createMeetStore(initial = {}) {
3087
3315
  return () => listeners.delete(listener);
3088
3316
  },
3089
3317
  patch(next) {
3318
+ if ("phase" in next || "reason" in next || "phaseNotice" in next) {
3319
+ throw new TypeError("meet store: the phase moves only through store.dispatch(event)");
3320
+ }
3090
3321
  commit({ ...state, ...next });
3091
3322
  },
3323
+ dispatch(event, detail) {
3324
+ const to = nextPhase(state.phase, event);
3325
+ if (to === null) return false;
3326
+ const reason = detail?.reason !== void 0 ? detail.reason : reasonOf(to);
3327
+ const phaseNotice = detail?.notice ?? null;
3328
+ if (to === state.phase && reason === state.reason && phaseNotice === state.phaseNotice) return true;
3329
+ commit({ ...state, phase: to, reason, phaseNotice });
3330
+ return true;
3331
+ },
3092
3332
  setParticipants(participants) {
3093
3333
  const names = learnNames(state.names, participants);
3094
3334
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
@@ -4082,7 +4322,8 @@ function createLiveKitDriver(handlers, options = {}) {
4082
4322
  }).on(RoomEvent.Disconnected, (reason) => {
4083
4323
  audioContainer?.remove();
4084
4324
  audioContainer = null;
4085
- handlers.disconnected(reason === void 0 ? null : String(reason));
4325
+ const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
4326
+ handlers.disconnected(named);
4086
4327
  });
4087
4328
  try {
4088
4329
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -4430,7 +4671,7 @@ function createMeetingSession(options) {
4430
4671
  }
4431
4672
  function apply(read) {
4432
4673
  const snapshot = store.snapshot();
4433
- if (snapshot.phase !== "lobby") return;
4674
+ if (!isWaitingPhase(snapshot.phase)) return;
4434
4675
  const admission = read.admission_state ?? null;
4435
4676
  const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4436
4677
  if (admission === "admitted" && !ended) {
@@ -4447,6 +4688,7 @@ function createMeetingSession(options) {
4447
4688
  if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4448
4689
  store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4449
4690
  }
4691
+ store.dispatch({ type: "admission", admission: "ended" }, { reason: "ended" });
4450
4692
  return;
4451
4693
  }
4452
4694
  if (admission === null) return;
@@ -4458,6 +4700,7 @@ function createMeetingSession(options) {
4458
4700
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4459
4701
  liveVersion: read.live_version ?? 0
4460
4702
  });
4703
+ store.dispatch({ type: "admission", admission });
4461
4704
  if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4462
4705
  stopWaiting();
4463
4706
  }
@@ -4553,7 +4796,7 @@ function createMeetingSession(options) {
4553
4796
  // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4554
4797
  event: MEET_EVENTS.stateChanged,
4555
4798
  onMessage: () => {
4556
- if (store.snapshot().phase === "lobby") void readState2();
4799
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4557
4800
  void refreshQueue();
4558
4801
  }
4559
4802
  }
@@ -4565,7 +4808,7 @@ function createMeetingSession(options) {
4565
4808
  if (meeting === null) return;
4566
4809
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4567
4810
  store.patch({ meeting: fresh, locked: fresh.locked });
4568
- if (store.snapshot().phase === "lobby") void readState2();
4811
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4569
4812
  }
4570
4813
  });
4571
4814
  },
@@ -4732,10 +4975,34 @@ function createMeetingSession(options) {
4732
4975
  },
4733
4976
  stopWaiting,
4734
4977
  readState: readState2,
4978
+ async readExit(connection) {
4979
+ const meeting = store.snapshot().meeting;
4980
+ const pass = options.resolvePass?.() ?? null;
4981
+ if (meeting === null || pass === null) return void 0;
4982
+ try {
4983
+ const response = await options.api.request({
4984
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}&connection=${encodeURIComponent(connection)}`,
4985
+ method: "GET",
4986
+ requireAuth: false,
4987
+ operation: "meet.state(exit)",
4988
+ headers: { [MEETING_PASS_HEADER]: pass }
4989
+ });
4990
+ if (!response.ok) return null;
4991
+ const read = await response.json();
4992
+ return {
4993
+ exitReason: read.exit_reason ?? null,
4994
+ sessionPhase: read.session_phase ?? null,
4995
+ meetingPhase: read.meeting_phase ?? null
4996
+ };
4997
+ } catch {
4998
+ return null;
4999
+ }
5000
+ },
4735
5001
  async withdraw() {
4736
5002
  stopWaiting();
4737
5003
  await postLeave("withdraw");
4738
5004
  patchWaiting({ admission: "withdrawn" });
5005
+ store.dispatch({ type: "admission", admission: "withdrawn" });
4739
5006
  hint();
4740
5007
  },
4741
5008
  async askAgain() {
@@ -5359,6 +5626,9 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5359
5626
  }
5360
5627
 
5361
5628
  // src/core/room.ts
5629
+ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5630
+ return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5631
+ }
5362
5632
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5363
5633
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5364
5634
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5426,7 +5696,7 @@ function createRoomEngine(options) {
5426
5696
  let disposed = false;
5427
5697
  let moving = false;
5428
5698
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5429
- async function leaveRoom() {
5699
+ async function dropRoom() {
5430
5700
  const target = driver;
5431
5701
  driver = null;
5432
5702
  if (current !== null) {
@@ -5435,9 +5705,14 @@ function createRoomEngine(options) {
5435
5705
  }
5436
5706
  store.setLocalTrack("microphone", IDLE_TRACK);
5437
5707
  store.setLocalTrack("camera", IDLE_TRACK);
5438
- store.patch({ phase: "left" });
5708
+ store.patch({ share: NO_SHARE, connection: "stable" });
5439
5709
  await target?.disconnect();
5440
5710
  }
5711
+ async function leaveRoom() {
5712
+ store.dispatch({ type: "leave" });
5713
+ await dropRoom();
5714
+ options.onLeft?.(null);
5715
+ }
5441
5716
  async function confirmMeetingEnd() {
5442
5717
  const meeting = store.snapshot().meeting;
5443
5718
  const target = driver;
@@ -5459,7 +5734,9 @@ function createRoomEngine(options) {
5459
5734
  const latest = store.snapshot().meeting;
5460
5735
  if (latest === null) return;
5461
5736
  store.patch({ meeting: { ...latest, endedAt: verified.endedAt } });
5462
- await leaveRoom();
5737
+ store.dispatch({ type: "ended" }, { reason: "ended" });
5738
+ await dropRoom();
5739
+ options.onLeft?.("ended");
5463
5740
  } catch (error) {
5464
5741
  report("error", `Meet could not confirm or finish meeting teardown: ${String(error)}`, "Check the meeting link again; if media remains active, close this meeting tab.");
5465
5742
  } finally {
@@ -5471,12 +5748,12 @@ function createRoomEngine(options) {
5471
5748
  options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
5472
5749
  }
5473
5750
  let ambiguousShareDenials = 0;
5474
- function fail(error, operation) {
5751
+ function failJoin(error, operation) {
5475
5752
  const normalized = normalizeMeetError(error, operation);
5476
5753
  store.patch({
5477
- phase: "failed",
5478
5754
  failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5479
5755
  });
5756
+ store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
5480
5757
  report(
5481
5758
  normalized.code === "session-unavailable" ? "warn" : "error",
5482
5759
  normalized.message,
@@ -5484,6 +5761,14 @@ function createRoomEngine(options) {
5484
5761
  );
5485
5762
  return normalized;
5486
5763
  }
5764
+ async function lostRoom(error, operation) {
5765
+ const normalized = normalizeMeetError(error, operation);
5766
+ store.patch({ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason } });
5767
+ await dropRoom();
5768
+ store.dispatch({ type: "exit", to: "disconnected" });
5769
+ report("error", normalized.message, normalized.remedy);
5770
+ return normalized;
5771
+ }
5487
5772
  const handlers = {
5488
5773
  participants(list) {
5489
5774
  const joinedAt = new Date(now()).toISOString();
@@ -5526,9 +5811,12 @@ function createRoomEngine(options) {
5526
5811
  store.setQuality(asParticipantIdentity(who), value);
5527
5812
  },
5528
5813
  connectionState(state) {
5529
- if (state === "connected") store.patch({ phase: "connected", failure: null });
5530
- else if (state === "reconnecting") store.patch({ phase: "reconnecting" });
5531
- else if (state === "connecting") store.patch({ phase: "connecting" });
5814
+ if (state === "connected") {
5815
+ if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5816
+ store.dispatch({ type: "connected" });
5817
+ } else if (state === "reconnecting") {
5818
+ store.patch({ connection: "reconnecting" });
5819
+ }
5532
5820
  },
5533
5821
  transcription(event) {
5534
5822
  const snapshot = store.snapshot();
@@ -5655,17 +5943,48 @@ function createRoomEngine(options) {
5655
5943
  }
5656
5944
  },
5657
5945
  disconnected(reason) {
5658
- if (moving) return;
5659
- if (current !== null) {
5660
- tokens.invalidate(current.roomName);
5661
- current = null;
5662
- }
5663
- store.patch({ phase: "left" });
5664
- options.onLeft?.(reason);
5946
+ if (moving || driver === null) return;
5947
+ void resolveExitAndApply(reason);
5665
5948
  }
5666
5949
  };
5950
+ async function resolveExitAndApply(hint) {
5951
+ const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5952
+ await dropRoom();
5953
+ let server;
5954
+ if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
5955
+ server = void 0;
5956
+ } else {
5957
+ try {
5958
+ server = await options.readExit(connection);
5959
+ } catch {
5960
+ server = null;
5961
+ }
5962
+ }
5963
+ if (disposed) return;
5964
+ const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5965
+ store.patch({ connection: resolved.connection });
5966
+ if (resolved.to === "waiting:knocking") {
5967
+ store.patch({
5968
+ waiting: {
5969
+ admission: "knocking",
5970
+ admittersPresent: null,
5971
+ reknockAllowed: true,
5972
+ knockExpiresAt: null,
5973
+ pollSeconds: store.snapshot().waiting?.pollSeconds ?? 3,
5974
+ liveVersion: 0
5975
+ }
5976
+ });
5977
+ }
5978
+ store.dispatch({ type: "exit", to: resolved.to }, { reason: resolved.reason, notice: resolved.notice });
5979
+ report(
5980
+ resolved.to === "disconnected" ? "warn" : "info",
5981
+ `meet: the room connection ended (${hint ?? "no LiveKit reason"}) \u2192 ${resolved.to}`,
5982
+ resolved.to === "disconnected" ? "Press Rejoin; the meeting is still going." : "The screen says what happened."
5983
+ );
5984
+ options.onLeft?.(hint);
5985
+ }
5667
5986
  async function connect(request) {
5668
- store.patch({ phase: "requesting-token", failure: null, waiting: null });
5987
+ store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
5669
5988
  const token = await tokens.get({
5670
5989
  roomName: request.roomName,
5671
5990
  identity,
@@ -5688,10 +6007,10 @@ function createRoomEngine(options) {
5688
6007
  }
5689
6008
  });
5690
6009
  if (token.lobby || !token.canSubscribe) {
6010
+ const admission = token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking";
5691
6011
  store.patch({
5692
- phase: "lobby",
5693
6012
  waiting: {
5694
- admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
6013
+ admission,
5695
6014
  admittersPresent: null,
5696
6015
  reknockAllowed: true,
5697
6016
  knockExpiresAt: null,
@@ -5699,9 +6018,10 @@ function createRoomEngine(options) {
5699
6018
  liveVersion: 0
5700
6019
  }
5701
6020
  });
6021
+ store.dispatch({ type: "admission", admission });
5702
6022
  return;
5703
6023
  }
5704
- store.patch({ phase: "connecting", waiting: null });
6024
+ store.patch({ waiting: null });
5705
6025
  const created = await options.driverFactory(handlers);
5706
6026
  if (disposed) {
5707
6027
  await created.disconnect();
@@ -5709,7 +6029,8 @@ function createRoomEngine(options) {
5709
6029
  }
5710
6030
  driver = created;
5711
6031
  await created.connect(token.serverUrl, token.token);
5712
- store.patch({ phase: "connected", failure: null });
6032
+ store.patch({ failure: null, connection: "stable" });
6033
+ store.dispatch({ type: "connected" });
5713
6034
  store.setLocalTrack("microphone", IDLE_TRACK);
5714
6035
  store.setLocalTrack("camera", IDLE_TRACK);
5715
6036
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
@@ -5780,9 +6101,17 @@ function createRoomEngine(options) {
5780
6101
  return {
5781
6102
  join(request) {
5782
6103
  if (joining !== null) return joining;
6104
+ if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
6105
+ report(
6106
+ "warn",
6107
+ `meet.join: cannot join from "${store.snapshot().phase}"`,
6108
+ "Join is offered from the pre-join, left, disconnected and replaced screens only."
6109
+ );
6110
+ return Promise.resolve();
6111
+ }
5783
6112
  lastRequest = request;
5784
6113
  joining = connect(request).catch((error) => {
5785
- throw fail(error, "meet.join");
6114
+ throw failJoin(error, "meet.join");
5786
6115
  }).finally(() => {
5787
6116
  joining = null;
5788
6117
  });
@@ -5799,10 +6128,16 @@ function createRoomEngine(options) {
5799
6128
  return;
5800
6129
  }
5801
6130
  if (joining !== null) return joining;
6131
+ const phase = store.snapshot().phase;
6132
+ const moved = phase === "joining" || store.dispatch({ type: "join", click: true }) || store.dispatch({ type: "admitted" });
6133
+ if (!moved) {
6134
+ report("warn", `meet.rejoin: cannot rejoin from "${phase}"`, "Rejoin is offered after leaving, a lost connection, or admission.");
6135
+ return;
6136
+ }
5802
6137
  tokens.invalidate(request.roomName);
5803
6138
  current = null;
5804
6139
  joining = connect(request).catch((error) => {
5805
- throw fail(error, "meet.rejoin");
6140
+ throw failJoin(error, "meet.rejoin");
5806
6141
  }).finally(() => {
5807
6142
  joining = null;
5808
6143
  });
@@ -5847,11 +6182,11 @@ function createRoomEngine(options) {
5847
6182
  joining = connect(lastRequest).finally(() => {
5848
6183
  joining = null;
5849
6184
  });
5850
- await joining.catch((again) => {
5851
- throw fail(again, "meet.moveToBreakout");
6185
+ await joining.catch(async (again) => {
6186
+ throw await lostRoom(again, "meet.moveToBreakout");
5852
6187
  });
5853
6188
  } else {
5854
- throw fail(error, "meet.moveToBreakout");
6189
+ throw await lostRoom(error, "meet.moveToBreakout");
5855
6190
  }
5856
6191
  throw normalized;
5857
6192
  } finally {
@@ -5859,6 +6194,9 @@ function createRoomEngine(options) {
5859
6194
  }
5860
6195
  },
5861
6196
  leave: leaveRoom,
6197
+ backToPrejoin() {
6198
+ store.dispatch({ type: "back_to_prejoin", click: true });
6199
+ },
5862
6200
  // Turning a device on mid-meeting is the SAME operation as turning it on
5863
6201
  // during the join, so it runs the same function. A failure here used to
5864
6202
  // stamp the room `failed` — a busy microphone would blank a live meeting.
@@ -5872,21 +6210,41 @@ function createRoomEngine(options) {
5872
6210
  },
5873
6211
  async setScreenShareEnabled(enabled) {
5874
6212
  if (driver === null) return;
6213
+ if (enabled && driver.supportsScreenShare?.() === false) {
6214
+ store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
6215
+ report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
6216
+ return;
6217
+ }
6218
+ if (enabled) store.patch({ share: { state: "picking", reason: null, remedy: null, at: now() } });
5875
6219
  try {
5876
6220
  await driver.setScreenShareEnabled(enabled);
5877
6221
  ambiguousShareDenials = 0;
6222
+ store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
5878
6223
  } catch (error) {
5879
6224
  const kind = classifyScreenShareError(error);
5880
6225
  if (kind === "ambiguous") {
5881
6226
  ambiguousShareDenials += 1;
5882
- if (ambiguousShareDenials < 2) return;
6227
+ if (ambiguousShareDenials < 2) {
6228
+ store.patch({ share: NO_SHARE });
6229
+ return;
6230
+ }
5883
6231
  }
6232
+ ambiguousShareDenials = 0;
5884
6233
  const normalized = normalizeScreenShareError(error, "meet.setScreenShareEnabled");
5885
6234
  report("warn", normalized.message, normalized.remedy);
5886
- const text3 = kind === "other" ? `Screen sharing is not available. ${normalized.remedy}` : `Screen sharing did not start. ${SCREEN_SHARE_BLOCKED_REMEDY}`;
5887
- store.patch({ actionNotice: { text: text3, failed: true, at: now() } });
6235
+ store.patch({
6236
+ share: {
6237
+ state: "refused",
6238
+ reason: kind === "other" ? "failed" : "blocked",
6239
+ remedy: kind === "other" ? normalized.remedy : SCREEN_SHARE_BLOCKED_REMEDY_FOR(normalized.remedy),
6240
+ at: now()
6241
+ }
6242
+ });
5888
6243
  }
5889
6244
  },
6245
+ dismissShareNotice() {
6246
+ if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6247
+ },
5890
6248
  async raiseHand(raised) {
5891
6249
  const local = store.snapshot().localIdentity;
5892
6250
  if (local !== null) store.setHand(local, raised);
@@ -6853,28 +7211,6 @@ function createMeetingRecordCache(options = {}) {
6853
7211
  };
6854
7212
  }
6855
7213
 
6856
- // src/core/reasons.ts
6857
- var JOIN_REFUSAL_REASONS = [
6858
- "not_found",
6859
- "cancelled",
6860
- "ended",
6861
- "expired",
6862
- "too_early",
6863
- "sign_in_required",
6864
- "wrong_account",
6865
- "not_invited",
6866
- "locked",
6867
- "removed",
6868
- "lobby_rejects",
6869
- "meeting_full",
6870
- "platform_capacity",
6871
- "verification_required",
6872
- "registration_required"
6873
- ];
6874
- function isJoinRefusalReason(value) {
6875
- return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
6876
- }
6877
-
6878
7214
  // src/core/reactions.ts
6879
7215
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
6880
7216
 
@@ -6973,43 +7309,12 @@ function endedRecord(snapshot, given) {
6973
7309
  function observedPhase(input) {
6974
7310
  const { snapshot } = input;
6975
7311
  if (snapshot === null) return "resolving";
6976
- const raw = snapshot.phase;
6977
- const live = raw === "connected" || raw === "reconnecting";
6978
- if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
6979
- const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
6980
- if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
6981
- switch (raw) {
6982
- case "idle":
6983
- case "failed":
6984
- case "left":
6985
- return "prejoin";
6986
- case "lobby":
6987
- return waitingPhase(snapshot);
6988
- case "requesting-token":
6989
- case "connecting":
6990
- return "joining";
6991
- case "connected":
6992
- case "reconnecting":
6993
- return "in_call";
6994
- }
7312
+ const phase = snapshot.phase;
7313
+ if (endedRecord(snapshot, input.meeting ?? null) && !isLivePhase(phase) && !isCallPhase(phase)) return "ended";
7314
+ return phase;
6995
7315
  }
6996
- function waitingPhase(snapshot) {
6997
- switch (snapshot.waiting?.admission) {
6998
- case "waiting_host":
6999
- return "waiting:host_not_started";
7000
- case "denied":
7001
- return "denied";
7002
- case "expired":
7003
- return "knock_expired";
7004
- case "withdrawn":
7005
- return "left";
7006
- case "ended":
7007
- return "ended";
7008
- case "removed":
7009
- return "refused:removed";
7010
- default:
7011
- return "waiting:knocking";
7012
- }
7316
+ function isCallPhase(phase) {
7317
+ return phase.startsWith("ringing_") || phase.startsWith("call_") || phase === "answered_elsewhere";
7013
7318
  }
7014
7319
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
7015
7320
  function observe(input) {
@@ -7022,10 +7327,10 @@ function observe(input) {
7022
7327
  const deviceInPlay = phase === "joining" || inCall;
7023
7328
  return {
7024
7329
  phase,
7025
- reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
7330
+ reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
7026
7331
  role,
7027
- connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
7028
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
7332
+ connection: snapshot?.connection ?? "stable",
7333
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
7029
7334
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
7030
7335
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7031
7336
  audioBlocked: null,