@marktiderman/genesis-core 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +143 -10
  2. package/dist/chunk-ADD2RIGA.js +108 -0
  3. package/dist/chunk-ADD2RIGA.js.map +1 -0
  4. package/dist/{chunk-IZT2O57C.js → chunk-CKGGE6WA.js} +36 -16
  5. package/dist/chunk-CKGGE6WA.js.map +1 -0
  6. package/dist/{chunk-DVW5GXWZ.js → chunk-KV6KH3L2.js} +16 -11
  7. package/dist/chunk-KV6KH3L2.js.map +1 -0
  8. package/dist/chunk-KXVHDK3Q.js +71 -0
  9. package/dist/chunk-KXVHDK3Q.js.map +1 -0
  10. package/dist/chunk-OBUBP4BY.js +556 -0
  11. package/dist/chunk-OBUBP4BY.js.map +1 -0
  12. package/dist/chunk-V3GDAELU.js +3 -0
  13. package/dist/{chunk-HT43MPKW.js.map → chunk-V3GDAELU.js.map} +1 -1
  14. package/dist/{chunk-RLY7TZUY.js → chunk-Y475Z7BM.js} +247 -22
  15. package/dist/chunk-Y475Z7BM.js.map +1 -0
  16. package/dist/{chunk-ZK5C3OOT.js → chunk-ZKMAU6NN.js} +12 -3
  17. package/dist/chunk-ZKMAU6NN.js.map +1 -0
  18. package/dist/hooks/index.d.ts +4 -2
  19. package/dist/hooks/index.js +7 -5
  20. package/dist/hooks/mutation-mode.d.ts +40 -30
  21. package/dist/hooks/mutation-mode.js +4 -1
  22. package/dist/hooks/related-keys.d.ts +2 -2
  23. package/dist/hooks/use-capabilities.d.ts +1 -1
  24. package/dist/hooks/use-infinite-resource.d.ts +50 -0
  25. package/dist/hooks/use-infinite-resource.js +6 -0
  26. package/dist/hooks/use-infinite-resource.js.map +1 -0
  27. package/dist/hooks/use-live.d.ts +27 -0
  28. package/dist/hooks/use-live.js +5 -0
  29. package/dist/hooks/use-live.js.map +1 -0
  30. package/dist/hooks/use-one.d.ts +9 -2
  31. package/dist/hooks/use-one.js +2 -1
  32. package/dist/hooks/use-resource-form.d.ts +10 -2
  33. package/dist/hooks/use-resource-form.js +5 -3
  34. package/dist/hooks/use-resource.d.ts +14 -2
  35. package/dist/hooks/use-resource.js +4 -2
  36. package/dist/index.d.ts +6 -4
  37. package/dist/index.js +9 -7
  38. package/dist/provider/index.d.ts +59 -7
  39. package/dist/provider/index.js +1 -1
  40. package/dist/{read-outcome-BQkYcV4t.d.ts → read-outcome-C_9HbXvb.d.ts} +1 -1
  41. package/dist/schema/index.d.ts +4 -4
  42. package/dist/{types-C5bjsbxz.d.ts → types-C7FJtFXV.d.ts} +20 -0
  43. package/dist/{types-Dsqqr9s7.d.ts → types-CS9UpxQS.d.ts} +73 -4
  44. package/package.json +1 -1
  45. package/dist/chunk-DVW5GXWZ.js.map +0 -1
  46. package/dist/chunk-HT43MPKW.js +0 -3
  47. package/dist/chunk-IZT2O57C.js.map +0 -1
  48. package/dist/chunk-QH4P5MI2.js +0 -278
  49. package/dist/chunk-QH4P5MI2.js.map +0 -1
  50. package/dist/chunk-RLY7TZUY.js.map +0 -1
  51. package/dist/chunk-ZK5C3OOT.js.map +0 -1
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/hooks/use-resource.ts"],"names":["keepPreviousData","keepPreviousPlaceholder"],"mappings":"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","file":"chunk-KV6KH3L2.js","sourcesContent":["import { useState, useCallback, useMemo } from \"react\";\nimport {\n keepPreviousData as keepPreviousPlaceholder,\n useQuery,\n useMutation,\n useQueryClient,\n type UseQueryResult,\n type UseMutationResult,\n} from \"@tanstack/react-query\";\nimport { useDataProvider } from \"../provider/context\";\nimport type {\n BaseRecord,\n Identifier,\n SortParam,\n FilterParam,\n ListParams,\n ListResult,\n InvalidationKey,\n WriteEvent,\n} from \"../provider/types\";\nimport { relatedKeys } from \"./related-keys\";\nimport {\n cachedRecord,\n executeWrite,\n invalidateResource,\n type MutationModeOptions,\n} from \"./mutation-mode\";\nimport { useLiveInvalidation, type LiveOptions } from \"./use-live\";\n\n// ---------------------------------------------------------------------------\n// Options\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceOptions<T> extends MutationModeOptions {\n defaultSort?: SortParam;\n /**\n * Re-synced whenever its value changes (deep-equal, order-independent),\n * not just its identity — safe to pass a scope/audience-derived array that\n * changes across renders. This only covers CHANGES on an already-mounted\n * hook, though: if this starts `undefined` while the real value resolves\n * asynchronously, gate `enabled` on that same readiness signal too — the\n * hook cannot tell \"no defaultFilters intended\" apart from \"not resolved\n * yet,\" so an ungated hook's very first fetch can still run unscoped.\n */\n defaultFilters?: FilterParam[];\n defaultPerPage?: number;\n /** The page to start on (from 1), e.g. restored from the URL. Default 1. */\n defaultPage?: number;\n /** The search to start with, e.g. restored from the URL. Default \"\". */\n defaultSearch?: string;\n /** Supabase select/join syntax, e.g. \"*, author:profiles(name)\". */\n select?: string;\n /** Fields to search across when setSearch is called. Required for server-side search. */\n searchFields?: string[];\n /** Disable auto-fetching. */\n enabled?: boolean;\n /**\n * Other query keys to invalidate after a create, update or delete, on top\n * of `[resource]` and the provider's own `invalidates` (e.g. `[[\"me\"]]`\n * when a write changes the signed-in user's roles).\n */\n invalidates?: InvalidationKey[];\n /**\n * While the next page / sort / search is loading, keep returning the\n * previous result (`list.isPlaceholderData` is true meanwhile) instead of\n * dropping to `undefined`. A paged list then stays on screen between pages\n * rather than flashing a skeleton. Default false.\n */\n keepPreviousData?: boolean;\n /**\n * Keep the list current while mounted: subscribe to the provider's change\n * events (`DataProvider.subscribe`) and refetch this resource's cached\n * reads (lists and records) when it reports one, once per burst of\n * events. Events are hints: rows always come from the provider's normal\n * read. Pending optimistic and undoable writes stay applied over the\n * refetch. Needs a provider that serves `subscribe` for the resource\n * (`capabilities(resource).realtime`); otherwise it does nothing. Off by\n * default.\n */\n live?: boolean | LiveOptions;\n /**\n * After the write commits, with the provider's row. In `optimistic` and\n * `undoable` modes that is after `create` / `update` / `remove` have\n * already resolved.\n */\n onCreateSuccess?: (data: T) => void;\n onUpdateSuccess?: (data: T) => void;\n onDeleteSuccess?: () => void;\n /**\n * A write failed. In `optimistic` and `undoable` modes this is the only\n * place a failure is reported (the cache has already been put back), so\n * show it to the user here.\n */\n onError?: (error: Error) => void;\n}\n\n// ---------------------------------------------------------------------------\n// Return type\n// ---------------------------------------------------------------------------\n\nexport interface UseResourceReturn<T> {\n /** TanStack Query result for the list query. */\n list: UseQueryResult<ListResult<T>>;\n /** Current list params (pagination, sort, filters, search). */\n params: ListParams;\n setSort: (sort: SortParam | undefined) => void;\n setFilters: (filters: FilterParam[]) => void;\n setPage: (page: number) => void;\n setPerPage: (perPage: number) => void;\n setSearch: (query: string) => void;\n /**\n * Create a new record. Returns the created item. In `optimistic` and\n * `undoable` modes it resolves at once with the values given (no `id` yet;\n * `onCreateSuccess` gets the stored row).\n */\n create: (data: Partial<T>) => Promise<T>;\n /**\n * Update an existing record by id. Returns the updated item. In\n * `optimistic` and `undoable` modes it resolves at once with the cached\n * row merged with `data`.\n */\n update: (id: Identifier, data: Partial<T>) => Promise<T>;\n /** Delete a record by id. Resolves at once in `optimistic` / `undoable` modes. */\n remove: (id: Identifier) => Promise<void>;\n /** True when any mutation is in-flight. */\n isMutating: boolean;\n createMutation: UseMutationResult<T, Error, Partial<T>>;\n updateMutation: UseMutationResult<T, Error, { id: Identifier; data: Partial<T> }>;\n deleteMutation: UseMutationResult<void, Error, Identifier>;\n}\n\n// ---------------------------------------------------------------------------\n// Filter identity — a deep-equality key independent of object-key order, so\n// a caller that rebuilds the same logical filters via a differently-ordered\n// spread (e.g. `{...base, operator: \"eq\"}` vs `{operator: \"eq\", ...base}`)\n// doesn't trip a spurious resync + pagination reset on an unrelated rerender.\n// ---------------------------------------------------------------------------\n\nfunction stableFilterKey(filters: FilterParam[] | undefined): string | undefined {\n if (!filters) return undefined;\n return JSON.stringify(\n filters.map((f) => {\n const sorted: Record<string, unknown> = {};\n for (const key of Object.keys(f).sort()) {\n sorted[key] = (f as unknown as Record<string, unknown>)[key];\n }\n return sorted;\n }),\n );\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\nexport function useResource<T extends BaseRecord = BaseRecord>(\n resource: string,\n options: UseResourceOptions<T> = {},\n): UseResourceReturn<T> {\n const {\n defaultSort,\n defaultFilters,\n defaultPerPage = 25,\n defaultPage = 1,\n defaultSearch = \"\",\n select,\n searchFields: searchFieldsOption,\n enabled = true,\n invalidates,\n keepPreviousData = false,\n live,\n onCreateSuccess,\n onUpdateSuccess,\n onDeleteSuccess,\n onError,\n mutationMode = \"pessimistic\",\n undoDelayMs,\n onUndoable,\n } = options;\n\n const provider = useDataProvider();\n const queryClient = useQueryClient();\n\n // -------------------------------------------------------------------------\n // State\n // -------------------------------------------------------------------------\n\n const [sort, setSortRaw] = useState<SortParam | undefined>(defaultSort);\n const [filters, setFiltersRaw] = useState<FilterParam[]>(defaultFilters ?? []);\n const [page, setPage] = useState(Math.max(1, Math.floor(defaultPage)));\n const [perPage, setPerPageRaw] = useState(defaultPerPage);\n const [searchQuery, setSearchQuery] = useState(defaultSearch);\n\n // `defaultFilters` often resolves asynchronously — a caller's scope/audience\n // (team, project, ...) can still be loading on first render, so the array\n // it passes in changes value (not just identity) after mount, and may later\n // clear back to undefined (e.g. the scope itself is cleared). Re-syncing\n // here, during render rather than in an effect, means `filters` is already\n // correct by the time `params`/`list` below compute for this pass — no\n // stray unscoped fetch on the frame before an effect would have caught up.\n //\n // This only closes the gap for `defaultFilters` CHANGING across renders on\n // an already-mounted, already-enabled hook (e.g. a scope switch). It cannot\n // help the very first render: `filters` still seeds from whatever\n // `defaultFilters` is on mount, and `undefined` here is genuinely ambiguous\n // between \"this caller never uses defaultFilters\" (in which case forcing a\n // fetch to wait would break it forever) and \"the real value hasn't resolved\n // yet.\" A caller in the second case MUST also gate `enabled` on that same\n // readiness signal for its very first fetch to be correctly scoped — this\n // hook has no way to tell the two cases apart on its own.\n const defaultFiltersKey = stableFilterKey(defaultFilters);\n const [syncedFiltersKey, setSyncedFiltersKey] = useState(defaultFiltersKey);\n if (defaultFiltersKey !== syncedFiltersKey) {\n setSyncedFiltersKey(defaultFiltersKey);\n setFiltersRaw(defaultFilters ?? []);\n setPage(1);\n }\n\n const setSort = useCallback((s: SortParam | undefined) => {\n setSortRaw(s);\n setPage(1);\n }, []);\n\n const setFilters = useCallback((f: FilterParam[]) => {\n setFiltersRaw(f);\n setPage(1);\n }, []);\n\n const setPerPage = useCallback((value: number) => {\n setPerPageRaw(value);\n setPage(1);\n }, []);\n\n const setSearch = useCallback((query: string) => {\n setSearchQuery(query);\n setPage(1);\n }, []);\n\n // -------------------------------------------------------------------------\n // Params\n // -------------------------------------------------------------------------\n\n const params = useMemo<ListParams>(() => {\n const p: ListParams = {\n pagination: { page, perPage },\n sort,\n filters: filters.length > 0 ? filters : undefined,\n select,\n };\n if (searchQuery && searchFieldsOption && searchFieldsOption.length > 0) {\n p.search = { query: searchQuery, fields: searchFieldsOption };\n }\n return p;\n }, [page, perPage, sort, filters, select, searchQuery, searchFieldsOption]);\n\n // -------------------------------------------------------------------------\n // List query\n // -------------------------------------------------------------------------\n\n const listQueryKey = useMemo(\n () => [resource, \"list\", params] as const,\n [resource, params],\n );\n\n const list = useQuery<ListResult<T>>({\n queryKey: listQueryKey,\n queryFn: () => provider.getList<T>(resource, params),\n enabled,\n placeholderData: keepPreviousData ? keepPreviousPlaceholder : undefined,\n });\n\n // Every cached read of the resource: its lists, and the records a peek or\n // detail page shows from it.\n useLiveInvalidation(resource, live, enabled, { filters: params.filters }, [resource]);\n\n // -------------------------------------------------------------------------\n // Mutations\n // -------------------------------------------------------------------------\n\n const invalidateAfter = useCallback(\n (event: WriteEvent) => {\n // Pending optimistic/undoable patches are re-applied to the refetch.\n void invalidateResource(queryClient, resource);\n for (const queryKey of relatedKeys(provider, event, invalidates)) {\n void queryClient.invalidateQueries({ queryKey: [...queryKey] });\n }\n },\n [queryClient, resource, provider, invalidates],\n );\n\n const defaultOnError = useCallback(\n (error: Error) => {\n console.error(`[useResource] ${resource} mutation failed:`, error.message);\n },\n [resource],\n );\n const reportError = onError ?? defaultOnError;\n\n const modeArgs = {\n queryClient,\n provider,\n resource,\n mode: mutationMode,\n undoDelayMs,\n onUndoable,\n failedLater: reportError,\n };\n\n const createMutation = useMutation<T, Error, Partial<T>>({\n mutationFn: async (data) => {\n const outcome = await executeWrite<T>({\n ...modeArgs,\n kind: \"create\",\n data: data as Record<string, unknown>,\n send: async ({ keepalive }) => {\n const result = await provider.create<T>(resource, { data, ...(keepalive ? { keepalive } : {}) });\n return { record: result.data, audit: result.audit };\n },\n committed: (record) => {\n invalidateAfter({ resource, kind: \"create\", id: record.id, record });\n onCreateSuccess?.(record);\n },\n });\n return outcome.status === \"committed\" ? outcome.record : (data as T);\n },\n onError: reportError,\n });\n\n const updateMutation = useMutation<\n T,\n Error,\n { id: Identifier; data: Partial<T> }\n >({\n mutationFn: async ({ id, data }) => {\n const before = cachedRecord(queryClient, resource, id);\n const outcome = await executeWrite<T>({\n ...modeArgs,\n kind: \"update\",\n id,\n data: data as Record<string, unknown>,\n send: async (ctx) => {\n const result = await provider.update<T>(resource, {\n id,\n data: (ctx.data ?? data) as Partial<T>,\n ...(ctx.keepalive ? { keepalive: true } : {}),\n });\n return { record: result.data, audit: result.audit };\n },\n committed: (record) => {\n invalidateAfter({ resource, kind: \"update\", id, record });\n onUpdateSuccess?.(record);\n },\n });\n return outcome.status === \"committed\"\n ? outcome.record\n : ({ ...(before ?? {}), ...data, id: before?.id ?? id } as T);\n },\n onError: reportError,\n });\n\n const deleteMutation = useMutation<void, Error, Identifier>({\n mutationFn: async (id) => {\n await executeWrite<void>({\n ...modeArgs,\n kind: \"delete\",\n id,\n send: async ({ keepalive }) => {\n const result = await provider.deleteOne(resource, { id, ...(keepalive ? { keepalive } : {}) });\n return { record: undefined, audit: result?.audit };\n },\n committed: () => {\n invalidateAfter({ resource, kind: \"delete\", id });\n onDeleteSuccess?.();\n },\n });\n },\n onError: reportError,\n });\n\n // -------------------------------------------------------------------------\n // Convenience wrappers\n // -------------------------------------------------------------------------\n\n const create = useCallback(\n (data: Partial<T>) => createMutation.mutateAsync(data),\n [createMutation],\n );\n\n const update = useCallback(\n (id: Identifier, data: Partial<T>) =>\n updateMutation.mutateAsync({ id, data }),\n [updateMutation],\n );\n\n const remove = useCallback(\n (id: Identifier) => deleteMutation.mutateAsync(id),\n [deleteMutation],\n );\n\n const isMutating =\n createMutation.isPending ||\n updateMutation.isPending ||\n deleteMutation.isPending;\n\n return {\n list,\n params,\n setSort,\n setFilters,\n setPage,\n setPerPage,\n setSearch,\n create,\n update,\n remove,\n isMutating,\n createMutation,\n updateMutation,\n deleteMutation,\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"]}