@ai-matrx/meet 0.7.78 → 0.7.80

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,32 @@
1
1
  # Changelog — `@ai-matrx/meet`
2
2
 
3
+ ## 0.7.80
4
+
5
+ - `attendanceCsv` writes through THE one writer, `@ai-matrx/kit/delimited` (AP-2): CRLF records and trailing CRLF unchanged; a name that opens like a formula gets a leading `'`.
6
+
7
+ **Consumer action:** none.
8
+
9
+ ## 0.7.79
10
+
11
+ **A meeting's record is read once per tab, and kept live.** `useMeetingRecord` held the bundle
12
+ in its own state and read all four parts (`meet_participants`, `meet_notes`,
13
+ `meet_transcript_segments`, `meet_recordings`) on every mount — so a board tile that slept
14
+ and woke (React `<Activity>` re-runs every effect), a Remove + Undo, and every second view of
15
+ the same meeting read the whole record again. The record now lives in one per-tab cache
16
+ (`core/record-cache.ts`, keyed by reader + meeting): the first view reads, later views and
17
+ remounts render the kept record and read nothing; a read in flight is joined; Retry and a
18
+ reconnect read again, and an older answer never paints over a newer one. A member's record is
19
+ kept current by ONE change-feed channel per meeting (notes, transcript, attendance,
20
+ recordings — applied row by row, no re-read), held while any view is mounted and 60 s after;
21
+ past that the record is marked stale and the next view reads it once. A meeting rename
22
+ re-assembles the kept record instead of re-reading it. New exports: `loadMeetingRecordParts`,
23
+ `assembleMeetingRecord`, `createMeetingRecordCache`, `MEETING_RECORD_GRACE_MS`, and
24
+ `meetingRecordCache()` from `/react`. Guard: `react/meeting-record-reads-once.test.tsx`
25
+ (4/4 red on 0.7.78).
26
+
27
+ Consumer action: none. A host test that renders the record for the same meeting twice and
28
+ expects two reads resets the cache between cases (`meetingRecordCache().reset()`).
29
+
3
30
  ## 0.7.78
4
31
 
5
32
  **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];
@@ -4547,6 +4551,7 @@ function fileDownloadPath(fileId) {
4547
4551
  }
4548
4552
 
4549
4553
  // src/core/after.ts
4554
+ var import_delimited = require("@ai-matrx/kit/delimited");
4550
4555
  var import_format = require("@ai-matrx/kit/format");
4551
4556
  function kindOf(person) {
4552
4557
  if (person.isAgent) return "assistant";
@@ -4685,11 +4690,6 @@ function attendanceTotals(rows) {
4685
4690
  averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
4686
4691
  };
4687
4692
  }
4688
- function csvCell(value) {
4689
- if (value === null) return "";
4690
- const text3 = String(value);
4691
- return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
4692
- }
4693
4693
  var ATTENDANCE_CSV_HEADER = [
4694
4694
  "Meeting",
4695
4695
  "Meeting started",
@@ -4701,11 +4701,11 @@ var ATTENDANCE_CSV_HEADER = [
4701
4701
  "Admission"
4702
4702
  ];
4703
4703
  function attendanceCsv(reports) {
4704
- const lines = [ATTENDANCE_CSV_HEADER.join(",")];
4705
- for (const { meeting, rows } of reports) {
4706
- for (const row of rows) {
4707
- lines.push(
4708
- [
4704
+ return (0, import_delimited.toDelimited)(
4705
+ [
4706
+ ATTENDANCE_CSV_HEADER,
4707
+ ...reports.flatMap(
4708
+ ({ meeting, rows }) => rows.map((row) => [
4709
4709
  meeting.title,
4710
4710
  meeting.startedAt ?? meeting.scheduledFor,
4711
4711
  row.name,
@@ -4714,12 +4714,11 @@ function attendanceCsv(reports) {
4714
4714
  row.leftAt,
4715
4715
  row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
4716
4716
  row.admission
4717
- ].map(csvCell).join(",")
4718
- );
4719
- }
4720
- }
4721
- return `${lines.join("\r\n")}\r
4722
- `;
4717
+ ])
4718
+ )
4719
+ ],
4720
+ { lineEnding: "\r\n", trailingNewline: true }
4721
+ );
4723
4722
  }
4724
4723
  function recordingOffsetMs(at, recording) {
4725
4724
  if (recording === null || recording.state !== "available") return null;
@@ -5869,6 +5868,9 @@ function restrictionFor(part, error) {
5869
5868
  };
5870
5869
  }
5871
5870
  async function loadMeetingRecord(repository, meetingId, meeting, reader = { userId: null }) {
5871
+ return assembleMeetingRecord(meeting, await loadMeetingRecordParts(repository, meetingId, reader));
5872
+ }
5873
+ async function loadMeetingRecordParts(repository, meetingId, reader = { userId: null }) {
5872
5874
  const restrictions = [];
5873
5875
  async function attempt(part, run, empty) {
5874
5876
  if (reader.userId === null) {
@@ -5896,6 +5898,10 @@ async function loadMeetingRecord(repository, meetingId, meeting, reader = { user
5896
5898
  []
5897
5899
  )
5898
5900
  ]);
5901
+ return { attendees, notes, transcript, recordings, restrictions };
5902
+ }
5903
+ function assembleMeetingRecord(meeting, parts) {
5904
+ const { attendees, notes, transcript, recordings, restrictions } = parts;
5899
5905
  return {
5900
5906
  meeting,
5901
5907
  attendees,
@@ -6240,4 +6246,208 @@ async function setGuestRecordAccess(args) {
6240
6246
  };
6241
6247
  }
6242
6248
  var GUEST_RECORD_NOT_SHARED = NOT_SHARED;
6249
+
6250
+ // src/core/record-cache.ts
6251
+ var MEETING_RECORD_GRACE_MS = 6e4;
6252
+ var INITIAL = { loading: true, record: null, failure: null };
6253
+ function upsert2(rows, row, removedId, order) {
6254
+ const id = row?.id ?? removedId;
6255
+ if (id === void 0) return rows;
6256
+ const rest = rows.filter((existing) => existing.id !== id);
6257
+ return row === null ? rest : [...rest, row].sort(order);
6258
+ }
6259
+ var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
6260
+ var ascending = (a, b) => a === b ? 0 : a === null ? 1 : b === null ? -1 : a < b ? -1 : 1;
6261
+ function createMeetingRecordCache(options = {}) {
6262
+ const graceMs = options.graceMs ?? MEETING_RECORD_GRACE_MS;
6263
+ const entries = /* @__PURE__ */ new Map();
6264
+ const listeners = /* @__PURE__ */ new Set();
6265
+ const notify = () => {
6266
+ for (const listener of listeners) listener();
6267
+ };
6268
+ const entryOf = (key) => {
6269
+ let entry = entries.get(key);
6270
+ if (!entry) {
6271
+ entry = {
6272
+ state: INITIAL,
6273
+ meeting: null,
6274
+ parts: null,
6275
+ fresh: false,
6276
+ seq: 0,
6277
+ inflight: null,
6278
+ holders: 0,
6279
+ close: null,
6280
+ owner: null,
6281
+ grace: null
6282
+ };
6283
+ entries.set(key, entry);
6284
+ }
6285
+ return entry;
6286
+ };
6287
+ const assemble = (entry) => {
6288
+ if (entry.meeting === null || entry.parts === null) return;
6289
+ entry.state = { loading: false, failure: null, record: assembleMeetingRecord(entry.meeting, entry.parts) };
6290
+ notify();
6291
+ };
6292
+ return {
6293
+ state: (key) => key === null ? INITIAL : entries.get(key)?.state ?? INITIAL,
6294
+ subscribe(listener) {
6295
+ listeners.add(listener);
6296
+ return () => {
6297
+ listeners.delete(listener);
6298
+ };
6299
+ },
6300
+ ensure(key, read, { force = false } = {}) {
6301
+ const entry = entryOf(key);
6302
+ if (!force) {
6303
+ if (entry.inflight !== null) return entry.inflight;
6304
+ if (entry.fresh) return Promise.resolve();
6305
+ }
6306
+ const seq = ++entry.seq;
6307
+ if (entry.state.failure !== null) {
6308
+ entry.state = { ...entry.state, loading: true, failure: null };
6309
+ notify();
6310
+ }
6311
+ const request = read().then(
6312
+ (answer) => {
6313
+ if (seq !== entry.seq) return;
6314
+ entry.fresh = true;
6315
+ if (answer.kind === "parts") {
6316
+ entry.meeting = answer.meeting;
6317
+ entry.parts = answer.parts;
6318
+ assemble(entry);
6319
+ } else {
6320
+ entry.meeting = answer.bundle.meeting;
6321
+ entry.parts = null;
6322
+ entry.state = { loading: false, failure: null, record: answer.bundle };
6323
+ notify();
6324
+ }
6325
+ },
6326
+ (thrown) => {
6327
+ if (seq !== entry.seq) return;
6328
+ entry.fresh = false;
6329
+ entry.state = {
6330
+ loading: false,
6331
+ record: entry.state.record,
6332
+ failure: {
6333
+ message: thrown?.message ?? String(thrown),
6334
+ remedy: thrown?.remedy ?? "The meeting record could not be read. Reload the page; nothing was changed."
6335
+ }
6336
+ };
6337
+ notify();
6338
+ }
6339
+ );
6340
+ const tracked = request.finally(() => {
6341
+ if (entry.inflight === tracked) entry.inflight = null;
6342
+ });
6343
+ entry.inflight = tracked;
6344
+ return tracked;
6345
+ },
6346
+ setMeeting(key, meeting) {
6347
+ const entry = entries.get(key);
6348
+ if (!entry || entry.meeting === meeting) return;
6349
+ if (entry.parts !== null) {
6350
+ entry.meeting = meeting;
6351
+ assemble(entry);
6352
+ } else if (entry.state.record !== null) {
6353
+ entry.meeting = meeting;
6354
+ entry.state = { ...entry.state, record: { ...entry.state.record, meeting } };
6355
+ notify();
6356
+ }
6357
+ },
6358
+ apply(key, change) {
6359
+ const entry = entries.get(key);
6360
+ const parts = entry?.parts;
6361
+ if (!entry || !parts) return;
6362
+ const refused = parts.restrictions.some((r) => r.part === change.part);
6363
+ if (refused) return;
6364
+ switch (change.part) {
6365
+ case "transcript":
6366
+ entry.parts = {
6367
+ ...parts,
6368
+ transcript: upsert2(
6369
+ parts.transcript,
6370
+ change.row,
6371
+ change.removedId,
6372
+ (a, b) => ascending(a.startedAt, b.startedAt) || byId(a, b)
6373
+ )
6374
+ };
6375
+ break;
6376
+ case "notes":
6377
+ entry.parts = {
6378
+ ...parts,
6379
+ notes: upsert2(
6380
+ parts.notes,
6381
+ change.row,
6382
+ change.removedId,
6383
+ (a, b) => ascending(a.createdAt, b.createdAt) || byId(a, b)
6384
+ )
6385
+ };
6386
+ break;
6387
+ case "attendance":
6388
+ entry.parts = {
6389
+ ...parts,
6390
+ attendees: upsert2(
6391
+ parts.attendees,
6392
+ change.row,
6393
+ change.removedId,
6394
+ (a, b) => ascending(a.joinedAt, b.joinedAt) || byId(a, b)
6395
+ )
6396
+ };
6397
+ break;
6398
+ case "recording":
6399
+ entry.parts = {
6400
+ ...parts,
6401
+ // Newest first, as the read orders them.
6402
+ recordings: upsert2(
6403
+ parts.recordings,
6404
+ change.row,
6405
+ change.removedId,
6406
+ (a, b) => ascending(b.startedAt, a.startedAt) || byId(a, b)
6407
+ )
6408
+ };
6409
+ break;
6410
+ }
6411
+ assemble(entry);
6412
+ },
6413
+ hold(key, open, owner = null) {
6414
+ const entry = entryOf(key);
6415
+ if (entry.grace !== null) {
6416
+ clearTimeout(entry.grace);
6417
+ entry.grace = null;
6418
+ }
6419
+ if (entry.close !== null && entry.owner !== owner) {
6420
+ entry.close();
6421
+ entry.close = null;
6422
+ }
6423
+ if (entry.close === null) {
6424
+ entry.close = open();
6425
+ entry.owner = owner;
6426
+ }
6427
+ entry.holders += 1;
6428
+ let released = false;
6429
+ return () => {
6430
+ if (released) return;
6431
+ released = true;
6432
+ entry.holders -= 1;
6433
+ if (entry.holders > 0) return;
6434
+ entry.grace = setTimeout(() => {
6435
+ entry.grace = null;
6436
+ if (entry.holders > 0) return;
6437
+ entry.close?.();
6438
+ entry.close = null;
6439
+ entry.fresh = false;
6440
+ }, graceMs);
6441
+ };
6442
+ },
6443
+ reset() {
6444
+ for (const entry of entries.values()) {
6445
+ if (entry.grace !== null) clearTimeout(entry.grace);
6446
+ entry.close?.();
6447
+ }
6448
+ entries.clear();
6449
+ notify();
6450
+ }
6451
+ };
6452
+ }
6243
6453
  //# sourceMappingURL=index.cjs.map