@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/dist/react.js CHANGED
@@ -1901,13 +1901,13 @@ function mergeCaption(list, caption) {
1901
1901
  return list.map((entry, position) => position === index ? caption : entry);
1902
1902
  }
1903
1903
  function mergeRecord(list, record) {
1904
- const byId = list.findIndex((entry) => entry.source === "record" && entry.id === record.id);
1905
- if (byId !== -1) {
1906
- const existing = list[byId];
1904
+ const byId2 = list.findIndex((entry) => entry.source === "record" && entry.id === record.id);
1905
+ if (byId2 !== -1) {
1906
+ const existing = list[byId2];
1907
1907
  if (existing !== void 0 && existing.text === record.text && existing.final === record.final) {
1908
1908
  return list;
1909
1909
  }
1910
- return list.map((entry, position) => position === byId ? record : entry);
1910
+ return list.map((entry, position) => position === byId2 ? record : entry);
1911
1911
  }
1912
1912
  const captionIndex = list.findIndex((entry) => supersedes(record, entry));
1913
1913
  if (captionIndex === -1) return [...list, record];
@@ -5481,50 +5481,8 @@ function useLocalUserId() {
5481
5481
  }
5482
5482
 
5483
5483
  // src/react/hooks.ts
5484
- import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
5485
-
5486
- // src/core/layout.ts
5487
- var DEFAULT_PAGE_SIZE = 12;
5488
- function orderParticipants(participants, activeSpeakers) {
5489
- const speakerRank = /* @__PURE__ */ new Map();
5490
- activeSpeakers.forEach((identity, index) => speakerRank.set(identity, index));
5491
- return [...participants].sort((a, b) => {
5492
- const speakerA = speakerRank.get(a.identity) ?? Number.MAX_SAFE_INTEGER;
5493
- const speakerB = speakerRank.get(b.identity) ?? Number.MAX_SAFE_INTEGER;
5494
- if (speakerA !== speakerB) return speakerA - speakerB;
5495
- if (a.handRaised !== b.handRaised) return a.handRaised ? -1 : 1;
5496
- if (a.cameraEnabled !== b.cameraEnabled) return a.cameraEnabled ? -1 : 1;
5497
- if (a.joinedAt !== b.joinedAt) return a.joinedAt < b.joinedAt ? -1 : 1;
5498
- return a.identity < b.identity ? -1 : 1;
5499
- });
5500
- }
5501
- function computeStage(args) {
5502
- const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
5503
- const ordered = orderParticipants(args.participants, args.activeSpeakers);
5504
- const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
5505
- const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
5506
- const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
5507
- const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
5508
- const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
5509
- const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
5510
- const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
5511
- const tiles = rest.slice(page * pageSize, page * pageSize + pageSize);
5512
- return {
5513
- feature,
5514
- tiles,
5515
- overflow: rest.length - tiles.length,
5516
- page,
5517
- pageCount,
5518
- presenting: sharer !== null
5519
- };
5520
- }
5521
- function gridColumns(count2) {
5522
- if (count2 <= 1) return 1;
5523
- if (count2 <= 4) return 2;
5524
- if (count2 <= 9) return 3;
5525
- if (count2 <= 16) return 4;
5526
- return 5;
5527
- }
5484
+ import { useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2, useSyncExternalStore as useSyncExternalStore2 } from "react";
5485
+ import { useRealtimeManager as useRealtimeManager2 } from "@ai-matrx/realtime/react";
5528
5486
 
5529
5487
  // src/core/record.ts
5530
5488
  import { formatDurationMs } from "@ai-matrx/kit/format";
@@ -5548,6 +5506,9 @@ function restrictionFor(part, error) {
5548
5506
  };
5549
5507
  }
5550
5508
  async function loadMeetingRecord(repository, meetingId, meeting, reader = { userId: null }) {
5509
+ return assembleMeetingRecord(meeting, await loadMeetingRecordParts(repository, meetingId, reader));
5510
+ }
5511
+ async function loadMeetingRecordParts(repository, meetingId, reader = { userId: null }) {
5551
5512
  const restrictions = [];
5552
5513
  async function attempt(part, run, empty) {
5553
5514
  if (reader.userId === null) {
@@ -5575,6 +5536,10 @@ async function loadMeetingRecord(repository, meetingId, meeting, reader = { user
5575
5536
  []
5576
5537
  )
5577
5538
  ]);
5539
+ return { attendees, notes, transcript, recordings, restrictions };
5540
+ }
5541
+ function assembleMeetingRecord(meeting, parts) {
5542
+ const { attendees, notes, transcript, recordings, restrictions } = parts;
5578
5543
  return {
5579
5544
  meeting,
5580
5545
  attendees,
@@ -5920,6 +5885,253 @@ async function setGuestRecordAccess(args) {
5920
5885
  }
5921
5886
  var GUEST_RECORD_NOT_SHARED = NOT_SHARED;
5922
5887
 
5888
+ // src/core/record-cache.ts
5889
+ var MEETING_RECORD_GRACE_MS = 6e4;
5890
+ var INITIAL = { loading: true, record: null, failure: null };
5891
+ function upsert2(rows, row, removedId, order) {
5892
+ const id = row?.id ?? removedId;
5893
+ if (id === void 0) return rows;
5894
+ const rest = rows.filter((existing) => existing.id !== id);
5895
+ return row === null ? rest : [...rest, row].sort(order);
5896
+ }
5897
+ var byId = (a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
5898
+ var ascending = (a, b) => a === b ? 0 : a === null ? 1 : b === null ? -1 : a < b ? -1 : 1;
5899
+ function createMeetingRecordCache(options = {}) {
5900
+ const graceMs = options.graceMs ?? MEETING_RECORD_GRACE_MS;
5901
+ const entries = /* @__PURE__ */ new Map();
5902
+ const listeners = /* @__PURE__ */ new Set();
5903
+ const notify = () => {
5904
+ for (const listener of listeners) listener();
5905
+ };
5906
+ const entryOf = (key) => {
5907
+ let entry = entries.get(key);
5908
+ if (!entry) {
5909
+ entry = {
5910
+ state: INITIAL,
5911
+ meeting: null,
5912
+ parts: null,
5913
+ fresh: false,
5914
+ seq: 0,
5915
+ inflight: null,
5916
+ holders: 0,
5917
+ close: null,
5918
+ owner: null,
5919
+ grace: null
5920
+ };
5921
+ entries.set(key, entry);
5922
+ }
5923
+ return entry;
5924
+ };
5925
+ const assemble = (entry) => {
5926
+ if (entry.meeting === null || entry.parts === null) return;
5927
+ entry.state = { loading: false, failure: null, record: assembleMeetingRecord(entry.meeting, entry.parts) };
5928
+ notify();
5929
+ };
5930
+ return {
5931
+ state: (key) => key === null ? INITIAL : entries.get(key)?.state ?? INITIAL,
5932
+ subscribe(listener) {
5933
+ listeners.add(listener);
5934
+ return () => {
5935
+ listeners.delete(listener);
5936
+ };
5937
+ },
5938
+ ensure(key, read, { force = false } = {}) {
5939
+ const entry = entryOf(key);
5940
+ if (!force) {
5941
+ if (entry.inflight !== null) return entry.inflight;
5942
+ if (entry.fresh) return Promise.resolve();
5943
+ }
5944
+ const seq = ++entry.seq;
5945
+ if (entry.state.failure !== null) {
5946
+ entry.state = { ...entry.state, loading: true, failure: null };
5947
+ notify();
5948
+ }
5949
+ const request = read().then(
5950
+ (answer) => {
5951
+ if (seq !== entry.seq) return;
5952
+ entry.fresh = true;
5953
+ if (answer.kind === "parts") {
5954
+ entry.meeting = answer.meeting;
5955
+ entry.parts = answer.parts;
5956
+ assemble(entry);
5957
+ } else {
5958
+ entry.meeting = answer.bundle.meeting;
5959
+ entry.parts = null;
5960
+ entry.state = { loading: false, failure: null, record: answer.bundle };
5961
+ notify();
5962
+ }
5963
+ },
5964
+ (thrown) => {
5965
+ if (seq !== entry.seq) return;
5966
+ entry.fresh = false;
5967
+ entry.state = {
5968
+ loading: false,
5969
+ record: entry.state.record,
5970
+ failure: {
5971
+ message: thrown?.message ?? String(thrown),
5972
+ remedy: thrown?.remedy ?? "The meeting record could not be read. Reload the page; nothing was changed."
5973
+ }
5974
+ };
5975
+ notify();
5976
+ }
5977
+ );
5978
+ const tracked = request.finally(() => {
5979
+ if (entry.inflight === tracked) entry.inflight = null;
5980
+ });
5981
+ entry.inflight = tracked;
5982
+ return tracked;
5983
+ },
5984
+ setMeeting(key, meeting) {
5985
+ const entry = entries.get(key);
5986
+ if (!entry || entry.meeting === meeting) return;
5987
+ if (entry.parts !== null) {
5988
+ entry.meeting = meeting;
5989
+ assemble(entry);
5990
+ } else if (entry.state.record !== null) {
5991
+ entry.meeting = meeting;
5992
+ entry.state = { ...entry.state, record: { ...entry.state.record, meeting } };
5993
+ notify();
5994
+ }
5995
+ },
5996
+ apply(key, change) {
5997
+ const entry = entries.get(key);
5998
+ const parts = entry?.parts;
5999
+ if (!entry || !parts) return;
6000
+ const refused = parts.restrictions.some((r) => r.part === change.part);
6001
+ if (refused) return;
6002
+ switch (change.part) {
6003
+ case "transcript":
6004
+ entry.parts = {
6005
+ ...parts,
6006
+ transcript: upsert2(
6007
+ parts.transcript,
6008
+ change.row,
6009
+ change.removedId,
6010
+ (a, b) => ascending(a.startedAt, b.startedAt) || byId(a, b)
6011
+ )
6012
+ };
6013
+ break;
6014
+ case "notes":
6015
+ entry.parts = {
6016
+ ...parts,
6017
+ notes: upsert2(
6018
+ parts.notes,
6019
+ change.row,
6020
+ change.removedId,
6021
+ (a, b) => ascending(a.createdAt, b.createdAt) || byId(a, b)
6022
+ )
6023
+ };
6024
+ break;
6025
+ case "attendance":
6026
+ entry.parts = {
6027
+ ...parts,
6028
+ attendees: upsert2(
6029
+ parts.attendees,
6030
+ change.row,
6031
+ change.removedId,
6032
+ (a, b) => ascending(a.joinedAt, b.joinedAt) || byId(a, b)
6033
+ )
6034
+ };
6035
+ break;
6036
+ case "recording":
6037
+ entry.parts = {
6038
+ ...parts,
6039
+ // Newest first, as the read orders them.
6040
+ recordings: upsert2(
6041
+ parts.recordings,
6042
+ change.row,
6043
+ change.removedId,
6044
+ (a, b) => ascending(b.startedAt, a.startedAt) || byId(a, b)
6045
+ )
6046
+ };
6047
+ break;
6048
+ }
6049
+ assemble(entry);
6050
+ },
6051
+ hold(key, open, owner = null) {
6052
+ const entry = entryOf(key);
6053
+ if (entry.grace !== null) {
6054
+ clearTimeout(entry.grace);
6055
+ entry.grace = null;
6056
+ }
6057
+ if (entry.close !== null && entry.owner !== owner) {
6058
+ entry.close();
6059
+ entry.close = null;
6060
+ }
6061
+ if (entry.close === null) {
6062
+ entry.close = open();
6063
+ entry.owner = owner;
6064
+ }
6065
+ entry.holders += 1;
6066
+ let released = false;
6067
+ return () => {
6068
+ if (released) return;
6069
+ released = true;
6070
+ entry.holders -= 1;
6071
+ if (entry.holders > 0) return;
6072
+ entry.grace = setTimeout(() => {
6073
+ entry.grace = null;
6074
+ if (entry.holders > 0) return;
6075
+ entry.close?.();
6076
+ entry.close = null;
6077
+ entry.fresh = false;
6078
+ }, graceMs);
6079
+ };
6080
+ },
6081
+ reset() {
6082
+ for (const entry of entries.values()) {
6083
+ if (entry.grace !== null) clearTimeout(entry.grace);
6084
+ entry.close?.();
6085
+ }
6086
+ entries.clear();
6087
+ notify();
6088
+ }
6089
+ };
6090
+ }
6091
+
6092
+ // src/core/layout.ts
6093
+ var DEFAULT_PAGE_SIZE = 12;
6094
+ function orderParticipants(participants, activeSpeakers) {
6095
+ const speakerRank = /* @__PURE__ */ new Map();
6096
+ activeSpeakers.forEach((identity, index) => speakerRank.set(identity, index));
6097
+ return [...participants].sort((a, b) => {
6098
+ const speakerA = speakerRank.get(a.identity) ?? Number.MAX_SAFE_INTEGER;
6099
+ const speakerB = speakerRank.get(b.identity) ?? Number.MAX_SAFE_INTEGER;
6100
+ if (speakerA !== speakerB) return speakerA - speakerB;
6101
+ if (a.handRaised !== b.handRaised) return a.handRaised ? -1 : 1;
6102
+ if (a.cameraEnabled !== b.cameraEnabled) return a.cameraEnabled ? -1 : 1;
6103
+ if (a.joinedAt !== b.joinedAt) return a.joinedAt < b.joinedAt ? -1 : 1;
6104
+ return a.identity < b.identity ? -1 : 1;
6105
+ });
6106
+ }
6107
+ function computeStage(args) {
6108
+ const pageSize = Math.max(1, args.pageSize ?? DEFAULT_PAGE_SIZE);
6109
+ const ordered = orderParticipants(args.participants, args.activeSpeakers);
6110
+ const sharer = ordered.find((participant) => participant.screenSharing) ?? null;
6111
+ const pinnedParticipant = args.pinned === null ? null : ordered.find((participant) => participant.identity === args.pinned) ?? null;
6112
+ const spotlit = args.spotlight == null ? null : ordered.find((participant) => participant.identity === args.spotlight) ?? null;
6113
+ const feature = sharer !== null ? sharer : pinnedParticipant !== null && args.layout !== "grid" ? pinnedParticipant : spotlit !== null ? spotlit : args.layout === "grid" ? null : ordered[0] ?? null;
6114
+ const rest = feature === null ? ordered : ordered.filter((participant) => participant.identity !== feature.identity);
6115
+ const pageCount = Math.max(1, Math.ceil(rest.length / pageSize));
6116
+ const page = Math.min(Math.max(0, args.page ?? 0), pageCount - 1);
6117
+ const tiles = rest.slice(page * pageSize, page * pageSize + pageSize);
6118
+ return {
6119
+ feature,
6120
+ tiles,
6121
+ overflow: rest.length - tiles.length,
6122
+ page,
6123
+ pageCount,
6124
+ presenting: sharer !== null
6125
+ };
6126
+ }
6127
+ function gridColumns(count2) {
6128
+ if (count2 <= 1) return 1;
6129
+ if (count2 <= 4) return 2;
6130
+ if (count2 <= 9) return 3;
6131
+ if (count2 <= 16) return 4;
6132
+ return 5;
6133
+ }
6134
+
5923
6135
  // src/react/hooks.ts
5924
6136
  var EMPTY_NAMES = {};
5925
6137
  var NO_RUNTIME_REMEDY = "Render this inside a <MeetProvider> that has a Supabase client, an organization id and an identity (userId or guestName). The provider stays inert until all three are real, and while it is inert this package's components render nothing rather than a dead control.";
@@ -6505,16 +6717,22 @@ function useMeeting() {
6505
6717
  function useMeetingId() {
6506
6718
  return useMeetSnapshot()?.meeting?.id ?? null;
6507
6719
  }
6720
+ var RECORD_CACHE_KEY = /* @__PURE__ */ Symbol.for("@ai-matrx/meet/meeting-record-cache");
6721
+ function meetingRecordCache() {
6722
+ const holder = globalThis;
6723
+ let cache = holder[RECORD_CACHE_KEY];
6724
+ if (cache === void 0) {
6725
+ cache = createMeetingRecordCache();
6726
+ holder[RECORD_CACHE_KEY] = cache;
6727
+ }
6728
+ return cache;
6729
+ }
6730
+ var rowIdOf = (row) => typeof row["id"] === "string" ? row["id"] : void 0;
6508
6731
  function useMeetingRecord(meeting) {
6509
6732
  const host = useMeetHost();
6510
6733
  const repository = host?.repository ?? null;
6511
6734
  const ai = host?.ai ?? null;
6512
6735
  const readerId = host?.identity.userId ?? null;
6513
- const [recordReaderId, setRecordReaderId] = useState2(readerId);
6514
- const [record, setRecord] = useState2(null);
6515
- const [loading, setLoading] = useState2(true);
6516
- const [failure, setFailure] = useState2(null);
6517
- const [nonce, setNonce] = useState2(0);
6518
6736
  const [asking, setAsking] = useState2(false);
6519
6737
  const [answer, setAnswer] = useState2(null);
6520
6738
  const [coverage, setCoverage] = useState2(null);
@@ -6523,57 +6741,107 @@ function useMeetingRecord(meeting) {
6523
6741
  const api = host?.api ?? null;
6524
6742
  const tokens = host?.tokens ?? null;
6525
6743
  const roomName = meeting?.roomName ?? null;
6526
- useEffect2(() => {
6527
- if (repository === null || meeting === null || meetingId === null) return void 0;
6528
- let live = true;
6529
- setLoading(true);
6530
- setFailure(null);
6531
- setRecord(null);
6532
- const load = readerId === null && api !== null && tokens !== null && roomName !== null ? loadGuestMeetingRecord({
6533
- api,
6534
- repository,
6535
- meetingId,
6536
- meeting,
6537
- roomToken: tokens.remembered(roomName)
6538
- }) : loadMeetingRecord(repository, meetingId, meeting, { userId: readerId }).then(
6539
- (direct) => (
6540
- // A SIGNED-IN OUTSIDER IS NOT A STRANGER (2026-10-01). The database
6541
- // answers members and invitees; a person from another organization
6542
- // the host admitted — or a guest who later made an account — reads
6543
- // the guest share through the server, with their session as the pass.
6544
- api !== null && direct.restrictions.some((part) => part.part === "notes") ? loadGuestMeetingRecord({
6744
+ const manager = useRealtimeManager2();
6745
+ const cache = meetingRecordCache();
6746
+ const key = repository !== null && meetingId !== null ? `${readerId ?? `guest:${roomName ?? "-"}`}|${meetingId}` : null;
6747
+ const state = useSyncExternalStore2(
6748
+ cache.subscribe,
6749
+ () => cache.state(key),
6750
+ () => cache.state(null)
6751
+ );
6752
+ const readRef = useRef2(() => Promise.reject(new Error("not ready")));
6753
+ readRef.current = async () => {
6754
+ if (repository === null || meeting === null || meetingId === null) throw new Error("No meeting to read.");
6755
+ if (readerId === null && api !== null && tokens !== null && roomName !== null) {
6756
+ return {
6757
+ kind: "bundle",
6758
+ bundle: await loadGuestMeetingRecord({
6545
6759
  api,
6546
6760
  repository,
6547
6761
  meetingId,
6548
6762
  meeting,
6549
- roomToken: null,
6550
- session: true
6551
- }).then(
6552
- (viaServer) => viaServer.restrictions.length < direct.restrictions.length ? viaServer : direct
6553
- ) : direct
6554
- )
6555
- );
6556
- void load.then((bundle) => {
6557
- if (!live) return;
6558
- setRecordReaderId(readerId);
6559
- setRecord(bundle);
6560
- setLoading(false);
6561
- }).catch((thrown) => {
6562
- if (!live) return;
6563
- setFailure({
6564
- message: thrown.message,
6565
- remedy: thrown.remedy ?? "The meeting record could not be read. Reload the page; nothing was changed."
6763
+ roomToken: tokens.remembered(roomName)
6764
+ })
6765
+ };
6766
+ }
6767
+ const parts = await loadMeetingRecordParts(repository, meetingId, { userId: readerId });
6768
+ if (api !== null && parts.restrictions.some((part) => part.part === "notes")) {
6769
+ const viaServer = await loadGuestMeetingRecord({
6770
+ api,
6771
+ repository,
6772
+ meetingId,
6773
+ meeting,
6774
+ roomToken: null,
6775
+ session: true
6566
6776
  });
6567
- setLoading(false);
6777
+ if (viaServer.restrictions.length < parts.restrictions.length) return { kind: "bundle", bundle: viaServer };
6778
+ }
6779
+ return { kind: "parts", meeting, parts };
6780
+ };
6781
+ useEffect2(() => {
6782
+ if (key === null) return;
6783
+ void cache.ensure(key, () => readRef.current());
6784
+ }, [cache, key]);
6785
+ useEffect2(() => {
6786
+ if (key !== null && meeting !== null) cache.setMeeting(key, meeting);
6787
+ }, [cache, key, meeting]);
6788
+ useEffect2(() => {
6789
+ if (key === null || manager === null || repository === null || meetingId === null || readerId === null) {
6790
+ return void 0;
6791
+ }
6792
+ const repo = repository;
6793
+ const binding = (table, apply) => ({
6794
+ event: "*",
6795
+ schema: repo.schema,
6796
+ table,
6797
+ filter: `meeting_id=eq.${meetingId}`,
6798
+ rowId: rowIdOf,
6799
+ onChange: ({ row, payload }) => {
6800
+ if (row === null) return;
6801
+ const gone = payload.eventType === "DELETE";
6802
+ apply(gone ? null : row, gone ? rowIdOf(row) : void 0);
6803
+ }
6568
6804
  });
6569
- return () => {
6570
- live = false;
6571
- };
6572
- }, [repository, meeting, meetingId, readerId, api, tokens, roomName, nonce]);
6805
+ return cache.hold(
6806
+ key,
6807
+ () => {
6808
+ const handle = manager.open({
6809
+ topic: meetingTopic(meetingId),
6810
+ postgresChanges: [
6811
+ binding(
6812
+ repo.tables.notes,
6813
+ (row, removedId) => cache.apply(key, { part: "notes", row: row && repo.projectNote(row), removedId })
6814
+ ),
6815
+ binding(
6816
+ repo.tables.transcripts,
6817
+ (row, removedId) => cache.apply(key, { part: "transcript", row: row && repo.projectSegment(row), removedId })
6818
+ ),
6819
+ binding(
6820
+ repo.tables.participants,
6821
+ (row, removedId) => cache.apply(key, { part: "attendance", row: row && repo.projectAttendee(row), removedId })
6822
+ ),
6823
+ binding(
6824
+ repo.tables.recordings,
6825
+ (row, removedId) => (
6826
+ // An archived recording is off the record (the read filters it the same way).
6827
+ row !== null && row["deleted_at"] != null ? cache.apply(key, { part: "recording", row: null, removedId: rowIdOf(row) }) : cache.apply(key, { part: "recording", row: row && repo.projectRecording(row), removedId })
6828
+ )
6829
+ )
6830
+ ],
6831
+ // Realtime has no replay: a socket that blipped reads the record again.
6832
+ onBackfill: async () => {
6833
+ await cache.ensure(key, () => readRef.current(), { force: true });
6834
+ }
6835
+ });
6836
+ return () => handle.close();
6837
+ },
6838
+ manager
6839
+ );
6840
+ }, [cache, key, manager, repository, meetingId, readerId]);
6573
6841
  return {
6574
- loading: loading && failure === null,
6575
- record: recordReaderId === readerId ? record : null,
6576
- failure,
6842
+ loading: state.loading && state.failure === null,
6843
+ record: state.record,
6844
+ failure: state.failure,
6577
6845
  asking,
6578
6846
  answer,
6579
6847
  coverage,
@@ -6604,7 +6872,9 @@ function useMeetingRecord(meeting) {
6604
6872
  },
6605
6873
  [ai, meetingId]
6606
6874
  ),
6607
- reload: useCallback2(() => setNonce((value) => value + 1), [])
6875
+ reload: useCallback2(() => {
6876
+ if (key !== null) void cache.ensure(key, () => readRef.current(), { force: true });
6877
+ }, [cache, key])
6608
6878
  };
6609
6879
  }
6610
6880
  function useMeetings(limit = 25, organizationFilter = null) {
@@ -6709,6 +6979,7 @@ import {
6709
6979
  import { avatarPaletteIndex, formatDurationMs as formatDurationMs4, getInitials } from "@ai-matrx/kit/format";
6710
6980
 
6711
6981
  // src/core/after.ts
6982
+ import { toDelimited } from "@ai-matrx/kit/delimited";
6712
6983
  import { formatDurationMs as formatDurationMs2 } from "@ai-matrx/kit/format";
6713
6984
  function kindOf(person) {
6714
6985
  if (person.isAgent) return "assistant";
@@ -6847,11 +7118,6 @@ function attendanceTotals(rows) {
6847
7118
  averageMs: timed.length === 0 ? null : Math.round(timed.reduce((sum, row) => sum + (row.durationMs ?? 0), 0) / timed.length)
6848
7119
  };
6849
7120
  }
6850
- function csvCell(value) {
6851
- if (value === null) return "";
6852
- const text3 = String(value);
6853
- return /[",\n\r]/.test(text3) ? `"${text3.replace(/"/g, '""')}"` : text3;
6854
- }
6855
7121
  var ATTENDANCE_CSV_HEADER = [
6856
7122
  "Meeting",
6857
7123
  "Meeting started",
@@ -6863,11 +7129,11 @@ var ATTENDANCE_CSV_HEADER = [
6863
7129
  "Admission"
6864
7130
  ];
6865
7131
  function attendanceCsv(reports) {
6866
- const lines = [ATTENDANCE_CSV_HEADER.join(",")];
6867
- for (const { meeting, rows } of reports) {
6868
- for (const row of rows) {
6869
- lines.push(
6870
- [
7132
+ return toDelimited(
7133
+ [
7134
+ ATTENDANCE_CSV_HEADER,
7135
+ ...reports.flatMap(
7136
+ ({ meeting, rows }) => rows.map((row) => [
6871
7137
  meeting.title,
6872
7138
  meeting.startedAt ?? meeting.scheduledFor,
6873
7139
  row.name,
@@ -6876,12 +7142,11 @@ function attendanceCsv(reports) {
6876
7142
  row.leftAt,
6877
7143
  row.durationMs === null ? null : Math.round(row.durationMs / 6e3) / 10,
6878
7144
  row.admission
6879
- ].map(csvCell).join(",")
6880
- );
6881
- }
6882
- }
6883
- return `${lines.join("\r\n")}\r
6884
- `;
7145
+ ])
7146
+ )
7147
+ ],
7148
+ { lineEnding: "\r\n", trailingNewline: true }
7149
+ );
6885
7150
  }
6886
7151
  function recordingOffsetMs(at, recording) {
6887
7152
  if (recording === null || recording.state !== "available") return null;
@@ -11051,6 +11316,7 @@ export {
11051
11316
  LockIcon,
11052
11317
  MEETING_INVITE_KIND,
11053
11318
  MEETING_INVITE_VERSION,
11319
+ MEETING_RECORD_GRACE_MS,
11054
11320
  MEET_ANSWER_CONTEXT_EVENT,
11055
11321
  MEET_ANSWER_EVENTS,
11056
11322
  MEET_EVENTS,
@@ -11128,6 +11394,7 @@ export {
11128
11394
  asParticipantIdentity,
11129
11395
  asRoomName,
11130
11396
  asUserId,
11397
+ assembleMeetingRecord,
11131
11398
  assignedRoom,
11132
11399
  attendanceCsv,
11133
11400
  attendanceReport,
@@ -11157,6 +11424,7 @@ export {
11157
11424
  createMeetRepository,
11158
11425
  createMeetStore,
11159
11426
  createMeetingInviteHandler,
11427
+ createMeetingRecordCache,
11160
11428
  createMeetingSession,
11161
11429
  createMemoryDeviceStorage,
11162
11430
  createRecordingController,
@@ -11191,12 +11459,14 @@ export {
11191
11459
  loadMeetingActivities,
11192
11460
  loadMeetingChat,
11193
11461
  loadMeetingRecord,
11462
+ loadMeetingRecordParts,
11194
11463
  mapConnectionState,
11195
11464
  mapQuality,
11196
11465
  meetingInviteAction,
11197
11466
  meetingLength,
11198
11467
  meetingLink,
11199
11468
  meetingPath,
11469
+ meetingRecordCache,
11200
11470
  meetingSlug,
11201
11471
  meetingTopic,
11202
11472
  meetingWhen,