@ai-matrx/meet 0.7.78 → 0.7.79

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/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog — `@ai-matrx/meet`
2
2
 
3
+ ## 0.7.79
4
+
5
+ **A meeting's record is read once per tab, and kept live.** `useMeetingRecord` held the bundle
6
+ in its own state and read all four parts (`meet_participants`, `meet_notes`,
7
+ `meet_transcript_segments`, `meet_recordings`) on every mount — so a board tile that slept
8
+ and woke (React `<Activity>` re-runs every effect), a Remove + Undo, and every second view of
9
+ the same meeting read the whole record again. The record now lives in one per-tab cache
10
+ (`core/record-cache.ts`, keyed by reader + meeting): the first view reads, later views and
11
+ remounts render the kept record and read nothing; a read in flight is joined; Retry and a
12
+ reconnect read again, and an older answer never paints over a newer one. A member's record is
13
+ kept current by ONE change-feed channel per meeting (notes, transcript, attendance,
14
+ recordings — applied row by row, no re-read), held while any view is mounted and 60 s after;
15
+ past that the record is marked stale and the next view reads it once. A meeting rename
16
+ re-assembles the kept record instead of re-reading it. New exports: `loadMeetingRecordParts`,
17
+ `assembleMeetingRecord`, `createMeetingRecordCache`, `MEETING_RECORD_GRACE_MS`, and
18
+ `meetingRecordCache()` from `/react`. Guard: `react/meeting-record-reads-once.test.tsx`
19
+ (4/4 red on 0.7.78).
20
+
21
+ Consumer action: none. A host test that renders the record for the same meeting twice and
22
+ expects two reads resets the cache between cases (`meetingRecordCache().reset()`).
23
+
3
24
  ## 0.7.78
4
25
 
5
26
  **A meeting can run inside a box.** `.mx-meet` was `height: 100dvh`, so a stage mounted in a
package/dist/index.cjs CHANGED
@@ -50,6 +50,7 @@ __export(src_exports, {
50
50
  LISTEN_ONLY_NOTICE: () => LISTEN_ONLY_NOTICE,
51
51
  MEETING_INVITE_KIND: () => MEETING_INVITE_KIND,
52
52
  MEETING_INVITE_VERSION: () => MEETING_INVITE_VERSION,
53
+ MEETING_RECORD_GRACE_MS: () => MEETING_RECORD_GRACE_MS,
53
54
  MEET_ANSWER_CONTEXT_EVENT: () => MEET_ANSWER_CONTEXT_EVENT,
54
55
  MEET_ANSWER_EVENTS: () => MEET_ANSWER_EVENTS,
55
56
  MEET_EVENTS: () => MEET_EVENTS,
@@ -84,6 +85,7 @@ __export(src_exports, {
84
85
  asParticipantIdentity: () => asParticipantIdentity,
85
86
  asRoomName: () => asRoomName,
86
87
  asUserId: () => asUserId,
88
+ assembleMeetingRecord: () => assembleMeetingRecord,
87
89
  assignedRoom: () => assignedRoom,
88
90
  attendanceCsv: () => attendanceCsv,
89
91
  attendanceReport: () => attendanceReport,
@@ -113,6 +115,7 @@ __export(src_exports, {
113
115
  createMeetRepository: () => createMeetRepository,
114
116
  createMeetStore: () => createMeetStore,
115
117
  createMeetingInviteHandler: () => createMeetingInviteHandler,
118
+ createMeetingRecordCache: () => createMeetingRecordCache,
116
119
  createMeetingSession: () => createMeetingSession,
117
120
  createMemoryDeviceStorage: () => createMemoryDeviceStorage,
118
121
  createRecordingController: () => createRecordingController,
@@ -147,6 +150,7 @@ __export(src_exports, {
147
150
  loadMeetingActivities: () => loadMeetingActivities,
148
151
  loadMeetingChat: () => loadMeetingChat,
149
152
  loadMeetingRecord: () => loadMeetingRecord,
153
+ loadMeetingRecordParts: () => loadMeetingRecordParts,
150
154
  mapConnectionState: () => mapConnectionState,
151
155
  mapQuality: () => mapQuality,
152
156
  meetingInviteAction: () => meetingInviteAction,
@@ -1033,13 +1037,13 @@ function mergeCaption(list, caption) {
1033
1037
  return list.map((entry, position) => position === index ? caption : entry);
1034
1038
  }
1035
1039
  function mergeRecord(list, record) {
1036
- const byId = list.findIndex((entry) => entry.source === "record" && entry.id === record.id);
1037
- if (byId !== -1) {
1038
- const existing = list[byId];
1040
+ const byId2 = list.findIndex((entry) => entry.source === "record" && entry.id === record.id);
1041
+ if (byId2 !== -1) {
1042
+ const existing = list[byId2];
1039
1043
  if (existing !== void 0 && existing.text === record.text && existing.final === record.final) {
1040
1044
  return list;
1041
1045
  }
1042
- return list.map((entry, position) => position === byId ? record : entry);
1046
+ return list.map((entry, position) => position === byId2 ? record : entry);
1043
1047
  }
1044
1048
  const captionIndex = list.findIndex((entry) => supersedes(record, entry));
1045
1049
  if (captionIndex === -1) return [...list, record];
@@ -5869,6 +5873,9 @@ function restrictionFor(part, error) {
5869
5873
  };
5870
5874
  }
5871
5875
  async function loadMeetingRecord(repository, meetingId, meeting, reader = { userId: null }) {
5876
+ return assembleMeetingRecord(meeting, await loadMeetingRecordParts(repository, meetingId, reader));
5877
+ }
5878
+ async function loadMeetingRecordParts(repository, meetingId, reader = { userId: null }) {
5872
5879
  const restrictions = [];
5873
5880
  async function attempt(part, run, empty) {
5874
5881
  if (reader.userId === null) {
@@ -5896,6 +5903,10 @@ async function loadMeetingRecord(repository, meetingId, meeting, reader = { user
5896
5903
  []
5897
5904
  )
5898
5905
  ]);
5906
+ return { attendees, notes, transcript, recordings, restrictions };
5907
+ }
5908
+ function assembleMeetingRecord(meeting, parts) {
5909
+ const { attendees, notes, transcript, recordings, restrictions } = parts;
5899
5910
  return {
5900
5911
  meeting,
5901
5912
  attendees,
@@ -6240,4 +6251,208 @@ async function setGuestRecordAccess(args) {
6240
6251
  };
6241
6252
  }
6242
6253
  var GUEST_RECORD_NOT_SHARED = NOT_SHARED;
6254
+
6255
+ // src/core/record-cache.ts
6256
+ var MEETING_RECORD_GRACE_MS = 6e4;
6257
+ var INITIAL = { loading: true, record: null, failure: null };
6258
+ function upsert2(rows, row, removedId, order) {
6259
+ const id = row?.id ?? removedId;
6260
+ if (id === void 0) return rows;
6261
+ const rest = rows.filter((existing) => existing.id !== id);
6262
+ return row === null ? rest : [...rest, row].sort(order);
6263
+ }
6264
+ var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
6265
+ var ascending = (a, b) => a === b ? 0 : a === null ? 1 : b === null ? -1 : a < b ? -1 : 1;
6266
+ function createMeetingRecordCache(options = {}) {
6267
+ const graceMs = options.graceMs ?? MEETING_RECORD_GRACE_MS;
6268
+ const entries = /* @__PURE__ */ new Map();
6269
+ const listeners = /* @__PURE__ */ new Set();
6270
+ const notify = () => {
6271
+ for (const listener of listeners) listener();
6272
+ };
6273
+ const entryOf = (key) => {
6274
+ let entry = entries.get(key);
6275
+ if (!entry) {
6276
+ entry = {
6277
+ state: INITIAL,
6278
+ meeting: null,
6279
+ parts: null,
6280
+ fresh: false,
6281
+ seq: 0,
6282
+ inflight: null,
6283
+ holders: 0,
6284
+ close: null,
6285
+ owner: null,
6286
+ grace: null
6287
+ };
6288
+ entries.set(key, entry);
6289
+ }
6290
+ return entry;
6291
+ };
6292
+ const assemble = (entry) => {
6293
+ if (entry.meeting === null || entry.parts === null) return;
6294
+ entry.state = { loading: false, failure: null, record: assembleMeetingRecord(entry.meeting, entry.parts) };
6295
+ notify();
6296
+ };
6297
+ return {
6298
+ state: (key) => key === null ? INITIAL : entries.get(key)?.state ?? INITIAL,
6299
+ subscribe(listener) {
6300
+ listeners.add(listener);
6301
+ return () => {
6302
+ listeners.delete(listener);
6303
+ };
6304
+ },
6305
+ ensure(key, read, { force = false } = {}) {
6306
+ const entry = entryOf(key);
6307
+ if (!force) {
6308
+ if (entry.inflight !== null) return entry.inflight;
6309
+ if (entry.fresh) return Promise.resolve();
6310
+ }
6311
+ const seq = ++entry.seq;
6312
+ if (entry.state.failure !== null) {
6313
+ entry.state = { ...entry.state, loading: true, failure: null };
6314
+ notify();
6315
+ }
6316
+ const request = read().then(
6317
+ (answer) => {
6318
+ if (seq !== entry.seq) return;
6319
+ entry.fresh = true;
6320
+ if (answer.kind === "parts") {
6321
+ entry.meeting = answer.meeting;
6322
+ entry.parts = answer.parts;
6323
+ assemble(entry);
6324
+ } else {
6325
+ entry.meeting = answer.bundle.meeting;
6326
+ entry.parts = null;
6327
+ entry.state = { loading: false, failure: null, record: answer.bundle };
6328
+ notify();
6329
+ }
6330
+ },
6331
+ (thrown) => {
6332
+ if (seq !== entry.seq) return;
6333
+ entry.fresh = false;
6334
+ entry.state = {
6335
+ loading: false,
6336
+ record: entry.state.record,
6337
+ failure: {
6338
+ message: thrown?.message ?? String(thrown),
6339
+ remedy: thrown?.remedy ?? "The meeting record could not be read. Reload the page; nothing was changed."
6340
+ }
6341
+ };
6342
+ notify();
6343
+ }
6344
+ );
6345
+ const tracked = request.finally(() => {
6346
+ if (entry.inflight === tracked) entry.inflight = null;
6347
+ });
6348
+ entry.inflight = tracked;
6349
+ return tracked;
6350
+ },
6351
+ setMeeting(key, meeting) {
6352
+ const entry = entries.get(key);
6353
+ if (!entry || entry.meeting === meeting) return;
6354
+ if (entry.parts !== null) {
6355
+ entry.meeting = meeting;
6356
+ assemble(entry);
6357
+ } else if (entry.state.record !== null) {
6358
+ entry.meeting = meeting;
6359
+ entry.state = { ...entry.state, record: { ...entry.state.record, meeting } };
6360
+ notify();
6361
+ }
6362
+ },
6363
+ apply(key, change) {
6364
+ const entry = entries.get(key);
6365
+ const parts = entry?.parts;
6366
+ if (!entry || !parts) return;
6367
+ const refused = parts.restrictions.some((r) => r.part === change.part);
6368
+ if (refused) return;
6369
+ switch (change.part) {
6370
+ case "transcript":
6371
+ entry.parts = {
6372
+ ...parts,
6373
+ transcript: upsert2(
6374
+ parts.transcript,
6375
+ change.row,
6376
+ change.removedId,
6377
+ (a, b) => ascending(a.startedAt, b.startedAt) || byId(a, b)
6378
+ )
6379
+ };
6380
+ break;
6381
+ case "notes":
6382
+ entry.parts = {
6383
+ ...parts,
6384
+ notes: upsert2(
6385
+ parts.notes,
6386
+ change.row,
6387
+ change.removedId,
6388
+ (a, b) => ascending(a.createdAt, b.createdAt) || byId(a, b)
6389
+ )
6390
+ };
6391
+ break;
6392
+ case "attendance":
6393
+ entry.parts = {
6394
+ ...parts,
6395
+ attendees: upsert2(
6396
+ parts.attendees,
6397
+ change.row,
6398
+ change.removedId,
6399
+ (a, b) => ascending(a.joinedAt, b.joinedAt) || byId(a, b)
6400
+ )
6401
+ };
6402
+ break;
6403
+ case "recording":
6404
+ entry.parts = {
6405
+ ...parts,
6406
+ // Newest first, as the read orders them.
6407
+ recordings: upsert2(
6408
+ parts.recordings,
6409
+ change.row,
6410
+ change.removedId,
6411
+ (a, b) => ascending(b.startedAt, a.startedAt) || byId(a, b)
6412
+ )
6413
+ };
6414
+ break;
6415
+ }
6416
+ assemble(entry);
6417
+ },
6418
+ hold(key, open, owner = null) {
6419
+ const entry = entryOf(key);
6420
+ if (entry.grace !== null) {
6421
+ clearTimeout(entry.grace);
6422
+ entry.grace = null;
6423
+ }
6424
+ if (entry.close !== null && entry.owner !== owner) {
6425
+ entry.close();
6426
+ entry.close = null;
6427
+ }
6428
+ if (entry.close === null) {
6429
+ entry.close = open();
6430
+ entry.owner = owner;
6431
+ }
6432
+ entry.holders += 1;
6433
+ let released = false;
6434
+ return () => {
6435
+ if (released) return;
6436
+ released = true;
6437
+ entry.holders -= 1;
6438
+ if (entry.holders > 0) return;
6439
+ entry.grace = setTimeout(() => {
6440
+ entry.grace = null;
6441
+ if (entry.holders > 0) return;
6442
+ entry.close?.();
6443
+ entry.close = null;
6444
+ entry.fresh = false;
6445
+ }, graceMs);
6446
+ };
6447
+ },
6448
+ reset() {
6449
+ for (const entry of entries.values()) {
6450
+ if (entry.grace !== null) clearTimeout(entry.grace);
6451
+ entry.close?.();
6452
+ }
6453
+ entries.clear();
6454
+ notify();
6455
+ }
6456
+ };
6457
+ }
6243
6458
  //# sourceMappingURL=index.cjs.map