@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/core.js CHANGED
@@ -1014,6 +1014,166 @@ var MEET_EVENTS = {
1014
1014
  stateChanged: "mx.state.changed"
1015
1015
  };
1016
1016
 
1017
+ // src/core/reasons.ts
1018
+ var JOIN_REFUSAL_REASONS = [
1019
+ "not_found",
1020
+ "cancelled",
1021
+ "ended",
1022
+ "expired",
1023
+ "too_early",
1024
+ "sign_in_required",
1025
+ "wrong_account",
1026
+ "not_invited",
1027
+ "locked",
1028
+ "removed",
1029
+ "lobby_rejects",
1030
+ "meeting_full",
1031
+ "platform_capacity",
1032
+ "verification_required",
1033
+ "registration_required"
1034
+ ];
1035
+ function isJoinRefusalReason(value) {
1036
+ return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
1037
+ }
1038
+
1039
+ // src/core/phase.ts
1040
+ var LIVE_PHASES = /* @__PURE__ */ new Set(["joining", "in_call"]);
1041
+ var WAITING_PHASES = /* @__PURE__ */ new Set([
1042
+ "waiting:host_not_started",
1043
+ "waiting:knocking",
1044
+ "denied",
1045
+ "knock_expired"
1046
+ ]);
1047
+ var JOINABLE_FROM = /* @__PURE__ */ new Set([
1048
+ "prejoin",
1049
+ "left",
1050
+ "disconnected",
1051
+ "superseded",
1052
+ "inactive",
1053
+ "denied",
1054
+ "knock_expired"
1055
+ ]);
1056
+ var NOT_ENDABLE = /* @__PURE__ */ new Set([
1057
+ "ended",
1058
+ "record",
1059
+ "unsupported",
1060
+ "ringing_out",
1061
+ "ringing_in",
1062
+ "call_declined",
1063
+ "call_missed",
1064
+ "call_busy",
1065
+ "answered_elsewhere"
1066
+ ]);
1067
+ var RINGING = /* @__PURE__ */ new Set(["ringing_out", "ringing_in"]);
1068
+ var IDLE_PHASES = /* @__PURE__ */ new Set([
1069
+ "resolving",
1070
+ "left",
1071
+ "call_declined",
1072
+ "call_missed",
1073
+ "call_busy",
1074
+ "answered_elsewhere"
1075
+ ]);
1076
+ function isLivePhase(phase) {
1077
+ return LIVE_PHASES.has(phase);
1078
+ }
1079
+ function isWaitingPhase(phase) {
1080
+ return WAITING_PHASES.has(phase);
1081
+ }
1082
+ function isRefusedPhase(phase) {
1083
+ return phase.startsWith("refused:");
1084
+ }
1085
+ function reasonOf(phase) {
1086
+ return isRefusedPhase(phase) ? phase.slice("refused:".length) : null;
1087
+ }
1088
+ function refusalPhase(reason) {
1089
+ if (reason === null || !isJoinRefusalReason(reason)) return "disconnected";
1090
+ return reason === "ended" ? "ended" : `refused:${reason}`;
1091
+ }
1092
+ function admissionPhase(admission) {
1093
+ switch (admission) {
1094
+ case "waiting_host":
1095
+ return "waiting:host_not_started";
1096
+ case "knocking":
1097
+ return "waiting:knocking";
1098
+ case "denied":
1099
+ return "denied";
1100
+ case "expired":
1101
+ return "knock_expired";
1102
+ case "withdrawn":
1103
+ return "left";
1104
+ case "ended":
1105
+ return "ended";
1106
+ case "removed":
1107
+ return "refused:removed";
1108
+ default:
1109
+ return null;
1110
+ }
1111
+ }
1112
+ function nextPhase(from, event) {
1113
+ switch (event.type) {
1114
+ case "resolved":
1115
+ return from === "resolving" ? "prejoin" : null;
1116
+ case "unsupported":
1117
+ return from === "resolving" ? "unsupported" : null;
1118
+ case "join":
1119
+ return JOINABLE_FROM.has(from) ? "joining" : null;
1120
+ case "back_to_prejoin":
1121
+ return from === "left" || from === "disconnected" ? "prejoin" : null;
1122
+ case "retry":
1123
+ return isRefusedPhase(from) || from === "unsupported" ? "resolving" : null;
1124
+ case "admission": {
1125
+ if (from !== "joining" && !WAITING_PHASES.has(from)) return null;
1126
+ return admissionPhase(event.admission);
1127
+ }
1128
+ case "admitted":
1129
+ return WAITING_PHASES.has(from) ? "joining" : null;
1130
+ case "connected":
1131
+ return from === "joining" ? "in_call" : null;
1132
+ case "join_failed":
1133
+ return from === "joining" ? refusalPhase(event.reason) : null;
1134
+ case "exit":
1135
+ return from === "in_call" || from === "joining" ? event.to : null;
1136
+ case "leave":
1137
+ return from === "in_call" || from === "joining" || WAITING_PHASES.has(from) ? "left" : null;
1138
+ case "ended":
1139
+ return NOT_ENDABLE.has(from) ? null : "ended";
1140
+ case "ring_out":
1141
+ return IDLE_PHASES.has(from) || from === "prejoin" ? "ringing_out" : null;
1142
+ case "ring_in":
1143
+ return IDLE_PHASES.has(from) ? "ringing_in" : null;
1144
+ case "call_answered":
1145
+ return RINGING.has(from) ? "joining" : null;
1146
+ case "call_outcome":
1147
+ return RINGING.has(from) ? event.outcome : null;
1148
+ case "reset":
1149
+ return LIVE_PHASES.has(from) ? null : "resolving";
1150
+ }
1151
+ }
1152
+ function resolveExit(input) {
1153
+ const { server, hint } = input;
1154
+ if (server === null) {
1155
+ return { to: "disconnected", reason: null, connection: "server_unreachable", notice: null };
1156
+ }
1157
+ const word = server?.exitReason ?? null;
1158
+ if (server !== void 0 && (server.sessionPhase === "ended" || server.meetingPhase === "ended" || word === "ended" || word === "time_limit")) {
1159
+ return { to: "ended", reason: word === "time_limit" ? "time_limit" : "ended", connection: "stable", notice: null };
1160
+ }
1161
+ if (word === "removed") return { to: "removed", reason: "removed", connection: "stable", notice: null };
1162
+ if (word === "displaced") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1163
+ if (word === "sent_to_waiting") {
1164
+ return { to: "waiting:knocking", reason: "sent_to_waiting", connection: "stable", notice: "sent-to-waiting" };
1165
+ }
1166
+ if (hint === "DUPLICATE_IDENTITY") return { to: "superseded", reason: "displaced", connection: "stable", notice: null };
1167
+ if (hint === "PARTICIPANT_REMOVED" && word === null && server === void 0) {
1168
+ return { to: "removed", reason: "removed", connection: "stable", notice: null };
1169
+ }
1170
+ if (hint === "ROOM_DELETED" && server === void 0) {
1171
+ return { to: "ended", reason: "ended", connection: "stable", notice: null };
1172
+ }
1173
+ if (input.frozen === true) return { to: "inactive", reason: "inactive", connection: "stable", notice: null };
1174
+ return { to: "disconnected", reason: null, connection: "stable", notice: null };
1175
+ }
1176
+
1017
1177
  // src/core/types.ts
1018
1178
  var asMeetingId = (value) => value;
1019
1179
  var asCallId = (value) => value;
@@ -1105,6 +1265,39 @@ function createCallCenter(options) {
1105
1265
  clearRing(invite.id);
1106
1266
  incoming.delete(invite.id);
1107
1267
  if (outgoing !== null && outgoing.id === invite.id) outgoing = invite;
1268
+ const held = options.store.snapshot().call;
1269
+ if (held !== null && held.invite.id === invite.id && held.direction === "incoming") {
1270
+ const outcome = invite.state === "connecting" || invite.state === "active" ? "answered_elsewhere" : invite.state === "declined" ? "reset" : "call_missed";
1271
+ const moved = outcome === "reset" ? options.store.dispatch({ type: "reset" }) : options.store.dispatch({ type: "call_outcome", outcome });
1272
+ if (moved) options.store.patch({ call: outcome === "reset" ? null : { ...held, invite } });
1273
+ }
1274
+ options.onSettled?.(invite);
1275
+ }
1276
+ function settleOutgoing(invite) {
1277
+ if (outgoing === null || outgoing.id !== invite.id) return;
1278
+ clearRing(invite.id);
1279
+ outgoing = invite;
1280
+ const store = options.store;
1281
+ const held = store.snapshot().call;
1282
+ if (held !== null && held.invite.id === invite.id) store.patch({ call: { ...held, invite } });
1283
+ switch (invite.state) {
1284
+ case "connecting":
1285
+ case "active":
1286
+ if (store.dispatch({ type: "call_answered" })) options.onAnswered?.(invite);
1287
+ break;
1288
+ case "declined":
1289
+ store.dispatch({ type: "call_outcome", outcome: "call_declined" });
1290
+ break;
1291
+ case "busy":
1292
+ store.dispatch({ type: "call_outcome", outcome: "call_busy" });
1293
+ break;
1294
+ case "missed":
1295
+ case "failed":
1296
+ store.dispatch({ type: "call_outcome", outcome: "call_missed" });
1297
+ break;
1298
+ default:
1299
+ break;
1300
+ }
1108
1301
  options.onSettled?.(invite);
1109
1302
  }
1110
1303
  function ingest(invite) {
@@ -1113,7 +1306,7 @@ function createCallCenter(options) {
1113
1306
  if (identity.userId === null) return;
1114
1307
  if (invite.callerUserId === identity.userId) return;
1115
1308
  const phase = options.store.snapshot().phase;
1116
- if (phase === "connected" || phase === "connecting" || phase === "reconnecting") {
1309
+ if (isLivePhase(phase) || phase === "ringing_out") {
1117
1310
  void repository.settleCallInvite(invite.id, "busy", null).then((settled) => {
1118
1311
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
1119
1312
  }).catch((error) => {
@@ -1126,6 +1319,9 @@ function createCallCenter(options) {
1126
1319
  return;
1127
1320
  }
1128
1321
  incoming.set(invite.id, { ...invite, state: "ringing" });
1322
+ if (options.store.snapshot().call === null && options.store.dispatch({ type: "ring_in" })) {
1323
+ options.store.patch({ call: { invite: { ...invite, state: "ringing" }, direction: "incoming" } });
1324
+ }
1129
1325
  options.onIncoming?.(invite);
1130
1326
  timeouts.set(
1131
1327
  invite.id,
@@ -1163,6 +1359,7 @@ function createCallCenter(options) {
1163
1359
  const invite = message.data;
1164
1360
  const held = incoming.get(invite.id);
1165
1361
  if (held !== void 0) settleLocal(invite);
1362
+ else if (outgoing?.id === invite.id) settleOutgoing(invite);
1166
1363
  }
1167
1364
  }
1168
1365
  ],
@@ -1229,6 +1426,9 @@ function createCallCenter(options) {
1229
1426
  expiresAt: new Date(timers.now() + ringTimeoutMs).toISOString()
1230
1427
  });
1231
1428
  outgoing = invite;
1429
+ if (options.store.dispatch({ type: "ring_out" })) {
1430
+ options.store.patch({ call: { invite, direction: "outgoing" } });
1431
+ }
1232
1432
  for (const callee of people) {
1233
1433
  to(callee).send(MEET_EVENTS.ring, invite);
1234
1434
  }
@@ -1237,8 +1437,7 @@ function createCallCenter(options) {
1237
1437
  timers.setTimeout(() => {
1238
1438
  if (outgoing?.id !== invite.id) return;
1239
1439
  void repository.settleCallInvite(invite.id, "missed", null).then((settled) => {
1240
- outgoing = settled;
1241
- options.onSettled?.(settled);
1440
+ settleOutgoing(settled);
1242
1441
  }).catch(() => void 0);
1243
1442
  }, ringTimeoutMs)
1244
1443
  );
@@ -1253,7 +1452,9 @@ function createCallCenter(options) {
1253
1452
  "A late accept must not resurrect a settled call. Close the ringing UI; the caller has already moved on."
1254
1453
  );
1255
1454
  }
1455
+ if (options.store.snapshot().phase === "ringing_in") options.store.dispatch({ type: "call_answered" });
1256
1456
  const settled = await repository.settleCallInvite(id, "connecting", null);
1457
+ options.store.patch({ call: { invite: settled, direction: "incoming" } });
1257
1458
  settleLocal(settled);
1258
1459
  channel?.send(MEET_EVENTS.settled, settled);
1259
1460
  to(settled.callerUserId).send(MEET_EVENTS.settled, { ...settled, mode });
@@ -1261,6 +1462,9 @@ function createCallCenter(options) {
1261
1462
  },
1262
1463
  async decline(id, message) {
1263
1464
  const settled = await repository.settleCallInvite(id, "declined", message ?? null);
1465
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1466
+ options.store.patch({ call: null });
1467
+ }
1264
1468
  settleLocal(settled);
1265
1469
  channel?.send(MEET_EVENTS.settled, settled);
1266
1470
  to(settled.callerUserId).send(MEET_EVENTS.settled, settled);
@@ -1270,6 +1474,9 @@ function createCallCenter(options) {
1270
1474
  const settled = await repository.settleCallInvite(id, "cancelled", null);
1271
1475
  clearRing(id);
1272
1476
  if (outgoing?.id === id) outgoing = settled;
1477
+ if (options.store.snapshot().call?.invite.id === id && options.store.dispatch({ type: "reset" })) {
1478
+ options.store.patch({ call: null });
1479
+ }
1273
1480
  for (const callee of settled.calleeUserIds) {
1274
1481
  to(callee).send(MEET_EVENTS.cancel, settled);
1275
1482
  }
@@ -1301,7 +1508,9 @@ var TABLES = {
1301
1508
  /** Wave 1 (0.8): who a meeting was sent to, and what they answered. */
1302
1509
  invitees: "meet_invitees",
1303
1510
  /** Wave 1 (0.8): one occurrence of a series cancelled or moved. */
1304
- occurrenceOverrides: "meet_occurrence_overrides"
1511
+ occurrenceOverrides: "meet_occurrence_overrides",
1512
+ /** S5: one row per RUN of a meeting; carries that run's note-taker state. */
1513
+ sessions: "meet_sessions"
1305
1514
  };
1306
1515
  var RPCS = {
1307
1516
  /** Atomic: create the invite row and stamp its expiry in one statement. */
@@ -1777,6 +1986,18 @@ function createMeetRepository(options) {
1777
1986
  );
1778
1987
  }
1779
1988
  }
1989
+ async function withRunNoteTaker(meeting) {
1990
+ const response = await db.from(TABLES.sessions).select("note_taker,started_at").eq("meeting_id", meeting.id).order("started_at", { ascending: false }).limit(1);
1991
+ const run = response.error === null ? (response.data ?? [])[0] : void 0;
1992
+ const noteTaker = run?.["note_taker"];
1993
+ if (typeof noteTaker !== "object" || noteTaker === null || Array.isArray(noteTaker)) {
1994
+ return meeting;
1995
+ }
1996
+ return {
1997
+ ...meeting,
1998
+ metadata: { ...meeting.metadata ?? {}, note_taker: noteTaker }
1999
+ };
2000
+ }
1780
2001
  return {
1781
2002
  schema: MEET_SCHEMA,
1782
2003
  tables: TABLES,
@@ -2015,13 +2236,13 @@ function createMeetRepository(options) {
2015
2236
  "meet.meetingBySlug",
2016
2237
  () => db.rpc(RPCS.meetingBySlug, { p_slug: slug })
2017
2238
  );
2018
- return projectMeeting(row);
2239
+ return withRunNoteTaker(projectMeeting(row));
2019
2240
  },
2020
2241
  async meeting(id) {
2021
2242
  const response = await db.from(TABLES.meetings).select(MEETING_COLUMNS).eq("id", id).single();
2022
2243
  if (response.error !== null) throw normalizeMeetError(response.error, "meet.meeting");
2023
2244
  if (response.data === null) throw invalidResponse("meet.meeting", "no such meeting");
2024
- return projectMeeting(response.data);
2245
+ return withRunNoteTaker(projectMeeting(response.data));
2025
2246
  },
2026
2247
  async listMeetings({ organizationId = null, limit = 25, before = null }) {
2027
2248
  let query = db.from(TABLES.meetings).select(MEETING_COLUMNS);
@@ -2782,8 +3003,15 @@ var NO_GRANTS = {
2782
3003
  canPublish: false,
2783
3004
  canSubscribe: false
2784
3005
  };
3006
+ var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
2785
3007
  var EMPTY = {
2786
- phase: "idle",
3008
+ phase: "resolving",
3009
+ reason: null,
3010
+ phaseNotice: null,
3011
+ roomName: null,
3012
+ connection: "stable",
3013
+ share: NO_SHARE,
3014
+ call: null,
2787
3015
  waiting: null,
2788
3016
  meeting: null,
2789
3017
  localIdentity: null,
@@ -2847,8 +3075,20 @@ function createMeetStore(initial = {}) {
2847
3075
  return () => listeners.delete(listener);
2848
3076
  },
2849
3077
  patch(next) {
3078
+ if ("phase" in next || "reason" in next || "phaseNotice" in next) {
3079
+ throw new TypeError("meet store: the phase moves only through store.dispatch(event)");
3080
+ }
2850
3081
  commit({ ...state, ...next });
2851
3082
  },
3083
+ dispatch(event, detail) {
3084
+ const to = nextPhase(state.phase, event);
3085
+ if (to === null) return false;
3086
+ const reason = detail?.reason !== void 0 ? detail.reason : reasonOf(to);
3087
+ const phaseNotice = detail?.notice ?? null;
3088
+ if (to === state.phase && reason === state.reason && phaseNotice === state.phaseNotice) return true;
3089
+ commit({ ...state, phase: to, reason, phaseNotice });
3090
+ return true;
3091
+ },
2852
3092
  setParticipants(participants) {
2853
3093
  const names = learnNames(state.names, participants);
2854
3094
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
@@ -3842,7 +4082,8 @@ function createLiveKitDriver(handlers, options = {}) {
3842
4082
  }).on(RoomEvent.Disconnected, (reason) => {
3843
4083
  audioContainer?.remove();
3844
4084
  audioContainer = null;
3845
- handlers.disconnected(reason === void 0 ? null : String(reason));
4085
+ const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
4086
+ handlers.disconnected(named);
3846
4087
  });
3847
4088
  try {
3848
4089
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -4190,7 +4431,7 @@ function createMeetingSession(options) {
4190
4431
  }
4191
4432
  function apply(read) {
4192
4433
  const snapshot = store.snapshot();
4193
- if (snapshot.phase !== "lobby") return;
4434
+ if (!isWaitingPhase(snapshot.phase)) return;
4194
4435
  const admission = read.admission_state ?? null;
4195
4436
  const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4196
4437
  if (admission === "admitted" && !ended) {
@@ -4207,6 +4448,7 @@ function createMeetingSession(options) {
4207
4448
  if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4208
4449
  store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4209
4450
  }
4451
+ store.dispatch({ type: "admission", admission: "ended" }, { reason: "ended" });
4210
4452
  return;
4211
4453
  }
4212
4454
  if (admission === null) return;
@@ -4218,6 +4460,7 @@ function createMeetingSession(options) {
4218
4460
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4219
4461
  liveVersion: read.live_version ?? 0
4220
4462
  });
4463
+ store.dispatch({ type: "admission", admission });
4221
4464
  if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4222
4465
  stopWaiting();
4223
4466
  }
@@ -4313,7 +4556,7 @@ function createMeetingSession(options) {
4313
4556
  // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4314
4557
  event: MEET_EVENTS.stateChanged,
4315
4558
  onMessage: () => {
4316
- if (store.snapshot().phase === "lobby") void readState2();
4559
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4317
4560
  void refreshQueue();
4318
4561
  }
4319
4562
  }
@@ -4325,7 +4568,7 @@ function createMeetingSession(options) {
4325
4568
  if (meeting === null) return;
4326
4569
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4327
4570
  store.patch({ meeting: fresh, locked: fresh.locked });
4328
- if (store.snapshot().phase === "lobby") void readState2();
4571
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4329
4572
  }
4330
4573
  });
4331
4574
  },
@@ -4492,10 +4735,34 @@ function createMeetingSession(options) {
4492
4735
  },
4493
4736
  stopWaiting,
4494
4737
  readState: readState2,
4738
+ async readExit(connection) {
4739
+ const meeting = store.snapshot().meeting;
4740
+ const pass = options.resolvePass?.() ?? null;
4741
+ if (meeting === null || pass === null) return void 0;
4742
+ try {
4743
+ const response = await options.api.request({
4744
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}&connection=${encodeURIComponent(connection)}`,
4745
+ method: "GET",
4746
+ requireAuth: false,
4747
+ operation: "meet.state(exit)",
4748
+ headers: { [MEETING_PASS_HEADER]: pass }
4749
+ });
4750
+ if (!response.ok) return null;
4751
+ const read = await response.json();
4752
+ return {
4753
+ exitReason: read.exit_reason ?? null,
4754
+ sessionPhase: read.session_phase ?? null,
4755
+ meetingPhase: read.meeting_phase ?? null
4756
+ };
4757
+ } catch {
4758
+ return null;
4759
+ }
4760
+ },
4495
4761
  async withdraw() {
4496
4762
  stopWaiting();
4497
4763
  await postLeave("withdraw");
4498
4764
  patchWaiting({ admission: "withdrawn" });
4765
+ store.dispatch({ type: "admission", admission: "withdrawn" });
4499
4766
  hint();
4500
4767
  },
4501
4768
  async askAgain() {
@@ -5119,6 +5386,9 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5119
5386
  }
5120
5387
 
5121
5388
  // src/core/room.ts
5389
+ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5390
+ return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5391
+ }
5122
5392
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5123
5393
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5124
5394
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5186,7 +5456,7 @@ function createRoomEngine(options) {
5186
5456
  let disposed = false;
5187
5457
  let moving = false;
5188
5458
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5189
- async function leaveRoom() {
5459
+ async function dropRoom() {
5190
5460
  const target = driver;
5191
5461
  driver = null;
5192
5462
  if (current !== null) {
@@ -5195,9 +5465,14 @@ function createRoomEngine(options) {
5195
5465
  }
5196
5466
  store.setLocalTrack("microphone", IDLE_TRACK);
5197
5467
  store.setLocalTrack("camera", IDLE_TRACK);
5198
- store.patch({ phase: "left" });
5468
+ store.patch({ share: NO_SHARE, connection: "stable" });
5199
5469
  await target?.disconnect();
5200
5470
  }
5471
+ async function leaveRoom() {
5472
+ store.dispatch({ type: "leave" });
5473
+ await dropRoom();
5474
+ options.onLeft?.(null);
5475
+ }
5201
5476
  async function confirmMeetingEnd() {
5202
5477
  const meeting = store.snapshot().meeting;
5203
5478
  const target = driver;
@@ -5219,7 +5494,9 @@ function createRoomEngine(options) {
5219
5494
  const latest = store.snapshot().meeting;
5220
5495
  if (latest === null) return;
5221
5496
  store.patch({ meeting: { ...latest, endedAt: verified.endedAt } });
5222
- await leaveRoom();
5497
+ store.dispatch({ type: "ended" }, { reason: "ended" });
5498
+ await dropRoom();
5499
+ options.onLeft?.("ended");
5223
5500
  } catch (error) {
5224
5501
  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.");
5225
5502
  } finally {
@@ -5231,12 +5508,12 @@ function createRoomEngine(options) {
5231
5508
  options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
5232
5509
  }
5233
5510
  let ambiguousShareDenials = 0;
5234
- function fail(error, operation) {
5511
+ function failJoin(error, operation) {
5235
5512
  const normalized = normalizeMeetError(error, operation);
5236
5513
  store.patch({
5237
- phase: "failed",
5238
5514
  failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5239
5515
  });
5516
+ store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
5240
5517
  report(
5241
5518
  normalized.code === "session-unavailable" ? "warn" : "error",
5242
5519
  normalized.message,
@@ -5244,6 +5521,14 @@ function createRoomEngine(options) {
5244
5521
  );
5245
5522
  return normalized;
5246
5523
  }
5524
+ async function lostRoom(error, operation) {
5525
+ const normalized = normalizeMeetError(error, operation);
5526
+ store.patch({ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason } });
5527
+ await dropRoom();
5528
+ store.dispatch({ type: "exit", to: "disconnected" });
5529
+ report("error", normalized.message, normalized.remedy);
5530
+ return normalized;
5531
+ }
5247
5532
  const handlers = {
5248
5533
  participants(list) {
5249
5534
  const joinedAt = new Date(now()).toISOString();
@@ -5286,9 +5571,12 @@ function createRoomEngine(options) {
5286
5571
  store.setQuality(asParticipantIdentity(who), value);
5287
5572
  },
5288
5573
  connectionState(state) {
5289
- if (state === "connected") store.patch({ phase: "connected", failure: null });
5290
- else if (state === "reconnecting") store.patch({ phase: "reconnecting" });
5291
- else if (state === "connecting") store.patch({ phase: "connecting" });
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
+ }
5292
5580
  },
5293
5581
  transcription(event) {
5294
5582
  const snapshot = store.snapshot();
@@ -5415,17 +5703,48 @@ function createRoomEngine(options) {
5415
5703
  }
5416
5704
  },
5417
5705
  disconnected(reason) {
5418
- if (moving) return;
5419
- if (current !== null) {
5420
- tokens.invalidate(current.roomName);
5421
- current = null;
5422
- }
5423
- store.patch({ phase: "left" });
5424
- options.onLeft?.(reason);
5706
+ if (moving || driver === null) return;
5707
+ void resolveExitAndApply(reason);
5425
5708
  }
5426
5709
  };
5710
+ async function resolveExitAndApply(hint) {
5711
+ const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5712
+ await dropRoom();
5713
+ let server;
5714
+ if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
5715
+ server = void 0;
5716
+ } else {
5717
+ try {
5718
+ server = await options.readExit(connection);
5719
+ } catch {
5720
+ server = null;
5721
+ }
5722
+ }
5723
+ if (disposed) return;
5724
+ const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5725
+ store.patch({ connection: resolved.connection });
5726
+ if (resolved.to === "waiting:knocking") {
5727
+ store.patch({
5728
+ waiting: {
5729
+ admission: "knocking",
5730
+ admittersPresent: null,
5731
+ reknockAllowed: true,
5732
+ knockExpiresAt: null,
5733
+ pollSeconds: store.snapshot().waiting?.pollSeconds ?? 3,
5734
+ liveVersion: 0
5735
+ }
5736
+ });
5737
+ }
5738
+ store.dispatch({ type: "exit", to: resolved.to }, { reason: resolved.reason, notice: resolved.notice });
5739
+ report(
5740
+ resolved.to === "disconnected" ? "warn" : "info",
5741
+ `meet: the room connection ended (${hint ?? "no LiveKit reason"}) \u2192 ${resolved.to}`,
5742
+ resolved.to === "disconnected" ? "Press Rejoin; the meeting is still going." : "The screen says what happened."
5743
+ );
5744
+ options.onLeft?.(hint);
5745
+ }
5427
5746
  async function connect(request) {
5428
- store.patch({ phase: "requesting-token", failure: null, waiting: null });
5747
+ store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
5429
5748
  const token = await tokens.get({
5430
5749
  roomName: request.roomName,
5431
5750
  identity,
@@ -5448,10 +5767,10 @@ function createRoomEngine(options) {
5448
5767
  }
5449
5768
  });
5450
5769
  if (token.lobby || !token.canSubscribe) {
5770
+ const admission = token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking";
5451
5771
  store.patch({
5452
- phase: "lobby",
5453
5772
  waiting: {
5454
- admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5773
+ admission,
5455
5774
  admittersPresent: null,
5456
5775
  reknockAllowed: true,
5457
5776
  knockExpiresAt: null,
@@ -5459,9 +5778,10 @@ function createRoomEngine(options) {
5459
5778
  liveVersion: 0
5460
5779
  }
5461
5780
  });
5781
+ store.dispatch({ type: "admission", admission });
5462
5782
  return;
5463
5783
  }
5464
- store.patch({ phase: "connecting", waiting: null });
5784
+ store.patch({ waiting: null });
5465
5785
  const created = await options.driverFactory(handlers);
5466
5786
  if (disposed) {
5467
5787
  await created.disconnect();
@@ -5469,7 +5789,8 @@ function createRoomEngine(options) {
5469
5789
  }
5470
5790
  driver = created;
5471
5791
  await created.connect(token.serverUrl, token.token);
5472
- store.patch({ phase: "connected", failure: null });
5792
+ store.patch({ failure: null, connection: "stable" });
5793
+ store.dispatch({ type: "connected" });
5473
5794
  store.setLocalTrack("microphone", IDLE_TRACK);
5474
5795
  store.setLocalTrack("camera", IDLE_TRACK);
5475
5796
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
@@ -5540,9 +5861,17 @@ function createRoomEngine(options) {
5540
5861
  return {
5541
5862
  join(request) {
5542
5863
  if (joining !== null) return joining;
5864
+ if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
5865
+ report(
5866
+ "warn",
5867
+ `meet.join: cannot join from "${store.snapshot().phase}"`,
5868
+ "Join is offered from the pre-join, left, disconnected and replaced screens only."
5869
+ );
5870
+ return Promise.resolve();
5871
+ }
5543
5872
  lastRequest = request;
5544
5873
  joining = connect(request).catch((error) => {
5545
- throw fail(error, "meet.join");
5874
+ throw failJoin(error, "meet.join");
5546
5875
  }).finally(() => {
5547
5876
  joining = null;
5548
5877
  });
@@ -5559,10 +5888,16 @@ function createRoomEngine(options) {
5559
5888
  return;
5560
5889
  }
5561
5890
  if (joining !== null) return joining;
5891
+ const phase = store.snapshot().phase;
5892
+ const moved = phase === "joining" || store.dispatch({ type: "join", click: true }) || store.dispatch({ type: "admitted" });
5893
+ if (!moved) {
5894
+ report("warn", `meet.rejoin: cannot rejoin from "${phase}"`, "Rejoin is offered after leaving, a lost connection, or admission.");
5895
+ return;
5896
+ }
5562
5897
  tokens.invalidate(request.roomName);
5563
5898
  current = null;
5564
5899
  joining = connect(request).catch((error) => {
5565
- throw fail(error, "meet.rejoin");
5900
+ throw failJoin(error, "meet.rejoin");
5566
5901
  }).finally(() => {
5567
5902
  joining = null;
5568
5903
  });
@@ -5607,11 +5942,11 @@ function createRoomEngine(options) {
5607
5942
  joining = connect(lastRequest).finally(() => {
5608
5943
  joining = null;
5609
5944
  });
5610
- await joining.catch((again) => {
5611
- throw fail(again, "meet.moveToBreakout");
5945
+ await joining.catch(async (again) => {
5946
+ throw await lostRoom(again, "meet.moveToBreakout");
5612
5947
  });
5613
5948
  } else {
5614
- throw fail(error, "meet.moveToBreakout");
5949
+ throw await lostRoom(error, "meet.moveToBreakout");
5615
5950
  }
5616
5951
  throw normalized;
5617
5952
  } finally {
@@ -5619,6 +5954,9 @@ function createRoomEngine(options) {
5619
5954
  }
5620
5955
  },
5621
5956
  leave: leaveRoom,
5957
+ backToPrejoin() {
5958
+ store.dispatch({ type: "back_to_prejoin", click: true });
5959
+ },
5622
5960
  // Turning a device on mid-meeting is the SAME operation as turning it on
5623
5961
  // during the join, so it runs the same function. A failure here used to
5624
5962
  // stamp the room `failed` — a busy microphone would blank a live meeting.
@@ -5632,21 +5970,41 @@ function createRoomEngine(options) {
5632
5970
  },
5633
5971
  async setScreenShareEnabled(enabled) {
5634
5972
  if (driver === null) return;
5973
+ if (enabled && driver.supportsScreenShare?.() === false) {
5974
+ store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
5975
+ report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
5976
+ return;
5977
+ }
5978
+ if (enabled) store.patch({ share: { state: "picking", reason: null, remedy: null, at: now() } });
5635
5979
  try {
5636
5980
  await driver.setScreenShareEnabled(enabled);
5637
5981
  ambiguousShareDenials = 0;
5982
+ store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
5638
5983
  } catch (error) {
5639
5984
  const kind = classifyScreenShareError(error);
5640
5985
  if (kind === "ambiguous") {
5641
5986
  ambiguousShareDenials += 1;
5642
- if (ambiguousShareDenials < 2) return;
5987
+ if (ambiguousShareDenials < 2) {
5988
+ store.patch({ share: NO_SHARE });
5989
+ return;
5990
+ }
5643
5991
  }
5992
+ ambiguousShareDenials = 0;
5644
5993
  const normalized = normalizeScreenShareError(error, "meet.setScreenShareEnabled");
5645
5994
  report("warn", normalized.message, normalized.remedy);
5646
- const text3 = kind === "other" ? `Screen sharing is not available. ${normalized.remedy}` : `Screen sharing did not start. ${SCREEN_SHARE_BLOCKED_REMEDY}`;
5647
- store.patch({ actionNotice: { text: text3, failed: true, at: now() } });
5995
+ store.patch({
5996
+ share: {
5997
+ state: "refused",
5998
+ reason: kind === "other" ? "failed" : "blocked",
5999
+ remedy: kind === "other" ? normalized.remedy : SCREEN_SHARE_BLOCKED_REMEDY_FOR(normalized.remedy),
6000
+ at: now()
6001
+ }
6002
+ });
5648
6003
  }
5649
6004
  },
6005
+ dismissShareNotice() {
6006
+ if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
6007
+ },
5650
6008
  async raiseHand(raised) {
5651
6009
  const local = store.snapshot().localIdentity;
5652
6010
  if (local !== null) store.setHand(local, raised);
@@ -6613,28 +6971,6 @@ function createMeetingRecordCache(options = {}) {
6613
6971
  };
6614
6972
  }
6615
6973
 
6616
- // src/core/reasons.ts
6617
- var JOIN_REFUSAL_REASONS = [
6618
- "not_found",
6619
- "cancelled",
6620
- "ended",
6621
- "expired",
6622
- "too_early",
6623
- "sign_in_required",
6624
- "wrong_account",
6625
- "not_invited",
6626
- "locked",
6627
- "removed",
6628
- "lobby_rejects",
6629
- "meeting_full",
6630
- "platform_capacity",
6631
- "verification_required",
6632
- "registration_required"
6633
- ];
6634
- function isJoinRefusalReason(value) {
6635
- return typeof value === "string" && JOIN_REFUSAL_REASONS.includes(value);
6636
- }
6637
-
6638
6974
  // src/core/reactions.ts
6639
6975
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
6640
6976
 
@@ -6733,43 +7069,12 @@ function endedRecord(snapshot, given) {
6733
7069
  function observedPhase(input) {
6734
7070
  const { snapshot } = input;
6735
7071
  if (snapshot === null) return "resolving";
6736
- const raw = snapshot.phase;
6737
- const live = raw === "connected" || raw === "reconnecting";
6738
- if (endedRecord(snapshot, input.meeting ?? null) && !live) return "ended";
6739
- const reason = raw === "failed" ? snapshot.failure?.reason ?? null : null;
6740
- if (reason !== null) return reason === "ended" ? "ended" : `refused:${reason}`;
6741
- switch (raw) {
6742
- case "idle":
6743
- case "failed":
6744
- case "left":
6745
- return "prejoin";
6746
- case "lobby":
6747
- return waitingPhase(snapshot);
6748
- case "requesting-token":
6749
- case "connecting":
6750
- return "joining";
6751
- case "connected":
6752
- case "reconnecting":
6753
- return "in_call";
6754
- }
7072
+ const phase = snapshot.phase;
7073
+ if (endedRecord(snapshot, input.meeting ?? null) && !isLivePhase(phase) && !isCallPhase(phase)) return "ended";
7074
+ return phase;
6755
7075
  }
6756
- function waitingPhase(snapshot) {
6757
- switch (snapshot.waiting?.admission) {
6758
- case "waiting_host":
6759
- return "waiting:host_not_started";
6760
- case "denied":
6761
- return "denied";
6762
- case "expired":
6763
- return "knock_expired";
6764
- case "withdrawn":
6765
- return "left";
6766
- case "ended":
6767
- return "ended";
6768
- case "removed":
6769
- return "refused:removed";
6770
- default:
6771
- return "waiting:knocking";
6772
- }
7076
+ function isCallPhase(phase) {
7077
+ return phase.startsWith("ringing_") || phase.startsWith("call_") || phase === "answered_elsewhere";
6773
7078
  }
6774
7079
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6775
7080
  function observe(input) {
@@ -6782,10 +7087,10 @@ function observe(input) {
6782
7087
  const deviceInPlay = phase === "joining" || inCall;
6783
7088
  return {
6784
7089
  phase,
6785
- reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
7090
+ reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
6786
7091
  role,
6787
- connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6788
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
7092
+ connection: snapshot?.connection ?? "stable",
7093
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
6789
7094
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6790
7095
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6791
7096
  audioBlocked: null,
@@ -6877,9 +7182,11 @@ export {
6877
7182
  GUEST_RECORD_ACCESS_METADATA_KEY,
6878
7183
  GUEST_RECORD_NOT_SHARED,
6879
7184
  IDLE_MEDIA,
7185
+ IDLE_PHASES,
6880
7186
  IDLE_TRACK,
6881
7187
  JOIN_REFUSAL_REASONS,
6882
7188
  LISTEN_ONLY_NOTICE,
7189
+ LIVE_PHASES,
6883
7190
  MEETING_INVITE_KIND,
6884
7191
  MEETING_INVITE_VERSION,
6885
7192
  MEETING_PASS_HEADER,
@@ -6895,6 +7202,7 @@ export {
6895
7202
  NOTE_TAKER_IDENTITY_PREFIX,
6896
7203
  NOTE_TAKER_METADATA_KEY,
6897
7204
  NO_GRANTS,
7205
+ NO_SHARE,
6898
7206
  ORGANIZATION_HEADER,
6899
7207
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
6900
7208
  PARTICIPANT_ROLE_ATTRIBUTE,
@@ -6908,9 +7216,11 @@ export {
6908
7216
  TABLES,
6909
7217
  TRANSCRIPTION_ATTRIBUTES,
6910
7218
  TRANSCRIPTION_TOPIC,
7219
+ WAITING_PHASES,
6911
7220
  absenceSentence,
6912
7221
  activeWrapUpSummary,
6913
7222
  activityAttention,
7223
+ admissionPhase,
6914
7224
  announceMeeting,
6915
7225
  appPanelOf,
6916
7226
  applyCollabPacket,
@@ -6980,10 +7290,13 @@ export {
6980
7290
  isJoinRefusalReason,
6981
7291
  isLinkFollower,
6982
7292
  isListenOnly,
7293
+ isLivePhase,
6983
7294
  isNoteTaker,
6984
7295
  isObserverRole,
6985
7296
  isRealName,
7297
+ isRefusedPhase,
6986
7298
  isRoomEventTopic,
7299
+ isWaitingPhase,
6987
7300
  learnName,
6988
7301
  learnNames,
6989
7302
  listenOnlyNotice,
@@ -7007,6 +7320,7 @@ export {
7007
7320
  mergeRecord,
7008
7321
  microphoneNoticeCode,
7009
7322
  moveToRoom,
7323
+ nextPhase,
7010
7324
  nextTranslationBatch,
7011
7325
  normalizeDeviceError,
7012
7326
  normalizeMeetError,
@@ -7039,10 +7353,13 @@ export {
7039
7353
  pushMeetingToGoogleCalendar,
7040
7354
  readServerRefusal,
7041
7355
  readableTranscript,
7356
+ reasonOf,
7042
7357
  recordingLabel,
7043
7358
  recordingNotice,
7044
7359
  recordingOffsetMs,
7360
+ refusalPhase,
7045
7361
  rememberedMeetingPass,
7362
+ resolveExit,
7046
7363
  roleOf,
7047
7364
  roomTopic,
7048
7365
  rootAttributes,