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.
- package/CHANGELOG.md +107 -0
- package/LICENSE +7 -0
- package/README.md +235 -3
- package/dist/a2ui/views.js +2 -2
- package/dist/agent-chat/a2ui-block.d.ts +60 -0
- package/dist/agent-chat/a2ui-block.js +69 -0
- package/dist/agent-chat/a2ui-block.js.map +1 -0
- package/dist/agent-chat/agui-client.d.ts +40 -0
- package/dist/agent-chat/agui-client.js +251 -0
- package/dist/agent-chat/agui-client.js.map +1 -0
- package/dist/agent-chat/bridge.d.ts +109 -0
- package/dist/agent-chat/bridge.js +353 -0
- package/dist/agent-chat/bridge.js.map +1 -0
- package/dist/agent-chat/session.d.ts +79 -0
- package/dist/agent-chat/session.js +391 -0
- package/dist/agent-chat/session.js.map +1 -0
- package/dist/agent-chat/step-label.d.ts +99 -0
- package/dist/agent-chat/step-label.js +116 -0
- package/dist/agent-chat/step-label.js.map +1 -0
- package/dist/agent-chat/store.d.ts +102 -0
- package/dist/agent-chat/store.js +876 -0
- package/dist/agent-chat/store.js.map +1 -0
- package/dist/agent-chat/types.d.ts +277 -0
- package/dist/agent-chat/types.js +17 -0
- package/dist/agent-chat/types.js.map +1 -0
- package/dist/agent-chat.d.ts +11 -0
- package/dist/agent-chat.js +11 -0
- package/dist/components/admin-permissions/admin-permissions.d.ts +66 -0
- package/dist/components/admin-permissions/admin-permissions.js +101 -0
- package/dist/components/admin-permissions/admin-permissions.js.map +1 -0
- package/dist/components/admin-permissions/context.d.ts +70 -0
- package/dist/components/admin-permissions/context.js +258 -0
- package/dist/components/admin-permissions/context.js.map +1 -0
- package/dist/components/admin-permissions/index.d.ts +10 -0
- package/dist/components/admin-permissions/licence.d.ts +15 -0
- package/dist/components/admin-permissions/licence.js +78 -0
- package/dist/components/admin-permissions/licence.js.map +1 -0
- package/dist/components/admin-permissions/matrix.d.ts +20 -0
- package/dist/components/admin-permissions/matrix.js +191 -0
- package/dist/components/admin-permissions/matrix.js.map +1 -0
- package/dist/components/admin-permissions/members.d.ts +18 -0
- package/dist/components/admin-permissions/members.js +185 -0
- package/dist/components/admin-permissions/members.js.map +1 -0
- package/dist/components/admin-permissions/role-assignment.d.ts +35 -0
- package/dist/components/admin-permissions/role-assignment.js +174 -0
- package/dist/components/admin-permissions/role-assignment.js.map +1 -0
- package/dist/components/admin-permissions/roles.d.ts +25 -0
- package/dist/components/admin-permissions/roles.js +168 -0
- package/dist/components/admin-permissions/roles.js.map +1 -0
- package/dist/components/admin-permissions/types.d.ts +152 -0
- package/dist/components/admin-permissions/types.js +63 -0
- package/dist/components/admin-permissions/types.js.map +1 -0
- package/dist/components/agent-chat-popup.d.ts +29 -0
- package/dist/components/agent-chat-popup.js +188 -0
- package/dist/components/agent-chat-popup.js.map +1 -0
- package/dist/components/agent-chat.d.ts +163 -0
- package/dist/components/agent-chat.js +673 -0
- package/dist/components/agent-chat.js.map +1 -0
- package/dist/components/app-shell.d.ts +126 -0
- package/dist/components/app-shell.js +297 -0
- package/dist/components/app-shell.js.map +1 -0
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/button-link.js +1 -1
- package/dist/components/button.d.ts +1 -1
- package/dist/components/checkbox.d.ts +1 -1
- package/dist/components/combobox.d.ts +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/consent-screen.d.ts +65 -0
- package/dist/components/consent-screen.js +123 -0
- package/dist/components/consent-screen.js.map +1 -0
- package/dist/components/data-table/data-table.d.ts +15 -1
- package/dist/components/data-table/data-table.js +18 -4
- package/dist/components/data-table/data-table.js.map +1 -1
- package/dist/components/data-table/index.d.ts +4 -4
- package/dist/components/data-table/parts.d.ts +27 -3
- package/dist/components/data-table/parts.js +175 -55
- package/dist/components/data-table/parts.js.map +1 -1
- package/dist/components/data-table/types.d.ts +61 -0
- package/dist/components/data-table/use-data-table.js +91 -6
- package/dist/components/data-table/use-data-table.js.map +1 -1
- package/dist/components/data-table/use-server-source.js +119 -28
- package/dist/components/data-table/use-server-source.js.map +1 -1
- package/dist/components/help-panel.d.ts +131 -0
- package/dist/components/help-panel.js +545 -0
- package/dist/components/help-panel.js.map +1 -0
- package/dist/components/login-screen.d.ts +127 -0
- package/dist/components/login-screen.js +339 -0
- package/dist/components/login-screen.js.map +1 -0
- package/dist/components/session-guard.d.ts +268 -0
- package/dist/components/session-guard.js +632 -0
- package/dist/components/session-guard.js.map +1 -0
- package/dist/components/toast.d.ts +1 -1
- package/dist/core.d.ts +5 -1
- package/dist/core.js +11 -7
- package/dist/data-table.d.ts +13 -4
- package/dist/data-table.js +10 -2
- package/dist/hooks/use-cortena-theme.js +49 -3
- package/dist/hooks/use-cortena-theme.js.map +1 -1
- package/dist/index.d.ts +17 -4
- package/dist/index.js +21 -8
- package/dist/markdown.d.ts +2 -1
- package/dist/markdown.js +2 -1
- package/package.json +18 -5
- package/src/agent-chat/a2ui-block.ts +118 -0
- package/src/agent-chat/agui-client.ts +405 -0
- package/src/agent-chat/bridge.ts +445 -0
- package/src/agent-chat/session.ts +549 -0
- package/src/agent-chat/step-label.ts +177 -0
- package/src/agent-chat/store.ts +1234 -0
- package/src/agent-chat/types.ts +308 -0
- package/src/components/admin-permissions/admin-permissions.tsx +130 -0
- package/src/components/admin-permissions/context.tsx +376 -0
- package/src/components/admin-permissions/index.tsx +32 -0
- package/src/components/admin-permissions/licence.tsx +84 -0
- package/src/components/admin-permissions/matrix.tsx +257 -0
- package/src/components/admin-permissions/members.tsx +204 -0
- package/src/components/admin-permissions/role-assignment.tsx +239 -0
- package/src/components/admin-permissions/roles.tsx +169 -0
- package/src/components/admin-permissions/types.ts +231 -0
- package/src/components/agent-chat-popup.tsx +289 -0
- package/src/components/agent-chat.tsx +1006 -0
- package/src/components/app-shell.tsx +502 -0
- package/src/components/consent-screen.tsx +239 -0
- package/src/components/data-table/data-table.tsx +36 -0
- package/src/components/data-table/index.tsx +6 -1
- package/src/components/data-table/parts.tsx +223 -47
- package/src/components/data-table/types.ts +68 -0
- package/src/components/data-table/use-data-table.ts +152 -4
- package/src/components/data-table/use-server-source.ts +150 -12
- package/src/components/help-panel.tsx +765 -0
- package/src/components/login-screen.tsx +479 -0
- package/src/components/session-guard.tsx +1071 -0
- package/src/entries/agent-chat.ts +137 -0
- package/src/entries/core.ts +8 -0
- package/src/entries/data-table.ts +41 -0
- package/src/entries/markdown.ts +25 -0
- package/src/hooks/use-cortena-theme.ts +63 -4
- 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
|
|
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
|
|
119
|
-
const
|
|
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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"}
|