@ai-matrx/associations 0.13.137 → 0.13.138

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,18 @@
1
1
  # Changelog — @ai-matrx/associations
2
2
 
3
+
4
+ ## 0.13.138
5
+
6
+ **A conversation's file inventory is read once per tab.** `useContainerLinks` kept the
7
+ `conversation_files` answer in its own state, so a chat tile that slept and woke, a Remove +
8
+ Undo and every second view of the same conversation read it again. It now lives in one cache
9
+ entry per conversation per store: the first view reads, later views and remounts render the
10
+ kept list, a read in flight is joined, a store write to the conversation still re-reads it,
11
+ and an older answer never lands over a newer one. Guard: `react/__tests__/hooks.test.tsx`
12
+ "reads a conversation's files once for a remount and a second view" (red on 0.13.137).
13
+
14
+ Consumer action: none.
15
+
3
16
  ## Unreleased
4
17
 
5
18
  **Resource groups are a dot and a name.** `AssociationCardGrid` no longer renders the one-line group descriptions ("Incoming sources of truth (Knowledge In).", "Filed under"'s line). Everything else of the restored tiles look stays. Arman 2026-09-30: descriptions are banned on in-app pages (`common-docs/policies/interface-text-is-layout.md`). `CONTENT_ROLES[].tagline` stays in the data for now. No consumer action required.
@@ -1823,6 +1823,66 @@ function metadataRecord(metadata) {
1823
1823
  }
1824
1824
  return Object.fromEntries(Object.entries(metadata));
1825
1825
  }
1826
+ var conversationFileCaches = /* @__PURE__ */ new WeakMap();
1827
+ function conversationFilesFor(store) {
1828
+ const existing = conversationFileCaches.get(store);
1829
+ if (existing) return existing;
1830
+ const entries = /* @__PURE__ */ new Map();
1831
+ const inflight = /* @__PURE__ */ new Map();
1832
+ const seqs = /* @__PURE__ */ new Map();
1833
+ const listeners = /* @__PURE__ */ new Set();
1834
+ const set = (entry) => {
1835
+ entries.set(entry.key, entry);
1836
+ for (const listener of listeners) listener();
1837
+ };
1838
+ const cache = {
1839
+ get: (key) => entries.get(key) ?? null,
1840
+ subscribe(listener) {
1841
+ listeners.add(listener);
1842
+ return () => {
1843
+ listeners.delete(listener);
1844
+ };
1845
+ },
1846
+ load(key, conversationId, force) {
1847
+ const kept = entries.get(key);
1848
+ if (!force) {
1849
+ const running = inflight.get(key);
1850
+ if (running) return running;
1851
+ if (kept && !kept.loading && kept.error === null) return Promise.resolve();
1852
+ }
1853
+ const seq = (seqs.get(key) ?? 0) + 1;
1854
+ seqs.set(key, seq);
1855
+ set({ key, files: kept?.files ?? [], error: kept?.error ?? null, loading: true });
1856
+ const request = store.services.associations.listConversationFiles(conversationId).then((result) => {
1857
+ if (seqs.get(key) !== seq) return;
1858
+ if (isAssociationsRpcErr(result)) {
1859
+ set({ key, files: entries.get(key)?.files ?? [], error: result.error.message, loading: false });
1860
+ return;
1861
+ }
1862
+ set({
1863
+ key,
1864
+ files: result.data.files.map((file) => ({
1865
+ edgeId: `conversation-file:${file.fileId}`,
1866
+ resourceId: file.fileId,
1867
+ token: "file",
1868
+ role: null,
1869
+ label: file.label,
1870
+ metadata: file.metadata
1871
+ })),
1872
+ error: null,
1873
+ loading: false
1874
+ });
1875
+ });
1876
+ const tracked = request.finally(() => {
1877
+ if (inflight.get(key) === tracked) inflight.delete(key);
1878
+ });
1879
+ inflight.set(key, tracked);
1880
+ return tracked;
1881
+ }
1882
+ };
1883
+ conversationFileCaches.set(store, cache);
1884
+ return cache;
1885
+ }
1826
1886
  function useContainerLinks(args) {
1827
1887
  const { containerType, containerId, orgId } = args;
1828
1888
  const outgoing = args.direction === "outgoing";
@@ -1838,58 +1898,21 @@ function useContainerLinks(args) {
1838
1898
  id: containerId
1839
1899
  });
1840
1900
  const conversationKey = !outgoing && containerType === "conversation" && containerId ? `conversation:${containerId}` : null;
1841
- const activeConversationKey = (0, import_react3.useRef)(conversationKey);
1842
- const conversationGeneration = (0, import_react3.useRef)(0);
1843
- const [conversationResult, setConversationResult] = (0, import_react3.useState)(null);
1844
- (0, import_react3.useLayoutEffect)(() => {
1845
- activeConversationKey.current = conversationKey;
1846
- conversationGeneration.current += 1;
1847
- }, [conversationKey]);
1848
- const loadConversationFiles = (0, import_react3.useCallback)(async () => {
1849
- const key = conversationKey;
1850
- if (!key || !containerId) return;
1851
- if (activeConversationKey.current !== key) return;
1852
- const generation = ++conversationGeneration.current;
1853
- setConversationResult((current) => ({
1854
- key,
1855
- files: current?.key === key ? current.files : [],
1856
- // Keep the visible failure context while retrying so the same control
1857
- // can disable itself and show progress instead of disappearing.
1858
- error: current?.key === key ? current.error : null,
1859
- loading: true
1860
- }));
1861
- const result = await store.services.associations.listConversationFiles(containerId);
1862
- if (generation !== conversationGeneration.current || activeConversationKey.current !== key) {
1863
- return;
1864
- }
1865
- if (isAssociationsRpcErr(result)) {
1866
- setConversationResult((current) => ({
1867
- key,
1868
- files: current?.key === key ? current.files : [],
1869
- error: result.error.message,
1870
- loading: false
1871
- }));
1872
- return;
1873
- }
1874
- setConversationResult({
1875
- key,
1876
- files: result.data.files.map((file) => ({
1877
- edgeId: `conversation-file:${file.fileId}`,
1878
- resourceId: file.fileId,
1879
- token: "file",
1880
- role: null,
1881
- label: file.label,
1882
- metadata: file.metadata
1883
- })),
1884
- error: null,
1885
- loading: false
1886
- });
1887
- }, [containerId, conversationKey, store]);
1901
+ const files = conversationFilesFor(store);
1902
+ const conversationResult = (0, import_react3.useSyncExternalStore)(
1903
+ files.subscribe,
1904
+ () => conversationKey ? files.get(conversationKey) : null,
1905
+ () => null
1906
+ );
1907
+ const loadConversationFiles = (0, import_react3.useCallback)(
1908
+ async (force = true) => {
1909
+ if (!conversationKey || !containerId) return;
1910
+ await files.load(conversationKey, containerId, force);
1911
+ },
1912
+ [containerId, conversationKey, files]
1913
+ );
1888
1914
  (0, import_react3.useEffect)(() => {
1889
- const timeout = setTimeout(() => {
1890
- void loadConversationFiles();
1891
- }, 0);
1892
- return () => clearTimeout(timeout);
1915
+ void loadConversationFiles(false);
1893
1916
  }, [loadConversationFiles]);
1894
1917
  const lastGenericFetch = (0, import_react3.useRef)({
1895
1918
  key: conversationKey,