@marktiderman/genesis-core 1.5.1 → 1.7.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/README.md +112 -0
- package/dist/chunk-ADD2RIGA.js +108 -0
- package/dist/chunk-ADD2RIGA.js.map +1 -0
- package/dist/chunk-KXVHDK3Q.js +71 -0
- package/dist/chunk-KXVHDK3Q.js.map +1 -0
- package/dist/{chunk-F3BE7NGM.js → chunk-OBUBP4BY.js} +83 -47
- package/dist/chunk-OBUBP4BY.js.map +1 -0
- package/dist/{chunk-6Q477CHB.js → chunk-QFR6YJ3R.js} +7 -4
- package/dist/chunk-QFR6YJ3R.js.map +1 -0
- package/dist/chunk-V3GDAELU.js +3 -0
- package/dist/{chunk-HT43MPKW.js.map → chunk-V3GDAELU.js.map} +1 -1
- package/dist/{chunk-3JSFTLDV.js → chunk-Y3SCYMNN.js} +6 -6
- package/dist/{chunk-3JSFTLDV.js.map → chunk-Y3SCYMNN.js.map} +1 -1
- package/dist/{chunk-E5VYFAQO.js → chunk-Y475Z7BM.js} +202 -11
- package/dist/chunk-Y475Z7BM.js.map +1 -0
- package/dist/{chunk-ZK5C3OOT.js → chunk-ZKMAU6NN.js} +12 -3
- package/dist/chunk-ZKMAU6NN.js.map +1 -0
- package/dist/hooks/index.d.ts +4 -2
- package/dist/hooks/index.js +9 -7
- package/dist/hooks/mutation-mode.d.ts +2 -2
- package/dist/hooks/mutation-mode.js +1 -1
- package/dist/hooks/related-keys.d.ts +2 -2
- package/dist/hooks/use-capabilities.d.ts +1 -1
- package/dist/hooks/use-infinite-resource.d.ts +50 -0
- package/dist/hooks/use-infinite-resource.js +6 -0
- package/dist/hooks/use-infinite-resource.js.map +1 -0
- package/dist/hooks/use-live.d.ts +27 -0
- package/dist/hooks/use-live.js +5 -0
- package/dist/hooks/use-live.js.map +1 -0
- package/dist/hooks/use-one.d.ts +9 -2
- package/dist/hooks/use-one.js +2 -1
- package/dist/hooks/use-resource-form.d.ts +2 -2
- package/dist/hooks/use-resource-form.js +6 -5
- package/dist/hooks/use-resource.d.ts +14 -2
- package/dist/hooks/use-resource.js +4 -3
- package/dist/index.d.ts +6 -4
- package/dist/index.js +10 -8
- package/dist/navigation.d.ts +108 -0
- package/dist/navigation.js +101 -0
- package/dist/navigation.js.map +1 -0
- package/dist/provider/index.d.ts +54 -6
- package/dist/provider/index.js +1 -1
- package/dist/{read-outcome-kgNhfuST.d.ts → read-outcome-C_9HbXvb.d.ts} +1 -1
- package/dist/schema/index.d.ts +4 -4
- package/dist/slots.d.ts +60 -0
- package/dist/slots.js +12 -0
- package/dist/slots.js.map +1 -0
- package/dist/{types-5cuflvha.d.ts → types-C7FJtFXV.d.ts} +12 -0
- package/dist/{types-DDOmG9hj.d.ts → types-CS9UpxQS.d.ts} +62 -4
- package/package.json +9 -1
- package/dist/chunk-6Q477CHB.js.map +0 -1
- package/dist/chunk-E5VYFAQO.js.map +0 -1
- package/dist/chunk-F3BE7NGM.js.map +0 -1
- package/dist/chunk-HT43MPKW.js +0 -3
- package/dist/chunk-ZK5C3OOT.js.map +0 -1
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ const provider = createSupabaseProvider(supabase, {
|
|
|
101
101
|
| `toFormValues(row)` | Row → edit-form values, for a form shaped differently from its table. Served as `DataProvider.toFormValues`, which `useResourceForm` applies in edit mode. The reverse is `prepare`. |
|
|
102
102
|
| `invalidates` | Other query keys a write makes stale, as a list or `(event) => keys`. Served as `DataProvider.invalidates`, which `useResource` and `useResourceForm` (and so `ResourcePage` and `ResourceForm`) invalidate after their writes, on top of `[resource]`. |
|
|
103
103
|
| `onWrite(event)` | Called after every committed create, update and delete, including writes made straight through the provider. Awaited; if it throws, the error is logged and the write is still reported saved. |
|
|
104
|
+
| `realtime` / `resources[r].realtime` | Serve `subscribe` through Supabase Realtime, for `live` lists and records. Off by default: `true`, a list of resources, or `(resource) => boolean`; a resource's own `realtime` wins. See [Live updates](#live-updates-realtime). |
|
|
104
105
|
|
|
105
106
|
Both hooks also take an `invalidates` option for keys one screen cares about:
|
|
106
107
|
`useResource("tasks", { invalidates: [["me"]] })`.
|
|
@@ -232,6 +233,117 @@ const tasks = useResource("tasks", {
|
|
|
232
233
|
await tasks.remove(id); // resolves at once; sent in 5 s unless undone
|
|
233
234
|
```
|
|
234
235
|
|
|
236
|
+
## Cursor paging
|
|
237
|
+
|
|
238
|
+
For long lists that grow with "Load more", page by cursor (keyset) instead of
|
|
239
|
+
offset: pages stay right while rows are added or removed, and a deep page
|
|
240
|
+
costs no more than the first.
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
const tasks = useInfiniteResource("tasks", {
|
|
244
|
+
perPage: 50,
|
|
245
|
+
defaultSort: { field: "due_at", order: "asc" },
|
|
246
|
+
});
|
|
247
|
+
tasks.rows; // every loaded row
|
|
248
|
+
tasks.total; // from the first page's count
|
|
249
|
+
tasks.hasNextPage && tasks.fetchNextPage();
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
- **The contract.** `getList` with `pagination: { page, perPage, cursor }`,
|
|
253
|
+
where `cursor` is `null` for the first page and the previous page's
|
|
254
|
+
`nextCursor` after that. A provider that serves it
|
|
255
|
+
(`capabilities().cursorPagination`) returns `nextCursor` (`null` on the
|
|
256
|
+
last page) and may return `total: -1` after the first page (not counted).
|
|
257
|
+
A provider that doesn't ignores `cursor` and pages by `page`;
|
|
258
|
+
`useInfiniteResource` then pages by offset until `total` rows are loaded.
|
|
259
|
+
- **Supabase.**
|
|
260
|
+
- It orders by the sort column and then the `orderKey` (default `id`) and
|
|
261
|
+
fetches one row more than the page to tell whether there is a next
|
|
262
|
+
page. A page is at most `maxPerPage - 1` rows, so that extra row fits
|
|
263
|
+
under PostgREST's `max-rows`.
|
|
264
|
+
- The cursor (opaque, base64 JSON) holds the last row's sort and key
|
|
265
|
+
values and the sort it was made for. A cursor from another sort is
|
|
266
|
+
refused.
|
|
267
|
+
- The keyset filter handles ascending and descending sorts, ties (broken
|
|
268
|
+
by the key) and null sort values, matching Postgres's default null
|
|
269
|
+
placement (last when ascending, first when descending, pinned in the
|
|
270
|
+
query).
|
|
271
|
+
- It refuses, with an `invalid` `DataReadError` whose `cause` says why:
|
|
272
|
+
`orderKey: false`, a sort on an embedded field (`author.name`), a sort
|
|
273
|
+
column with non-scalar values, and a `select` that leaves out the sort
|
|
274
|
+
or key column.
|
|
275
|
+
- Only the first page is counted.
|
|
276
|
+
- **Writes and pending patches.** Pages live under
|
|
277
|
+
`[resource, "infinite", params]`, so writes (which invalidate `[resource]`)
|
|
278
|
+
refetch them. Optimistic and undoable patches are applied to every loaded
|
|
279
|
+
page and survive refetches, like lists.
|
|
280
|
+
- **A row shows once.** A row that moved into a later page while the user
|
|
281
|
+
scrolled (its sort value changed, or rows were added before it) is shown
|
|
282
|
+
where it first appeared, not twice.
|
|
283
|
+
|
|
284
|
+
## Live updates (realtime)
|
|
285
|
+
|
|
286
|
+
`useResource(resource, { live: true })`, `useOne(resource, id, { live: true })`
|
|
287
|
+
and genesis-ui's `<ResourcePage live />` keep what is on screen current while
|
|
288
|
+
they are mounted. They subscribe through the provider's optional
|
|
289
|
+
`subscribe(resource, params, callback)` and, when it reports a change,
|
|
290
|
+
refetch the resource's cached reads (`useResource`: every `[resource, …]`
|
|
291
|
+
query; `useOne`: that record, and only for its own id).
|
|
292
|
+
|
|
293
|
+
- **Events are hints, never data.** The callback gets `{ type: "created" |
|
|
294
|
+
"updated" | "deleted", id? }`, or `{ type: "changed" }` when changes may have
|
|
295
|
+
been missed (after a reconnect). Nothing from an event is written to the
|
|
296
|
+
cache: rows always come from the provider's normal read, under its RLS.
|
|
297
|
+
- **One refetch per burst.** Events within `debounceMs` (default 250) of each
|
|
298
|
+
other cause one refetch: `live: { debounceMs: 1000 }`. A steady stream
|
|
299
|
+
still refetches at least every `4 × debounceMs`.
|
|
300
|
+
- **Pending writes stay applied.** A refetch while an optimistic or undoable
|
|
301
|
+
write is pending gets that write's patch re-applied (see
|
|
302
|
+
[Mutation modes](#mutation-modes)), so a row deleted with Undo stays hidden
|
|
303
|
+
and an edit stays shown. Your own committed writes come back as events too;
|
|
304
|
+
the extra refetch is harmless and is not filtered out.
|
|
305
|
+
- **A hidden tab waits.** While the page is hidden, reads are only marked
|
|
306
|
+
stale; they refetch when it is shown again (with TanStack Query's default
|
|
307
|
+
`refetchOnWindowFocus`) or when next used.
|
|
308
|
+
- **Without `subscribe`**, or for a resource the provider doesn't watch,
|
|
309
|
+
`live` does nothing. `capabilities(resource).realtime` says whether it
|
|
310
|
+
will work.
|
|
311
|
+
|
|
312
|
+
### Supabase setup
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const provider = createSupabaseProvider(supabase, {
|
|
316
|
+
resources: { tasks: { realtime: true } }, // or realtime: true | ["tasks"] for several
|
|
317
|
+
});
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
```sql
|
|
321
|
+
-- 1. Publish the table's changes to Realtime.
|
|
322
|
+
alter publication supabase_realtime add table public.tasks;
|
|
323
|
+
-- 2. Keep RLS on: Realtime only sends an insert or update to a subscriber
|
|
324
|
+
-- whose SELECT policies allow the new row.
|
|
325
|
+
alter table public.tasks enable row level security;
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- The provider opens one `postgres_changes` channel per subscription and
|
|
329
|
+
removes it on unsubscribe. `useOne`'s channel is not filtered on the
|
|
330
|
+
server (Realtime never delivers deletes through a filter); its id is
|
|
331
|
+
matched on arrival. A
|
|
332
|
+
resource named `schema.table` listens in that schema; otherwise `public`.
|
|
333
|
+
- **What leaks, and what doesn't.** Insert and update events are checked
|
|
334
|
+
against RLS by Realtime. **Delete events are not:** every subscriber to the
|
|
335
|
+
table receives the deleted row's primary key (with RLS on, Supabase sends
|
|
336
|
+
only the key, even under `REPLICA IDENTITY FULL`). The provider passes on only the id, and the hooks only refetch,
|
|
337
|
+
so a user never sees a row their read doesn't return. Don't enable
|
|
338
|
+
realtime on a table whose ids are themselves sensitive.
|
|
339
|
+
- **Cost.** Each mounted live list or record is one Realtime channel, and
|
|
340
|
+
each change refetches every live list of that table for every subscriber
|
|
341
|
+
(debounced per client). Realtime checks RLS once per subscriber per
|
|
342
|
+
change, so busy tables with many viewers multiply database load. Enable it
|
|
343
|
+
where people watch shared data change, not on every table.
|
|
344
|
+
- `capabilities(resource).realtime` is true only for resources with realtime
|
|
345
|
+
on and a client with `channel()` (any `@supabase/supabase-js` client).
|
|
346
|
+
|
|
235
347
|
## Unsaved changes
|
|
236
348
|
|
|
237
349
|
`useResourceForm({ warnWhenUnsaved: true })` (default off) warns while the
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { useLiveInvalidation } from './chunk-KXVHDK3Q.js';
|
|
2
|
+
import { useDataProvider } from './chunk-GGSXSI6Z.js';
|
|
3
|
+
import { useState, useCallback, useMemo } from 'react';
|
|
4
|
+
import { useInfiniteQuery } from '@tanstack/react-query';
|
|
5
|
+
|
|
6
|
+
function filterKey(filters) {
|
|
7
|
+
return JSON.stringify(
|
|
8
|
+
(filters ?? []).map(
|
|
9
|
+
(f) => Object.fromEntries(Object.entries(f).sort(([a], [b]) => a.localeCompare(b)))
|
|
10
|
+
)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
function useInfiniteResource(resource, options = {}) {
|
|
14
|
+
const {
|
|
15
|
+
defaultSort,
|
|
16
|
+
defaultFilters,
|
|
17
|
+
perPage = 25,
|
|
18
|
+
defaultSearch = "",
|
|
19
|
+
select,
|
|
20
|
+
searchFields,
|
|
21
|
+
enabled = true,
|
|
22
|
+
live
|
|
23
|
+
} = options;
|
|
24
|
+
const provider = useDataProvider();
|
|
25
|
+
const [sort, setSort] = useState(defaultSort);
|
|
26
|
+
const [filters, setFiltersRaw] = useState(defaultFilters ?? []);
|
|
27
|
+
const [search, setSearch] = useState(defaultSearch);
|
|
28
|
+
const defaultKey = filterKey(defaultFilters);
|
|
29
|
+
const [syncedKey, setSyncedKey] = useState(defaultKey);
|
|
30
|
+
if (defaultKey !== syncedKey) {
|
|
31
|
+
setSyncedKey(defaultKey);
|
|
32
|
+
setFiltersRaw(defaultFilters ?? []);
|
|
33
|
+
}
|
|
34
|
+
const setFilters = useCallback((next) => setFiltersRaw(next), []);
|
|
35
|
+
const params = useMemo(() => {
|
|
36
|
+
const p = {
|
|
37
|
+
perPage,
|
|
38
|
+
sort,
|
|
39
|
+
filters: filters.length > 0 ? filters : void 0,
|
|
40
|
+
select
|
|
41
|
+
};
|
|
42
|
+
if (search && searchFields && searchFields.length > 0) {
|
|
43
|
+
p.search = { query: search, fields: searchFields };
|
|
44
|
+
}
|
|
45
|
+
return p;
|
|
46
|
+
}, [perPage, sort, filters, select, search, searchFields]);
|
|
47
|
+
const query = useInfiniteQuery({
|
|
48
|
+
queryKey: [resource, "infinite", params],
|
|
49
|
+
initialPageParam: { page: 1, cursor: null },
|
|
50
|
+
queryFn: ({ pageParam }) => {
|
|
51
|
+
const { perPage: size, ...rest } = params;
|
|
52
|
+
return provider.getList(resource, {
|
|
53
|
+
...rest,
|
|
54
|
+
pagination: { page: pageParam.page, perPage: size, cursor: pageParam.cursor }
|
|
55
|
+
});
|
|
56
|
+
},
|
|
57
|
+
getNextPageParam: (last, pages, lastParam) => {
|
|
58
|
+
if (last.nextCursor !== void 0) {
|
|
59
|
+
return last.nextCursor ? { page: lastParam.page + 1, cursor: last.nextCursor } : void 0;
|
|
60
|
+
}
|
|
61
|
+
const loaded = pages.reduce((n, p) => n + p.data.length, 0);
|
|
62
|
+
const total2 = pages[0]?.total ?? 0;
|
|
63
|
+
if (last.data.length < params.perPage || loaded >= total2) return void 0;
|
|
64
|
+
return { page: lastParam.page + 1, cursor: null };
|
|
65
|
+
},
|
|
66
|
+
enabled
|
|
67
|
+
});
|
|
68
|
+
useLiveInvalidation(resource, live, enabled, { filters: params.filters }, [resource]);
|
|
69
|
+
const rows = useMemo(() => {
|
|
70
|
+
const seen = /* @__PURE__ */ new Set();
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const page of query.data?.pages ?? []) {
|
|
73
|
+
for (const row of page.data) {
|
|
74
|
+
const id = row?.id;
|
|
75
|
+
if (id != null) {
|
|
76
|
+
const key = String(id);
|
|
77
|
+
if (seen.has(key)) continue;
|
|
78
|
+
seen.add(key);
|
|
79
|
+
}
|
|
80
|
+
out.push(row);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}, [query.data]);
|
|
85
|
+
const firstTotal = query.data?.pages[0]?.total;
|
|
86
|
+
const total = firstTotal === void 0 || firstTotal < 0 ? void 0 : firstTotal;
|
|
87
|
+
const { fetchNextPage: fetchNext, hasNextPage, isFetchingNextPage } = query;
|
|
88
|
+
const fetchNextPage = useCallback(() => {
|
|
89
|
+
if (!hasNextPage || isFetchingNextPage) return;
|
|
90
|
+
void fetchNext();
|
|
91
|
+
}, [fetchNext, hasNextPage, isFetchingNextPage]);
|
|
92
|
+
return {
|
|
93
|
+
query,
|
|
94
|
+
rows,
|
|
95
|
+
total,
|
|
96
|
+
fetchNextPage,
|
|
97
|
+
hasNextPage: !!hasNextPage,
|
|
98
|
+
isFetchingNextPage,
|
|
99
|
+
params,
|
|
100
|
+
setSort,
|
|
101
|
+
setFilters,
|
|
102
|
+
setSearch
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export { useInfiniteResource };
|
|
107
|
+
//# sourceMappingURL=chunk-ADD2RIGA.js.map
|
|
108
|
+
//# sourceMappingURL=chunk-ADD2RIGA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-infinite-resource.ts"],"names":["total"],"mappings":"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","file":"chunk-ADD2RIGA.js","sourcesContent":["import { useCallback, useMemo, useState } from \"react\";\nimport {\n useInfiniteQuery,\n type InfiniteData,\n type UseInfiniteQueryResult,\n} from \"@tanstack/react-query\";\nimport { useDataProvider } from \"../provider/context\";\nimport type {\n BaseRecord,\n FilterParam,\n ListParams,\n ListResult,\n SortParam,\n} from \"../provider/types\";\nimport { useLiveInvalidation, type LiveOptions } from \"./use-live\";\n\n// ---------------------------------------------------------------------------\n// useInfiniteResource — a list that grows with \"Load more\", by cursor.\n//\n// Asks for the first page with `cursor: null`. A provider that serves cursor\n// paging answers with `nextCursor` and is paged by keyset from then on; one\n// that doesn't answers without it and is paged by `page` (offset) until\n// `total` rows are loaded. Pages live under `[resource, \"infinite\", params]`,\n// so writes on the resource (which invalidate `[resource]`) refetch them,\n// and pending optimistic and undoable writes are patched into every page.\n// ---------------------------------------------------------------------------\n\nexport interface UseInfiniteResourceOptions {\n defaultSort?: SortParam;\n /** Re-synced when its value changes, like `useResource`'s. */\n defaultFilters?: FilterParam[];\n /** Rows per page. Default 25. */\n perPage?: number;\n /** The search to start with. Default \"\". */\n defaultSearch?: string;\n /** Supabase select/join syntax. Must include the sort and key columns. */\n select?: string;\n /** Fields to search across when `setSearch` is called. */\n searchFields?: string[];\n /** Disable fetching. */\n enabled?: boolean;\n /** Refetch the loaded pages when the provider reports a change. See `UseResourceOptions.live`. */\n live?: boolean | LiveOptions;\n}\n\n/** Where one page starts: its offset page, and its cursor (`null` for the first). */\nexport interface InfinitePageParam {\n page: number;\n cursor: string | null;\n}\n\nexport interface UseInfiniteResourceReturn<T> {\n /** The TanStack infinite query. */\n query: UseInfiniteQueryResult<InfiniteData<ListResult<T>, InfinitePageParam>>;\n /** Every loaded row, in order. */\n rows: T[];\n /** Rows matching the filters and search, from the first page's count. */\n total: number | undefined;\n /** Loads the next page. Does nothing while one is loading or none is left. */\n fetchNextPage: () => void;\n hasNextPage: boolean;\n isFetchingNextPage: boolean;\n /** The params every page is asked with (without its page and cursor). */\n params: Omit<ListParams, \"pagination\"> & { perPage: number };\n setSort: (sort: SortParam | undefined) => void;\n setFilters: (filters: FilterParam[]) => void;\n setSearch: (query: string) => void;\n}\n\nfunction filterKey(filters: FilterParam[] | undefined): string {\n return JSON.stringify(\n (filters ?? []).map((f) =>\n Object.fromEntries(Object.entries(f).sort(([a], [b]) => a.localeCompare(b))),\n ),\n );\n}\n\nexport function useInfiniteResource<T extends BaseRecord = BaseRecord>(\n resource: string,\n options: UseInfiniteResourceOptions = {},\n): UseInfiniteResourceReturn<T> {\n const {\n defaultSort,\n defaultFilters,\n perPage = 25,\n defaultSearch = \"\",\n select,\n searchFields,\n enabled = true,\n live,\n } = options;\n const provider = useDataProvider();\n\n const [sort, setSort] = useState<SortParam | undefined>(defaultSort);\n const [filters, setFiltersRaw] = useState<FilterParam[]>(defaultFilters ?? []);\n const [search, setSearch] = useState(defaultSearch);\n\n // Follow `defaultFilters` by value, during render (see useResource).\n const defaultKey = filterKey(defaultFilters);\n const [syncedKey, setSyncedKey] = useState(defaultKey);\n if (defaultKey !== syncedKey) {\n setSyncedKey(defaultKey);\n setFiltersRaw(defaultFilters ?? []);\n }\n\n const setFilters = useCallback((next: FilterParam[]) => setFiltersRaw(next), []);\n\n const params = useMemo(() => {\n const p: Omit<ListParams, \"pagination\"> & { perPage: number } = {\n perPage,\n sort,\n filters: filters.length > 0 ? filters : undefined,\n select,\n };\n if (search && searchFields && searchFields.length > 0) {\n p.search = { query: search, fields: searchFields };\n }\n return p;\n }, [perPage, sort, filters, select, search, searchFields]);\n\n const query = useInfiniteQuery<\n ListResult<T>,\n Error,\n InfiniteData<ListResult<T>, InfinitePageParam>,\n readonly unknown[],\n InfinitePageParam\n >({\n queryKey: [resource, \"infinite\", params],\n initialPageParam: { page: 1, cursor: null },\n queryFn: ({ pageParam }) => {\n const { perPage: size, ...rest } = params;\n return provider.getList<T>(resource, {\n ...rest,\n pagination: { page: pageParam.page, perPage: size, cursor: pageParam.cursor },\n });\n },\n getNextPageParam: (last, pages, lastParam) => {\n if (last.nextCursor !== undefined) {\n // Cursor mode: the provider says whether there is more.\n return last.nextCursor ? { page: lastParam.page + 1, cursor: last.nextCursor } : undefined;\n }\n // Offset mode: more while fewer than `total` rows are loaded and the\n // last page was full.\n const loaded = pages.reduce((n, p) => n + p.data.length, 0);\n const total = pages[0]?.total ?? 0;\n if (last.data.length < params.perPage || loaded >= total) return undefined;\n return { page: lastParam.page + 1, cursor: null };\n },\n enabled,\n });\n\n useLiveInvalidation(resource, live, enabled, { filters: params.filters }, [resource]);\n\n // A row can move between pages while the user scrolls (its sort value\n // changed, or rows before it were added): show it once, where it first appears.\n const rows = useMemo(() => {\n const seen = new Set<string>();\n const out: T[] = [];\n for (const page of query.data?.pages ?? []) {\n for (const row of page.data) {\n const id = row?.id;\n if (id != null) {\n const key = String(id);\n if (seen.has(key)) continue;\n seen.add(key);\n }\n out.push(row);\n }\n }\n return out;\n }, [query.data]);\n const firstTotal = query.data?.pages[0]?.total;\n const total = firstTotal === undefined || firstTotal < 0 ? undefined : firstTotal;\n\n const { fetchNextPage: fetchNext, hasNextPage, isFetchingNextPage } = query;\n const fetchNextPage = useCallback(() => {\n if (!hasNextPage || isFetchingNextPage) return;\n void fetchNext();\n }, [fetchNext, hasNextPage, isFetchingNextPage]);\n\n return {\n query,\n rows,\n total,\n fetchNextPage,\n hasNextPage: !!hasNextPage,\n isFetchingNextPage,\n params,\n setSort,\n setFilters,\n setSearch,\n };\n}\n"]}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useDataProvider } from './chunk-GGSXSI6Z.js';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
|
+
import { useQueryClient } from '@tanstack/react-query';
|
|
4
|
+
|
|
5
|
+
var DEFAULT_LIVE_DEBOUNCE_MS = 250;
|
|
6
|
+
var warnedNoSubscribe = /* @__PURE__ */ new WeakSet();
|
|
7
|
+
function useLiveInvalidation(resource, live, enabled, params, queryKey, accept) {
|
|
8
|
+
const provider = useDataProvider();
|
|
9
|
+
const queryClient = useQueryClient();
|
|
10
|
+
const on = !!live && enabled;
|
|
11
|
+
const debounceMs = Math.max(
|
|
12
|
+
0,
|
|
13
|
+
(typeof live === "object" ? live.debounceMs : void 0) ?? DEFAULT_LIVE_DEBOUNCE_MS
|
|
14
|
+
);
|
|
15
|
+
const paramsKey = JSON.stringify(params);
|
|
16
|
+
const keyKey = JSON.stringify(queryKey);
|
|
17
|
+
const latest = useRef({ queryKey, accept, params });
|
|
18
|
+
latest.current = { queryKey, accept, params };
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!on) return;
|
|
21
|
+
if (typeof provider.subscribe !== "function") {
|
|
22
|
+
if (process.env.NODE_ENV !== "production" && !warnedNoSubscribe.has(provider)) {
|
|
23
|
+
warnedNoSubscribe.add(provider);
|
|
24
|
+
console.warn(
|
|
25
|
+
`[genesis] "${resource}" asked for live updates, but the data provider has no subscribe(); the list will only refresh on its own reads.`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
let timer;
|
|
31
|
+
let burstStart = 0;
|
|
32
|
+
let active = true;
|
|
33
|
+
const maxWaitMs = debounceMs * 4;
|
|
34
|
+
const fire = () => {
|
|
35
|
+
timer = void 0;
|
|
36
|
+
if (!active) return;
|
|
37
|
+
const hidden = typeof document !== "undefined" && document.visibilityState === "hidden";
|
|
38
|
+
void queryClient.invalidateQueries({
|
|
39
|
+
queryKey: [...latest.current.queryKey],
|
|
40
|
+
refetchType: hidden ? "none" : "active"
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
let unsubscribe;
|
|
44
|
+
try {
|
|
45
|
+
unsubscribe = provider.subscribe(resource, latest.current.params, (event) => {
|
|
46
|
+
if (!active) return;
|
|
47
|
+
const test = latest.current.accept;
|
|
48
|
+
if (test && event.type !== "changed" && !test(event)) return;
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
if (timer === void 0) burstStart = now;
|
|
51
|
+
else clearTimeout(timer);
|
|
52
|
+
timer = setTimeout(fire, Math.min(debounceMs, Math.max(0, burstStart + maxWaitMs - now)));
|
|
53
|
+
});
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`[genesis] subscribing to "${resource}" failed:`, error);
|
|
56
|
+
}
|
|
57
|
+
return () => {
|
|
58
|
+
active = false;
|
|
59
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
60
|
+
try {
|
|
61
|
+
unsubscribe?.();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(`[genesis] unsubscribing from "${resource}" failed:`, error);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, [on, provider, queryClient, resource, debounceMs, paramsKey, keyKey]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export { DEFAULT_LIVE_DEBOUNCE_MS, useLiveInvalidation };
|
|
70
|
+
//# sourceMappingURL=chunk-KXVHDK3Q.js.map
|
|
71
|
+
//# sourceMappingURL=chunk-KXVHDK3Q.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hooks/use-live.ts"],"names":[],"mappings":"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","file":"chunk-KXVHDK3Q.js","sourcesContent":["// ---------------------------------------------------------------------------\n// Live reads — refetch a resource's cached reads when the provider reports a\n// change (`DataProvider.subscribe`).\n//\n// Events are invalidation hints only. Nothing from an event's payload is\n// written to the cache: the refetch goes through the provider's normal read,\n// so what a user sees is always what that read (and its RLS) returns.\n//\n// Pending optimistic and undoable writes are safe: their patches are\n// re-applied to whatever a refetch brings (see mutation-mode.ts), so a row\n// deleted with undo stays hidden and an edit stays shown until the write\n// settles. A change caused by this client's own write arrives as an event\n// too; the refetch it causes is idempotent, so it is not filtered out.\n// ---------------------------------------------------------------------------\n\nimport { useEffect, useRef } from \"react\";\nimport { useQueryClient, type QueryKey } from \"@tanstack/react-query\";\nimport { useDataProvider } from \"../provider/context\";\nimport type { ResourceChangeEvent, SubscribeParams } from \"../provider/types\";\n\n/** How long a burst of change events is gathered into one refetch. */\nexport const DEFAULT_LIVE_DEBOUNCE_MS = 250;\n\n/** `live` as an object, to tune it. `live: true` takes the defaults. */\nexport interface LiveOptions {\n /**\n * Events arriving within this many milliseconds of each other cause one\n * refetch, not one each. Default {@link DEFAULT_LIVE_DEBOUNCE_MS}. A\n * steady stream still refetches at least every `4 × debounceMs`.\n */\n debounceMs?: number;\n}\n\nconst warnedNoSubscribe = new WeakSet<object>();\n\n/**\n * Subscribes while mounted and `enabled`, and invalidates `queryKey` (a\n * prefix) once per burst of events that `accept` lets through. While the\n * page is hidden the queries are only marked stale; they refetch when it is\n * shown again (TanStack Query's `refetchOnWindowFocus`) or next used.\n *\n * @internal Used by `useResource` and `useOne`'s `live` option.\n */\nexport function useLiveInvalidation(\n resource: string,\n live: boolean | LiveOptions | undefined,\n enabled: boolean,\n params: SubscribeParams,\n queryKey: QueryKey,\n accept?: (event: ResourceChangeEvent) => boolean,\n): void {\n const provider = useDataProvider();\n const queryClient = useQueryClient();\n const on = !!live && enabled;\n const debounceMs = Math.max(\n 0,\n (typeof live === \"object\" ? live.debounceMs : undefined) ?? DEFAULT_LIVE_DEBOUNCE_MS,\n );\n const paramsKey = JSON.stringify(params);\n const keyKey = JSON.stringify(queryKey);\n\n // The latest values, read when an event arrives, so a new array or\n // callback each render doesn't resubscribe.\n const latest = useRef({ queryKey, accept, params });\n latest.current = { queryKey, accept, params };\n\n useEffect(() => {\n if (!on) return;\n if (typeof provider.subscribe !== \"function\") {\n if (process.env.NODE_ENV !== \"production\" && !warnedNoSubscribe.has(provider)) {\n warnedNoSubscribe.add(provider);\n console.warn(\n `[genesis] \"${resource}\" asked for live updates, but the data provider has no subscribe(); the list will only refresh on its own reads.`,\n );\n }\n return;\n }\n let timer: ReturnType<typeof setTimeout> | undefined;\n let burstStart = 0;\n let active = true;\n // A busy table must not postpone the refetch forever.\n const maxWaitMs = debounceMs * 4;\n const fire = () => {\n timer = undefined;\n if (!active) return;\n const hidden = typeof document !== \"undefined\" && document.visibilityState === \"hidden\";\n void queryClient.invalidateQueries({\n queryKey: [...latest.current.queryKey],\n refetchType: hidden ? \"none\" : \"active\",\n });\n };\n let unsubscribe: (() => void) | undefined;\n try {\n unsubscribe = provider.subscribe(resource, latest.current.params, (event) => {\n if (!active) return;\n const test = latest.current.accept;\n if (test && event.type !== \"changed\" && !test(event)) return;\n // Coalesce: one refetch per burst, after it goes quiet (or has\n // lasted `maxWaitMs`).\n const now = Date.now();\n if (timer === undefined) burstStart = now;\n else clearTimeout(timer);\n timer = setTimeout(fire, Math.min(debounceMs, Math.max(0, burstStart + maxWaitMs - now)));\n });\n } catch (error) {\n console.error(`[genesis] subscribing to \"${resource}\" failed:`, error);\n }\n return () => {\n active = false;\n if (timer !== undefined) clearTimeout(timer);\n try {\n unsubscribe?.();\n } catch (error) {\n console.error(`[genesis] unsubscribing from \"${resource}\" failed:`, error);\n }\n };\n // `paramsKey` / `keyKey` stand for `params` / `queryKey` by value.\n }, [on, provider, queryClient, resource, debounceMs, paramsKey, keyKey]);\n}\n"]}
|
|
@@ -8,6 +8,36 @@ function sameId(a, b) {
|
|
|
8
8
|
function isListResult(value) {
|
|
9
9
|
return typeof value === "object" && value !== null && Array.isArray(value.data);
|
|
10
10
|
}
|
|
11
|
+
function isInfiniteLists(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && Array.isArray(value.pages) && Array.isArray(value.pageParams);
|
|
13
|
+
}
|
|
14
|
+
function mapLists(key, hash, value, fn) {
|
|
15
|
+
if (key[1] === "list") return isListResult(value) ? fn(value, hash) : value;
|
|
16
|
+
if (key[1] !== "infinite" || !isInfiniteLists(value)) return value;
|
|
17
|
+
let changed = false;
|
|
18
|
+
let rowDelta = 0;
|
|
19
|
+
const pages = value.pages.map((page, i) => {
|
|
20
|
+
if (!isListResult(page)) return page;
|
|
21
|
+
const next = fn(page, `${hash}#${i}`);
|
|
22
|
+
if (next !== page) {
|
|
23
|
+
changed = true;
|
|
24
|
+
rowDelta += next.data.length - page.data.length;
|
|
25
|
+
}
|
|
26
|
+
return next;
|
|
27
|
+
});
|
|
28
|
+
if (!changed) return value;
|
|
29
|
+
const withTotals = value.pages.map((orig, i) => {
|
|
30
|
+
const page = pages[i];
|
|
31
|
+
if (!isListResult(page) || !isListResult(orig) || typeof orig.total !== "number") return page;
|
|
32
|
+
return { ...page, total: orig.total < 0 ? orig.total : Math.max(0, orig.total + rowDelta) };
|
|
33
|
+
});
|
|
34
|
+
return { ...value, pages: withTotals };
|
|
35
|
+
}
|
|
36
|
+
function listsIn(key, value) {
|
|
37
|
+
if (key[1] === "list") return isListResult(value) ? [value] : [];
|
|
38
|
+
if (key[1] === "infinite" && isInfiniteLists(value)) return value.pages.filter(isListResult);
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
11
41
|
function isDetailKeyFor(key, resource, id) {
|
|
12
42
|
return key[0] === resource && key[1] === "detail" && sameId(key[2], id);
|
|
13
43
|
}
|
|
@@ -34,10 +64,11 @@ function cachedRecord(queryClient, resource, id) {
|
|
|
34
64
|
for (const [key, value] of queryClient.getQueriesData({ queryKey: [resource, "detail"] })) {
|
|
35
65
|
if (isDetailKeyFor(key, resource, id) && value && typeof value === "object") return value;
|
|
36
66
|
}
|
|
37
|
-
for (const [, value] of queryClient.getQueriesData({ queryKey: [resource
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
67
|
+
for (const [key, value] of queryClient.getQueriesData({ queryKey: [resource] })) {
|
|
68
|
+
for (const list of listsIn(key, value)) {
|
|
69
|
+
const found = list.data.find((row) => sameId(row.id, id));
|
|
70
|
+
if (found) return found;
|
|
71
|
+
}
|
|
41
72
|
}
|
|
42
73
|
return void 0;
|
|
43
74
|
}
|
|
@@ -96,25 +127,27 @@ function revertFields(row, data, before) {
|
|
|
96
127
|
return out;
|
|
97
128
|
}
|
|
98
129
|
function applyPatches(key, hash, value, patches) {
|
|
99
|
-
if (key[1] === "list"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
if (key[1] === "list" || key[1] === "infinite") {
|
|
131
|
+
return mapLists(key, hash, value, (list, listHash) => {
|
|
132
|
+
let rows = list.data;
|
|
133
|
+
let total = list.total;
|
|
134
|
+
let changed = false;
|
|
135
|
+
for (const p of patches) {
|
|
136
|
+
const index = rows.findIndex((row) => sameId(row.id, p.id));
|
|
137
|
+
if (index < 0) continue;
|
|
138
|
+
p.before.set(listHash, { row: rows[index], index });
|
|
139
|
+
if (p.kind === "update") {
|
|
140
|
+
rows = rows.slice();
|
|
141
|
+
rows[index] = applyUpdate(rows[index], p.data ?? {});
|
|
142
|
+
} else {
|
|
143
|
+
rows = rows.filter((_, i) => i !== index);
|
|
144
|
+
if (typeof total === "number" && total >= 0) total = Math.max(0, total - 1);
|
|
145
|
+
}
|
|
146
|
+
changed = true;
|
|
113
147
|
}
|
|
114
|
-
changed
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return typeof total === "number" ? { ...value, data: rows, total } : { ...value, data: rows };
|
|
148
|
+
if (!changed) return list;
|
|
149
|
+
return typeof total === "number" ? { ...list, data: rows, total } : { ...list, data: rows };
|
|
150
|
+
});
|
|
118
151
|
}
|
|
119
152
|
if (key[1] === "detail" && value && typeof value === "object") {
|
|
120
153
|
let row = value;
|
|
@@ -183,28 +216,29 @@ function retractPatch(reg, queryClient, patch) {
|
|
|
183
216
|
for (const query of queryClient.getQueryCache().findAll({ queryKey: [patch.resource] })) {
|
|
184
217
|
const key = query.queryKey;
|
|
185
218
|
const value = query.state.data;
|
|
186
|
-
const b = patch.before.get(query.queryHash);
|
|
187
219
|
let next = value;
|
|
188
|
-
if (key[1] === "list"
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
220
|
+
if (key[1] === "list" || key[1] === "infinite") {
|
|
221
|
+
next = mapLists(key, query.queryHash, value, (list, listHash) => {
|
|
222
|
+
const b = patch.before.get(listHash);
|
|
223
|
+
const at = list.data.findIndex((row) => sameId(row.id, patch.id));
|
|
224
|
+
if (patch.kind === "update") {
|
|
225
|
+
const src = b ?? firstBefore(patch);
|
|
226
|
+
if (at < 0 || !src || !patch.data) return list;
|
|
227
|
+
const rows2 = list.data.slice();
|
|
228
|
+
rows2[at] = reapply(revertFields(rows2[at], patch.data, src.row));
|
|
229
|
+
return { ...list, data: rows2 };
|
|
230
|
+
}
|
|
231
|
+
if (at >= 0 || !b || remaining.some((p) => p.kind === "delete")) return list;
|
|
232
|
+
const rows = list.data.slice();
|
|
199
233
|
rows.splice(Math.min(b.index, rows.length), 0, reapply(b.row));
|
|
200
|
-
|
|
201
|
-
...
|
|
234
|
+
return {
|
|
235
|
+
...list,
|
|
202
236
|
data: rows,
|
|
203
|
-
...typeof
|
|
237
|
+
...typeof list.total === "number" && list.total >= 0 ? { total: list.total + 1 } : {}
|
|
204
238
|
};
|
|
205
|
-
}
|
|
239
|
+
});
|
|
206
240
|
} else if (key[1] === "detail" && patch.kind === "update" && patch.data && sameId(key[2], patch.id) && value && typeof value === "object") {
|
|
207
|
-
const src =
|
|
241
|
+
const src = patch.before.get(query.queryHash) ?? firstBefore(patch);
|
|
208
242
|
if (!src) continue;
|
|
209
243
|
next = reapply(revertFields(value, patch.data, src.row));
|
|
210
244
|
}
|
|
@@ -266,12 +300,14 @@ function refreshFields(reg, queryClient, patch, keys) {
|
|
|
266
300
|
const key = query.queryKey;
|
|
267
301
|
const value = query.state.data;
|
|
268
302
|
let next = value;
|
|
269
|
-
if (key[1] === "list"
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
303
|
+
if (key[1] === "list" || key[1] === "infinite") {
|
|
304
|
+
next = mapLists(key, query.queryHash, value, (list, listHash) => {
|
|
305
|
+
const at = list.data.findIndex((row) => sameId(row.id, patch.id));
|
|
306
|
+
if (at < 0) return list;
|
|
307
|
+
const rows = list.data.slice();
|
|
308
|
+
rows[at] = fix(rows[at], listHash);
|
|
309
|
+
return { ...list, data: rows };
|
|
310
|
+
});
|
|
275
311
|
} else if (key[1] === "detail" && sameId(key[2], patch.id) && value && typeof value === "object") {
|
|
276
312
|
next = fix(value, query.queryHash);
|
|
277
313
|
}
|
|
@@ -516,5 +552,5 @@ async function executeWrite(args) {
|
|
|
516
552
|
}
|
|
517
553
|
|
|
518
554
|
export { DEFAULT_UNDO_DELAY_MS, DROPPED_WRITE_MESSAGE, cachedRecord, executeWrite, invalidateResource, pendingWriteCount, providerServesUndo, sameValue };
|
|
519
|
-
//# sourceMappingURL=chunk-
|
|
520
|
-
//# sourceMappingURL=chunk-
|
|
555
|
+
//# sourceMappingURL=chunk-OBUBP4BY.js.map
|
|
556
|
+
//# sourceMappingURL=chunk-OBUBP4BY.js.map
|