@asteby/metacore-runtime-react 22.0.0 → 23.0.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.
@@ -4,10 +4,30 @@
4
4
  // per-field option loader + result cache lives here to guarantee they facet
5
5
  // identically (one board and its table sibling hit the same `/facets` endpoint
6
6
  // with the same caching).
7
- import { useCallback, useEffect, useRef } from 'react'
7
+ import { useCallback, useEffect, useRef, useState } from 'react'
8
8
  import { useApi } from './api-context'
9
9
  import type { FilterOption } from './dynamic-columns-shim'
10
10
 
11
+ export interface UseFacetLoadersResult {
12
+ /**
13
+ * Stable per-field loader → `<facetsBase>?field=&q=&limit=`. Cached per
14
+ * `field+query`. Null `facetsBase` → yields `undefined` (column stays text).
15
+ */
16
+ getFacetLoader: (
17
+ field: string,
18
+ ) => ((q?: string) => Promise<FilterOption[]>) | undefined
19
+ /**
20
+ * Warms the caches for a set of facet fields in ONE parallel burst (called
21
+ * when metadata resolves), so a popover opens instantly with values + counts
22
+ * instead of showing "Cargando…". Deduped by the field set; a field whose
23
+ * request fails is simply omitted (it degrades to lazy/text — the rest are
24
+ * unaffected). Resolved options also land in `facetOptions`.
25
+ */
26
+ prefetchFacets: (fields: string[]) => void
27
+ /** Prefetched options per field (empty until `prefetchFacets` resolves). */
28
+ facetOptions: Map<string, FilterOption[]>
29
+ }
30
+
11
31
  /**
12
32
  * Long-text / body columns aren't worth faceting (thousands of unique values,
13
33
  * each a paragraph). Heuristic on the metadata, not a hardcoded name list:
@@ -29,26 +49,39 @@ export function isLongTextColumn(c: {
29
49
  }
30
50
 
31
51
  /**
32
- * Returns `getFacetLoader(field)` a stable per-field loader that resolves the
33
- * column's distinct values + counts from `<facetsBase>?field=&q=&limit=`. Loader
34
- * identity is memoized per field (so it doesn't churn the config memo that
35
- * depends on it) and results are cached per `field+query`. Both caches reset
36
- * when `facetsBase` changes (model switch). A null `facetsBase` returns a
37
- * factory that yields `undefined` — the caller keeps the column as plain text.
52
+ * Facet option machinery: a stable per-field lazy loader (`getFacetLoader`, with
53
+ * a `field+query` result cache) plus `prefetchFacets` to warm every facet
54
+ * field's values up front and `facetOptions` holding those prewarmed results.
55
+ * Loader identity is memoized per field (so it doesn't churn the config memo
56
+ * that depends on it). Caches reset when `facetsBase` changes (model switch).
38
57
  */
39
- export function useFacetLoaders(facetsBase: string | null) {
58
+ export function useFacetLoaders(facetsBase: string | null): UseFacetLoadersResult {
40
59
  const api = useApi()
41
60
  const loaderCache = useRef<
42
61
  Map<string, (q?: string) => Promise<FilterOption[]>>
43
62
  >(new Map())
44
63
  const resultCache = useRef<Map<string, FilterOption[]>>(new Map())
64
+ const prefetchSig = useRef<string | null>(null)
65
+ const mountedRef = useRef(true)
66
+ const [facetOptions, setFacetOptions] = useState<Map<string, FilterOption[]>>(
67
+ new Map(),
68
+ )
69
+
70
+ useEffect(() => {
71
+ mountedRef.current = true
72
+ return () => {
73
+ mountedRef.current = false
74
+ }
75
+ }, [])
45
76
 
46
77
  useEffect(() => {
47
78
  loaderCache.current.clear()
48
79
  resultCache.current.clear()
80
+ prefetchSig.current = null
81
+ setFacetOptions(new Map())
49
82
  }, [facetsBase])
50
83
 
51
- return useCallback(
84
+ const getFacetLoader = useCallback(
52
85
  (field: string) => {
53
86
  if (!facetsBase) return undefined
54
87
  const existing = loaderCache.current.get(field)
@@ -76,4 +109,38 @@ export function useFacetLoaders(facetsBase: string | null) {
76
109
  },
77
110
  [api, facetsBase],
78
111
  )
112
+
113
+ const prefetchFacets = useCallback(
114
+ (fields: string[]) => {
115
+ if (!facetsBase || fields.length === 0) return
116
+ const sig = [...fields].sort().join('|')
117
+ if (prefetchSig.current === sig) return
118
+ prefetchSig.current = sig
119
+ // One parallel burst; the loader seeds its own `${field}::` cache so the
120
+ // subsequent lazy open (q === '') is a cache hit. allSettled so one bad
121
+ // field never blocks the others.
122
+ void Promise.allSettled(
123
+ fields.map(async (field) => {
124
+ const loader = getFacetLoader(field)
125
+ if (!loader) return null
126
+ const opts = await loader()
127
+ return { field, opts }
128
+ }),
129
+ ).then((results) => {
130
+ if (!mountedRef.current) return
131
+ setFacetOptions((prev) => {
132
+ const next = new Map(prev)
133
+ for (const r of results) {
134
+ if (r.status === 'fulfilled' && r.value) {
135
+ next.set(r.value.field, r.value.opts)
136
+ }
137
+ }
138
+ return next
139
+ })
140
+ })
141
+ },
142
+ [facetsBase, getFacetLoader],
143
+ )
144
+
145
+ return { getFacetLoader, prefetchFacets, facetOptions }
79
146
  }