@ai-matrx/meet 0.10.7 → 0.11.1

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.cjs CHANGED
@@ -46,9 +46,11 @@ __export(src_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(src_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(src_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(src_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(src_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(src_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. */
@@ -2017,6 +2238,18 @@ function createMeetRepository(options) {
2017
2238
  );
2018
2239
  }
2019
2240
  }
2241
+ async function withRunNoteTaker(meeting) {
2242
+ const response = await db.from(TABLES.sessions).select("note_taker,started_at").eq("meeting_id", meeting.id).order("started_at", { ascending: false }).limit(1);
2243
+ const run = response.error === null ? (response.data ?? [])[0] : void 0;
2244
+ const noteTaker = run?.["note_taker"];
2245
+ if (typeof noteTaker !== "object" || noteTaker === null || Array.isArray(noteTaker)) {
2246
+ return meeting;
2247
+ }
2248
+ return {
2249
+ ...meeting,
2250
+ metadata: { ...meeting.metadata ?? {}, note_taker: noteTaker }
2251
+ };
2252
+ }
2020
2253
  return {
2021
2254
  schema: MEET_SCHEMA,
2022
2255
  tables: TABLES,
@@ -2255,13 +2488,13 @@ function createMeetRepository(options) {
2255
2488
  "meet.meetingBySlug",
2256
2489
  () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })
2257
2490
  );
2258
- return projectMeeting(row);
2491
+ return withRunNoteTaker(projectMeeting(row));
2259
2492
  },
2260
2493
  async meeting(id) {
2261
2494
  const response = await db.from(TABLES.meetings).select(MEETING_COLUMNS).eq("id", id).single();
2262
2495
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.meeting");
2263
2496
  if (response.data === null) throw invalidResponse("meet.meeting", "no such meeting");
2264
- return projectMeeting(response.data);
2497
+ return withRunNoteTaker(projectMeeting(response.data));
2265
2498
  },
2266
2499
  async listMeetings({ organizationId = null, limit = 25, before = null }) {
2267
2500
  let query = db.from(TABLES.meetings).select(MEETING_COLUMNS);
@@ -3022,8 +3255,15 @@ var NO_GRANTS = {
3022
3255
  canPublish: false,
3023
3256
  canSubscribe: false
3024
3257
  };
3258
+ var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
3025
3259
  var EMPTY = {
3026
- phase: "idle",
3260
+ phase: "resolving",
3261
+ reason: null,
3262
+ phaseNotice: null,
3263
+ roomName: null,
3264
+ connection: "stable",
3265
+ share: NO_SHARE,
3266
+ call: null,
3027
3267
  waiting: null,
3028
3268
  meeting: null,
3029
3269
  localIdentity: null,
@@ -3087,8 +3327,20 @@ function createMeetStore(initial = {}) {
3087
3327
  return () => listeners.delete(listener);
3088
3328
  },
3089
3329
  patch(next) {
3330
+ if ("phase" in next || "reason" in next || "phaseNotice" in next) {
3331
+ throw new TypeError("meet store: the phase moves only through store.dispatch(event)");
3332
+ }
3090
3333
  commit({ ...state, ...next });
3091
3334
  },
3335
+ dispatch(event, detail) {
3336
+ const to = nextPhase(state.phase, event);
3337
+ if (to === null) return false;
3338
+ const reason = detail?.reason !== void 0 ? detail.reason : reasonOf(to);
3339
+ const phaseNotice = detail?.notice ?? null;
3340
+ if (to === state.phase && reason === state.reason && phaseNotice === state.phaseNotice) return true;
3341
+ commit({ ...state, phase: to, reason, phaseNotice });
3342
+ return true;
3343
+ },
3092
3344
  setParticipants(participants) {
3093
3345
  const names = learnNames(state.names, participants);
3094
3346
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
@@ -4082,7 +4334,8 @@ function createLiveKitDriver(handlers, options = {}) {
4082
4334
  }).on(RoomEvent.Disconnected, (reason) => {
4083
4335
  audioContainer?.remove();
4084
4336
  audioContainer = null;
4085
- handlers.disconnected(reason === void 0 ? null : String(reason));
4337
+ const named = reason === void 0 ? null : module2?.DisconnectReason?.[reason] ?? String(reason);
4338
+ handlers.disconnected(named);
4086
4339
  });
4087
4340
  try {
4088
4341
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -4430,7 +4683,7 @@ function createMeetingSession(options) {
4430
4683
  }
4431
4684
  function apply(read) {
4432
4685
  const snapshot = store.snapshot();
4433
- if (snapshot.phase !== "lobby") return;
4686
+ if (!isWaitingPhase(snapshot.phase)) return;
4434
4687
  const admission = read.admission_state ?? null;
4435
4688
  const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4436
4689
  if (admission === "admitted" && !ended) {
@@ -4447,6 +4700,7 @@ function createMeetingSession(options) {
4447
4700
  if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4448
4701
  store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4449
4702
  }
4703
+ store.dispatch({ type: "admission", admission: "ended" }, { reason: "ended" });
4450
4704
  return;
4451
4705
  }
4452
4706
  if (admission === null) return;
@@ -4458,6 +4712,7 @@ function createMeetingSession(options) {
4458
4712
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4459
4713
  liveVersion: read.live_version ?? 0
4460
4714
  });
4715
+ store.dispatch({ type: "admission", admission });
4461
4716
  if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4462
4717
  stopWaiting();
4463
4718
  }
@@ -4553,7 +4808,7 @@ function createMeetingSession(options) {
4553
4808
  // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4554
4809
  event: MEET_EVENTS.stateChanged,
4555
4810
  onMessage: () => {
4556
- if (store.snapshot().phase === "lobby") void readState2();
4811
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4557
4812
  void refreshQueue();
4558
4813
  }
4559
4814
  }
@@ -4565,7 +4820,7 @@ function createMeetingSession(options) {
4565
4820
  if (meeting === null) return;
4566
4821
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4567
4822
  store.patch({ meeting: fresh, locked: fresh.locked });
4568
- if (store.snapshot().phase === "lobby") void readState2();
4823
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4569
4824
  }
4570
4825
  });
4571
4826
  },
@@ -4732,10 +4987,34 @@ function createMeetingSession(options) {
4732
4987
  },
4733
4988
  stopWaiting,
4734
4989
  readState: readState2,
4990
+ async readExit(connection) {
4991
+ const meeting = store.snapshot().meeting;
4992
+ const pass = options.resolvePass?.() ?? null;
4993
+ if (meeting === null || pass === null) return void 0;
4994
+ try {
4995
+ const response = await options.api.request({
4996
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}&connection=${encodeURIComponent(connection)}`,
4997
+ method: "GET",
4998
+ requireAuth: false,
4999
+ operation: "meet.state(exit)",
5000
+ headers: { [MEETING_PASS_HEADER]: pass }
5001
+ });
5002
+ if (!response.ok) return null;
5003
+ const read = await response.json();
5004
+ return {
5005
+ exitReason: read.exit_reason ?? null,
5006
+ sessionPhase: read.session_phase ?? null,
5007
+ meetingPhase: read.meeting_phase ?? null
5008
+ };
5009
+ } catch {
5010
+ return null;
5011
+ }
5012
+ },
4735
5013
  async withdraw() {
4736
5014
  stopWaiting();
4737
5015
  await postLeave("withdraw");
4738
5016
  patchWaiting({ admission: "withdrawn" });
5017
+ store.dispatch({ type: "admission", admission: "withdrawn" });
4739
5018
  hint();
4740
5019
  },
4741
5020
  async askAgain() {
@@ -5359,6 +5638,9 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5359
5638
  }
5360
5639
 
5361
5640
  // src/core/room.ts
5641
+ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5642
+ return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5643
+ }
5362
5644
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5363
5645
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5364
5646
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5426,7 +5708,7 @@ function createRoomEngine(options) {
5426
5708
  let disposed = false;
5427
5709
  let moving = false;
5428
5710
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5429
- async function leaveRoom() {
5711
+ async function dropRoom() {
5430
5712
  const target = driver;
5431
5713
  driver = null;
5432
5714
  if (current !== null) {
@@ -5435,9 +5717,14 @@ function createRoomEngine(options) {
5435
5717
  }
5436
5718
  store.setLocalTrack("microphone", IDLE_TRACK);
5437
5719
  store.setLocalTrack("camera", IDLE_TRACK);
5438
- store.patch({ phase: "left" });
5720
+ store.patch({ share: NO_SHARE, connection: "stable" });
5439
5721
  await target?.disconnect();
5440
5722
  }
5723
+ async function leaveRoom() {
5724
+ store.dispatch({ type: "leave" });
5725
+ await dropRoom();
5726
+ options.onLeft?.(null);
5727
+ }
5441
5728
  async function confirmMeetingEnd() {
5442
5729
  const meeting = store.snapshot().meeting;
5443
5730
  const target = driver;
@@ -5459,7 +5746,9 @@ function createRoomEngine(options) {
5459
5746
  const latest = store.snapshot().meeting;
5460
5747
  if (latest === null) return;
5461
5748
  store.patch({ meeting: { ...latest, endedAt: verified.endedAt } });
5462
- await leaveRoom();
5749
+ store.dispatch({ type: "ended" }, { reason: "ended" });
5750
+ await dropRoom();
5751
+ options.onLeft?.("ended");
5463
5752
  } catch (error) {
5464
5753
  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
5754
  } finally {
@@ -5471,12 +5760,12 @@ function createRoomEngine(options) {
5471
5760
  options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
5472
5761
  }
5473
5762
  let ambiguousShareDenials = 0;
5474
- function fail(error, operation) {
5763
+ function failJoin(error, operation) {
5475
5764
  const normalized = normalizeMeetError(error, operation);
5476
5765
  store.patch({
5477
- phase: "failed",
5478
5766
  failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5479
5767
  });
5768
+ store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
5480
5769
  report(
5481
5770
  normalized.code === "session-unavailable" ? "warn" : "error",
5482
5771
  normalized.message,
@@ -5484,6 +5773,14 @@ function createRoomEngine(options) {
5484
5773
  );
5485
5774
  return normalized;
5486
5775
  }
5776
+ async function lostRoom(error, operation) {
5777
+ const normalized = normalizeMeetError(error, operation);
5778
+ store.patch({ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason } });
5779
+ await dropRoom();
5780
+ store.dispatch({ type: "exit", to: "disconnected" });
5781
+ report("error", normalized.message, normalized.remedy);
5782
+ return normalized;
5783
+ }
5487
5784
  const handlers = {
5488
5785
  participants(list) {
5489
5786
  const joinedAt = new Date(now()).toISOString();
@@ -5526,9 +5823,12 @@ function createRoomEngine(options) {
5526
5823
  store.setQuality(asParticipantIdentity(who), value);
5527
5824
  },
5528
5825
  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" });
5826
+ if (state === "connected") {
5827
+ if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5828
+ store.dispatch({ type: "connected" });
5829
+ } else if (state === "reconnecting") {
5830
+ store.patch({ connection: "reconnecting" });
5831
+ }
5532
5832
  },
5533
5833
  transcription(event) {
5534
5834
  const snapshot = store.snapshot();
@@ -5655,17 +5955,48 @@ function createRoomEngine(options) {
5655
5955
  }
5656
5956
  },
5657
5957
  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);
5958
+ if (moving || driver === null) return;
5959
+ void resolveExitAndApply(reason);
5665
5960
  }
5666
5961
  };
5962
+ async function resolveExitAndApply(hint) {
5963
+ const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5964
+ await dropRoom();
5965
+ let server;
5966
+ if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
5967
+ server = void 0;
5968
+ } else {
5969
+ try {
5970
+ server = await options.readExit(connection);
5971
+ } catch {
5972
+ server = null;
5973
+ }
5974
+ }
5975
+ if (disposed) return;
5976
+ const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5977
+ store.patch({ connection: resolved.connection });
5978
+ if (resolved.to === "waiting:knocking") {
5979
+ store.patch({
5980
+ waiting: {
5981
+ admission: "knocking",
5982
+ admittersPresent: null,
5983
+ reknockAllowed: true,
5984
+ knockExpiresAt: null,
5985
+ pollSeconds: store.snapshot().waiting?.pollSeconds ?? 3,
5986
+ liveVersion: 0
5987
+ }
5988
+ });
5989
+ }
5990
+ store.dispatch({ type: "exit", to: resolved.to }, { reason: resolved.reason, notice: resolved.notice });
5991
+ report(
5992
+ resolved.to === "disconnected" ? "warn" : "info",
5993
+ `meet: the room connection ended (${hint ?? "no LiveKit reason"}) \u2192 ${resolved.to}`,
5994
+ resolved.to === "disconnected" ? "Press Rejoin; the meeting is still going." : "The screen says what happened."
5995
+ );
5996
+ options.onLeft?.(hint);
5997
+ }
5667
5998
  async function connect(request) {
5668
- store.patch({ phase: "requesting-token", failure: null, waiting: null });
5999
+ store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
5669
6000
  const token = await tokens.get({
5670
6001
  roomName: request.roomName,
5671
6002
  identity,
@@ -5688,10 +6019,10 @@ function createRoomEngine(options) {
5688
6019
  }
5689
6020
  });
5690
6021
  if (token.lobby || !token.canSubscribe) {
6022
+ const admission = token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking";
5691
6023
  store.patch({
5692
- phase: "lobby",
5693
6024
  waiting: {
5694
- admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
6025
+ admission,
5695
6026
  admittersPresent: null,
5696
6027
  reknockAllowed: true,
5697
6028
  knockExpiresAt: null,
@@ -5699,9 +6030,10 @@ function createRoomEngine(options) {
5699
6030
  liveVersion: 0
5700
6031
  }
5701
6032
  });
6033
+ store.dispatch({ type: "admission", admission });
5702
6034
  return;
5703
6035
  }
5704
- store.patch({ phase: "connecting", waiting: null });
6036
+ store.patch({ waiting: null });
5705
6037
  const created = await options.driverFactory(handlers);
5706
6038
  if (disposed) {
5707
6039
  await created.disconnect();
@@ -5709,7 +6041,8 @@ function createRoomEngine(options) {
5709
6041
  }
5710
6042
  driver = created;
5711
6043
  await created.connect(token.serverUrl, token.token);
5712
- store.patch({ phase: "connected", failure: null });
6044
+ store.patch({ failure: null, connection: "stable" });
6045
+ store.dispatch({ type: "connected" });
5713
6046
  store.setLocalTrack("microphone", IDLE_TRACK);
5714
6047
  store.setLocalTrack("camera", IDLE_TRACK);
5715
6048
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
@@ -5780,9 +6113,17 @@ function createRoomEngine(options) {
5780
6113
  return {
5781
6114
  join(request) {
5782
6115
  if (joining !== null) return joining;
6116
+ if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
6117
+ report(
6118
+ "warn",
6119
+ `meet.join: cannot join from "${store.snapshot().phase}"`,
6120
+ "Join is offered from the pre-join, left, disconnected and replaced screens only."
6121
+ );
6122
+ return Promise.resolve();
6123
+ }
5783
6124
  lastRequest = request;
5784
6125
  joining = connect(request).catch((error) => {
5785
- throw fail(error, "meet.join");
6126
+ throw failJoin(error, "meet.join");
5786
6127
  }).finally(() => {
5787
6128
  joining = null;
5788
6129
  });
@@ -5799,10 +6140,16 @@ function createRoomEngine(options) {
5799
6140
  return;
5800
6141
  }
5801
6142
  if (joining !== null) return joining;
6143
+ const phase = store.snapshot().phase;
6144
+ const moved = phase === "joining" || store.dispatch({ type: "join", click: true }) || store.dispatch({ type: "admitted" });
6145
+ if (!moved) {
6146
+ report("warn", `meet.rejoin: cannot rejoin from "${phase}"`, "Rejoin is offered after leaving, a lost connection, or admission.");
6147
+ return;
6148
+ }
5802
6149
  tokens.invalidate(request.roomName);
5803
6150
  current = null;
5804
6151
  joining = connect(request).catch((error) => {
5805
- throw fail(error, "meet.rejoin");
6152
+ throw failJoin(error, "meet.rejoin");
5806
6153
  }).finally(() => {
5807
6154
  joining = null;
5808
6155
  });
@@ -5847,11 +6194,11 @@ function createRoomEngine(options) {
5847
6194
  joining = connect(lastRequest).finally(() => {
5848
6195
  joining = null;
5849
6196
  });
5850
- await joining.catch((again) => {
5851
- throw fail(again, "meet.moveToBreakout");
6197
+ await joining.catch(async (again) => {
6198
+ throw await lostRoom(again, "meet.moveToBreakout");
5852
6199
  });
5853
6200
  } else {
5854
- throw fail(error, "meet.moveToBreakout");
6201
+ throw await lostRoom(error, "meet.moveToBreakout");
5855
6202
  }
5856
6203
  throw normalized;
5857
6204
  } finally {
@@ -5859,6 +6206,9 @@ function createRoomEngine(options) {
5859
6206
  }
5860
6207
  },
5861
6208
  leave: leaveRoom,
6209
+ backToPrejoin() {
6210
+ store.dispatch({ type: "back_to_prejoin", click: true });
6211
+ },
5862
6212
  // Turning a device on mid-meeting is the SAME operation as turning it on
5863
6213
  // during the join, so it runs the same function. A failure here used to
5864
6214
  // stamp the room `failed` — a busy microphone would blank a live meeting.
@@ -5872,21 +6222,41 @@ function createRoomEngine(options) {
5872
6222
  },
5873
6223
  async setScreenShareEnabled(enabled) {
5874
6224
  if (driver === null) return;
6225
+ if (enabled && driver.supportsScreenShare?.() === false) {
6226
+ store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
6227
+ report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
6228
+ return;
6229
+ }
6230
+ if (enabled) store.patch({ share: { state: "picking", reason: null, remedy: null, at: now() } });
5875
6231
  try {
5876
6232
  await driver.setScreenShareEnabled(enabled);
5877
6233
  ambiguousShareDenials = 0;
6234
+ store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
5878
6235
  } catch (error) {
5879
6236
  const kind = classifyScreenShareError(error);
5880
6237
  if (kind === "ambiguous") {
5881
6238
  ambiguousShareDenials += 1;
5882
- if (ambiguousShareDenials < 2) return;
6239
+ if (ambiguousShareDenials < 2) {
6240
+ store.patch({ share: NO_SHARE });
6241
+ return;
6242
+ }
5883
6243
  }
6244
+ ambiguousShareDenials = 0;
5884
6245
  const normalized = normalizeScreenShareError(error, "meet.setScreenShareEnabled");
5885
6246
  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() } });
6247
+ store.patch({
6248
+ share: {
6249
+ state: "refused",
6250
+ reason: kind === "other" ? "failed" : "blocked",
6251
+ remedy: kind === "other" ? normalized.remedy : SCREEN_SHARE_BLOCKED_REMEDY_FOR(normalized.remedy),
6252
+ at: now()
6253
+ }
6254
+ });
5888
6255
  }
5889
6256
  },
6257
+ dismissShareNotice() {
6258
+ if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6259
+ },
5890
6260
  async raiseHand(raised) {
5891
6261
  const local = store.snapshot().localIdentity;
5892
6262
  if (local !== null) store.setHand(local, raised);
@@ -6853,28 +7223,6 @@ function createMeetingRecordCache(options = {}) {
6853
7223
  };
6854
7224
  }
6855
7225
 
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
7226
  // src/core/reactions.ts
6879
7227
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
6880
7228
 
@@ -6973,43 +7321,12 @@ function endedRecord(snapshot, given) {
6973
7321
  function observedPhase(input) {
6974
7322
  const { snapshot } = input;
6975
7323
  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
- }
7324
+ const phase = snapshot.phase;
7325
+ if (endedRecord(snapshot, input.meeting ?? null) && !isLivePhase(phase) && !isCallPhase(phase)) return "ended";
7326
+ return phase;
6995
7327
  }
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
- }
7328
+ function isCallPhase(phase) {
7329
+ return phase.startsWith("ringing_") || phase.startsWith("call_") || phase === "answered_elsewhere";
7013
7330
  }
7014
7331
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
7015
7332
  function observe(input) {
@@ -7022,10 +7339,10 @@ function observe(input) {
7022
7339
  const deviceInPlay = phase === "joining" || inCall;
7023
7340
  return {
7024
7341
  phase,
7025
- reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
7342
+ reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
7026
7343
  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,
7344
+ connection: snapshot?.connection ?? "stable",
7345
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
7029
7346
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
7030
7347
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
7031
7348
  audioBlocked: null,