cortena-ui 1.4.2 → 1.6.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.
Files changed (138) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/LICENSE +7 -0
  3. package/README.md +235 -3
  4. package/dist/a2ui/views.js +2 -2
  5. package/dist/agent-chat/a2ui-block.d.ts +60 -0
  6. package/dist/agent-chat/a2ui-block.js +69 -0
  7. package/dist/agent-chat/a2ui-block.js.map +1 -0
  8. package/dist/agent-chat/agui-client.d.ts +40 -0
  9. package/dist/agent-chat/agui-client.js +251 -0
  10. package/dist/agent-chat/agui-client.js.map +1 -0
  11. package/dist/agent-chat/bridge.d.ts +109 -0
  12. package/dist/agent-chat/bridge.js +353 -0
  13. package/dist/agent-chat/bridge.js.map +1 -0
  14. package/dist/agent-chat/session.d.ts +79 -0
  15. package/dist/agent-chat/session.js +391 -0
  16. package/dist/agent-chat/session.js.map +1 -0
  17. package/dist/agent-chat/step-label.d.ts +99 -0
  18. package/dist/agent-chat/step-label.js +116 -0
  19. package/dist/agent-chat/step-label.js.map +1 -0
  20. package/dist/agent-chat/store.d.ts +102 -0
  21. package/dist/agent-chat/store.js +876 -0
  22. package/dist/agent-chat/store.js.map +1 -0
  23. package/dist/agent-chat/types.d.ts +277 -0
  24. package/dist/agent-chat/types.js +17 -0
  25. package/dist/agent-chat/types.js.map +1 -0
  26. package/dist/agent-chat.d.ts +11 -0
  27. package/dist/agent-chat.js +11 -0
  28. package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
  29. package/dist/components/admin-permissions/admin-permissions.js +101 -0
  30. package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
  31. package/dist/components/admin-permissions/context.d.ts +70 -0
  32. package/dist/components/admin-permissions/context.js +258 -0
  33. package/dist/components/admin-permissions/context.js.map +1 -0
  34. package/dist/components/admin-permissions/index.d.ts +10 -0
  35. package/dist/components/admin-permissions/licence.d.ts +15 -0
  36. package/dist/components/admin-permissions/licence.js +78 -0
  37. package/dist/components/admin-permissions/licence.js.map +1 -0
  38. package/dist/components/admin-permissions/matrix.d.ts +20 -0
  39. package/dist/components/admin-permissions/matrix.js +191 -0
  40. package/dist/components/admin-permissions/matrix.js.map +1 -0
  41. package/dist/components/admin-permissions/members.d.ts +18 -0
  42. package/dist/components/admin-permissions/members.js +185 -0
  43. package/dist/components/admin-permissions/members.js.map +1 -0
  44. package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
  45. package/dist/components/admin-permissions/role-assignment.js +174 -0
  46. package/dist/components/admin-permissions/role-assignment.js.map +1 -0
  47. package/dist/components/admin-permissions/roles.d.ts +25 -0
  48. package/dist/components/admin-permissions/roles.js +168 -0
  49. package/dist/components/admin-permissions/roles.js.map +1 -0
  50. package/dist/components/admin-permissions/types.d.ts +152 -0
  51. package/dist/components/admin-permissions/types.js +63 -0
  52. package/dist/components/admin-permissions/types.js.map +1 -0
  53. package/dist/components/agent-chat-popup.d.ts +29 -0
  54. package/dist/components/agent-chat-popup.js +188 -0
  55. package/dist/components/agent-chat-popup.js.map +1 -0
  56. package/dist/components/agent-chat.d.ts +163 -0
  57. package/dist/components/agent-chat.js +673 -0
  58. package/dist/components/agent-chat.js.map +1 -0
  59. package/dist/components/app-shell.d.ts +126 -0
  60. package/dist/components/app-shell.js +297 -0
  61. package/dist/components/app-shell.js.map +1 -0
  62. package/dist/components/badge.d.ts +1 -1
  63. package/dist/components/button-link.js +1 -1
  64. package/dist/components/button.d.ts +1 -1
  65. package/dist/components/checkbox.d.ts +1 -1
  66. package/dist/components/combobox.d.ts +1 -1
  67. package/dist/components/combobox.js +1 -1
  68. package/dist/components/consent-screen.d.ts +65 -0
  69. package/dist/components/consent-screen.js +123 -0
  70. package/dist/components/consent-screen.js.map +1 -0
  71. package/dist/components/data-table/data-table.d.ts +15 -1
  72. package/dist/components/data-table/data-table.js +18 -4
  73. package/dist/components/data-table/data-table.js.map +1 -1
  74. package/dist/components/data-table/index.d.ts +4 -4
  75. package/dist/components/data-table/parts.d.ts +27 -3
  76. package/dist/components/data-table/parts.js +175 -55
  77. package/dist/components/data-table/parts.js.map +1 -1
  78. package/dist/components/data-table/types.d.ts +61 -0
  79. package/dist/components/data-table/use-data-table.js +91 -6
  80. package/dist/components/data-table/use-data-table.js.map +1 -1
  81. package/dist/components/data-table/use-server-source.js +119 -28
  82. package/dist/components/data-table/use-server-source.js.map +1 -1
  83. package/dist/components/help-panel.d.ts +131 -0
  84. package/dist/components/help-panel.js +545 -0
  85. package/dist/components/help-panel.js.map +1 -0
  86. package/dist/components/login-screen.d.ts +127 -0
  87. package/dist/components/login-screen.js +339 -0
  88. package/dist/components/login-screen.js.map +1 -0
  89. package/dist/components/session-guard.d.ts +268 -0
  90. package/dist/components/session-guard.js +632 -0
  91. package/dist/components/session-guard.js.map +1 -0
  92. package/dist/components/toast.d.ts +1 -1
  93. package/dist/core.d.ts +5 -1
  94. package/dist/core.js +11 -7
  95. package/dist/data-table.d.ts +13 -4
  96. package/dist/data-table.js +10 -2
  97. package/dist/hooks/use-cortena-theme.js +49 -3
  98. package/dist/hooks/use-cortena-theme.js.map +1 -1
  99. package/dist/index.d.ts +17 -4
  100. package/dist/index.js +21 -8
  101. package/dist/markdown.d.ts +2 -1
  102. package/dist/markdown.js +2 -1
  103. package/package.json +18 -5
  104. package/src/agent-chat/a2ui-block.ts +118 -0
  105. package/src/agent-chat/agui-client.ts +405 -0
  106. package/src/agent-chat/bridge.ts +445 -0
  107. package/src/agent-chat/session.ts +549 -0
  108. package/src/agent-chat/step-label.ts +177 -0
  109. package/src/agent-chat/store.ts +1234 -0
  110. package/src/agent-chat/types.ts +308 -0
  111. package/src/components/admin-permissions/admin-permissions.tsx +130 -0
  112. package/src/components/admin-permissions/context.tsx +376 -0
  113. package/src/components/admin-permissions/index.tsx +32 -0
  114. package/src/components/admin-permissions/licence.tsx +84 -0
  115. package/src/components/admin-permissions/matrix.tsx +257 -0
  116. package/src/components/admin-permissions/members.tsx +204 -0
  117. package/src/components/admin-permissions/role-assignment.tsx +239 -0
  118. package/src/components/admin-permissions/roles.tsx +169 -0
  119. package/src/components/admin-permissions/types.ts +231 -0
  120. package/src/components/agent-chat-popup.tsx +289 -0
  121. package/src/components/agent-chat.tsx +1006 -0
  122. package/src/components/app-shell.tsx +502 -0
  123. package/src/components/consent-screen.tsx +239 -0
  124. package/src/components/data-table/data-table.tsx +36 -0
  125. package/src/components/data-table/index.tsx +6 -1
  126. package/src/components/data-table/parts.tsx +223 -47
  127. package/src/components/data-table/types.ts +68 -0
  128. package/src/components/data-table/use-data-table.ts +152 -4
  129. package/src/components/data-table/use-server-source.ts +150 -12
  130. package/src/components/help-panel.tsx +765 -0
  131. package/src/components/login-screen.tsx +479 -0
  132. package/src/components/session-guard.tsx +1071 -0
  133. package/src/entries/agent-chat.ts +137 -0
  134. package/src/entries/core.ts +8 -0
  135. package/src/entries/data-table.ts +41 -0
  136. package/src/entries/markdown.ts +25 -0
  137. package/src/hooks/use-cortena-theme.ts +63 -4
  138. package/src/index.ts +6 -0
@@ -7,6 +7,50 @@ import * as React from "react";
7
7
  import { useTable } from "@tanstack/react-table";
8
8
  //#region src/components/data-table/use-data-table.ts
9
9
  const EMPTY_ROWS = [];
10
+ /** The floor on `autoRefresh.intervalMs`; anything shorter is load, not freshness. */
11
+ const AUTO_REFRESH_MIN_MS = 3e4;
12
+ /** Used when `autoRefresh` names no interval. */
13
+ const AUTO_REFRESH_DEFAULT_MS = 6e4;
14
+ /** `cortena.data-table.auto-refresh.<stateKey>` holds `"on"` or `"off"`. */
15
+ const AUTO_REFRESH_STORAGE_PREFIX = "cortena.data-table.auto-refresh.";
16
+ /** One warning per process, not one per render. */
17
+ const warned = { missingStateKey: false };
18
+ function readStoredFlag(key) {
19
+ try {
20
+ return window.localStorage.getItem(key) === "on";
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+ /**
26
+ * The user's auto-refresh choice, remembered per table.
27
+ *
28
+ * It starts off and is read from storage in an effect rather than in the
29
+ * initial state, so a server-rendered table and its first client render agree
30
+ * — a switch that hydrates in the opposite position is a React mismatch and,
31
+ * worse, a table that starts polling before the page is interactive.
32
+ */
33
+ function usePersistedFlag(storageKey) {
34
+ const [on, setOn] = React.useState(false);
35
+ React.useEffect(() => {
36
+ setOn(storageKey ? readStoredFlag(storageKey) : false);
37
+ }, [storageKey]);
38
+ return [on, React.useCallback((next) => {
39
+ setOn(next);
40
+ if (!storageKey) return;
41
+ try {
42
+ window.localStorage.setItem(storageKey, next ? "on" : "off");
43
+ } catch {}
44
+ }, [storageKey])];
45
+ }
46
+ const subscribeVisibility = (onChange) => {
47
+ document.addEventListener("visibilitychange", onChange);
48
+ return () => document.removeEventListener("visibilitychange", onChange);
49
+ };
50
+ /** True while the tab is in the background. Polling a tab nobody is looking at is a battery bill. */
51
+ function useDocumentHidden() {
52
+ return React.useSyncExternalStore(subscribeVisibility, () => document.visibilityState === "hidden", () => false);
53
+ }
10
54
  function useDebounced(value, ms) {
11
55
  const [debounced, setDebounced] = React.useState(value);
12
56
  React.useEffect(() => {
@@ -32,7 +76,7 @@ function useDebounced(value, ms) {
32
76
  * whole instance to `<DataTable instance={...} />`.
33
77
  */
34
78
  function useDataTable(options) {
35
- const { columns, dataSource, getRowId, getSubRows, pagination: paginationMode = "pages", pageSize = 25, enableRowSelection = false, enableExpanding = false, getRowCanExpand, enableSorting = true, enableMultiSort = false, enableGlobalFilter = true, enableColumnFilters = true, enableHiding = true, enableColumnPinning = true, enableColumnResizing = false, initialState, onCellCommit, onRowSelectionChange, searchDebounceMs = 250 } = options;
79
+ const { columns, dataSource, getRowId, getSubRows, pagination: paginationMode = "pages", pageSize = 25, enableRowSelection = false, enableExpanding = false, getRowCanExpand, enableSorting = true, enableMultiSort = false, enableGlobalFilter = true, enableColumnFilters = true, enableHiding = true, enableColumnPinning = true, enableColumnResizing = false, initialState, onCellCommit, onRowSelectionChange, searchDebounceMs = 250, autoRefresh, stateKey } = options;
36
80
  const isServer = dataSource.kind === "server";
37
81
  const [globalFilter, setGlobalFilter] = React.useState(initialState?.globalFilter ?? "");
38
82
  const [columnFilters, setColumnFilters] = React.useState(initialState?.columnFilters ?? []);
@@ -41,6 +85,8 @@ function useDataTable(options) {
41
85
  pageIndex: 0,
42
86
  pageSize
43
87
  });
88
+ const [rowSelection, setRowSelection] = React.useState(initialState?.rowSelection ?? {});
89
+ const [editingCell, setEditingCell] = React.useState(null);
44
90
  const searchQuery = useDebounced(globalFilter, isServer ? searchDebounceMs : 0);
45
91
  const scopeKey = JSON.stringify([
46
92
  searchQuery,
@@ -69,7 +115,21 @@ function useDataTable(options) {
69
115
  pagination.pageIndex,
70
116
  pagination.pageSize
71
117
  ]);
72
- const server = useServerSource(isServer ? dataSource : void 0, query, paginationMode);
118
+ const wantsAuto = isServer && autoRefresh !== void 0;
119
+ React.useEffect(() => {
120
+ if (!wantsAuto || stateKey || warned.missingStateKey) return;
121
+ warned.missingStateKey = true;
122
+ console.warn("[cortena-ui] DataTable: `autoRefresh` needs a `stateKey`. Without one the user's on/off choice cannot be remembered between visits, and two tables on one screen would share the same setting.");
123
+ }, [wantsAuto, stateKey]);
124
+ const [autoEnabled, setAutoEnabled] = usePersistedFlag(wantsAuto && stateKey ? AUTO_REFRESH_STORAGE_PREFIX + stateKey : null);
125
+ const hidden = useDocumentHidden();
126
+ const baseIntervalMs = Math.max(autoRefresh?.intervalMs ?? 6e4, AUTO_REFRESH_MIN_MS);
127
+ const pausedBy = hidden ? "hidden" : editingCell ? "editing" : Object.keys(rowSelection).length > 0 ? "selection" : autoRefresh?.paused ? "app" : null;
128
+ const autoActive = wantsAuto && autoEnabled && pausedBy === null;
129
+ const server = useServerSource(isServer ? dataSource : void 0, query, paginationMode, wantsAuto ? {
130
+ intervalMs: baseIntervalMs,
131
+ active: autoActive
132
+ } : void 0);
73
133
  const data = isServer ? server.rows : dataSource.rows;
74
134
  const resolvedColumns = React.useMemo(() => {
75
135
  const helper = createColumnHelper$1();
@@ -105,12 +165,14 @@ function useDataTable(options) {
105
165
  globalFilter,
106
166
  columnFilters,
107
167
  sorting,
108
- pagination
168
+ pagination,
169
+ rowSelection
109
170
  },
110
171
  onGlobalFilterChange: setGlobalFilter,
111
172
  onColumnFiltersChange: setColumnFilters,
112
173
  onSortingChange: setSorting,
113
174
  onPaginationChange: setPagination,
175
+ onRowSelectionChange: setRowSelection,
114
176
  manualFiltering: isServer,
115
177
  manualSorting: isServer,
116
178
  manualPagination: isServer || paginationMode !== "pages",
@@ -128,7 +190,6 @@ function useDataTable(options) {
128
190
  columnResizeMode: "onChange",
129
191
  globalFilterFn: "search"
130
192
  });
131
- const rowSelection = table.state.rowSelection;
132
193
  const onSelectionRef = React.useRef(onRowSelectionChange);
133
194
  onSelectionRef.current = onRowSelectionChange;
134
195
  const firstSelection = React.useRef(true);
@@ -139,7 +200,6 @@ function useDataTable(options) {
139
200
  }
140
201
  onSelectionRef.current?.(table.getSelectedRowModel().rows.map((r) => r.original));
141
202
  }, [rowSelection]);
142
- const [editingCell, setEditingCell] = React.useState(null);
143
203
  const commit = React.useCallback(async (row, columnId, value) => {
144
204
  await ((table.getColumn(columnId)?.columnDef.meta)?.onCommit ?? onCellCommit)?.(row.original, columnId, value);
145
205
  }, [table, onCellCommit]);
@@ -149,6 +209,28 @@ function useDataTable(options) {
149
209
  stop: () => setEditingCell(null),
150
210
  commit
151
211
  }), [editingCell, commit]);
212
+ const lastUpdatedRef = React.useRef(null);
213
+ if (server.lastUpdatedAt && server.lastUpdatedAt > (lastUpdatedRef.current ?? 0)) lastUpdatedRef.current = server.lastUpdatedAt;
214
+ const lastUpdatedAt = lastUpdatedRef.current;
215
+ const autoState = React.useMemo(() => wantsAuto ? {
216
+ enabled: autoEnabled,
217
+ setEnabled: setAutoEnabled,
218
+ active: autoActive && !server.mutating,
219
+ pausedBy: autoEnabled ? pausedBy ?? (server.mutating ? "mutation" : null) : null,
220
+ intervalMs: server.intervalMs === false ? baseIntervalMs : server.intervalMs,
221
+ baseIntervalMs,
222
+ backingOff: server.failures > 0
223
+ } : null, [
224
+ wantsAuto,
225
+ autoEnabled,
226
+ setAutoEnabled,
227
+ autoActive,
228
+ pausedBy,
229
+ server.mutating,
230
+ server.intervalMs,
231
+ server.failures,
232
+ baseIntervalMs
233
+ ]);
152
234
  return {
153
235
  table,
154
236
  dataSource,
@@ -162,10 +244,13 @@ function useDataTable(options) {
162
244
  fetchNextPage: server.fetchNextPage,
163
245
  hasNextPage: server.hasNextPage,
164
246
  searchQuery,
247
+ lastUpdatedAt,
248
+ lastErrorAt: server.lastErrorAt,
249
+ autoRefresh: autoState,
165
250
  editing
166
251
  };
167
252
  }
168
253
  //#endregion
169
- export { useDataTable };
254
+ export { AUTO_REFRESH_DEFAULT_MS, AUTO_REFRESH_MIN_MS, AUTO_REFRESH_STORAGE_PREFIX, useDataTable };
170
255
 
171
256
  //# sourceMappingURL=use-data-table.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-data-table.js","names":["createColumnHelper"],"sources":["../../../src/components/data-table/use-data-table.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useTable,\n type ColumnFiltersState,\n type PaginationState,\n type RowData,\n type SortingState,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { createColumnHelper, dataTableFeatures } from \"./features\";\nimport { expandColumn, selectColumn } from \"./system-columns\";\nimport type {\n DataSource,\n DataTableColumnDef,\n DataTableEditingCell,\n DataTableInstance,\n DataTableRow,\n DataTableTable,\n TableQuery,\n UseDataTableOptions,\n} from \"./types\";\nimport { useServerSource } from \"./use-server-source\";\n\nconst EMPTY_ROWS: never[] = [];\n\nfunction useDebounced<T>(value: T, ms: number): T {\n const [debounced, setDebounced] = React.useState(value);\n React.useEffect(() => {\n if (ms <= 0) {\n setDebounced(value);\n return;\n }\n const id = setTimeout(() => setDebounced(value), ms);\n return () => clearTimeout(id);\n }, [value, ms]);\n return ms <= 0 ? value : debounced;\n}\n\n/**\n * Builds the table for a `dataSource`. This is where the design decision's\n * \"the component sets the manual flags itself\" lives: a server source flips\n * `manualFiltering`, `manualSorting` and `manualPagination` and feeds the\n * table's own filter / sort / page state back out as a `TableQuery`; a client\n * source leaves every row model in memory. Everything else (selection,\n * expansion, visibility, pinning, sizing) is owned by the table.\n *\n * Use it directly when the default layout is not enough: pass the returned\n * `table` to `DataTableToolbar`, `DataTablePagination` and friends, or the\n * whole instance to `<DataTable instance={...} />`.\n */\nexport function useDataTable<Row extends RowData>(\n options: UseDataTableOptions<Row>,\n): DataTableInstance<Row> {\n const {\n columns,\n dataSource,\n getRowId,\n getSubRows,\n pagination: paginationMode = \"pages\",\n pageSize = 25,\n enableRowSelection = false,\n enableExpanding = false,\n getRowCanExpand,\n enableSorting = true,\n enableMultiSort = false,\n enableGlobalFilter = true,\n enableColumnFilters = true,\n enableHiding = true,\n enableColumnPinning = true,\n enableColumnResizing = false,\n initialState,\n onCellCommit,\n onRowSelectionChange,\n searchDebounceMs = 250,\n } = options;\n\n const isServer = dataSource.kind === \"server\";\n\n // Slices the server query is built from are React state; the table mirrors\n // them through `state` + `on*Change`, which v9 still supports alongside atoms.\n const [globalFilter, setGlobalFilter] = React.useState<string>(\n (initialState?.globalFilter as string | undefined) ?? \"\",\n );\n const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(\n initialState?.columnFilters ?? [],\n );\n const [sorting, setSorting] = React.useState<SortingState>(initialState?.sorting ?? []);\n const [pagination, setPagination] = React.useState<PaginationState>(\n initialState?.pagination ?? { pageIndex: 0, pageSize },\n );\n\n const searchQuery = useDebounced(globalFilter, isServer ? searchDebounceMs : 0);\n\n // A new search, filter or sort starts from page one again. Client sources\n // get this from `autoResetPageIndex`; manual pagination has to do it here.\n const scopeKey = JSON.stringify([searchQuery, columnFilters, sorting]);\n const lastScope = React.useRef(scopeKey);\n React.useEffect(() => {\n if (lastScope.current === scopeKey) return;\n lastScope.current = scopeKey;\n setPagination((p) => (p.pageIndex === 0 ? p : { ...p, pageIndex: 0 }));\n }, [scopeKey]);\n\n const query = React.useMemo<TableQuery>(\n () => ({\n // Trimmed for the same reason the client filter trims (features.ts): a\n // backend asked for `q: \" \"` returns nothing, and the user sees an empty\n // table with no explanation. Whitespace-only means no search term.\n q: searchQuery.trim() || undefined,\n filters: columnFilters.length ? columnFilters : undefined,\n sort: sorting.length ? sorting : undefined,\n page: pagination.pageIndex,\n pageSize: pagination.pageSize,\n }),\n [searchQuery, columnFilters, sorting, pagination.pageIndex, pagination.pageSize],\n );\n\n const server = useServerSource<Row>(isServer ? dataSource : undefined, query, paginationMode);\n\n const data: ReadonlyArray<Row> = isServer ? server.rows : dataSource.rows;\n\n // System columns first; then each consumer column with its meta defaults\n // applied (a faceted column filters with `oneOf` unless told otherwise).\n const resolvedColumns = React.useMemo(() => {\n const helper = createColumnHelper<Row>();\n const out: DataTableColumnDef<Row, any>[] = [];\n if (enableRowSelection && !columns.some((c) => c.id === \"select\")) out.push(selectColumn(helper));\n if (enableExpanding && !columns.some((c) => c.id === \"expand\")) out.push(expandColumn(helper));\n for (const column of columns) {\n const filter = column.meta?.filter;\n if (filter === \"faceted\" && column.filterFn === undefined) {\n out.push({ ...column, filterFn: \"oneOf\" });\n } else if (filter === \"text\" && column.filterFn === undefined) {\n out.push({ ...column, filterFn: \"includesString\" });\n } else {\n out.push(column);\n }\n }\n return out;\n }, [columns, enableRowSelection, enableExpanding]);\n\n const table: DataTableTable<Row> = useTable({\n features: dataTableFeatures<Row>(),\n columns: resolvedColumns,\n data: data.length ? data : EMPTY_ROWS,\n getRowId,\n getSubRows,\n initialState,\n state: { globalFilter, columnFilters, sorting, pagination },\n onGlobalFilterChange: setGlobalFilter,\n onColumnFiltersChange: setColumnFilters,\n onSortingChange: setSorting,\n onPaginationChange: setPagination,\n manualFiltering: isServer,\n manualSorting: isServer,\n manualPagination: isServer || paginationMode !== \"pages\",\n rowCount: isServer ? server.total : undefined,\n enableRowSelection,\n enableExpanding,\n getRowCanExpand: getRowCanExpand ?? (enableExpanding ? () => true : undefined),\n enableSorting,\n enableMultiSort,\n enableGlobalFilter,\n enableColumnFilters,\n enableHiding,\n enableColumnPinning,\n enableColumnResizing,\n columnResizeMode: \"onChange\",\n // `search`, not `includesString`: it trims the query, so a box holding only\n // whitespace filters nothing rather than emptying the grid. See features.ts.\n globalFilterFn: \"search\",\n });\n\n // Selection callback with original rows, for consumers that keep selection outside.\n const rowSelection = table.state.rowSelection;\n const onSelectionRef = React.useRef(onRowSelectionChange);\n onSelectionRef.current = onRowSelectionChange;\n const firstSelection = React.useRef(true);\n React.useEffect(() => {\n if (firstSelection.current) {\n firstSelection.current = false;\n return;\n }\n onSelectionRef.current?.(table.getSelectedRowModel().rows.map((r) => r.original));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rowSelection]);\n\n const [editingCell, setEditingCell] = React.useState<DataTableEditingCell | null>(null);\n const commit = React.useCallback(\n async (row: DataTableRow<Row>, columnId: string, value: unknown) => {\n const meta = table.getColumn(columnId)?.columnDef.meta;\n const handler = meta?.onCommit ?? onCellCommit;\n await handler?.(row.original, columnId, value);\n },\n [table, onCellCommit],\n );\n const editing = React.useMemo(\n () => ({\n cell: editingCell,\n start: (cell: DataTableEditingCell) => setEditingCell(cell),\n stop: () => setEditingCell(null),\n commit,\n }),\n [editingCell, commit],\n );\n\n const total = isServer\n ? (server.total ?? 0)\n : paginationMode === \"pages\"\n ? table.getPrePaginatedRowModel().rows.length\n : table.getRowModel().rows.length;\n\n return {\n table,\n dataSource: dataSource as DataSource<Row>,\n paginationMode,\n rows: data as Row[],\n total,\n isLoading: server.isLoading,\n isFetching: server.isFetching,\n error: server.error,\n refetch: server.refetch,\n fetchNextPage: server.fetchNextPage,\n hasNextPage: server.hasNextPage,\n searchQuery,\n editing,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-data-table.js","names":["createColumnHelper"],"sources":["../../../src/components/data-table/use-data-table.ts"],"sourcesContent":["\"use client\";\n\nimport {\n useTable,\n type ColumnFiltersState,\n type PaginationState,\n type RowData,\n type RowSelectionState,\n type SortingState,\n} from \"@tanstack/react-table\";\nimport * as React from \"react\";\nimport { createColumnHelper, dataTableFeatures } from \"./features\";\nimport { expandColumn, selectColumn } from \"./system-columns\";\nimport type {\n DataSource,\n DataTableAutoRefreshState,\n DataTableColumnDef,\n DataTableEditingCell,\n DataTableInstance,\n DataTableRefreshPause,\n DataTableRow,\n DataTableTable,\n TableQuery,\n UseDataTableOptions,\n} from \"./types\";\nimport { useServerSource } from \"./use-server-source\";\n\nconst EMPTY_ROWS: never[] = [];\n\n/** The floor on `autoRefresh.intervalMs`; anything shorter is load, not freshness. */\nexport const AUTO_REFRESH_MIN_MS = 30_000;\n/** Used when `autoRefresh` names no interval. */\nexport const AUTO_REFRESH_DEFAULT_MS = 60_000;\n/** `cortena.data-table.auto-refresh.<stateKey>` holds `\"on\"` or `\"off\"`. */\nexport const AUTO_REFRESH_STORAGE_PREFIX = \"cortena.data-table.auto-refresh.\";\n\n/** One warning per process, not one per render. */\nconst warned = { missingStateKey: false };\n\nfunction readStoredFlag(key: string): boolean {\n try {\n return window.localStorage.getItem(key) === \"on\";\n } catch {\n return false;\n }\n}\n\n/**\n * The user's auto-refresh choice, remembered per table.\n *\n * It starts off and is read from storage in an effect rather than in the\n * initial state, so a server-rendered table and its first client render agree\n * — a switch that hydrates in the opposite position is a React mismatch and,\n * worse, a table that starts polling before the page is interactive.\n */\nfunction usePersistedFlag(storageKey: string | null): [boolean, (on: boolean) => void] {\n const [on, setOn] = React.useState(false);\n React.useEffect(() => {\n setOn(storageKey ? readStoredFlag(storageKey) : false);\n }, [storageKey]);\n const set = React.useCallback(\n (next: boolean) => {\n setOn(next);\n if (!storageKey) return;\n try {\n window.localStorage.setItem(storageKey, next ? \"on\" : \"off\");\n } catch {\n /* Private mode, or storage full: the switch still works for this visit. */\n }\n },\n [storageKey],\n );\n return [on, set];\n}\n\nconst subscribeVisibility = (onChange: () => void) => {\n document.addEventListener(\"visibilitychange\", onChange);\n return () => document.removeEventListener(\"visibilitychange\", onChange);\n};\n\n/** True while the tab is in the background. Polling a tab nobody is looking at is a battery bill. */\nfunction useDocumentHidden(): boolean {\n return React.useSyncExternalStore(\n subscribeVisibility,\n () => document.visibilityState === \"hidden\",\n () => false,\n );\n}\n\nfunction useDebounced<T>(value: T, ms: number): T {\n const [debounced, setDebounced] = React.useState(value);\n React.useEffect(() => {\n if (ms <= 0) {\n setDebounced(value);\n return;\n }\n const id = setTimeout(() => setDebounced(value), ms);\n return () => clearTimeout(id);\n }, [value, ms]);\n return ms <= 0 ? value : debounced;\n}\n\n/**\n * Builds the table for a `dataSource`. This is where the design decision's\n * \"the component sets the manual flags itself\" lives: a server source flips\n * `manualFiltering`, `manualSorting` and `manualPagination` and feeds the\n * table's own filter / sort / page state back out as a `TableQuery`; a client\n * source leaves every row model in memory. Everything else (selection,\n * expansion, visibility, pinning, sizing) is owned by the table.\n *\n * Use it directly when the default layout is not enough: pass the returned\n * `table` to `DataTableToolbar`, `DataTablePagination` and friends, or the\n * whole instance to `<DataTable instance={...} />`.\n */\nexport function useDataTable<Row extends RowData>(\n options: UseDataTableOptions<Row>,\n): DataTableInstance<Row> {\n const {\n columns,\n dataSource,\n getRowId,\n getSubRows,\n pagination: paginationMode = \"pages\",\n pageSize = 25,\n enableRowSelection = false,\n enableExpanding = false,\n getRowCanExpand,\n enableSorting = true,\n enableMultiSort = false,\n enableGlobalFilter = true,\n enableColumnFilters = true,\n enableHiding = true,\n enableColumnPinning = true,\n enableColumnResizing = false,\n initialState,\n onCellCommit,\n onRowSelectionChange,\n searchDebounceMs = 250,\n autoRefresh,\n stateKey,\n } = options;\n\n const isServer = dataSource.kind === \"server\";\n\n // Slices the server query is built from are React state; the table mirrors\n // them through `state` + `on*Change`, which v9 still supports alongside atoms.\n const [globalFilter, setGlobalFilter] = React.useState<string>(\n (initialState?.globalFilter as string | undefined) ?? \"\",\n );\n const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>(\n initialState?.columnFilters ?? [],\n );\n const [sorting, setSorting] = React.useState<SortingState>(initialState?.sorting ?? []);\n const [pagination, setPagination] = React.useState<PaginationState>(\n initialState?.pagination ?? { pageIndex: 0, pageSize },\n );\n // Selection and the open editor are lifted for the same reason the query\n // slices are: auto-refresh has to know about them before the query for this\n // render is declared, and `table.state` only exists after `useTable`.\n const [rowSelection, setRowSelection] = React.useState<RowSelectionState>(\n initialState?.rowSelection ?? {},\n );\n const [editingCell, setEditingCell] = React.useState<DataTableEditingCell | null>(null);\n\n const searchQuery = useDebounced(globalFilter, isServer ? searchDebounceMs : 0);\n\n // A new search, filter or sort starts from page one again. Client sources\n // get this from `autoResetPageIndex`; manual pagination has to do it here.\n const scopeKey = JSON.stringify([searchQuery, columnFilters, sorting]);\n const lastScope = React.useRef(scopeKey);\n React.useEffect(() => {\n if (lastScope.current === scopeKey) return;\n lastScope.current = scopeKey;\n setPagination((p) => (p.pageIndex === 0 ? p : { ...p, pageIndex: 0 }));\n }, [scopeKey]);\n\n const query = React.useMemo<TableQuery>(\n () => ({\n // Trimmed for the same reason the client filter trims (features.ts): a\n // backend asked for `q: \" \"` returns nothing, and the user sees an empty\n // table with no explanation. Whitespace-only means no search term.\n q: searchQuery.trim() || undefined,\n filters: columnFilters.length ? columnFilters : undefined,\n sort: sorting.length ? sorting : undefined,\n page: pagination.pageIndex,\n pageSize: pagination.pageSize,\n }),\n [searchQuery, columnFilters, sorting, pagination.pageIndex, pagination.pageSize],\n );\n\n /* Freshness (DESIGN-D19 / P-39) -------------------------------------- */\n\n const wantsAuto = isServer && autoRefresh !== undefined;\n React.useEffect(() => {\n if (!wantsAuto || stateKey || warned.missingStateKey) return;\n warned.missingStateKey = true;\n console.warn(\n \"[cortena-ui] DataTable: `autoRefresh` needs a `stateKey`. Without one the \" +\n \"user's on/off choice cannot be remembered between visits, and two tables \" +\n \"on one screen would share the same setting.\",\n );\n }, [wantsAuto, stateKey]);\n const storageKey = wantsAuto && stateKey ? AUTO_REFRESH_STORAGE_PREFIX + stateKey : null;\n const [autoEnabled, setAutoEnabled] = usePersistedFlag(storageKey);\n const hidden = useDocumentHidden();\n const baseIntervalMs = Math.max(\n autoRefresh?.intervalMs ?? AUTO_REFRESH_DEFAULT_MS,\n AUTO_REFRESH_MIN_MS,\n );\n // The polite rules, in the order a user would explain them. Each one is a\n // reason the interval is not running, and the toolbar says which.\n const pausedBy: DataTableRefreshPause | null = hidden\n ? \"hidden\"\n : editingCell\n ? \"editing\"\n : Object.keys(rowSelection).length > 0\n ? \"selection\"\n : autoRefresh?.paused\n ? \"app\"\n : null;\n const autoActive = wantsAuto && autoEnabled && pausedBy === null;\n\n const server = useServerSource<Row>(\n isServer ? dataSource : undefined,\n query,\n paginationMode,\n wantsAuto ? { intervalMs: baseIntervalMs, active: autoActive } : undefined,\n );\n\n const data: ReadonlyArray<Row> = isServer ? server.rows : dataSource.rows;\n\n // System columns first; then each consumer column with its meta defaults\n // applied (a faceted column filters with `oneOf` unless told otherwise).\n const resolvedColumns = React.useMemo(() => {\n const helper = createColumnHelper<Row>();\n const out: DataTableColumnDef<Row, any>[] = [];\n if (enableRowSelection && !columns.some((c) => c.id === \"select\")) out.push(selectColumn(helper));\n if (enableExpanding && !columns.some((c) => c.id === \"expand\")) out.push(expandColumn(helper));\n for (const column of columns) {\n const filter = column.meta?.filter;\n if (filter === \"faceted\" && column.filterFn === undefined) {\n out.push({ ...column, filterFn: \"oneOf\" });\n } else if (filter === \"text\" && column.filterFn === undefined) {\n out.push({ ...column, filterFn: \"includesString\" });\n } else {\n out.push(column);\n }\n }\n return out;\n }, [columns, enableRowSelection, enableExpanding]);\n\n const table: DataTableTable<Row> = useTable({\n features: dataTableFeatures<Row>(),\n columns: resolvedColumns,\n data: data.length ? data : EMPTY_ROWS,\n getRowId,\n getSubRows,\n initialState,\n state: { globalFilter, columnFilters, sorting, pagination, rowSelection },\n onGlobalFilterChange: setGlobalFilter,\n onColumnFiltersChange: setColumnFilters,\n onSortingChange: setSorting,\n onPaginationChange: setPagination,\n onRowSelectionChange: setRowSelection,\n manualFiltering: isServer,\n manualSorting: isServer,\n manualPagination: isServer || paginationMode !== \"pages\",\n rowCount: isServer ? server.total : undefined,\n enableRowSelection,\n enableExpanding,\n getRowCanExpand: getRowCanExpand ?? (enableExpanding ? () => true : undefined),\n enableSorting,\n enableMultiSort,\n enableGlobalFilter,\n enableColumnFilters,\n enableHiding,\n enableColumnPinning,\n enableColumnResizing,\n columnResizeMode: \"onChange\",\n // `search`, not `includesString`: it trims the query, so a box holding only\n // whitespace filters nothing rather than emptying the grid. See features.ts.\n globalFilterFn: \"search\",\n });\n\n // Selection callback with original rows, for consumers that keep selection outside.\n const onSelectionRef = React.useRef(onRowSelectionChange);\n onSelectionRef.current = onRowSelectionChange;\n const firstSelection = React.useRef(true);\n React.useEffect(() => {\n if (firstSelection.current) {\n firstSelection.current = false;\n return;\n }\n onSelectionRef.current?.(table.getSelectedRowModel().rows.map((r) => r.original));\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [rowSelection]);\n\n const commit = React.useCallback(\n async (row: DataTableRow<Row>, columnId: string, value: unknown) => {\n const meta = table.getColumn(columnId)?.columnDef.meta;\n const handler = meta?.onCommit ?? onCellCommit;\n await handler?.(row.original, columnId, value);\n },\n [table, onCellCommit],\n );\n const editing = React.useMemo(\n () => ({\n cell: editingCell,\n start: (cell: DataTableEditingCell) => setEditingCell(cell),\n stop: () => setEditingCell(null),\n commit,\n }),\n [editingCell, commit],\n );\n\n // The stamp keeps the last real response time: with keepPreviousData a page\n // change puts a placeholder on screen whose `dataUpdatedAt` is zero, and the\n // stamp must not blink out while the next page loads.\n const lastUpdatedRef = React.useRef<number | null>(null);\n if (server.lastUpdatedAt && server.lastUpdatedAt > (lastUpdatedRef.current ?? 0)) {\n lastUpdatedRef.current = server.lastUpdatedAt;\n }\n const lastUpdatedAt = lastUpdatedRef.current;\n\n const autoState = React.useMemo<DataTableAutoRefreshState | null>(\n () =>\n wantsAuto\n ? {\n enabled: autoEnabled,\n setEnabled: setAutoEnabled,\n active: autoActive && !server.mutating,\n // A mutation in flight is the one pause only the query layer can\n // see; it is folded in here so the toolbar can still say why.\n pausedBy: autoEnabled ? (pausedBy ?? (server.mutating ? \"mutation\" : null)) : null,\n intervalMs: server.intervalMs === false ? baseIntervalMs : server.intervalMs,\n baseIntervalMs,\n backingOff: server.failures > 0,\n }\n : null,\n [\n wantsAuto,\n autoEnabled,\n setAutoEnabled,\n autoActive,\n pausedBy,\n server.mutating,\n server.intervalMs,\n server.failures,\n baseIntervalMs,\n ],\n );\n\n const total = isServer\n ? (server.total ?? 0)\n : paginationMode === \"pages\"\n ? table.getPrePaginatedRowModel().rows.length\n : table.getRowModel().rows.length;\n\n return {\n table,\n dataSource: dataSource as DataSource<Row>,\n paginationMode,\n rows: data as Row[],\n total,\n isLoading: server.isLoading,\n isFetching: server.isFetching,\n error: server.error,\n refetch: server.refetch,\n fetchNextPage: server.fetchNextPage,\n hasNextPage: server.hasNextPage,\n searchQuery,\n lastUpdatedAt,\n lastErrorAt: server.lastErrorAt,\n autoRefresh: autoState,\n editing,\n };\n}\n"],"mappings":"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"}
@@ -1,8 +1,41 @@
1
1
  "use client";
2
2
  "use client";
3
3
  import * as React from "react";
4
- import { QueryClientContext, keepPreviousData, useInfiniteQuery, useQuery } from "@tanstack/react-query";
4
+ import { QueryClientContext, keepPreviousData, useInfiniteQuery, useIsMutating, useQuery } from "@tanstack/react-query";
5
5
  //#region src/components/data-table/use-server-source.ts
6
+ /** Errors stretch the interval rather than hammering a failing endpoint. */
7
+ const AUTO_REFRESH_MAX_MS = 3e5;
8
+ function backoffOf(base, failures) {
9
+ return Math.min(base * 2 ** failures, AUTO_REFRESH_MAX_MS);
10
+ }
11
+ /**
12
+ * Consecutive failures, counted from the timestamps the source reports: one
13
+ * more for every new error, back to zero on any success.
14
+ *
15
+ * It is state, and it is read *before* the query is declared, so the interval
16
+ * handed to `refetchInterval` on the next render already carries the back-off.
17
+ * The timestamps only exist after the query, hence the ref: the effect runs
18
+ * after the commit, by which time the render has filled it in.
19
+ */
20
+ function useFailureCount(stamps) {
21
+ const [failures, setFailures] = React.useState(0);
22
+ const seen = React.useRef({
23
+ ok: null,
24
+ err: null
25
+ });
26
+ React.useEffect(() => {
27
+ const { ok, err } = stamps.current;
28
+ if (err !== null && err !== seen.current.err) {
29
+ seen.current.err = err;
30
+ setFailures((n) => n + 1);
31
+ }
32
+ if (ok !== null && ok !== seen.current.ok) {
33
+ seen.current.ok = ok;
34
+ setFailures(0);
35
+ }
36
+ });
37
+ return failures;
38
+ }
6
39
  const EMPTY = [];
7
40
  function toError(value) {
8
41
  return value instanceof Error ? value : new Error(String(value));
@@ -24,16 +57,23 @@ function scopeOf(query) {
24
57
  * mid-life would change the hook order, which React forbids, and is not a
25
58
  * real scenario.
26
59
  */
27
- function useServerSource(source, query, mode) {
60
+ function useServerSource(source, query, mode, auto) {
28
61
  const client = React.useContext(QueryClientContext);
29
62
  const [strategy] = React.useState(() => client ? "query" : "local");
30
- return strategy === "query" ? useQueryStrategy(source, query, mode) : useLocalStrategy(source, query, mode);
63
+ return strategy === "query" ? useQueryStrategy(source, query, mode, auto) : useLocalStrategy(source, query, mode, auto);
31
64
  }
32
- function useQueryStrategy(source, query, mode) {
65
+ function useQueryStrategy(source, query, mode, auto) {
33
66
  const id = React.useId();
34
67
  const base = source?.queryKey ?? ["cortena-data-table", id];
35
68
  const infinite = mode === "infinite";
36
69
  const fetch = source?.fetch;
70
+ const mutating = useIsMutating() > 0;
71
+ const stamps = React.useRef({
72
+ ok: null,
73
+ err: null
74
+ });
75
+ const failures = useFailureCount(stamps);
76
+ const refetchInterval = auto && auto.active && !mutating ? backoffOf(auto.intervalMs, failures) : false;
37
77
  const page = useQuery({
38
78
  queryKey: [
39
79
  ...base,
@@ -42,7 +82,8 @@ function useQueryStrategy(source, query, mode) {
42
82
  ],
43
83
  queryFn: ({ signal }) => fetch(query, signal),
44
84
  enabled: Boolean(fetch) && !infinite,
45
- placeholderData: keepPreviousData
85
+ placeholderData: keepPreviousData,
86
+ refetchInterval: infinite ? false : refetchInterval
46
87
  });
47
88
  const scope = scopeOf(query);
48
89
  const pages = useInfiniteQuery({
@@ -60,8 +101,16 @@ function useQueryStrategy(source, query, mode) {
60
101
  return all.reduce((n, p) => n + p.rows.length, 0) < last.total && last.rows.length > 0 ? all.length : void 0;
61
102
  },
62
103
  enabled: Boolean(fetch) && infinite,
63
- placeholderData: keepPreviousData
104
+ placeholderData: keepPreviousData,
105
+ refetchInterval: infinite ? refetchInterval : false
64
106
  });
107
+ const live = infinite ? pages : page;
108
+ const lastUpdatedAt = live.dataUpdatedAt || null;
109
+ const lastErrorAt = live.errorUpdatedAt || null;
110
+ stamps.current = {
111
+ ok: lastUpdatedAt,
112
+ err: lastErrorAt
113
+ };
65
114
  const flatRows = React.useMemo(() => pages.data ? pages.data.pages.flatMap((p) => p.rows) : EMPTY, [pages.data]);
66
115
  if (!fetch) return {
67
116
  rows: EMPTY,
@@ -71,7 +120,12 @@ function useQueryStrategy(source, query, mode) {
71
120
  error: null,
72
121
  refetch: () => {},
73
122
  fetchNextPage: () => {},
74
- hasNextPage: false
123
+ hasNextPage: false,
124
+ lastUpdatedAt: null,
125
+ lastErrorAt: null,
126
+ failures: 0,
127
+ intervalMs: false,
128
+ mutating
75
129
  };
76
130
  if (infinite) return {
77
131
  rows: flatRows,
@@ -83,7 +137,12 @@ function useQueryStrategy(source, query, mode) {
83
137
  fetchNextPage: () => {
84
138
  if (pages.hasNextPage && !pages.isFetchingNextPage) pages.fetchNextPage();
85
139
  },
86
- hasNextPage: pages.hasNextPage
140
+ hasNextPage: pages.hasNextPage,
141
+ lastUpdatedAt,
142
+ lastErrorAt,
143
+ failures,
144
+ intervalMs: refetchInterval,
145
+ mutating
87
146
  };
88
147
  return {
89
148
  rows: page.data?.rows ?? EMPTY,
@@ -93,10 +152,15 @@ function useQueryStrategy(source, query, mode) {
93
152
  error: page.error ? toError(page.error) : null,
94
153
  refetch: () => void page.refetch(),
95
154
  fetchNextPage: () => {},
96
- hasNextPage: false
155
+ hasNextPage: false,
156
+ lastUpdatedAt,
157
+ lastErrorAt,
158
+ failures,
159
+ intervalMs: refetchInterval,
160
+ mutating
97
161
  };
98
162
  }
99
- function useLocalStrategy(source, query, mode) {
163
+ function useLocalStrategy(source, query, mode, auto) {
100
164
  const infinite = mode === "infinite";
101
165
  const fetch = source?.fetch;
102
166
  const scope = scopeOf(query);
@@ -111,12 +175,17 @@ function useLocalStrategy(source, query, mode) {
111
175
  pages: [],
112
176
  scopeKey,
113
177
  page: -1,
178
+ tick: -1,
114
179
  isFetching: false,
115
- error: null
180
+ error: null,
181
+ updatedAt: null,
182
+ errorAt: null
116
183
  });
117
184
  const loadedForScope = state.scopeKey === scopeKey ? state.pages : EMPTY;
118
- const targetPage = infinite ? loadedForScope.length : query.page;
119
- const needs = infinite ? loadedForScope.length < wantedPages : !(state.scopeKey === scopeKey && state.page === query.page && state.pages.length > 0);
185
+ const stale = state.tick !== tick;
186
+ const restart = stale || state.scopeKey !== scopeKey;
187
+ const targetPage = infinite ? restart ? 0 : loadedForScope.length : query.page;
188
+ const needs = stale || (infinite ? loadedForScope.length < wantedPages : !(state.scopeKey === scopeKey && state.page === query.page && state.pages.length > 0));
120
189
  const requestKey = `${scopeKey}|${targetPage}|${tick}`;
121
190
  const fetchRef = React.useRef(fetch);
122
191
  React.useEffect(() => {
@@ -137,18 +206,23 @@ function useLocalStrategy(source, query, mode) {
137
206
  }, controller.signal).then((result) => {
138
207
  if (controller.signal.aborted) return;
139
208
  setState((s) => ({
140
- pages: infinite && s.scopeKey === scopeKey ? [...s.pages, result] : [result],
209
+ pages: infinite && !restart && s.scopeKey === scopeKey ? [...s.pages, result] : [result],
141
210
  scopeKey,
142
211
  page: targetPage,
212
+ tick,
143
213
  isFetching: false,
144
- error: null
214
+ error: null,
215
+ updatedAt: Date.now(),
216
+ errorAt: s.errorAt
145
217
  }));
146
218
  }, (err) => {
147
219
  if (controller.signal.aborted) return;
148
220
  setState((s) => ({
149
221
  ...s,
222
+ tick,
150
223
  isFetching: false,
151
- error: toError(err)
224
+ error: toError(err),
225
+ errorAt: Date.now()
152
226
  }));
153
227
  });
154
228
  return () => controller.abort();
@@ -157,6 +231,21 @@ function useLocalStrategy(source, query, mode) {
157
231
  requestKey,
158
232
  needs
159
233
  ]);
234
+ const stamps = React.useRef({
235
+ ok: null,
236
+ err: null
237
+ });
238
+ stamps.current = {
239
+ ok: state.updatedAt,
240
+ err: state.errorAt
241
+ };
242
+ const failures = useFailureCount(stamps);
243
+ const intervalMs = auto && auto.active ? backoffOf(auto.intervalMs, failures) : false;
244
+ React.useEffect(() => {
245
+ if (intervalMs === false) return;
246
+ const id = setInterval(() => setTick((t) => t + 1), intervalMs);
247
+ return () => clearInterval(id);
248
+ }, [intervalMs]);
160
249
  const rows = React.useMemo(() => infinite ? state.pages.flatMap((p) => p.rows) : state.pages[0]?.rows ?? EMPTY, [state.pages, infinite]);
161
250
  const total = state.pages.at(-1)?.total;
162
251
  const hasNextPage = infinite && total !== void 0 && rows.length < total;
@@ -168,7 +257,12 @@ function useLocalStrategy(source, query, mode) {
168
257
  error: null,
169
258
  refetch: () => {},
170
259
  fetchNextPage: () => {},
171
- hasNextPage: false
260
+ hasNextPage: false,
261
+ lastUpdatedAt: null,
262
+ lastErrorAt: null,
263
+ failures: 0,
264
+ intervalMs: false,
265
+ mutating: false
172
266
  };
173
267
  return {
174
268
  rows,
@@ -176,25 +270,22 @@ function useLocalStrategy(source, query, mode) {
176
270
  isLoading: state.pages.length === 0 && !state.error,
177
271
  isFetching: state.isFetching,
178
272
  error: state.error,
179
- refetch: () => {
180
- setState((s) => ({
181
- ...s,
182
- pages: [],
183
- page: -1,
184
- error: null
185
- }));
186
- setTick((t) => t + 1);
187
- },
273
+ refetch: () => setTick((t) => t + 1),
188
274
  fetchNextPage: () => {
189
275
  if (hasNextPage && !state.isFetching) setWanted({
190
276
  scopeKey,
191
277
  n: loadedForScope.length + 1
192
278
  });
193
279
  },
194
- hasNextPage
280
+ hasNextPage,
281
+ lastUpdatedAt: state.updatedAt,
282
+ lastErrorAt: state.errorAt,
283
+ failures,
284
+ intervalMs,
285
+ mutating: false
195
286
  };
196
287
  }
197
288
  //#endregion
198
- export { useServerSource };
289
+ export { AUTO_REFRESH_MAX_MS, useServerSource };
199
290
 
200
291
  //# sourceMappingURL=use-server-source.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"use-server-source.js","names":[],"sources":["../../../src/components/data-table/use-server-source.ts"],"sourcesContent":["\"use client\";\n\nimport {\n keepPreviousData,\n QueryClientContext,\n useInfiniteQuery,\n useQuery,\n} from \"@tanstack/react-query\";\nimport * as React from \"react\";\nimport type { ServerSource, TableQuery, TableResult } from \"./types\";\n\nexport interface ServerSourceState<Row> {\n rows: Row[];\n total: number | undefined;\n isLoading: boolean;\n isFetching: boolean;\n error: Error | null;\n refetch: () => void;\n fetchNextPage: () => void;\n hasNextPage: boolean;\n}\n\nconst EMPTY: never[] = [];\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\n/** The part of a query that identifies a result set; the page is separate so infinite mode can append. */\nfunction scopeOf(query: TableQuery): Omit<TableQuery, \"page\"> {\n const { page: _page, ...scope } = query;\n return scope;\n}\n\n/**\n * Runs a server source. With a `QueryClientProvider` above the table, pages\n * go through TanStack Query: cached by query, previous page kept on screen\n * while the next loads, and the in-flight request aborted through `signal`\n * when the query changes. Without one, a small local fetch state does the\n * same minus the cache, so the table never requires the optional peer to be\n * wired up.\n *\n * The strategy is chosen once per mount: a provider appearing or vanishing\n * mid-life would change the hook order, which React forbids, and is not a\n * real scenario.\n */\nexport function useServerSource<Row>(\n source: ServerSource<Row> | undefined,\n query: TableQuery,\n mode: \"pages\" | \"infinite\" | \"none\",\n): ServerSourceState<Row> {\n const client = React.useContext(QueryClientContext);\n const [strategy] = React.useState<\"query\" | \"local\">(() => (client ? \"query\" : \"local\"));\n // Hook order is stable for the life of the component because `strategy` is.\n return strategy === \"query\"\n ? useQueryStrategy(source, query, mode)\n : useLocalStrategy(source, query, mode);\n}\n\nfunction useQueryStrategy<Row>(\n source: ServerSource<Row> | undefined,\n query: TableQuery,\n mode: \"pages\" | \"infinite\" | \"none\",\n): ServerSourceState<Row> {\n const id = React.useId();\n const base = source?.queryKey ?? [\"cortena-data-table\", id];\n const infinite = mode === \"infinite\";\n const fetch = source?.fetch;\n\n const page = useQuery({\n queryKey: [...base, \"page\", query],\n queryFn: ({ signal }) => fetch!(query, signal),\n enabled: Boolean(fetch) && !infinite,\n placeholderData: keepPreviousData,\n });\n\n const scope = scopeOf(query);\n const pages = useInfiniteQuery({\n queryKey: [...base, \"infinite\", scope],\n queryFn: ({ pageParam, signal }) => fetch!({ ...scope, page: pageParam }, signal),\n initialPageParam: 0,\n getNextPageParam: (last: TableResult<Row>, all: TableResult<Row>[]) => {\n const loaded = all.reduce((n, p) => n + p.rows.length, 0);\n return loaded < last.total && last.rows.length > 0 ? all.length : undefined;\n },\n enabled: Boolean(fetch) && infinite,\n placeholderData: keepPreviousData,\n });\n\n const flatRows = React.useMemo(\n () => (pages.data ? pages.data.pages.flatMap((p) => p.rows) : EMPTY),\n [pages.data],\n );\n\n if (!fetch) {\n return {\n rows: EMPTY,\n total: undefined,\n isLoading: false,\n isFetching: false,\n error: null,\n refetch: () => {},\n fetchNextPage: () => {},\n hasNextPage: false,\n };\n }\n\n if (infinite) {\n return {\n rows: flatRows,\n total: pages.data?.pages.at(-1)?.total,\n isLoading: pages.isPending,\n isFetching: pages.isFetching,\n error: pages.error ? toError(pages.error) : null,\n refetch: () => void pages.refetch(),\n fetchNextPage: () => {\n if (pages.hasNextPage && !pages.isFetchingNextPage) void pages.fetchNextPage();\n },\n hasNextPage: pages.hasNextPage,\n };\n }\n\n return {\n rows: page.data?.rows ?? EMPTY,\n total: page.data?.total,\n isLoading: page.isPending,\n isFetching: page.isFetching,\n error: page.error ? toError(page.error) : null,\n refetch: () => void page.refetch(),\n fetchNextPage: () => {},\n hasNextPage: false,\n };\n}\n\ninterface LocalState<Row> {\n pages: TableResult<Row>[];\n scopeKey: string;\n page: number;\n isFetching: boolean;\n error: Error | null;\n}\n\nfunction useLocalStrategy<Row>(\n source: ServerSource<Row> | undefined,\n query: TableQuery,\n mode: \"pages\" | \"infinite\" | \"none\",\n): ServerSourceState<Row> {\n const infinite = mode === \"infinite\";\n const fetch = source?.fetch;\n const scope = scopeOf(query);\n const scopeKey = JSON.stringify(scope);\n const [tick, setTick] = React.useState(0);\n // Pages wanted in infinite mode, scoped so a new query starts over at one.\n const [wanted, setWanted] = React.useState({ scopeKey, n: 1 });\n const wantedPages = wanted.scopeKey === scopeKey ? wanted.n : 1;\n const [state, setState] = React.useState<LocalState<Row>>({\n pages: [],\n scopeKey,\n page: -1,\n isFetching: false,\n error: null,\n });\n\n const loadedForScope = state.scopeKey === scopeKey ? state.pages : EMPTY;\n const targetPage = infinite ? loadedForScope.length : query.page;\n const needs = infinite\n ? loadedForScope.length < wantedPages\n : !(state.scopeKey === scopeKey && state.page === query.page && state.pages.length > 0);\n const requestKey = `${scopeKey}|${targetPage}|${tick}`;\n\n // The consumer's `fetch` is usually an inline arrow — `fetch: q => api.list(q)`\n // is the documented call — so it is a new function on every parent render.\n // Depending on it would abort and refire the request each time; the request's\n // identity is `requestKey`, so the latest function is read from a ref instead.\n const fetchRef = React.useRef(fetch);\n React.useEffect(() => {\n fetchRef.current = fetch;\n });\n\n const hasFetch = Boolean(fetch);\n\n React.useEffect(() => {\n const run = fetchRef.current;\n if (!run || !needs) return;\n const controller = new AbortController();\n setState((s) => ({ ...s, isFetching: true }));\n run({ ...scope, page: targetPage }, controller.signal).then(\n (result) => {\n if (controller.signal.aborted) return;\n setState((s) => ({\n pages: infinite && s.scopeKey === scopeKey ? [...s.pages, result] : [result],\n scopeKey,\n page: targetPage,\n isFetching: false,\n error: null,\n }));\n },\n (err: unknown) => {\n if (controller.signal.aborted) return;\n setState((s) => ({ ...s, isFetching: false, error: toError(err) }));\n },\n );\n return () => controller.abort();\n // `requestKey` is the identity of the request; `scope` and `targetPage` derive\n // from it. `fetch` itself is held in a ref, so only its presence is a dep.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hasFetch, requestKey, needs]);\n\n // Like keepPreviousData: the last result stays on screen while the next loads.\n const rows = React.useMemo(\n () => (infinite ? state.pages.flatMap((p) => p.rows) : (state.pages[0]?.rows ?? EMPTY)),\n [state.pages, infinite],\n );\n const total = state.pages.at(-1)?.total;\n const hasNextPage = infinite && total !== undefined && rows.length < total;\n\n if (!fetch) {\n return {\n rows: EMPTY,\n total: undefined,\n isLoading: false,\n isFetching: false,\n error: null,\n refetch: () => {},\n fetchNextPage: () => {},\n hasNextPage: false,\n };\n }\n\n return {\n rows,\n total,\n isLoading: state.pages.length === 0 && !state.error,\n isFetching: state.isFetching,\n error: state.error,\n refetch: () => {\n setState((s) => ({ ...s, pages: [], page: -1, error: null }));\n setTick((t) => t + 1);\n },\n fetchNextPage: () => {\n if (hasNextPage && !state.isFetching) setWanted({ scopeKey, n: loadedForScope.length + 1 });\n },\n hasNextPage,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"use-server-source.js","names":[],"sources":["../../../src/components/data-table/use-server-source.ts"],"sourcesContent":["\"use client\";\n\nimport {\n keepPreviousData,\n QueryClientContext,\n useInfiniteQuery,\n useIsMutating,\n useQuery,\n} from \"@tanstack/react-query\";\nimport * as React from \"react\";\nimport type { ServerSource, TableQuery, TableResult } from \"./types\";\n\nexport interface ServerSourceState<Row> {\n rows: Row[];\n total: number | undefined;\n isLoading: boolean;\n isFetching: boolean;\n error: Error | null;\n refetch: () => void;\n fetchNextPage: () => void;\n hasNextPage: boolean;\n /** `Date.now()` of the last successful response; null before the first. */\n lastUpdatedAt: number | null;\n /** `Date.now()` of the last failure; null before the first. */\n lastErrorAt: number | null;\n /** Consecutive failures. Doubles the interval; zero again after a success. */\n failures: number;\n /** The interval actually scheduled, false when nothing is polling. */\n intervalMs: number | false;\n /** A mutation is in flight on the query client, so polling is held. */\n mutating: boolean;\n}\n\n/** What the interval needs from the caller; the pauses it cannot see are already applied. */\nexport interface ServerSourceAutoRefresh {\n /** Base interval, already clamped to the 30 s floor. */\n intervalMs: number;\n /** The switch is on and nothing the table can see is pausing it. */\n active: boolean;\n}\n\n/** Errors stretch the interval rather than hammering a failing endpoint. */\nexport const AUTO_REFRESH_MAX_MS = 300_000;\n\nfunction backoffOf(base: number, failures: number): number {\n return Math.min(base * 2 ** failures, AUTO_REFRESH_MAX_MS);\n}\n\nexport interface RequestStamps {\n ok: number | null;\n err: number | null;\n}\n\n/**\n * Consecutive failures, counted from the timestamps the source reports: one\n * more for every new error, back to zero on any success.\n *\n * It is state, and it is read *before* the query is declared, so the interval\n * handed to `refetchInterval` on the next render already carries the back-off.\n * The timestamps only exist after the query, hence the ref: the effect runs\n * after the commit, by which time the render has filled it in.\n */\nfunction useFailureCount(stamps: React.RefObject<RequestStamps>): number {\n const [failures, setFailures] = React.useState(0);\n const seen = React.useRef<RequestStamps>({ ok: null, err: null });\n React.useEffect(() => {\n const { ok, err } = stamps.current;\n if (err !== null && err !== seen.current.err) {\n seen.current.err = err;\n setFailures((n) => n + 1);\n }\n if (ok !== null && ok !== seen.current.ok) {\n seen.current.ok = ok;\n setFailures(0);\n }\n });\n return failures;\n}\n\nconst EMPTY: never[] = [];\n\nfunction toError(value: unknown): Error {\n return value instanceof Error ? value : new Error(String(value));\n}\n\n/** The part of a query that identifies a result set; the page is separate so infinite mode can append. */\nfunction scopeOf(query: TableQuery): Omit<TableQuery, \"page\"> {\n const { page: _page, ...scope } = query;\n return scope;\n}\n\n/**\n * Runs a server source. With a `QueryClientProvider` above the table, pages\n * go through TanStack Query: cached by query, previous page kept on screen\n * while the next loads, and the in-flight request aborted through `signal`\n * when the query changes. Without one, a small local fetch state does the\n * same minus the cache, so the table never requires the optional peer to be\n * wired up.\n *\n * The strategy is chosen once per mount: a provider appearing or vanishing\n * mid-life would change the hook order, which React forbids, and is not a\n * real scenario.\n */\nexport function useServerSource<Row>(\n source: ServerSource<Row> | undefined,\n query: TableQuery,\n mode: \"pages\" | \"infinite\" | \"none\",\n auto?: ServerSourceAutoRefresh,\n): ServerSourceState<Row> {\n const client = React.useContext(QueryClientContext);\n const [strategy] = React.useState<\"query\" | \"local\">(() => (client ? \"query\" : \"local\"));\n // Hook order is stable for the life of the component because `strategy` is.\n return strategy === \"query\"\n ? useQueryStrategy(source, query, mode, auto)\n : useLocalStrategy(source, query, mode, auto);\n}\n\nfunction useQueryStrategy<Row>(\n source: ServerSource<Row> | undefined,\n query: TableQuery,\n mode: \"pages\" | \"infinite\" | \"none\",\n auto?: ServerSourceAutoRefresh,\n): ServerSourceState<Row> {\n const id = React.useId();\n const base = source?.queryKey ?? [\"cortena-data-table\", id];\n const infinite = mode === \"infinite\";\n const fetch = source?.fetch;\n\n // A mutation in flight is the fifth pause: refetching over a save the user\n // has just fired shows them the row as it was a moment before it changed.\n const mutating = useIsMutating() > 0;\n // The interval and the Refresh button are the same query: `refetchInterval`\n // and `refetch()` on one `useQuery`, never a second fetch path beside it.\n const stamps = React.useRef<RequestStamps>({ ok: null, err: null });\n const failures = useFailureCount(stamps);\n const refetchInterval =\n auto && auto.active && !mutating ? backoffOf(auto.intervalMs, failures) : (false as const);\n\n const page = useQuery({\n queryKey: [...base, \"page\", query],\n queryFn: ({ signal }) => fetch!(query, signal),\n enabled: Boolean(fetch) && !infinite,\n placeholderData: keepPreviousData,\n refetchInterval: infinite ? false : refetchInterval,\n });\n\n const scope = scopeOf(query);\n const pages = useInfiniteQuery({\n queryKey: [...base, \"infinite\", scope],\n queryFn: ({ pageParam, signal }) => fetch!({ ...scope, page: pageParam }, signal),\n initialPageParam: 0,\n getNextPageParam: (last: TableResult<Row>, all: TableResult<Row>[]) => {\n const loaded = all.reduce((n, p) => n + p.rows.length, 0);\n return loaded < last.total && last.rows.length > 0 ? all.length : undefined;\n },\n enabled: Boolean(fetch) && infinite,\n placeholderData: keepPreviousData,\n refetchInterval: infinite ? refetchInterval : false,\n });\n\n const live = infinite ? pages : page;\n const lastUpdatedAt = live.dataUpdatedAt || null;\n const lastErrorAt = live.errorUpdatedAt || null;\n stamps.current = { ok: lastUpdatedAt, err: lastErrorAt };\n\n const flatRows = React.useMemo(\n () => (pages.data ? pages.data.pages.flatMap((p) => p.rows) : EMPTY),\n [pages.data],\n );\n\n if (!fetch) {\n return {\n rows: EMPTY,\n total: undefined,\n isLoading: false,\n isFetching: false,\n error: null,\n refetch: () => {},\n fetchNextPage: () => {},\n hasNextPage: false,\n lastUpdatedAt: null,\n lastErrorAt: null,\n failures: 0,\n intervalMs: false,\n mutating,\n };\n }\n\n if (infinite) {\n return {\n rows: flatRows,\n total: pages.data?.pages.at(-1)?.total,\n isLoading: pages.isPending,\n isFetching: pages.isFetching,\n error: pages.error ? toError(pages.error) : null,\n refetch: () => void pages.refetch(),\n fetchNextPage: () => {\n if (pages.hasNextPage && !pages.isFetchingNextPage) void pages.fetchNextPage();\n },\n hasNextPage: pages.hasNextPage,\n lastUpdatedAt,\n lastErrorAt,\n failures,\n intervalMs: refetchInterval,\n mutating,\n };\n }\n\n return {\n rows: page.data?.rows ?? EMPTY,\n total: page.data?.total,\n isLoading: page.isPending,\n isFetching: page.isFetching,\n error: page.error ? toError(page.error) : null,\n refetch: () => void page.refetch(),\n fetchNextPage: () => {},\n hasNextPage: false,\n lastUpdatedAt,\n lastErrorAt,\n failures,\n intervalMs: refetchInterval,\n mutating,\n };\n}\n\ninterface LocalState<Row> {\n pages: TableResult<Row>[];\n scopeKey: string;\n page: number;\n /** The refresh generation this state answers; `tick` moving on means stale. */\n tick: number;\n isFetching: boolean;\n error: Error | null;\n updatedAt: number | null;\n errorAt: number | null;\n}\n\nfunction useLocalStrategy<Row>(\n source: ServerSource<Row> | undefined,\n query: TableQuery,\n mode: \"pages\" | \"infinite\" | \"none\",\n auto?: ServerSourceAutoRefresh,\n): ServerSourceState<Row> {\n const infinite = mode === \"infinite\";\n const fetch = source?.fetch;\n const scope = scopeOf(query);\n const scopeKey = JSON.stringify(scope);\n const [tick, setTick] = React.useState(0);\n // Pages wanted in infinite mode, scoped so a new query starts over at one.\n const [wanted, setWanted] = React.useState({ scopeKey, n: 1 });\n const wantedPages = wanted.scopeKey === scopeKey ? wanted.n : 1;\n const [state, setState] = React.useState<LocalState<Row>>({\n pages: [],\n scopeKey,\n page: -1,\n tick: -1,\n isFetching: false,\n error: null,\n updatedAt: null,\n errorAt: null,\n });\n\n const loadedForScope = state.scopeKey === scopeKey ? state.pages : EMPTY;\n // A refresh — the button or the interval — bumps `tick`; the rows already on\n // screen stay there until the new ones arrive, exactly as keepPreviousData\n // behaves under a provider. Nothing about sort, filters, page or selection\n // is touched, because none of them live here.\n const stale = state.tick !== tick;\n const restart = stale || state.scopeKey !== scopeKey;\n const targetPage = infinite ? (restart ? 0 : loadedForScope.length) : query.page;\n const needs =\n stale ||\n (infinite\n ? loadedForScope.length < wantedPages\n : !(state.scopeKey === scopeKey && state.page === query.page && state.pages.length > 0));\n const requestKey = `${scopeKey}|${targetPage}|${tick}`;\n\n // The consumer's `fetch` is usually an inline arrow — `fetch: q => api.list(q)`\n // is the documented call — so it is a new function on every parent render.\n // Depending on it would abort and refire the request each time; the request's\n // identity is `requestKey`, so the latest function is read from a ref instead.\n const fetchRef = React.useRef(fetch);\n React.useEffect(() => {\n fetchRef.current = fetch;\n });\n\n const hasFetch = Boolean(fetch);\n\n React.useEffect(() => {\n const run = fetchRef.current;\n if (!run || !needs) return;\n const controller = new AbortController();\n setState((s) => ({ ...s, isFetching: true }));\n run({ ...scope, page: targetPage }, controller.signal).then(\n (result) => {\n if (controller.signal.aborted) return;\n setState((s) => ({\n pages: infinite && !restart && s.scopeKey === scopeKey ? [...s.pages, result] : [result],\n scopeKey,\n page: targetPage,\n tick,\n isFetching: false,\n error: null,\n updatedAt: Date.now(),\n errorAt: s.errorAt,\n }));\n },\n (err: unknown) => {\n if (controller.signal.aborted) return;\n // `tick` is recorded on the failure too: without it the request stays\n // \"wanted\" and a single dead endpoint is retried on every render.\n setState((s) => ({\n ...s,\n tick,\n isFetching: false,\n error: toError(err),\n errorAt: Date.now(),\n }));\n },\n );\n return () => controller.abort();\n // `requestKey` is the identity of the request; `scope` and `targetPage` derive\n // from it. `fetch` itself is held in a ref, so only its presence is a dep.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [hasFetch, requestKey, needs]);\n\n const stamps = React.useRef<RequestStamps>({ ok: null, err: null });\n stamps.current = { ok: state.updatedAt, err: state.errorAt };\n const failures = useFailureCount(stamps);\n const intervalMs = auto && auto.active ? backoffOf(auto.intervalMs, failures) : (false as const);\n\n // Without a QueryClientProvider there is no `refetchInterval` to lean on, so\n // the same `tick` the button bumps is bumped on a timer. Still one path.\n React.useEffect(() => {\n if (intervalMs === false) return;\n const id = setInterval(() => setTick((t) => t + 1), intervalMs);\n return () => clearInterval(id);\n }, [intervalMs]);\n\n // Like keepPreviousData: the last result stays on screen while the next loads.\n const rows = React.useMemo(\n () => (infinite ? state.pages.flatMap((p) => p.rows) : (state.pages[0]?.rows ?? EMPTY)),\n [state.pages, infinite],\n );\n const total = state.pages.at(-1)?.total;\n const hasNextPage = infinite && total !== undefined && rows.length < total;\n\n if (!fetch) {\n return {\n rows: EMPTY,\n total: undefined,\n isLoading: false,\n isFetching: false,\n error: null,\n refetch: () => {},\n fetchNextPage: () => {},\n hasNextPage: false,\n lastUpdatedAt: null,\n lastErrorAt: null,\n failures: 0,\n intervalMs: false,\n mutating: false,\n };\n }\n\n return {\n rows,\n total,\n isLoading: state.pages.length === 0 && !state.error,\n isFetching: state.isFetching,\n error: state.error,\n refetch: () => setTick((t) => t + 1),\n fetchNextPage: () => {\n if (hasNextPage && !state.isFetching) setWanted({ scopeKey, n: loadedForScope.length + 1 });\n },\n hasNextPage,\n lastUpdatedAt: state.updatedAt,\n lastErrorAt: state.errorAt,\n failures,\n intervalMs,\n mutating: false,\n };\n}\n"],"mappings":"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"}