@octanejs/tanstack-query 0.1.2

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @octanejs/tanstack-query
2
+
3
+ [TanStack Query](https://tanstack.com/query) for the [octane](https://github.com/octanejs/octane) UI framework.
4
+
5
+ TanStack Query separates a framework-agnostic core (`@tanstack/query-core` — the
6
+ `QueryClient`, observers, and caches) from a React binding (`@tanstack/react-query`)
7
+ built on `useSyncExternalStore` + context. This package reuses the core verbatim and
8
+ reimplements only the binding on octane's hooks. The public surface matches
9
+ `@tanstack/react-query`, so most query code works by changing the import.
10
+
11
+ ```tsx
12
+ // before
13
+ import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
14
+ // after
15
+ import { QueryClient, QueryClientProvider, useQuery } from '@octanejs/tanstack-query';
16
+
17
+ const client = new QueryClient();
18
+
19
+ function Todos() @{
20
+ const q = useQuery({ queryKey: ['todos'], queryFn: fetchTodos });
21
+ @if (q.isPending) { <span>loading…</span> }
22
+ @else if (q.isError) { <span>{(q.error as Error).message as string}</span> }
23
+ @else { <ul>{q.data as unknown}</ul> }
24
+ }
25
+
26
+ function App() @{
27
+ <QueryClientProvider client={client}>
28
+ <Todos />
29
+ </QueryClientProvider>
30
+ }
31
+ ```
32
+
33
+ ## What's bound
34
+
35
+ - Queries: `useQuery`, `useInfiniteQuery`, `useSuspenseQuery`,
36
+ `useSuspenseInfiniteQuery`, `useQueries`, `usePrefetchQuery`,
37
+ `usePrefetchInfiniteQuery`
38
+ - Mutations: `useMutation`, `useMutationState`
39
+ - Status: `useIsFetching`, `useIsMutating`
40
+ - Components / context: `QueryClientProvider`, `useQueryClient`, `QueryClientContext`,
41
+ `HydrationBoundary`, `IsRestoringProvider`, `useIsRestoring`,
42
+ `QueryErrorResetBoundary`, `useQueryErrorResetBoundary`
43
+ - everything from `@tanstack/query-core` (`QueryClient`, `QueryCache`, observers,
44
+ `dehydrate`/`hydrate`, …), re-exported verbatim.
45
+
46
+ The whole `@tanstack/react-query` surface is bound. The separate companion packages
47
+ (`@tanstack/react-query-persist-client`, `@tanstack/react-query-devtools`) are not
48
+ included.
49
+
50
+ ## How it works
51
+
52
+ octane keys hooks by a compiler-injected per-call-site `Symbol`, appended as the last
53
+ argument of every `use*` call. The binding's hooks compose several base hooks
54
+ (`useState` for the observer, `useSyncExternalStore` to subscribe, `useEffect` for
55
+ option changes), so each forwards a distinct sub-slot derived from the one it receives.
56
+ `QueryClientProvider` is a component (it renders a context Provider + a mount effect),
57
+ so it's authored in `.tsrx`.
58
+
59
+ ## Suspense & error boundaries
60
+
61
+ A query with `suspense: true` suspends, and a query/mutation with `throwOnError` throws
62
+ its error. Catch them with either octane form — the `<Suspense>` / `<ErrorBoundary>`
63
+ components or the `@try { } @pending { } @catch { }` directive:
64
+
65
+ ```tsx
66
+ import { Suspense, ErrorBoundary } from 'octane';
67
+
68
+ <ErrorBoundary fallback={(err) => <Oops error={err} />}>
69
+ <Suspense fallback={<Spinner />}>
70
+ <Profile /> {/* useQuery({ queryKey, queryFn, suspense: true }) */}
71
+ </Suspense>
72
+ </ErrorBoundary>
73
+ ```
74
+
75
+ ## Persistence & error resets
76
+
77
+ `IsRestoringProvider` / `useIsRestoring` gate fetching while a persisted client is
78
+ restored, and `QueryErrorResetBoundary` / `useQueryErrorResetBoundary` coordinate
79
+ error-boundary retries with `@catch` / `<ErrorBoundary>` — call the boundary's
80
+ `reset()` alongside `useQueryErrorResetBoundary().reset()` so a `throwOnError` query
81
+ refetches instead of immediately re-throwing.
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@octanejs/tanstack-query",
3
+ "version": "0.1.2",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "description": "TanStack Query bindings for the octane renderer — reuses @tanstack/query-core and swaps the React binding for octane's hooks.",
7
+ "author": {
8
+ "name": "Dominic Gannaway",
9
+ "email": "dg@domgan.com"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/octanejs/octane.git",
17
+ "directory": "packages/tanstack-query"
18
+ },
19
+ "main": "src/index.ts",
20
+ "module": "src/index.ts",
21
+ "types": "src/index.ts",
22
+ "files": [
23
+ "src",
24
+ "README.md"
25
+ ],
26
+ "exports": {
27
+ ".": "./src/index.ts"
28
+ },
29
+ "dependencies": {
30
+ "@tanstack/query-core": "^5.0.0",
31
+ "octane": "0.1.3"
32
+ },
33
+ "devDependencies": {
34
+ "@tanstack/react-query": "^5.0.0",
35
+ "@tsrx/react": "^0.2.37",
36
+ "esbuild": "^0.28.1",
37
+ "react": "^19.2.0",
38
+ "react-dom": "^19.2.0",
39
+ "vitest": "^4.1.9"
40
+ },
41
+ "scripts": {
42
+ "test": "vitest run"
43
+ }
44
+ }
@@ -0,0 +1,74 @@
1
+ // HydrationBoundary — hydrates dehydrated query state into the client, then
2
+ // renders its children. Full port of react-query's HydrationBoundary.tsx:
3
+ //
4
+ // - NEW queries hydrate IN RENDER (after SSR, hydration must land before the
5
+ // children render; during a transition, prerender as much as is safe).
6
+ // - EXISTING queries hydrate in an EFFECT: during a transition we must not
7
+ // update live queries/observers until the transition commits — an aborted
8
+ // transition must not clobber committed data (octane has transitions, so
9
+ // upstream's abort-safety applies here too).
10
+ // - A dehydrated query counts as NEWER than the cache when its dataUpdatedAt
11
+ // is ahead, OR when it carries a streaming `promise` and was dehydrated
12
+ // after the cached data settled (`dehydratedAt > dataUpdatedAt`) — that's
13
+ // what lets later stream chunks re-hydrate an already-settled query.
14
+ import { useRef, useMemo, useEffect } from 'octane';
15
+ import { hydrate } from '@tanstack/query-core';
16
+ import { resolveClient } from './context.ts';
17
+
18
+ export function HydrationBoundary(props) @{
19
+ const client = resolveClient(props.queryClient);
20
+ const state = props.state;
21
+
22
+ const optionsRef = useRef(props.options);
23
+ useEffect(() => {
24
+ optionsRef.current = props.options;
25
+ });
26
+
27
+ // "In render" on purpose (the memo is a perf cache, not a semantic barrier) —
28
+ // hydrate new queries now, queue existing ones for the commit effect.
29
+ const hydrationQueue = useMemo(() => {
30
+ if (state && typeof state === 'object') {
31
+ const queryCache = client.getQueryCache();
32
+ const queries = state.queries || [];
33
+
34
+ const newQueries = [];
35
+ const existingQueries = [];
36
+ for (const dehydratedQuery of queries) {
37
+ const existingQuery = queryCache.get(dehydratedQuery.queryHash);
38
+ if (!existingQuery) {
39
+ newQueries.push(dehydratedQuery);
40
+ } else {
41
+ const hydrationIsNewer =
42
+ dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt ||
43
+ dehydratedQuery.promise && existingQuery.state.status !== 'pending' &&
44
+ existingQuery.state.fetchStatus !== 'fetching' &&
45
+ dehydratedQuery.dehydratedAt !== undefined &&
46
+ dehydratedQuery.dehydratedAt > existingQuery.state.dataUpdatedAt;
47
+ if (hydrationIsNewer) {
48
+ existingQueries.push(dehydratedQuery);
49
+ }
50
+ }
51
+ }
52
+
53
+ if (newQueries.length > 0) {
54
+ // hydrate() is idempotent for queries — safe to run in render.
55
+ hydrate(client, { queries: newQueries }, optionsRef.current);
56
+ }
57
+ if (existingQueries.length > 0) {
58
+ return existingQueries;
59
+ }
60
+ }
61
+ return undefined;
62
+ }, [client, state]);
63
+
64
+ // Existing-query hydration lands at commit time (transition abort-safe).
65
+ useEffect(() => {
66
+ if (hydrationQueue) {
67
+ hydrate(client, { queries: hydrationQueue }, optionsRef.current);
68
+ }
69
+ }, [client, hydrationQueue]);
70
+
71
+ <>
72
+ {props.children}
73
+ </>
74
+ }
@@ -0,0 +1,9 @@
1
+ // Type declaration for the .tsrx component (resolved by relative path).
2
+ import type { DehydratedState, HydrateOptions, QueryClient } from '@tanstack/query-core';
3
+
4
+ export declare const HydrationBoundary: (props: {
5
+ state: DehydratedState | null | undefined;
6
+ options?: HydrateOptions;
7
+ children?: unknown;
8
+ queryClient?: QueryClient;
9
+ }) => unknown;
@@ -0,0 +1,16 @@
1
+ import { useEffect } from 'octane';
2
+ import { QueryClientContext } from './context.ts';
3
+
4
+ // QueryClientProvider — provides the QueryClient via context and mounts/unmounts
5
+ // it with the component. It's authored in .tsrx because it's a COMPONENT that
6
+ // renders a context Provider with children + a mount effect, which only the
7
+ // octane compiler can emit (the binding's hooks stay plain TS).
8
+ export function QueryClientProvider(props) @{
9
+ useEffect(() => {
10
+ props.client.mount();
11
+ return () => {
12
+ props.client.unmount();
13
+ };
14
+ }, [props.client]);
15
+ <QueryClientContext.Provider value={props.client}>{props.children}</QueryClientContext.Provider>
16
+ }
@@ -0,0 +1,10 @@
1
+ // Type declaration for the .tsrx provider component (QueryClientProvider.tsrx).
2
+ // It's a SPECIFIC module declaration (resolved by relative path), not an ambient
3
+ // `declare module '*.tsrx'` — so it types only this module and doesn't pollute a
4
+ // consumer's own .tsrx imports. The runtime resolves the real compiled .tsrx.
5
+ import type { QueryClient } from '@tanstack/query-core';
6
+
7
+ export declare const QueryClientProvider: (props: {
8
+ client: QueryClient;
9
+ children?: unknown;
10
+ }) => unknown;
@@ -0,0 +1,21 @@
1
+ import { useState, isChildrenBlock } from 'octane';
2
+ import { QueryErrorResetBoundaryContext, createValue } from './errorResetBoundary.ts';
3
+
4
+ // QueryErrorResetBoundary — provides a fresh reset coordinator to its subtree.
5
+ // Place it above an error boundary; call `.reset()` from the boundary's retry so
6
+ // the query refetches instead of immediately re-throwing. Children may be a
7
+ // render prop receiving the boundary value — upstream's documented primary form:
8
+ // `<QueryErrorResetBoundary>{({ reset }) => <ErrorBoundary onReset={reset}…/>}
9
+ // </QueryErrorResetBoundary>` (react-query QueryErrorResetBoundary.tsx renders
10
+ // `typeof children === 'function' ? children(value) : children`; octane
11
+ // additionally excludes compiled children-blocks via isChildrenBlock).
12
+ export function QueryErrorResetBoundary(props) @{
13
+ const [value] = useState(() => createValue());
14
+ const resolved =
15
+ typeof props.children === 'function' && !isChildrenBlock(props.children)
16
+ ? props.children(value)
17
+ : props.children;
18
+ <QueryErrorResetBoundaryContext.Provider
19
+ value={value}
20
+ >{resolved}</QueryErrorResetBoundaryContext.Provider>
21
+ }
@@ -0,0 +1,4 @@
1
+ // Type declaration for the .tsrx QueryErrorResetBoundary component (see
2
+ // QueryErrorResetBoundary.tsrx). A specific module declaration (not an ambient
3
+ // `*.tsrx` wildcard), so it doesn't pollute a consumer's own .tsrx imports.
4
+ export declare const QueryErrorResetBoundary: (props: { children?: unknown }) => unknown;
package/src/context.ts ADDED
@@ -0,0 +1,29 @@
1
+ import { createContext, useContext } from 'octane';
2
+ import type { QueryClient } from '@tanstack/query-core';
3
+
4
+ // The QueryClient context — read by useQuery/useMutation/useQueryClient. Mirrors
5
+ // react-query's QueryClientContext.
6
+ export const QueryClientContext = createContext<QueryClient | undefined>(undefined);
7
+
8
+ // Resolve the client: an explicitly-passed one wins, else the context value.
9
+ // `useContext` is keyed by context identity (not a per-call-site slot), so it's
10
+ // safe to call from this binding code without a compiler-injected slot.
11
+ export function resolveClient(queryClient: QueryClient | undefined): QueryClient {
12
+ const ctxClient = useContext(QueryClientContext);
13
+ const client = queryClient ?? ctxClient;
14
+ if (!client) {
15
+ throw new Error('No QueryClient set, use QueryClientProvider to set one');
16
+ }
17
+ return client;
18
+ }
19
+
20
+ // Signature matches @tanstack/react-query's QueryClientProvider.tsx.
21
+ export function useQueryClient(queryClient?: QueryClient): QueryClient;
22
+
23
+ export function useQueryClient(...args: unknown[]): QueryClient {
24
+ // Args are `[queryClient?, slot?]`; the slot (symbol) is appended by the
25
+ // compiler. An explicit client is the first non-symbol arg.
26
+ const queryClient =
27
+ args.length && typeof args[0] !== 'symbol' ? (args[0] as QueryClient) : undefined;
28
+ return resolveClient(queryClient);
29
+ }
@@ -0,0 +1,28 @@
1
+ import { createContext, useContext } from 'octane';
2
+ import type { QueryErrorResetBoundaryValue } from './types';
3
+
4
+ export type { QueryErrorResetBoundaryValue };
5
+
6
+ export function createValue(): QueryErrorResetBoundaryValue {
7
+ let isReset = false;
8
+ return {
9
+ clearReset: () => {
10
+ isReset = false;
11
+ },
12
+ reset: () => {
13
+ isReset = true;
14
+ },
15
+ isReset: () => isReset,
16
+ };
17
+ }
18
+
19
+ // The reset coordinator for error-boundary retries. While "reset", a thrown query
20
+ // error is NOT re-thrown (so the boundary's retry refetches instead of looping).
21
+ // The default value means: without a `<QueryErrorResetBoundary>`, `isReset()` is
22
+ // always false — identical to having no boundary.
23
+ export const QueryErrorResetBoundaryContext =
24
+ createContext<QueryErrorResetBoundaryValue>(createValue());
25
+
26
+ export function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue {
27
+ return useContext(QueryErrorResetBoundaryContext);
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,74 @@
1
+ // @octanejs/tanstack-query — TanStack Query for the octane renderer.
2
+ //
3
+ // Reuses @tanstack/query-core verbatim (QueryClient, observers, caches — all
4
+ // framework-agnostic) and reimplements the React binding on octane's hooks. The
5
+ // public surface matches @tanstack/react-query, so most query code works by
6
+ // changing the import.
7
+ export * from '@tanstack/query-core';
8
+
9
+ export { useQuery } from './useQuery';
10
+ export { useMutation } from './useMutation';
11
+ export { useInfiniteQuery } from './useInfiniteQuery';
12
+ export { useSuspenseQuery, useSuspenseInfiniteQuery } from './useSuspenseQuery';
13
+ export { useSuspenseQueries } from './useSuspenseQueries';
14
+ export { usePrefetchQuery, usePrefetchInfiniteQuery } from './usePrefetch';
15
+ export { useQueries } from './useQueries';
16
+ export { queryOptions } from './queryOptions';
17
+ export { infiniteQueryOptions } from './infiniteQueryOptions';
18
+ export { mutationOptions } from './mutationOptions';
19
+ export { useIsFetching } from './useIsFetching';
20
+ export { useMutationState, useIsMutating } from './useMutationState';
21
+ export { useQueryClient, QueryClientContext } from './context';
22
+ export { QueryClientProvider } from './QueryClientProvider.tsrx';
23
+ export { HydrationBoundary } from './HydrationBoundary.tsrx';
24
+ export { IsRestoringProvider, useIsRestoring } from './isRestoring';
25
+ export { QueryErrorResetBoundary } from './QueryErrorResetBoundary.tsrx';
26
+ export { useQueryErrorResetBoundary } from './errorResetBoundary';
27
+
28
+ // The binding-level type surface (1:1 with @tanstack/react-query's exports).
29
+ export type {
30
+ AnyUseBaseQueryOptions,
31
+ UseBaseQueryOptions,
32
+ UsePrefetchQueryOptions,
33
+ AnyUseQueryOptions,
34
+ UseQueryOptions,
35
+ AnyUseSuspenseQueryOptions,
36
+ UseSuspenseQueryOptions,
37
+ AnyUseInfiniteQueryOptions,
38
+ UseInfiniteQueryOptions,
39
+ AnyUseSuspenseInfiniteQueryOptions,
40
+ UseSuspenseInfiniteQueryOptions,
41
+ UseBaseQueryResult,
42
+ UseQueryResult,
43
+ UseSuspenseQueryResult,
44
+ DefinedUseQueryResult,
45
+ UseInfiniteQueryResult,
46
+ DefinedUseInfiniteQueryResult,
47
+ UseSuspenseInfiniteQueryResult,
48
+ AnyUseMutationOptions,
49
+ UseMutationOptions,
50
+ UseMutateFunction,
51
+ UseMutateAsyncFunction,
52
+ UseBaseMutationResult,
53
+ UseMutationResult,
54
+ QueryClientProviderProps,
55
+ HydrationBoundaryProps,
56
+ QueryErrorResetFunction,
57
+ QueryErrorIsResetFunction,
58
+ QueryErrorClearResetFunction,
59
+ QueryErrorResetBoundaryValue,
60
+ QueryErrorResetBoundaryFunction,
61
+ QueryErrorResetBoundaryProps,
62
+ } from './types';
63
+ export type {
64
+ DefinedInitialDataOptions,
65
+ UndefinedInitialDataOptions,
66
+ UnusedSkipTokenOptions,
67
+ } from './queryOptions';
68
+ export type {
69
+ DefinedInitialDataInfiniteOptions,
70
+ UndefinedInitialDataInfiniteOptions,
71
+ UnusedSkipTokenInfiniteOptions,
72
+ } from './infiniteQueryOptions';
73
+ export type { QueriesOptions, QueriesResults } from './queries-types';
74
+ export type { SuspenseQueriesOptions, SuspenseQueriesResults } from './suspense-queries-types';
@@ -0,0 +1,94 @@
1
+ // Verbatim port of @tanstack/react-query's infiniteQueryOptions.ts — a typed identity helper
2
+ import type {
3
+ DataTag,
4
+ DefaultError,
5
+ InfiniteData,
6
+ InitialDataFunction,
7
+ NonUndefinedGuard,
8
+ OmitKeyof,
9
+ QueryKey,
10
+ SkipToken,
11
+ } from '@tanstack/query-core';
12
+ import type { UseInfiniteQueryOptions } from './types';
13
+
14
+ export type UndefinedInitialDataInfiniteOptions<
15
+ TQueryFnData,
16
+ TError = DefaultError,
17
+ TData = InfiniteData<TQueryFnData>,
18
+ TQueryKey extends QueryKey = QueryKey,
19
+ TPageParam = unknown,
20
+ > = UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
21
+ initialData?:
22
+ | undefined
23
+ | NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
24
+ | InitialDataFunction<NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>>;
25
+ };
26
+
27
+ export type UnusedSkipTokenInfiniteOptions<
28
+ TQueryFnData,
29
+ TError = DefaultError,
30
+ TData = InfiniteData<TQueryFnData>,
31
+ TQueryKey extends QueryKey = QueryKey,
32
+ TPageParam = unknown,
33
+ > = OmitKeyof<
34
+ UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
35
+ 'queryFn'
36
+ > & {
37
+ queryFn?: Exclude<
38
+ UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>['queryFn'],
39
+ SkipToken | undefined
40
+ >;
41
+ };
42
+
43
+ export type DefinedInitialDataInfiniteOptions<
44
+ TQueryFnData,
45
+ TError = DefaultError,
46
+ TData = InfiniteData<TQueryFnData>,
47
+ TQueryKey extends QueryKey = QueryKey,
48
+ TPageParam = unknown,
49
+ > = UseInfiniteQueryOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
50
+ initialData:
51
+ | NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>
52
+ | (() => NonUndefinedGuard<InfiniteData<TQueryFnData, TPageParam>>)
53
+ | undefined;
54
+ };
55
+
56
+ export function infiniteQueryOptions<
57
+ TQueryFnData,
58
+ TError = DefaultError,
59
+ TData = InfiniteData<TQueryFnData>,
60
+ TQueryKey extends QueryKey = QueryKey,
61
+ TPageParam = unknown,
62
+ >(
63
+ options: DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
64
+ ): DefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
65
+ queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
66
+ };
67
+
68
+ export function infiniteQueryOptions<
69
+ TQueryFnData,
70
+ TError = DefaultError,
71
+ TData = InfiniteData<TQueryFnData>,
72
+ TQueryKey extends QueryKey = QueryKey,
73
+ TPageParam = unknown,
74
+ >(
75
+ options: UnusedSkipTokenInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
76
+ ): UnusedSkipTokenInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
77
+ queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
78
+ };
79
+
80
+ export function infiniteQueryOptions<
81
+ TQueryFnData,
82
+ TError = DefaultError,
83
+ TData = InfiniteData<TQueryFnData>,
84
+ TQueryKey extends QueryKey = QueryKey,
85
+ TPageParam = unknown,
86
+ >(
87
+ options: UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam>,
88
+ ): UndefinedInitialDataInfiniteOptions<TQueryFnData, TError, TData, TQueryKey, TPageParam> & {
89
+ queryKey: DataTag<TQueryKey, InfiniteData<TQueryFnData>, TError>;
90
+ };
91
+
92
+ export function infiniteQueryOptions(options: unknown) {
93
+ return options;
94
+ }
@@ -0,0 +1,115 @@
1
+ // Shared internals for the binding hooks.
2
+ import { shouldThrowError } from '@tanstack/query-core';
3
+
4
+ // Derive a stable, distinct sub-slot from a wrapper's compiler-injected slot, so
5
+ // a hook composing multiple base hooks gives each one its own identity. Tags are
6
+ // namespaced per hook (e.g. ':oq:obs', ':ms:cb') to avoid cross-hook collisions.
7
+ // Memoized: subSlot runs on EVERY hook call every render, and the naive form
8
+ // pays a string concat + global symbol-registry lookup each time. The cache is
9
+ // keyed by the slot symbol itself; the minted value is byte-identical to the
10
+ // uncached Symbol.for result, so identity is preserved across HMR re-evals and
11
+ // the per-package copies of this helper. Key universe is bounded: slots are
12
+ // per-call-site module constants (never minted per render).
13
+ const subSlotCache = new Map<symbol, Map<string, symbol>>();
14
+ export function subSlot(slot: symbol | undefined, tag: string): symbol | undefined {
15
+ if (slot === undefined) return undefined;
16
+ let byTag = subSlotCache.get(slot);
17
+ if (byTag === undefined) subSlotCache.set(slot, (byTag = new Map()));
18
+ let sym = byTag.get(tag);
19
+ if (sym === undefined) byTag.set(tag, (sym = Symbol.for((slot.description ?? '') + ':' + tag)));
20
+ return sym;
21
+ }
22
+
23
+ // Split the compiler-injected trailing slot off a hook's runtime args, returning
24
+ // the user args (everything before it) and the slot.
25
+ export function splitSlot(args: any[]): [any[], symbol | undefined] {
26
+ const tail = args[args.length - 1];
27
+ const slot = typeof tail === 'symbol' ? (tail as symbol) : undefined;
28
+ return [slot !== undefined ? args.slice(0, -1) : args, slot];
29
+ }
30
+
31
+ // react-query's default suspense throwOnError: only throw if there's no data.
32
+ export const defaultThrowOnError = (_error: unknown, query: any): boolean =>
33
+ query.state.data === undefined;
34
+
35
+ // react-query's ensureSuspenseTimers: a suspense query gets a >=1s staleTime/gcTime
36
+ // floor so it can't immediately refetch and re-trigger the fallback in a loop.
37
+ export function ensureSuspenseTimers(defaultedOptions: any): void {
38
+ if (defaultedOptions.suspense) {
39
+ const MIN = 1000;
40
+ const clamp = (value: any) => (value === 'static' ? value : Math.max(value ?? MIN, MIN));
41
+ const orig = defaultedOptions.staleTime;
42
+ defaultedOptions.staleTime =
43
+ typeof orig === 'function' ? (...args: any[]) => clamp(orig(...args)) : clamp(orig);
44
+ if (typeof defaultedOptions.gcTime === 'number') {
45
+ defaultedOptions.gcTime = Math.max(defaultedOptions.gcTime, MIN);
46
+ }
47
+ }
48
+ }
49
+
50
+ // prevent-error-boundary-retry: when a query opts into throwing, don't retry on
51
+ // mount so an already-errored cached query re-throws immediately — UNLESS the
52
+ // reset boundary has been reset, in which case a retry is expected.
53
+ export function ensurePreventErrorBoundaryRetry(
54
+ options: any,
55
+ errorResetBoundary: { isReset: () => boolean },
56
+ query: any,
57
+ ): void {
58
+ const throwOnError =
59
+ query?.state.error && typeof options.throwOnError === 'function'
60
+ ? shouldThrowError(options.throwOnError, [query.state.error, query])
61
+ : options.throwOnError;
62
+ if (options.suspense || options.experimental_prefetchInRender || throwOnError) {
63
+ if (!errorResetBoundary.isReset()) {
64
+ options.retryOnMount = false;
65
+ }
66
+ }
67
+ }
68
+
69
+ export const shouldSuspend = (defaultedOptions: any, result: any): boolean =>
70
+ defaultedOptions?.suspense && result.isPending;
71
+
72
+ // A pending result that will actually fetch (not blocked by persistence restore).
73
+ export const willFetch = (result: any, isRestoring: boolean): boolean =>
74
+ result.isLoading && result.isFetching && !isRestoring;
75
+
76
+ // react-query's suspense fetch: kick the optimistic fetch and — CRUCIALLY — clear
77
+ // the reset boundary on error. Without the clearReset, a boundary reset→retry
78
+ // that fails AGAIN leaves `isReset()` true at replay, `getHasError` returns
79
+ // false, and a suspense component falls through to render with undefined data
80
+ // instead of re-throwing to the boundary. The returned promise resolves even on
81
+ // error (the replay surfaces the error through the error-boundary throw).
82
+ export function fetchOptimistic(
83
+ defaultedOptions: any,
84
+ observer: any,
85
+ errorResetBoundary: { clearReset: () => void },
86
+ ): Promise<unknown> {
87
+ return observer.fetchOptimistic(defaultedOptions).catch(() => {
88
+ errorResetBoundary.clearReset();
89
+ });
90
+ }
91
+
92
+ // react-query's getHasError: only throw if the query errored, the boundary isn't
93
+ // reset, the query isn't refetching, and the options opt into throwing.
94
+ export function getHasError({
95
+ result,
96
+ errorResetBoundary,
97
+ throwOnError,
98
+ query,
99
+ suspense,
100
+ }: {
101
+ result: any;
102
+ errorResetBoundary: { isReset: () => boolean };
103
+ throwOnError: any;
104
+ query: any;
105
+ suspense: any;
106
+ }): boolean {
107
+ return (
108
+ result.isError &&
109
+ !errorResetBoundary.isReset() &&
110
+ !result.isFetching &&
111
+ query &&
112
+ ((suspense && result.data === undefined) ||
113
+ shouldThrowError(throwOnError, [result.error, query]))
114
+ );
115
+ }
@@ -0,0 +1,13 @@
1
+ import { createContext, useContext } from 'octane';
2
+
3
+ // IsRestoring — true while a persisted client is being restored from storage. While
4
+ // restoring, queries read the cache but don't subscribe/fetch (they wait for the
5
+ // restore to finish). Defaults to false, so without a provider it's inert.
6
+ export const IsRestoringContext = createContext(false);
7
+
8
+ export function useIsRestoring(): boolean {
9
+ return useContext(IsRestoringContext);
10
+ }
11
+
12
+ // `<IsRestoringProvider value={true}>…` — octane's built-in context Provider.
13
+ export const IsRestoringProvider = IsRestoringContext.Provider;