@ai-matrx/meet 0.10.7 → 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.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. */
@@ -2782,8 +2991,15 @@ var NO_GRANTS = {
2782
2991
  canPublish: false,
2783
2992
  canSubscribe: false
2784
2993
  };
2994
+ var NO_SHARE = { state: "none", reason: null, remedy: null, at: 0 };
2785
2995
  var EMPTY = {
2786
- phase: "idle",
2996
+ phase: "resolving",
2997
+ reason: null,
2998
+ phaseNotice: null,
2999
+ roomName: null,
3000
+ connection: "stable",
3001
+ share: NO_SHARE,
3002
+ call: null,
2787
3003
  waiting: null,
2788
3004
  meeting: null,
2789
3005
  localIdentity: null,
@@ -2847,8 +3063,20 @@ function createMeetStore(initial = {}) {
2847
3063
  return () => listeners.delete(listener);
2848
3064
  },
2849
3065
  patch(next) {
3066
+ if ("phase" in next || "reason" in next || "phaseNotice" in next) {
3067
+ throw new TypeError("meet store: the phase moves only through store.dispatch(event)");
3068
+ }
2850
3069
  commit({ ...state, ...next });
2851
3070
  },
3071
+ dispatch(event, detail) {
3072
+ const to = nextPhase(state.phase, event);
3073
+ if (to === null) return false;
3074
+ const reason = detail?.reason !== void 0 ? detail.reason : reasonOf(to);
3075
+ const phaseNotice = detail?.notice ?? null;
3076
+ if (to === state.phase && reason === state.reason && phaseNotice === state.phaseNotice) return true;
3077
+ commit({ ...state, phase: to, reason, phaseNotice });
3078
+ return true;
3079
+ },
2852
3080
  setParticipants(participants) {
2853
3081
  const names = learnNames(state.names, participants);
2854
3082
  const hands = new Map(state.participants.map((p) => [p.identity, p.handRaised]));
@@ -3842,7 +4070,8 @@ function createLiveKitDriver(handlers, options = {}) {
3842
4070
  }).on(RoomEvent.Disconnected, (reason) => {
3843
4071
  audioContainer?.remove();
3844
4072
  audioContainer = null;
3845
- handlers.disconnected(reason === void 0 ? null : String(reason));
4073
+ const named = reason === void 0 ? null : module?.DisconnectReason?.[reason] ?? String(reason);
4074
+ handlers.disconnected(named);
3846
4075
  });
3847
4076
  try {
3848
4077
  created.registerTextStreamHandler(TRANSCRIPTION_TOPIC, (reader, participantInfo) => {
@@ -4190,7 +4419,7 @@ function createMeetingSession(options) {
4190
4419
  }
4191
4420
  function apply(read) {
4192
4421
  const snapshot = store.snapshot();
4193
- if (snapshot.phase !== "lobby") return;
4422
+ if (!isWaitingPhase(snapshot.phase)) return;
4194
4423
  const admission = read.admission_state ?? null;
4195
4424
  const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4196
4425
  if (admission === "admitted" && !ended) {
@@ -4207,6 +4436,7 @@ function createMeetingSession(options) {
4207
4436
  if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4208
4437
  store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4209
4438
  }
4439
+ store.dispatch({ type: "admission", admission: "ended" }, { reason: "ended" });
4210
4440
  return;
4211
4441
  }
4212
4442
  if (admission === null) return;
@@ -4218,6 +4448,7 @@ function createMeetingSession(options) {
4218
4448
  pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4219
4449
  liveVersion: read.live_version ?? 0
4220
4450
  });
4451
+ store.dispatch({ type: "admission", admission });
4221
4452
  if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4222
4453
  stopWaiting();
4223
4454
  }
@@ -4313,7 +4544,7 @@ function createMeetingSession(options) {
4313
4544
  // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4314
4545
  event: MEET_EVENTS.stateChanged,
4315
4546
  onMessage: () => {
4316
- if (store.snapshot().phase === "lobby") void readState2();
4547
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4317
4548
  void refreshQueue();
4318
4549
  }
4319
4550
  }
@@ -4325,7 +4556,7 @@ function createMeetingSession(options) {
4325
4556
  if (meeting === null) return;
4326
4557
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4327
4558
  store.patch({ meeting: fresh, locked: fresh.locked });
4328
- if (store.snapshot().phase === "lobby") void readState2();
4559
+ if (isWaitingPhase(store.snapshot().phase)) void readState2();
4329
4560
  }
4330
4561
  });
4331
4562
  },
@@ -4492,10 +4723,34 @@ function createMeetingSession(options) {
4492
4723
  },
4493
4724
  stopWaiting,
4494
4725
  readState: readState2,
4726
+ async readExit(connection) {
4727
+ const meeting = store.snapshot().meeting;
4728
+ const pass = options.resolvePass?.() ?? null;
4729
+ if (meeting === null || pass === null) return void 0;
4730
+ try {
4731
+ const response = await options.api.request({
4732
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}&connection=${encodeURIComponent(connection)}`,
4733
+ method: "GET",
4734
+ requireAuth: false,
4735
+ operation: "meet.state(exit)",
4736
+ headers: { [MEETING_PASS_HEADER]: pass }
4737
+ });
4738
+ if (!response.ok) return null;
4739
+ const read = await response.json();
4740
+ return {
4741
+ exitReason: read.exit_reason ?? null,
4742
+ sessionPhase: read.session_phase ?? null,
4743
+ meetingPhase: read.meeting_phase ?? null
4744
+ };
4745
+ } catch {
4746
+ return null;
4747
+ }
4748
+ },
4495
4749
  async withdraw() {
4496
4750
  stopWaiting();
4497
4751
  await postLeave("withdraw");
4498
4752
  patchWaiting({ admission: "withdrawn" });
4753
+ store.dispatch({ type: "admission", admission: "withdrawn" });
4499
4754
  hint();
4500
4755
  },
4501
4756
  async askAgain() {
@@ -5119,6 +5374,9 @@ async function pushMeetingToGoogleCalendar(api, meetingId, target) {
5119
5374
  }
5120
5375
 
5121
5376
  // src/core/room.ts
5377
+ function SCREEN_SHARE_BLOCKED_REMEDY_FOR(remedy) {
5378
+ return /administrator|policy/i.test(remedy) ? remedy : SCREEN_SHARE_BLOCKED_REMEDY;
5379
+ }
5122
5380
  var PARTICIPANT_ROLE_ATTRIBUTE = "matrx.participant.role";
5123
5381
  var PARTICIPANT_CAN_RECORD_ATTRIBUTE = "matrx.participant.can_record";
5124
5382
  var PARTICIPANT_STANDING_ATTRIBUTE = "matrx.participant.standing";
@@ -5186,7 +5444,7 @@ function createRoomEngine(options) {
5186
5444
  let disposed = false;
5187
5445
  let moving = false;
5188
5446
  const confirmingEnds = /* @__PURE__ */ new WeakSet();
5189
- async function leaveRoom() {
5447
+ async function dropRoom() {
5190
5448
  const target = driver;
5191
5449
  driver = null;
5192
5450
  if (current !== null) {
@@ -5195,9 +5453,14 @@ function createRoomEngine(options) {
5195
5453
  }
5196
5454
  store.setLocalTrack("microphone", IDLE_TRACK);
5197
5455
  store.setLocalTrack("camera", IDLE_TRACK);
5198
- store.patch({ phase: "left" });
5456
+ store.patch({ share: NO_SHARE, connection: "stable" });
5199
5457
  await target?.disconnect();
5200
5458
  }
5459
+ async function leaveRoom() {
5460
+ store.dispatch({ type: "leave" });
5461
+ await dropRoom();
5462
+ options.onLeft?.(null);
5463
+ }
5201
5464
  async function confirmMeetingEnd() {
5202
5465
  const meeting = store.snapshot().meeting;
5203
5466
  const target = driver;
@@ -5219,7 +5482,9 @@ function createRoomEngine(options) {
5219
5482
  const latest = store.snapshot().meeting;
5220
5483
  if (latest === null) return;
5221
5484
  store.patch({ meeting: { ...latest, endedAt: verified.endedAt } });
5222
- await leaveRoom();
5485
+ store.dispatch({ type: "ended" }, { reason: "ended" });
5486
+ await dropRoom();
5487
+ options.onLeft?.("ended");
5223
5488
  } catch (error) {
5224
5489
  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
5490
  } finally {
@@ -5231,12 +5496,12 @@ function createRoomEngine(options) {
5231
5496
  options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
5232
5497
  }
5233
5498
  let ambiguousShareDenials = 0;
5234
- function fail(error, operation) {
5499
+ function failJoin(error, operation) {
5235
5500
  const normalized = normalizeMeetError(error, operation);
5236
5501
  store.patch({
5237
- phase: "failed",
5238
5502
  failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason }
5239
5503
  });
5504
+ store.dispatch({ type: "join_failed", reason: normalized.reason ?? null });
5240
5505
  report(
5241
5506
  normalized.code === "session-unavailable" ? "warn" : "error",
5242
5507
  normalized.message,
@@ -5244,6 +5509,14 @@ function createRoomEngine(options) {
5244
5509
  );
5245
5510
  return normalized;
5246
5511
  }
5512
+ async function lostRoom(error, operation) {
5513
+ const normalized = normalizeMeetError(error, operation);
5514
+ store.patch({ failure: { message: normalized.message, remedy: normalized.remedy, reason: normalized.reason } });
5515
+ await dropRoom();
5516
+ store.dispatch({ type: "exit", to: "disconnected" });
5517
+ report("error", normalized.message, normalized.remedy);
5518
+ return normalized;
5519
+ }
5247
5520
  const handlers = {
5248
5521
  participants(list) {
5249
5522
  const joinedAt = new Date(now()).toISOString();
@@ -5286,9 +5559,12 @@ function createRoomEngine(options) {
5286
5559
  store.setQuality(asParticipantIdentity(who), value);
5287
5560
  },
5288
5561
  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" });
5562
+ if (state === "connected") {
5563
+ if (store.snapshot().connection !== "stable") store.patch({ connection: "stable" });
5564
+ store.dispatch({ type: "connected" });
5565
+ } else if (state === "reconnecting") {
5566
+ store.patch({ connection: "reconnecting" });
5567
+ }
5292
5568
  },
5293
5569
  transcription(event) {
5294
5570
  const snapshot = store.snapshot();
@@ -5415,17 +5691,48 @@ function createRoomEngine(options) {
5415
5691
  }
5416
5692
  },
5417
5693
  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);
5694
+ if (moving || driver === null) return;
5695
+ void resolveExitAndApply(reason);
5425
5696
  }
5426
5697
  };
5698
+ async function resolveExitAndApply(hint) {
5699
+ const connection = current?.identity ?? store.snapshot().localIdentity ?? null;
5700
+ await dropRoom();
5701
+ let server;
5702
+ if (options.readExit === void 0 || connection === null || store.snapshot().meeting === null) {
5703
+ server = void 0;
5704
+ } else {
5705
+ try {
5706
+ server = await options.readExit(connection);
5707
+ } catch {
5708
+ server = null;
5709
+ }
5710
+ }
5711
+ if (disposed) return;
5712
+ const resolved = resolveExit({ server, hint, frozen: options.wasFrozen?.() ?? false });
5713
+ store.patch({ connection: resolved.connection });
5714
+ if (resolved.to === "waiting:knocking") {
5715
+ store.patch({
5716
+ waiting: {
5717
+ admission: "knocking",
5718
+ admittersPresent: null,
5719
+ reknockAllowed: true,
5720
+ knockExpiresAt: null,
5721
+ pollSeconds: store.snapshot().waiting?.pollSeconds ?? 3,
5722
+ liveVersion: 0
5723
+ }
5724
+ });
5725
+ }
5726
+ store.dispatch({ type: "exit", to: resolved.to }, { reason: resolved.reason, notice: resolved.notice });
5727
+ report(
5728
+ resolved.to === "disconnected" ? "warn" : "info",
5729
+ `meet: the room connection ended (${hint ?? "no LiveKit reason"}) \u2192 ${resolved.to}`,
5730
+ resolved.to === "disconnected" ? "Press Rejoin; the meeting is still going." : "The screen says what happened."
5731
+ );
5732
+ options.onLeft?.(hint);
5733
+ }
5427
5734
  async function connect(request) {
5428
- store.patch({ phase: "requesting-token", failure: null, waiting: null });
5735
+ store.patch({ failure: null, waiting: null, roomName: request.roomName, share: NO_SHARE, connection: "stable" });
5429
5736
  const token = await tokens.get({
5430
5737
  roomName: request.roomName,
5431
5738
  identity,
@@ -5448,10 +5755,10 @@ function createRoomEngine(options) {
5448
5755
  }
5449
5756
  });
5450
5757
  if (token.lobby || !token.canSubscribe) {
5758
+ const admission = token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking";
5451
5759
  store.patch({
5452
- phase: "lobby",
5453
5760
  waiting: {
5454
- admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5761
+ admission,
5455
5762
  admittersPresent: null,
5456
5763
  reknockAllowed: true,
5457
5764
  knockExpiresAt: null,
@@ -5459,9 +5766,10 @@ function createRoomEngine(options) {
5459
5766
  liveVersion: 0
5460
5767
  }
5461
5768
  });
5769
+ store.dispatch({ type: "admission", admission });
5462
5770
  return;
5463
5771
  }
5464
- store.patch({ phase: "connecting", waiting: null });
5772
+ store.patch({ waiting: null });
5465
5773
  const created = await options.driverFactory(handlers);
5466
5774
  if (disposed) {
5467
5775
  await created.disconnect();
@@ -5469,7 +5777,8 @@ function createRoomEngine(options) {
5469
5777
  }
5470
5778
  driver = created;
5471
5779
  await created.connect(token.serverUrl, token.token);
5472
- store.patch({ phase: "connected", failure: null });
5780
+ store.patch({ failure: null, connection: "stable" });
5781
+ store.dispatch({ type: "connected" });
5473
5782
  store.setLocalTrack("microphone", IDLE_TRACK);
5474
5783
  store.setLocalTrack("camera", IDLE_TRACK);
5475
5784
  const effects = options.effectsStorage?.load() ?? store.snapshot().effects;
@@ -5540,9 +5849,17 @@ function createRoomEngine(options) {
5540
5849
  return {
5541
5850
  join(request) {
5542
5851
  if (joining !== null) return joining;
5852
+ if (store.snapshot().phase !== "joining" && !store.dispatch({ type: "join", click: true })) {
5853
+ report(
5854
+ "warn",
5855
+ `meet.join: cannot join from "${store.snapshot().phase}"`,
5856
+ "Join is offered from the pre-join, left, disconnected and replaced screens only."
5857
+ );
5858
+ return Promise.resolve();
5859
+ }
5543
5860
  lastRequest = request;
5544
5861
  joining = connect(request).catch((error) => {
5545
- throw fail(error, "meet.join");
5862
+ throw failJoin(error, "meet.join");
5546
5863
  }).finally(() => {
5547
5864
  joining = null;
5548
5865
  });
@@ -5559,10 +5876,16 @@ function createRoomEngine(options) {
5559
5876
  return;
5560
5877
  }
5561
5878
  if (joining !== null) return joining;
5879
+ const phase = store.snapshot().phase;
5880
+ const moved = phase === "joining" || store.dispatch({ type: "join", click: true }) || store.dispatch({ type: "admitted" });
5881
+ if (!moved) {
5882
+ report("warn", `meet.rejoin: cannot rejoin from "${phase}"`, "Rejoin is offered after leaving, a lost connection, or admission.");
5883
+ return;
5884
+ }
5562
5885
  tokens.invalidate(request.roomName);
5563
5886
  current = null;
5564
5887
  joining = connect(request).catch((error) => {
5565
- throw fail(error, "meet.rejoin");
5888
+ throw failJoin(error, "meet.rejoin");
5566
5889
  }).finally(() => {
5567
5890
  joining = null;
5568
5891
  });
@@ -5607,11 +5930,11 @@ function createRoomEngine(options) {
5607
5930
  joining = connect(lastRequest).finally(() => {
5608
5931
  joining = null;
5609
5932
  });
5610
- await joining.catch((again) => {
5611
- throw fail(again, "meet.moveToBreakout");
5933
+ await joining.catch(async (again) => {
5934
+ throw await lostRoom(again, "meet.moveToBreakout");
5612
5935
  });
5613
5936
  } else {
5614
- throw fail(error, "meet.moveToBreakout");
5937
+ throw await lostRoom(error, "meet.moveToBreakout");
5615
5938
  }
5616
5939
  throw normalized;
5617
5940
  } finally {
@@ -5619,6 +5942,9 @@ function createRoomEngine(options) {
5619
5942
  }
5620
5943
  },
5621
5944
  leave: leaveRoom,
5945
+ backToPrejoin() {
5946
+ store.dispatch({ type: "back_to_prejoin", click: true });
5947
+ },
5622
5948
  // Turning a device on mid-meeting is the SAME operation as turning it on
5623
5949
  // during the join, so it runs the same function. A failure here used to
5624
5950
  // stamp the room `failed` — a busy microphone would blank a live meeting.
@@ -5632,21 +5958,41 @@ function createRoomEngine(options) {
5632
5958
  },
5633
5959
  async setScreenShareEnabled(enabled) {
5634
5960
  if (driver === null) return;
5961
+ if (enabled && driver.supportsScreenShare?.() === false) {
5962
+ store.patch({ share: { state: "refused", reason: "unsupported", remedy: null, at: now() } });
5963
+ report("warn", "meet: this browser cannot share a screen", "Share from a desktop browser (Chrome, Edge, Firefox or Safari).");
5964
+ return;
5965
+ }
5966
+ if (enabled) store.patch({ share: { state: "picking", reason: null, remedy: null, at: now() } });
5635
5967
  try {
5636
5968
  await driver.setScreenShareEnabled(enabled);
5637
5969
  ambiguousShareDenials = 0;
5970
+ store.patch({ share: { state: enabled ? "presenting" : "none", reason: null, remedy: null, at: now() } });
5638
5971
  } catch (error) {
5639
5972
  const kind = classifyScreenShareError(error);
5640
5973
  if (kind === "ambiguous") {
5641
5974
  ambiguousShareDenials += 1;
5642
- if (ambiguousShareDenials < 2) return;
5975
+ if (ambiguousShareDenials < 2) {
5976
+ store.patch({ share: NO_SHARE });
5977
+ return;
5978
+ }
5643
5979
  }
5980
+ ambiguousShareDenials = 0;
5644
5981
  const normalized = normalizeScreenShareError(error, "meet.setScreenShareEnabled");
5645
5982
  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() } });
5983
+ store.patch({
5984
+ share: {
5985
+ state: "refused",
5986
+ reason: kind === "other" ? "failed" : "blocked",
5987
+ remedy: kind === "other" ? normalized.remedy : SCREEN_SHARE_BLOCKED_REMEDY_FOR(normalized.remedy),
5988
+ at: now()
5989
+ }
5990
+ });
5648
5991
  }
5649
5992
  },
5993
+ dismissShareNotice() {
5994
+ if (store.snapshot().share.state === "refused") store.patch({ share: NO_SHARE });
5995
+ },
5650
5996
  async raiseHand(raised) {
5651
5997
  const local = store.snapshot().localIdentity;
5652
5998
  if (local !== null) store.setHand(local, raised);
@@ -6613,28 +6959,6 @@ function createMeetingRecordCache(options = {}) {
6613
6959
  };
6614
6960
  }
6615
6961
 
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
6962
  // src/core/reactions.ts
6639
6963
  var MEET_REACTIONS = ["\u{1F496}", "\u{1F44D}", "\u{1F389}", "\u{1F44F}", "\u{1F602}", "\u{1F62E}", "\u{1F622}", "\u{1F914}", "\u{1F44E}"];
6640
6964
 
@@ -6733,43 +7057,12 @@ function endedRecord(snapshot, given) {
6733
7057
  function observedPhase(input) {
6734
7058
  const { snapshot } = input;
6735
7059
  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
- }
7060
+ const phase = snapshot.phase;
7061
+ if (endedRecord(snapshot, input.meeting ?? null) && !isLivePhase(phase) && !isCallPhase(phase)) return "ended";
7062
+ return phase;
6755
7063
  }
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
- }
7064
+ function isCallPhase(phase) {
7065
+ return phase.startsWith("ringing_") || phase.startsWith("call_") || phase === "answered_elsewhere";
6773
7066
  }
6774
7067
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6775
7068
  function observe(input) {
@@ -6782,10 +7075,10 @@ function observe(input) {
6782
7075
  const deviceInPlay = phase === "joining" || inCall;
6783
7076
  return {
6784
7077
  phase,
6785
- reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
7078
+ reason: phase === "ended" ? snapshot?.reason ?? null : snapshot?.reason ?? reasonOf(phase),
6786
7079
  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,
7080
+ connection: snapshot?.connection ?? "stable",
7081
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot !== null && isWaitingPhase(snapshot.phase) ? snapshot.waiting?.admittersPresent ?? null : null,
6789
7082
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6790
7083
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6791
7084
  audioBlocked: null,
@@ -6877,9 +7170,11 @@ export {
6877
7170
  GUEST_RECORD_ACCESS_METADATA_KEY,
6878
7171
  GUEST_RECORD_NOT_SHARED,
6879
7172
  IDLE_MEDIA,
7173
+ IDLE_PHASES,
6880
7174
  IDLE_TRACK,
6881
7175
  JOIN_REFUSAL_REASONS,
6882
7176
  LISTEN_ONLY_NOTICE,
7177
+ LIVE_PHASES,
6883
7178
  MEETING_INVITE_KIND,
6884
7179
  MEETING_INVITE_VERSION,
6885
7180
  MEETING_PASS_HEADER,
@@ -6895,6 +7190,7 @@ export {
6895
7190
  NOTE_TAKER_IDENTITY_PREFIX,
6896
7191
  NOTE_TAKER_METADATA_KEY,
6897
7192
  NO_GRANTS,
7193
+ NO_SHARE,
6898
7194
  ORGANIZATION_HEADER,
6899
7195
  PARTICIPANT_CAN_RECORD_ATTRIBUTE,
6900
7196
  PARTICIPANT_ROLE_ATTRIBUTE,
@@ -6908,9 +7204,11 @@ export {
6908
7204
  TABLES,
6909
7205
  TRANSCRIPTION_ATTRIBUTES,
6910
7206
  TRANSCRIPTION_TOPIC,
7207
+ WAITING_PHASES,
6911
7208
  absenceSentence,
6912
7209
  activeWrapUpSummary,
6913
7210
  activityAttention,
7211
+ admissionPhase,
6914
7212
  announceMeeting,
6915
7213
  appPanelOf,
6916
7214
  applyCollabPacket,
@@ -6980,10 +7278,13 @@ export {
6980
7278
  isJoinRefusalReason,
6981
7279
  isLinkFollower,
6982
7280
  isListenOnly,
7281
+ isLivePhase,
6983
7282
  isNoteTaker,
6984
7283
  isObserverRole,
6985
7284
  isRealName,
7285
+ isRefusedPhase,
6986
7286
  isRoomEventTopic,
7287
+ isWaitingPhase,
6987
7288
  learnName,
6988
7289
  learnNames,
6989
7290
  listenOnlyNotice,
@@ -7007,6 +7308,7 @@ export {
7007
7308
  mergeRecord,
7008
7309
  microphoneNoticeCode,
7009
7310
  moveToRoom,
7311
+ nextPhase,
7010
7312
  nextTranslationBatch,
7011
7313
  normalizeDeviceError,
7012
7314
  normalizeMeetError,
@@ -7039,10 +7341,13 @@ export {
7039
7341
  pushMeetingToGoogleCalendar,
7040
7342
  readServerRefusal,
7041
7343
  readableTranscript,
7344
+ reasonOf,
7042
7345
  recordingLabel,
7043
7346
  recordingNotice,
7044
7347
  recordingOffsetMs,
7348
+ refusalPhase,
7045
7349
  rememberedMeetingPass,
7350
+ resolveExit,
7046
7351
  roleOf,
7047
7352
  roomTopic,
7048
7353
  rootAttributes,