@asteby/metacore-runtime-react 36.1.0 → 37.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @asteby/metacore-runtime-react
2
2
 
3
+ ## 37.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a0616e1: `DynamicTable` acepta la prop opcional `allowedActionKeys?: string[]`: un allowlist de acciones de fila (por `key` del manifest v3) a renderizar para ESA instancia de la tabla, en vez de todas las acciones que el modelo declara. Permite que dos vistas del mismo modelo (una tabla genérica y una pantalla de propósito específico, ej. una cola de aprobación de crédito) muestren distintas acciones sin tocar el manifest ni duplicar el modelo. `undefined` conserva el comportamiento actual (todas las acciones).
8
+ - d1ffa4f: Realtime en React: `RealtimeProvider`, `useRealtime`, `useRealtimeInvalidate` (invalida las queries del QueryClient del host cuya key menciona el modelo/tabla), `useRealtimeStatus`, `useRealtimeTick` y `queryKeyMatchesEvent`. `DynamicTable` y `DynamicKanban` aceptan la prop opt-in `realtime` (apagada por default; `<RealtimeProvider defaultRealtime>` cambia el default) para refetch en vivo ante `DATA_EVENT` del modelo.
9
+
10
+ ### Patch Changes
11
+
12
+ - Updated dependencies [d1ffa4f]
13
+ - @asteby/metacore-sdk@3.7.0
14
+
3
15
  ## 36.1.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -33,6 +33,39 @@ Peers: `react`, `react-dom`, `react-i18next`, `i18next`, `@tanstack/react-router
33
33
  | `<LicenseGate state={…} onActivate={…}>` | El blindaje del negocio: envuelve la app; bloquea con un modal full-screen no descartable cuando la licencia de instancia falta/venció, degrada con banner en `grace`/`stale`. Branding-aware. |
34
34
  | `<LicenseExpiryBanner state={…} />` / `<LicenseStatusBadge status={…} />` | Piezas sueltas del gate para Ajustes u otros lugares. |
35
35
  | `isLicenseOperable` / `isLicenseBlocking` / `isPresetEntitled` / `isTrialExpired` | Helpers puros sobre `LicenseState`, espejo de `State.Operable()/Blocking()` del backend. |
36
+ | `<RealtimeProvider client={…} />` / `useRealtime()` / `useRealtimeInvalidate()` / `useRealtimeStatus()` / `useRealtimeTick()` | Reacción en vivo a `DATA_EVENT` del host (ver [Realtime](#realtime)). |
37
+
38
+ ## Realtime
39
+
40
+ El host expone un `RealtimeAPI` (`@asteby/metacore-sdk`) — en ops viaja como
41
+ `host.realtime` a las rutas inmersivas y como `api.realtime` a `register(api)`.
42
+ Los frames son org-scoped y **sin valores de fila** (identidad + qué llaves
43
+ cambiaron); la reacción típica es "refetch lo que muestro".
44
+
45
+ ```tsx
46
+ // Host: montar una vez, dentro del QueryClientProvider y del provider de WS.
47
+ <RealtimeProvider client={realtimeClient}>…</RealtimeProvider>
48
+
49
+ // Addon federado: pasar el cliente explícito — el contexto React NO cruza la
50
+ // frontera de module federation, el objeto plano sí.
51
+ useRealtime({ models: ['SalesOrder'], client: host.realtime }, (e) => {
52
+ if (e.action === 'resync' || e.id === currentId) refetch()
53
+ })
54
+
55
+ // Invalidar react-query (usa SIEMPRE el QueryClient del host — nunca crees otro):
56
+ useRealtimeInvalidate({ models: ['SalesOrder', 'sales_order_items'], client: host.realtime })
57
+
58
+ // Tablas y kanbans: opt-in por prop (apagado por default).
59
+ <DynamicTable model="SalesOrder" realtime />
60
+ <DynamicKanban model="WorkOrder" realtime />
61
+ // …o global para todo lo que cuelgue del provider:
62
+ <RealtimeProvider client={realtimeClient} defaultRealtime>
63
+ ```
64
+
65
+ `useRealtimeInvalidate` invalida toda query cuya key (string, array anidado u
66
+ objeto) mencione el modelo, la tabla o `addon.Model` — también como segmento de
67
+ una ruta (`/data/sales_orders?…`). Se puede reemplazar el matcher con `match`.
68
+ Todos los hooks son no-op cuando no hay cliente (host sin realtime).
36
69
 
37
70
  ## Blindaje de licencia (`LicenseGate`)
38
71
 
@@ -106,6 +106,12 @@ export interface DynamicKanbanProps {
106
106
  endpoint?: string;
107
107
  /** Bump to force a metadata + records refetch (same contract as DynamicTable). */
108
108
  refreshTrigger?: any;
109
+ /**
110
+ * Refetch the board when the host's realtime client reports a data event
111
+ * for this model (same contract as DynamicTable's `realtime`). Off by
112
+ * default; `<RealtimeProvider defaultRealtime>` flips the default.
113
+ */
114
+ realtime?: boolean;
109
115
  /** Called when a card is clicked (outside its action menu). */
110
116
  onCardClick?: (row: any) => void;
111
117
  /**
@@ -136,5 +142,5 @@ export interface DynamicKanbanProps {
136
142
  */
137
143
  defaultFilters?: Record<string, any>;
138
144
  }
139
- export declare function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize, lanePageSize, timeZone, currency, defaultFilters, }: DynamicKanbanProps): React.JSX.Element;
145
+ export declare function DynamicKanban({ model, endpoint, refreshTrigger, realtime: realtimeProp, onCardClick, onAction, pageSize, lanePageSize, timeZone, currency, defaultFilters, }: DynamicKanbanProps): React.JSX.Element;
140
146
  //# sourceMappingURL=dynamic-kanban.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"dynamic-kanban.d.ts","sourceRoot":"","sources":["../src/dynamic-kanban.tsx"],"names":[],"mappings":"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"}
@@ -12,6 +12,7 @@ import { Badge, Button, Card, CardContent, DropdownMenu, DropdownMenuContent, Dr
12
12
  import { ColumnFilterControl, FilterValueCombobox } from '@asteby/metacore-ui/data-table';
13
13
  import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib';
14
14
  import { useApi } from './api-context';
15
+ import { useRealtimeDefault, useRealtimeTick } from './realtime-context';
15
16
  import { useStageAutomations, StageAutomationsButton, } from './stage-automations';
16
17
  import { useCustomStages, splitCustomStages, mergeLaneStages, resolveSmartLanes, cardMatchesStageFilters, smartLaneParams, AddStageColumn, CustomStageDialog, CustomStageDeleteDialog, StageConfigDialog, SmartLane, } from './custom-stages';
17
18
  import { useStageOverrides } from './stage-overrides';
@@ -261,10 +262,17 @@ function useIsDarkTheme() {
261
262
  }, []);
262
263
  return isDark;
263
264
  }
264
- export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, onAction, pageSize = 50, lanePageSize = 25, timeZone, currency, defaultFilters, }) {
265
+ export function DynamicKanban({ model, endpoint, refreshTrigger, realtime: realtimeProp, onCardClick, onAction, pageSize = 50, lanePageSize = 25, timeZone, currency, defaultFilters, }) {
265
266
  const { t, i18n } = useTranslation();
266
267
  const api = useApi();
267
268
  const isDark = useIsDarkTheme();
269
+ // Realtime refetch (opt-in) — debounced counter bumped by DATA_EVENTs for
270
+ // this model; folded into the board refetch effect next to refreshTrigger.
271
+ const realtimeDefault = useRealtimeDefault();
272
+ const realtimeTick = useRealtimeTick({
273
+ models: [model],
274
+ enabled: realtimeProp ?? realtimeDefault,
275
+ });
268
276
  // Stage automations (Bitrix-style per-lane rules). Degrades to no-op when
269
277
  // the host has no `/stage-automations` endpoint — the ⚡ affordance hides.
270
278
  const automations = useStageAutomations(model);
@@ -518,7 +526,9 @@ export function DynamicKanban({ model, endpoint, refreshTrigger, onCardClick, on
518
526
  void fetchData();
519
527
  }, 200);
520
528
  return () => clearTimeout(handle);
521
- }, [fetchData, metadata, refreshTrigger]);
529
+ // realtimeTick: data events for this model (see the `realtime` prop).
530
+ // eslint-disable-next-line react-hooks/exhaustive-deps
531
+ }, [fetchData, metadata, refreshTrigger, realtimeTick]);
522
532
  // Filterable fields for the toolbar, in metadata order (explicit filters
523
533
  // first, then filterable columns), each labeled from its metadata source.
524
534
  const filterFields = useMemo(() => {
@@ -20,6 +20,17 @@ export interface DynamicTableProps {
20
20
  /** Hide the export action on this view. See `hideImport`. */
21
21
  hideExport?: boolean;
22
22
  hiddenColumns?: string[];
23
+ /**
24
+ * Row-action ALLOWLIST for this table instance, by action key (manifest
25
+ * v3 NavItem.actions, carried through the host's nav item for this route).
26
+ * When set, only these row actions render — even though the model may
27
+ * declare more. Lets two views of the SAME model differ: a generic list
28
+ * keeps every action, while a purpose-built screen (e.g. a credit-approval
29
+ * queue) shows only authorize_credit/reject_credit instead of every action
30
+ * SalesOrder declares (Generar factura, Cancelar, etc. included).
31
+ * Undefined → every row action the model declares (unchanged default).
32
+ */
33
+ allowedActionKeys?: string[];
23
34
  onAction?: (action: string, row: any) => void;
24
35
  /**
25
36
  * Called when the user clicks anywhere on a data row (not on a checkbox,
@@ -29,6 +40,13 @@ export interface DynamicTableProps {
29
40
  */
30
41
  onRowClick?: (row: any) => void;
31
42
  refreshTrigger?: any;
43
+ /**
44
+ * Refetch when the host's realtime client reports a data event for this
45
+ * model (created/updated/deleted by anyone in the org, or a `resync`).
46
+ * Off by default; `<RealtimeProvider defaultRealtime>` turns it on for
47
+ * every table, and an explicit prop always wins. No-op without a client.
48
+ */
49
+ realtime?: boolean;
32
50
  defaultFilters?: Record<string, any>;
33
51
  extraColumns?: ColumnDef<any>[];
34
52
  /**
@@ -73,5 +91,5 @@ export interface DynamicTableProps {
73
91
  */
74
92
  infiniteScroll?: boolean;
75
93
  }
76
- export declare function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync, hideImport, hideExport, hiddenColumns, onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns, getDynamicColumns, timeZone, currency, pagination: paginationMode, infiniteScroll: infiniteScrollProp, }: DynamicTableProps): import("react").JSX.Element;
94
+ export declare function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync, hideImport, hideExport, hiddenColumns, allowedActionKeys, onAction, onRowClick, refreshTrigger, realtime: realtimeProp, defaultFilters, extraColumns, getDynamicColumns, timeZone, currency, pagination: paginationMode, infiniteScroll: infiniteScrollProp, }: DynamicTableProps): import("react").JSX.Element;
77
95
  //# sourceMappingURL=dynamic-table.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAgC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AA4FnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,aAAoB,EACpB,UAAU,EACV,UAAU,EACV,aAAkB,EAClB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,UAAU,EAAE,cAAc,EAC1B,cAAc,EAAE,kBAA0B,GAC7C,EAAE,iBAAiB,+BAkvCnB"}
1
+ {"version":3,"file":"dynamic-table.d.ts","sourceRoot":"","sources":["../src/dynamic-table.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EAajB,MAAM,uBAAuB,CAAA;AAiC9B,OAAO,KAAK,EAAsB,iBAAiB,EAAE,MAAM,wBAAwB,CAAA;AA4FnF,MAAM,WAAW,iBAAiB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,6DAA6D;IAC7D,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAA;IACxB;;;;;;;;;OASG;IACH,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAA;IAC5B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC7C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,IAAI,CAAA;IAC/B,cAAc,CAAC,EAAE,GAAG,CAAA;IACpB;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;IACpC,YAAY,CAAC,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,CAAA;IAC/B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;;;;;;;;;;;OAaG;IACH,UAAU,CAAC,EAAE,OAAO,GAAG,UAAU,CAAA;IACjC;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,YAAY,CAAC,EACzB,KAAK,EACL,QAAQ,EACR,gBAAgB,EAChB,aAAoB,EACpB,UAAU,EACV,UAAU,EACV,aAAkB,EAClB,iBAAiB,EACjB,QAAQ,EACR,UAAU,EACV,cAAc,EACd,QAAQ,EAAE,YAAY,EACtB,cAAc,EACd,YAAiB,EACjB,iBAA4C,EAC5C,QAAQ,EACR,QAAQ,EACR,UAAU,EAAE,cAAc,EAC1B,cAAc,EAAE,kBAA0B,GAC7C,EAAE,iBAAiB,+BAuwCnB"}
@@ -23,6 +23,7 @@ import { toast } from 'sonner';
23
23
  import { Progress } from './dialogs/_primitives';
24
24
  import { useMetadataCache } from './metadata-cache';
25
25
  import { useApi, useCurrentBranch } from './api-context';
26
+ import { useRealtimeDefault, useRealtimeTick } from './realtime-context';
26
27
  import { defaultGetDynamicColumns, DATE_CELL_TYPES, aggregateOf, formatAggregateTotal } from './dynamic-columns';
27
28
  import { useFacetLoaders, isLongTextColumn } from './use-facet-loaders';
28
29
  import { translateOptionLabels } from './filter-chips';
@@ -104,13 +105,20 @@ function writeStoredPageSize(model, size) {
104
105
  // quota / private mode — persistence is a nicety, never fatal
105
106
  }
106
107
  }
107
- export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync = true, hideImport, hideExport, hiddenColumns = [], onAction, onRowClick, refreshTrigger, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone, currency, pagination: paginationMode, infiniteScroll: infiniteScrollProp = false, }) {
108
+ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync = true, hideImport, hideExport, hiddenColumns = [], allowedActionKeys, onAction, onRowClick, refreshTrigger, realtime: realtimeProp, defaultFilters, extraColumns = [], getDynamicColumns = defaultGetDynamicColumns, timeZone, currency, pagination: paginationMode, infiniteScroll: infiniteScrollProp = false, }) {
108
109
  // The explicit `pagination` prop wins; the legacy `infiniteScroll` boolean
109
110
  // still selects the mode when `pagination` is absent (back-compat).
110
111
  const infiniteScroll = paginationMode ? paginationMode === 'infinite' : infiniteScrollProp;
111
112
  const { t, i18n } = useTranslation();
112
113
  const api = useApi();
113
114
  const currentBranch = useCurrentBranch();
115
+ // Realtime refetch (opt-in): a debounced counter that bumps on every
116
+ // DATA_EVENT for this model and rides the same deps as `refreshTrigger`.
117
+ const realtimeDefault = useRealtimeDefault();
118
+ const realtimeTick = useRealtimeTick({
119
+ models: [model],
120
+ enabled: realtimeProp ?? realtimeDefault,
121
+ });
114
122
  const prevBranchId = useRef(currentBranch?.id);
115
123
  const { getMetadata, setMetadata: cacheMetadata } = useMetadataCache();
116
124
  const cachedMeta = getMetadata(model);
@@ -611,7 +619,8 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
611
619
  finally {
612
620
  setLoadingData(false);
613
621
  }
614
- }, [model, metadata, pagination, buildFilterParams, refreshTrigger, endpoint, currentBranch?.id, api, enableUrlSync]);
622
+ // eslint-disable-next-line react-hooks/exhaustive-deps
623
+ }, [model, metadata, pagination, buildFilterParams, refreshTrigger, realtimeTick, endpoint, currentBranch?.id, api, enableUrlSync]);
615
624
  // Columns whose metadata opts into a footer total (display_config.aggregate
616
625
  // → styleConfig.aggregate). When empty, no footer row is rendered and no
617
626
  // aggregate request is made.
@@ -772,8 +781,9 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
772
781
  // matching the classic path (fetchData carries refreshTrigger in its
773
782
  // deps). Without it the comment above lied: infinite lists silently
774
783
  // failed to reload after a create ("a veces no recarga la tabla").
784
+ // realtimeTick plays the same role for data events (see the `realtime` prop).
775
785
  // eslint-disable-next-line react-hooks/exhaustive-deps
776
- }, [infiniteScroll, metadata, filterSignature, refreshTrigger]);
786
+ }, [infiniteScroll, metadata, filterSignature, refreshTrigger, realtimeTick]);
777
787
  const handleRefresh = useCallback(() => {
778
788
  // Infinite mode owns its own list: refresh reloads page 1 and drops the
779
789
  // accumulated pages (a classic fetchData would collapse it to one small
@@ -999,15 +1009,27 @@ export function DynamicTable({ model, endpoint, mutationEndpoint, enableUrlSync
999
1009
  // Row-action column only renders per-row actions. Table-level placements
1000
1010
  // ("table"/"create") are surfaced by <ModelActionToolbar> at the page
1001
1011
  // level, so strip them here to avoid a meaningless per-row button.
1002
- const rowMetadata = viewMetadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
1003
- ? { ...viewMetadata, actions: viewMetadata.actions.filter((a) => !a.placement || a.placement === 'row') }
1004
- : viewMetadata;
1012
+ // `allowedActionKeys`, when given, further narrows the row set to a
1013
+ // per-VIEW allowlist two nav entries on the same model (a generic
1014
+ // list vs. a purpose-built approval queue) can then show different
1015
+ // actions instead of every action the model declares.
1016
+ const rowMetadata = (() => {
1017
+ let actions = viewMetadata.actions;
1018
+ if (actions?.some((a) => a.placement === 'table' || a.placement === 'create')) {
1019
+ actions = actions.filter((a) => !a.placement || a.placement === 'row');
1020
+ }
1021
+ if (allowedActionKeys && actions) {
1022
+ const allowed = new Set(allowedActionKeys);
1023
+ actions = actions.filter((a) => allowed.has(a.key));
1024
+ }
1025
+ return actions === viewMetadata.actions ? viewMetadata : { ...viewMetadata, actions };
1026
+ })();
1005
1027
  const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone, currency);
1006
1028
  const filteredBase = baseColumns.filter((col) => !hiddenColumns.includes(col.id));
1007
1029
  const actionsCol = filteredBase.find((c) => c.id === 'actions');
1008
1030
  const otherCols = filteredBase.filter((c) => c.id !== 'actions');
1009
1031
  return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])];
1010
- }, [viewMetadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency]);
1032
+ }, [viewMetadata, handleInternalAction, hiddenColumns, allowedActionKeys, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency]);
1011
1033
  const filters = useMemo(() => [], []);
1012
1034
  const table = useReactTable({
1013
1035
  data,
package/dist/index.d.ts CHANGED
@@ -41,6 +41,7 @@ export * from './print-document-button';
41
41
  export * from './use-org-document-templates';
42
42
  export * from './document-template-editor';
43
43
  export * from './metadata-cache';
44
+ export { RealtimeProvider, useRealtimeClient, useRealtimeDefault, useRealtime, useRealtimeStatus, useRealtimeInvalidate, useRealtimeTick, queryKeyMatchesEvent, type RealtimeContextValue, type RealtimeProviderProps, type UseRealtimeOptions, type UseRealtimeInvalidateOptions, type UseRealtimeTickOptions, } from './realtime-context';
44
45
  export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwapSubscriber, type AddonManifestChangedMessage, type ManifestHotSwapClient, type WireHotSwapInvalidationOptions, } from './manifest-hotswap-subscriber';
45
46
  export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, type HotSwapReloadStrategy, type HotSwapReloadConfig, type HotSwapReloadAction, type HotSwapReloadDeps, type UseHotSwapReloadResult, } from './hotswap-reload-policy';
46
47
  export * from './dynamic-icon';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -46,6 +46,7 @@ export * from './print-document-button';
46
46
  export * from './use-org-document-templates';
47
47
  export * from './document-template-editor';
48
48
  export * from './metadata-cache';
49
+ export { RealtimeProvider, useRealtimeClient, useRealtimeDefault, useRealtime, useRealtimeStatus, useRealtimeInvalidate, useRealtimeTick, queryKeyMatchesEvent, } from './realtime-context';
49
50
  export { ADDON_MANIFEST_CHANGED_TYPE, wireHotSwapInvalidation, useManifestHotSwapSubscriber, } from './manifest-hotswap-subscriber';
50
51
  export { useHotSwapReload, applyHotSwapReload, withVersionParam, clearFederationContainer, shortenHash, } from './hotswap-reload-policy';
51
52
  export * from './dynamic-icon';
@@ -0,0 +1,82 @@
1
+ import type { QueryKey } from '@tanstack/react-query';
2
+ import type { DataEvent, DataEventHandler, RealtimeAPI, RealtimeStatus, RealtimeSubscribeOptions } from '@asteby/metacore-sdk';
3
+ export interface RealtimeContextValue {
4
+ /** Host-provided client, or null when the host has no realtime. */
5
+ client: RealtimeAPI | null;
6
+ /**
7
+ * When true, `DynamicTable` / `DynamicKanban` refetch on data events
8
+ * unless a component passes `realtime={false}`. Default false (opt-in).
9
+ */
10
+ defaultRealtime: boolean;
11
+ }
12
+ export interface RealtimeProviderProps {
13
+ client: RealtimeAPI | null | undefined;
14
+ /** Turn realtime refetch on for every dynamic table/kanban below. Default false. */
15
+ defaultRealtime?: boolean;
16
+ children: React.ReactNode;
17
+ }
18
+ /**
19
+ * Mount once, inside the host's WebSocket provider and QueryClientProvider.
20
+ * `client` may be null while the host is still connecting — hooks become
21
+ * no-ops and re-subscribe when a client appears.
22
+ */
23
+ export declare function RealtimeProvider({ client, defaultRealtime, children }: RealtimeProviderProps): import("react").JSX.Element;
24
+ /** The host realtime client from context (null when none is mounted). */
25
+ export declare function useRealtimeClient(explicit?: RealtimeAPI | null): RealtimeAPI | null;
26
+ /** Whether dynamic components should refetch on data events by default. */
27
+ export declare function useRealtimeDefault(): boolean;
28
+ export interface UseRealtimeOptions extends RealtimeSubscribeOptions {
29
+ /** Pass `host.realtime` / `api.realtime` to bypass the context. */
30
+ client?: RealtimeAPI | null;
31
+ /** Set false to pause the subscription without unmounting. Default true. */
32
+ enabled?: boolean;
33
+ }
34
+ /**
35
+ * Subscribe to data events for the given models. The handler is kept in a
36
+ * ref so callers don't need to memoise it; the subscription is torn down on
37
+ * unmount and re-created when models/events/client change.
38
+ *
39
+ * useRealtime({ models: ['SalesOrder'], client: host.realtime }, (e) => {
40
+ * if (e.action === 'resync' || e.id === currentId) refetch()
41
+ * })
42
+ */
43
+ export declare function useRealtime(options: UseRealtimeOptions, handler: DataEventHandler): void;
44
+ /** Live status of the realtime client ('closed' when none). */
45
+ export declare function useRealtimeStatus(explicit?: RealtimeAPI | null): RealtimeStatus;
46
+ /** Default matcher: any string segment of the query key (case-insensitive)
47
+ * equals the event's model, table or `addon.model`, or contains the model
48
+ * as a path segment (`/data/sales_orders`). */
49
+ export declare function queryKeyMatchesEvent(queryKey: QueryKey, event: DataEvent): boolean;
50
+ export interface UseRealtimeInvalidateOptions extends UseRealtimeOptions {
51
+ /** Override which queries a given event invalidates. Default: `queryKeyMatchesEvent`. */
52
+ match?: (queryKey: QueryKey, event: DataEvent) => boolean;
53
+ /** Coalesce bursts before invalidating (ms). Default 250. */
54
+ debounceMs?: number;
55
+ /** Observe each event after it was scheduled for invalidation. */
56
+ onEvent?: DataEventHandler;
57
+ }
58
+ /**
59
+ * Invalidate every react-query query whose key mentions the event's model /
60
+ * table (see `queryKeyMatchesEvent`). Uses the host's QueryClient. Bursts
61
+ * are debounced so a coalesced frame storm becomes a single refetch round.
62
+ *
63
+ * useRealtimeInvalidate({ models: ['SalesOrder', 'sales_order_items'], client: host.realtime })
64
+ */
65
+ export declare function useRealtimeInvalidate(options: UseRealtimeInvalidateOptions): void;
66
+ export interface UseRealtimeTickOptions {
67
+ /** Models/tables to watch. */
68
+ models: string[];
69
+ /** Explicit client (federated addons). */
70
+ client?: RealtimeAPI | null;
71
+ /** Master switch — the caller resolves prop vs provider default. */
72
+ enabled: boolean;
73
+ /** Coalesce bursts before ticking (ms). Default 300. */
74
+ debounceMs?: number;
75
+ }
76
+ /**
77
+ * A counter that increments (debounced) whenever a data event lands for one
78
+ * of `models`. Fold it into a refetch effect's dependencies — that is exactly
79
+ * what `DynamicTable` / `DynamicKanban` do behind their `realtime` prop.
80
+ */
81
+ export declare function useRealtimeTick(options: UseRealtimeTickOptions): number;
82
+ //# sourceMappingURL=realtime-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"realtime-context.d.ts","sourceRoot":"","sources":["../src/realtime-context.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AAErD,OAAO,KAAK,EACR,SAAS,EAET,gBAAgB,EAChB,WAAW,EACX,cAAc,EACd,wBAAwB,EAC3B,MAAM,sBAAsB,CAAA;AAE7B,MAAM,WAAW,oBAAoB;IACjC,mEAAmE;IACnE,MAAM,EAAE,WAAW,GAAG,IAAI,CAAA;IAC1B;;;OAGG;IACH,eAAe,EAAE,OAAO,CAAA;CAC3B;AAID,MAAM,WAAW,qBAAqB;IAClC,MAAM,EAAE,WAAW,GAAG,IAAI,GAAG,SAAS,CAAA;IACtC,oFAAoF;IACpF,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;CAC5B;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,EAAE,MAAM,EAAE,eAAuB,EAAE,QAAQ,EAAE,EAAE,qBAAqB,+BAMpG;AAED,yEAAyE;AACzE,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,WAAW,GAAG,IAAI,CAGnF;AAED,2EAA2E;AAC3E,wBAAgB,kBAAkB,IAAI,OAAO,CAE5C;AAED,MAAM,WAAW,kBAAmB,SAAQ,wBAAwB;IAChE,mEAAmE;IACnE,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,4EAA4E;IAC5E,OAAO,CAAC,EAAE,OAAO,CAAA;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,EAAE,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAoBxF;AAED,+DAA+D;AAC/D,wBAAgB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,IAAI,GAAG,cAAc,CAa/E;AAED;;gDAEgD;AAChD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,GAAG,OAAO,CAelF;AAgBD,MAAM,WAAW,4BAA6B,SAAQ,kBAAkB;IACpE,yFAAyF;IACzF,KAAK,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,KAAK,OAAO,CAAA;IACzD,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,kEAAkE;IAClE,OAAO,CAAC,EAAE,gBAAgB,CAAA;CAC7B;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,4BAA4B,GAAG,IAAI,CAiCjF;AAED,MAAM,WAAW,sBAAsB;IACnC,8BAA8B;IAC9B,MAAM,EAAE,MAAM,EAAE,CAAA;IAChB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,WAAW,GAAG,IAAI,CAAA;IAC3B,oEAAoE;IACpE,OAAO,EAAE,OAAO,CAAA;IAChB,wDAAwD;IACxD,UAAU,CAAC,EAAE,MAAM,CAAA;CACtB;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAuBvE"}
@@ -0,0 +1,182 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // realtime-context — React surface over the host's RealtimeAPI
3
+ // (`@asteby/metacore-sdk`): a provider the host mounts once, `useRealtime`
4
+ // for handlers, `useRealtimeInvalidate` to refresh react-query caches, and
5
+ // `useRealtimeTick` — the opt-in refetch signal `DynamicTable` /
6
+ // `DynamicKanban` consume through their `realtime` prop.
7
+ //
8
+ // Module-federation note: a federated addon may run its OWN copy of this
9
+ // module, in which case React context from the host is invisible to it. Every
10
+ // hook therefore accepts an explicit `client` (pass `host.realtime` or
11
+ // `api.realtime`) that wins over the context — the plain object crosses the
12
+ // federation boundary, the context does not.
13
+ //
14
+ // react-query: `useRealtimeInvalidate` uses `useQueryClient()` so it always
15
+ // targets the QueryClient the host mounted (single instance per page).
16
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
17
+ import { useQueryClient } from '@tanstack/react-query';
18
+ const RealtimeContext = createContext({ client: null, defaultRealtime: false });
19
+ /**
20
+ * Mount once, inside the host's WebSocket provider and QueryClientProvider.
21
+ * `client` may be null while the host is still connecting — hooks become
22
+ * no-ops and re-subscribe when a client appears.
23
+ */
24
+ export function RealtimeProvider({ client, defaultRealtime = false, children }) {
25
+ const value = useMemo(() => ({ client: client ?? null, defaultRealtime }), [client, defaultRealtime]);
26
+ return _jsx(RealtimeContext.Provider, { value: value, children: children });
27
+ }
28
+ /** The host realtime client from context (null when none is mounted). */
29
+ export function useRealtimeClient(explicit) {
30
+ const ctx = useContext(RealtimeContext);
31
+ return explicit ?? ctx.client;
32
+ }
33
+ /** Whether dynamic components should refetch on data events by default. */
34
+ export function useRealtimeDefault() {
35
+ return useContext(RealtimeContext).defaultRealtime;
36
+ }
37
+ /**
38
+ * Subscribe to data events for the given models. The handler is kept in a
39
+ * ref so callers don't need to memoise it; the subscription is torn down on
40
+ * unmount and re-created when models/events/client change.
41
+ *
42
+ * useRealtime({ models: ['SalesOrder'], client: host.realtime }, (e) => {
43
+ * if (e.action === 'resync' || e.id === currentId) refetch()
44
+ * })
45
+ */
46
+ export function useRealtime(options, handler) {
47
+ const client = useRealtimeClient(options.client);
48
+ const handlerRef = useRef(handler);
49
+ handlerRef.current = handler;
50
+ const enabled = options.enabled ?? true;
51
+ const modelsKey = (options.models ?? []).map((m) => m.trim().toLowerCase()).sort().join('|');
52
+ const eventsKey = (options.events ?? []).slice().sort().join('|');
53
+ useEffect(() => {
54
+ if (!client || !enabled || !modelsKey)
55
+ return;
56
+ const models = modelsKey.split('|');
57
+ const events = eventsKey ? eventsKey.split('|') : undefined;
58
+ let off = () => { };
59
+ try {
60
+ off = client.subscribe({ models, events }, (event) => handlerRef.current(event));
61
+ }
62
+ catch {
63
+ /* a host client must not throw, but never take the page down */
64
+ }
65
+ return off;
66
+ }, [client, enabled, modelsKey, eventsKey]);
67
+ }
68
+ /** Live status of the realtime client ('closed' when none). */
69
+ export function useRealtimeStatus(explicit) {
70
+ const client = useRealtimeClient(explicit);
71
+ const [status, setStatus] = useState(() => client?.status() ?? 'closed');
72
+ useEffect(() => {
73
+ if (!client) {
74
+ setStatus('closed');
75
+ return;
76
+ }
77
+ setStatus(client.status());
78
+ if (!client.onStatus)
79
+ return;
80
+ return client.onStatus(setStatus);
81
+ }, [client]);
82
+ return status;
83
+ }
84
+ /** Default matcher: any string segment of the query key (case-insensitive)
85
+ * equals the event's model, table or `addon.model`, or contains the model
86
+ * as a path segment (`/data/sales_orders`). */
87
+ export function queryKeyMatchesEvent(queryKey, event) {
88
+ const needles = [event.model, event.table ?? '', `${event.addon}.${event.model}`]
89
+ .map((s) => s.toLowerCase())
90
+ .filter(Boolean);
91
+ const parts = flattenKey(queryKey);
92
+ for (const part of parts) {
93
+ const p = part.toLowerCase();
94
+ for (const n of needles) {
95
+ if (p === n)
96
+ return true;
97
+ // '/data/sales_orders?x=1' or 'pos/SalesOrder' style segments.
98
+ const segs = p.split(/[/?&=]/);
99
+ if (segs.includes(n))
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+ function flattenKey(key, out = [], depth = 0) {
106
+ if (depth > 4)
107
+ return out;
108
+ for (const item of key) {
109
+ if (typeof item === 'string')
110
+ out.push(item);
111
+ else if (Array.isArray(item))
112
+ flattenKey(item, out, depth + 1);
113
+ else if (item && typeof item === 'object') {
114
+ for (const v of Object.values(item)) {
115
+ if (typeof v === 'string')
116
+ out.push(v);
117
+ }
118
+ }
119
+ }
120
+ return out;
121
+ }
122
+ /**
123
+ * Invalidate every react-query query whose key mentions the event's model /
124
+ * table (see `queryKeyMatchesEvent`). Uses the host's QueryClient. Bursts
125
+ * are debounced so a coalesced frame storm becomes a single refetch round.
126
+ *
127
+ * useRealtimeInvalidate({ models: ['SalesOrder', 'sales_order_items'], client: host.realtime })
128
+ */
129
+ export function useRealtimeInvalidate(options) {
130
+ const queryClient = useQueryClient();
131
+ const { match = queryKeyMatchesEvent, debounceMs = 250, onEvent } = options;
132
+ const pendingRef = useRef([]);
133
+ const timerRef = useRef(null);
134
+ const matchRef = useRef(match);
135
+ matchRef.current = match;
136
+ const onEventRef = useRef(onEvent);
137
+ onEventRef.current = onEvent;
138
+ const flush = useCallback(() => {
139
+ timerRef.current = null;
140
+ const batch = pendingRef.current;
141
+ pendingRef.current = [];
142
+ if (batch.length === 0)
143
+ return;
144
+ void queryClient.invalidateQueries({
145
+ predicate: (query) => batch.some((event) => matchRef.current(query.queryKey, event)),
146
+ });
147
+ }, [queryClient]);
148
+ useRealtime(options, (event) => {
149
+ pendingRef.current.push(event);
150
+ onEventRef.current?.(event);
151
+ if (timerRef.current)
152
+ return;
153
+ timerRef.current = setTimeout(flush, debounceMs);
154
+ });
155
+ useEffect(() => () => {
156
+ if (timerRef.current)
157
+ clearTimeout(timerRef.current);
158
+ }, []);
159
+ }
160
+ /**
161
+ * A counter that increments (debounced) whenever a data event lands for one
162
+ * of `models`. Fold it into a refetch effect's dependencies — that is exactly
163
+ * what `DynamicTable` / `DynamicKanban` do behind their `realtime` prop.
164
+ */
165
+ export function useRealtimeTick(options) {
166
+ const [tick, setTick] = useState(0);
167
+ const timerRef = useRef(null);
168
+ const debounceMs = options.debounceMs ?? 300;
169
+ useRealtime({ models: options.models, client: options.client, enabled: options.enabled }, () => {
170
+ if (timerRef.current)
171
+ return;
172
+ timerRef.current = setTimeout(() => {
173
+ timerRef.current = null;
174
+ setTick((t) => t + 1);
175
+ }, debounceMs);
176
+ });
177
+ useEffect(() => () => {
178
+ if (timerRef.current)
179
+ clearTimeout(timerRef.current);
180
+ }, []);
181
+ return tick;
182
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asteby/metacore-runtime-react",
3
- "version": "36.1.0",
3
+ "version": "37.0.0",
4
4
  "description": "React runtime for metacore hosts — renders addon contributions dynamically",
5
5
  "repository": {
6
6
  "type": "git",
@@ -37,7 +37,7 @@
37
37
  "react-i18next": ">=13",
38
38
  "sonner": ">=1.7",
39
39
  "zustand": ">=5",
40
- "@asteby/metacore-sdk": "^3.6.0",
40
+ "@asteby/metacore-sdk": "^3.7.0",
41
41
  "@asteby/metacore-ui": "^2.17.3"
42
42
  },
43
43
  "peerDependenciesMeta": {
@@ -67,7 +67,7 @@
67
67
  "typescript": "^6.0.0",
68
68
  "vitest": "^4.0.0",
69
69
  "zustand": "^5.0.0",
70
- "@asteby/metacore-sdk": "3.6.0",
70
+ "@asteby/metacore-sdk": "3.7.0",
71
71
  "@asteby/metacore-ui": "2.17.3"
72
72
  },
73
73
  "scripts": {
@@ -0,0 +1,195 @@
1
+ // @vitest-environment happy-dom
2
+ //
3
+ // Realtime hooks over a fake RealtimeAPI: useRealtime subscribes/unsubscribes
4
+ // with normalised models, an explicit client beats the context (federation
5
+ // boundary), useRealtimeInvalidate targets the host QueryClient by key match,
6
+ // and useRealtimeTick debounces bursts into one bump.
7
+ import { afterEach, describe, expect, it, vi } from 'vitest'
8
+ import { act, cleanup, renderHook } from '@testing-library/react'
9
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
10
+ import type { ReactNode } from 'react'
11
+ import type { DataEvent, DataEventHandler, RealtimeAPI, RealtimeSubscribeOptions } from '@asteby/metacore-sdk'
12
+ import {
13
+ RealtimeProvider,
14
+ queryKeyMatchesEvent,
15
+ useRealtime,
16
+ useRealtimeInvalidate,
17
+ useRealtimeStatus,
18
+ useRealtimeTick,
19
+ } from '../realtime-context'
20
+
21
+ afterEach(cleanup)
22
+
23
+ function fakeClient() {
24
+ const subs: Array<{ opts: RealtimeSubscribeOptions; handler: DataEventHandler }> = []
25
+ const statusListeners = new Set<(s: 'connecting' | 'open' | 'closed') => void>()
26
+ let status: 'connecting' | 'open' | 'closed' = 'open'
27
+ const client: RealtimeAPI & { subs: typeof subs; emit: (e: DataEvent) => void; setStatus: (s: typeof status) => void } = {
28
+ subs,
29
+ subscribe(opts, handler) {
30
+ const entry = { opts, handler }
31
+ subs.push(entry)
32
+ return () => {
33
+ const i = subs.indexOf(entry)
34
+ if (i >= 0) subs.splice(i, 1)
35
+ }
36
+ },
37
+ status: () => status,
38
+ onStatus(l) {
39
+ statusListeners.add(l)
40
+ return () => statusListeners.delete(l)
41
+ },
42
+ emit(e) {
43
+ subs.forEach((s) => s.handler(e))
44
+ },
45
+ setStatus(s) {
46
+ status = s
47
+ statusListeners.forEach((l) => l(s))
48
+ },
49
+ }
50
+ return client
51
+ }
52
+
53
+ const event = (over: Partial<DataEvent> = {}): DataEvent => ({
54
+ org_id: 'org',
55
+ addon: 'pos',
56
+ model: 'SalesOrder',
57
+ table: 'sales_orders',
58
+ action: 'updated',
59
+ id: '1',
60
+ at: '2026-08-31T00:00:00Z',
61
+ ...over,
62
+ })
63
+
64
+ describe('useRealtime', () => {
65
+ it('subscribes through the context client with normalised models and tears down on unmount', () => {
66
+ const client = fakeClient()
67
+ const handler = vi.fn()
68
+ const wrapper = ({ children }: { children: ReactNode }) => (
69
+ <RealtimeProvider client={client}>{children}</RealtimeProvider>
70
+ )
71
+ const { unmount } = renderHook(() => useRealtime({ models: [' SalesOrder ', 'WorkOrder'] }, handler), { wrapper })
72
+ expect(client.subs).toHaveLength(1)
73
+ expect(client.subs[0]!.opts.models).toEqual(['salesorder', 'workorder'])
74
+ client.emit(event())
75
+ expect(handler).toHaveBeenCalledTimes(1)
76
+ unmount()
77
+ expect(client.subs).toHaveLength(0)
78
+ })
79
+
80
+ it('prefers an explicit client over the context (federated addons)', () => {
81
+ const ctxClient = fakeClient()
82
+ const explicit = fakeClient()
83
+ const wrapper = ({ children }: { children: ReactNode }) => (
84
+ <RealtimeProvider client={ctxClient}>{children}</RealtimeProvider>
85
+ )
86
+ renderHook(() => useRealtime({ models: ['SalesOrder'], client: explicit }, () => {}), { wrapper })
87
+ expect(explicit.subs).toHaveLength(1)
88
+ expect(ctxClient.subs).toHaveLength(0)
89
+ })
90
+
91
+ it('is a no-op without a client, without models, or when disabled', () => {
92
+ const client = fakeClient()
93
+ renderHook(() => useRealtime({ models: ['SalesOrder'] }, () => {}))
94
+ renderHook(() => useRealtime({ models: [], client }, () => {}))
95
+ renderHook(() => useRealtime({ models: ['SalesOrder'], client, enabled: false }, () => {}))
96
+ expect(client.subs).toHaveLength(0)
97
+ })
98
+ })
99
+
100
+ describe('useRealtimeStatus', () => {
101
+ it('tracks the client status', () => {
102
+ const client = fakeClient()
103
+ const { result } = renderHook(() => useRealtimeStatus(client))
104
+ expect(result.current).toBe('open')
105
+ act(() => client.setStatus('connecting'))
106
+ expect(result.current).toBe('connecting')
107
+ const { result: none } = renderHook(() => useRealtimeStatus(null))
108
+ expect(none.current).toBe('closed')
109
+ })
110
+ })
111
+
112
+ describe('queryKeyMatchesEvent', () => {
113
+ it('matches model, table, qualified name and path segments (case-insensitive)', () => {
114
+ const e = event()
115
+ expect(queryKeyMatchesEvent(['salesorder', 'list'], e)).toBe(true)
116
+ expect(queryKeyMatchesEvent(['data', 'sales_orders', { page: 1 }], e)).toBe(true)
117
+ expect(queryKeyMatchesEvent(['pos.SalesOrder'], e)).toBe(true)
118
+ expect(queryKeyMatchesEvent(['/data/sales_orders?page=1'], e)).toBe(true)
119
+ expect(queryKeyMatchesEvent([{ model: 'SalesOrder' }], e)).toBe(true)
120
+ expect(queryKeyMatchesEvent(['work_orders'], e)).toBe(false)
121
+ expect(queryKeyMatchesEvent(['sales_orders_archive'], e)).toBe(false)
122
+ })
123
+ })
124
+
125
+ describe('useRealtimeInvalidate', () => {
126
+ it('invalidates only the queries whose key mentions the event model, debounced', async () => {
127
+ vi.useFakeTimers()
128
+ try {
129
+ const client = fakeClient()
130
+ const queryClient = new QueryClient()
131
+ queryClient.setQueryData(['sales_orders', 'list'], [])
132
+ queryClient.setQueryData(['work_orders', 'list'], [])
133
+ const invalidate = vi.spyOn(queryClient, 'invalidateQueries')
134
+ const wrapper = ({ children }: { children: ReactNode }) => (
135
+ <QueryClientProvider client={queryClient}>
136
+ <RealtimeProvider client={client}>{children}</RealtimeProvider>
137
+ </QueryClientProvider>
138
+ )
139
+ const onEvent = vi.fn()
140
+ renderHook(() => useRealtimeInvalidate({ models: ['SalesOrder'], debounceMs: 100, onEvent }), { wrapper })
141
+
142
+ act(() => {
143
+ client.emit(event({ id: '1' }))
144
+ client.emit(event({ id: '2' }))
145
+ })
146
+ expect(onEvent).toHaveBeenCalledTimes(2)
147
+ expect(invalidate).not.toHaveBeenCalled()
148
+ act(() => {
149
+ vi.advanceTimersByTime(120)
150
+ })
151
+ expect(invalidate).toHaveBeenCalledTimes(1)
152
+ const predicate = (invalidate.mock.calls[0]![0] as { predicate: (q: { queryKey: unknown[] }) => boolean }).predicate
153
+ expect(predicate({ queryKey: ['sales_orders', 'list'] })).toBe(true)
154
+ expect(predicate({ queryKey: ['work_orders', 'list'] })).toBe(false)
155
+ expect(queryClient.getQueryState(['sales_orders', 'list'])?.isInvalidated).toBe(true)
156
+ expect(queryClient.getQueryState(['work_orders', 'list'])?.isInvalidated).toBe(false)
157
+ } finally {
158
+ vi.useRealTimers()
159
+ }
160
+ })
161
+ })
162
+
163
+ describe('useRealtimeTick', () => {
164
+ it('bumps once per burst and stays quiet when disabled', () => {
165
+ vi.useFakeTimers()
166
+ try {
167
+ const client = fakeClient()
168
+ const { result, rerender } = renderHook(
169
+ ({ enabled }: { enabled: boolean }) =>
170
+ useRealtimeTick({ models: ['SalesOrder'], client, enabled, debounceMs: 50 }),
171
+ { initialProps: { enabled: true } },
172
+ )
173
+ expect(result.current).toBe(0)
174
+ act(() => {
175
+ client.emit(event())
176
+ client.emit(event({ id: '2' }))
177
+ client.emit(event({ id: '3' }))
178
+ })
179
+ expect(result.current).toBe(0)
180
+ act(() => {
181
+ vi.advanceTimersByTime(60)
182
+ })
183
+ expect(result.current).toBe(1)
184
+
185
+ rerender({ enabled: false })
186
+ expect(client.subs).toHaveLength(0)
187
+ act(() => {
188
+ vi.advanceTimersByTime(100)
189
+ })
190
+ expect(result.current).toBe(1)
191
+ } finally {
192
+ vi.useRealTimers()
193
+ }
194
+ })
195
+ })
@@ -90,6 +90,7 @@ import {
90
90
  import { ColumnFilterControl, FilterValueCombobox, type ColumnFilterType } from '@asteby/metacore-ui/data-table'
91
91
  import { generateBadgeStyles, optionColor } from '@asteby/metacore-ui/lib'
92
92
  import { useApi } from './api-context'
93
+ import { useRealtimeDefault, useRealtimeTick } from './realtime-context'
93
94
  import {
94
95
  useStageAutomations,
95
96
  StageAutomationsButton,
@@ -462,6 +463,12 @@ export interface DynamicKanbanProps {
462
463
  endpoint?: string
463
464
  /** Bump to force a metadata + records refetch (same contract as DynamicTable). */
464
465
  refreshTrigger?: any
466
+ /**
467
+ * Refetch the board when the host's realtime client reports a data event
468
+ * for this model (same contract as DynamicTable's `realtime`). Off by
469
+ * default; `<RealtimeProvider defaultRealtime>` flips the default.
470
+ */
471
+ realtime?: boolean
465
472
  /** Called when a card is clicked (outside its action menu). */
466
473
  onCardClick?: (row: any) => void
467
474
  /**
@@ -497,6 +504,7 @@ export function DynamicKanban({
497
504
  model,
498
505
  endpoint,
499
506
  refreshTrigger,
507
+ realtime: realtimeProp,
500
508
  onCardClick,
501
509
  onAction,
502
510
  pageSize = 50,
@@ -508,6 +516,13 @@ export function DynamicKanban({
508
516
  const { t, i18n } = useTranslation()
509
517
  const api = useApi()
510
518
  const isDark = useIsDarkTheme()
519
+ // Realtime refetch (opt-in) — debounced counter bumped by DATA_EVENTs for
520
+ // this model; folded into the board refetch effect next to refreshTrigger.
521
+ const realtimeDefault = useRealtimeDefault()
522
+ const realtimeTick = useRealtimeTick({
523
+ models: [model],
524
+ enabled: realtimeProp ?? realtimeDefault,
525
+ })
511
526
 
512
527
  // Stage automations (Bitrix-style per-lane rules). Degrades to no-op when
513
528
  // the host has no `/stage-automations` endpoint — the ⚡ affordance hides.
@@ -782,7 +797,9 @@ export function DynamicKanban({
782
797
  void fetchData()
783
798
  }, 200)
784
799
  return () => clearTimeout(handle)
785
- }, [fetchData, metadata, refreshTrigger])
800
+ // realtimeTick: data events for this model (see the `realtime` prop).
801
+ // eslint-disable-next-line react-hooks/exhaustive-deps
802
+ }, [fetchData, metadata, refreshTrigger, realtimeTick])
786
803
 
787
804
  // Filterable fields for the toolbar, in metadata order (explicit filters
788
805
  // first, then filterable columns), each labeled from its metadata source.
@@ -64,6 +64,7 @@ import { toast } from 'sonner'
64
64
  import { Progress } from './dialogs/_primitives'
65
65
  import { useMetadataCache } from './metadata-cache'
66
66
  import { useApi, useCurrentBranch } from './api-context'
67
+ import { useRealtimeDefault, useRealtimeTick } from './realtime-context'
67
68
  import type { ColumnFilterConfig, GetDynamicColumns } from './dynamic-columns-shim'
68
69
  import { defaultGetDynamicColumns, DATE_CELL_TYPES, aggregateOf, formatAggregateTotal } from './dynamic-columns'
69
70
  import { useFacetLoaders, isLongTextColumn } from './use-facet-loaders'
@@ -176,6 +177,17 @@ export interface DynamicTableProps {
176
177
  /** Hide the export action on this view. See `hideImport`. */
177
178
  hideExport?: boolean
178
179
  hiddenColumns?: string[]
180
+ /**
181
+ * Row-action ALLOWLIST for this table instance, by action key (manifest
182
+ * v3 NavItem.actions, carried through the host's nav item for this route).
183
+ * When set, only these row actions render — even though the model may
184
+ * declare more. Lets two views of the SAME model differ: a generic list
185
+ * keeps every action, while a purpose-built screen (e.g. a credit-approval
186
+ * queue) shows only authorize_credit/reject_credit instead of every action
187
+ * SalesOrder declares (Generar factura, Cancelar, etc. included).
188
+ * Undefined → every row action the model declares (unchanged default).
189
+ */
190
+ allowedActionKeys?: string[]
179
191
  onAction?: (action: string, row: any) => void
180
192
  /**
181
193
  * Called when the user clicks anywhere on a data row (not on a checkbox,
@@ -185,6 +197,13 @@ export interface DynamicTableProps {
185
197
  */
186
198
  onRowClick?: (row: any) => void
187
199
  refreshTrigger?: any
200
+ /**
201
+ * Refetch when the host's realtime client reports a data event for this
202
+ * model (created/updated/deleted by anyone in the org, or a `resync`).
203
+ * Off by default; `<RealtimeProvider defaultRealtime>` turns it on for
204
+ * every table, and an explicit prop always wins. No-op without a client.
205
+ */
206
+ realtime?: boolean
188
207
  defaultFilters?: Record<string, any>
189
208
  extraColumns?: ColumnDef<any>[]
190
209
  /**
@@ -238,9 +257,11 @@ export function DynamicTable({
238
257
  hideImport,
239
258
  hideExport,
240
259
  hiddenColumns = [],
260
+ allowedActionKeys,
241
261
  onAction,
242
262
  onRowClick,
243
263
  refreshTrigger,
264
+ realtime: realtimeProp,
244
265
  defaultFilters,
245
266
  extraColumns = [],
246
267
  getDynamicColumns = defaultGetDynamicColumns,
@@ -255,6 +276,13 @@ export function DynamicTable({
255
276
  const { t, i18n } = useTranslation()
256
277
  const api = useApi()
257
278
  const currentBranch = useCurrentBranch()
279
+ // Realtime refetch (opt-in): a debounced counter that bumps on every
280
+ // DATA_EVENT for this model and rides the same deps as `refreshTrigger`.
281
+ const realtimeDefault = useRealtimeDefault()
282
+ const realtimeTick = useRealtimeTick({
283
+ models: [model],
284
+ enabled: realtimeProp ?? realtimeDefault,
285
+ })
258
286
 
259
287
  const prevBranchId = useRef(currentBranch?.id)
260
288
 
@@ -742,7 +770,8 @@ export function DynamicTable({
742
770
  } finally {
743
771
  setLoadingData(false)
744
772
  }
745
- }, [model, metadata, pagination, buildFilterParams, refreshTrigger, endpoint, currentBranch?.id, api, enableUrlSync])
773
+ // eslint-disable-next-line react-hooks/exhaustive-deps
774
+ }, [model, metadata, pagination, buildFilterParams, refreshTrigger, realtimeTick, endpoint, currentBranch?.id, api, enableUrlSync])
746
775
 
747
776
  // Columns whose metadata opts into a footer total (display_config.aggregate
748
777
  // → styleConfig.aggregate). When empty, no footer row is rendered and no
@@ -911,8 +940,9 @@ export function DynamicTable({
911
940
  // matching the classic path (fetchData carries refreshTrigger in its
912
941
  // deps). Without it the comment above lied: infinite lists silently
913
942
  // failed to reload after a create ("a veces no recarga la tabla").
943
+ // realtimeTick plays the same role for data events (see the `realtime` prop).
914
944
  // eslint-disable-next-line react-hooks/exhaustive-deps
915
- }, [infiniteScroll, metadata, filterSignature, refreshTrigger])
945
+ }, [infiniteScroll, metadata, filterSignature, refreshTrigger, realtimeTick])
916
946
 
917
947
  const handleRefresh = useCallback(() => {
918
948
  // Infinite mode owns its own list: refresh reloads page 1 and drops the
@@ -1124,15 +1154,27 @@ export function DynamicTable({
1124
1154
  // Row-action column only renders per-row actions. Table-level placements
1125
1155
  // ("table"/"create") are surfaced by <ModelActionToolbar> at the page
1126
1156
  // level, so strip them here to avoid a meaningless per-row button.
1127
- const rowMetadata = viewMetadata.actions?.some((a) => a.placement === 'table' || a.placement === 'create')
1128
- ? { ...viewMetadata, actions: viewMetadata.actions.filter((a) => !a.placement || a.placement === 'row') }
1129
- : viewMetadata
1157
+ // `allowedActionKeys`, when given, further narrows the row set to a
1158
+ // per-VIEW allowlist two nav entries on the same model (a generic
1159
+ // list vs. a purpose-built approval queue) can then show different
1160
+ // actions instead of every action the model declares.
1161
+ const rowMetadata = (() => {
1162
+ let actions = viewMetadata.actions
1163
+ if (actions?.some((a) => a.placement === 'table' || a.placement === 'create')) {
1164
+ actions = actions.filter((a) => !a.placement || a.placement === 'row')
1165
+ }
1166
+ if (allowedActionKeys && actions) {
1167
+ const allowed = new Set(allowedActionKeys)
1168
+ actions = actions.filter((a) => allowed.has(a.key))
1169
+ }
1170
+ return actions === viewMetadata.actions ? viewMetadata : { ...viewMetadata, actions }
1171
+ })()
1130
1172
  const baseColumns = getDynamicColumns(rowMetadata, handleInternalAction, t, i18n.language, columnFilterConfigs, timeZone, currency)
1131
1173
  const filteredBase = baseColumns.filter((col: ColumnDef<any>) => !hiddenColumns.includes(col.id as string))
1132
1174
  const actionsCol = filteredBase.find((c: ColumnDef<any>) => c.id === 'actions')
1133
1175
  const otherCols = filteredBase.filter((c: ColumnDef<any>) => c.id !== 'actions')
1134
1176
  return [...otherCols, ...extraColumns, ...(actionsCol ? [actionsCol] : [])]
1135
- }, [viewMetadata, handleInternalAction, hiddenColumns, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency])
1177
+ }, [viewMetadata, handleInternalAction, hiddenColumns, allowedActionKeys, extraColumns, t, i18n.language, columnFilterConfigs, getDynamicColumns, timeZone, currency])
1136
1178
 
1137
1179
  const filters = useMemo(() => [], [])
1138
1180
 
package/src/index.ts CHANGED
@@ -213,6 +213,21 @@ export * from './print-document-button'
213
213
  export * from './use-org-document-templates'
214
214
  export * from './document-template-editor'
215
215
  export * from './metadata-cache'
216
+ export {
217
+ RealtimeProvider,
218
+ useRealtimeClient,
219
+ useRealtimeDefault,
220
+ useRealtime,
221
+ useRealtimeStatus,
222
+ useRealtimeInvalidate,
223
+ useRealtimeTick,
224
+ queryKeyMatchesEvent,
225
+ type RealtimeContextValue,
226
+ type RealtimeProviderProps,
227
+ type UseRealtimeOptions,
228
+ type UseRealtimeInvalidateOptions,
229
+ type UseRealtimeTickOptions,
230
+ } from './realtime-context'
216
231
  export {
217
232
  ADDON_MANIFEST_CHANGED_TYPE,
218
233
  wireHotSwapInvalidation,
@@ -0,0 +1,248 @@
1
+ // realtime-context — React surface over the host's RealtimeAPI
2
+ // (`@asteby/metacore-sdk`): a provider the host mounts once, `useRealtime`
3
+ // for handlers, `useRealtimeInvalidate` to refresh react-query caches, and
4
+ // `useRealtimeTick` — the opt-in refetch signal `DynamicTable` /
5
+ // `DynamicKanban` consume through their `realtime` prop.
6
+ //
7
+ // Module-federation note: a federated addon may run its OWN copy of this
8
+ // module, in which case React context from the host is invisible to it. Every
9
+ // hook therefore accepts an explicit `client` (pass `host.realtime` or
10
+ // `api.realtime`) that wins over the context — the plain object crosses the
11
+ // federation boundary, the context does not.
12
+ //
13
+ // react-query: `useRealtimeInvalidate` uses `useQueryClient()` so it always
14
+ // targets the QueryClient the host mounted (single instance per page).
15
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'
16
+ import type { QueryKey } from '@tanstack/react-query'
17
+ import { useQueryClient } from '@tanstack/react-query'
18
+ import type {
19
+ DataEvent,
20
+ DataEventAction,
21
+ DataEventHandler,
22
+ RealtimeAPI,
23
+ RealtimeStatus,
24
+ RealtimeSubscribeOptions,
25
+ } from '@asteby/metacore-sdk'
26
+
27
+ export interface RealtimeContextValue {
28
+ /** Host-provided client, or null when the host has no realtime. */
29
+ client: RealtimeAPI | null
30
+ /**
31
+ * When true, `DynamicTable` / `DynamicKanban` refetch on data events
32
+ * unless a component passes `realtime={false}`. Default false (opt-in).
33
+ */
34
+ defaultRealtime: boolean
35
+ }
36
+
37
+ const RealtimeContext = createContext<RealtimeContextValue>({ client: null, defaultRealtime: false })
38
+
39
+ export interface RealtimeProviderProps {
40
+ client: RealtimeAPI | null | undefined
41
+ /** Turn realtime refetch on for every dynamic table/kanban below. Default false. */
42
+ defaultRealtime?: boolean
43
+ children: React.ReactNode
44
+ }
45
+
46
+ /**
47
+ * Mount once, inside the host's WebSocket provider and QueryClientProvider.
48
+ * `client` may be null while the host is still connecting — hooks become
49
+ * no-ops and re-subscribe when a client appears.
50
+ */
51
+ export function RealtimeProvider({ client, defaultRealtime = false, children }: RealtimeProviderProps) {
52
+ const value = useMemo<RealtimeContextValue>(
53
+ () => ({ client: client ?? null, defaultRealtime }),
54
+ [client, defaultRealtime],
55
+ )
56
+ return <RealtimeContext.Provider value={value}>{children}</RealtimeContext.Provider>
57
+ }
58
+
59
+ /** The host realtime client from context (null when none is mounted). */
60
+ export function useRealtimeClient(explicit?: RealtimeAPI | null): RealtimeAPI | null {
61
+ const ctx = useContext(RealtimeContext)
62
+ return explicit ?? ctx.client
63
+ }
64
+
65
+ /** Whether dynamic components should refetch on data events by default. */
66
+ export function useRealtimeDefault(): boolean {
67
+ return useContext(RealtimeContext).defaultRealtime
68
+ }
69
+
70
+ export interface UseRealtimeOptions extends RealtimeSubscribeOptions {
71
+ /** Pass `host.realtime` / `api.realtime` to bypass the context. */
72
+ client?: RealtimeAPI | null
73
+ /** Set false to pause the subscription without unmounting. Default true. */
74
+ enabled?: boolean
75
+ }
76
+
77
+ /**
78
+ * Subscribe to data events for the given models. The handler is kept in a
79
+ * ref so callers don't need to memoise it; the subscription is torn down on
80
+ * unmount and re-created when models/events/client change.
81
+ *
82
+ * useRealtime({ models: ['SalesOrder'], client: host.realtime }, (e) => {
83
+ * if (e.action === 'resync' || e.id === currentId) refetch()
84
+ * })
85
+ */
86
+ export function useRealtime(options: UseRealtimeOptions, handler: DataEventHandler): void {
87
+ const client = useRealtimeClient(options.client)
88
+ const handlerRef = useRef(handler)
89
+ handlerRef.current = handler
90
+ const enabled = options.enabled ?? true
91
+ const modelsKey = (options.models ?? []).map((m) => m.trim().toLowerCase()).sort().join('|')
92
+ const eventsKey = (options.events ?? []).slice().sort().join('|')
93
+
94
+ useEffect(() => {
95
+ if (!client || !enabled || !modelsKey) return
96
+ const models = modelsKey.split('|')
97
+ const events = eventsKey ? (eventsKey.split('|') as DataEventAction[]) : undefined
98
+ let off: () => void = () => {}
99
+ try {
100
+ off = client.subscribe({ models, events }, (event) => handlerRef.current(event))
101
+ } catch {
102
+ /* a host client must not throw, but never take the page down */
103
+ }
104
+ return off
105
+ }, [client, enabled, modelsKey, eventsKey])
106
+ }
107
+
108
+ /** Live status of the realtime client ('closed' when none). */
109
+ export function useRealtimeStatus(explicit?: RealtimeAPI | null): RealtimeStatus {
110
+ const client = useRealtimeClient(explicit)
111
+ const [status, setStatus] = useState<RealtimeStatus>(() => client?.status() ?? 'closed')
112
+ useEffect(() => {
113
+ if (!client) {
114
+ setStatus('closed')
115
+ return
116
+ }
117
+ setStatus(client.status())
118
+ if (!client.onStatus) return
119
+ return client.onStatus(setStatus)
120
+ }, [client])
121
+ return status
122
+ }
123
+
124
+ /** Default matcher: any string segment of the query key (case-insensitive)
125
+ * equals the event's model, table or `addon.model`, or contains the model
126
+ * as a path segment (`/data/sales_orders`). */
127
+ export function queryKeyMatchesEvent(queryKey: QueryKey, event: DataEvent): boolean {
128
+ const needles = [event.model, event.table ?? '', `${event.addon}.${event.model}`]
129
+ .map((s) => s.toLowerCase())
130
+ .filter(Boolean)
131
+ const parts = flattenKey(queryKey)
132
+ for (const part of parts) {
133
+ const p = part.toLowerCase()
134
+ for (const n of needles) {
135
+ if (p === n) return true
136
+ // '/data/sales_orders?x=1' or 'pos/SalesOrder' style segments.
137
+ const segs = p.split(/[/?&=]/)
138
+ if (segs.includes(n)) return true
139
+ }
140
+ }
141
+ return false
142
+ }
143
+
144
+ function flattenKey(key: QueryKey, out: string[] = [], depth = 0): string[] {
145
+ if (depth > 4) return out
146
+ for (const item of key as unknown[]) {
147
+ if (typeof item === 'string') out.push(item)
148
+ else if (Array.isArray(item)) flattenKey(item as QueryKey, out, depth + 1)
149
+ else if (item && typeof item === 'object') {
150
+ for (const v of Object.values(item as Record<string, unknown>)) {
151
+ if (typeof v === 'string') out.push(v)
152
+ }
153
+ }
154
+ }
155
+ return out
156
+ }
157
+
158
+ export interface UseRealtimeInvalidateOptions extends UseRealtimeOptions {
159
+ /** Override which queries a given event invalidates. Default: `queryKeyMatchesEvent`. */
160
+ match?: (queryKey: QueryKey, event: DataEvent) => boolean
161
+ /** Coalesce bursts before invalidating (ms). Default 250. */
162
+ debounceMs?: number
163
+ /** Observe each event after it was scheduled for invalidation. */
164
+ onEvent?: DataEventHandler
165
+ }
166
+
167
+ /**
168
+ * Invalidate every react-query query whose key mentions the event's model /
169
+ * table (see `queryKeyMatchesEvent`). Uses the host's QueryClient. Bursts
170
+ * are debounced so a coalesced frame storm becomes a single refetch round.
171
+ *
172
+ * useRealtimeInvalidate({ models: ['SalesOrder', 'sales_order_items'], client: host.realtime })
173
+ */
174
+ export function useRealtimeInvalidate(options: UseRealtimeInvalidateOptions): void {
175
+ const queryClient = useQueryClient()
176
+ const { match = queryKeyMatchesEvent, debounceMs = 250, onEvent } = options
177
+ const pendingRef = useRef<DataEvent[]>([])
178
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
179
+ const matchRef = useRef(match)
180
+ matchRef.current = match
181
+ const onEventRef = useRef(onEvent)
182
+ onEventRef.current = onEvent
183
+
184
+ const flush = useCallback(() => {
185
+ timerRef.current = null
186
+ const batch = pendingRef.current
187
+ pendingRef.current = []
188
+ if (batch.length === 0) return
189
+ void queryClient.invalidateQueries({
190
+ predicate: (query) => batch.some((event) => matchRef.current(query.queryKey, event)),
191
+ })
192
+ }, [queryClient])
193
+
194
+ useRealtime(options, (event) => {
195
+ pendingRef.current.push(event)
196
+ onEventRef.current?.(event)
197
+ if (timerRef.current) return
198
+ timerRef.current = setTimeout(flush, debounceMs)
199
+ })
200
+
201
+ useEffect(
202
+ () => () => {
203
+ if (timerRef.current) clearTimeout(timerRef.current)
204
+ },
205
+ [],
206
+ )
207
+ }
208
+
209
+ export interface UseRealtimeTickOptions {
210
+ /** Models/tables to watch. */
211
+ models: string[]
212
+ /** Explicit client (federated addons). */
213
+ client?: RealtimeAPI | null
214
+ /** Master switch — the caller resolves prop vs provider default. */
215
+ enabled: boolean
216
+ /** Coalesce bursts before ticking (ms). Default 300. */
217
+ debounceMs?: number
218
+ }
219
+
220
+ /**
221
+ * A counter that increments (debounced) whenever a data event lands for one
222
+ * of `models`. Fold it into a refetch effect's dependencies — that is exactly
223
+ * what `DynamicTable` / `DynamicKanban` do behind their `realtime` prop.
224
+ */
225
+ export function useRealtimeTick(options: UseRealtimeTickOptions): number {
226
+ const [tick, setTick] = useState(0)
227
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
228
+ const debounceMs = options.debounceMs ?? 300
229
+
230
+ useRealtime(
231
+ { models: options.models, client: options.client, enabled: options.enabled },
232
+ () => {
233
+ if (timerRef.current) return
234
+ timerRef.current = setTimeout(() => {
235
+ timerRef.current = null
236
+ setTick((t) => t + 1)
237
+ }, debounceMs)
238
+ },
239
+ )
240
+
241
+ useEffect(
242
+ () => () => {
243
+ if (timerRef.current) clearTimeout(timerRef.current)
244
+ },
245
+ [],
246
+ )
247
+ return tick
248
+ }