diff --git a/.changeset/calm-tools-work.md b/.changeset/calm-tools-work.md new file mode 100644 index 00000000000..75f04056919 --- /dev/null +++ b/.changeset/calm-tools-work.md @@ -0,0 +1,5 @@ +--- +'@tanstack/octane-query': minor +--- + +Add the official TanStack Query adapter for Octane. diff --git a/.changeset/config.json b/.changeset/config.json index 984e1d89f1c..b3e26951bdf 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -13,6 +13,7 @@ "@tanstack/angular-query-experimental", "@tanstack/angular-query-persist-client", "@tanstack/eslint-plugin-query", + "@tanstack/octane-query", "@tanstack/preact-query", "@tanstack/preact-query-devtools", "@tanstack/preact-query-persist-client", diff --git a/docs/config.json b/docs/config.json index a68832565dc..d8a0a39f6d7 100644 --- a/docs/config.json +++ b/docs/config.json @@ -217,6 +217,23 @@ "to": "framework/preact/graphql" } ] + }, + { + "label": "octane", + "children": [ + { + "label": "Overview", + "to": "framework/octane/overview" + }, + { + "label": "Installation", + "to": "framework/octane/installation" + }, + { + "label": "Quick Start", + "to": "framework/octane/quick-start" + } + ] } ] }, @@ -949,6 +966,19 @@ "to": "framework/preact/guides/does-this-replace-client-state" } ] + }, + { + "label": "octane", + "children": [ + { + "label": "Suspense and Error Boundaries", + "to": "framework/octane/guides/suspense" + }, + { + "label": "Server Rendering and Hydration", + "to": "framework/octane/guides/ssr" + } + ] } ] }, @@ -1427,6 +1457,15 @@ "to": "framework/preact/reference/functions/HydrationBoundary" } ] + }, + { + "label": "octane", + "children": [ + { + "label": "Octane Reference", + "to": "framework/octane/reference/index" + } + ] } ] }, diff --git a/docs/framework/octane/guides/ssr.md b/docs/framework/octane/guides/ssr.md new file mode 100644 index 00000000000..ba6b18a410d --- /dev/null +++ b/docs/framework/octane/guides/ssr.md @@ -0,0 +1,63 @@ +--- +id: ssr +title: Server Rendering and Hydration +--- + +Octane Query supports both prefetched hydration and render-time Suspense on the +server. The binding re-exports Query Core's `dehydrate` and `hydrate` functions, +and `HydrationBoundary` accepts dehydrated queries, including pending queries +that resolve in a later stream chunk. + +## Per-request clients + +Create a new `QueryClient` for every server request. Never share one server +client between users. + +```ts +import { QueryClient, dehydrate, hydrate } from '@tanstack/octane-query' + +export async function loadQueryState() { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { staleTime: 60_000 }, + }, + }) + + await queryClient.prefetchQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + }) + + return { + queryClient, + state: dehydrate(queryClient), + } +} + +export function restoreQueryState(state: unknown) { + const queryClient = new QueryClient() + hydrate(queryClient, state) + return queryClient +} +``` + +Serialize the dehydrated state through the application's SSR pipeline, restore +it before the browser renders query consumers, and provide the browser client +with `QueryClientProvider`. Use `HydrationBoundary` when only a subtree owns a +dehydrated state payload. + +The adapter owns Query's Octane binding and hydration behavior. Routing, +serialization, and response streaming remain responsibilities of the +application's TanStack Start or other SSR pipeline. + +## Render-time Suspense + +Suspense hooks can start a query during server rendering. Octane tracks each +thenable with the compiler-provided hook slot, so sequential suspense calls and +streamed continuations resume at the correct call site. Wrap those consumers in +an Octane Suspense boundary and let the SSR renderer stream the resolved +content. + +For persisted browser caches, wrap consumers in `IsRestoringProvider` while the +restore is in progress. Query observers will avoid fetching until restoration +finishes. diff --git a/docs/framework/octane/guides/suspense.md b/docs/framework/octane/guides/suspense.md new file mode 100644 index 00000000000..219b64fb975 --- /dev/null +++ b/docs/framework/octane/guides/suspense.md @@ -0,0 +1,43 @@ +--- +id: suspense +title: Suspense and Error Boundaries +--- + +Use `useSuspenseQuery`, `useSuspenseInfiniteQuery`, or +`useSuspenseQueries` when pending work should be handled by an Octane Suspense +boundary. These hooks integrate with Octane's `use(thenable)` behavior and +return results whose `data` is defined after the boundary resolves. + +```tsrx +import { ErrorBoundary, Suspense } from 'octane' +import { useSuspenseQuery } from '@tanstack/octane-query' + +function Profile() @{ + const profile = useSuspenseQuery({ + queryKey: ['profile'], + queryFn: fetchProfile, + }) + +

{profile.data.name}

+} + +function Page() @{ +

{error.message}

}> + {'Loading...'}

}> + +
+
+} +``` + +Octane's `@pending` and `@catch` directives can be used instead of the +component boundaries. + +By default, a suspense query only throws an error when no cached data is +available. Set `throwOnError` on non-suspense queries or mutations when their +errors should reach the nearest error boundary. + +Use `QueryErrorResetBoundary` or `useQueryErrorResetBoundary` to coordinate a +boundary retry with the Query cache. Reset the Query boundary when the Octane +error boundary retries so the failed query can fetch again instead of +immediately throwing the same error. diff --git a/docs/framework/octane/installation.md b/docs/framework/octane/installation.md new file mode 100644 index 00000000000..5b43e44d5ba --- /dev/null +++ b/docs/framework/octane/installation.md @@ -0,0 +1,24 @@ +--- +id: installation +title: Installation +--- + +Install the official adapter and Octane: + +```bash +pnpm add @tanstack/octane-query octane +``` + +Or use the equivalent command for npm, Yarn, or Bun. + +`octane` is a peer dependency. `@tanstack/query-core` is included by the +adapter and all of its public APIs are re-exported from +`@tanstack/octane-query`. + +## Compiler requirement + +The adapter publishes TSRX source so Octane can compile the correct client and +server output. Use Octane's standard Vite integration in the consuming app. +The package is not a precompiled Node.js runtime entry point. + +The adapter requires Node.js 22 or newer for its package tooling. diff --git a/docs/framework/octane/overview.md b/docs/framework/octane/overview.md new file mode 100644 index 00000000000..0d3c3bfdeb1 --- /dev/null +++ b/docs/framework/octane/overview.md @@ -0,0 +1,52 @@ +--- +id: overview +title: Overview +--- + +`@tanstack/octane-query` is the official Octane adapter for TanStack Query. It +uses `@tanstack/query-core` unchanged and binds Query observers to Octane's +hooks, context, Suspense, and component lifecycle. + +Most binding-level code can move from React Query by changing the import. The +adapter includes queries, infinite queries, parallel queries, mutations, +prefetch hooks, cache status hooks, Suspense hooks, error reset boundaries, +restoration state, and hydration. It also re-exports every Query Core API. + +```tsrx +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/octane-query' + +const queryClient = new QueryClient() + +function Todos() @{ + const query = useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + }) + + @if (query.isPending) { + {'Loading'} + } @else if (query.isError) { + {query.error.message} + } @else { + + } +} + +function App() @{ + + + +} +``` + +Octane hooks receive compiler-injected call-site slots. The adapter derives +stable sub-slots for the Query observer, subscription, and effects, so call the +hooks normally and let the Octane compiler transform them. + +Start with [Installation](./installation.md), then [Quick Start](./quick-start.md). +See [Suspense and Error Boundaries](./guides/suspense.md) and [Server Rendering +and Hydration](./guides/ssr.md) for the framework-specific behavior. diff --git a/docs/framework/octane/quick-start.md b/docs/framework/octane/quick-start.md new file mode 100644 index 00000000000..e96fa509900 --- /dev/null +++ b/docs/framework/octane/quick-start.md @@ -0,0 +1,60 @@ +--- +id: quick-start +title: Quick Start +--- + +Create one `QueryClient` for the browser application, provide it above query +consumers, and call the Octane hooks from component bodies. + +```tsrx +import { + QueryClient, + QueryClientProvider, + useMutation, + useQuery, + useQueryClient, +} from '@tanstack/octane-query' +import { addTodo, getTodos } from './api' + +const queryClient = new QueryClient() + +function Todos() @{ + const client = useQueryClient() + const todos = useQuery({ + queryKey: ['todos'], + queryFn: getTodos, + }) + const addTodoMutation = useMutation({ + mutationFn: addTodo, + onSuccess: async () => { + await client.invalidateQueries({ queryKey: ['todos'] }) + }, + }) + + @if (todos.isPending) { +

{'Loading...'}

+ } @else if (todos.isError) { +

{todos.error.message}

+ } @else { + + + } +} + +function App() @{ + + + +} +``` + +Query option objects, keys, invalidation, retries, caching, and mutations use +the same TanStack Query contracts as the other framework adapters. The Octane +adapter changes the rendering and subscription layer, not Query Core. diff --git a/docs/framework/octane/reference/functions/infiniteQueryOptions.md b/docs/framework/octane/reference/functions/infiniteQueryOptions.md new file mode 100644 index 00000000000..564cc27340a --- /dev/null +++ b/docs/framework/octane/reference/functions/infiniteQueryOptions.md @@ -0,0 +1,126 @@ +--- +id: infiniteQueryOptions +title: infiniteQueryOptions +--- + +# Function: infiniteQueryOptions() + +## Call Signature + +```ts +function infiniteQueryOptions(options): UseInfiniteQueryOptions & object & object; +``` + +Defined in: packages/octane-query/src/infiniteQueryOptions.ts:76 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +#### TPageParam + +`TPageParam` = `unknown` + +### Parameters + +#### options + +[`DefinedInitialDataInfiniteOptions`](../type-aliases/DefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +### Returns + +[`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> & `object` & `object` + +## Call Signature + +```ts +function infiniteQueryOptions(options): OmitKeyof, "queryFn"> & object & object; +``` + +Defined in: packages/octane-query/src/infiniteQueryOptions.ts:100 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +#### TPageParam + +`TPageParam` = `unknown` + +### Parameters + +#### options + +[`UnusedSkipTokenInfiniteOptions`](../type-aliases/UnusedSkipTokenInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +### Returns + +`OmitKeyof`\<[`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"queryFn"`\> & `object` & `object` + +## Call Signature + +```ts +function infiniteQueryOptions(options): UseInfiniteQueryOptions & object & object; +``` + +Defined in: packages/octane-query/src/infiniteQueryOptions.ts:124 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +#### TPageParam + +`TPageParam` = `unknown` + +### Parameters + +#### options + +[`UndefinedInitialDataInfiniteOptions`](../type-aliases/UndefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +### Returns + +[`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> & `object` & `object` diff --git a/docs/framework/octane/reference/functions/mutationOptions.md b/docs/framework/octane/reference/functions/mutationOptions.md new file mode 100644 index 00000000000..c297d4a9dbe --- /dev/null +++ b/docs/framework/octane/reference/functions/mutationOptions.md @@ -0,0 +1,78 @@ +--- +id: mutationOptions +title: mutationOptions +--- + +# Function: mutationOptions() + +## Call Signature + +```ts +function mutationOptions(options): WithRequired, "mutationKey">; +``` + +Defined in: packages/octane-query/src/mutationOptions.ts:5 + +### Type Parameters + +#### TData + +`TData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TVariables + +`TVariables` = `void` + +#### TOnMutateResult + +`TOnMutateResult` = `unknown` + +### Parameters + +#### options + +`WithRequired`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> + +### Returns + +`WithRequired`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> + +## Call Signature + +```ts +function mutationOptions(options): Omit, "mutationKey">; +``` + +Defined in: packages/octane-query/src/mutationOptions.ts:19 + +### Type Parameters + +#### TData + +`TData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TVariables + +`TVariables` = `void` + +#### TOnMutateResult + +`TOnMutateResult` = `unknown` + +### Parameters + +#### options + +`Omit`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> + +### Returns + +`Omit`\<[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"mutationKey"`\> diff --git a/docs/framework/octane/reference/functions/queryOptions.md b/docs/framework/octane/reference/functions/queryOptions.md new file mode 100644 index 00000000000..0eaa0dd323a --- /dev/null +++ b/docs/framework/octane/reference/functions/queryOptions.md @@ -0,0 +1,114 @@ +--- +id: queryOptions +title: queryOptions +--- + +# Function: queryOptions() + +## Call Signature + +```ts +function queryOptions(options): Omit, "queryFn"> & object & object; +``` + +Defined in: packages/octane-query/src/queryOptions.ts:53 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `TQueryFnData` + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### Parameters + +#### options + +[`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +### Returns + +`Omit`\<[`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` + +## Call Signature + +```ts +function queryOptions(options): OmitKeyof, "queryFn"> & object & object; +``` + +Defined in: packages/octane-query/src/queryOptions.ts:64 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `TQueryFnData` + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### Parameters + +#### options + +[`UnusedSkipTokenOptions`](../type-aliases/UnusedSkipTokenOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +### Returns + +`OmitKeyof`\<[`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> & `object` & `object` + +## Call Signature + +```ts +function queryOptions(options): UseQueryOptions & object & object; +``` + +Defined in: packages/octane-query/src/queryOptions.ts:75 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `TQueryFnData` + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### Parameters + +#### options + +[`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +### Returns + +[`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> & `object` & `object` diff --git a/docs/framework/octane/reference/functions/useInfiniteQuery.md b/docs/framework/octane/reference/functions/useInfiniteQuery.md new file mode 100644 index 00000000000..2d90a95204f --- /dev/null +++ b/docs/framework/octane/reference/functions/useInfiniteQuery.md @@ -0,0 +1,138 @@ +--- +id: useInfiniteQuery +title: useInfiniteQuery +--- + +# Function: useInfiniteQuery() + +## Call Signature + +```ts +function useInfiniteQuery(options, queryClient?): DefinedUseInfiniteQueryResult; +``` + +Defined in: packages/octane-query/src/useInfiniteQuery.ts:21 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +#### TPageParam + +`TPageParam` = `unknown` + +### Parameters + +#### options + +[`DefinedInitialDataInfiniteOptions`](../type-aliases/DefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +#### queryClient? + +`QueryClient` + +### Returns + +[`DefinedUseInfiniteQueryResult`](../type-aliases/DefinedUseInfiniteQueryResult.md)\<`TData`, `TError`\> + +## Call Signature + +```ts +function useInfiniteQuery(options, queryClient?): UseInfiniteQueryResult; +``` + +Defined in: packages/octane-query/src/useInfiniteQuery.ts:38 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +#### TPageParam + +`TPageParam` = `unknown` + +### Parameters + +#### options + +[`UndefinedInitialDataInfiniteOptions`](../type-aliases/UndefinedInitialDataInfiniteOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +#### queryClient? + +`QueryClient` + +### Returns + +[`UseInfiniteQueryResult`](../type-aliases/UseInfiniteQueryResult.md)\<`TData`, `TError`\> + +## Call Signature + +```ts +function useInfiniteQuery(options, queryClient?): UseInfiniteQueryResult; +``` + +Defined in: packages/octane-query/src/useInfiniteQuery.ts:55 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +#### TPageParam + +`TPageParam` = `unknown` + +### Parameters + +#### options + +[`UseInfiniteQueryOptions`](../interfaces/UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +#### queryClient? + +`QueryClient` + +### Returns + +[`UseInfiniteQueryResult`](../type-aliases/UseInfiniteQueryResult.md)\<`TData`, `TError`\> diff --git a/docs/framework/octane/reference/functions/useIsFetching.md b/docs/framework/octane/reference/functions/useIsFetching.md new file mode 100644 index 00000000000..3b027d771ae --- /dev/null +++ b/docs/framework/octane/reference/functions/useIsFetching.md @@ -0,0 +1,26 @@ +--- +id: useIsFetching +title: useIsFetching +--- + +# Function: useIsFetching() + +```ts +function useIsFetching(filters?, queryClient?): number; +``` + +Defined in: packages/octane-query/src/useIsFetching.ts:8 + +## Parameters + +### filters? + +`QueryFilters`\ + +### queryClient? + +`QueryClient` + +## Returns + +`number` diff --git a/docs/framework/octane/reference/functions/useIsMutating.md b/docs/framework/octane/reference/functions/useIsMutating.md new file mode 100644 index 00000000000..b31853f334d --- /dev/null +++ b/docs/framework/octane/reference/functions/useIsMutating.md @@ -0,0 +1,26 @@ +--- +id: useIsMutating +title: useIsMutating +--- + +# Function: useIsMutating() + +```ts +function useIsMutating(filters?, queryClient?): number; +``` + +Defined in: packages/octane-query/src/useMutationState.ts:73 + +## Parameters + +### filters? + +`MutationFilters`\<`unknown`, `Error`, `unknown`, `unknown`\> + +### queryClient? + +`QueryClient` + +## Returns + +`number` diff --git a/docs/framework/octane/reference/functions/useIsRestoring.md b/docs/framework/octane/reference/functions/useIsRestoring.md new file mode 100644 index 00000000000..148632fb7d3 --- /dev/null +++ b/docs/framework/octane/reference/functions/useIsRestoring.md @@ -0,0 +1,16 @@ +--- +id: useIsRestoring +title: useIsRestoring +--- + +# Function: useIsRestoring() + +```ts +function useIsRestoring(): boolean; +``` + +Defined in: packages/octane-query/src/isRestoring.ts:8 + +## Returns + +`boolean` diff --git a/docs/framework/octane/reference/functions/useMutation.md b/docs/framework/octane/reference/functions/useMutation.md new file mode 100644 index 00000000000..613975227e8 --- /dev/null +++ b/docs/framework/octane/reference/functions/useMutation.md @@ -0,0 +1,44 @@ +--- +id: useMutation +title: useMutation +--- + +# Function: useMutation() + +```ts +function useMutation(options, queryClient?): UseMutationResult; +``` + +Defined in: packages/octane-query/src/useMutation.ts:23 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `Error` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` + +## Parameters + +### options + +[`UseMutationOptions`](../interfaces/UseMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> + +### queryClient? + +`QueryClient` + +## Returns + +[`UseMutationResult`](../type-aliases/UseMutationResult.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> diff --git a/docs/framework/octane/reference/functions/useMutationState.md b/docs/framework/octane/reference/functions/useMutationState.md new file mode 100644 index 00000000000..cc11c36cc07 --- /dev/null +++ b/docs/framework/octane/reference/functions/useMutationState.md @@ -0,0 +1,32 @@ +--- +id: useMutationState +title: useMutationState +--- + +# Function: useMutationState() + +```ts +function useMutationState(options?, queryClient?): TResult[]; +``` + +Defined in: packages/octane-query/src/useMutationState.ts:27 + +## Type Parameters + +### TResult + +`TResult` = `MutationState`\<`unknown`, `Error`, `unknown`, `unknown`\> + +## Parameters + +### options? + +`MutationStateOptions`\<`TResult`\> + +### queryClient? + +`QueryClient` + +## Returns + +`TResult`[] diff --git a/docs/framework/octane/reference/functions/usePrefetchInfiniteQuery.md b/docs/framework/octane/reference/functions/usePrefetchInfiniteQuery.md new file mode 100644 index 00000000000..7704aed510c --- /dev/null +++ b/docs/framework/octane/reference/functions/usePrefetchInfiniteQuery.md @@ -0,0 +1,48 @@ +--- +id: usePrefetchInfiniteQuery +title: usePrefetchInfiniteQuery +--- + +# Function: usePrefetchInfiniteQuery() + +```ts +function usePrefetchInfiniteQuery(options, queryClient?): void; +``` + +Defined in: packages/octane-query/src/usePrefetch.ts:31 + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `Error` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### TPageParam + +`TPageParam` = `unknown` + +## Parameters + +### options + +`FetchInfiniteQueryOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +### queryClient? + +`QueryClient` + +## Returns + +`void` diff --git a/docs/framework/octane/reference/functions/usePrefetchQuery.md b/docs/framework/octane/reference/functions/usePrefetchQuery.md new file mode 100644 index 00000000000..da76e630f49 --- /dev/null +++ b/docs/framework/octane/reference/functions/usePrefetchQuery.md @@ -0,0 +1,44 @@ +--- +id: usePrefetchQuery +title: usePrefetchQuery +--- + +# Function: usePrefetchQuery() + +```ts +function usePrefetchQuery(options, queryClient?): void; +``` + +Defined in: packages/octane-query/src/usePrefetch.ts:13 + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `Error` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +## Parameters + +### options + +[`UsePrefetchQueryOptions`](../interfaces/UsePrefetchQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +### queryClient? + +`QueryClient` + +## Returns + +`void` diff --git a/docs/framework/octane/reference/functions/useQueries.md b/docs/framework/octane/reference/functions/useQueries.md new file mode 100644 index 00000000000..e1015f2f484 --- /dev/null +++ b/docs/framework/octane/reference/functions/useQueries.md @@ -0,0 +1,47 @@ +--- +id: useQueries +title: useQueries +--- + +# Function: useQueries() + +```ts +function useQueries(options, queryClient?): TCombinedResult; +``` + +Defined in: packages/octane-query/src/useQueries.ts:38 + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TCombinedResult + +`TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseQueryResult`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseQueryResult`\<`Head`\>, `GetUseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseQueryResult`\<`Head`\>, `GetUseQueryResult`\<`Head`\>, `GetUseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GetUseQueryResult\\]\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GetUseQueryResult\\]\> \} + +## Parameters + +### options + +#### combine? + +(`result`) => `TCombinedResult` + +#### queries + + \| readonly \[`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseQueryOptionsForUseQueries`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseQueryOptionsForUseQueries`\<`Head`\>, `GetUseQueryOptionsForUseQueries`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : readonly ...[] *extends* \[`...(...)[]`\] ? \[`...(...)[]`\] : ... *extends* ... ? ... : ... : readonly `unknown`[] *extends* `T` ? `T` : `T` *extends* `UseQueryOptionsForUseQueries`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] ? `UseQueryOptionsForUseQueries`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] : `UseQueryOptionsForUseQueries`\<`unknown`, `Error`, `unknown`, readonly ...[]\>[]\] + \| readonly \[\{ \[K in string \| number \| symbol\]: GetUseQueryOptionsForUseQueries\\]\> \}\] + +#### subscribed? + +`boolean` + +### queryClient? + +`QueryClient` + +## Returns + +`TCombinedResult` diff --git a/docs/framework/octane/reference/functions/useQuery.md b/docs/framework/octane/reference/functions/useQuery.md new file mode 100644 index 00000000000..af40f908fa6 --- /dev/null +++ b/docs/framework/octane/reference/functions/useQuery.md @@ -0,0 +1,126 @@ +--- +id: useQuery +title: useQuery +--- + +# Function: useQuery() + +## Call Signature + +```ts +function useQuery(options, queryClient?): DefinedUseQueryResult, TError>; +``` + +Defined in: packages/octane-query/src/useQuery.ts:17 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `TQueryFnData` + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### Parameters + +#### options + +[`DefinedInitialDataOptions`](../type-aliases/DefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +#### queryClient? + +`QueryClient` + +### Returns + +[`DefinedUseQueryResult`](../type-aliases/DefinedUseQueryResult.md)\<`NoInfer`\<`TData`\>, `TError`\> + +## Call Signature + +```ts +function useQuery(options, queryClient?): UseQueryResult, TError>; +``` + +Defined in: packages/octane-query/src/useQuery.ts:27 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `TQueryFnData` + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### Parameters + +#### options + +[`UndefinedInitialDataOptions`](../type-aliases/UndefinedInitialDataOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +#### queryClient? + +`QueryClient` + +### Returns + +[`UseQueryResult`](../type-aliases/UseQueryResult.md)\<`NoInfer`\<`TData`\>, `TError`\> + +## Call Signature + +```ts +function useQuery(options, queryClient?): UseQueryResult, TError>; +``` + +Defined in: packages/octane-query/src/useQuery.ts:37 + +### Type Parameters + +#### TQueryFnData + +`TQueryFnData` = `unknown` + +#### TError + +`TError` = `Error` + +#### TData + +`TData` = `TQueryFnData` + +#### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### Parameters + +#### options + +[`UseQueryOptions`](../interfaces/UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +#### queryClient? + +`QueryClient` + +### Returns + +[`UseQueryResult`](../type-aliases/UseQueryResult.md)\<`NoInfer`\<`TData`\>, `TError`\> diff --git a/docs/framework/octane/reference/functions/useQueryClient.md b/docs/framework/octane/reference/functions/useQueryClient.md new file mode 100644 index 00000000000..4ee235d598b --- /dev/null +++ b/docs/framework/octane/reference/functions/useQueryClient.md @@ -0,0 +1,22 @@ +--- +id: useQueryClient +title: useQueryClient +--- + +# Function: useQueryClient() + +```ts +function useQueryClient(queryClient?): QueryClient; +``` + +Defined in: packages/octane-query/src/context.ts:25 + +## Parameters + +### queryClient? + +`QueryClient` + +## Returns + +`QueryClient` diff --git a/docs/framework/octane/reference/functions/useQueryErrorResetBoundary.md b/docs/framework/octane/reference/functions/useQueryErrorResetBoundary.md new file mode 100644 index 00000000000..8e3434a5d6e --- /dev/null +++ b/docs/framework/octane/reference/functions/useQueryErrorResetBoundary.md @@ -0,0 +1,16 @@ +--- +id: useQueryErrorResetBoundary +title: useQueryErrorResetBoundary +--- + +# Function: useQueryErrorResetBoundary() + +```ts +function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue; +``` + +Defined in: packages/octane-query/src/errorResetBoundary.ts:30 + +## Returns + +[`QueryErrorResetBoundaryValue`](../interfaces/QueryErrorResetBoundaryValue.md) diff --git a/docs/framework/octane/reference/functions/useSuspenseInfiniteQuery.md b/docs/framework/octane/reference/functions/useSuspenseInfiniteQuery.md new file mode 100644 index 00000000000..f6cdfc683bc --- /dev/null +++ b/docs/framework/octane/reference/functions/useSuspenseInfiniteQuery.md @@ -0,0 +1,48 @@ +--- +id: useSuspenseInfiniteQuery +title: useSuspenseInfiniteQuery +--- + +# Function: useSuspenseInfiniteQuery() + +```ts +function useSuspenseInfiniteQuery(options, queryClient?): UseSuspenseInfiniteQueryResult; +``` + +Defined in: packages/octane-query/src/useSuspenseQuery.ts:56 + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` + +### TError + +`TError` = `Error` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### TPageParam + +`TPageParam` = `unknown` + +## Parameters + +### options + +[`UseSuspenseInfiniteQueryOptions`](../interfaces/UseSuspenseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +### queryClient? + +`QueryClient` + +## Returns + +[`UseSuspenseInfiniteQueryResult`](../type-aliases/UseSuspenseInfiniteQueryResult.md)\<`TData`, `TError`\> diff --git a/docs/framework/octane/reference/functions/useSuspenseQueries.md b/docs/framework/octane/reference/functions/useSuspenseQueries.md new file mode 100644 index 00000000000..26dcca4e0fd --- /dev/null +++ b/docs/framework/octane/reference/functions/useSuspenseQueries.md @@ -0,0 +1,83 @@ +--- +id: useSuspenseQueries +title: useSuspenseQueries +--- + +# Function: useSuspenseQueries() + +## Call Signature + +```ts +function useSuspenseQueries(options, queryClient?): TCombinedResult; +``` + +Defined in: packages/octane-query/src/useSuspenseQueries.ts:12 + +### Type Parameters + +#### T + +`T` *extends* `any`[] + +#### TCombinedResult + +`TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \} + +### Parameters + +#### options + +##### combine? + +(`result`) => `TCombinedResult` + +##### queries + + \| readonly \[`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>, `GetUseSuspenseQueryOptions`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : ...[] *extends* \[`...(...)[]`\] ? \[`...(...)[]`\] : ... *extends* ... ? ... : ... : `unknown`[] *extends* `T` ? `T` : `T` *extends* [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] ? [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] : [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`unknown`, `Error`, `unknown`, readonly ...[]\>[]\] + \| readonly \[\{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryOptions\\]\> \}\] + +#### queryClient? + +`QueryClient` + +### Returns + +`TCombinedResult` + +## Call Signature + +```ts +function useSuspenseQueries(options, queryClient?): TCombinedResult; +``` + +Defined in: packages/octane-query/src/useSuspenseQueries.ts:25 + +### Type Parameters + +#### T + +`T` *extends* `any`[] + +#### TCombinedResult + +`TCombinedResult` = `T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>, `GetUseSuspenseQueryResult`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...(...)[]`\] *extends* \[\] ? \[\] : ... *extends* ... ? ... : ... : \[`...{ [K in (...)]: (...) }[]`\] : \[...\{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \}\[\]\] : \{ \[K in string \| number \| symbol\]: GetUseSuspenseQueryResult\\]\> \} + +### Parameters + +#### options + +##### combine? + +(`result`) => `TCombinedResult` + +##### queries + +readonly \[`T` *extends* \[\] ? \[\] : `T` *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>\] : `T` *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...Tails[]`\] *extends* \[`Head`\] ? \[`GetUseSuspenseQueryOptions`\<`Head`\>, `GetUseSuspenseQueryOptions`\<`Head`\>\] : \[`...Tails[]`\] *extends* \[`Head`, `...Tails[]`\] ? \[`...Tails[]`\] *extends* \[\] ? \[\] : \[`...(...)[]`\] *extends* \[...\] ? \[..., ..., ...\] : ... *extends* ... ? ... : ... : `unknown`[] *extends* \[`...Tails[]`\] ? \[`...Tails[]`\] : \[`...(...)[]`\] *extends* ...[] ? ...[] : ...[] : `unknown`[] *extends* `T` ? `T` : `T` *extends* [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] ? [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>[] : [`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`unknown`, `Error`, `unknown`, readonly `unknown`[]\>[]\] + +#### queryClient? + +`QueryClient` + +### Returns + +`TCombinedResult` diff --git a/docs/framework/octane/reference/functions/useSuspenseQuery.md b/docs/framework/octane/reference/functions/useSuspenseQuery.md new file mode 100644 index 00000000000..5a331e24bf2 --- /dev/null +++ b/docs/framework/octane/reference/functions/useSuspenseQuery.md @@ -0,0 +1,44 @@ +--- +id: useSuspenseQuery +title: useSuspenseQuery +--- + +# Function: useSuspenseQuery() + +```ts +function useSuspenseQuery(options, queryClient?): UseSuspenseQueryResult; +``` + +Defined in: packages/octane-query/src/useSuspenseQuery.ts:25 + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `Error` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +## Parameters + +### options + +[`UseSuspenseQueryOptions`](../interfaces/UseSuspenseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\> + +### queryClient? + +`QueryClient` + +## Returns + +[`UseSuspenseQueryResult`](../type-aliases/UseSuspenseQueryResult.md)\<`TData`, `TError`\> diff --git a/docs/framework/octane/reference/index.md b/docs/framework/octane/reference/index.md new file mode 100644 index 00000000000..c7aea70c1cc --- /dev/null +++ b/docs/framework/octane/reference/index.md @@ -0,0 +1,83 @@ +--- +id: "@tanstack/octane-query" +title: "@tanstack/octane-query" +--- + +# @tanstack/octane-query + +## Interfaces + +- [HydrationBoundaryProps](interfaces/HydrationBoundaryProps.md) +- [QueryClientProviderProps](interfaces/QueryClientProviderProps.md) +- [QueryErrorResetBoundaryProps](interfaces/QueryErrorResetBoundaryProps.md) +- [QueryErrorResetBoundaryValue](interfaces/QueryErrorResetBoundaryValue.md) +- [UseBaseQueryOptions](interfaces/UseBaseQueryOptions.md) +- [UseInfiniteQueryOptions](interfaces/UseInfiniteQueryOptions.md) +- [UseMutationOptions](interfaces/UseMutationOptions.md) +- [UsePrefetchQueryOptions](interfaces/UsePrefetchQueryOptions.md) +- [UseQueryOptions](interfaces/UseQueryOptions.md) +- [UseSuspenseInfiniteQueryOptions](interfaces/UseSuspenseInfiniteQueryOptions.md) +- [UseSuspenseQueryOptions](interfaces/UseSuspenseQueryOptions.md) + +## Type Aliases + +- [AnyUseBaseQueryOptions](type-aliases/AnyUseBaseQueryOptions.md) +- [AnyUseInfiniteQueryOptions](type-aliases/AnyUseInfiniteQueryOptions.md) +- [AnyUseMutationOptions](type-aliases/AnyUseMutationOptions.md) +- [AnyUseQueryOptions](type-aliases/AnyUseQueryOptions.md) +- [AnyUseSuspenseInfiniteQueryOptions](type-aliases/AnyUseSuspenseInfiniteQueryOptions.md) +- [AnyUseSuspenseQueryOptions](type-aliases/AnyUseSuspenseQueryOptions.md) +- [DefinedInitialDataInfiniteOptions](type-aliases/DefinedInitialDataInfiniteOptions.md) +- [DefinedInitialDataOptions](type-aliases/DefinedInitialDataOptions.md) +- [DefinedUseInfiniteQueryResult](type-aliases/DefinedUseInfiniteQueryResult.md) +- [DefinedUseQueryResult](type-aliases/DefinedUseQueryResult.md) +- [QueriesOptions](type-aliases/QueriesOptions.md) +- [QueriesResults](type-aliases/QueriesResults.md) +- [QueryErrorClearResetFunction](type-aliases/QueryErrorClearResetFunction.md) +- [QueryErrorIsResetFunction](type-aliases/QueryErrorIsResetFunction.md) +- [QueryErrorResetBoundaryFunction](type-aliases/QueryErrorResetBoundaryFunction.md) +- [QueryErrorResetFunction](type-aliases/QueryErrorResetFunction.md) +- [SuspenseQueriesOptions](type-aliases/SuspenseQueriesOptions.md) +- [SuspenseQueriesResults](type-aliases/SuspenseQueriesResults.md) +- [UndefinedInitialDataInfiniteOptions](type-aliases/UndefinedInitialDataInfiniteOptions.md) +- [UndefinedInitialDataOptions](type-aliases/UndefinedInitialDataOptions.md) +- [UnusedSkipTokenInfiniteOptions](type-aliases/UnusedSkipTokenInfiniteOptions.md) +- [UnusedSkipTokenOptions](type-aliases/UnusedSkipTokenOptions.md) +- [UseBaseMutationResult](type-aliases/UseBaseMutationResult.md) +- [UseBaseQueryResult](type-aliases/UseBaseQueryResult.md) +- [UseInfiniteQueryResult](type-aliases/UseInfiniteQueryResult.md) +- [UseMutateAsyncFunction](type-aliases/UseMutateAsyncFunction.md) +- [UseMutateFunction](type-aliases/UseMutateFunction.md) +- [UseMutationResult](type-aliases/UseMutationResult.md) +- [UseQueryResult](type-aliases/UseQueryResult.md) +- [UseSuspenseInfiniteQueryResult](type-aliases/UseSuspenseInfiniteQueryResult.md) +- [UseSuspenseQueryResult](type-aliases/UseSuspenseQueryResult.md) + +## Variables + +- [HydrationBoundary](variables/HydrationBoundary.md) +- [IsRestoringProvider](variables/IsRestoringProvider.md) +- [QueryClientContext](variables/QueryClientContext.md) +- [QueryClientProvider](variables/QueryClientProvider.md) +- [QueryErrorResetBoundary](variables/QueryErrorResetBoundary.md) + +## Functions + +- [infiniteQueryOptions](functions/infiniteQueryOptions.md) +- [mutationOptions](functions/mutationOptions.md) +- [queryOptions](functions/queryOptions.md) +- [useInfiniteQuery](functions/useInfiniteQuery.md) +- [useIsFetching](functions/useIsFetching.md) +- [useIsMutating](functions/useIsMutating.md) +- [useIsRestoring](functions/useIsRestoring.md) +- [useMutation](functions/useMutation.md) +- [useMutationState](functions/useMutationState.md) +- [usePrefetchInfiniteQuery](functions/usePrefetchInfiniteQuery.md) +- [usePrefetchQuery](functions/usePrefetchQuery.md) +- [useQueries](functions/useQueries.md) +- [useQuery](functions/useQuery.md) +- [useQueryClient](functions/useQueryClient.md) +- [useQueryErrorResetBoundary](functions/useQueryErrorResetBoundary.md) +- [useSuspenseInfiniteQuery](functions/useSuspenseInfiniteQuery.md) +- [useSuspenseQueries](functions/useSuspenseQueries.md) +- [useSuspenseQuery](functions/useSuspenseQuery.md) diff --git a/docs/framework/octane/reference/interfaces/HydrationBoundaryProps.md b/docs/framework/octane/reference/interfaces/HydrationBoundaryProps.md new file mode 100644 index 00000000000..f2f764d0817 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/HydrationBoundaryProps.md @@ -0,0 +1,57 @@ +--- +id: HydrationBoundaryProps +title: HydrationBoundaryProps +--- + +# Interface: HydrationBoundaryProps + +Defined in: packages/octane-query/src/types.ts:260 + +## Properties + +### children? + +```ts +optional children: unknown; +``` + +Defined in: packages/octane-query/src/types.ts:268 + +*** + +### options? + +```ts +optional options: OmitKeyof & object; +``` + +Defined in: packages/octane-query/src/types.ts:262 + +#### Type Declaration + +##### defaultOptions? + +```ts +optional defaultOptions: OmitKeyof<{ +}, "mutations">; +``` + +*** + +### queryClient? + +```ts +optional queryClient: QueryClient; +``` + +Defined in: packages/octane-query/src/types.ts:269 + +*** + +### state + +```ts +state: DehydratedState | null | undefined; +``` + +Defined in: packages/octane-query/src/types.ts:261 diff --git a/docs/framework/octane/reference/interfaces/QueryClientProviderProps.md b/docs/framework/octane/reference/interfaces/QueryClientProviderProps.md new file mode 100644 index 00000000000..347f11548f9 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/QueryClientProviderProps.md @@ -0,0 +1,28 @@ +--- +id: QueryClientProviderProps +title: QueryClientProviderProps +--- + +# Interface: QueryClientProviderProps + +Defined in: packages/octane-query/src/types.ts:255 + +## Properties + +### children? + +```ts +optional children: unknown; +``` + +Defined in: packages/octane-query/src/types.ts:257 + +*** + +### client + +```ts +client: QueryClient; +``` + +Defined in: packages/octane-query/src/types.ts:256 diff --git a/docs/framework/octane/reference/interfaces/QueryErrorResetBoundaryProps.md b/docs/framework/octane/reference/interfaces/QueryErrorResetBoundaryProps.md new file mode 100644 index 00000000000..947e7d3beea --- /dev/null +++ b/docs/framework/octane/reference/interfaces/QueryErrorResetBoundaryProps.md @@ -0,0 +1,18 @@ +--- +id: QueryErrorResetBoundaryProps +title: QueryErrorResetBoundaryProps +--- + +# Interface: QueryErrorResetBoundaryProps + +Defined in: packages/octane-query/src/types.ts:286 + +## Properties + +### children + +```ts +children: unknown; +``` + +Defined in: packages/octane-query/src/types.ts:287 diff --git a/docs/framework/octane/reference/interfaces/QueryErrorResetBoundaryValue.md b/docs/framework/octane/reference/interfaces/QueryErrorResetBoundaryValue.md new file mode 100644 index 00000000000..b4bcc8edfed --- /dev/null +++ b/docs/framework/octane/reference/interfaces/QueryErrorResetBoundaryValue.md @@ -0,0 +1,38 @@ +--- +id: QueryErrorResetBoundaryValue +title: QueryErrorResetBoundaryValue +--- + +# Interface: QueryErrorResetBoundaryValue + +Defined in: packages/octane-query/src/types.ts:276 + +## Properties + +### clearReset + +```ts +clearReset: QueryErrorClearResetFunction; +``` + +Defined in: packages/octane-query/src/types.ts:277 + +*** + +### isReset + +```ts +isReset: QueryErrorIsResetFunction; +``` + +Defined in: packages/octane-query/src/types.ts:278 + +*** + +### reset + +```ts +reset: QueryErrorResetFunction; +``` + +Defined in: packages/octane-query/src/types.ts:279 diff --git a/docs/framework/octane/reference/interfaces/UseBaseQueryOptions.md b/docs/framework/octane/reference/interfaces/UseBaseQueryOptions.md new file mode 100644 index 00000000000..b5014943599 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/UseBaseQueryOptions.md @@ -0,0 +1,47 @@ +--- +id: UseBaseQueryOptions +title: UseBaseQueryOptions +--- + +# Interface: UseBaseQueryOptions\ + +Defined in: packages/octane-query/src/types.ts:34 + +## Extends + +- `QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryData`, `TQueryKey`\> + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryData + +`TQueryData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +## Properties + +### subscribed? + +```ts +optional subscribed: boolean; +``` + +Defined in: packages/octane-query/src/types.ts:51 + +Set this to `false` to unsubscribe this observer from updates to the query cache. +Defaults to `true`. diff --git a/docs/framework/octane/reference/interfaces/UseInfiniteQueryOptions.md b/docs/framework/octane/reference/interfaces/UseInfiniteQueryOptions.md new file mode 100644 index 00000000000..718e35ca379 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/UseInfiniteQueryOptions.md @@ -0,0 +1,47 @@ +--- +id: UseInfiniteQueryOptions +title: UseInfiniteQueryOptions +--- + +# Interface: UseInfiniteQueryOptions\ + +Defined in: packages/octane-query/src/types.ts:108 + +## Extends + +- `OmitKeyof`\<`InfiniteQueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"suspense"`\> + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +### TPageParam + +`TPageParam` = `unknown` + +## Properties + +### subscribed? + +```ts +optional subscribed: boolean; +``` + +Defined in: packages/octane-query/src/types.ts:128 + +Set this to `false` to unsubscribe this observer from updates to the query cache. +Defaults to `true`. diff --git a/docs/framework/octane/reference/interfaces/UseMutationOptions.md b/docs/framework/octane/reference/interfaces/UseMutationOptions.md new file mode 100644 index 00000000000..c7f48c2ce86 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/UseMutationOptions.md @@ -0,0 +1,30 @@ +--- +id: UseMutationOptions +title: UseMutationOptions +--- + +# Interface: UseMutationOptions\ + +Defined in: packages/octane-query/src/types.ts:197 + +## Extends + +- `OmitKeyof`\<`MutationObserverOptions`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>, `"_defaulted"`\> + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` diff --git a/docs/framework/octane/reference/interfaces/UsePrefetchQueryOptions.md b/docs/framework/octane/reference/interfaces/UsePrefetchQueryOptions.md new file mode 100644 index 00000000000..6d669e6cef1 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/UsePrefetchQueryOptions.md @@ -0,0 +1,40 @@ +--- +id: UsePrefetchQueryOptions +title: UsePrefetchQueryOptions +--- + +# Interface: UsePrefetchQueryOptions\ + +Defined in: packages/octane-query/src/types.ts:54 + +## Extends + +- `OmitKeyof`\<`FetchQueryOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"`\> + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +## Properties + +### queryFn? + +```ts +optional queryFn: QueryFunction; +``` + +Defined in: packages/octane-query/src/types.ts:63 diff --git a/docs/framework/octane/reference/interfaces/UseQueryOptions.md b/docs/framework/octane/reference/interfaces/UseQueryOptions.md new file mode 100644 index 00000000000..cdd1d84918e --- /dev/null +++ b/docs/framework/octane/reference/interfaces/UseQueryOptions.md @@ -0,0 +1,49 @@ +--- +id: UseQueryOptions +title: UseQueryOptions +--- + +# Interface: UseQueryOptions\ + +Defined in: packages/octane-query/src/types.ts:70 + +## Extends + +- `OmitKeyof`\<[`UseBaseQueryOptions`](UseBaseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`\>, `"suspense"`\> + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +## Properties + +### subscribed? + +```ts +optional subscribed: boolean; +``` + +Defined in: packages/octane-query/src/types.ts:51 + +Set this to `false` to unsubscribe this observer from updates to the query cache. +Defaults to `true`. + +#### Inherited from + +```ts +OmitKeyof.subscribed +``` diff --git a/docs/framework/octane/reference/interfaces/UseSuspenseInfiniteQueryOptions.md b/docs/framework/octane/reference/interfaces/UseSuspenseInfiniteQueryOptions.md new file mode 100644 index 00000000000..0988ad117e8 --- /dev/null +++ b/docs/framework/octane/reference/interfaces/UseSuspenseInfiniteQueryOptions.md @@ -0,0 +1,63 @@ +--- +id: UseSuspenseInfiniteQueryOptions +title: UseSuspenseInfiniteQueryOptions +--- + +# Interface: UseSuspenseInfiniteQueryOptions\ + +Defined in: packages/octane-query/src/types.ts:133 + +## Extends + +- `OmitKeyof`\<[`UseInfiniteQueryOptions`](UseInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>, `"queryFn"` \| `"enabled"` \| `"throwOnError"` \| `"placeholderData"`\> + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +### TPageParam + +`TPageParam` = `unknown` + +## Properties + +### queryFn? + +```ts +optional queryFn: QueryFunction; +``` + +Defined in: packages/octane-query/src/types.ts:143 + +*** + +### subscribed? + +```ts +optional subscribed: boolean; +``` + +Defined in: packages/octane-query/src/types.ts:128 + +Set this to `false` to unsubscribe this observer from updates to the query cache. +Defaults to `true`. + +#### Inherited from + +```ts +OmitKeyof.subscribed +``` diff --git a/docs/framework/octane/reference/interfaces/UseSuspenseQueryOptions.md b/docs/framework/octane/reference/interfaces/UseSuspenseQueryOptions.md new file mode 100644 index 00000000000..ba692b0845c --- /dev/null +++ b/docs/framework/octane/reference/interfaces/UseSuspenseQueryOptions.md @@ -0,0 +1,59 @@ +--- +id: UseSuspenseQueryOptions +title: UseSuspenseQueryOptions +--- + +# Interface: UseSuspenseQueryOptions\ + +Defined in: packages/octane-query/src/types.ts:86 + +## Extends + +- `OmitKeyof`\<[`UseQueryOptions`](UseQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`\>, `"queryFn"` \| `"enabled"` \| `"throwOnError"` \| `"placeholderData"`\> + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +## Properties + +### queryFn? + +```ts +optional queryFn: QueryFunction; +``` + +Defined in: packages/octane-query/src/types.ts:95 + +*** + +### subscribed? + +```ts +optional subscribed: boolean; +``` + +Defined in: packages/octane-query/src/types.ts:51 + +Set this to `false` to unsubscribe this observer from updates to the query cache. +Defaults to `true`. + +#### Inherited from + +```ts +OmitKeyof.subscribed +``` diff --git a/docs/framework/octane/reference/type-aliases/AnyUseBaseQueryOptions.md b/docs/framework/octane/reference/type-aliases/AnyUseBaseQueryOptions.md new file mode 100644 index 00000000000..60a06d14f5c --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AnyUseBaseQueryOptions.md @@ -0,0 +1,12 @@ +--- +id: AnyUseBaseQueryOptions +title: AnyUseBaseQueryOptions +--- + +# Type Alias: AnyUseBaseQueryOptions + +```ts +type AnyUseBaseQueryOptions = UseBaseQueryOptions; +``` + +Defined in: packages/octane-query/src/types.ts:27 diff --git a/docs/framework/octane/reference/type-aliases/AnyUseInfiniteQueryOptions.md b/docs/framework/octane/reference/type-aliases/AnyUseInfiniteQueryOptions.md new file mode 100644 index 00000000000..4898cc0f069 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AnyUseInfiniteQueryOptions.md @@ -0,0 +1,12 @@ +--- +id: AnyUseInfiniteQueryOptions +title: AnyUseInfiniteQueryOptions +--- + +# Type Alias: AnyUseInfiniteQueryOptions + +```ts +type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions; +``` + +Defined in: packages/octane-query/src/types.ts:101 diff --git a/docs/framework/octane/reference/type-aliases/AnyUseMutationOptions.md b/docs/framework/octane/reference/type-aliases/AnyUseMutationOptions.md new file mode 100644 index 00000000000..e32b0001531 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AnyUseMutationOptions.md @@ -0,0 +1,12 @@ +--- +id: AnyUseMutationOptions +title: AnyUseMutationOptions +--- + +# Type Alias: AnyUseMutationOptions + +```ts +type AnyUseMutationOptions = UseMutationOptions; +``` + +Defined in: packages/octane-query/src/types.ts:196 diff --git a/docs/framework/octane/reference/type-aliases/AnyUseQueryOptions.md b/docs/framework/octane/reference/type-aliases/AnyUseQueryOptions.md new file mode 100644 index 00000000000..f4b2100c16b --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AnyUseQueryOptions.md @@ -0,0 +1,12 @@ +--- +id: AnyUseQueryOptions +title: AnyUseQueryOptions +--- + +# Type Alias: AnyUseQueryOptions + +```ts +type AnyUseQueryOptions = UseQueryOptions; +``` + +Defined in: packages/octane-query/src/types.ts:69 diff --git a/docs/framework/octane/reference/type-aliases/AnyUseSuspenseInfiniteQueryOptions.md b/docs/framework/octane/reference/type-aliases/AnyUseSuspenseInfiniteQueryOptions.md new file mode 100644 index 00000000000..7d8095797dd --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AnyUseSuspenseInfiniteQueryOptions.md @@ -0,0 +1,12 @@ +--- +id: AnyUseSuspenseInfiniteQueryOptions +title: AnyUseSuspenseInfiniteQueryOptions +--- + +# Type Alias: AnyUseSuspenseInfiniteQueryOptions + +```ts +type AnyUseSuspenseInfiniteQueryOptions = UseSuspenseInfiniteQueryOptions; +``` + +Defined in: packages/octane-query/src/types.ts:131 diff --git a/docs/framework/octane/reference/type-aliases/AnyUseSuspenseQueryOptions.md b/docs/framework/octane/reference/type-aliases/AnyUseSuspenseQueryOptions.md new file mode 100644 index 00000000000..da414c634b6 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/AnyUseSuspenseQueryOptions.md @@ -0,0 +1,12 @@ +--- +id: AnyUseSuspenseQueryOptions +title: AnyUseSuspenseQueryOptions +--- + +# Type Alias: AnyUseSuspenseQueryOptions + +```ts +type AnyUseSuspenseQueryOptions = UseSuspenseQueryOptions; +``` + +Defined in: packages/octane-query/src/types.ts:80 diff --git a/docs/framework/octane/reference/type-aliases/DefinedInitialDataInfiniteOptions.md b/docs/framework/octane/reference/type-aliases/DefinedInitialDataInfiniteOptions.md new file mode 100644 index 00000000000..041dc3deaaf --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/DefinedInitialDataInfiniteOptions.md @@ -0,0 +1,45 @@ +--- +id: DefinedInitialDataInfiniteOptions +title: DefinedInitialDataInfiniteOptions +--- + +# Type Alias: DefinedInitialDataInfiniteOptions\ + +```ts +type DefinedInitialDataInfiniteOptions = UseInfiniteQueryOptions & object; +``` + +Defined in: packages/octane-query/src/infiniteQueryOptions.ts:57 + +## Type Declaration + +### initialData + +```ts +initialData: + | NonUndefinedGuard> + | () => NonUndefinedGuard> + | undefined; +``` + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`\> + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +### TPageParam + +`TPageParam` = `unknown` diff --git a/docs/framework/octane/reference/type-aliases/DefinedInitialDataOptions.md b/docs/framework/octane/reference/type-aliases/DefinedInitialDataOptions.md new file mode 100644 index 00000000000..556f75939b2 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/DefinedInitialDataOptions.md @@ -0,0 +1,46 @@ +--- +id: DefinedInitialDataOptions +title: DefinedInitialDataOptions +--- + +# Type Alias: DefinedInitialDataOptions\ + +```ts +type DefinedInitialDataOptions = Omit, "queryFn"> & object; +``` + +Defined in: packages/octane-query/src/queryOptions.ts:41 + +## Type Declaration + +### initialData + +```ts +initialData: + | NonUndefinedGuard +| () => NonUndefinedGuard; +``` + +### queryFn? + +```ts +optional queryFn: QueryFunction; +``` + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/octane/reference/type-aliases/DefinedUseInfiniteQueryResult.md b/docs/framework/octane/reference/type-aliases/DefinedUseInfiniteQueryResult.md new file mode 100644 index 00000000000..49c4e0033fa --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/DefinedUseInfiniteQueryResult.md @@ -0,0 +1,22 @@ +--- +id: DefinedUseInfiniteQueryResult +title: DefinedUseInfiniteQueryResult +--- + +# Type Alias: DefinedUseInfiniteQueryResult\ + +```ts +type DefinedUseInfiniteQueryResult = DefinedInfiniteQueryObserverResult; +``` + +Defined in: packages/octane-query/src/types.ts:183 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/octane/reference/type-aliases/DefinedUseQueryResult.md b/docs/framework/octane/reference/type-aliases/DefinedUseQueryResult.md new file mode 100644 index 00000000000..efcb72988c5 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/DefinedUseQueryResult.md @@ -0,0 +1,22 @@ +--- +id: DefinedUseQueryResult +title: DefinedUseQueryResult +--- + +# Type Alias: DefinedUseQueryResult\ + +```ts +type DefinedUseQueryResult = DefinedQueryObserverResult; +``` + +Defined in: packages/octane-query/src/types.ts:173 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/octane/reference/type-aliases/QueriesOptions.md b/docs/framework/octane/reference/type-aliases/QueriesOptions.md new file mode 100644 index 00000000000..f5ef47df08e --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/QueriesOptions.md @@ -0,0 +1,28 @@ +--- +id: QueriesOptions +title: QueriesOptions +--- + +# Type Alias: QueriesOptions\ + +```ts +type QueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? UseQueryOptionsForUseQueries[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryOptionsForUseQueries] : T extends [infer Head, ...(infer Tails)] ? QueriesOptions<[...Tails], [...TResults, GetUseQueryOptionsForUseQueries], [...TDepth, 1]> : ReadonlyArray extends T ? T : T extends UseQueryOptionsForUseQueries[] ? UseQueryOptionsForUseQueries[] : UseQueryOptionsForUseQueries[]; +``` + +Defined in: packages/octane-query/src/queries-types.ts:126 + +QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TResults + +`TResults` *extends* `any`[] = \[\] + +### TDepth + +`TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] diff --git a/docs/framework/octane/reference/type-aliases/QueriesResults.md b/docs/framework/octane/reference/type-aliases/QueriesResults.md new file mode 100644 index 00000000000..ac721e440ab --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/QueriesResults.md @@ -0,0 +1,28 @@ +--- +id: QueriesResults +title: QueriesResults +--- + +# Type Alias: QueriesResults\ + +```ts +type QueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? UseQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseQueryResult] : T extends [infer Head, ...(infer Tails)] ? QueriesResults<[...Tails], [...TResults, GetUseQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetUseQueryResult }; +``` + +Defined in: packages/octane-query/src/queries-types.ts:168 + +QueriesResults reducer recursively maps type param to results + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TResults + +`TResults` *extends* `any`[] = \[\] + +### TDepth + +`TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] diff --git a/docs/framework/octane/reference/type-aliases/QueryErrorClearResetFunction.md b/docs/framework/octane/reference/type-aliases/QueryErrorClearResetFunction.md new file mode 100644 index 00000000000..565c62765d0 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/QueryErrorClearResetFunction.md @@ -0,0 +1,16 @@ +--- +id: QueryErrorClearResetFunction +title: QueryErrorClearResetFunction +--- + +# Type Alias: QueryErrorClearResetFunction() + +```ts +type QueryErrorClearResetFunction = () => void; +``` + +Defined in: packages/octane-query/src/types.ts:274 + +## Returns + +`void` diff --git a/docs/framework/octane/reference/type-aliases/QueryErrorIsResetFunction.md b/docs/framework/octane/reference/type-aliases/QueryErrorIsResetFunction.md new file mode 100644 index 00000000000..2ee160af3e3 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/QueryErrorIsResetFunction.md @@ -0,0 +1,16 @@ +--- +id: QueryErrorIsResetFunction +title: QueryErrorIsResetFunction +--- + +# Type Alias: QueryErrorIsResetFunction() + +```ts +type QueryErrorIsResetFunction = () => boolean; +``` + +Defined in: packages/octane-query/src/types.ts:273 + +## Returns + +`boolean` diff --git a/docs/framework/octane/reference/type-aliases/QueryErrorResetBoundaryFunction.md b/docs/framework/octane/reference/type-aliases/QueryErrorResetBoundaryFunction.md new file mode 100644 index 00000000000..3ee99074fb7 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/QueryErrorResetBoundaryFunction.md @@ -0,0 +1,22 @@ +--- +id: QueryErrorResetBoundaryFunction +title: QueryErrorResetBoundaryFunction +--- + +# Type Alias: QueryErrorResetBoundaryFunction() + +```ts +type QueryErrorResetBoundaryFunction = (value) => unknown; +``` + +Defined in: packages/octane-query/src/types.ts:282 + +## Parameters + +### value + +[`QueryErrorResetBoundaryValue`](../interfaces/QueryErrorResetBoundaryValue.md) + +## Returns + +`unknown` diff --git a/docs/framework/octane/reference/type-aliases/QueryErrorResetFunction.md b/docs/framework/octane/reference/type-aliases/QueryErrorResetFunction.md new file mode 100644 index 00000000000..be4e4f96a87 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/QueryErrorResetFunction.md @@ -0,0 +1,16 @@ +--- +id: QueryErrorResetFunction +title: QueryErrorResetFunction +--- + +# Type Alias: QueryErrorResetFunction() + +```ts +type QueryErrorResetFunction = () => void; +``` + +Defined in: packages/octane-query/src/types.ts:272 + +## Returns + +`void` diff --git a/docs/framework/octane/reference/type-aliases/SuspenseQueriesOptions.md b/docs/framework/octane/reference/type-aliases/SuspenseQueriesOptions.md new file mode 100644 index 00000000000..acd37543813 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/SuspenseQueriesOptions.md @@ -0,0 +1,28 @@ +--- +id: SuspenseQueriesOptions +title: SuspenseQueriesOptions +--- + +# Type Alias: SuspenseQueriesOptions\ + +```ts +type SuspenseQueriesOptions = TDepth["length"] extends MAXIMUM_DEPTH ? UseSuspenseQueryOptions[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryOptions] : T extends [infer Head, ...(infer Tails)] ? SuspenseQueriesOptions<[...Tails], [...TResults, GetUseSuspenseQueryOptions], [...TDepth, 1]> : unknown[] extends T ? T : T extends UseSuspenseQueryOptions[] ? UseSuspenseQueryOptions[] : UseSuspenseQueryOptions[]; +``` + +Defined in: packages/octane-query/src/suspense-queries-types.ts:108 + +SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TResults + +`TResults` *extends* `any`[] = \[\] + +### TDepth + +`TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] diff --git a/docs/framework/octane/reference/type-aliases/SuspenseQueriesResults.md b/docs/framework/octane/reference/type-aliases/SuspenseQueriesResults.md new file mode 100644 index 00000000000..6681e596544 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/SuspenseQueriesResults.md @@ -0,0 +1,28 @@ +--- +id: SuspenseQueriesResults +title: SuspenseQueriesResults +--- + +# Type Alias: SuspenseQueriesResults\ + +```ts +type SuspenseQueriesResults = TDepth["length"] extends MAXIMUM_DEPTH ? UseSuspenseQueryResult[] : T extends [] ? [] : T extends [infer Head] ? [...TResults, GetUseSuspenseQueryResult] : T extends [infer Head, ...(infer Tails)] ? SuspenseQueriesResults<[...Tails], [...TResults, GetUseSuspenseQueryResult], [...TDepth, 1]> : { [K in keyof T]: GetUseSuspenseQueryResult }; +``` + +Defined in: packages/octane-query/src/suspense-queries-types.ts:145 + +SuspenseQueriesResults reducer recursively maps type param to results + +## Type Parameters + +### T + +`T` *extends* `any`[] + +### TResults + +`TResults` *extends* `any`[] = \[\] + +### TDepth + +`TDepth` *extends* `ReadonlyArray`\<`number`\> = \[\] diff --git a/docs/framework/octane/reference/type-aliases/UndefinedInitialDataInfiniteOptions.md b/docs/framework/octane/reference/type-aliases/UndefinedInitialDataInfiniteOptions.md new file mode 100644 index 00000000000..32a19efaa97 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UndefinedInitialDataInfiniteOptions.md @@ -0,0 +1,44 @@ +--- +id: UndefinedInitialDataInfiniteOptions +title: UndefinedInitialDataInfiniteOptions +--- + +# Type Alias: UndefinedInitialDataInfiniteOptions\ + +```ts +type UndefinedInitialDataInfiniteOptions = UseInfiniteQueryOptions & object; +``` + +Defined in: packages/octane-query/src/infiniteQueryOptions.ts:14 + +## Type Declaration + +### initialData? + +```ts +optional initialData: + | NonUndefinedGuard> +| InitialDataFunction>>; +``` + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`\> + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +### TPageParam + +`TPageParam` = `unknown` diff --git a/docs/framework/octane/reference/type-aliases/UndefinedInitialDataOptions.md b/docs/framework/octane/reference/type-aliases/UndefinedInitialDataOptions.md new file mode 100644 index 00000000000..53a2e3d2c24 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UndefinedInitialDataOptions.md @@ -0,0 +1,40 @@ +--- +id: UndefinedInitialDataOptions +title: UndefinedInitialDataOptions +--- + +# Type Alias: UndefinedInitialDataOptions\ + +```ts +type UndefinedInitialDataOptions = UseQueryOptions & object; +``` + +Defined in: packages/octane-query/src/queryOptions.ts:14 + +## Type Declaration + +### initialData? + +```ts +optional initialData: + | InitialDataFunction> +| NonUndefinedGuard; +``` + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/octane/reference/type-aliases/UnusedSkipTokenInfiniteOptions.md b/docs/framework/octane/reference/type-aliases/UnusedSkipTokenInfiniteOptions.md new file mode 100644 index 00000000000..36083495628 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UnusedSkipTokenInfiniteOptions.md @@ -0,0 +1,42 @@ +--- +id: UnusedSkipTokenInfiniteOptions +title: UnusedSkipTokenInfiniteOptions +--- + +# Type Alias: UnusedSkipTokenInfiniteOptions\ + +```ts +type UnusedSkipTokenInfiniteOptions = OmitKeyof, "queryFn"> & object; +``` + +Defined in: packages/octane-query/src/infiniteQueryOptions.ts:35 + +## Type Declaration + +### queryFn? + +```ts +optional queryFn: Exclude["queryFn"], SkipToken | undefined>; +``` + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`\> + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +### TPageParam + +`TPageParam` = `unknown` diff --git a/docs/framework/octane/reference/type-aliases/UnusedSkipTokenOptions.md b/docs/framework/octane/reference/type-aliases/UnusedSkipTokenOptions.md new file mode 100644 index 00000000000..1e490ada1f5 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UnusedSkipTokenOptions.md @@ -0,0 +1,38 @@ +--- +id: UnusedSkipTokenOptions +title: UnusedSkipTokenOptions +--- + +# Type Alias: UnusedSkipTokenOptions\ + +```ts +type UnusedSkipTokenOptions = OmitKeyof, "queryFn"> & object; +``` + +Defined in: packages/octane-query/src/queryOptions.ts:26 + +## Type Declaration + +### queryFn? + +```ts +optional queryFn: Exclude["queryFn"], SkipToken | undefined>; +``` + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/octane/reference/type-aliases/UseBaseMutationResult.md b/docs/framework/octane/reference/type-aliases/UseBaseMutationResult.md new file mode 100644 index 00000000000..29216799f85 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseBaseMutationResult.md @@ -0,0 +1,40 @@ +--- +id: UseBaseMutationResult +title: UseBaseMutationResult +--- + +# Type Alias: UseBaseMutationResult\ + +```ts +type UseBaseMutationResult = Override, { + mutate: UseMutateFunction; +}> & object; +``` + +Defined in: packages/octane-query/src/types.ts:225 + +## Type Declaration + +### mutateAsync + +```ts +mutateAsync: UseMutateAsyncFunction; +``` + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `unknown` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` diff --git a/docs/framework/octane/reference/type-aliases/UseBaseQueryResult.md b/docs/framework/octane/reference/type-aliases/UseBaseQueryResult.md new file mode 100644 index 00000000000..377fbde19e4 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseBaseQueryResult.md @@ -0,0 +1,22 @@ +--- +id: UseBaseQueryResult +title: UseBaseQueryResult +--- + +# Type Alias: UseBaseQueryResult\ + +```ts +type UseBaseQueryResult = QueryObserverResult; +``` + +Defined in: packages/octane-query/src/types.ts:155 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/octane/reference/type-aliases/UseInfiniteQueryResult.md b/docs/framework/octane/reference/type-aliases/UseInfiniteQueryResult.md new file mode 100644 index 00000000000..117307752d1 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseInfiniteQueryResult.md @@ -0,0 +1,22 @@ +--- +id: UseInfiniteQueryResult +title: UseInfiniteQueryResult +--- + +# Type Alias: UseInfiniteQueryResult\ + +```ts +type UseInfiniteQueryResult = InfiniteQueryObserverResult; +``` + +Defined in: packages/octane-query/src/types.ts:178 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/octane/reference/type-aliases/UseMutateAsyncFunction.md b/docs/framework/octane/reference/type-aliases/UseMutateAsyncFunction.md new file mode 100644 index 00000000000..8ab1e16c4fa --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseMutateAsyncFunction.md @@ -0,0 +1,30 @@ +--- +id: UseMutateAsyncFunction +title: UseMutateAsyncFunction +--- + +# Type Alias: UseMutateAsyncFunction\ + +```ts +type UseMutateAsyncFunction = MutateFunction; +``` + +Defined in: packages/octane-query/src/types.ts:218 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` diff --git a/docs/framework/octane/reference/type-aliases/UseMutateFunction.md b/docs/framework/octane/reference/type-aliases/UseMutateFunction.md new file mode 100644 index 00000000000..8d1cdf00c78 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseMutateFunction.md @@ -0,0 +1,40 @@ +--- +id: UseMutateFunction +title: UseMutateFunction +--- + +# Type Alias: UseMutateFunction()\ + +```ts +type UseMutateFunction = (...args) => void; +``` + +Defined in: packages/octane-query/src/types.ts:207 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` + +## Parameters + +### args + +...`Parameters`\<`MutateFunction`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>\> + +## Returns + +`void` diff --git a/docs/framework/octane/reference/type-aliases/UseMutationResult.md b/docs/framework/octane/reference/type-aliases/UseMutationResult.md new file mode 100644 index 00000000000..15a33c9fff0 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseMutationResult.md @@ -0,0 +1,30 @@ +--- +id: UseMutationResult +title: UseMutationResult +--- + +# Type Alias: UseMutationResult\ + +```ts +type UseMutationResult = UseBaseMutationResult; +``` + +Defined in: packages/octane-query/src/types.ts:242 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `unknown` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` diff --git a/docs/framework/octane/reference/type-aliases/UseQueryResult.md b/docs/framework/octane/reference/type-aliases/UseQueryResult.md new file mode 100644 index 00000000000..346e9814bd6 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseQueryResult.md @@ -0,0 +1,22 @@ +--- +id: UseQueryResult +title: UseQueryResult +--- + +# Type Alias: UseQueryResult\ + +```ts +type UseQueryResult = UseBaseQueryResult; +``` + +Defined in: packages/octane-query/src/types.ts:160 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/octane/reference/type-aliases/UseSuspenseInfiniteQueryResult.md b/docs/framework/octane/reference/type-aliases/UseSuspenseInfiniteQueryResult.md new file mode 100644 index 00000000000..f7e8c495007 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseSuspenseInfiniteQueryResult.md @@ -0,0 +1,22 @@ +--- +id: UseSuspenseInfiniteQueryResult +title: UseSuspenseInfiniteQueryResult +--- + +# Type Alias: UseSuspenseInfiniteQueryResult\ + +```ts +type UseSuspenseInfiniteQueryResult = OmitKeyof, "isPlaceholderData" | "promise">; +``` + +Defined in: packages/octane-query/src/types.ts:188 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/octane/reference/type-aliases/UseSuspenseQueryResult.md b/docs/framework/octane/reference/type-aliases/UseSuspenseQueryResult.md new file mode 100644 index 00000000000..06fae636577 --- /dev/null +++ b/docs/framework/octane/reference/type-aliases/UseSuspenseQueryResult.md @@ -0,0 +1,22 @@ +--- +id: UseSuspenseQueryResult +title: UseSuspenseQueryResult +--- + +# Type Alias: UseSuspenseQueryResult\ + +```ts +type UseSuspenseQueryResult = DistributiveOmit, "isPlaceholderData" | "promise">; +``` + +Defined in: packages/octane-query/src/types.ts:165 + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/octane/reference/variables/HydrationBoundary.md b/docs/framework/octane/reference/variables/HydrationBoundary.md new file mode 100644 index 00000000000..4b8f67ec988 --- /dev/null +++ b/docs/framework/octane/reference/variables/HydrationBoundary.md @@ -0,0 +1,12 @@ +--- +id: HydrationBoundary +title: HydrationBoundary +--- + +# Variable: HydrationBoundary + +```ts +const HydrationBoundary: ComponentBody; +``` + +Defined in: packages/octane-query/src/HydrationBoundary.tsrx.d.ts:4 diff --git a/docs/framework/octane/reference/variables/IsRestoringProvider.md b/docs/framework/octane/reference/variables/IsRestoringProvider.md new file mode 100644 index 00000000000..bf4b4de0963 --- /dev/null +++ b/docs/framework/octane/reference/variables/IsRestoringProvider.md @@ -0,0 +1,15 @@ +--- +id: IsRestoringProvider +title: IsRestoringProvider +--- + +# Variable: IsRestoringProvider + +```ts +const IsRestoringProvider: ComponentBody<{ + children?: any; + value: boolean; +}, any> = IsRestoringContext.Provider; +``` + +Defined in: packages/octane-query/src/isRestoring.ts:13 diff --git a/docs/framework/octane/reference/variables/QueryClientContext.md b/docs/framework/octane/reference/variables/QueryClientContext.md new file mode 100644 index 00000000000..542c842dfb8 --- /dev/null +++ b/docs/framework/octane/reference/variables/QueryClientContext.md @@ -0,0 +1,12 @@ +--- +id: QueryClientContext +title: QueryClientContext +--- + +# Variable: QueryClientContext + +```ts +const QueryClientContext: Context; +``` + +Defined in: packages/octane-query/src/context.ts:6 diff --git a/docs/framework/octane/reference/variables/QueryClientProvider.md b/docs/framework/octane/reference/variables/QueryClientProvider.md new file mode 100644 index 00000000000..8f3b3408799 --- /dev/null +++ b/docs/framework/octane/reference/variables/QueryClientProvider.md @@ -0,0 +1,12 @@ +--- +id: QueryClientProvider +title: QueryClientProvider +--- + +# Variable: QueryClientProvider + +```ts +const QueryClientProvider: ComponentBody; +``` + +Defined in: packages/octane-query/src/QueryClientProvider.tsrx.d.ts:4 diff --git a/docs/framework/octane/reference/variables/QueryErrorResetBoundary.md b/docs/framework/octane/reference/variables/QueryErrorResetBoundary.md new file mode 100644 index 00000000000..38a520150e1 --- /dev/null +++ b/docs/framework/octane/reference/variables/QueryErrorResetBoundary.md @@ -0,0 +1,12 @@ +--- +id: QueryErrorResetBoundary +title: QueryErrorResetBoundary +--- + +# Variable: QueryErrorResetBoundary + +```ts +const QueryErrorResetBoundary: QueryErrorResetBoundaryComponent; +``` + +Defined in: packages/octane-query/src/QueryErrorResetBoundary.tsrx.d.ts:11 diff --git a/knip.json b/knip.json index 2bbf42195f9..8271897172c 100644 --- a/knip.json +++ b/knip.json @@ -8,6 +8,7 @@ "react-dom" ], "ignoreWorkspaces": ["examples/**", "integrations/**"], + "tags": ["-knipignore"], "workspaces": { "packages/angular-query-experimental": { "entry": [ diff --git a/packages/octane-query/README.md b/packages/octane-query/README.md new file mode 100644 index 00000000000..a918ab2e00f --- /dev/null +++ b/packages/octane-query/README.md @@ -0,0 +1,71 @@ +# TanStack Query for Octane + +Official Octane bindings for [TanStack Query](https://tanstack.com/query). +The package reuses `@tanstack/query-core` and exposes the same query, mutation, +suspense, hydration, and error-reset primitives as the React adapter through +Octane hooks and components. + +```tsrx +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/octane-query' + +const queryClient = new QueryClient() + +function Todos() @{ + const query = useQuery({ + queryKey: ['todos'], + queryFn: fetchTodos, + }) + + @if (query.isPending) { + {'Loading'} + } @else if (query.isError) { + {query.error.message} + } @else { +
    {query.data as unknown}
+ } +} + +function App() @{ + + + +} +``` + +## API coverage + +- Queries: `useQuery`, `useInfiniteQuery`, `useSuspenseQuery`, + `useSuspenseInfiniteQuery`, `useQueries`, `useSuspenseQueries`, + `usePrefetchQuery`, and `usePrefetchInfiniteQuery` +- Mutations and status: `useMutation`, `useMutationState`, `useIsFetching`, and + `useIsMutating` +- Context and boundaries: `QueryClientProvider`, `useQueryClient`, + `QueryClientContext`, `HydrationBoundary`, `IsRestoringProvider`, + `useIsRestoring`, `QueryErrorResetBoundary`, and + `useQueryErrorResetBoundary` +- Query Core: every export from `@tanstack/query-core`, including + `QueryClient`, `dehydrate`, and `hydrate` + +The separate React companion packages, including React Query Devtools and +`@tanstack/react-query-persist-client`, are not part of this adapter. + +## Suspense, errors, and hydration + +Suspense hooks integrate with Octane's `use(thenable)` semantics. Catch pending +and error states with Octane's `Suspense` and `ErrorBoundary` components or its +`@pending` and `@catch` directives. Use `QueryErrorResetBoundary` or +`useQueryErrorResetBoundary` when a retry must reset a failed query. + +For server rendering, create a `QueryClient` per request. The adapter re-exports +Query Core's `dehydrate` and `hydrate`, and `HydrationBoundary` also accepts +streamed dehydrated queries. `IsRestoringProvider` can pause fetching while a +persisted cache is restored. + +The package publishes TSRX source so the Octane compiler can produce the +correct client and server output. Its manifest marks the binding hooks as +manually slotted; consumers should use the standard Octane Vite integration. +It is a compiler-source package, not a direct Node.js runtime entry point. diff --git a/packages/octane-query/octane-compiler.d.ts b/packages/octane-query/octane-compiler.d.ts new file mode 100644 index 00000000000..e8d69c1aa91 --- /dev/null +++ b/packages/octane-query/octane-compiler.d.ts @@ -0,0 +1,22 @@ +declare module 'octane/compiler' { + export interface SourceMap { + version: number + sources: Array + sourcesContent?: Array + names: Array + mappings: string + file?: string + sourceRoot?: string + } + + export interface CompileResult { + code: string + map?: SourceMap | null + } + + export function compile( + source: string, + filename: string, + options?: { mode?: 'client' | 'server' }, + ): CompileResult +} diff --git a/packages/octane-query/package.json b/packages/octane-query/package.json new file mode 100644 index 00000000000..d8b82942628 --- /dev/null +++ b/packages/octane-query/package.json @@ -0,0 +1,85 @@ +{ + "name": "@tanstack/octane-query", + "version": "5.101.4", + "description": "Hooks for managing, caching and syncing asynchronous and remote data in Octane", + "author": "tannerlinsley", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/query.git", + "directory": "packages/octane-query" + }, + "homepage": "https://tanstack.com/query", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "keywords": [ + "octane", + "query", + "tanstack" + ], + "scripts": { + "clean": "premove ./coverage ./dist-ts", + "compile": "tsc --build", + "test:eslint": "eslint --concurrency=auto ./src ./tests ./scripts", + "test:types": "npm-run-all --serial test:types:*", + "test:types:ts54": "node ../../node_modules/typescript54/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts55": "node ../../node_modules/typescript55/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts56": "node ../../node_modules/typescript56/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts57": "node ../../node_modules/typescript57/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts58": "node ../../node_modules/typescript58/lib/tsc.js --build tsconfig.legacy.json", + "test:types:ts59": "node ../../node_modules/typescript59/lib/tsc.js --build tsconfig.legacy.json", + "test:types:tscurrent": "tsc --build", + "test:types:ts60": "node ../../node_modules/typescript60/lib/tsc.js --build tsconfig.legacy.json", + "test:lib": "vitest", + "test:lib:dev": "pnpm run test:lib --watch", + "test:build": "publint --strict && node scripts/check-package.mjs && node scripts/check-tsrx.mjs", + "build": "tsc --noEmit -p tsconfig.prod.json && node scripts/check-tsrx.mjs" + }, + "type": "module", + "types": "./src/index.ts", + "module": "./src/index.ts", + "exports": { + ".": { + "@tanstack/custom-condition": "./src/index.ts", + "types": "./src/index.ts", + "module": "./src/index.ts" + }, + "./package.json": "./package.json" + }, + "sideEffects": false, + "files": [ + "src", + "!src/__tests__", + "README.md" + ], + "engines": { + "node": ">=22" + }, + "octane": { + "hookSlots": { + "manual": [ + "src" + ] + } + }, + "dependencies": { + "@tanstack/query-core": "workspace:*" + }, + "devDependencies": { + "@tanstack/react-query": "workspace:*", + "@tsrx/react": "^0.2.45", + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "esbuild": "^0.27.2", + "npm-run-all2": "^5.0.0", + "octane": "catalog:", + "react": "^19.2.1", + "react-dom": "^19.2.1", + "vite": "^6.4.1" + }, + "peerDependencies": { + "octane": "^0.1.12" + } +} diff --git a/packages/octane-query/scripts/check-package.mjs b/packages/octane-query/scripts/check-package.mjs new file mode 100644 index 00000000000..f72914d6cdc --- /dev/null +++ b/packages/octane-query/scripts/check-package.mjs @@ -0,0 +1,158 @@ +import { spawnSync } from 'node:child_process' +import { + mkdir, + mkdtemp, + readFile, + rename, + rm, + writeFile, +} from 'node:fs/promises' +import { dirname, isAbsolute, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import process from 'node:process' +import { compile } from 'octane/compiler' +import { build } from 'vite' + +const packageRoot = fileURLToPath(new URL('..', import.meta.url)) +const temporaryDirectory = await mkdtemp(join(packageRoot, '.pack-smoke-')) +const packageManager = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' + +function run(command, args) { + const result = spawnSync(command, args, { + cwd: packageRoot, + encoding: 'utf8', + }) + if (result.status !== 0) { + throw new Error(result.stderr || `${command} failed`) + } + return result.stdout +} + +try { + const packedResult = JSON.parse( + run(packageManager, [ + 'pack', + '--pack-destination', + temporaryDirectory, + '--json', + ]), + ) + const tarballPath = isAbsolute(packedResult.filename) + ? packedResult.filename + : join(temporaryDirectory, packedResult.filename) + run('tar', ['-xzf', tarballPath, '-C', temporaryDirectory]) + + const extractedRoot = join( + temporaryDirectory, + 'node_modules/@tanstack/octane-query', + ) + await mkdir(dirname(extractedRoot), { recursive: true }) + await rename(join(temporaryDirectory, 'package'), extractedRoot) + const packedManifest = JSON.parse( + await readFile(join(extractedRoot, 'package.json'), 'utf8'), + ) + const requiredFiles = [ + 'README.md', + 'src/index.ts', + 'src/HydrationBoundary.tsrx', + 'src/HydrationBoundary.tsrx.d.ts', + 'src/QueryClientProvider.tsrx', + 'src/QueryClientProvider.tsrx.d.ts', + 'src/QueryErrorResetBoundary.tsrx', + 'src/QueryErrorResetBoundary.tsrx.d.ts', + ] + + for (const file of requiredFiles) { + await readFile(join(extractedRoot, file)) + } + + if (packedResult.files.some((file) => file.path.includes('/__tests__/'))) { + throw new Error('Packed package contains source tests') + } + if (packedManifest.dependencies['@tanstack/query-core'] === 'workspace:*') { + throw new Error( + 'Packed manifest contains an unresolved workspace dependency', + ) + } + if ( + JSON.stringify(packedManifest.octane?.hookSlots?.manual) !== + JSON.stringify(['src']) + ) { + throw new Error( + 'Packed manifest is missing the manual Octane slot contract', + ) + } + + const consumerPath = join(temporaryDirectory, 'consumer.tsrx') + await writeFile( + consumerPath, + `import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/octane-query' + +const client = new QueryClient() + +function Result() @{ + const query = useQuery({ queryKey: ['pack-smoke'], queryFn: () => Promise.resolve('ok') }) + {String(query.data ?? query.status)} +} + +export function App() @{ + + + +} +`, + ) + + const compiledModes = new Set() + const compilerPlugin = { + name: 'tanstack-octane-query:pack-smoke-compiler', + enforce: 'pre', + transform(code, id, options) { + const filename = id.split('?', 1)[0] + if (!filename.endsWith('.tsrx')) { + return null + } + const mode = options?.ssr ? 'server' : 'client' + compiledModes.add(mode) + return compile(code, filename, { mode }) + }, + } + const sharedConfig = { + configFile: false, + logLevel: 'silent', + root: temporaryDirectory, + plugins: [compilerPlugin], + resolve: { + alias: { + '@tanstack/query-core': join(packageRoot, '../query-core/src/index.ts'), + }, + conditions: ['module'], + }, + } + + await build({ + ...sharedConfig, + build: { + write: false, + lib: { entry: consumerPath, formats: ['es'] }, + }, + }) + await build({ + ...sharedConfig, + build: { + write: false, + ssr: consumerPath, + }, + ssr: { + noExternal: ['@tanstack/octane-query'], + }, + }) + + if (!compiledModes.has('client') || !compiledModes.has('server')) { + throw new Error( + 'Packed consumer was not compiled in client and server modes', + ) + } +} finally { + await rm(temporaryDirectory, { force: true, recursive: true }) +} diff --git a/packages/octane-query/scripts/check-tsrx.mjs b/packages/octane-query/scripts/check-tsrx.mjs new file mode 100644 index 00000000000..892bdcfcb5c --- /dev/null +++ b/packages/octane-query/scripts/check-tsrx.mjs @@ -0,0 +1,17 @@ +import { readFile } from 'node:fs/promises' +import { fileURLToPath } from 'node:url' +import { compile } from 'octane/compiler' + +const componentUrls = [ + new URL('../src/HydrationBoundary.tsrx', import.meta.url), + new URL('../src/QueryClientProvider.tsrx', import.meta.url), + new URL('../src/QueryErrorResetBoundary.tsrx', import.meta.url), +] + +for (const componentUrl of componentUrls) { + const filename = fileURLToPath(componentUrl) + const source = await readFile(componentUrl, 'utf8') + + compile(source, filename, { mode: 'client' }) + compile(source, filename, { mode: 'server' }) +} diff --git a/packages/octane-query/src/HydrationBoundary.tsrx b/packages/octane-query/src/HydrationBoundary.tsrx new file mode 100644 index 00000000000..f30e3680c79 --- /dev/null +++ b/packages/octane-query/src/HydrationBoundary.tsrx @@ -0,0 +1,83 @@ +// HydrationBoundary — hydrates dehydrated query state into the client, then +// renders its children. Full port of react-query's HydrationBoundary.tsx: +// +// - NEW queries hydrate IN RENDER (after SSR, hydration must land before the +// children render; during a transition, prerender as much as is safe). +// - EXISTING queries hydrate in an EFFECT: during a transition we must not +// update live queries/observers until the transition commits — an aborted +// transition must not clobber committed data (octane has transitions, so +// upstream's abort-safety applies here too). +// - A dehydrated query counts as NEWER than the cache when its dataUpdatedAt +// is ahead, OR when it carries a streaming `promise` and was dehydrated +// after the cached data settled (`dehydratedAt > dataUpdatedAt`) — that's +// what lets later stream chunks re-hydrate an already-settled query. +import { useRef, useMemo, useEffect } from 'octane'; +import { hydrate } from '@tanstack/query-core'; +import { resolveClient } from './context.ts'; +import type { OctaneNode } from 'octane'; +import type { DehydratedState, HydrateOptions, QueryClient } from '@tanstack/query-core'; + +type HydrationBoundaryProps = { + state?: unknown; + options?: HydrateOptions; + queryClient?: QueryClient; + children?: OctaneNode; +}; + +export function HydrationBoundary(props: HydrationBoundaryProps) @{ + const client = resolveClient(props.queryClient); + const state = props.state; + + const optionsRef = useRef(props.options); + useEffect(() => { + optionsRef.current = props.options; + }); + + // "In render" on purpose (the memo is a perf cache, not a semantic barrier) — + // hydrate new queries now, queue existing ones for the commit effect. + const hydrationQueue = useMemo(() => { + if (state && typeof state === 'object') { + const queryCache = client.getQueryCache(); + const queries = (state as DehydratedState).queries || []; + + const newQueries = []; + const existingQueries = []; + for (const dehydratedQuery of queries) { + const existingQuery = queryCache.get(dehydratedQuery.queryHash); + if (!existingQuery) { + newQueries.push(dehydratedQuery); + } else { + const hydrationIsNewer = + dehydratedQuery.state.dataUpdatedAt > existingQuery.state.dataUpdatedAt || + dehydratedQuery.promise && existingQuery.state.status !== 'pending' && + existingQuery.state.fetchStatus !== 'fetching' && + dehydratedQuery.dehydratedAt !== undefined && + dehydratedQuery.dehydratedAt > existingQuery.state.dataUpdatedAt; + if (hydrationIsNewer) { + existingQueries.push(dehydratedQuery); + } + } + } + + if (newQueries.length > 0) { + // hydrate() is idempotent for queries — safe to run in render. + hydrate(client, { queries: newQueries }, optionsRef.current); + } + if (existingQueries.length > 0) { + return existingQueries; + } + } + return undefined; + }, [client, state]); + + // Existing-query hydration lands at commit time (transition abort-safe). + useEffect(() => { + if (hydrationQueue) { + hydrate(client, { queries: hydrationQueue }, optionsRef.current); + } + }, [client, hydrationQueue]); + + <> + {props.children} + +} diff --git a/packages/octane-query/src/HydrationBoundary.tsrx.d.ts b/packages/octane-query/src/HydrationBoundary.tsrx.d.ts new file mode 100644 index 00000000000..2661827c23b --- /dev/null +++ b/packages/octane-query/src/HydrationBoundary.tsrx.d.ts @@ -0,0 +1,4 @@ +import type { ComponentBody } from 'octane' +import type { HydrationBoundaryProps } from './types' + +export declare const HydrationBoundary: ComponentBody diff --git a/packages/octane-query/src/QueryClientProvider.tsrx b/packages/octane-query/src/QueryClientProvider.tsrx new file mode 100644 index 00000000000..7073131c1d2 --- /dev/null +++ b/packages/octane-query/src/QueryClientProvider.tsrx @@ -0,0 +1,23 @@ +import { useEffect } from 'octane'; +import { QueryClientContext } from './context.ts'; +import type { OctaneNode } from 'octane'; +import type { QueryClient } from '@tanstack/query-core'; + +type QueryClientProviderProps = { + client: QueryClient; + children?: OctaneNode; +}; + +// QueryClientProvider — provides the QueryClient via context and mounts/unmounts +// it with the component. It's authored in .tsrx because it's a COMPONENT that +// renders a context Provider with children + a mount effect, which only the +// octane compiler can emit (the binding's hooks stay plain TS). +export function QueryClientProvider(props: QueryClientProviderProps) @{ + useEffect(() => { + props.client.mount(); + return () => { + props.client.unmount(); + }; + }, [props.client]); + {props.children} +} diff --git a/packages/octane-query/src/QueryClientProvider.tsrx.d.ts b/packages/octane-query/src/QueryClientProvider.tsrx.d.ts new file mode 100644 index 00000000000..b70e3a34afe --- /dev/null +++ b/packages/octane-query/src/QueryClientProvider.tsrx.d.ts @@ -0,0 +1,4 @@ +import type { ComponentBody } from 'octane' +import type { QueryClientProviderProps } from './types' + +export declare const QueryClientProvider: ComponentBody diff --git a/packages/octane-query/src/QueryErrorResetBoundary.tsrx b/packages/octane-query/src/QueryErrorResetBoundary.tsrx new file mode 100644 index 00000000000..fba6c306b8b --- /dev/null +++ b/packages/octane-query/src/QueryErrorResetBoundary.tsrx @@ -0,0 +1,26 @@ +import { useState, isChildrenBlock } from 'octane'; +import { QueryErrorResetBoundaryContext, createValue } from './errorResetBoundary.ts'; +import type { OctaneNode } from 'octane'; + +type QueryErrorResetBoundaryProps = { + children?: OctaneNode | ((value: ReturnType) => OctaneNode); +}; + +// QueryErrorResetBoundary — provides a fresh reset coordinator to its subtree. +// Place it above an error boundary; call `.reset()` from the boundary's retry so +// the query refetches instead of immediately re-throwing. Children may be a +// render prop receiving the boundary value — upstream's documented primary form: +// `{({ reset }) => } +// ` (react-query QueryErrorResetBoundary.tsx renders +// `typeof children === 'function' ? children(value) : children`; octane +// additionally excludes compiled children-blocks via isChildrenBlock). +export function QueryErrorResetBoundary(props: QueryErrorResetBoundaryProps) @{ + const [value] = useState(() => createValue()); + const resolved = + typeof props.children === 'function' && !isChildrenBlock(props.children) + ? props.children(value) + : props.children; + {resolved} +} diff --git a/packages/octane-query/src/QueryErrorResetBoundary.tsrx.d.ts b/packages/octane-query/src/QueryErrorResetBoundary.tsrx.d.ts new file mode 100644 index 00000000000..b8c91689b44 --- /dev/null +++ b/packages/octane-query/src/QueryErrorResetBoundary.tsrx.d.ts @@ -0,0 +1,11 @@ +import type { ComponentBody } from 'octane' +import type { + QueryErrorResetBoundaryFunction, + QueryErrorResetBoundaryProps, +} from './types' + +export interface QueryErrorResetBoundaryComponent extends ComponentBody { + (props: { children: QueryErrorResetBoundaryFunction }): void +} + +export declare const QueryErrorResetBoundary: QueryErrorResetBoundaryComponent diff --git a/packages/octane-query/src/__tests__/types.test-d.ts b/packages/octane-query/src/__tests__/types.test-d.ts new file mode 100644 index 00000000000..51ea573f9c2 --- /dev/null +++ b/packages/octane-query/src/__tests__/types.test-d.ts @@ -0,0 +1,62 @@ +import { expectTypeOf, it } from 'vitest' +import { + QueryErrorResetBoundary, + useQueries, + useSuspenseQueries, +} from '../index' +import type { QueryErrorResetBoundaryValue } from '../index' + +it('infers each useQueries tuple entry', () => { + const result = useQueries({ + queries: [ + { + queryKey: ['count'], + queryFn: async () => 1, + }, + { + queryKey: ['label'], + queryFn: async () => 'ready', + }, + ], + }) + + expectTypeOf(result[0].data).toEqualTypeOf() + expectTypeOf(result[1].data).toEqualTypeOf() +}) + +it('infers each useSuspenseQueries tuple entry as defined', () => { + const result = useSuspenseQueries({ + queries: [ + { + queryKey: ['count'], + queryFn: async () => 1, + }, + { + queryKey: ['label'], + queryFn: async () => 'ready', + }, + ], + }) + + expectTypeOf(result[0].data).toEqualTypeOf() + expectTypeOf(result[1].data).toEqualTypeOf() +}) + +it('contextually types the query error reset render prop', () => { + QueryErrorResetBoundary({ + children: ({ clearReset, isReset, reset }) => { + expectTypeOf(clearReset).toEqualTypeOf<() => void>() + expectTypeOf(isReset).toEqualTypeOf<() => boolean>() + expectTypeOf(reset).toEqualTypeOf<() => void>() + return undefined + }, + }) +}) + +it('exports the query error reset boundary value type', () => { + expectTypeOf().toEqualTypeOf<{ + clearReset: () => void + isReset: () => boolean + reset: () => void + }>() +}) diff --git a/packages/octane-query/src/context.ts b/packages/octane-query/src/context.ts new file mode 100644 index 00000000000..cfd7eb7e4b1 --- /dev/null +++ b/packages/octane-query/src/context.ts @@ -0,0 +1,35 @@ +import { createContext, useContext } from 'octane' +import type { QueryClient } from '@tanstack/query-core' + +// The QueryClient context — read by useQuery/useMutation/useQueryClient. Mirrors +// react-query's QueryClientContext. +export const QueryClientContext = createContext( + undefined, +) + +// Resolve the client: an explicitly-passed one wins, else the context value. +// `useContext` is keyed by context identity (not a per-call-site slot), so it's +// safe to call from this binding code without a compiler-injected slot. +export function resolveClient( + queryClient: QueryClient | undefined, +): QueryClient { + const ctxClient = useContext(QueryClientContext) + const client = queryClient ?? ctxClient + if (!client) { + throw new Error('No QueryClient set, use QueryClientProvider to set one') + } + return client +} + +// Signature matches @tanstack/react-query's QueryClientProvider.tsx. +export function useQueryClient(queryClient?: QueryClient): QueryClient + +export function useQueryClient(...args: Array): QueryClient { + // Args are `[queryClient?, slot?]`; the slot (symbol) is appended by the + // compiler. An explicit client is the first non-symbol arg. + const queryClient = + args.length && typeof args[0] !== 'symbol' + ? (args[0] as QueryClient) + : undefined + return resolveClient(queryClient) +} diff --git a/packages/octane-query/src/errorResetBoundary.ts b/packages/octane-query/src/errorResetBoundary.ts new file mode 100644 index 00000000000..e53cf1c5892 --- /dev/null +++ b/packages/octane-query/src/errorResetBoundary.ts @@ -0,0 +1,32 @@ +import { createContext, useContext } from 'octane' +import type { QueryErrorResetBoundaryValue } from './types' + +/** + * @knipignore Consumed by QueryErrorResetBoundary.tsrx, which Knip cannot parse. + */ +export function createValue(): QueryErrorResetBoundaryValue { + let isReset = false + return { + clearReset: () => { + isReset = false + }, + reset: () => { + isReset = true + }, + isReset: () => isReset, + } +} + +// The reset coordinator for error-boundary retries. While "reset", a thrown query +// error is NOT re-thrown (so the boundary's retry refetches instead of looping). +// The default value means: without a ``, `isReset()` is +// always false — identical to having no boundary. +/** + * @knipignore Consumed by QueryErrorResetBoundary.tsrx, which Knip cannot parse. + */ +export const QueryErrorResetBoundaryContext = + createContext(createValue()) + +export function useQueryErrorResetBoundary(): QueryErrorResetBoundaryValue { + return useContext(QueryErrorResetBoundaryContext) +} diff --git a/packages/octane-query/src/index.ts b/packages/octane-query/src/index.ts new file mode 100644 index 00000000000..7c6babdd09e --- /dev/null +++ b/packages/octane-query/src/index.ts @@ -0,0 +1,77 @@ +// @tanstack/octane-query — TanStack Query for the Octane renderer. +// +// Reuses @tanstack/query-core verbatim (QueryClient, observers, caches — all +// framework-agnostic) and reimplements the React binding on octane's hooks. The +// public surface matches @tanstack/react-query, so most query code works by +// changing the import. +export * from '@tanstack/query-core' + +export { useQuery } from './useQuery' +export { useMutation } from './useMutation' +export { useInfiniteQuery } from './useInfiniteQuery' +export { useSuspenseQuery, useSuspenseInfiniteQuery } from './useSuspenseQuery' +export { useSuspenseQueries } from './useSuspenseQueries' +export { usePrefetchQuery, usePrefetchInfiniteQuery } from './usePrefetch' +export { useQueries } from './useQueries' +export { queryOptions } from './queryOptions' +export { infiniteQueryOptions } from './infiniteQueryOptions' +export { mutationOptions } from './mutationOptions' +export { useIsFetching } from './useIsFetching' +export { useMutationState, useIsMutating } from './useMutationState' +export { useQueryClient, QueryClientContext } from './context' +export { QueryClientProvider } from './QueryClientProvider.tsrx' +export { HydrationBoundary } from './HydrationBoundary.tsrx' +export { IsRestoringProvider, useIsRestoring } from './isRestoring' +export { QueryErrorResetBoundary } from './QueryErrorResetBoundary.tsrx' +export { useQueryErrorResetBoundary } from './errorResetBoundary' + +// The binding-level type surface (1:1 with @tanstack/react-query's exports). +export type { + AnyUseBaseQueryOptions, + UseBaseQueryOptions, + UsePrefetchQueryOptions, + AnyUseQueryOptions, + UseQueryOptions, + AnyUseSuspenseQueryOptions, + UseSuspenseQueryOptions, + AnyUseInfiniteQueryOptions, + UseInfiniteQueryOptions, + AnyUseSuspenseInfiniteQueryOptions, + UseSuspenseInfiniteQueryOptions, + UseBaseQueryResult, + UseQueryResult, + UseSuspenseQueryResult, + DefinedUseQueryResult, + UseInfiniteQueryResult, + DefinedUseInfiniteQueryResult, + UseSuspenseInfiniteQueryResult, + AnyUseMutationOptions, + UseMutationOptions, + UseMutateFunction, + UseMutateAsyncFunction, + UseBaseMutationResult, + UseMutationResult, + QueryClientProviderProps, + HydrationBoundaryProps, + QueryErrorResetFunction, + QueryErrorIsResetFunction, + QueryErrorClearResetFunction, + QueryErrorResetBoundaryValue, + QueryErrorResetBoundaryFunction, + QueryErrorResetBoundaryProps, +} from './types' +export type { + DefinedInitialDataOptions, + UndefinedInitialDataOptions, + UnusedSkipTokenOptions, +} from './queryOptions' +export type { + DefinedInitialDataInfiniteOptions, + UndefinedInitialDataInfiniteOptions, + UnusedSkipTokenInfiniteOptions, +} from './infiniteQueryOptions' +export type { QueriesOptions, QueriesResults } from './queries-types' +export type { + SuspenseQueriesOptions, + SuspenseQueriesResults, +} from './suspense-queries-types' diff --git a/packages/octane-query/src/infiniteQueryOptions.ts b/packages/octane-query/src/infiniteQueryOptions.ts new file mode 100644 index 00000000000..9b7a2fdd6cb --- /dev/null +++ b/packages/octane-query/src/infiniteQueryOptions.ts @@ -0,0 +1,150 @@ +// Verbatim port of @tanstack/react-query's infiniteQueryOptions.ts — a typed identity helper +import type { + DataTag, + DefaultError, + InfiniteData, + InitialDataFunction, + NonUndefinedGuard, + OmitKeyof, + QueryKey, + SkipToken, +} from '@tanstack/query-core' +import type { UseInfiniteQueryOptions } from './types' + +export type UndefinedInitialDataInfiniteOptions< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = UseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam +> & { + initialData?: + | undefined + | NonUndefinedGuard> + | InitialDataFunction< + NonUndefinedGuard> + > +} + +export type UnusedSkipTokenInfiniteOptions< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = OmitKeyof< + UseInfiniteQueryOptions, + 'queryFn' +> & { + queryFn?: Exclude< + UseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >['queryFn'], + SkipToken | undefined + > +} + +export type DefinedInitialDataInfiniteOptions< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = UseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam +> & { + initialData: + | NonUndefinedGuard> + | (() => NonUndefinedGuard>) + | undefined +} + +export function infiniteQueryOptions< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: DefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, +): DefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam +> & { + queryKey: DataTag, TError> +} + +export function infiniteQueryOptions< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: UnusedSkipTokenInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, +): UnusedSkipTokenInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam +> & { + queryKey: DataTag, TError> +} + +export function infiniteQueryOptions< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: UndefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, +): UndefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam +> & { + queryKey: DataTag, TError> +} + +export function infiniteQueryOptions(options: unknown) { + return options +} diff --git a/packages/octane-query/src/internal.ts b/packages/octane-query/src/internal.ts new file mode 100644 index 00000000000..c93270321c9 --- /dev/null +++ b/packages/octane-query/src/internal.ts @@ -0,0 +1,186 @@ +// Shared internals for the binding hooks. +import { shouldThrowError } from '@tanstack/query-core' +import { use, useId, useRef } from 'octane' + +// Derive a stable, distinct sub-slot from a wrapper's compiler-injected slot, so +// a hook composing multiple base hooks gives each one its own identity. Tags are +// namespaced per hook (e.g. ':oq:obs', ':ms:cb') to avoid cross-hook collisions. +// Memoized: subSlot runs on EVERY hook call every render, and the naive form +// pays a string concat + global symbol-registry lookup each time. The cache is +// keyed by the slot symbol itself; the minted value is byte-identical to the +// uncached Symbol.for result, so identity is preserved across HMR re-evals and +// the per-package copies of this helper. Key universe is bounded: slots are +// per-call-site module constants (never minted per render). +const subSlotCache = new Map>() +export function subSlot( + slot: symbol | undefined, + tag: string, +): symbol | undefined { + if (slot === undefined) return undefined + let byTag = subSlotCache.get(slot) + if (byTag === undefined) subSlotCache.set(slot, (byTag = new Map())) + let sym = byTag.get(tag) + if (sym === undefined) + byTag.set(tag, (sym = Symbol.for((slot.description ?? '') + ':' + tag))) + return sym +} + +// Split the compiler-injected trailing slot off a hook's runtime args, returning +// the user args (everything before it) and the slot. +export function splitSlot(args: Array): [Array, symbol | undefined] { + const tail = args[args.length - 1] + const slot = typeof tail === 'symbol' ? tail : undefined + return [slot !== undefined ? args.slice(0, -1) : args, slot] +} + +// react-query's default suspense throwOnError: only throw if there's no data. +export const defaultThrowOnError = (_error: unknown, query: any): boolean => + query.state.data === undefined + +// react-query's ensureSuspenseTimers: a suspense query gets a >=1s staleTime/gcTime +// floor so it can't immediately refetch and re-trigger the fallback in a loop. +export function ensureSuspenseTimers(defaultedOptions: any): void { + if (defaultedOptions.suspense) { + const MIN = 1000 + const clamp = (value: any) => + value === 'static' ? value : Math.max(value ?? MIN, MIN) + const orig = defaultedOptions.staleTime + defaultedOptions.staleTime = + typeof orig === 'function' + ? (...args: Array) => clamp(orig(...args)) + : clamp(orig) + if (typeof defaultedOptions.gcTime === 'number') { + defaultedOptions.gcTime = Math.max(defaultedOptions.gcTime, MIN) + } + } +} + +// prevent-error-boundary-retry: when a query opts into throwing, don't retry on +// mount so an already-errored cached query re-throws immediately — UNLESS the +// reset boundary has been reset, in which case a retry is expected. +export function ensurePreventErrorBoundaryRetry( + options: any, + errorResetBoundary: { isReset: () => boolean }, + query: any, +): void { + const throwOnError = + query?.state.error && typeof options.throwOnError === 'function' + ? shouldThrowError(options.throwOnError, [query.state.error, query]) + : options.throwOnError + if ( + options.suspense || + options.experimental_prefetchInRender || + throwOnError + ) { + if (!errorResetBoundary.isReset()) { + options.retryOnMount = false + } + } +} + +export const shouldSuspend = (defaultedOptions: any, result: any): boolean => + defaultedOptions?.suspense && result.isPending + +// A pending result that will actually fetch (not blocked by persistence restore). +export const willFetch = (result: any, isRestoring: boolean): boolean => + result.isLoading && result.isFetching && !isRestoring + +// react-query's suspense fetch: kick the optimistic fetch and — CRUCIALLY — clear +// the reset boundary on error. Without the clearReset, a boundary reset→retry +// that fails AGAIN leaves `isReset()` true at replay, `getHasError` returns +// false, and a suspense component falls through to render with undefined data +// instead of re-throwing to the boundary. The returned promise resolves even on +// error (the replay surfaces the error through the error-boundary throw). +export function fetchOptimistic( + defaultedOptions: any, + observer: any, + errorResetBoundary: { clearReset: () => void }, +): Promise { + return observer.fetchOptimistic(defaultedOptions).catch(() => { + errorResetBoundary.clearReset() + }) +} + +type SuspensePromise = PromiseLike & { + status?: 'pending' | 'fulfilled' | 'rejected' +} + +interface SuspensePromiseCache { + key: unknown + promise: SuspensePromise | undefined +} + +// The server runtime accepts an explicit replay site key. Octane's universal +// public type currently exposes only the client-compatible one-argument form. +const useWithSiteKey = use as unknown as ( + usable: PromiseLike, + siteKey?: symbol | string, +) => unknown + +/** + * Suspend through a stable `use()` occurrence for one query-hook call site. + * + * `use()` tracks thenables by dynamic call order. A component with two + * sequential suspense queries initially reaches only the first query; when it + * resolves, the first query no longer needs to suspend and the second query is + * reached for the first time. If the first call simply skips `use()`, the + * second query takes its call-order position and replay reuses the first + * query's fulfilled promise, exposing the second query's still-pending result. + * + * Retain the promise that suspended this query hook and keep reading it after + * it settles. That reserves the query hook's dynamic call-order position on + * the client. Streaming SSR replays fresh hook passes, so an unconditional + * `useId()` gives each query-hook instance a deterministic server replay key, + * including when a custom hook is called more than once. A new query key or + * retry replaces a settled promise; re-renders during the same pending fetch + * reuse the in-flight promise. + */ +export function useSuspensePromise( + needsSuspense: boolean, + key: unknown, + createPromise: () => PromiseLike, + slot: symbol | undefined, +): void { + const siteKey = useId(subSlot(slot, 'id')) + const cache = useRef( + { key: undefined, promise: undefined }, + slot, + ).current + if ( + needsSuspense && + (cache.promise === undefined || + cache.key !== key || + cache.promise.status !== 'pending') + ) { + cache.key = key + cache.promise = createPromise() + } + if (cache.promise !== undefined) { + useWithSiteKey(cache.promise, siteKey) + } +} + +// react-query's getHasError: only throw if the query errored, the boundary isn't +// reset, the query isn't refetching, and the options opt into throwing. +export function getHasError({ + result, + errorResetBoundary, + throwOnError, + query, + suspense, +}: { + result: any + errorResetBoundary: { isReset: () => boolean } + throwOnError: any + query: any + suspense: any +}): boolean { + return ( + result.isError && + !errorResetBoundary.isReset() && + !result.isFetching && + query && + ((suspense && result.data === undefined) || + shouldThrowError(throwOnError, [result.error, query])) + ) +} diff --git a/packages/octane-query/src/isRestoring.ts b/packages/octane-query/src/isRestoring.ts new file mode 100644 index 00000000000..007f1e2bc40 --- /dev/null +++ b/packages/octane-query/src/isRestoring.ts @@ -0,0 +1,13 @@ +import { createContext, useContext } from 'octane' + +// IsRestoring — true while a persisted client is being restored from storage. While +// restoring, queries read the cache but don't subscribe/fetch (they wait for the +// restore to finish). Defaults to false, so without a provider it's inert. +const IsRestoringContext = createContext(false) + +export function useIsRestoring(): boolean { + return useContext(IsRestoringContext) +} + +// `…` — octane's built-in context Provider. +export const IsRestoringProvider = IsRestoringContext.Provider diff --git a/packages/octane-query/src/mutationOptions.ts b/packages/octane-query/src/mutationOptions.ts new file mode 100644 index 00000000000..3499348d263 --- /dev/null +++ b/packages/octane-query/src/mutationOptions.ts @@ -0,0 +1,42 @@ +// Verbatim port of @tanstack/react-query's mutationOptions.ts — a typed identity helper +import type { DefaultError, WithRequired } from '@tanstack/query-core' +import type { UseMutationOptions } from './types' + +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: WithRequired< + UseMutationOptions, + 'mutationKey' + >, +): WithRequired< + UseMutationOptions, + 'mutationKey' +> +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: Omit< + UseMutationOptions, + 'mutationKey' + >, +): Omit< + UseMutationOptions, + 'mutationKey' +> +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: UseMutationOptions, +): UseMutationOptions { + return options +} diff --git a/packages/octane-query/src/queries-types.ts b/packages/octane-query/src/queries-types.ts new file mode 100644 index 00000000000..651413f1569 --- /dev/null +++ b/packages/octane-query/src/queries-types.ts @@ -0,0 +1,184 @@ +// The useQueries option/result tuple types — a 1:1 port of the type block in +// @tanstack/react-query's useQueries.ts (pure query-core generics, no React). +// `GetUseQueryOptionsForUseQueries` / `GetUseQueryResult` recursively unwrap +// per-entry generics so tuple inputs infer per-index option/result types. +import type { + DefinedUseQueryResult, + UseQueryOptions, + UseQueryResult, +} from './types' +import type { + DefaultError, + OmitKeyof, + QueriesPlaceholderDataFunction, + QueryFunction, + QueryKey, + ThrowOnError, +} from '@tanstack/query-core' + +// This defines the `UseQueryOptions` that are accepted in `QueriesOptions` & `GetOptions`. +// `placeholderData` function always gets undefined passed +type UseQueryOptionsForUseQueries< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = OmitKeyof< + UseQueryOptions, + 'placeholderData' | 'subscribed' +> & { + placeholderData?: TQueryFnData | QueriesPlaceholderDataFunction +} + +// Avoid TS depth-limit error in case of large array literal +type MAXIMUM_DEPTH = 20 + +// Widen the type of the symbol to enable type inference even if skipToken is not immutable. +type SkipTokenForUseQueries = symbol + +export type GetUseQueryOptionsForUseQueries = + // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData } + T extends { + queryFnData: infer TQueryFnData + error?: infer TError + data: infer TData + } + ? UseQueryOptionsForUseQueries + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? UseQueryOptionsForUseQueries + : T extends { data: infer TData; error?: infer TError } + ? UseQueryOptionsForUseQueries + : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] + T extends [infer TQueryFnData, infer TError, infer TData] + ? UseQueryOptionsForUseQueries + : T extends [infer TQueryFnData, infer TError] + ? UseQueryOptionsForUseQueries + : T extends [infer TQueryFnData] + ? UseQueryOptionsForUseQueries + : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided + T extends { + queryFn?: + | QueryFunction + | SkipTokenForUseQueries + select?: (data: any) => infer TData + throwOnError?: ThrowOnError + } + ? UseQueryOptionsForUseQueries< + TQueryFnData, + unknown extends TError ? DefaultError : TError, + unknown extends TData ? TQueryFnData : TData, + TQueryKey + > + : // Fallback + UseQueryOptionsForUseQueries + +// A defined initialData setting should return a DefinedUseQueryResult rather than UseQueryResult +type GetDefinedOrUndefinedQueryResult = T extends { + initialData?: infer TInitialData +} + ? unknown extends TInitialData + ? UseQueryResult + : TInitialData extends TData + ? DefinedUseQueryResult + : TInitialData extends () => infer TInitialDataResult + ? unknown extends TInitialDataResult + ? UseQueryResult + : TInitialDataResult extends TData + ? DefinedUseQueryResult + : UseQueryResult + : UseQueryResult + : UseQueryResult + +type GetUseQueryResult = + // Part 1: responsible for mapping explicit type parameter to function result, if object + T extends { queryFnData: any; error?: infer TError; data: infer TData } + ? GetDefinedOrUndefinedQueryResult + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? GetDefinedOrUndefinedQueryResult + : T extends { data: infer TData; error?: infer TError } + ? GetDefinedOrUndefinedQueryResult + : // Part 2: responsible for mapping explicit type parameter to function result, if tuple + T extends [any, infer TError, infer TData] + ? GetDefinedOrUndefinedQueryResult + : T extends [infer TQueryFnData, infer TError] + ? GetDefinedOrUndefinedQueryResult + : T extends [infer TQueryFnData] + ? GetDefinedOrUndefinedQueryResult + : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided + T extends { + queryFn?: + | QueryFunction + | SkipTokenForUseQueries + select?: (data: any) => infer TData + throwOnError?: ThrowOnError + } + ? GetDefinedOrUndefinedQueryResult< + T, + unknown extends TData ? TQueryFnData : TData, + unknown extends TError ? DefaultError : TError + > + : // Fallback + UseQueryResult + +/** + * QueriesOptions reducer recursively unwraps function arguments to infer/enforce type param + */ +export type QueriesOptions< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetUseQueryOptionsForUseQueries] + : T extends [infer Head, ...infer Tails] + ? QueriesOptions< + [...Tails], + [...TResults, GetUseQueryOptionsForUseQueries], + [...TDepth, 1] + > + : ReadonlyArray extends T + ? T + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! + // use this to infer the param types in the case of Array.map() argument + T extends Array< + UseQueryOptionsForUseQueries< + infer TQueryFnData, + infer TError, + infer TData, + infer TQueryKey + > + > + ? Array< + UseQueryOptionsForUseQueries< + TQueryFnData, + TError, + TData, + TQueryKey + > + > + : // Fallback + Array + +/** + * QueriesResults reducer recursively maps type param to results + */ +export type QueriesResults< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetUseQueryResult] + : T extends [infer Head, ...infer Tails] + ? QueriesResults< + [...Tails], + [...TResults, GetUseQueryResult], + [...TDepth, 1] + > + : { [K in keyof T]: GetUseQueryResult } diff --git a/packages/octane-query/src/queryOptions.ts b/packages/octane-query/src/queryOptions.ts new file mode 100644 index 00000000000..b42e41d1a5b --- /dev/null +++ b/packages/octane-query/src/queryOptions.ts @@ -0,0 +1,88 @@ +// Verbatim port of @tanstack/react-query's queryOptions.ts — a typed identity helper +import type { + DataTag, + DefaultError, + InitialDataFunction, + NonUndefinedGuard, + OmitKeyof, + QueryFunction, + QueryKey, + SkipToken, +} from '@tanstack/query-core' +import type { UseQueryOptions } from './types' + +export type UndefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = UseQueryOptions & { + initialData?: + | undefined + | InitialDataFunction> + | NonUndefinedGuard +} + +export type UnusedSkipTokenOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = OmitKeyof< + UseQueryOptions, + 'queryFn' +> & { + queryFn?: Exclude< + UseQueryOptions['queryFn'], + SkipToken | undefined + > +} + +export type DefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit, 'queryFn'> & { + initialData: + | NonUndefinedGuard + | (() => NonUndefinedGuard) + queryFn?: QueryFunction +} + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: DefinedInitialDataOptions, +): DefinedInitialDataOptions & { + queryKey: DataTag +} + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UnusedSkipTokenOptions, +): UnusedSkipTokenOptions & { + queryKey: DataTag +} + +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UndefinedInitialDataOptions, +): UndefinedInitialDataOptions & { + queryKey: DataTag +} + +export function queryOptions(options: unknown) { + return options +} diff --git a/packages/octane-query/src/suspense-queries-types.ts b/packages/octane-query/src/suspense-queries-types.ts new file mode 100644 index 00000000000..eeff73b00bd --- /dev/null +++ b/packages/octane-query/src/suspense-queries-types.ts @@ -0,0 +1,161 @@ +// The useSuspenseQueries option/result tuple types — a 1:1 port of the type +// block in @tanstack/react-query's useSuspenseQueries.ts (pure query-core +// generics, no React). +import type { UseSuspenseQueryOptions, UseSuspenseQueryResult } from './types' +import type { + DefaultError, + QueryFunction, + ThrowOnError, +} from '@tanstack/query-core' + +// Avoid TS depth-limit error in case of large array literal +type MAXIMUM_DEPTH = 20 + +// Widen the type of the symbol to enable type inference even if skipToken is not immutable. +type SkipTokenForUseQueries = symbol + +export type GetUseSuspenseQueryOptions = + // Part 1: responsible for applying explicit type parameter to function arguments, if object { queryFnData: TQueryFnData, error: TError, data: TData } + T extends { + queryFnData: infer TQueryFnData + error?: infer TError + data: infer TData + } + ? UseSuspenseQueryOptions + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? UseSuspenseQueryOptions + : T extends { data: infer TData; error?: infer TError } + ? UseSuspenseQueryOptions + : // Part 2: responsible for applying explicit type parameter to function arguments, if tuple [TQueryFnData, TError, TData] + T extends [infer TQueryFnData, infer TError, infer TData] + ? UseSuspenseQueryOptions + : T extends [infer TQueryFnData, infer TError] + ? UseSuspenseQueryOptions + : T extends [infer TQueryFnData] + ? UseSuspenseQueryOptions + : // Part 3: responsible for inferring and enforcing type if no explicit parameter was provided + T extends { + queryFn?: + | QueryFunction + | SkipTokenForUseQueries + select?: (data: any) => infer TData + throwOnError?: ThrowOnError + } + ? UseSuspenseQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey + > + : T extends { + queryFn?: + | QueryFunction + | SkipTokenForUseQueries + throwOnError?: ThrowOnError + } + ? UseSuspenseQueryOptions< + TQueryFnData, + TError, + TQueryFnData, + TQueryKey + > + : // Fallback + UseSuspenseQueryOptions + +type GetUseSuspenseQueryResult = + // Part 1: responsible for mapping explicit type parameter to function result, if object + T extends { queryFnData: any; error?: infer TError; data: infer TData } + ? UseSuspenseQueryResult + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? UseSuspenseQueryResult + : T extends { data: infer TData; error?: infer TError } + ? UseSuspenseQueryResult + : // Part 2: responsible for mapping explicit type parameter to function result, if tuple + T extends [any, infer TError, infer TData] + ? UseSuspenseQueryResult + : T extends [infer TQueryFnData, infer TError] + ? UseSuspenseQueryResult + : T extends [infer TQueryFnData] + ? UseSuspenseQueryResult + : // Part 3: responsible for mapping inferred type to results, if no explicit parameter was provided + T extends { + queryFn?: + | QueryFunction + | SkipTokenForUseQueries + select?: (data: any) => infer TData + throwOnError?: ThrowOnError + } + ? UseSuspenseQueryResult< + unknown extends TData ? TQueryFnData : TData, + unknown extends TError ? DefaultError : TError + > + : T extends { + queryFn?: + | QueryFunction + | SkipTokenForUseQueries + throwOnError?: ThrowOnError + } + ? UseSuspenseQueryResult< + TQueryFnData, + unknown extends TError ? DefaultError : TError + > + : // Fallback + UseSuspenseQueryResult + +/** + * SuspenseQueriesOptions reducer recursively unwraps function arguments to infer/enforce type param + */ +export type SuspenseQueriesOptions< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetUseSuspenseQueryOptions] + : T extends [infer Head, ...infer Tails] + ? SuspenseQueriesOptions< + [...Tails], + [...TResults, GetUseSuspenseQueryOptions], + [...TDepth, 1] + > + : Array extends T + ? T + : // If T is *some* array but we couldn't assign unknown[] to it, then it must hold some known/homogenous type! + // use this to infer the param types in the case of Array.map() argument + T extends Array< + UseSuspenseQueryOptions< + infer TQueryFnData, + infer TError, + infer TData, + infer TQueryKey + > + > + ? Array< + UseSuspenseQueryOptions + > + : // Fallback + Array + +/** + * SuspenseQueriesResults reducer recursively maps type param to results + */ +export type SuspenseQueriesResults< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetUseSuspenseQueryResult] + : T extends [infer Head, ...infer Tails] + ? SuspenseQueriesResults< + [...Tails], + [...TResults, GetUseSuspenseQueryResult], + [...TDepth, 1] + > + : { [K in keyof T]: GetUseSuspenseQueryResult } diff --git a/packages/octane-query/src/types.ts b/packages/octane-query/src/types.ts new file mode 100644 index 00000000000..8c79c4e0ec8 --- /dev/null +++ b/packages/octane-query/src/types.ts @@ -0,0 +1,288 @@ +// The binding-level type surface — a 1:1 port of @tanstack/react-query's +// types.ts (it is built entirely on query-core generics; nothing here is +// React-specific), plus the component prop / context-value types at the bottom +// (where upstream's React.ReactNode becomes octane's `unknown` renderable). +import type { + DefaultError, + DefinedInfiniteQueryObserverResult, + DefinedQueryObserverResult, + DehydratedState, + DistributiveOmit, + FetchQueryOptions, + HydrateOptions, + InfiniteQueryObserverOptions, + InfiniteQueryObserverResult, + MutateFunction, + MutationObserverOptions, + MutationObserverResult, + OmitKeyof, + Override, + QueryClient, + QueryKey, + QueryObserverOptions, + QueryObserverResult, + SkipToken, +} from '@tanstack/query-core' + +export type AnyUseBaseQueryOptions = UseBaseQueryOptions< + any, + any, + any, + any, + any +> +export interface UseBaseQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> extends QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey +> { + /** + * Set this to `false` to unsubscribe this observer from updates to the query cache. + * Defaults to `true`. + */ + subscribed?: boolean +} + +export interface UsePrefetchQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> extends OmitKeyof< + FetchQueryOptions, + 'queryFn' +> { + queryFn?: Exclude< + FetchQueryOptions['queryFn'], + SkipToken + > +} + +export type AnyUseQueryOptions = UseQueryOptions +export interface UseQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> extends OmitKeyof< + UseBaseQueryOptions, + 'suspense' +> {} + +export type AnyUseSuspenseQueryOptions = UseSuspenseQueryOptions< + any, + any, + any, + any +> +export interface UseSuspenseQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> extends OmitKeyof< + UseQueryOptions, + 'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData' +> { + queryFn?: Exclude< + UseQueryOptions['queryFn'], + SkipToken + > +} + +export type AnyUseInfiniteQueryOptions = UseInfiniteQueryOptions< + any, + any, + any, + any, + any +> +export interface UseInfiniteQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> extends OmitKeyof< + InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + 'suspense' +> { + /** + * Set this to `false` to unsubscribe this observer from updates to the query cache. + * Defaults to `true`. + */ + subscribed?: boolean +} + +export type AnyUseSuspenseInfiniteQueryOptions = + UseSuspenseInfiniteQueryOptions +export interface UseSuspenseInfiniteQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> extends OmitKeyof< + UseInfiniteQueryOptions, + 'queryFn' | 'enabled' | 'throwOnError' | 'placeholderData' +> { + queryFn?: Exclude< + UseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >['queryFn'], + SkipToken + > +} + +export type UseBaseQueryResult< + TData = unknown, + TError = DefaultError, +> = QueryObserverResult + +export type UseQueryResult< + TData = unknown, + TError = DefaultError, +> = UseBaseQueryResult + +export type UseSuspenseQueryResult< + TData = unknown, + TError = DefaultError, +> = DistributiveOmit< + DefinedQueryObserverResult, + 'isPlaceholderData' | 'promise' +> + +export type DefinedUseQueryResult< + TData = unknown, + TError = DefaultError, +> = DefinedQueryObserverResult + +export type UseInfiniteQueryResult< + TData = unknown, + TError = DefaultError, +> = InfiniteQueryObserverResult + +export type DefinedUseInfiniteQueryResult< + TData = unknown, + TError = DefaultError, +> = DefinedInfiniteQueryObserverResult + +export type UseSuspenseInfiniteQueryResult< + TData = unknown, + TError = DefaultError, +> = OmitKeyof< + DefinedInfiniteQueryObserverResult, + 'isPlaceholderData' | 'promise' +> + +export type AnyUseMutationOptions = UseMutationOptions +export interface UseMutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> extends OmitKeyof< + MutationObserverOptions, + '_defaulted' +> {} + +export type UseMutateFunction< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> = ( + ...args: Parameters< + MutateFunction + > +) => void + +export type UseMutateAsyncFunction< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> = MutateFunction + +export type UseBaseMutationResult< + TData = unknown, + TError = DefaultError, + TVariables = unknown, + TOnMutateResult = unknown, +> = Override< + MutationObserverResult, + { mutate: UseMutateFunction } +> & { + mutateAsync: UseMutateAsyncFunction< + TData, + TError, + TVariables, + TOnMutateResult + > +} + +export type UseMutationResult< + TData = unknown, + TError = DefaultError, + TVariables = unknown, + TOnMutateResult = unknown, +> = UseBaseMutationResult + +// --------------------------------------------------------------------------- +// Component prop + context-value types (octane: children are renderables, so +// upstream's React.ReactNode becomes `unknown`; render-prop children are also +// accepted where upstream accepts them). +// --------------------------------------------------------------------------- + +export interface QueryClientProviderProps { + client: QueryClient + children?: unknown +} + +export interface HydrationBoundaryProps { + state: DehydratedState | null | undefined + options?: OmitKeyof & { + defaultOptions?: OmitKeyof< + Exclude, + 'mutations' + > + } + children?: unknown + queryClient?: QueryClient +} + +export type QueryErrorResetFunction = () => void +export type QueryErrorIsResetFunction = () => boolean +export type QueryErrorClearResetFunction = () => void + +export interface QueryErrorResetBoundaryValue { + clearReset: QueryErrorClearResetFunction + isReset: QueryErrorIsResetFunction + reset: QueryErrorResetFunction +} + +export type QueryErrorResetBoundaryFunction = ( + value: QueryErrorResetBoundaryValue, +) => unknown + +export interface QueryErrorResetBoundaryProps { + children: QueryErrorResetBoundaryFunction | unknown +} diff --git a/packages/octane-query/src/useBaseQuery.ts b/packages/octane-query/src/useBaseQuery.ts new file mode 100644 index 00000000000..c47ba1371d8 --- /dev/null +++ b/packages/octane-query/src/useBaseQuery.ts @@ -0,0 +1,168 @@ +// `useBaseQuery` — the shared core of `useQuery` (and friends), reimplemented on +// octane's hooks. Mirrors @tanstack/react-query's useBaseQuery: it creates a +// query Observer, subscribes to it via useSyncExternalStore, and pushes option +// changes through useEffect. The single compiler-injected slot is split into +// distinct sub-slots for each internal base hook, the same way the zustand +// `traditional` binding does. +import { useCallback, useEffect, useState, useSyncExternalStore } from 'octane' +import { environmentManager, noop, notifyManager } from '@tanstack/query-core' +import { resolveClient } from './context' +import { useIsRestoring } from './isRestoring' +import { useQueryErrorResetBoundary } from './errorResetBoundary' +import { + ensurePreventErrorBoundaryRetry, + ensureSuspenseTimers, + fetchOptimistic, + getHasError, + shouldSuspend, + subSlot, + useSuspensePromise, + willFetch, +} from './internal' + +export function useBaseQuery( + options: any, + Observer: any, + queryClient: any, + slot: symbol | undefined, +): any { + if (process.env.NODE_ENV !== 'production') { + if (typeof options !== 'object' || Array.isArray(options)) { + throw new Error( + 'Bad argument type. Starting with v5, only the "Object" form is allowed when calling query related functions. Please use the error stack to find the culprit call. More info here: https://tanstack.com/query/latest/docs/react/guides/migrating-to-v5#supports-a-single-signature-one-object', + ) + } + } + + const oq = (tag: string) => subSlot(slot, 'oq:' + tag) + const client = resolveClient(queryClient) + const isRestoring = useIsRestoring() + const errorResetBoundary = useQueryErrorResetBoundary() + const defaultedOptions = client.defaultQueryOptions(options) + + ;(client.getDefaultOptions().queries as any)?._experimental_beforeQuery?.( + defaultedOptions, + ) + + const query = client.getQueryCache().get(defaultedOptions.queryHash) + + if (process.env.NODE_ENV !== 'production') { + if (!defaultedOptions.queryFn) { + console.error( + `[${defaultedOptions.queryHash}]: No queryFn was passed as an option, and no default queryFn was found. The queryFn parameter is only optional when using a default queryFn. More info here: https://tanstack.com/query/latest/docs/framework/react/guides/default-query-function`, + ) + } + } + + // `subscribed: false` makes a passive query (read the cache, never subscribe). + // While restoring a persisted client, queries also stay passive. + const subscribed = options.subscribed !== false + defaultedOptions._optimisticResults = isRestoring + ? 'isRestoring' + : subscribed + ? 'optimistic' + : undefined + + ensureSuspenseTimers(defaultedOptions) + ensurePreventErrorBoundaryRetry(defaultedOptions, errorResetBoundary, query) + // Clear the reset boundary on mount (so a fresh mount can throw again). + useEffect( + () => { + errorResetBoundary.clearReset() + }, + [errorResetBoundary], + oq('clr'), + ) + + // Probed BEFORE creating the Observer — the constructor can create the entry. + const isNewCacheEntry = !client + .getQueryCache() + .get(defaultedOptions.queryHash) + + const [observer] = useState( + () => new Observer(client, defaultedOptions), + oq('obs'), + ) + + const result = observer.getOptimisticResult(defaultedOptions) + + const shouldSubscribe = !isRestoring && subscribed + useSyncExternalStore( + useCallback( + (onStoreChange: () => void) => { + const unsubscribe = shouldSubscribe + ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) + : noop + observer.updateResult() + return unsubscribe + }, + [observer, shouldSubscribe], + oq('cb'), + ), + () => observer.getCurrentResult(), + () => observer.getCurrentResult(), + oq('uses'), + ) + + useEffect( + () => { + observer.setOptions(defaultedOptions) + }, + [defaultedOptions, observer], + oq('eff'), + ) + + // Suspend on a promise retained for this query-hook call site. Keeping the + // settled promise in the dynamic use() sequence prevents a later sequential + // query from taking this query's replay position. fetchOptimistic clears the + // reset boundary on error; the replay surfaces that error through the + // error-boundary throw below. + useSuspensePromise( + shouldSuspend(defaultedOptions, result), + defaultedOptions.queryHash, + () => fetchOptimistic(defaultedOptions, observer, errorResetBoundary), + oq('sus'), + ) + + // Error boundary: throw so the nearest @try/@catch (or ) handles + // it, when the query errored and the options opt into throwing. + if ( + getHasError({ + result, + errorResetBoundary, + throwOnError: defaultedOptions.throwOnError, + query, + suspense: defaultedOptions.suspense, + }) + ) { + throw result.error + } + + ;(client.getDefaultOptions().queries as any)?._experimental_afterQuery?.( + defaultedOptions, + result, + ) + + // experimental_prefetchInRender: kick the fetch during render so + // `result.promise` settles even if the component unmounts, and finalize the + // observer's thenable (via updateResult) when the data lands. + if ( + defaultedOptions.experimental_prefetchInRender && + !environmentManager.isServer() && + willFetch(result, isRestoring) + ) { + const promise = isNewCacheEntry + ? // Fetch immediately on render so `.promise` resolves even on unmount. + fetchOptimistic(defaultedOptions, observer, errorResetBoundary) + : // Subscribe to the cache promise to finalize the current thenable. + query?.promise + + promise?.catch(noop).finally(() => { + observer.updateResult() + }) + } + + return !defaultedOptions.notifyOnChangeProps + ? observer.trackResult(result) + : result +} diff --git a/packages/octane-query/src/useInfiniteQuery.ts b/packages/octane-query/src/useInfiniteQuery.ts new file mode 100644 index 00000000000..895da60424e --- /dev/null +++ b/packages/octane-query/src/useInfiniteQuery.ts @@ -0,0 +1,75 @@ +import { InfiniteQueryObserver } from '@tanstack/query-core' +import { useBaseQuery } from './useBaseQuery' +import { splitSlot } from './internal' +import type { + DefaultError, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/query-core' +import type { + DefinedUseInfiniteQueryResult, + UseInfiniteQueryOptions, + UseInfiniteQueryResult, +} from './types' +import type { + DefinedInitialDataInfiniteOptions, + UndefinedInitialDataInfiniteOptions, +} from './infiniteQueryOptions' + +// Overloads match @tanstack/react-query's useInfiniteQuery.ts. +export function useInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: DefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): DefinedUseInfiniteQueryResult + +export function useInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: UndefinedInitialDataInfiniteOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): UseInfiniteQueryResult + +export function useInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: UseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): UseInfiniteQueryResult + +export function useInfiniteQuery(options: any, ...rest: Array): any { + const [user, slot] = splitSlot(rest) + return useBaseQuery(options, InfiniteQueryObserver, user[0], slot) +} diff --git a/packages/octane-query/src/useIsFetching.ts b/packages/octane-query/src/useIsFetching.ts new file mode 100644 index 00000000000..df699564245 --- /dev/null +++ b/packages/octane-query/src/useIsFetching.ts @@ -0,0 +1,29 @@ +import { notifyManager } from '@tanstack/query-core' +import { useCallback, useSyncExternalStore } from 'octane' +import { resolveClient } from './context' +import { splitSlot, subSlot } from './internal' +import type { QueryClient, QueryFilters } from '@tanstack/query-core' + +// Signature matches @tanstack/react-query's useIsFetching.ts. +export function useIsFetching( + filters?: QueryFilters, + queryClient?: QueryClient, +): number + +export function useIsFetching(...args: Array): number { + const [user, slot] = splitSlot(args) + const filters = user[0] + const client = resolveClient(user[1]) + const queryCache = client.getQueryCache() + return useSyncExternalStore( + useCallback( + (onStoreChange: () => void) => + queryCache.subscribe(notifyManager.batchCalls(onStoreChange)), + [queryCache], + subSlot(slot, 'if:cb'), + ), + () => client.isFetching(filters), + () => client.isFetching(filters), + subSlot(slot, 'if:uses'), + ) +} diff --git a/packages/octane-query/src/useMutation.ts b/packages/octane-query/src/useMutation.ts new file mode 100644 index 00000000000..7a9c8d6c1d3 --- /dev/null +++ b/packages/octane-query/src/useMutation.ts @@ -0,0 +1,78 @@ +// `useMutation` — reimplemented on octane's hooks, mirroring +// @tanstack/react-query: a MutationObserver subscribed via useSyncExternalStore, +// with a stable `mutate` callback. The single compiler-injected slot is split +// into distinct sub-slots for each internal base hook. +import { useCallback, useEffect, useState, useSyncExternalStore } from 'octane' +import { + MutationObserver, + noop, + notifyManager, + shouldThrowError, +} from '@tanstack/query-core' +import { resolveClient } from './context' +import { splitSlot, subSlot as subSlotBase } from './internal' +import type { DefaultError, QueryClient } from '@tanstack/query-core' +import type { UseMutationOptions, UseMutationResult } from './types' + +// Namespaced per-hook (':om:') on top of the shared helper — the minted symbols +// are byte-identical to the previous private copy, so slot identity is stable. +const subSlot = (slot: symbol | undefined, tag: string) => + subSlotBase(slot, 'om:' + tag) + +// Signature matches @tanstack/react-query's useMutation.ts. +export function useMutation< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: UseMutationOptions, + queryClient?: QueryClient, +): UseMutationResult + +export function useMutation(options: any, ...rest: Array): any { + const [user, slot] = splitSlot(rest) + const client = resolveClient(user[0]) + + const [observer] = useState( + () => new MutationObserver(client, options), + subSlot(slot, 'obs'), + ) + + useEffect( + () => { + observer.setOptions(options) + }, + [observer, options], + subSlot(slot, 'eff'), + ) + + const result = useSyncExternalStore( + useCallback( + (onStoreChange: () => void) => + observer.subscribe(notifyManager.batchCalls(onStoreChange)), + [observer], + subSlot(slot, 'cb'), + ), + () => observer.getCurrentResult(), + () => observer.getCurrentResult(), + subSlot(slot, 'uses'), + ) + + const mutate = useCallback( + (variables: any, mutateOptions: any) => { + observer.mutate(variables, mutateOptions).catch(noop) + }, + [observer], + subSlot(slot, 'mut'), + ) + + if ( + result.error && + shouldThrowError(observer.options.throwOnError, [result.error]) + ) { + throw result.error + } + + return { ...result, mutate, mutateAsync: result.mutate } +} diff --git a/packages/octane-query/src/useMutationState.ts b/packages/octane-query/src/useMutationState.ts new file mode 100644 index 00000000000..4f8e7a0157f --- /dev/null +++ b/packages/octane-query/src/useMutationState.ts @@ -0,0 +1,88 @@ +import { notifyManager, replaceEqualDeep } from '@tanstack/query-core' +import { useCallback, useEffect, useRef, useSyncExternalStore } from 'octane' +import { resolveClient } from './context' +import { splitSlot, subSlot } from './internal' +import type { + Mutation, + MutationFilters, + MutationState, + QueryClient, +} from '@tanstack/query-core' + +// Per upstream useMutationState.ts (kept module-private there too). +type MutationStateOptions = { + filters?: MutationFilters + select?: (mutation: Mutation) => TResult +} + +function getResult(mutationCache: any, options: any): Array { + return mutationCache + .findAll(options.filters) + .map((mutation: any) => + options.select ? options.select(mutation) : mutation.state, + ) +} + +// Signatures match @tanstack/react-query's useMutationState.ts. +export function useMutationState( + options?: MutationStateOptions, + queryClient?: QueryClient, +): Array + +export function useMutationState(...args: Array): Array { + const [user, slot] = splitSlot(args) + const options = user[0] ?? {} + const mutationCache = resolveClient(user[1]).getMutationCache() + + const optionsRef = useRef(options, subSlot(slot, 'ms:opt')) + const result = useRef | null>(null, subSlot(slot, 'ms:res')) + if (result.current === null) { + result.current = getResult(mutationCache, options) + } + useEffect( + () => { + optionsRef.current = options + }, + undefined, + subSlot(slot, 'ms:eff'), + ) + + return useSyncExternalStore( + useCallback( + (onStoreChange: () => void) => + mutationCache.subscribe(() => { + const next = replaceEqualDeep( + result.current, + getResult(mutationCache, optionsRef.current), + ) + if (result.current !== next) { + result.current = next + notifyManager.schedule(onStoreChange) + } + }), + [mutationCache], + subSlot(slot, 'ms:cb'), + ), + () => result.current as Array, + () => result.current as Array, + subSlot(slot, 'ms:uses'), + ) +} + +// Signature matches @tanstack/react-query's useMutationState.ts. +export function useIsMutating( + filters?: MutationFilters, + queryClient?: QueryClient, +): number + +export function useIsMutating(...args: Array): number { + const [user, slot] = splitSlot(args) + const filters = user[0] + const client = resolveClient(user[1]) + // Forward this hook's slot to useMutationState (it owns the base hooks). + return (useMutationState as (...a: Array) => Array)( + { filters: { ...filters, status: 'pending' } }, + client, + slot, + ).length +} diff --git a/packages/octane-query/src/usePrefetch.ts b/packages/octane-query/src/usePrefetch.ts new file mode 100644 index 00000000000..3d32ffd94be --- /dev/null +++ b/packages/octane-query/src/usePrefetch.ts @@ -0,0 +1,57 @@ +import { resolveClient } from './context' +import { splitSlot } from './internal' +import type { + DefaultError, + FetchInfiniteQueryOptions, + QueryClient, + QueryKey, +} from '@tanstack/query-core' +import type { UsePrefetchQueryOptions } from './types' + +// Signatures match @tanstack/react-query's usePrefetchQuery.tsx / +// usePrefetchInfiniteQuery.tsx. +export function usePrefetchQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UsePrefetchQueryOptions, + queryClient?: QueryClient, +): void + +export function usePrefetchQuery(options: any, ...rest: Array): void { + const [user] = splitSlot(rest) + const client = resolveClient(user[0]) + if (!client.getQueryState(options.queryKey)) { + client.prefetchQuery(options) + } +} + +export function usePrefetchInfiniteQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: FetchInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): void + +export function usePrefetchInfiniteQuery( + options: any, + ...rest: Array +): void { + const [user] = splitSlot(rest) + const client = resolveClient(user[0]) + if (!client.getQueryState(options.queryKey)) { + client.prefetchInfiniteQuery(options) + } +} diff --git a/packages/octane-query/src/useQueries.ts b/packages/octane-query/src/useQueries.ts new file mode 100644 index 00000000000..06494567f45 --- /dev/null +++ b/packages/octane-query/src/useQueries.ts @@ -0,0 +1,167 @@ +import { + QueriesObserver, + QueryObserver, + noop, + notifyManager, +} from '@tanstack/query-core' +import { + useCallback, + useEffect, + useMemo, + useState, + useSyncExternalStore, +} from 'octane' +import { resolveClient } from './context' +import { useIsRestoring } from './isRestoring' +import { useQueryErrorResetBoundary } from './errorResetBoundary' +import { + ensurePreventErrorBoundaryRetry, + ensureSuspenseTimers, + fetchOptimistic, + getHasError, + shouldSuspend, + splitSlot, + subSlot, + useSuspensePromise, +} from './internal' +import type { QueryClient } from '@tanstack/query-core' +import type { + GetUseQueryOptionsForUseQueries, + QueriesOptions, + QueriesResults, +} from './queries-types' + +// Signature matches @tanstack/react-query's useQueries.ts — per-entry tuple +// inference via QueriesOptions/QueriesResults, with `combine` re-typing the +// aggregate result. The untyped implementation signature also accepts the +// compiler-injected trailing slot symbol. +export function useQueries< + T extends Array, + TCombinedResult = QueriesResults, +>( + options: { + queries: + | readonly [...QueriesOptions] + | readonly [...{ [K in keyof T]: GetUseQueryOptionsForUseQueries }] + combine?: (result: QueriesResults) => TCombinedResult + subscribed?: boolean + }, + queryClient?: QueryClient, +): TCombinedResult + +export function useQueries(options: any, ...rest: Array): any { + const [user, slot] = splitSlot(rest) + const client = resolveClient(user[0]) + const isRestoring = useIsRestoring() + const errorResetBoundary = useQueryErrorResetBoundary() + const { queries, ...restOptions } = options + const qs = (tag: string) => subSlot(slot, 'qs:' + tag) + + const defaultedQueries = useMemo( + () => + queries.map((opts: any) => { + const defaulted = client.defaultQueryOptions(opts) + defaulted._optimisticResults = isRestoring + ? 'isRestoring' + : 'optimistic' + return defaulted + }), + [queries, client, isRestoring], + qs('memo'), + ) + defaultedQueries.forEach((queryOptions: any) => { + ensureSuspenseTimers(queryOptions) + ensurePreventErrorBoundaryRetry( + queryOptions, + errorResetBoundary, + client.getQueryCache().get(queryOptions.queryHash), + ) + }) + useEffect( + () => { + errorResetBoundary.clearReset() + }, + [errorResetBoundary], + qs('clr'), + ) + + const [observer] = useState( + () => new QueriesObserver(client, defaultedQueries, restOptions), + qs('obs'), + ) + + const [optimisticResult, getCombinedResult, trackResult] = + observer.getOptimisticResult(defaultedQueries, restOptions.combine) + + const shouldSubscribe = !isRestoring && restOptions.subscribed !== false + useSyncExternalStore( + useCallback( + (onStoreChange: () => void) => + shouldSubscribe + ? observer.subscribe(notifyManager.batchCalls(onStoreChange)) + : noop, + [observer, shouldSubscribe], + qs('cb'), + ), + () => observer.getCurrentResult(), + () => observer.getCurrentResult(), + qs('uses'), + ) + + useEffect( + () => { + observer.setQueries(defaultedQueries, restOptions) + }, + [defaultedQueries, restOptions, observer], + qs('eff'), + ) + + // If any query should suspend, await their optimistic fetches as one retained + // suspense promise. Retaining it gives a sequential useQueries call the same + // stable replay position as useBaseQuery above. + const suspenseIndexes: Array = [] + for (let index = 0; index < optimisticResult.length; index++) { + if (shouldSuspend(defaultedQueries[index], optimisticResult[index])) { + suspenseIndexes.push(index) + } + } + useSuspensePromise( + suspenseIndexes.length > 0, + JSON.stringify( + suspenseIndexes.map((index) => defaultedQueries[index].queryHash), + ), + () => + Promise.all( + suspenseIndexes.map((index) => { + const opts = defaultedQueries[index] + // fetchOptimistic clears the reset boundary on error (upstream + // suspense.ts) so a reset→retry→fail-again re-throws to the boundary. + return fetchOptimistic( + opts, + new QueryObserver(client, opts), + errorResetBoundary, + ) + }), + ), + qs('sus'), + ) + + const firstError = optimisticResult.find((result: any, index: number) => { + const query = defaultedQueries[index] + return ( + query && + getHasError({ + result, + errorResetBoundary, + throwOnError: query.throwOnError, + query: client.getQueryCache().get(query.queryHash), + suspense: query.suspense, + }) + ) + }) + if (firstError?.error) { + throw firstError.error + } + + return getCombinedResult(trackResult()) +} diff --git a/packages/octane-query/src/useQuery.ts b/packages/octane-query/src/useQuery.ts new file mode 100644 index 00000000000..96636f55b12 --- /dev/null +++ b/packages/octane-query/src/useQuery.ts @@ -0,0 +1,54 @@ +import { QueryObserver } from '@tanstack/query-core' +import { useBaseQuery } from './useBaseQuery' +import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core' +import type { + DefinedUseQueryResult, + UseQueryOptions, + UseQueryResult, +} from './types' +import type { + DefinedInitialDataOptions, + UndefinedInitialDataOptions, +} from './queryOptions' + +// Overloads match @tanstack/react-query's useQuery.ts: `initialData` narrows the +// result to DefinedUseQueryResult. The untyped implementation signature also +// accepts the compiler-injected trailing slot symbol (never visible to users). +export function useQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: DefinedInitialDataOptions, + queryClient?: QueryClient, +): DefinedUseQueryResult, TError> + +export function useQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UndefinedInitialDataOptions, + queryClient?: QueryClient, +): UseQueryResult, TError> + +export function useQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UseQueryOptions, + queryClient?: QueryClient, +): UseQueryResult, TError> + +export function useQuery(options: any, ...rest: Array): any { + // `[queryClient?, slot?]` — the slot (symbol) is the compiler-injected trailing + // arg; an explicit client is the first non-symbol arg. + const tail = rest[rest.length - 1] + const slot = typeof tail === 'symbol' ? tail : undefined + const queryClient = typeof rest[0] !== 'symbol' ? rest[0] : undefined + return useBaseQuery(options, QueryObserver, queryClient, slot) +} diff --git a/packages/octane-query/src/useSuspenseQueries.ts b/packages/octane-query/src/useSuspenseQueries.ts new file mode 100644 index 00000000000..ab735557518 --- /dev/null +++ b/packages/octane-query/src/useSuspenseQueries.ts @@ -0,0 +1,58 @@ +import { skipToken } from '@tanstack/query-core' +import { defaultThrowOnError } from './internal' +import { useQueries } from './useQueries' +import type { QueryClient } from '@tanstack/query-core' +import type { + GetUseSuspenseQueryOptions, + SuspenseQueriesOptions, + SuspenseQueriesResults, +} from './suspense-queries-types' + +// Signature matches @tanstack/react-query's useSuspenseQueries.ts. +export function useSuspenseQueries< + T extends Array, + TCombinedResult = SuspenseQueriesResults, +>( + options: { + queries: + | readonly [...SuspenseQueriesOptions] + | readonly [...{ [K in keyof T]: GetUseSuspenseQueryOptions }] + combine?: (result: SuspenseQueriesResults) => TCombinedResult + }, + queryClient?: QueryClient, +): TCombinedResult + +export function useSuspenseQueries< + T extends Array, + TCombinedResult = SuspenseQueriesResults, +>( + options: { + queries: readonly [...SuspenseQueriesOptions] + combine?: (result: SuspenseQueriesResults) => TCombinedResult + }, + queryClient?: QueryClient, +): TCombinedResult + +export function useSuspenseQueries(options: any, ...rest: Array): any { + return useQueries( + { + ...options, + queries: options.queries.map((query: any) => { + if ( + process.env.NODE_ENV !== 'production' && + query.queryFn === skipToken + ) { + console.error('skipToken is not allowed for useSuspenseQueries') + } + return { + ...query, + suspense: true, + throwOnError: defaultThrowOnError, + enabled: true, + placeholderData: undefined, + } + }), + }, + ...rest, + ) +} diff --git a/packages/octane-query/src/useSuspenseQuery.ts b/packages/octane-query/src/useSuspenseQuery.ts new file mode 100644 index 00000000000..83c593e08c0 --- /dev/null +++ b/packages/octane-query/src/useSuspenseQuery.ts @@ -0,0 +1,94 @@ +import { + InfiniteQueryObserver, + QueryObserver, + skipToken, +} from '@tanstack/query-core' +import { useBaseQuery } from './useBaseQuery' +import { defaultThrowOnError, splitSlot } from './internal' +import type { + DefaultError, + InfiniteData, + QueryClient, + QueryKey, +} from '@tanstack/query-core' +import type { + UseSuspenseInfiniteQueryOptions, + UseSuspenseInfiniteQueryResult, + UseSuspenseQueryOptions, + UseSuspenseQueryResult, +} from './types' + +// Signatures match @tanstack/react-query's useSuspenseQuery.ts / +// useSuspenseInfiniteQuery.ts (data is always defined; no placeholderData / +// enabled / throwOnError options). The untyped implementation signature also +// accepts the compiler-injected trailing slot symbol. +export function useSuspenseQuery< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UseSuspenseQueryOptions, + queryClient?: QueryClient, +): UseSuspenseQueryResult + +export function useSuspenseQuery(options: any, ...rest: Array): any { + const [user, slot] = splitSlot(rest) + if (process.env.NODE_ENV !== 'production') { + if (options.queryFn === skipToken) { + console.error('skipToken is not allowed for useSuspenseQuery') + } + } + return useBaseQuery( + { + ...options, + enabled: true, + suspense: true, + throwOnError: defaultThrowOnError, + placeholderData: undefined, + }, + QueryObserver, + user[0], + slot, + ) +} + +export function useSuspenseInfiniteQuery< + TQueryFnData, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: UseSuspenseInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, + queryClient?: QueryClient, +): UseSuspenseInfiniteQueryResult + +export function useSuspenseInfiniteQuery( + options: any, + ...rest: Array +): any { + const [user, slot] = splitSlot(rest) + if (process.env.NODE_ENV !== 'production') { + if (options.queryFn === skipToken) { + console.error('skipToken is not allowed for useSuspenseInfiniteQuery') + } + } + return useBaseQuery( + { + ...options, + enabled: true, + suspense: true, + throwOnError: defaultThrowOnError, + }, + InfiniteQueryObserver, + user[0], + slot, + ) +} diff --git a/packages/octane-query/tests/_fixtures/app.tsrx b/packages/octane-query/tests/_fixtures/app.tsrx new file mode 100644 index 00000000000..8dfdd9bd10d --- /dev/null +++ b/packages/octane-query/tests/_fixtures/app.tsrx @@ -0,0 +1,56 @@ +import { + QueryClientProvider, + useQuery, + useMutation, + useQueryClient, +} from '@tanstack/octane-query'; + +// Reads a query through the CONTEXT client (no explicit client arg). +export function Reader(props: { queryFn: () => Promise }) @{ + const q = useQuery({ queryKey: ['k'], queryFn: props.queryFn, retry: false }); +
{(q.isPending + ? 'pending' + : q.isError + ? 'error:' + (q.error as Error).message + : 'data:' + q.data) as string}
+} + +export function App(props: { client: unknown; queryFn: () => Promise }) @{ + + + +} + +// useQueryClient resolves the provided client. +export function ClientProbe(props: { onClient: (c: unknown) => void }) @{ + const client = useQueryClient(); + props.onClient(client); + ok +} + +export function ProbeApp(props: { client: unknown; onClient: (c: unknown) => void }) @{ + + + +} + +// Mutation lifecycle. +export function Mutator(props: { mutationFn: (v: string) => Promise }) @{ + const m = useMutation({ mutationFn: props.mutationFn }); +
+ {(m.isIdle + ? 'idle' + : m.isPending + ? 'pending' + : m.isError + ? 'error' + : 'data:' + m.data) as string} + +
+} + +export function MutApp(props: { client: unknown; mutationFn: (v: string) => Promise }) @{ + + + +} diff --git a/packages/octane-query/tests/_fixtures/async-diff.tsrx b/packages/octane-query/tests/_fixtures/async-diff.tsrx new file mode 100644 index 00000000000..5b58f446c65 --- /dev/null +++ b/packages/octane-query/tests/_fixtures/async-diff.tsrx @@ -0,0 +1,50 @@ +import { useState } from 'octane'; +import { + QueryClient, + QueryClientProvider, + useMutation, + useQuery, +} from '@tanstack/octane-query'; + +function delayed(value: T, ms: number): Promise { + return new Promise((resolve) => setTimeout(() => resolve(value), ms)); +} + +function AsyncReader() @{ + const q = useQuery({ + queryKey: ['a'], + queryFn: () => delayed('fetched', 10), + retry: false, + }); +
{'data=' + q.data + ' status=' + q.status + ' pending=' + q.isPending + ' fetching=' + + q.isFetching + + ' success=' + + q.isSuccess as string}
+} + +export function AsyncApp() @{ + const [client] = useState( + () => new QueryClient({ defaultOptions: { queries: { retry: false } } }), + ); + + + +} + +function MutationReader() @{ + const mutation = useMutation({ + mutationFn: (value: string) => delayed('done:' + value, 10), + }); +
+ +
{'status=' + mutation.status + ' data=' + mutation.data + ' vars=' + + mutation.variables as string}
+
+} + +export function MutationApp() @{ + const [client] = useState(() => new QueryClient()); + + + +} diff --git a/packages/octane-query/tests/_fixtures/boundaries.tsrx b/packages/octane-query/tests/_fixtures/boundaries.tsrx new file mode 100644 index 00000000000..f0869369bb8 --- /dev/null +++ b/packages/octane-query/tests/_fixtures/boundaries.tsrx @@ -0,0 +1,53 @@ +import { ErrorBoundary } from 'octane'; +import { + QueryClientProvider, + QueryErrorResetBoundary, + useQueryErrorResetBoundary, + IsRestoringProvider, + useQuery, +} from '@tanstack/octane-query'; + +// --- IsRestoring: a restoring subtree reads cache but doesn't subscribe/fetch --- +function RestoreReader(props: { queryFn: () => Promise }) @{ + const q = useQuery({ queryKey: ['r'], queryFn: props.queryFn, retry: false }); +
{(q.isPending ? 'pending' : 'data:' + q.data) as string}
+} + +export function RestoringApp(props: { + client: unknown; + restoring: boolean; + queryFn: () => Promise; +}) @{ + + + + + +} + +// --- QueryErrorResetBoundary: reset() lets a throwOnError query retry --- +function ErrReader(props: { queryFn: () => Promise }) @{ + const q = useQuery({ queryKey: ['e'], queryFn: props.queryFn, retry: false, throwOnError: true }); + {'data:' + q.data as string} +} + +function Boundary(props: { queryFn: () => Promise }) @{ + const errReset = useQueryErrorResetBoundary(); + const onRetry = (reset: () => void) => () => { + errReset.reset(); + reset(); + }; + } + > + + +} + +export function ResetApp(props: { client: unknown; queryFn: () => Promise }) @{ + + + + + +} diff --git a/packages/octane-query/tests/_fixtures/cached-diff.tsrx b/packages/octane-query/tests/_fixtures/cached-diff.tsrx new file mode 100644 index 00000000000..35023bde249 --- /dev/null +++ b/packages/octane-query/tests/_fixtures/cached-diff.tsrx @@ -0,0 +1,26 @@ +import { + QueryClient, + QueryClientProvider, + useQuery, +} from '@tanstack/octane-query'; + +const client = new QueryClient(); + +export function Cached() @{ + const q = useQuery({ + queryKey: ['c'], + queryFn: () => Promise.resolve('fetched'), + initialData: 'seed', + staleTime: Infinity, + }); +
{'data=' + q.data + ' status=' + q.status + ' pending=' + q.isPending + ' fetching=' + + q.isFetching + + ' success=' + + q.isSuccess as string}
+} + +export function CachedApp() @{ + + + +} diff --git a/packages/octane-query/tests/_fixtures/extra.tsrx b/packages/octane-query/tests/_fixtures/extra.tsrx new file mode 100644 index 00000000000..c46c6466e0f --- /dev/null +++ b/packages/octane-query/tests/_fixtures/extra.tsrx @@ -0,0 +1,19 @@ +import { QueryClientProvider, useQuery } from '@tanstack/octane-query'; + +// queryKey derived from a prop — changing the prop swaps the query (exercises +// observer.setOptions → query change → refetch → re-render). +export function KeyedReader(props: { k: number }) @{ + const q = useQuery({ + queryKey: ['keyed', props.k], + queryFn: () => Promise.resolve('v' + props.k), + retry: false, + staleTime: Infinity, + }); +
{(q.isPending ? 'pending' : 'data:' + q.data) as string}
+} + +export function KeyedApp(props: { client: unknown; k: number }) @{ + + + +} diff --git a/packages/octane-query/tests/_fixtures/followups.tsrx b/packages/octane-query/tests/_fixtures/followups.tsrx new file mode 100644 index 00000000000..d28c09c8acf --- /dev/null +++ b/packages/octane-query/tests/_fixtures/followups.tsrx @@ -0,0 +1,118 @@ +import { Suspense } from 'octane'; +import { + QueryClientProvider, + HydrationBoundary, + useInfiniteQuery, + useSuspenseQuery, + useQueries, + useIsFetching, + useIsMutating, + usePrefetchQuery, + useMutation, + useQuery, +} from '@tanstack/octane-query'; + +export function Infinite(props: { client: unknown }) @{ + const q = useInfiniteQuery( + { + queryKey: ['inf'], + queryFn: (ctx: { pageParam: number }) => Promise.resolve('p' + ctx.pageParam), + initialPageParam: 0, + getNextPageParam: () => undefined, + retry: false, + }, + props.client, + ); +
{(q.isPending ? 'pending' : 'pages:' + q.data.pages.join(',')) as string}
+} + +export function Queries(props: { client: unknown }) @{ + const results = useQueries( + { + queries: [ + { queryKey: ['qa'], queryFn: () => Promise.resolve('A'), retry: false }, + { queryKey: ['qb'], queryFn: () => Promise.resolve('B'), retry: false }, + ], + }, + props.client, + ); +
{results.map((r: any) => (r.isPending ? '?' : r.data)).join(',') as string}
+} + +export function Fetching(props: { client: unknown; queryFn: () => Promise }) @{ + const reader = useQuery({ queryKey: ['fz'], queryFn: props.queryFn, retry: false }, props.client); + const fetching = useIsFetching(undefined, props.client); +
{'fetching:' + fetching + ' status:' + reader.status as string}
+} + +export function Mutating(props: { client: unknown; mutationFn: (v: string) => Promise }) @{ + const m = useMutation({ mutationKey: ['mz'], mutationFn: props.mutationFn }, props.client); + const pending = useIsMutating(undefined, props.client); +
+ {'pending:' + pending as string} + +
+} + +export function Prefetch(props: { client: unknown }) @{ + usePrefetchQuery( + { queryKey: ['pf'], queryFn: () => Promise.resolve('PF'), retry: false }, + props.client, + ); + ok +} + +function SuspenseReader(props: { queryFn: () => Promise }) @{ + const q = useSuspenseQuery({ queryKey: ['susq'], queryFn: props.queryFn, retry: false }); + {'data:' + q.data as string} +} + +function SequentialSuspenseReader(props: { + queryFnA: () => Promise; + queryFnB: () => Promise; +}) @{ + const a = useSuspenseQuery({ queryKey: ['susq-a'], queryFn: props.queryFnA, retry: false }); + const b = useSuspenseQuery({ queryKey: ['susq-b'], queryFn: props.queryFnB, retry: false }); + {a.data + '/' + b.data as string} +} + +export function SuspenseQApp(props: { client: unknown; queryFn: () => Promise }) @{ + + loading}> + + + +} + +export function SequentialSuspenseQApp(props: { + client: unknown; + queryFnA: () => Promise; + queryFnB: () => Promise; +}) @{ + + @try { + + } @pending { + loading + } @catch (error) { + {'error:' + (error as Error).message as string} + } + +} + +function HydratedReader() @{ + const q = useQuery({ + queryKey: ['h'], + queryFn: () => Promise.resolve('refetched'), + staleTime: Infinity, + }); + {'data:' + q.data as string} +} + +export function HydrationApp(props: { client: unknown; state: unknown }) @{ + + + + + +} diff --git a/packages/octane-query/tests/_fixtures/parity.tsrx b/packages/octane-query/tests/_fixtures/parity.tsrx new file mode 100644 index 00000000000..4186cc6d33b --- /dev/null +++ b/packages/octane-query/tests/_fixtures/parity.tsrx @@ -0,0 +1,260 @@ +import { ErrorBoundary, Suspense, useState } from 'octane'; +import { + QueryClientProvider, + QueryErrorResetBoundary, + useQueryErrorResetBoundary, + IsRestoringProvider, + HydrationBoundary, + useQuery, + useQueries, + useInfiniteQuery, + useSuspenseQuery, + useSuspenseInfiniteQuery, + usePrefetchInfiniteQuery, + useMutation, + useMutationState, + skipToken, +} from '@tanstack/octane-query'; + +// --- Suspense error retry loop (clearReset): a suspense query that ALWAYS +// fails. After reset()+retry the refetch fails again — the boundary must show +// the error again (not fall through and render undefined data). --- +function AlwaysFailReader(props: { queryFn: () => Promise }) @{ + const q = useSuspenseQuery({ queryKey: ['fail'], queryFn: props.queryFn, retry: false }); + {'data:' + q.data as string} +} + +function SuspenseRetryBoundary(props: { queryFn: () => Promise }) @{ + const errReset = useQueryErrorResetBoundary(); + const onRetry = (reset: () => void) => () => { + errReset.reset(); + reset(); + }; +
+ {(e as Error).message as string} + +
} + > + loading}> + + +
+} + +export function SuspenseRetryApp(props: { client: unknown; queryFn: () => Promise }) @{ + + + + + +} + +// --- QueryErrorResetBoundary render-prop children --- +export const renderPropValues: unknown[] = []; + +export function RenderPropResetApp(props: { client: unknown }) @{ + + + {(value) => { + renderPropValues.push(value); + return {'reset:' + typeof value.reset}; + }} + + +} + +// --- HydrationBoundary --- +function HydratedReader(props: { queryKey: unknown[] }) @{ + const q = useQuery({ queryKey: props.queryKey, queryFn: skipToken }); + {String(q.data ?? 'none') as string} +} + +export function HydrationApp(props: { client: unknown; state: unknown; queryKey: unknown[] }) @{ + + + + + +} + +// --- experimental_prefetchInRender: expose result.promise --- +export const promiseCapture: { promise: Promise | null } = { promise: null }; + +function PrefetchInRenderReader(props: { queryFn: () => Promise }) @{ + const q = useQuery({ + queryKey: ['pir'], + queryFn: props.queryFn, + experimental_prefetchInRender: true, + retry: false, + }); + promiseCapture.promise = q.promise; + {(q.isPending ? 'pending' : 'data:' + q.data) as string} +} + +export function PrefetchInRenderApp(props: { client: unknown; queryFn: () => Promise }) @{ + + + +} + +// --- useQueries combine --- +function CombineReader() @{ + const combined = useQueries({ + queries: [ + { queryKey: ['c', 1], queryFn: () => Promise.resolve(1) }, + { queryKey: ['c', 2], queryFn: () => Promise.resolve(2) }, + ], + combine: (results) => ({ + sum: results.reduce((acc: number, r: any) => acc + (r.data ?? 0), 0), + allDone: results.every((r: any) => r.isSuccess), + }), + }); + {combined.sum + ':' + combined.allDone} +} + +export function CombineApp(props: { client: unknown }) @{ + + + +} + +// --- skipToken: never fetches --- +export const skipTokenCalls = { count: 0 }; + +function SkipTokenReader() @{ + const q = useQuery({ queryKey: ['skip'], queryFn: skipToken }); + {q.status + ':' + q.fetchStatus} +} + +export function SkipTokenApp(props: { client: unknown }) @{ + + + +} + +// --- useInfiniteQuery: fetchNextPage --- +function InfiniteReader(props: { pageFn: (page: number) => Promise }) @{ + const q = useInfiniteQuery({ + queryKey: ['inf'], + queryFn: ({ pageParam }) => props.pageFn(pageParam as number), + initialPageParam: 0, + getNextPageParam: (_last: unknown, all: unknown[]) => (all.length < 3 ? all.length : undefined), + }); +
+ {(q.data ? q.data.pages.join(',') : 'none') as string} + {String(q.hasNextPage)} + +
+} + +export function InfiniteApp(props: { + client: unknown; + pageFn: (page: number) => Promise; +}) @{ + + + +} + +// --- useSuspenseInfiniteQuery --- +function SuspenseInfiniteReader(props: { pageFn: (page: number) => Promise }) @{ + const q = useSuspenseInfiniteQuery({ + queryKey: ['sinf'], + queryFn: ({ pageParam }) => props.pageFn(pageParam as number), + initialPageParam: 0, + getNextPageParam: () => undefined, + }); + {q.data.pages.join(',') as string} +} + +export function SuspenseInfiniteApp(props: { + client: unknown; + pageFn: (page: number) => Promise; +}) @{ + + loading}> + + + +} + +// --- usePrefetchInfiniteQuery --- +function PrefetchInfinite(props: { pageFn: (page: number) => Promise }) @{ + usePrefetchInfiniteQuery({ + queryKey: ['pinf'], + queryFn: ({ pageParam }) => props.pageFn(pageParam as number), + initialPageParam: 0, + }); + {'prefetched'} +} + +export function PrefetchInfiniteApp(props: { + client: unknown; + pageFn: (page: number) => Promise; +}) @{ + + + +} + +// --- useMutationState: filters + select --- +export const mutationCounter = { n: 0 }; + +function MutationStateReader(props: { mutationFn: (v: string) => Promise }) @{ + const mutation = useMutation({ mutationKey: ['m1'], mutationFn: props.mutationFn }); + const variables = useMutationState({ + filters: { mutationKey: ['m1'] }, + select: (m) => m.state.variables, + }); +
+ + {variables.join(',') || 'none' as string} +
+} + +export function MutationStateApp(props: { + client: unknown; + mutationFn: (v: string) => Promise; +}) @{ + + + +} + +// --- Tracked props: a data-only reader must NOT re-render on isFetching flips --- +export const trackedRenders = { count: 0 }; + +function TrackedReader(props: { queryFn: () => Promise }) @{ + trackedRenders.count++; + const q = useQuery({ queryKey: ['tracked'], queryFn: props.queryFn, staleTime: 0 }); + {String(q.data ?? 'none') as string} +} + +export function TrackedApp(props: { client: unknown; queryFn: () => Promise }) @{ + + + +} + +// --- isRestoring flip: true → false starts the fetch --- +export const restoreSetter: { set: ((v: boolean) => void) | null } = { set: null }; + +function FlipRestoring(props: { queryFn: () => Promise }) @{ + const [restoring, setRestoring] = useState(true); + restoreSetter.set = setRestoring; + + + +} + +function RestoreFlipReader(props: { queryFn: () => Promise }) @{ + const q = useQuery({ queryKey: ['flip'], queryFn: props.queryFn, retry: false }); + {(q.isPending ? 'pending' : 'data:' + q.data) as string} +} + +export function FlipRestoringApp(props: { client: unknown; queryFn: () => Promise }) @{ + + + +} diff --git a/packages/octane-query/tests/_fixtures/smoke.tsrx b/packages/octane-query/tests/_fixtures/smoke.tsrx new file mode 100644 index 00000000000..168f49bc2b7 --- /dev/null +++ b/packages/octane-query/tests/_fixtures/smoke.tsrx @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/octane-query'; + +export function Todo(props) @{ + const q = useQuery({ queryKey: ['todo'], queryFn: props.queryFn, retry: false }, props.client); +
{(q.isPending + ? 'pending' + : q.isError + ? 'error:' + (q.error as Error).message + : 'data:' + q.data) as string}
+} diff --git a/packages/octane-query/tests/_fixtures/streaming-ssr.tsrx b/packages/octane-query/tests/_fixtures/streaming-ssr.tsrx new file mode 100644 index 00000000000..86181443e61 --- /dev/null +++ b/packages/octane-query/tests/_fixtures/streaming-ssr.tsrx @@ -0,0 +1,91 @@ +import { + QueryClientProvider, + useSuspenseQueries, + useSuspenseQuery, +} from '@tanstack/octane-query'; +import type { QueryClient } from '@tanstack/query-core'; + +type ReaderProps = { + a: () => Promise; + b: () => Promise; +}; + +type AppProps = ReaderProps & { + client: QueryClient; +}; + +function QueryReader(props: ReaderProps) @{ + const a = useSuspenseQuery({ + queryKey: ['streaming-ssr', 'query', 'a'], + queryFn: props.a, + retry: false, + }); + const b = useSuspenseQuery({ + queryKey: ['streaming-ssr', 'query', 'b'], + queryFn: props.b, + retry: false, + }); + {a.data + '/' + b.data as string} +} + +function QueriesReader(props: ReaderProps) @{ + const [a] = useSuspenseQueries({ + queries: [ + { + queryKey: ['streaming-ssr', 'queries', 'a'], + queryFn: props.a, + retry: false, + }, + ], + }); + const [b] = useSuspenseQueries({ + queries: [ + { + queryKey: ['streaming-ssr', 'queries', 'b'], + queryFn: props.b, + retry: false, + }, + ], + }); + {a.data + '/' + b.data as string} +} + +function useWrappedQuery( + key: 'a' | 'b', + queryFn: () => Promise, +) { + return useSuspenseQuery({ + queryKey: ['streaming-ssr', 'wrapped-query', key], + queryFn, + retry: false, + }); +} + +function WrappedQueryReader(props: ReaderProps) @{ + const a = useWrappedQuery('a', props.a); + const b = useWrappedQuery('b', props.b); + {a.data + '/' + b.data as string} +} + +export function StreamingSuspenseApp(props: AppProps & { + mode: 'query' | 'queries'; +}) @{ + const Reader = props.mode === 'query' ? QueryReader : QueriesReader; + + @try { + + } @pending { + loading + } + +} + +export function WrappedStreamingSuspenseApp(props: AppProps) @{ + + @try { + + } @pending { + loading + } + +} diff --git a/packages/octane-query/tests/_fixtures/suspense.tsrx b/packages/octane-query/tests/_fixtures/suspense.tsrx new file mode 100644 index 00000000000..bb9288335bf --- /dev/null +++ b/packages/octane-query/tests/_fixtures/suspense.tsrx @@ -0,0 +1,53 @@ +import { Suspense } from 'octane'; +import { QueryClientProvider, useQuery, useSuspenseQueries } from '@tanstack/octane-query'; + +// A suspense query: throws its in-flight promise (→ @pending) and, on error, +// throws the error (→ @catch). +export function SuspendingReader(props: { queryFn: () => Promise }) @{ + const q = useQuery({ queryKey: ['s'], queryFn: props.queryFn, suspense: true, retry: false }); + {'data:' + q.data as string} +} + +export function SuspenseApp(props: { client: unknown; queryFn: () => Promise }) @{ + + @try { + + } @pending { + loading + } @catch (e) { + {'caught:' + (e as Error).message as string} + } + +} + +// The react-query-idiomatic form: octane's component (not @try) wraps a +// suspense query. +export function SuspenseComponentApp(props: { client: unknown; queryFn: () => Promise }) @{ + + loading}> + + + +} + +function SuspenseQueriesReader(props: { a: () => Promise; b: () => Promise }) @{ + const results = useSuspenseQueries({ + queries: [ + { queryKey: ['sq', 'a'], queryFn: props.a }, + { queryKey: ['sq', 'b'], queryFn: props.b }, + ], + }); + {'data:' + results[0].data + '/' + results[1].data as string} +} + +export function SuspenseQueriesApp(props: { + client: unknown; + a: () => Promise; + b: () => Promise; +}) @{ + + loading}> + + + +} diff --git a/packages/octane-query/tests/_fixtures/transition-suspense.tsrx b/packages/octane-query/tests/_fixtures/transition-suspense.tsrx new file mode 100644 index 00000000000..ee52e7482dd --- /dev/null +++ b/packages/octane-query/tests/_fixtures/transition-suspense.tsrx @@ -0,0 +1,64 @@ +import { useState, useTransition } from 'octane'; +import { QueryClientProvider, useSuspenseQuery } from '@tanstack/octane-query'; + +// --------------------------------------------------------------------------- +// React useTransition + Suspense parity for useSuspenseQuery. +// +// A `useSuspenseQuery` whose queryKey depends on `value`. When `value` changes +// inside a transition, the query refetches under a NEW key and the reader +// re-suspends. The query observer notifies ASYNCHRONOUSLY (query-core's +// notifyManager uses setTimeout(0) — a macrotask), so by the time the re-render +// that re-suspends on the new in-flight fetch actually runs, octane's +// transition window has already closed and the re-render is URGENT. +// +// React keeps the previously committed content on screen until the new query +// resolves — it does NOT flash the Suspense fallback. This fixture lets the +// test drive that exact sequence: commit value=1, transition to value=2, +// observe the value=1 content holds, then resolves to value=2. +// +// `props.bindSetValue` captures the (transition-wrapped) value setter, and +// `props.queryFn(value)` returns a per-value promise the test controls. +// --------------------------------------------------------------------------- + +function PendingProbe() @{ + const [isPending] = useTransition(); + + {isPending ? 'pending' : 'idle'} + +} + +function Reader(props) @{ + const [value, setValue] = useState(1); + const [, start] = useTransition(); + // Expose a transition-priority value setter to the test. + props.bindSetValue((v) => start(() => setValue(v))); + // Expose an URGENT (non-transition) value setter so the test can re-render the + // reader under a new key WHILE the boundary is transition-held — modelling the + // query observer's async macrotask notify, which lands at urgent priority. + if (props.bindSetValueUrgent) props.bindSetValueUrgent((v) => setValue(v)); + const q = useSuspenseQuery({ + queryKey: ['transition-suspense', value], + queryFn: () => props.queryFn(value), + retry: false, + }); + {'data:' + q.data as string} +} + +export function TransitionSuspenseApp(props) @{ + +
+ + @try { + + } @pending { + loading + } @catch (e) { + {'caught:' + (e as Error).message as string} + } +
+
+} diff --git a/packages/octane-query/tests/_helpers.ts b/packages/octane-query/tests/_helpers.ts new file mode 100644 index 00000000000..d181d16b7aa --- /dev/null +++ b/packages/octane-query/tests/_helpers.ts @@ -0,0 +1,94 @@ +/** + * Test helpers — thin wrapper around createRoot + flushSync so tests can + * mount a component, inspect the DOM, fire events, and unmount cleanly. + */ +import { + createRoot, + delegateEvents, + drainPassiveEffects, + flushSync, +} from 'octane' +import type { ComponentBody, Root } from 'octane' + +// Delegated events used by test fixtures. Setup-once. +delegateEvents(['click', 'input', 'change', 'keydown', 'submit']) + +interface MountResult { + container: HTMLElement + root: Root + html: () => string + unmount: () => void + click: (selector: string) => void + find: (selector: string) => Element + findAll: (selector: string) => Array + /** Re-render with new props (drains queued renders synchronously). */ + update: (body: ComponentBody, props?: TProps) => void +} + +export function mount( + body: ComponentBody, + props?: TProps, +): MountResult { + const container = document.createElement('div') + document.body.appendChild(container) + const root = createRoot(container) + try { + root.render(body, props) + flushSync(() => {}) + } catch (err) { + // A throwing initial render can't be unmounted by the caller because mount + // never returns. Release the root's delegation/ownership registrations as + // well as its container before preserving the original render error. + try { + root.unmount() + } finally { + container.remove() + } + throw err + } + return { + container, + root, + html() { + return container.innerHTML + }, + unmount() { + root.unmount() + container.remove() + }, + click(selector) { + const el = container.querySelector(selector) + if (!el) throw new Error(`no element matching ${selector}`) + flushSync(() => { + // HTMLElement has `.click()`. SVGElement / MathMLElement do NOT + // (they're not in the HTMLElement prototype chain), so we dispatch + // a bubbling click event explicitly — matches the real browser path + // the runtime listens on at the delegation root. + if (typeof (el as HTMLElement).click === 'function') { + ;(el as HTMLElement).click() + } else { + el.dispatchEvent( + new MouseEvent('click', { bubbles: true, cancelable: true }), + ) + } + }) + }, + find(selector) { + const el = container.querySelector(selector) + if (!el) throw new Error(`no element matching ${selector}`) + return el + }, + findAll(selector) { + return Array.from(container.querySelectorAll(selector)) + }, + update(nextBody, nextProps) { + flushSync(() => root.render(nextBody, nextProps)) + }, + } +} + +/** Older name kept for tests that read more naturally with it. */ +export function nextPaint(): Promise { + drainPassiveEffects() + return Promise.resolve() +} diff --git a/packages/octane-query/tests/conformance/boundaries.test.ts b/packages/octane-query/tests/conformance/boundaries.test.ts new file mode 100644 index 00000000000..fc1981309f7 --- /dev/null +++ b/packages/octane-query/tests/conformance/boundaries.test.ts @@ -0,0 +1,70 @@ +/** + * IsRestoringProvider / useIsRestoring and QueryErrorResetBoundary / + * useQueryErrorResetBoundary — the persistence-restore gate and the + * error-boundary reset coordinator. + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/octane-query' +import { mount, nextPaint } from '../_helpers' +import { ResetApp, RestoringApp } from '../_fixtures/boundaries.tsrx' + +let client: QueryClient +beforeEach(() => { + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + client.mount() +}) + +async function flush() { + for (let i = 0; i < 6; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +describe('IsRestoring', () => { + it('does NOT subscribe/fetch while restoring', async () => { + let called = 0 + const queryFn = () => { + called++ + return Promise.resolve('x') + } + const r = mount(RestoringApp, { client, restoring: true, queryFn }) + await flush() + expect(called).toBe(0) + expect(r.find('#status').textContent).toBe('pending') + r.unmount() + }) + + it('fetches normally when not restoring', async () => { + let called = 0 + const queryFn = () => { + called++ + return Promise.resolve('y') + } + const r = mount(RestoringApp, { client, restoring: false, queryFn }) + await flush() + expect(called).toBeGreaterThan(0) + expect(r.find('#status').textContent).toBe('data:y') + r.unmount() + }) +}) + +describe('QueryErrorResetBoundary', () => { + it('reset() lets the query retry instead of re-throwing', async () => { + let calls = 0 + const queryFn = () => { + calls++ + return calls === 1 + ? Promise.reject(new Error('boom')) + : Promise.resolve('recovered') + } + const r = mount(ResetApp, { client, queryFn }) + await flush() + // First fetch errored → @catch/ fallback (the retry button). + expect(r.find('#retry').textContent).toBe('retry') + r.click('#retry') + await flush() + expect(r.find('#data').textContent).toBe('data:recovered') + r.unmount() + }) +}) diff --git a/packages/octane-query/tests/conformance/exports.test.ts b/packages/octane-query/tests/conformance/exports.test.ts new file mode 100644 index 00000000000..d767326367b --- /dev/null +++ b/packages/octane-query/tests/conformance/exports.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it } from 'vitest' +import * as query from '@tanstack/octane-query' +import * as reactQuery from '@tanstack/react-query' + +describe('@tanstack/octane-query export surface', () => { + it('provides EVERY runtime export of real @tanstack/react-query', () => { + // The gold-standard parity lock: diff the whole runtime surface against + // the actual react-query module. Anything upstream ships that the port + // lacks fails here by name. + const upstream = Object.keys(reactQuery).sort() + const port = new Set(Object.keys(query)) + const missing = upstream.filter((name) => !port.has(name)) + expect(missing).toEqual([]) + }) + + it('exports nothing upstream keeps private (no accidental superset)', () => { + const upstream = new Set(Object.keys(reactQuery)) + const extras = Object.keys(query) + .filter((name) => !upstream.has(name)) + .sort() + // The port intentionally has NO binding-level extras. (query-core + // re-exports that react-query happens not to forward would show up here — + // keep that list empty too, so the surfaces stay byte-comparable.) + expect(extras).toEqual([]) + }) + + it('option helper exports are identity helpers like @tanstack/react-query', () => { + const q = { queryKey: ['x'], queryFn: async () => 'x' } + const inf = { + queryKey: ['i'], + queryFn: async () => ['i'], + initialPageParam: 0, + } + const mut = { mutationFn: async (value: string) => value } + expect(query.queryOptions(q as any)).toBe(q) + expect(query.infiniteQueryOptions(inf as any)).toBe(inf) + expect(query.mutationOptions(mut as any)).toBe(mut) + }) +}) diff --git a/packages/octane-query/tests/conformance/extra.test.ts b/packages/octane-query/tests/conformance/extra.test.ts new file mode 100644 index 00000000000..5823ee3bc9c --- /dev/null +++ b/packages/octane-query/tests/conformance/extra.test.ts @@ -0,0 +1,90 @@ +/** + * Additional conformance driven by the port review: query-key change → refetch, + * unmount → observer unsubscribe, the explicit-client (no-provider) path, and the + * provider mounting/unmounting the client. + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/octane-query' +import { mount, nextPaint } from '../_helpers' +import { KeyedApp } from '../_fixtures/extra.tsrx' +import { App } from '../_fixtures/app.tsrx' +import { Todo } from '../_fixtures/smoke.tsrx' + +let client: QueryClient +beforeEach(() => { + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + client.mount() +}) + +async function flush() { + for (let i = 0; i < 5; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +describe('query-key change', () => { + it('swaps the query and refetches when the key changes', async () => { + const r = mount(KeyedApp, { client, k: 1 }) + await flush() + expect(r.find('#status').textContent).toBe('data:v1') + r.update(KeyedApp, { client, k: 2 }) + await flush() + expect(r.find('#status').textContent).toBe('data:v2') + r.unmount() + }) +}) + +describe('unmount unsubscribes the observer', () => { + it('drops the query observer on unmount (no leak)', async () => { + let resolveFn: (v: string) => void = () => {} + const queryFn = () => new Promise((res) => (resolveFn = res)) + const r = mount(App, { client, queryFn }) + await flush() + resolveFn('x') + await flush() + const query = client.getQueryCache().find({ queryKey: ['k'] })! + expect(query.observers.length).toBeGreaterThan(0) + r.unmount() + await flush() + expect(query.observers.length).toBe(0) + }) +}) + +describe('explicit client (no provider)', () => { + it('useQuery(options, client) resolves the passed client', async () => { + let resolveFn: (v: string) => void = () => {} + const queryFn = () => new Promise((res) => (resolveFn = res)) + const r = mount(Todo, { client, queryFn }) + expect(r.find('#status').textContent).toBe('pending') + await flush() + resolveFn('direct') + await flush() + expect(r.find('#status').textContent).toBe('data:direct') + r.unmount() + }) +}) + +describe('QueryClientProvider mounts/unmounts the client', () => { + it('calls client.mount() on mount and client.unmount() on unmount', async () => { + const c = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + let mounts = 0 + let unmounts = 0 + const realMount = c.mount.bind(c) + const realUnmount = c.unmount.bind(c) + c.mount = () => { + mounts++ + return realMount() + } + c.unmount = () => { + unmounts++ + return realUnmount() + } + const r = mount(App, { client: c, queryFn: () => Promise.resolve('x') }) + await flush() + expect(mounts).toBe(1) + r.unmount() + await flush() + expect(unmounts).toBe(1) + }) +}) diff --git a/packages/octane-query/tests/conformance/followups.test.ts b/packages/octane-query/tests/conformance/followups.test.ts new file mode 100644 index 00000000000..8f2e42c863c --- /dev/null +++ b/packages/octane-query/tests/conformance/followups.test.ts @@ -0,0 +1,150 @@ +/** + * Follow-up hooks: useInfiniteQuery, useSuspenseQuery, useQueries, useIsFetching, + * useIsMutating (→ useMutationState), and usePrefetchQuery — each driving the real + * query-core observers/caches. + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient, dehydrate } from '@tanstack/octane-query' +import { mount, nextPaint } from '../_helpers' +import { + Fetching, + HydrationApp, + Infinite, + Mutating, + Prefetch, + Queries, + SequentialSuspenseQApp, + SuspenseQApp, +} from '../_fixtures/followups.tsrx' + +let client: QueryClient +beforeEach(() => { + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + client.mount() +}) + +async function flush() { + for (let i = 0; i < 6; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +describe('useInfiniteQuery', () => { + it('loads the first page', async () => { + const r = mount(Infinite, { client }) + expect(r.find('#status').textContent).toBe('pending') + await flush() + expect(r.find('#status').textContent).toBe('pages:p0') + r.unmount() + }) +}) + +describe('useQueries', () => { + it('runs multiple queries and combines their results', async () => { + const r = mount(Queries, { client }) + await flush() + expect(r.find('#status').textContent).toBe('A,B') + r.unmount() + }) +}) + +describe('useIsFetching', () => { + it('reports 1 while fetching and 0 once settled', async () => { + let resolveFn: (v: string) => void = () => {} + const queryFn = () => new Promise((res) => (resolveFn = res)) + const r = mount(Fetching, { client, queryFn }) + await flush() + expect(r.find('#status').textContent).toBe('fetching:1 status:pending') + resolveFn('x') + await flush() + expect(r.find('#status').textContent).toBe('fetching:0 status:success') + r.unmount() + }) +}) + +describe('useIsMutating / useMutationState', () => { + it('reports 1 while a mutation is pending, then 0', async () => { + let resolveFn: (v: string) => void = () => {} + const mutationFn = () => new Promise((res) => (resolveFn = res)) + const r = mount(Mutating, { client, mutationFn }) + expect(r.find('#status').textContent).toBe('pending:0') + r.click('#go') + await flush() + expect(r.find('#status').textContent).toBe('pending:1') + resolveFn('done') + await flush() + expect(r.find('#status').textContent).toBe('pending:0') + r.unmount() + }) +}) + +describe('usePrefetchQuery', () => { + it('prefetches into the cache', async () => { + const r = mount(Prefetch, { client }) + await flush() + expect(client.getQueryData(['pf'])).toBe('PF') + r.unmount() + }) +}) + +describe('HydrationBoundary', () => { + it('hydrates dehydrated state into the client before children read it', async () => { + const source = new QueryClient() + await source.prefetchQuery({ + queryKey: ['h'], + queryFn: () => Promise.resolve('hydrated'), + }) + const state = dehydrate(source) + const r = mount(HydrationApp, { client, state }) + await flush() + // staleTime:Infinity + hydrated data ⇒ no refetch; the hydrated value shows. + expect(r.find('#hdata').textContent).toBe('data:hydrated') + r.unmount() + }) +}) + +describe('useSuspenseQuery', () => { + it('suspends (fallback), then renders the guaranteed data', async () => { + let resolveFn: (v: string) => void = () => {} + const queryFn = () => new Promise((res) => (resolveFn = res)) + const r = mount(SuspenseQApp, { client, queryFn }) + expect(r.find('#fallback').textContent).toBe('loading') + await flush() + resolveFn('ready') + await flush() + expect(r.find('#data').textContent).toBe('data:ready') + r.unmount() + }) + + it('keeps sequential queries suspended until each query has data', async () => { + let resolveA: (value: string) => void = () => {} + let resolveB: (value: string) => void = () => {} + const promiseA = new Promise((resolve) => { + resolveA = resolve + }) + const promiseB = new Promise((resolve) => { + resolveB = resolve + }) + const queryFnA = () => promiseA + const queryFnB = () => promiseB + + const r = mount(SequentialSuspenseQApp, { client, queryFnA, queryFnB }) + expect(r.find('#sequential-fallback').textContent).toBe('loading') + + resolveA('A') + await flush() + // Resolving the first query must not expose the second query's pending + // `data`; the boundary stays suspended until that data is also defined. + expect(r.findAll('#sequential-fallback')).toHaveLength(1) + expect(r.findAll('#sequential-data')).toHaveLength(0) + expect(r.findAll('#sequential-error')).toHaveLength(0) + + resolveB('B') + await flush() + expect(r.find('#sequential-data').textContent).toBe('A/B') + expect(r.findAll('#sequential-fallback')).toHaveLength(0) + expect(r.findAll('#sequential-error')).toHaveLength(0) + r.unmount() + }) +}) diff --git a/packages/octane-query/tests/conformance/parity.test.ts b/packages/octane-query/tests/conformance/parity.test.ts new file mode 100644 index 00000000000..7b9f87daf80 --- /dev/null +++ b/packages/octane-query/tests/conformance/parity.test.ts @@ -0,0 +1,304 @@ +/** + * Parity sweep — the behaviors added/fixed to close the audit gaps: the suspense + * clearReset retry loop, render-prop QueryErrorResetBoundary, HydrationBoundary + * newer/older/streaming semantics, experimental_prefetchInRender's promise, + * useQueries combine, skipToken, fetchNextPage, useSuspenseInfiniteQuery, + * usePrefetchInfiniteQuery, useMutationState filters+select, tracked-props + * render efficiency, and the isRestoring true→false flip. + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient, dehydrate } from '@tanstack/octane-query' +import { mount, nextPaint } from '../_helpers' +import { + CombineApp, + FlipRestoringApp, + HydrationApp, + InfiniteApp, + MutationStateApp, + PrefetchInRenderApp, + PrefetchInfiniteApp, + RenderPropResetApp, + SkipTokenApp, + SuspenseInfiniteApp, + SuspenseRetryApp, + TrackedApp, + mutationCounter, + promiseCapture, + renderPropValues, + restoreSetter, + trackedRenders, +} from '../_fixtures/parity.tsrx' + +let client: QueryClient +beforeEach(() => { + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + client.mount() +}) + +async function flush(n = 6) { + for (let i = 0; i < n; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +describe('suspense error retry loop (clearReset)', () => { + it('reset → retry → fail AGAIN re-throws to the boundary (no undefined-data fall-through)', async () => { + let calls = 0 + const queryFn = () => { + calls++ + return Promise.reject(new Error('boom-' + calls)) + } + const r = mount(SuspenseRetryApp, { client, queryFn }) + await flush() + + // First failure reaches the boundary. + expect(r.find('#sq-msg').textContent).toBe('boom-1') + + // Reset + retry: the refetch fails again — the boundary must re-show the + // error. Before the clearReset fix, isReset() stayed true at replay so the + // component fell through and rendered undefined data. + ;(r.find('#sq-retry') as HTMLElement).click() + await flush() + + expect(calls).toBeGreaterThanOrEqual(2) + expect(r.findAll('#sq-data').length).toBe(0) + expect(r.find('#sq-msg').textContent).toContain('boom') + r.unmount() + }) +}) + +describe('QueryErrorResetBoundary render-prop children', () => { + it('invokes function children with the boundary value', async () => { + renderPropValues.length = 0 + const r = mount(RenderPropResetApp, { client }) + await flush() + + expect(r.find('#rp-child').textContent).toBe('reset:function') + const value = renderPropValues[0] as any + expect(typeof value.reset).toBe('function') + expect(typeof value.isReset).toBe('function') + expect(typeof value.clearReset).toBe('function') + // The provided context value is the same object handed to children. + expect(value.isReset()).toBe(false) + value.reset() + expect(value.isReset()).toBe(true) + r.unmount() + }) +}) + +describe('HydrationBoundary', () => { + // Real dehydrate() payload for `key`, with dataUpdatedAt shifted so the + // entry is deterministically newer/older than the cache. + function dehydratedWith( + key: Array, + data: string, + dataUpdatedAt: number, + ) { + const source = new QueryClient() + source.setQueryData(key, data) + const state = dehydrate(source) as any + state.queries[0].state.dataUpdatedAt = dataUpdatedAt + return state + } + + it('hydrates NEWER dehydrated data over the cache, leaves NEWER cache data alone', async () => { + const key = ['h1'] + client.setQueryData(key, 'cached') + const cachedAt = client.getQueryCache().find({ queryKey: key })!.state + .dataUpdatedAt + + // Newer dehydrated entry — wins. + const r = mount(HydrationApp, { + client, + state: dehydratedWith(key, 'newer', cachedAt + 1000), + queryKey: key, + }) + await flush() + expect(r.find('#hydrated').textContent).toBe('newer') + r.unmount() + + // Older dehydrated entry — must NOT clobber. + const r2 = mount(HydrationApp, { + client, + state: dehydratedWith(key, 'older', cachedAt - 1000), + queryKey: key, + }) + await flush() + expect(r2.find('#hydrated').textContent).toBe('newer') + r2.unmount() + }) + + it('a dehydrated STREAMING entry (pending promise + later dehydratedAt) re-hydrates a settled query', async () => { + const key = ['h2'] + client.setQueryData(key, 'settled') + + // A REAL streaming chunk: dehydrate a source whose query is still + // PENDING, with the documented streaming opt-in (shouldDehydrateQuery + // including pending) — the payload then carries the in-flight promise + + // dehydratedAt. Its dataUpdatedAt is 0 (no data yet), so only upstream's + // streaming condition (promise && dehydratedAt > cache.dataUpdatedAt) + // applies. + const source = new QueryClient() + const inFlight = source.prefetchQuery({ + queryKey: key, + queryFn: () => + new Promise((res) => setTimeout(() => res('streamed'), 5)), + }) + const chunk = dehydrate(source, { + shouldDehydrateQuery: () => true, + }) as any + expect(chunk.queries[0].promise).toBeTruthy() + chunk.queries[0].dehydratedAt = + client.getQueryCache().find({ queryKey: key })!.state.dataUpdatedAt + 1000 + + const r = mount(HydrationApp, { client, state: chunk, queryKey: key }) + await inFlight + await flush() + expect(r.find('#hydrated').textContent).toBe('streamed') + r.unmount() + }) + + it('round-trips a real dehydrate() payload', async () => { + const source = new QueryClient() + source.setQueryData(['rt'], 'round-trip') + const state = dehydrate(source) + + const r = mount(HydrationApp, { client, state, queryKey: ['rt'] }) + await flush() + expect(r.find('#hydrated').textContent).toBe('round-trip') + r.unmount() + }) +}) + +describe('experimental_prefetchInRender', () => { + it('result.promise resolves with the data', async () => { + promiseCapture.promise = null + const queryFn = () => + new Promise((res) => setTimeout(() => res('pir-data'), 5)) + const r = mount(PrefetchInRenderApp, { client, queryFn }) + + expect(promiseCapture.promise).toBeTruthy() + await expect(promiseCapture.promise).resolves.toBe('pir-data') + await flush() + expect(r.find('#pir').textContent).toBe('data:pir-data') + r.unmount() + }) +}) + +describe('useQueries combine', () => { + it('renders the combined aggregate, not the raw results array', async () => { + const r = mount(CombineApp, { client }) + await flush() + expect(r.find('#combined').textContent).toBe('3:true') + r.unmount() + }) +}) + +describe('skipToken', () => { + it('disables the query — pending status, idle fetchStatus, no fetch', async () => { + const r = mount(SkipTokenApp, { client }) + await flush() + expect(r.find('#skip').textContent).toBe('pending:idle') + r.unmount() + }) +}) + +describe('useInfiniteQuery fetchNextPage', () => { + it('appends pages and reports hasNextPage', async () => { + const pageFn = (page: number) => Promise.resolve('p' + page) + const r = mount(InfiniteApp, { client, pageFn }) + await flush() + expect(r.find('#pages').textContent).toBe('p0') + expect(r.find('#has-next').textContent).toBe('true') + ;(r.find('#next') as HTMLElement).click() + await flush() + expect(r.find('#pages').textContent).toBe('p0,p1') + ;(r.find('#next') as HTMLElement).click() + await flush() + expect(r.find('#pages').textContent).toBe('p0,p1,p2') + expect(r.find('#has-next').textContent).toBe('false') + r.unmount() + }) +}) + +describe('useSuspenseInfiniteQuery', () => { + it('suspends then renders the first page', async () => { + const pageFn = (page: number) => + new Promise((res) => setTimeout(() => res('sp' + page), 5)) + const r = mount(SuspenseInfiniteApp, { client, pageFn }) + expect(r.findAll('#sinf-loading').length).toBe(1) + await flush() + expect(r.find('#sinf').textContent).toBe('sp0') + r.unmount() + }) +}) + +describe('usePrefetchInfiniteQuery', () => { + it('primes the cache with the first page', async () => { + const pageFn = (page: number) => Promise.resolve('pf' + page) + const r = mount(PrefetchInfiniteApp, { client, pageFn }) + await flush() + const state = client.getQueryState(['pinf']) as any + expect(state?.data?.pages).toEqual(['pf0']) + r.unmount() + }) +}) + +describe('useMutationState filters + select', () => { + it('selects variables of mutations matching the key filter', async () => { + mutationCounter.n = 0 + const mutationFn = (v: string) => Promise.resolve(v) + const r = mount(MutationStateApp, { client, mutationFn }) + await flush() + expect(r.find('#vars').textContent).toBe('none') + ;(r.find('#mutate') as HTMLElement).click() + await flush() + expect(r.find('#vars').textContent).toBe('v-1') + ;(r.find('#mutate') as HTMLElement).click() + await flush() + expect(r.find('#vars').textContent).toBe('v-1,v-2') + r.unmount() + }) +}) + +describe('tracked properties', () => { + it('a data-only reader does NOT re-render on an isFetching-only change', async () => { + trackedRenders.count = 0 + const queryFn = () => + new Promise((res) => setTimeout(() => res('t'), 2)) + const r = mount(TrackedApp, { client, queryFn }) + await flush() + expect(r.find('#tracked').textContent).toBe('t') + const rendersAfterLoad = trackedRenders.count + + // A background refetch resolving to the SAME data flips isFetching + // true→false but leaves data untouched — the tracked-props subscription + // must not re-render the component for it. + await client.refetchQueries({ queryKey: ['tracked'] }) + await flush() + expect(r.find('#tracked').textContent).toBe('t') + expect(trackedRenders.count).toBe(rendersAfterLoad) + r.unmount() + }) +}) + +describe('isRestoring flip', () => { + it('true → false starts the fetch that restore was holding back', async () => { + let calls = 0 + const queryFn = () => { + calls++ + return Promise.resolve('restored') + } + const r = mount(FlipRestoringApp, { client, queryFn }) + await flush() + expect(calls).toBe(0) + expect(r.find('#flip').textContent).toBe('pending') + + restoreSetter.set!(false) + await flush() + expect(calls).toBeGreaterThan(0) + expect(r.find('#flip').textContent).toBe('data:restored') + r.unmount() + }) +}) diff --git a/packages/octane-query/tests/conformance/query.test.ts b/packages/octane-query/tests/conformance/query.test.ts new file mode 100644 index 00000000000..3f8e64c868a --- /dev/null +++ b/packages/octane-query/tests/conformance/query.test.ts @@ -0,0 +1,79 @@ +/** + * @tanstack/octane-query conformance — useQuery / useMutation / QueryClientProvider / + * useQueryClient on octane, driving the REAL @tanstack/query-core (observers + + * caches reused verbatim). Exercises the async query lifecycle (pending → + * success / error), context client resolution, and the mutation lifecycle. + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/octane-query' +import { mount, nextPaint } from '../_helpers' +import { App, MutApp, ProbeApp } from '../_fixtures/app.tsrx' + +let client: QueryClient + +beforeEach(() => { + client = new QueryClient({ + defaultOptions: { queries: { retry: false }, mutations: {} }, + }) +}) + +// query-core batches notifications on a macrotask; flush a few cycles + paints. +async function flush() { + for (let i = 0; i < 5; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +describe('useQuery lifecycle (via QueryClientProvider)', () => { + it('pending -> success', async () => { + let resolveFn: (v: string) => void = () => {} + const queryFn = () => new Promise((r) => (resolveFn = r)) + const r = mount(App, { client, queryFn }) + expect(r.find('#status').textContent).toBe('pending') + await flush() + resolveFn('world') + await flush() + expect(r.find('#status').textContent).toBe('data:world') + r.unmount() + }) + + it('pending -> error (retry disabled)', async () => { + let rejectFn: (e: Error) => void = () => {} + const queryFn = () => new Promise((_res, rej) => (rejectFn = rej)) + const r = mount(App, { client, queryFn }) + expect(r.find('#status').textContent).toBe('pending') + await flush() + rejectFn(new Error('boom')) + await flush() + expect(r.find('#status').textContent).toBe('error:boom') + r.unmount() + }) +}) + +describe('useQueryClient', () => { + it('resolves the client provided by QueryClientProvider', async () => { + let seen: unknown = null + const r = mount(ProbeApp, { client, onClient: (c: unknown) => (seen = c) }) + await nextPaint() + expect(r.find('#probe').textContent).toBe('ok') + expect(seen).toBe(client) + r.unmount() + }) +}) + +describe('useMutation lifecycle', () => { + it('idle -> pending -> success on mutate()', async () => { + let resolveFn: (v: string) => void = () => {} + const mutationFn = () => new Promise((r) => (resolveFn = r)) + const r = mount(MutApp, { client, mutationFn }) + expect(r.find('#mstatus').textContent).toBe('idle') + r.click('#go') + await flush() + expect(r.find('#mstatus').textContent).toBe('pending') + resolveFn('done') + await flush() + expect(r.find('#mstatus').textContent).toBe('data:done') + r.unmount() + }) +}) diff --git a/packages/octane-query/tests/conformance/streaming-ssr.test.ts b/packages/octane-query/tests/conformance/streaming-ssr.test.ts new file mode 100644 index 00000000000..bb75354f614 --- /dev/null +++ b/packages/octane-query/tests/conformance/streaming-ssr.test.ts @@ -0,0 +1,81 @@ +// @vitest-environment node + +import { QueryClient } from '@tanstack/query-core' +import { renderToReadableStream } from 'octane/server' +import { describe, expect, it } from 'vitest' +import { + StreamingSuspenseApp, + WrappedStreamingSuspenseApp, +} from '../_fixtures/streaming-ssr.tsrx' + +function deferred() { + let resolve!: (value: T) => void + const promise = new Promise((res) => { + resolve = res + }) + return { promise, resolve } +} + +async function renderSequentialSuspense( + component: typeof StreamingSuspenseApp | typeof WrappedStreamingSuspenseApp, + extraProps: { mode: 'query' | 'queries' } | undefined, +) { + const a = deferred() + const b = deferred() + const bStarted = deferred() + let bCalls = 0 + const stream = await renderToReadableStream(component, { + ...extraProps, + client: new QueryClient(), + a: () => a.promise, + b: () => { + bCalls++ + bStarted.resolve() + return b.promise + }, + }) + const responseText = new Response(stream).text() + + let allReady = false + void stream.allReady.then(() => { + allReady = true + }) + + a.resolve('A') + await bStarted.promise + await Promise.resolve() + const finishedBeforeBResolved = allReady + + b.resolve('B') + const html = await responseText + + return { bCalls, finishedBeforeBResolved, html } +} + +describe('streaming SSR suspense replay', () => { + it.each(['query', 'queries'] as const)( + 'waits for both sequential %s hooks', + async (mode) => { + const result = await renderSequentialSuspense(StreamingSuspenseApp, { + mode, + }) + + expect(result.finishedBeforeBResolved).toBe(false) + expect(result.bCalls).toBe(1) + expect(result.html).toContain('A/B') + expect(result.html).not.toContain('A/undefined') + }, + ) + + it('keeps custom hook instances distinct during replay', async () => { + const result = await renderSequentialSuspense( + WrappedStreamingSuspenseApp, + undefined, + ) + + expect(result.finishedBeforeBResolved).toBe(false) + expect(result.bCalls).toBe(1) + expect(result.html).toContain('A/B') + expect(result.html).not.toContain('A/undefined') + }) +}) diff --git a/packages/octane-query/tests/conformance/suspense.test.ts b/packages/octane-query/tests/conformance/suspense.test.ts new file mode 100644 index 00000000000..e510d8dfaa4 --- /dev/null +++ b/packages/octane-query/tests/conformance/suspense.test.ts @@ -0,0 +1,81 @@ +/** + * Suspense + error-boundary integration: a `suspense: true` query throws its + * in-flight promise (caught by octane's `@pending`) and, on error, throws the + * error (caught by `@catch`) — the octane equivalents of React Suspense / + * ErrorBoundary. + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/octane-query' +import { mount, nextPaint } from '../_helpers' +import { + SuspenseApp, + SuspenseComponentApp, + SuspenseQueriesApp, +} from '../_fixtures/suspense.tsrx' + +let client: QueryClient +beforeEach(() => { + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + client.mount() +}) + +async function flush() { + for (let i = 0; i < 6; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +describe('suspense query', () => { + it('shows @pending while loading, then the data', async () => { + let resolveFn: (v: string) => void = () => {} + const queryFn = () => new Promise((res) => (resolveFn = res)) + const r = mount(SuspenseApp, { client, queryFn }) + // First render suspends → @pending fallback. + expect(r.find('#fallback').textContent).toBe('loading') + await flush() + resolveFn('ready') + await flush() + expect(r.find('#data').textContent).toBe('data:ready') + r.unmount() + }) + + it('shows @catch when the suspense query errors', async () => { + let rejectFn: (e: Error) => void = () => {} + const queryFn = () => new Promise((_res, rej) => (rejectFn = rej)) + const r = mount(SuspenseApp, { client, queryFn }) + expect(r.find('#fallback').textContent).toBe('loading') + await flush() + rejectFn(new Error('nope')) + await flush() + expect(r.find('#caught').textContent).toBe('caught:nope') + r.unmount() + }) + + it('works with octane’s component (not just @try)', async () => { + let resolveFn: (v: string) => void = () => {} + const queryFn = () => new Promise((res) => (resolveFn = res)) + const r = mount(SuspenseComponentApp, { client, queryFn }) + expect(r.find('#fallback').textContent).toBe('loading') + await flush() + resolveFn('ready') + await flush() + expect(r.find('#data').textContent).toBe('data:ready') + r.unmount() + }) + + it('supports useSuspenseQueries for multiple suspense queries', async () => { + let resolveA: (v: string) => void = () => {} + let resolveB: (v: string) => void = () => {} + const a = () => new Promise((res) => (resolveA = res)) + const b = () => new Promise((res) => (resolveB = res)) + const r = mount(SuspenseQueriesApp, { client, a, b }) + expect(r.find('#fallback').textContent).toBe('loading') + await flush() + resolveA('A') + resolveB('B') + await flush() + expect(r.find('#data').textContent).toBe('data:A/B') + r.unmount() + }) +}) diff --git a/packages/octane-query/tests/conformance/transition-suspense.test.ts b/packages/octane-query/tests/conformance/transition-suspense.test.ts new file mode 100644 index 00000000000..6edaa7682ca --- /dev/null +++ b/packages/octane-query/tests/conformance/transition-suspense.test.ts @@ -0,0 +1,195 @@ +/** + * React useTransition + Suspense parity for `useSuspenseQuery`. + * + * Per React's useTransition contract (ReactSuspense*-test "transition does not + * show fallback when refetching the same boundary"): once a transition is + * showing previously-committed content, React keeps it on screen until the new + * query resolves — it does NOT flash the Suspense fallback, even though the + * query observer's notification (and thus the re-render that re-suspends on the + * new fetch) arrives asynchronously at URGENT priority (query-core's + * notifyManager schedules on a setTimeout(0) macrotask). + * + * Before the octane fix this flashed the @pending fallback during the move; the + * runtime now continues the transition hold across the urgent re-suspend. + */ +import { beforeEach, describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/octane-query' +import { act } from 'octane' +import { mount, nextPaint } from '../_helpers' +import { TransitionSuspenseApp } from '../_fixtures/transition-suspense.tsrx' + +let client: QueryClient +beforeEach(() => { + client = new QueryClient({ defaultOptions: { queries: { retry: false } } }) + client.mount() +}) + +// Drain query-core's macrotask scheduler (setTimeout(0)) AND octane's passive +// effects several times so the observer's async notifications land. +async function flush() { + for (let i = 0; i < 8; i++) { + await new Promise((r) => setTimeout(r, 0)) + await nextPaint() + } +} + +function deferred() { + let resolve!: (v: T) => void + const promise = new Promise((res) => { + resolve = res + }) + return { promise, resolve } +} + +describe('useSuspenseQuery — transition keeps prior content, no fallback flash (React parity)', () => { + it('value=1 committed; transition to value=2 holds value=1 until value=2 resolves', async () => { + // Per-value controlled promises. value=1 resolves immediately so the first + // query commits content; value=2 stays pending until we resolve it. + const d1 = deferred() + const d2 = deferred() + const promises: Record> = { + 1: d1.promise, + 2: d2.promise, + } + const queryFn = (v: number) => promises[v] + + let setValue!: (v: number) => void + const bindSetValue = (fn: (v: number) => void) => { + setValue = fn + } + + const r = mount(TransitionSuspenseApp, { client, queryFn, bindSetValue }) + + // First render suspends → fallback while value=1's query is in flight. + expect(r.find('#fallback').textContent).toBe('loading') + + d1.resolve('one') + await flush() + // value=1 content committed. + expect(r.find('#data').textContent).toBe('data:one') + expect(r.findAll('#fallback')).toHaveLength(0) + expect(r.find('#pending').textContent).toBe('idle') + + // Transition to value=2: the reader re-renders under key ['…', 2] and the + // new query is in flight. The observer notifies asynchronously (macrotask) + // so the re-suspend re-render is URGENT — but because the boundary is + // transition-held, value=1 content STAYS and the fallback NEVER appears. + // + // The flash (pre-fix) is TRANSIENT — it shows only across the macrotask + // window between the observer's async notify and the eventual resolve, so + // asserting only after a full `flush()` would miss it. Watch the DOM with a + // MutationObserver so any momentary appearance of #fallback (or the loss of + // the value=1 content) is recorded for the whole in-flight window. + let fallbackEverSeen = false + let contentEverLost = false + const mo = new MutationObserver(() => { + if (r.container.querySelector('#fallback')) fallbackEverSeen = true + if (!r.container.querySelector('#data')) contentEverLost = true + }) + mo.observe(r.container, { childList: true, subtree: true }) + + await act(() => setValue(2)) + await flush() + mo.disconnect() + + expect(fallbackEverSeen).toBe(false) // <- pre-fix this FLASHED (true) + expect(contentEverLost).toBe(false) // value=1 content never removed + expect(r.find('#data').textContent).toBe('data:one') // OLD content held + expect(r.findAll('#fallback')).toHaveLength(0) + expect(r.find('#pending').textContent).toBe('pending') + + // Resolve value=2 → the held boundary commits the new content all at once + // and isPending returns to idle. The fallback never showed at any point. + d2.resolve('two') + await flush() + expect(r.find('#data').textContent).toBe('data:two') + expect(r.findAll('#fallback')).toHaveLength(0) + expect(r.find('#pending').textContent).toBe('idle') + + r.unmount() + }) + + it('an URGENT key change while held re-suspends the query without flashing the fallback', async () => { + // This is the exact failing shape from the hacker-news example: a transition + // changes the key and the boundary HOLDS on the new page's in-flight fetch; + // then the query observer notifies ASYNCHRONOUSLY (a setTimeout(0) macrotask) + // so a SUBSEQUENT re-render that re-suspends on a DIFFERENT in-flight query + // runs at URGENT priority. React keeps the prior content on screen; pre-fix + // octane flashed the @pending fallback on that urgent re-suspend. + // + // We model the urgent macrotask re-render by changing the key to value=3 + // urgently (NOT wrapped in a transition) WHILE the boundary is still held on + // value=2 — exactly the priority the observer notify lands at. + const d1 = deferred() + const d2 = deferred() + const d3 = deferred() + const promises: Record> = { + 1: d1.promise, + 2: d2.promise, + 3: d3.promise, + } + const queryFn = (v: number) => promises[v] + + let setValue!: (v: number) => void + let setValueUrgent!: (v: number) => void + const bindSetValue = (fn: (v: number) => void) => { + setValue = fn + } + const bindSetValueUrgent = (fn: (v: number) => void) => { + setValueUrgent = fn + } + + const r = mount(TransitionSuspenseApp, { + client, + queryFn, + bindSetValue, + bindSetValueUrgent, + }) + expect(r.find('#fallback').textContent).toBe('loading') + d1.resolve('one') + await flush() + expect(r.find('#data').textContent).toBe('data:one') + expect(r.find('#pending').textContent).toBe('idle') + + // Watch the DOM through the whole move so a transient fallback flash (or any + // removal of the value=1 content) is caught — the flash is not visible at a + // settled checkpoint. + let fallbackEverSeen = false + let contentEverLost = false + const mo = new MutationObserver(() => { + if (r.container.querySelector('#fallback')) fallbackEverSeen = true + if (!r.container.querySelector('#data')) contentEverLost = true + }) + mo.observe(r.container, { childList: true, subtree: true }) + + // Transition to value=2 → boundary HOLDS content-one on the value=2 fetch. + await act(() => setValue(2)) + await flush() + expect(r.find('#data').textContent).toBe('data:one') + expect(fallbackEverSeen).toBe(false) + + // While STILL held, an URGENT key change to value=3 re-renders the reader + // under key ['…', 3] and re-suspends on the value=3 fetch — a DIFFERENT + // thenable, at urgent priority. React holds; pre-fix octane flashed here. + await act(() => setValueUrgent(3)) + await flush() + expect(r.find('#data').textContent).toBe('data:one') // OLD content still held + expect(fallbackEverSeen).toBe(false) // <- pre-fix this FLASHED (true) + expect(r.find('#pending').textContent).toBe('pending') + + // Resolve the value=3 fetch → the held boundary commits content-three all at + // once and isPending returns to idle. + d2.resolve('two') + d3.resolve('three') + await flush() + mo.disconnect() + + expect(fallbackEverSeen).toBe(false) // never flashed at any point + expect(contentEverLost).toBe(false) // value=1 content was never removed + expect(r.find('#data').textContent).toBe('data:three') + expect(r.findAll('#fallback')).toHaveLength(0) + expect(r.find('#pending').textContent).toBe('idle') + + r.unmount() + }) +}) diff --git a/packages/octane-query/tests/differential/.gitignore b/packages/octane-query/tests/differential/.gitignore new file mode 100644 index 00000000000..e74e7ec3076 --- /dev/null +++ b/packages/octane-query/tests/differential/.gitignore @@ -0,0 +1 @@ +.react-cache/ diff --git a/packages/octane-query/tests/differential/_rig.ts b/packages/octane-query/tests/differential/_rig.ts new file mode 100644 index 00000000000..351528ed1af --- /dev/null +++ b/packages/octane-query/tests/differential/_rig.ts @@ -0,0 +1,158 @@ +import { basename, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { expect } from 'vitest' +import { + createRoot as createOctaneRoot, + drainPassiveEffects, + flushSync, +} from 'octane' +import * as React from 'react' +import { act as reactAct } from 'react' +import { createRoot as createReactRoot } from 'react-dom/client' +import type { Root as ReactRoot } from 'react-dom/client' + +const directory = dirname(fileURLToPath(import.meta.url)) +const cacheDirectory = join(directory, '.react-cache') + +;( + globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean } +).IS_REACT_ACT_ENVIRONMENT = true + +function hashString(value: string): string { + let hash = 5381 + for (let index = 0; index < value.length; index++) { + hash = ((hash << 5) + hash + value.charCodeAt(index)) | 0 + } + return Math.abs(hash).toString(36) +} + +function stripComments(value: string): string { + let output = '' + let index = 0 + while (index < value.length) { + const open = value.indexOf('', open + 4) + if (close === -1) { + break + } + index = close + 3 + } + return output +} + +function normalizeHtml(value: string): string { + return stripComments(value).replaceAll('\n', '').replaceAll('\t', '').trim() +} + +async function loadReactFixture( + sourcePath: string, +): Promise> { + const slug = basename(sourcePath).replace(/\.tsrx$/, '') + const outputPath = join( + cacheDirectory, + `${slug}-${hashString(sourcePath)}.js`, + ) + return import(/* @vite-ignore */ outputPath) +} + +interface DifferentialMount { + container: HTMLElement + click: (selector: string) => Promise +} + +interface DifferentialPair { + step: ( + name: string, + action: ( + octane: DifferentialMount, + react: DifferentialMount, + ) => void | Promise, + ) => Promise + unmount: () => void +} + +export async function mountDifferential( + sourcePath: string, + exportName: string, +): Promise { + const octaneModule = await import(/* @vite-ignore */ sourcePath) + const reactModule = await loadReactFixture(sourcePath) + const OctaneComponent = octaneModule[exportName] + const ReactComponent = reactModule[exportName] + + if (typeof OctaneComponent !== 'function') { + throw new Error(`Octane export ${exportName} was not found`) + } + if (typeof ReactComponent !== 'function') { + throw new Error(`React export ${exportName} was not found`) + } + + const octaneContainer = document.createElement('div') + const reactContainer = document.createElement('div') + document.body.append(octaneContainer, reactContainer) + + const octaneRoot = createOctaneRoot(octaneContainer) + octaneRoot.render(OctaneComponent) + flushSync(() => {}) + + const reactRoot: ReactRoot = createReactRoot(reactContainer) + await reactAct(async () => { + reactRoot.render(React.createElement(ReactComponent)) + }) + + function createMount( + container: HTMLElement, + react: boolean, + ): DifferentialMount { + return { + container, + async click(selector) { + let element = container.querySelector(selector) + if (!element && selector.startsWith('#')) { + const id = selector.slice(1) + element = + Array.from(container.getElementsByTagName('*')).find( + (candidate) => candidate.id === id, + ) ?? null + } + if (!(element instanceof HTMLElement)) { + throw new Error(`No element matches ${selector}`) + } + if (react) { + await reactAct(async () => element.click()) + } else { + flushSync(() => element.click()) + } + }, + } + } + + const octane = createMount(octaneContainer, false) + const react = createMount(reactContainer, true) + + return { + async step(name, action) { + await action(octane, react) + drainPassiveEffects() + await reactAct(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)) + }) + const octaneHtml = normalizeHtml(octaneContainer.innerHTML) + const reactHtml = normalizeHtml(reactContainer.innerHTML) + expect(octaneHtml, `Differential DOM divergence at ${name}`).toBe( + reactHtml, + ) + }, + unmount() { + octaneRoot.unmount() + reactAct(() => reactRoot.unmount()) + octaneContainer.remove() + reactContainer.remove() + }, + } +} diff --git a/packages/octane-query/tests/differential/_setup.ts b/packages/octane-query/tests/differential/_setup.ts new file mode 100644 index 00000000000..4bbefbbf4d4 --- /dev/null +++ b/packages/octane-query/tests/differential/_setup.ts @@ -0,0 +1,59 @@ +import { mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { basename, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { transformSync } from 'esbuild' +import { compile as compileToReact } from '@tsrx/react' + +const directory = dirname(fileURLToPath(import.meta.url)) +const fixtureDirectory = join(directory, '../_fixtures') +const cacheDirectory = join(directory, '.react-cache') +const fixtureNames = ['cached-diff.tsrx', 'async-diff.tsrx'] + +function hashString(value: string): string { + let hash = 5381 + for (let index = 0; index < value.length; index++) { + hash = ((hash << 5) + hash + value.charCodeAt(index)) | 0 + } + return Math.abs(hash).toString(36) +} + +function compileFixture(sourcePath: string): void { + const source = readFileSync(sourcePath, 'utf8') + const compiled = compileToReact(source, sourcePath) + if (compiled.errors.length) { + throw new Error(`React compilation failed for ${sourcePath}`) + } + + const transformed = transformSync(compiled.code, { + loader: 'tsx', + jsx: 'automatic', + jsxImportSource: 'react', + target: 'esnext', + format: 'esm', + sourcefile: sourcePath, + }) + const rewritten = transformed.code + .replace( + /from\s+["']@tanstack\/octane-query(\/[^"']*)?["']/g, + (_match, subpath) => `from "@tanstack/react-query${subpath || ''}"`, + ) + .replace(/from\s+["']octane["']/g, 'from "react"') + const slug = basename(sourcePath).replace(/\.tsrx$/, '') + const outputPath = join( + cacheDirectory, + `${slug}-${hashString(sourcePath)}.js`, + ) + writeFileSync(outputPath, rewritten) +} + +export function setup(): void { + rmSync(cacheDirectory, { force: true, recursive: true }) + mkdirSync(cacheDirectory, { recursive: true }) + for (const fixtureName of fixtureNames) { + compileFixture(join(fixtureDirectory, fixtureName)) + } +} + +export function teardown(): void { + rmSync(cacheDirectory, { force: true, recursive: true }) +} diff --git a/packages/octane-query/tests/differential/parity.test.ts b/packages/octane-query/tests/differential/parity.test.ts new file mode 100644 index 00000000000..d13394ba306 --- /dev/null +++ b/packages/octane-query/tests/differential/parity.test.ts @@ -0,0 +1,39 @@ +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { describe, expect, it } from 'vitest' +import { mountDifferential } from './_rig' + +const directory = dirname(fileURLToPath(import.meta.url)) +const cachedFixture = resolve(directory, '../_fixtures/cached-diff.tsrx') +const asyncFixture = resolve(directory, '../_fixtures/async-diff.tsrx') +const settle = () => + new Promise((timerResolve) => setTimeout(timerResolve, 40)) + +describe('Octane and React Query differential parity', () => { + it('renders the same initialData result shape', async () => { + expect.hasAssertions() + const pair = await mountDifferential(cachedFixture, 'CachedApp') + await pair.step('mount', () => {}) + pair.unmount() + }) + + it('renders the same pending and success query states', async () => { + expect.hasAssertions() + const pair = await mountDifferential(asyncFixture, 'AsyncApp') + await pair.step('pending', () => {}) + await pair.step('success', settle) + pair.unmount() + }) + + it('renders the same idle, pending, and success mutation states', async () => { + expect.hasAssertions() + const pair = await mountDifferential(asyncFixture, 'MutationApp') + await pair.step('idle', () => {}) + await pair.step('pending', async (octane, react) => { + await octane.click('#go') + await react.click('#go') + }) + await pair.step('success', settle) + pair.unmount() + }) +}) diff --git a/packages/octane-query/tests/tsconfig.json b/packages/octane-query/tests/tsconfig.json new file mode 100644 index 00000000000..f3e6e0b55a5 --- /dev/null +++ b/packages/octane-query/tests/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "composite": false, + "emitDeclarationOnly": false, + "noEmit": true + }, + "include": ["**/*.ts"] +} diff --git a/packages/octane-query/tsconfig.json b/packages/octane-query/tsconfig.json new file mode 100644 index 00000000000..0aedd03b91c --- /dev/null +++ b/packages/octane-query/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist-ts", + "rootDir": "." + }, + "include": ["src", "octane-compiler.d.ts", "*.config.ts", "package.json"], + "references": [{ "path": "../query-core" }] +} diff --git a/packages/octane-query/tsconfig.legacy.json b/packages/octane-query/tsconfig.legacy.json new file mode 100644 index 00000000000..703c53b1c71 --- /dev/null +++ b/packages/octane-query/tsconfig.legacy.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./dist-ts/legacy" + }, + "include": ["src"] +} diff --git a/packages/octane-query/tsconfig.prod.json b/packages/octane-query/tsconfig.prod.json new file mode 100644 index 00000000000..8b12fb09d8e --- /dev/null +++ b/packages/octane-query/tsconfig.prod.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "incremental": false, + "composite": false, + "rootDir": "../../" + }, + "include": ["src", "octane-compiler.d.ts"], + "exclude": ["src/__tests__"] +} diff --git a/packages/octane-query/vite.config.ts b/packages/octane-query/vite.config.ts new file mode 100644 index 00000000000..703df806387 --- /dev/null +++ b/packages/octane-query/vite.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from 'vitest/config' +import { octane } from 'octane/compiler/vite' + +import packageJson from './package.json' + +export default defineConfig({ + plugins: [octane()], + resolve: { + conditions: ['@tanstack/custom-condition'], + }, + environments: { + ssr: { + resolve: { + conditions: ['@tanstack/custom-condition'], + }, + }, + }, + test: { + name: packageJson.name, + dir: './tests', + watch: false, + environment: 'jsdom', + globalSetup: ['./tests/differential/_setup.ts'], + coverage: { + enabled: !!process.env.CI, + provider: 'istanbul', + include: ['src/**/*'], + exclude: ['src/__tests__/**'], + }, + typecheck: { enabled: true }, + restoreMocks: true, + retry: process.env.CI ? 3 : 0, + }, +}) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 4e37d863ded..29a8e66d27c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -24,6 +24,9 @@ packages: - '!examples/vue/2*' - '!examples/vue/nuxt*' +catalog: + octane: ^0.1.12 + overrides: 'chokidar': '5.0.0' '@types/react': '^19.2.7' diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 574fe8d8b60..91e93812edc 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -167,6 +167,13 @@ for (const pkg of [ outputDir: resolve(__dirname, '../docs/framework/preact/reference'), exclude: ['./packages/query-core/**/*'], }, + { + entryPoints: [resolve(__dirname, '../packages/octane-query/src/index.ts')], + tsconfig: resolve(__dirname, '../packages/octane-query/tsconfig.json'), + outputDir: resolve(__dirname, '../docs/framework/octane/reference'), + exclude: ['./packages/query-core/**/*'], + trimGeneratedMarkdown: true, + }, { entryPoints: [resolve(__dirname, '../packages/lit-query/src/index.ts')], tsconfig: resolve(__dirname, '../packages/lit-query/tsconfig.json'),