@ai-matrx/meet 0.9.0 → 0.10.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/index.js CHANGED
@@ -301,7 +301,17 @@ var MEET_ROUTES = {
301
301
  */
302
302
  collab: "/api/v1/meet/collab",
303
303
  /** Meet wave 5: translate finished caption lines into this viewer's language. */
304
- captionsTranslate: "/api/v1/meet/captions/translate"
304
+ captionsTranslate: "/api/v1/meet/captions/translate",
305
+ /**
306
+ * S4 — THE STATE READ (CORE-DESIGN §2.6): `GET ?meeting_id=` with `x-meet-pass`. This
307
+ * person's admission (lazy expiry applied), whether anyone who can admit is here, the run's
308
+ * phase. Read every `waiting_poll_seconds` while waiting and on every `mx.state.changed`.
309
+ */
310
+ state: "/api/v1/meet/state",
311
+ /** S4: `withdraw` (stop waiting) or `leave` (close this tab); the pass may ride in the body. */
312
+ leave: "/api/v1/meet/leave",
313
+ /** S4: a pass holder (guests included) saw the recording notice, for this run. */
314
+ consent: "/api/v1/meet/consent"
305
315
  };
306
316
  function meetingPath(template, meetingId) {
307
317
  return template.replace("{meeting_id}", encodeURIComponent(meetingId));
@@ -375,6 +385,8 @@ function parseRoomToken(body, roomName) {
375
385
  meetingPassExpiresAt: readString(payload, "meeting_pass_expires_at"),
376
386
  sessionId: readString(payload, "session_id"),
377
387
  lobby: readBoolean(payload, "lobby", false),
388
+ admissionState: readString(payload, "admission_state") ?? void 0,
389
+ pollSeconds: typeof payload["poll_seconds"] === "number" ? payload["poll_seconds"] : void 0,
378
390
  breakoutRoomId: readString(payload, "breakout_room_id") ?? null
379
391
  };
380
392
  }
@@ -987,14 +999,15 @@ var MEET_EVENTS = {
987
999
  cancel: "mx.call.cancel",
988
1000
  /** You answered (or declined) on another device. Stop ringing here. */
989
1001
  settled: "mx.call.settled",
990
- /** Someone is in the lobby. Hosts render the knock. */
991
- knock: "mx.lobby.knock",
992
- /** A host decided. The knocker learns it here — they have no DB read. */
993
- admission: "mx.lobby.admission",
994
- /** The room was locked or unlocked. */
995
- lock: "mx.room.lock",
996
- /** Recording started or stopped. EVERY participant renders the indicator. */
997
- recording: "mx.room.recording"
1002
+ /**
1003
+ * 🚨 S4 (CORE-DESIGN §2.6): the ONE room-channel event — payload-free "something changed,
1004
+ * read again". Nothing a person must see rides it: a waiting person learns admitted /
1005
+ * denied / expired / ended by READING their row (`GET /v1/meet/state`) on a timer and on
1006
+ * every hint. The four payload broadcasts (`mx.lobby.knock`, `mx.lobby.admission`,
1007
+ * `mx.room.lock`, `mx.room.recording`) are gone with their senders and receivers. The
1008
+ * server publishes the same event on the same topic (aidream `admission.room_topic`).
1009
+ */
1010
+ stateChanged: "mx.state.changed"
998
1011
  };
999
1012
 
1000
1013
  // src/core/types.ts
@@ -1020,6 +1033,7 @@ function isLinkFollower(standing) {
1020
1033
  function isHostRole(role) {
1021
1034
  return role === "host" || role === "cohost";
1022
1035
  }
1036
+ var WAITING_ADMISSION_STATES = ["waiting_host", "knocking"];
1023
1037
 
1024
1038
  // src/core/calls.ts
1025
1039
  var TRANSITIONS = {
@@ -1632,7 +1646,14 @@ var RPCS = {
1632
1646
  getOrCreateMeeting: "meet_get_or_create_meeting",
1633
1647
  /** Resolve a durable public link, before/during/after its scheduled time. */
1634
1648
  meetingBySlug: "meet_meeting_by_slug",
1635
- admit: "meet_admit_participant",
1649
+ /*
1650
+ * ── S4 ADMISSION (CORE-DESIGN §2.4). Each returns the decided row; the caller then
1651
+ * sends the payload-free `mx.state.changed` hint. Authorized by `who_may_admit`.
1652
+ */
1653
+ admit: "meet_admit",
1654
+ deny: "meet_deny",
1655
+ undeny: "meet_undeny",
1656
+ admitAll: "meet_admit_all",
1636
1657
  setLock: "meet_set_lock",
1637
1658
  recordConsent: "meet_record_consent",
1638
1659
  /*
@@ -1675,13 +1696,13 @@ var RPCS = {
1675
1696
  /** Titles, agendas, current notes and final transcript lines; snippets mark hits with ⟦ ⟧. */
1676
1697
  search: "meet_search"
1677
1698
  };
1678
- var VOID_RPCS = [RPCS.admit, RPCS.recordConsent];
1699
+ var VOID_RPCS = [RPCS.recordConsent];
1679
1700
  var MEETING_COLUMNS = "id,organization_id,room_name,slug,title,kind,host_user_id,scheduled_for,scheduled_duration_minutes,started_at,ended_at,locked,lobby_enabled,recording_policy,ai_enabled,metadata,time_zone,agenda,recurrence_rule,join_before_host,cancelled_at,cancellation_reason,deleted_at,calendar_sequence,version";
1680
1701
  var INVITEE_COLUMNS = "id,meeting_id,invitee_user_id,email,display_name,role,rsvp_state,responded_at,rsvp_note,last_sent_sequence,last_sent_at";
1681
1702
  var RECORDING_COLUMNS = "id,meeting_id,state,started_at,ended_at,started_by,file_id,failure_reason";
1682
1703
  var SEGMENT_COLUMNS = "id,meeting_id,identity,speaker,text,started_at,is_final,metadata";
1683
1704
  var NOTE_COLUMNS = "id,meeting_id,text,kind,assignee_user_id,created_at,metadata";
1684
- var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,is_agent,joined_at,left_at,sessions:metadata->sessions";
1705
+ var ATTENDEE_COLUMNS = "id,meeting_id,identity,participant_user_id,display_name,avatar_url,role,admission_state,knocked_at,knock_expires_at,is_agent,joined_at,left_at,sessions:metadata->sessions";
1685
1706
  function projectStays(value) {
1686
1707
  if (!Array.isArray(value)) return [];
1687
1708
  const stays = [];
@@ -2048,7 +2069,16 @@ function createMeetRepository(options) {
2048
2069
  const role = text(row, "role");
2049
2070
  const admission = text(row, "admission_state");
2050
2071
  const known = ["host", "cohost", "presenter", "participant", "viewer"];
2051
- const states = ["knocking", "admitted", "denied", "removed"];
2072
+ const states = [
2073
+ "waiting_host",
2074
+ "knocking",
2075
+ "admitted",
2076
+ "denied",
2077
+ "expired",
2078
+ "withdrawn",
2079
+ "removed",
2080
+ "ended"
2081
+ ];
2052
2082
  const userId = text(row, "participant_user_id");
2053
2083
  const identity = requireText(row, "identity", operation);
2054
2084
  return {
@@ -2062,6 +2092,8 @@ function createMeetRepository(options) {
2062
2092
  userId: userId === null ? null : asUserId(userId),
2063
2093
  role: known.includes(role) ? role : "participant",
2064
2094
  admissionState: states.includes(admission) ? admission : "admitted",
2095
+ knockedAt: text(row, "knocked_at"),
2096
+ knockExpiresAt: text(row, "knock_expires_at"),
2065
2097
  isAgent: bool(row, "is_agent", false),
2066
2098
  joinedAt: text(row, "joined_at"),
2067
2099
  leftAt: text(row, "left_at"),
@@ -2340,16 +2372,26 @@ function createMeetRepository(options) {
2340
2372
  hasMore: rows.length > limit
2341
2373
  };
2342
2374
  },
2343
- async admit({ meetingId, identity, admitted, byUserId }) {
2344
- await guardVoid(
2345
- "meet.admit",
2346
- () => db.rpc(RPCS.admit, {
2375
+ async decideAdmission({ meetingId, identity, decision, byUserId }) {
2376
+ const fn = decision === "admit" ? RPCS.admit : decision === "deny" ? RPCS.deny : RPCS.undeny;
2377
+ const row = await guard(
2378
+ `meet.${decision}`,
2379
+ () => db.rpc(fn, {
2347
2380
  p_meeting_id: meetingId,
2348
2381
  p_identity: identity,
2349
- p_admitted: admitted,
2350
2382
  p_by_user_id: byUserId
2351
2383
  })
2352
2384
  );
2385
+ return projectAttendee(row);
2386
+ },
2387
+ async admitAll(meetingId, byUserId) {
2388
+ const response = await db.rpc(RPCS.admitAll, {
2389
+ p_meeting_id: meetingId,
2390
+ p_by_user_id: byUserId
2391
+ });
2392
+ if (response.error !== null) throw normalizeMeetError(response.error, "meet.admitAll");
2393
+ const rows = Array.isArray(response.data) ? response.data : [];
2394
+ return rows.map(projectAttendee);
2353
2395
  },
2354
2396
  async setLock(meetingId, locked, byUserId) {
2355
2397
  const row = await guard(
@@ -2697,6 +2739,7 @@ var NO_GRANTS = {
2697
2739
  };
2698
2740
  var EMPTY = {
2699
2741
  phase: "idle",
2742
+ waiting: null,
2700
2743
  meeting: null,
2701
2744
  localIdentity: null,
2702
2745
  grants: NO_GRANTS,
@@ -3974,19 +4017,25 @@ function roleOf(participants, identity) {
3974
4017
  return participants.find((p) => p.identity === identity)?.role ?? "participant";
3975
4018
  }
3976
4019
  var END_MEETING_NOT_SERVED = "Ending a meeting is not available on this server yet";
3977
- function lobbyEntryFrom(attendee) {
3978
- if (attendee.admissionState !== "knocking") return null;
4020
+ function lobbyEntryFrom(attendee, now = Date.now()) {
4021
+ if (!WAITING_ADMISSION_STATES.includes(attendee.admissionState)) return null;
4022
+ const expires = attendee.knockExpiresAt ? Date.parse(attendee.knockExpiresAt) : Number.NaN;
4023
+ if (!Number.isNaN(expires) && expires <= now) return null;
3979
4024
  return {
3980
4025
  identity: attendee.identity,
3981
4026
  displayName: attendee.displayName,
3982
4027
  avatarUrl: attendee.avatarUrl,
3983
4028
  userId: attendee.userId,
3984
4029
  state: "knocking",
3985
- knockedAt: attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4030
+ knockedAt: attendee.knockedAt ?? attendee.joinedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4031
+ knockExpiresAt: attendee.knockExpiresAt ?? null,
3986
4032
  decidedAt: null,
3987
4033
  decidedBy: null
3988
4034
  };
3989
4035
  }
4036
+ function decidedState(state) {
4037
+ return WAITING_ADMISSION_STATES.includes(state) ? "expired" : state;
4038
+ }
3990
4039
  function createMeetingSession(options) {
3991
4040
  const { manager, repository, store, identity } = options;
3992
4041
  let channel = null;
@@ -4010,11 +4059,6 @@ function createMeetingSession(options) {
4010
4059
  }
4011
4060
  return identity.userId;
4012
4061
  }
4013
- function knockIdentity() {
4014
- return asParticipantIdentity(
4015
- identity.userId !== null ? `user:${identity.userId}` : `guest:${identity.deviceId}`
4016
- );
4017
- }
4018
4062
  function requireMeeting(operation) {
4019
4063
  const meeting = store.snapshot().meeting;
4020
4064
  if (meeting === null) {
@@ -4026,6 +4070,191 @@ function createMeetingSession(options) {
4026
4070
  }
4027
4071
  return meeting;
4028
4072
  }
4073
+ let polling = false;
4074
+ let pollTimer = null;
4075
+ let reading = null;
4076
+ let admittedOnce = false;
4077
+ let unlisten = null;
4078
+ function report(level, message, remedy) {
4079
+ options.onDiagnostic?.(remedy === void 0 ? { level, message } : { level, message, remedy });
4080
+ }
4081
+ function pollMs() {
4082
+ const seconds = store.snapshot().waiting?.pollSeconds ?? 3;
4083
+ return Math.max(1, Math.min(60, seconds)) * 1e3;
4084
+ }
4085
+ function schedule(ms2) {
4086
+ if (pollTimer !== null) clearTimeout(pollTimer);
4087
+ if (!polling) return;
4088
+ pollTimer = setTimeout(() => {
4089
+ pollTimer = null;
4090
+ void readState2().finally(() => schedule(pollMs()));
4091
+ }, ms2);
4092
+ }
4093
+ function listen() {
4094
+ if (unlisten !== null || typeof window === "undefined") return;
4095
+ const nudge = () => {
4096
+ if (typeof document !== "undefined" && document.visibilityState === "hidden") return;
4097
+ void readState2();
4098
+ };
4099
+ const goodbye = () => {
4100
+ const meeting = store.snapshot().meeting;
4101
+ const pass = options.resolvePass?.() ?? null;
4102
+ const waiting = store.snapshot().waiting;
4103
+ if (meeting === null || pass === null || waiting === null) return;
4104
+ if (!WAITING_ADMISSION_STATES.includes(waiting.admission)) return;
4105
+ try {
4106
+ navigator.sendBeacon?.(
4107
+ options.api.url(MEET_ROUTES.leave),
4108
+ new Blob([JSON.stringify({ meeting_id: meeting.id, intent: "withdraw", meeting_pass: pass })], {
4109
+ type: "application/json"
4110
+ })
4111
+ );
4112
+ } catch {
4113
+ }
4114
+ };
4115
+ window.addEventListener("online", nudge);
4116
+ window.addEventListener("pagehide", goodbye);
4117
+ document.addEventListener("visibilitychange", nudge);
4118
+ unlisten = () => {
4119
+ window.removeEventListener("online", nudge);
4120
+ window.removeEventListener("pagehide", goodbye);
4121
+ document.removeEventListener("visibilitychange", nudge);
4122
+ };
4123
+ }
4124
+ function stopWaiting() {
4125
+ polling = false;
4126
+ if (pollTimer !== null) clearTimeout(pollTimer);
4127
+ pollTimer = null;
4128
+ unlisten?.();
4129
+ unlisten = null;
4130
+ }
4131
+ function patchWaiting(next) {
4132
+ const current = store.snapshot().waiting;
4133
+ const base = current ?? {
4134
+ admission: "knocking",
4135
+ admittersPresent: null,
4136
+ reknockAllowed: true,
4137
+ knockExpiresAt: null,
4138
+ pollSeconds: 3,
4139
+ liveVersion: 0
4140
+ };
4141
+ store.patch({ waiting: { ...base, ...next } });
4142
+ }
4143
+ function apply(read) {
4144
+ const snapshot = store.snapshot();
4145
+ if (snapshot.phase !== "lobby") return;
4146
+ const admission = read.admission_state ?? null;
4147
+ const ended = admission === "ended" || read.session_phase === "ended" || read.meeting_phase === "ended";
4148
+ if (admission === "admitted" && !ended) {
4149
+ if (admittedOnce) return;
4150
+ admittedOnce = true;
4151
+ stopWaiting();
4152
+ options.onAdmitted?.();
4153
+ return;
4154
+ }
4155
+ if (ended) {
4156
+ stopWaiting();
4157
+ patchWaiting({ admission: "ended" });
4158
+ const meeting = snapshot.meeting;
4159
+ if (read.meeting_phase === "ended" && meeting !== null && meeting.endedAt === null) {
4160
+ store.patch({ meeting: { ...meeting, endedAt: (/* @__PURE__ */ new Date()).toISOString() } });
4161
+ }
4162
+ return;
4163
+ }
4164
+ if (admission === null) return;
4165
+ patchWaiting({
4166
+ admission,
4167
+ admittersPresent: read.admitters_present ?? null,
4168
+ reknockAllowed: read.reknock_allowed ?? true,
4169
+ knockExpiresAt: read.knock_expires_at ?? null,
4170
+ pollSeconds: read.poll_seconds ?? snapshot.waiting?.pollSeconds ?? 3,
4171
+ liveVersion: read.live_version ?? 0
4172
+ });
4173
+ if (admission === "denied" || admission === "expired" || admission === "withdrawn" || admission === "removed") {
4174
+ stopWaiting();
4175
+ }
4176
+ }
4177
+ async function readState2() {
4178
+ if (reading !== null) return reading;
4179
+ const meeting = store.snapshot().meeting;
4180
+ const pass = options.resolvePass?.() ?? null;
4181
+ if (meeting === null || pass === null) return;
4182
+ reading = (async () => {
4183
+ try {
4184
+ const response = await options.api.request({
4185
+ path: `${MEET_ROUTES.state}?meeting_id=${encodeURIComponent(meeting.id)}`,
4186
+ method: "GET",
4187
+ requireAuth: false,
4188
+ operation: "meet.state",
4189
+ headers: { [MEETING_PASS_HEADER]: pass }
4190
+ });
4191
+ if (!response.ok) {
4192
+ let body = null;
4193
+ try {
4194
+ body = await response.json();
4195
+ } catch {
4196
+ body = null;
4197
+ }
4198
+ const refusal2 = readServerRefusal(body);
4199
+ report(
4200
+ "warn",
4201
+ refusal2?.reason ?? `meet.state: the server answered ${response.status}`,
4202
+ refusal2?.remedy ?? "The waiting room keeps asking; nothing on this screen changes until the server answers."
4203
+ );
4204
+ return;
4205
+ }
4206
+ apply(await response.json());
4207
+ } catch (error) {
4208
+ report("info", `meet.state: ${String(error)}`, "Waiting continues; the state is read again shortly.");
4209
+ } finally {
4210
+ reading = null;
4211
+ }
4212
+ })();
4213
+ return reading;
4214
+ }
4215
+ async function postLeave(intent) {
4216
+ const meeting = requireMeeting(`meet.${intent}`);
4217
+ const pass = options.resolvePass?.() ?? null;
4218
+ const response = await options.api.request({
4219
+ path: MEET_ROUTES.leave,
4220
+ method: "POST",
4221
+ requireAuth: false,
4222
+ operation: `meet.${intent}`,
4223
+ body: { meeting_id: meeting.id, intent },
4224
+ ...pass !== null ? { headers: { [MEETING_PASS_HEADER]: pass } } : {}
4225
+ });
4226
+ if (!response.ok) {
4227
+ let body = null;
4228
+ try {
4229
+ body = await response.json();
4230
+ } catch {
4231
+ body = null;
4232
+ }
4233
+ const refusal2 = readServerRefusal(body);
4234
+ throw new MeetError(
4235
+ "transport",
4236
+ refusal2?.reason ?? `meet.${intent}: the server answered ${response.status}`,
4237
+ refusal2?.remedy ?? "You are still in the waiting room. Try again."
4238
+ );
4239
+ }
4240
+ }
4241
+ function hint() {
4242
+ channel?.send(MEET_EVENTS.stateChanged, { v: null });
4243
+ }
4244
+ async function refreshQueue() {
4245
+ const meeting = store.snapshot().meeting;
4246
+ if (meeting === null || isGuest(identity) || !isHostRole(effectiveRole(store.snapshot()))) return;
4247
+ try {
4248
+ const attendees = await repository.attendees(meeting.id);
4249
+ store.learn(attendees);
4250
+ for (const attendee of attendees) {
4251
+ const entry = lobbyEntryFrom(attendee);
4252
+ if (entry !== null) store.upsertLobby(entry);
4253
+ else store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4254
+ }
4255
+ } catch {
4256
+ }
4257
+ }
4029
4258
  return {
4030
4259
  watch(roomName) {
4031
4260
  channel?.close();
@@ -4033,58 +4262,22 @@ function createMeetingSession(options) {
4033
4262
  topic: roomTopic(roomName),
4034
4263
  broadcast: [
4035
4264
  {
4036
- event: MEET_EVENTS.knock,
4037
- onMessage: (message) => {
4038
- store.upsertLobby(message.data);
4039
- }
4040
- },
4041
- {
4042
- event: MEET_EVENTS.admission,
4043
- onMessage: (message) => {
4044
- const verdict = message.data;
4045
- const decided = asParticipantIdentity(verdict.identity);
4046
- store.setAdmission(
4047
- decided,
4048
- verdict.admitted ? "admitted" : "denied",
4049
- verdict.by === null ? null : verdict.by
4050
- );
4051
- if (decided === knockIdentity()) {
4052
- if (verdict.admitted) options.onAdmitted?.();
4053
- else options.onDenied?.();
4054
- }
4055
- }
4056
- },
4057
- {
4058
- event: MEET_EVENTS.lock,
4059
- onMessage: (message) => {
4060
- const { locked } = message.data;
4061
- const meeting = store.snapshot().meeting;
4062
- store.patch({
4063
- locked,
4064
- ...meeting !== null ? { meeting: { ...meeting, locked } } : {}
4065
- });
4066
- }
4067
- },
4068
- {
4069
- event: MEET_EVENTS.recording,
4070
- onMessage: (message) => {
4071
- const payload = message.data;
4072
- store.setRecording(
4073
- payload.state,
4074
- payload.by === null ? null : payload.by,
4075
- "room"
4076
- );
4265
+ // 🚨 PAYLOAD-FREE. Every receiver reads again; a spoofed hint only causes a read.
4266
+ event: MEET_EVENTS.stateChanged,
4267
+ onMessage: () => {
4268
+ if (store.snapshot().phase === "lobby") void readState2();
4269
+ void refreshQueue();
4077
4270
  }
4078
4271
  }
4079
4272
  ],
4080
- // THE BACKFILL DOOR. A host whose socket blipped must not miss a knock:
4081
- // re-reading the meeting restores lock/recording truth, and knockers
4082
- // re-announce on their own timer.
4273
+ // THE BACKFILL DOOR. A socket blip must not lose a decision: re-read the meeting
4274
+ // row, and a waiting tab re-reads its own state.
4083
4275
  onBackfill: async () => {
4084
4276
  const meeting = store.snapshot().meeting;
4085
4277
  if (meeting === null) return;
4086
4278
  const fresh = isGuest(identity) ? await repository.meetingBySlug(meeting.slug) : await readMeeting(meeting.id);
4087
4279
  store.patch({ meeting: fresh, locked: fresh.locked });
4280
+ if (store.snapshot().phase === "lobby") void readState2();
4088
4281
  }
4089
4282
  });
4090
4283
  },
@@ -4145,7 +4338,7 @@ function createMeetingSession(options) {
4145
4338
  store.upsertLobby(entry);
4146
4339
  return;
4147
4340
  }
4148
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4341
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4149
4342
  }
4150
4343
  },
4151
4344
  {
@@ -4197,62 +4390,75 @@ function createMeetingSession(options) {
4197
4390
  if (entry !== null) {
4198
4391
  store.upsertLobby(entry);
4199
4392
  } else {
4200
- store.setAdmission(attendee.identity, attendee.admissionState, null);
4393
+ store.setAdmission(attendee.identity, decidedState(attendee.admissionState), null);
4201
4394
  }
4202
4395
  }
4203
4396
  }
4204
4397
  });
4205
4398
  meetingChannel.requestBackfill("manual");
4206
4399
  },
4207
- knock(roomName) {
4208
- const handle = channel ?? manager.open({ topic: roomTopic(roomName) });
4209
- channel = handle;
4210
- const entry = {
4211
- identity: knockIdentity(),
4212
- displayName: identity.displayName,
4213
- avatarUrl: identity.avatarUrl,
4214
- userId: identity.userId,
4215
- state: "knocking",
4216
- knockedAt: (/* @__PURE__ */ new Date()).toISOString(),
4217
- decidedAt: null,
4218
- decidedBy: null
4219
- };
4220
- store.upsertLobby(entry);
4221
- handle.send(MEET_EVENTS.knock, entry);
4222
- },
4223
4400
  async admit(entry, admitted) {
4224
4401
  const by = requireHostUser("meet.admit");
4225
4402
  const meeting = requireMeeting("meet.admit");
4226
- await repository.admit({
4403
+ const row = await repository.decideAdmission({
4227
4404
  meetingId: meeting.id,
4228
4405
  identity: entry.identity,
4229
- admitted,
4406
+ decision: admitted ? "admit" : "deny",
4230
4407
  byUserId: by
4231
4408
  });
4232
4409
  store.upsertLobby({
4233
4410
  ...entry,
4234
- state: admitted ? "admitted" : "denied",
4411
+ state: decidedState(row.admissionState),
4235
4412
  decidedAt: (/* @__PURE__ */ new Date()).toISOString(),
4236
4413
  decidedBy: by
4237
4414
  });
4238
- channel?.send(MEET_EVENTS.admission, {
4415
+ hint();
4416
+ },
4417
+ async undeny(entry) {
4418
+ const by = requireHostUser("meet.undeny");
4419
+ const meeting = requireMeeting("meet.undeny");
4420
+ const row = await repository.decideAdmission({
4421
+ meetingId: meeting.id,
4239
4422
  identity: entry.identity,
4240
- admitted,
4241
- by
4423
+ decision: "undeny",
4424
+ byUserId: by
4242
4425
  });
4426
+ store.upsertLobby({ ...entry, state: decidedState(row.admissionState), decidedAt: (/* @__PURE__ */ new Date()).toISOString(), decidedBy: by });
4427
+ hint();
4243
4428
  },
4244
4429
  async admitAll() {
4245
- const knocking = store.snapshot().lobby.filter((entry) => entry.state === "knocking");
4246
- for (const entry of knocking) {
4247
- await this.admit(entry, true);
4430
+ const by = requireHostUser("meet.admitAll");
4431
+ const meeting = requireMeeting("meet.admitAll");
4432
+ const rows = await repository.admitAll(meeting.id, by);
4433
+ for (const row of rows) {
4434
+ store.setAdmission(row.identity, row.admissionState, by);
4248
4435
  }
4436
+ hint();
4437
+ },
4438
+ startWaiting() {
4439
+ if (polling) return;
4440
+ polling = true;
4441
+ admittedOnce = false;
4442
+ listen();
4443
+ schedule(0);
4444
+ },
4445
+ stopWaiting,
4446
+ readState: readState2,
4447
+ async withdraw() {
4448
+ stopWaiting();
4449
+ await postLeave("withdraw");
4450
+ patchWaiting({ admission: "withdrawn" });
4451
+ hint();
4452
+ },
4453
+ async askAgain() {
4454
+ await options.rejoin?.();
4249
4455
  },
4250
4456
  async setLock(locked) {
4251
4457
  const by = requireHostUser("meet.setLock");
4252
4458
  const meeting = requireMeeting("meet.setLock");
4253
4459
  const updated = await repository.setLock(meeting.id, locked, by);
4254
4460
  store.patch({ meeting: updated, locked: updated.locked });
4255
- channel?.send(MEET_EVENTS.lock, { locked: updated.locked });
4461
+ hint();
4256
4462
  return updated;
4257
4463
  },
4258
4464
  /**
@@ -4310,6 +4516,7 @@ function createMeetingSession(options) {
4310
4516
  return { endedAt, served: true };
4311
4517
  },
4312
4518
  dispose() {
4519
+ stopWaiting();
4313
4520
  channel?.close();
4314
4521
  channel = null;
4315
4522
  meetingChannel?.close();
@@ -5170,7 +5377,7 @@ function createRoomEngine(options) {
5170
5377
  }
5171
5378
  };
5172
5379
  async function connect(request) {
5173
- store.patch({ phase: "requesting-token", failure: null });
5380
+ store.patch({ phase: "requesting-token", failure: null, waiting: null });
5174
5381
  const token = await tokens.get({
5175
5382
  roomName: request.roomName,
5176
5383
  identity,
@@ -5190,11 +5397,21 @@ function createRoomEngine(options) {
5190
5397
  canSubscribe: token.canSubscribe
5191
5398
  }
5192
5399
  });
5193
- if (token.lobby) {
5194
- store.patch({ phase: "lobby" });
5400
+ if (token.lobby || !token.canSubscribe) {
5401
+ store.patch({
5402
+ phase: "lobby",
5403
+ waiting: {
5404
+ admission: token.admissionState !== void 0 && token.admissionState !== "admitted" ? token.admissionState : "knocking",
5405
+ admittersPresent: null,
5406
+ reknockAllowed: true,
5407
+ knockExpiresAt: null,
5408
+ pollSeconds: token.pollSeconds ?? 3,
5409
+ liveVersion: 0
5410
+ }
5411
+ });
5195
5412
  return;
5196
5413
  }
5197
- store.patch({ phase: "connecting" });
5414
+ store.patch({ phase: "connecting", waiting: null });
5198
5415
  const created = await options.driverFactory(handlers);
5199
5416
  if (disposed) {
5200
5417
  await created.disconnect();
@@ -6453,7 +6670,7 @@ function observedPhase(input) {
6453
6670
  case "left":
6454
6671
  return "prejoin";
6455
6672
  case "lobby":
6456
- return "waiting:knocking";
6673
+ return waitingPhase(snapshot);
6457
6674
  case "requesting-token":
6458
6675
  case "connecting":
6459
6676
  return "joining";
@@ -6462,6 +6679,24 @@ function observedPhase(input) {
6462
6679
  return "in_call";
6463
6680
  }
6464
6681
  }
6682
+ function waitingPhase(snapshot) {
6683
+ switch (snapshot.waiting?.admission) {
6684
+ case "waiting_host":
6685
+ return "waiting:host_not_started";
6686
+ case "denied":
6687
+ return "denied";
6688
+ case "expired":
6689
+ return "knock_expired";
6690
+ case "withdrawn":
6691
+ return "left";
6692
+ case "ended":
6693
+ return "ended";
6694
+ case "removed":
6695
+ return "refused:removed";
6696
+ default:
6697
+ return "waiting:knocking";
6698
+ }
6699
+ }
6465
6700
  var ADMITTER_ROLES = /* @__PURE__ */ new Set(["host", "cohost"]);
6466
6701
  function observe(input) {
6467
6702
  const { snapshot } = input;
@@ -6476,7 +6711,7 @@ function observe(input) {
6476
6711
  reason: phase.startsWith("refused:") ? phase.slice("refused:".length) : null,
6477
6712
  role,
6478
6713
  connection: snapshot?.phase === "reconnecting" ? "reconnecting" : "stable",
6479
- hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : null,
6714
+ hostPresent: inCall && snapshot !== null ? snapshot.participants.some((p) => ADMITTER_ROLES.has(p.role)) : snapshot?.phase === "lobby" ? snapshot.waiting?.admittersPresent ?? null : null,
6480
6715
  lobbyCount: canAdmit && snapshot !== null ? snapshot.lobby.filter((entry) => entry.state === "knocking").length : 0,
6481
6716
  recording: snapshot?.recording === "recording" || snapshot?.recording === "stopping",
6482
6717
  audioBlocked: null,