diff --git a/.changeset/lemon-memes-divide.md b/.changeset/lemon-memes-divide.md new file mode 100644 index 00000000000..c1f3e131b70 --- /dev/null +++ b/.changeset/lemon-memes-divide.md @@ -0,0 +1,5 @@ +--- +'@tanstack/lit-query': minor +--- + +Add initial @tanstack/lit-query package diff --git a/.gitignore b/.gitignore index 6e71fdf278c..638a5ac304f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ yarn.lock build coverage dist +dist-cjs dist-ts # misc diff --git a/docs/config.json b/docs/config.json index 13d211f7520..2b1561d691a 100644 --- a/docs/config.json +++ b/docs/config.json @@ -135,6 +135,27 @@ } ] }, + { + "label": "lit", + "children": [ + { + "label": "Overview", + "to": "framework/lit/overview" + }, + { + "label": "Installation", + "to": "framework/lit/installation" + }, + { + "label": "Quick Start", + "to": "framework/lit/quick-start" + }, + { + "label": "TypeScript", + "to": "framework/lit/typescript" + } + ] + }, { "label": "angular", "children": [ @@ -759,6 +780,47 @@ } ] }, + { + "label": "lit", + "children": [ + { + "label": "Reactive Controllers vs Hooks", + "to": "framework/lit/guides/reactive-controllers-vs-hooks" + }, + { + "label": "Queries", + "to": "framework/lit/guides/queries" + }, + { + "label": "Parallel Queries", + "to": "framework/lit/guides/parallel-queries" + }, + { + "label": "Query Keys", + "to": "framework/lit/guides/query-keys" + }, + { + "label": "Query Functions", + "to": "framework/lit/guides/query-functions" + }, + { + "label": "Mutations", + "to": "framework/lit/guides/mutations" + }, + { + "label": "Query Invalidation", + "to": "framework/lit/guides/query-invalidation" + }, + { + "label": "Infinite Queries", + "to": "framework/lit/guides/infinite-queries" + }, + { + "label": "SSR", + "to": "framework/lit/guides/ssr" + } + ] + }, { "label": "preact", "children": [ @@ -1183,6 +1245,83 @@ } ] }, + { + "label": "lit", + "children": [ + { + "label": "Lit Reference", + "to": "framework/lit/reference/index" + }, + { + "label": "QueryClientProvider", + "to": "framework/lit/reference/classes/QueryClientProvider" + }, + { + "label": "Functions / createQueryController", + "to": "framework/lit/reference/functions/createQueryController" + }, + { + "label": "Functions / createQueriesController", + "to": "framework/lit/reference/functions/createQueriesController" + }, + { + "label": "Functions / createInfiniteQueryController", + "to": "framework/lit/reference/functions/createInfiniteQueryController" + }, + { + "label": "Functions / createMutationController", + "to": "framework/lit/reference/functions/createMutationController" + }, + { + "label": "Functions / useIsFetching", + "to": "framework/lit/reference/functions/useIsFetching" + }, + { + "label": "Functions / useIsMutating", + "to": "framework/lit/reference/functions/useIsMutating" + }, + { + "label": "Functions / useMutationState", + "to": "framework/lit/reference/functions/useMutationState" + }, + { + "label": "Functions / useQueryClient", + "to": "framework/lit/reference/functions/useQueryClient" + }, + { + "label": "Functions / queryOptions", + "to": "framework/lit/reference/functions/queryOptions" + }, + { + "label": "Functions / infiniteQueryOptions", + "to": "framework/lit/reference/functions/infiniteQueryOptions" + }, + { + "label": "Functions / mutationOptions", + "to": "framework/lit/reference/functions/mutationOptions" + }, + { + "label": "Context / queryClientContext", + "to": "framework/lit/reference/variables/queryClientContext" + }, + { + "label": "Context / getDefaultQueryClient", + "to": "framework/lit/reference/functions/getDefaultQueryClient" + }, + { + "label": "Context / registerDefaultQueryClient", + "to": "framework/lit/reference/functions/registerDefaultQueryClient" + }, + { + "label": "Context / unregisterDefaultQueryClient", + "to": "framework/lit/reference/functions/unregisterDefaultQueryClient" + }, + { + "label": "Context / resolveQueryClient", + "to": "framework/lit/reference/functions/resolveQueryClient" + } + ] + }, { "label": "angular", "children": [ @@ -1532,6 +1671,23 @@ } ] }, + { + "label": "lit", + "children": [ + { + "label": "Basic", + "to": "framework/lit/examples/basic" + }, + { + "label": "Pagination", + "to": "framework/lit/examples/pagination" + }, + { + "label": "SSR", + "to": "framework/lit/examples/ssr" + } + ] + }, { "label": "angular", "children": [ diff --git a/docs/framework/lit/guides/infinite-queries.md b/docs/framework/lit/guides/infinite-queries.md new file mode 100644 index 00000000000..ab70c03178b --- /dev/null +++ b/docs/framework/lit/guides/infinite-queries.md @@ -0,0 +1,87 @@ +--- +id: infinite-queries +title: Infinite Queries +--- + +Infinite queries are for lists that load more data into one cache entry. Use [`createInfiniteQueryController`](../reference/functions/createInfiniteQueryController.md). + +An infinite query result contains: + +- `data.pages`: fetched pages +- `data.pageParams`: page parameters used for those pages +- `fetchNextPage` and `fetchPreviousPage` +- `hasNextPage` and `hasPreviousPage` +- `isFetchingNextPage` and `isFetchingPreviousPage` + +## Load More Example + +```ts +import { LitElement, html } from 'lit' +import { createInfiniteQueryController } from '@tanstack/lit-query' + +class ProjectsList extends LitElement { + private readonly projects = createInfiniteQueryController(this, { + queryKey: ['projects'], + queryFn: ({ pageParam }) => fetchProjectsPage(pageParam), + initialPageParam: 1, + getNextPageParam: (lastPage) => + lastPage.hasMore ? lastPage.page + 1 : undefined, + }) + + render() { + const query = this.projects() + + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error: ${query.error.message}` + + return html` + ${query.data.pages.map( + (page) => html` + ${page.projects.map((project) => html`

${project.name}

`)} + `, + )} + + + ` + } +} +``` + +## Page Parameters + +`initialPageParam` is required. `getNextPageParam` decides whether another page exists and what value should be passed as `pageParam` to the next query function call. + +```ts +createInfiniteQueryController(this, { + queryKey: ['projects'], + queryFn: ({ pageParam }) => fetchProjectsPage(pageParam), + initialPageParam: 1, + getNextPageParam: (lastPage) => + lastPage.hasMore ? lastPage.page + 1 : undefined, +}) +``` + +Returning `undefined` or `null` means there is no next page. + +## Avoid Overlapping Fetches + +There is one ongoing fetch for an infinite query cache entry. If you call `fetchNextPage` while a background refetch is running, you can overwrite data. Disable the button or check `!query.isFetching` before loading more: + +```ts +if (query.hasNextPage && !query.isFetching) { + this.projects.fetchNextPage() +} +``` + +## Paginated Alternative + +If your UI shows one page at a time, a normal query with a page in the key can be a better fit. The [Pagination example](../examples/pagination) uses `createQueryController`, `placeholderData: keepPreviousData`, prefetching, and mutations to demonstrate that pattern. diff --git a/docs/framework/lit/guides/mutations.md b/docs/framework/lit/guides/mutations.md new file mode 100644 index 00000000000..8e5c3fdfa42 --- /dev/null +++ b/docs/framework/lit/guides/mutations.md @@ -0,0 +1,166 @@ +--- +id: mutations +title: Mutations +--- + +Unlike queries, mutations are used to create, update, delete, or otherwise perform server side effects. In Lit, use [`createMutationController`](../reference/functions/createMutationController.md). + +```ts +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createMutationController, + createQueryController, +} from '@tanstack/lit-query' + +const queryClient = new QueryClient() + +class AppQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } +} + +customElements.define('app-query-provider', AppQueryProvider) + +class TodosView extends LitElement { + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodos, + }) + + private readonly addTodo = createMutationController(this, { + mutationFn: createTodo, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['todos'] }) + }, + }) + + render() { + const query = this.todos() + const mutation = this.addTodo() + const todos = query.data ?? [] + + return html` + ${mutation.isError ? html`

${mutation.error.message}

` : null} + ${mutation.isSuccess ? html`

Todo added

` : null} + + + + + ` + } +} + +customElements.define('todos-view', TodosView) +``` + +Render the element under the provider so the controllers can resolve the same `QueryClient` from Lit context: + +```html + + + +``` + +## Mutation States + +A mutation can be in one of these primary states: + +- `isIdle` or `status === 'idle'`: no mutation has run or it has been reset +- `isPending` or `status === 'pending'`: the mutation is running +- `isError` or `status === 'error'`: the mutation failed and `error` is available +- `isSuccess` or `status === 'success'`: the mutation finished and `data` is available + +## Variables + +Pass variables to the mutation function by calling `mutate`: + +```ts +this.addTodo.mutate({ + title: this.nextTitle, +}) +``` + +`mutate` throws synchronously if the controller cannot resolve a `QueryClient`, such as when the element is not under a connected `QueryClientProvider` and no explicit client was passed. `mutateAsync` reports the same setup problem as a rejected promise. + +Use `mutateAsync` when you want a promise: + +```ts +try { + const created = await this.addTodo.mutateAsync({ title: this.nextTitle }) + this.nextTitle = created.title +} catch (error) { + this.errorMessage = String(error) +} +``` + +## Resetting Mutation State + +The accessor includes `reset`: + +```ts +html` + ${mutation.isError + ? html`` + : null} +` +``` + +## Side Effects + +Mutation options support `onMutate`, `onError`, `onSuccess`, and `onSettled`. The pagination example passes an explicit `queryClient` to the controller and uses the same in-scope client for optimistic updates and rollback: + +```ts +private readonly favoriteMutation = createMutationController( + this, + { + mutationKey: ['toggle-project-favorite'], + mutationFn: async (input) => { + const response = await toggleProjectFavoriteOnServer(input) + return response.project + }, + onMutate: async (variables) => { + await queryClient.cancelQueries({ queryKey: ['projects'] }) + const snapshots = queryClient.getQueriesData({ + queryKey: ['projects'], + }) + + for (const [key, existing] of snapshots) { + if (!existing) continue + + queryClient.setQueryData(key, { + ...existing, + projects: existing.projects.map((project) => + project.id === variables.id + ? { ...project, isFavorite: variables.isFavorite } + : project, + ), + }) + } + + return { snapshots } + }, + onError: (_error, _variables, context) => { + for (const [key, snapshot] of context?.snapshots ?? []) { + queryClient.setQueryData(key, snapshot) + } + }, + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: ['projects'] }) + }, + }, + queryClient, +) +``` + +For the exact runnable flow, see the [Pagination example](../examples/pagination). diff --git a/docs/framework/lit/guides/parallel-queries.md b/docs/framework/lit/guides/parallel-queries.md new file mode 100644 index 00000000000..2d9bf967f7b --- /dev/null +++ b/docs/framework/lit/guides/parallel-queries.md @@ -0,0 +1,134 @@ +--- +id: parallel-queries +title: Parallel Queries +--- + +Parallel queries are queries that run at the same time so the UI does not wait for one request before starting the next. + +## Manual Parallel Queries + +When the number of queries is fixed, create multiple query controllers on the same host. They will all subscribe when the host connects. + +```ts +import { LitElement, html } from 'lit' +import { createQueryController } from '@tanstack/lit-query' + +class DashboardView extends LitElement { + private readonly users = createQueryController(this, { + queryKey: ['users'], + queryFn: fetchUsers, + }) + + private readonly teams = createQueryController(this, { + queryKey: ['teams'], + queryFn: fetchTeams, + }) + + private readonly projects = createQueryController(this, { + queryKey: ['projects'], + queryFn: fetchProjects, + }) + + render() { + const users = this.users() + const teams = this.teams() + const projects = this.projects() + + if (users.isPending || teams.isPending || projects.isPending) { + return html`Loading...` + } + + if (users.isError || teams.isError || projects.isError) { + return html`Unable to load dashboard` + } + + return html` + + ` + } +} +``` + +Each controller receives the same `ReactiveControllerHost`. If no explicit `QueryClient` is passed, each controller resolves the nearest connected `QueryClientProvider`. + +## Dynamic Parallel Queries + +When the number of queries changes with host state, use [`createQueriesController`](../reference/functions/createQueriesController.md). It accepts a `queries` array and returns an accessor for the array of query results. + +Use an options getter when the query list depends on reactive host fields: + +```ts +import { LitElement, html } from 'lit' +import { createQueriesController } from '@tanstack/lit-query' + +class UsersDetails extends LitElement { + static properties = { + userIds: { attribute: false }, + } + + userIds: Array = [] + + private readonly users = createQueriesController(this, () => ({ + queries: this.userIds.map((id) => ({ + queryKey: ['user', id], + queryFn: () => fetchUserById(id), + })), + })) + + render() { + const userQueries = this.users() + + return html` +
    + ${userQueries.map((query, index) => { + if (query.isPending) return html`
  • Loading...
  • ` + if (query.isError) return html`
  • Error loading user
  • ` + + return html`
  • ${this.userIds[index]}: ${query.data.name}
  • ` + })} +
+ ` + } +} +``` + +The order of the results matches the order of the input queries. + +## Combining Results + +Use `combine` when a component wants one derived value instead of an array of query results: + +```ts +private readonly dashboard = createQueriesController(this, { + queries: [ + { queryKey: ['stats'], queryFn: fetchStats }, + { queryKey: ['projects'], queryFn: fetchProjects }, + ], + combine: ([stats, projects]) => ({ + activeUsers: stats.data?.activeUsers ?? 0, + projects: projects.data ?? [], + isPending: stats.isPending || projects.isPending, + isError: stats.isError || projects.isError, + }), +}) +``` + +```ts +render() { + const dashboard = this.dashboard() + + if (dashboard.isPending) return html`Loading...` + if (dashboard.isError) return html`Unable to load dashboard` + + return html` +

Total projects: ${dashboard.projects.length}

+

Active users: ${dashboard.activeUsers}

+ ` +} +``` + +Having the same query key more than once in the `queries` array can cause those entries to share cached data. Deduplicate repeated keys first if each rendered row needs independent query state. diff --git a/docs/framework/lit/guides/queries.md b/docs/framework/lit/guides/queries.md new file mode 100644 index 00000000000..592cdcfd029 --- /dev/null +++ b/docs/framework/lit/guides/queries.md @@ -0,0 +1,141 @@ +--- +id: queries +title: Queries +--- + +New to Lit Query? Start with [Installation](../installation.md) and [Quick Start](../quick-start.md) before wiring query controllers into your elements. + +## Query Basics + +A query is a declarative dependency on an asynchronous source of data tied to a unique key. Use queries for reading server state. If a function creates, updates, or deletes server data, use a [mutation](./mutations.md) instead. + +In Lit, subscribe to a query with [`createQueryController`](../reference/functions/createQueryController.md): + +```ts +import { LitElement, html } from 'lit' +import { createQueryController } from '@tanstack/lit-query' + +class TodosView extends LitElement { + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodos, + }) + + render() { + const query = this.todos() + + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error: ${query.error.message}` + + return html` +
    + ${query.data.map((todo) => html`
  • ${todo.title}
  • `)} +
+ ` + } +} +``` + +The controller needs: + +- A `ReactiveControllerHost`, usually `this` inside a `LitElement` +- A unique `queryKey` +- A `queryFn` that returns a promise and throws on errors + +The returned accessor exposes the current `QueryObserverResult`. Call it in `render`, or read `.current`: + +```ts +const query = this.todos() +const sameQuery = this.todos.current +``` + +## Query States + +A query can be in one primary state at a time: + +- `isPending` or `status === 'pending'`: no data is available yet +- `isError` or `status === 'error'`: the query failed and `error` is available +- `isSuccess` or `status === 'success'`: data is available + +The result also includes `isFetching`, which can be true during the initial load or a background refetch. + +```ts +render() { + const query = this.todos() + + if (query.status === 'pending') { + return html`Loading...` + } + + if (query.status === 'error') { + return html`Error: ${query.error.message}` + } + + return html`` +} +``` + +TypeScript will narrow `query.data` after you check `pending` and `error` before reading it. + +## Fetch Status + +The `status` field describes whether data is available. The `fetchStatus` field describes what the query function is doing: + +- `fetchStatus === 'fetching'`: the query is currently fetching. +- `fetchStatus === 'paused'`: the query wanted to fetch, but fetching is paused. +- `fetchStatus === 'idle'`: the query is not fetching. + +These states are intentionally separate. Background refetching and stale-while-revalidate behavior can produce combinations like: + +- A successful query with cached data can have `status === 'success'` and `fetchStatus === 'fetching'` while a background refetch is running. +- A query with no data can have `status === 'pending'` and `fetchStatus === 'paused'` if fetching cannot start yet. + +Use `status` when deciding whether data can be rendered, and use `fetchStatus` or `isFetching` when deciding whether to show a network activity indicator: + +```ts +render() { + const query = this.todos() + + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error: ${query.error.message}` + + return html` + ${query.fetchStatus === 'fetching' + ? html`Refreshing...` + : null} + + ` +} +``` + +## Reactive Query Options + +Use an options getter when the query key or query function depends on host state: + +```ts +class UserTodos extends LitElement { + static properties = { + userId: { type: String }, + } + + userId = '' + + private readonly todos = createQueryController(this, () => ({ + queryKey: ['todos', this.userId], + queryFn: () => fetchTodos(this.userId), + enabled: this.userId.length > 0, + })) +} +``` + +The query key is used for caching, refetching, and sharing data between controllers. + +## Refetching + +The accessor includes `refetch`: + +```ts +html`` +``` + +For multiple queries that should run at the same time, see [Parallel Queries](./parallel-queries.md). diff --git a/docs/framework/lit/guides/query-functions.md b/docs/framework/lit/guides/query-functions.md new file mode 100644 index 00000000000..74845737e82 --- /dev/null +++ b/docs/framework/lit/guides/query-functions.md @@ -0,0 +1,81 @@ +--- +id: query-functions +title: Query Functions +--- + +A query function can be any function that returns a promise. The promise should resolve data or throw an error. + +```ts +createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodos, +}) + +createQueryController(this, () => ({ + queryKey: ['todo', this.todoId], + queryFn: () => fetchTodo(this.todoId), +})) +``` + +## Handling Errors + +TanStack Query needs failed query functions to throw or return a rejected promise. Some clients do that automatically. The browser `fetch` API does not, so check `response.ok` yourself: + +```ts +async function fetchTodos(): Promise { + const response = await fetch('/api/todos') + + if (!response.ok) { + throw new Error('Failed to fetch todos') + } + + return response.json() as Promise +} +``` + +The thrown error is available on the query result: + +```ts +const query = this.todos() + +if (query.isError) { + return html`Error: ${query.error.message}` +} +``` + +## Query Function Context + +TanStack Query passes a context object to every query function. It includes: + +- `queryKey`: the current query key +- `client`: the `QueryClient` +- `signal`: an `AbortSignal` for cancellation +- `meta`: optional query metadata + +```ts +createQueryController(this, { + queryKey: ['todos', { status: 'open' }], + queryFn: async ({ queryKey, signal }) => { + const [, filters] = queryKey + const response = await fetch(`/api/todos?status=${filters.status}`, { + signal, + }) + if (!response.ok) throw new Error('Failed to fetch todos') + return response.json() as Promise + }, +}) +``` + +Infinite query functions also receive `pageParam`: + +```ts +createInfiniteQueryController(this, { + queryKey: ['projects'], + queryFn: ({ pageParam }) => fetchProjectsPage(pageParam), + initialPageParam: 1, + getNextPageParam: (lastPage) => + lastPage.hasMore ? lastPage.page + 1 : undefined, +}) +``` + +See [Infinite Queries](./infinite-queries.md) for the controller-specific behavior. diff --git a/docs/framework/lit/guides/query-invalidation.md b/docs/framework/lit/guides/query-invalidation.md new file mode 100644 index 00000000000..0227469ad41 --- /dev/null +++ b/docs/framework/lit/guides/query-invalidation.md @@ -0,0 +1,83 @@ +--- +id: query-invalidation +title: Query Invalidation +--- + +Waiting for queries to become stale is not always enough. After a mutation succeeds, you often know that related cached data is out of date. Use `queryClient.invalidateQueries` to mark matching queries stale and refetch active observers. + +```ts +queryClient.invalidateQueries() + +queryClient.invalidateQueries({ + queryKey: ['todos'], +}) +``` + +When a query is invalidated: + +- It is marked stale, overriding any `staleTime` +- If a matching query is active in a controller, it can refetch in the background + +## Invalidation from Mutations + +```ts +private readonly addTodo = createMutationController(this, { + mutationFn: addTodo, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['todos'] }) + }, +}) +``` + +Use this pattern when the mutation result tells you related cached data is stale. The [Pagination example](../examples/pagination) shows invalidation after project mutations. + +## Query Matching + +Match a group of queries by prefix: + +```ts +queryClient.invalidateQueries({ queryKey: ['projects'] }) +``` + +Both of these query keys match: + +```ts +const projectsListKey = ['projects'] +const projectsPageKey = ['projects', 1, 250, false] +``` + +Use a more specific key when only one slice should be invalidated: + +```ts +queryClient.invalidateQueries({ + queryKey: ['projects', this.page], +}) +``` + +Use `exact: true` to match only the exact key: + +```ts +queryClient.invalidateQueries({ + queryKey: ['projects'], + exact: true, +}) +``` + +## Manual Cache Updates + +Invalidation is usually simpler than normalized cache updates. When you do need immediate UI updates, combine targeted cache writes with invalidation: + +```ts +queryClient.setQueryData(['todos'], (existing) => { + if (!existing) return existing + + return { + ...existing, + items: [...existing.items, createdTodo], + } +}) + +await queryClient.invalidateQueries({ queryKey: ['todos'] }) +``` + +For rollback with optimistic updates, see the mutation guide and the [Pagination example](../examples/pagination). diff --git a/docs/framework/lit/guides/query-keys.md b/docs/framework/lit/guides/query-keys.md new file mode 100644 index 00000000000..8a532e93e7e --- /dev/null +++ b/docs/framework/lit/guides/query-keys.md @@ -0,0 +1,85 @@ +--- +id: query-keys +title: Query Keys +--- + +TanStack Query manages caching by query key. Query keys must be arrays at the top level, and they should uniquely describe the data returned by the query function. + +## Simple Keys + +Use simple keys for list resources or non-hierarchical data: + +```ts +createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodos, +}) + +createQueryController(this, { + queryKey: ['settings'], + queryFn: fetchSettings, +}) +``` + +## Keys with Variables + +Include variables when they change what the query fetches: + +```ts +createQueryController(this, () => ({ + queryKey: ['todo', this.todoId], + queryFn: () => fetchTodo(this.todoId), +})) + +createQueryController(this, () => ({ + queryKey: ['projects', { page: this.page, filter: this.filter }], + queryFn: () => fetchProjects({ page: this.page, filter: this.filter }), +})) +``` + +The pagination example uses a key shaped like this: + +```ts +type ProjectsQueryKey = readonly ['projects', number, number, boolean] + +function projectsQueryKey( + page: number, + delayMs: number, + forceError: boolean, +): ProjectsQueryKey { + return ['projects', page, delayMs, forceError] as const +} +``` + +## Deterministic Hashing + +Object key order does not matter inside a query key: + +```ts +const keyA = ['todos', { status, page }] +const keyB = ['todos', { page, status }] +``` + +Array item order does matter: + +```ts +const keyA = ['todos', status, page] +const keyB = ['todos', page, status] +``` + +## Query Keys as Dependencies + +If your query function reads a reactive host property, include that value in the query key: + +```ts +class UserTodos extends LitElement { + userId = '' + + private readonly todos = createQueryController(this, () => ({ + queryKey: ['todos', this.userId], + queryFn: () => fetchTodos(this.userId), + })) +} +``` + +This lets Lit Query cache each user's todos separately and refetch when the host state changes. diff --git a/docs/framework/lit/guides/reactive-controllers-vs-hooks.md b/docs/framework/lit/guides/reactive-controllers-vs-hooks.md new file mode 100644 index 00000000000..0cb0530ed0b --- /dev/null +++ b/docs/framework/lit/guides/reactive-controllers-vs-hooks.md @@ -0,0 +1,111 @@ +--- +id: reactive-controllers-vs-hooks +title: Reactive Controllers vs Hooks +--- + +React Query examples use hooks. Lit Query uses reactive controllers. + +The job is similar: subscribe a component to a `QueryClient`, read the latest result, and update the component when the cache changes. The integration point is different because Lit components use the `ReactiveControllerHost` interface instead of React's render and hook system. + +## Mapping the Concepts + +| React Query | Lit Query | +| --------------------------- | ---------------------------------------------- | +| `useQuery(options)` | `createQueryController(this, options)` | +| `useQueries(options)` | `createQueriesController(this, options)` | +| `useMutation(options)` | `createMutationController(this, options)` | +| `useInfiniteQuery(options)` | `createInfiniteQueryController(this, options)` | +| `useIsFetching(options)` | `useIsFetching(this, options)` | +| `useIsMutating(options)` | `useIsMutating(this, options)` | +| `useMutationState(options)` | `useMutationState(this, options)` | +| Hook result object | Callable result accessor | +| React context provider | `QueryClientProvider` custom element | +| Component render rerun | `host.requestUpdate()` | + +## Host-Bound APIs + +Lit APIs that subscribe a component to query or mutation state receive a `host` as the first argument: + +```ts +class TodosView extends LitElement { + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodos, + }) +} +``` + +`this` is valid because `LitElement` implements `ReactiveControllerHost`. The controller attaches to the host, subscribes when the host connects, requests updates when the query result changes, and unsubscribes when the host disconnects. + +The host-bound APIs are [`createQueryController`](../reference/functions/createQueryController.md), [`createQueriesController`](../reference/functions/createQueriesController.md), [`createInfiniteQueryController`](../reference/functions/createInfiniteQueryController.md), [`createMutationController`](../reference/functions/createMutationController.md), [`useIsFetching`](../reference/functions/useIsFetching.md), [`useIsMutating`](../reference/functions/useIsMutating.md), and [`useMutationState`](../reference/functions/useMutationState.md). + +[`useQueryClient`](../reference/functions/useQueryClient.md) is different. It is not a reactive controller, does not accept a host, does not subscribe, and throws synchronously if no single default client is available. Use it only for imperative code that runs while exactly one `QueryClientProvider` is connected. Inside host-bound APIs, prefer the provider context or pass an explicit `QueryClient`. + +## Reading Results + +Lit Query controller creators return a callable accessor with a `current` property: + +```ts +const query = this.todos() +const sameQuery = this.todos.current +``` + +Render methods normally call the accessor: + +```ts +render() { + const query = this.todos() + + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error: ${query.error.message}` + + return html`` +} +``` + +## Reactive Options + +If query options depend on host state, pass a function. Lit Query re-reads function accessors during host updates: + +```ts +class ProjectView extends LitElement { + static properties = { + projectId: { type: Number }, + } + + projectId = 1 + + private readonly project = createQueryController(this, () => ({ + queryKey: ['project', this.projectId], + queryFn: () => fetchProject(this.projectId), + })) +} +``` + +If options are static, pass an object. If you mutate a static options object yourself, call the controller helper that causes the observer to see the new options, such as `refetch`, or prefer a function accessor for reactive state. + +## Provider Context + +Host-bound APIs can receive an explicit `QueryClient`, but most apps render under [`QueryClientProvider`](../reference/classes/QueryClientProvider.md). The provider uses Lit context to deliver the client to descendant controllers. + +```ts +customElements.define('query-client-provider', QueryClientProvider) +``` + +```ts +html` + + + +` +``` + +Custom element registration is always the application's responsibility. + +`QueryClientProvider` also registers its client in a process-local fallback store for [`useQueryClient`](../reference/functions/useQueryClient.md) and [`resolveQueryClient`](../reference/functions/resolveQueryClient.md). That fallback is intentionally conservative: + +- If no provider is connected, `useQueryClient()` throws. +- If exactly one distinct client is connected, `useQueryClient()` returns it. +- If multiple distinct clients are connected in the same JavaScript context, `useQueryClient()` and `resolveQueryClient()` throw because the fallback would be ambiguous. + +Multiple roots, micro-frontends, test suites with shared modules, and nested apps should avoid relying on the process-local fallback. Render host-bound controllers under the right provider, pass an explicit `QueryClient` to the controller, or cleanly disconnect providers between tests. diff --git a/docs/framework/lit/guides/ssr.md b/docs/framework/lit/guides/ssr.md new file mode 100644 index 00000000000..0c6cce2e476 --- /dev/null +++ b/docs/framework/lit/guides/ssr.md @@ -0,0 +1,145 @@ +--- +id: ssr +title: Server Rendering & Hydration +--- + +Lit Query can be used with server rendering by combining Lit SSR with TanStack Query Core hydration APIs re-exported from `@tanstack/lit-query`. + +The runnable source for this guide is the [SSR example](../examples/ssr). + +## Flow + +Server rendering has three phases: + +1. Create a per-request `QueryClient`. +2. Prefetch queries on the server and render Lit HTML with that client. +3. Dehydrate the cache into the HTML, then hydrate a browser `QueryClient` before the client app renders. + +Never share one server `QueryClient` between users or requests. + +## Server Prefetch and Render + +```ts +import { render } from '@lit-labs/ssr' +import { collectResult } from '@lit-labs/ssr/lib/render-result.js' +import { html } from 'lit' +import { QueryClient, dehydrate } from '@tanstack/lit-query' +import { createDataQueryOptions } from './api.js' +import './app.js' + +async function renderPage() { + const apiBaseUrl = 'https://example.com' + const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + }, + }, + }) + + await queryClient.prefetchQuery(createDataQueryOptions(apiBaseUrl)) + + const appHtml = await collectResult( + render( + html``, + ), + ) + + const dehydratedState = dehydrate(queryClient) + + return { appHtml, dehydratedState } +} +``` + +The server passes the same client into the Lit element with a property binding. This lets `createQueryController` read the prefetched cache during server render. If your query function calls `fetch` during SSR, pass an absolute API origin instead of relying on a browser-relative URL. + +## Client Hydration + +```ts +import '@lit-labs/ssr-client/lit-element-hydrate-support.js' +import { QueryClient, hydrate, type DehydratedState } from '@tanstack/lit-query' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + }, + }, +}) + +const dehydratedState = JSON.parse( + document.getElementById('__QUERY_STATE__')?.textContent ?? 'null', +) as DehydratedState + +queryClient.mount() +hydrate(queryClient, dehydratedState) + +const appElement = document.querySelector('ssr-app') as + | (HTMLElement & { queryClient?: QueryClient }) + | null + +if (!appElement) { + throw new Error('Expected the SSR app element to exist before hydration.') +} + +appElement.queryClient = queryClient +await import('./app.js') +``` + +Unmount the client when the page is unloaded if you mounted it manually: + +```ts +window.addEventListener( + 'pagehide', + () => { + queryClient.unmount() + }, + { once: true }, +) +``` + +## Component Pattern + +The SSR example creates its controller only after a `queryClient` property is available: + +```ts +import { LitElement } from 'lit' +import { + createQueryController, + type QueryClient, + type QueryResultAccessor, +} from '@tanstack/lit-query' +import { createDataQueryOptions, type DataResponse } from './api.js' + +class SsrApp extends LitElement { + static properties = { + apiBaseUrl: { attribute: 'api-base-url' }, + queryClient: { attribute: false }, + } + + apiBaseUrl = '' + queryClient?: QueryClient + private dataQuery?: QueryResultAccessor + + protected override willUpdate(): void { + if (!this.dataQuery && this.queryClient) { + this.dataQuery = createQueryController( + this, + createDataQueryOptions(this.apiBaseUrl), + this.queryClient, + ) + } + } +} +``` + +This explicit-client pattern is useful for SSR because the client is created by the renderer rather than discovered from a connected DOM provider. + +## Serialization + +Embed dehydrated state as JSON in the HTML and escape characters that can break out of a script tag. The example server uses a small serializer before replacing `__QUERY_STATE_JSON__` in the built HTML template. + +Lit Query re-exports `dehydrate` and `hydrate` from TanStack Query Core. Use `dehydrate(queryClient)` after server prefetching to capture the cache state. In the browser, parse that state, create a fresh `QueryClient`, call `hydrate(queryClient, dehydratedState)`, assign the client to the server-rendered element, and only then import the Lit component so it upgrades with the prefetched cache available. diff --git a/docs/framework/lit/installation.md b/docs/framework/lit/installation.md new file mode 100644 index 00000000000..dacbadb9e58 --- /dev/null +++ b/docs/framework/lit/installation.md @@ -0,0 +1,109 @@ +--- +id: installation +title: Installation +--- + +> IMPORTANT: The Lit adapter is currently experimental and v0.1. Pin exact versions if you need extra release stability while the API is early. + +Install Lit Query with Lit and TanStack Query Core: + +```bash +npm i @tanstack/lit-query @tanstack/query-core lit +``` + +or + +```bash +pnpm add @tanstack/lit-query @tanstack/query-core lit +``` + +or + +```bash +yarn add @tanstack/lit-query @tanstack/query-core lit +``` + +or + +```bash +bun add @tanstack/lit-query @tanstack/query-core lit +``` + +`@tanstack/query-core` is a peer dependency of `@tanstack/lit-query`. Even though the Lit docs import user-facing APIs from `@tanstack/lit-query`, your app should install `@tanstack/query-core` explicitly. + +## Requirements + +Lit Query is intended for Lit 2.8 and newer, including Lit 3. It uses Lit reactive controllers and Lit context, so query consumers should be `ReactiveControllerHost` instances such as `LitElement`. + +TanStack Query is optimized for modern browsers: + +```txt +Chrome >= 91 +Firefox >= 90 +Edge >= 91 +Safari >= 15 +iOS >= 15 +Opera >= 77 +``` + +## Provider Setup + +Create a `QueryClient`, provide it with `QueryClientProvider`, and register your custom element. The package exports the provider class but does not call `customElements.define` for you. + +### Subclass Pattern + +```ts +import { QueryClient, QueryClientProvider } from '@tanstack/lit-query' + +const queryClient = new QueryClient() + +class AppQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } +} + +customElements.define('app-query-provider', AppQueryProvider) +``` + +```html + + + +``` + +### Direct Provider Element + +You can register the provider class directly and bind its `client` property from a Lit template. The dot is important: `.client=${queryClient}` is a property binding, not an HTML attribute. + +```ts +import { LitElement, html } from 'lit' +import { QueryClient, QueryClientProvider } from '@tanstack/lit-query' + +const queryClient = new QueryClient() + +customElements.define('query-client-provider', QueryClientProvider) + +class AppRoot extends LitElement { + render() { + return html` + + + + ` + } +} +``` + +If a connected provider has no `client`, it throws. See the generated [`QueryClientProvider` reference](./reference/classes/QueryClientProvider.md) for the full contract. + +## Render Roots + +The snippets in these docs use Lit's default shadow DOM. Lit Query controllers and `QueryClientProvider` work with shadow DOM and light DOM because they use the host lifecycle and Lit context, not global selectors. + +Some runnable examples override `createRenderRoot()` and return `this` so their demo markup stays in light DOM for shared page styles and test selectors. That override is not required for Lit Query. Use light DOM only when your app has a separate reason to expose a component's internal markup to global CSS, tests, or non-shadow-DOM integration code. + +## Devtools Status + +Lit Devtools are not available yet. This is a current adapter limitation, not an installation step. diff --git a/docs/framework/lit/overview.md b/docs/framework/lit/overview.md new file mode 100644 index 00000000000..354848d6231 --- /dev/null +++ b/docs/framework/lit/overview.md @@ -0,0 +1,93 @@ +--- +id: overview +title: Overview +--- + +> IMPORTANT: The `@tanstack/lit-query` package is currently experimental and v0.1. Expect the Lit adapter API and docs to keep evolving. If you use it in production while it is early, pin the package to a patch version and upgrade deliberately. + +The `@tanstack/lit-query` package is the Lit adapter for TanStack Query. It gives Lit applications reactive controller APIs for fetching, caching, synchronizing, and updating server state. + +TanStack Query manages server state: data that is owned by a remote system, fetched asynchronously, shared across screens, and potentially changed by someone else at any time. It handles caching, request deduplication, stale data, background refetching, mutations, invalidation, pagination, and garbage collection. + +Lit Query exposes those features through [Lit reactive controllers](https://lit.dev/docs/composition/controllers/). A Lit reactive controller is attached to a `ReactiveControllerHost`, usually a `LitElement`. Lit Query controllers subscribe to the `QueryClient`, request host updates when results change, and are cleaned up with the host lifecycle. + +## Core APIs + +Most Lit applications use these APIs: + +- [`QueryClientProvider`](./reference/classes/QueryClientProvider.md) to provide a `QueryClient` through Lit context +- [`createQueryController`](./reference/functions/createQueryController.md) for queries +- [`createQueriesController`](./reference/functions/createQueriesController.md) for dynamic parallel queries +- [`createMutationController`](./reference/functions/createMutationController.md) for mutations +- [`createInfiniteQueryController`](./reference/functions/createInfiniteQueryController.md) for infinite queries +- [`useIsFetching`](./reference/functions/useIsFetching.md), [`useIsMutating`](./reference/functions/useIsMutating.md), and [`useMutationState`](./reference/functions/useMutationState.md) for cache state indicators + +The adapter also re-exports TanStack Query Core APIs from `@tanstack/lit-query`, so examples in the Lit docs use `@tanstack/lit-query` as the user-facing import path. + +## A First Query + +```ts +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createQueryController, +} from '@tanstack/lit-query' + +const queryClient = new QueryClient() + +class AppQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } +} + +customElements.define('app-query-provider', AppQueryProvider) + +class RepoStats extends LitElement { + private readonly repo = createQueryController(this, { + queryKey: ['repoData'], + queryFn: async () => { + const response = await fetch( + 'https://api.github.com/repos/TanStack/query', + ) + if (!response.ok) throw new Error('Failed to fetch repo data') + return response.json() as Promise<{ + name: string + description: string + stargazers_count: number + }> + }, + }) + + render() { + const query = this.repo() + + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error: ${query.error.message}` + + return html` +

${query.data.name}

+

${query.data.description}

+ ${query.data.stargazers_count} stars + ` + } +} + +customElements.define('repo-stats', RepoStats) +``` + +Render the provider above your query consumers: + +```html + + + +``` + +## Status Notes + +Lit Devtools are not available yet. Use the cache APIs and the generated API reference while the adapter matures. + +Start with [Installation](./installation.md), then [Quick Start](./quick-start.md), and use the [Reactive Controllers vs Hooks](./guides/reactive-controllers-vs-hooks.md) guide if you are coming from React Query. diff --git a/docs/framework/lit/quick-start.md b/docs/framework/lit/quick-start.md new file mode 100644 index 00000000000..69bff6c8f11 --- /dev/null +++ b/docs/framework/lit/quick-start.md @@ -0,0 +1,83 @@ +--- +id: quick-start +title: Quick Start +--- + +This snippet shows the three core Lit Query concepts: + +- [Queries](./guides/queries.md) +- [Mutations](./guides/mutations.md) +- [Query Invalidation](./guides/query-invalidation.md) + +For complete runnable examples, see [Basic](./examples/basic), [Pagination](./examples/pagination), and [SSR](./examples/ssr). + +```ts +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createMutationController, + createQueryController, +} from '@tanstack/lit-query' +import { addTodo, getTodos } from './api' + +const queryClient = new QueryClient() + +class AppQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } +} + +customElements.define('app-query-provider', AppQueryProvider) + +class TodosView extends LitElement { + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: getTodos, + }) + + private readonly createTodo = createMutationController(this, { + mutationFn: addTodo, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['todos'] }) + }, + }) + + render() { + const query = this.todos() + const mutation = this.createTodo() + + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error: ${query.error.message}` + + return html` +
    + ${query.data.map((todo) => html`
  • ${todo.title}
  • `)} +
+ + + ` + } +} + +customElements.define('todos-view', TodosView) +``` + +Mount the provider around your component: + +```html + + + +``` + +The controllers are created with `this` because a `LitElement` is a `ReactiveControllerHost`. Lit Query uses the host lifecycle to subscribe, request updates, and clean up when the element disconnects. + +Continue with [Reactive Controllers vs Hooks](./guides/reactive-controllers-vs-hooks.md) if you know React Query, or go straight to [Queries](./guides/queries.md). diff --git a/docs/framework/lit/reference/classes/QueryClientProvider.md b/docs/framework/lit/reference/classes/QueryClientProvider.md new file mode 100644 index 00000000000..df14c2769bb --- /dev/null +++ b/docs/framework/lit/reference/classes/QueryClientProvider.md @@ -0,0 +1,98 @@ +--- +id: QueryClientProvider +title: QueryClientProvider +--- + +# Class: QueryClientProvider + +Defined in: [packages/lit-query/src/QueryClientProvider.ts:64](https://github.com/TanStack/query/blob/main/packages/lit-query/src/QueryClientProvider.ts#L64) + +Lit element that provides a `QueryClient` to descendant Lit Query +controllers through Lit context. + +The `client` is a property, not an attribute. When rendering this element in +a Lit template, bind it with property binding: `.client=${queryClient}`. +The provider throws if it connects without a client, or if an already +connected provider has its client cleared. + +This class is not registered as a custom element by the package. Applications +must register either a subclass or the class itself with +`customElements.define`. + +## Examples + +```ts +import { html, LitElement } from 'lit' +import { QueryClient, QueryClientProvider } from '@tanstack/lit-query' + +const queryClient = new QueryClient() + +class AppQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } +} + +customElements.define('app-query-provider', AppQueryProvider) + +class AppRoot extends LitElement { + render() { + return html`` + } +} +``` + +```ts +import { html } from 'lit' +import { QueryClient, QueryClientProvider } from '@tanstack/lit-query' + +const queryClient = new QueryClient() + +customElements.define('query-client-provider', QueryClientProvider) + +const view = html` + + + +` +``` + +## Extends + +- `LitElement` + +## Constructors + +### Constructor + +```ts +new QueryClientProvider(): QueryClientProvider; +``` + +Defined in: [packages/lit-query/src/QueryClientProvider.ts:82](https://github.com/TanStack/query/blob/main/packages/lit-query/src/QueryClientProvider.ts#L82) + +#### Returns + +`QueryClientProvider` + +#### Overrides + +```ts +LitElement.constructor +``` + +## Properties + +### client + +```ts +client: QueryClient; +``` + +Defined in: [packages/lit-query/src/QueryClientProvider.ts:76](https://github.com/TanStack/query/blob/main/packages/lit-query/src/QueryClientProvider.ts#L76) + +The `QueryClient` provided to descendant controllers and global fallback +helpers while this provider is connected. + +Bind this as a property in Lit templates with `.client=${queryClient}`. diff --git a/docs/framework/lit/reference/functions/createInfiniteQueryController.md b/docs/framework/lit/reference/functions/createInfiniteQueryController.md new file mode 100644 index 00000000000..63bccc835af --- /dev/null +++ b/docs/framework/lit/reference/functions/createInfiniteQueryController.md @@ -0,0 +1,104 @@ +--- +id: createInfiniteQueryController +title: createInfiniteQueryController +--- + +# Function: createInfiniteQueryController() + +```ts +function createInfiniteQueryController( + host, + options, +queryClient?): InfiniteQueryResultAccessor; +``` + +Defined in: [packages/lit-query/src/createInfiniteQueryController.ts:364](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createInfiniteQueryController.ts#L364) + +Creates a Lit reactive controller that subscribes the host to an infinite +query. + +The returned accessor is callable and also exposes `current`, `refetch`, +`fetchNextPage`, `fetchPreviousPage`, and `destroy`. When `options` is a +function, it is re-read during host updates so query keys and options can +follow reactive host state. + +If `queryClient` is omitted, the controller resolves the client from the +nearest connected `QueryClientProvider`. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `Error` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### TPageParam + +`TPageParam` = `unknown` + +## Parameters + +### host + +`ReactiveControllerHost` + +The Lit reactive controller host that owns the infinite query +subscription. + +### options + +[`Accessor`](../type-aliases/Accessor.md)\<[`CreateInfiniteQueryOptions`](../type-aliases/CreateInfiniteQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\>\> + +Infinite query observer options, or a getter that returns +options. + +### queryClient? + +`QueryClient` + +Optional explicit query client. Provide this for +controllers that should not resolve a client from Lit context. + +## Returns + +[`InfiniteQueryResultAccessor`](../type-aliases/InfiniteQueryResultAccessor.md)\<`TData`, `TError`\> + +An accessor for the latest infinite query result with page helper +methods. + +## Example + +```ts +import { LitElement, html } from 'lit' +import { createInfiniteQueryController } from '@tanstack/lit-query' + +class ProjectsView extends LitElement { + private readonly projects = createInfiniteQueryController(this, { + queryKey: ['projects'], + queryFn: ({ pageParam }) => fetchProjects(pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextCursor, + }) + + render() { + const query = this.projects() + + return html` + + ` + } +} +``` diff --git a/docs/framework/lit/reference/functions/createMutationController.md b/docs/framework/lit/reference/functions/createMutationController.md new file mode 100644 index 00000000000..3c870e06fe2 --- /dev/null +++ b/docs/framework/lit/reference/functions/createMutationController.md @@ -0,0 +1,96 @@ +--- +id: createMutationController +title: createMutationController +--- + +# Function: createMutationController() + +```ts +function createMutationController( + host, + options, +queryClient?): MutationResultAccessor; +``` + +Defined in: [packages/lit-query/src/createMutationController.ts:338](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createMutationController.ts#L338) + +Creates a Lit reactive controller that subscribes the host to a mutation. + +The returned accessor is callable and also exposes `current`, `mutate`, +`mutateAsync`, `reset`, and `destroy`. When `options` is a function, it is +re-read during host updates so mutation options can follow reactive host +state. + +If `queryClient` is omitted, the controller resolves the client from the +nearest connected `QueryClientProvider`. + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `Error` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` + +## Parameters + +### host + +`ReactiveControllerHost` + +The Lit reactive controller host that owns the mutation +subscription. + +### options + +[`Accessor`](../type-aliases/Accessor.md)\<[`CreateMutationOptions`](../type-aliases/CreateMutationOptions.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\>\> + +Mutation observer options, or a getter that returns options. + +### queryClient? + +`QueryClient` + +Optional explicit query client. Provide this for +controllers that should not resolve a client from Lit context. + +## Returns + +[`MutationResultAccessor`](../type-aliases/MutationResultAccessor.md)\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> + +An accessor for the latest mutation result with mutation helper +methods. + +## Example + +```ts +import { LitElement, html } from 'lit' +import { createMutationController } from '@tanstack/lit-query' + +class AddTodoForm extends LitElement { + private readonly addTodo = createMutationController(this, { + mutationFn: (title: string) => + fetch('/api/todos', { method: 'POST', body: JSON.stringify({ title }) }), + }) + + render() { + const mutation = this.addTodo() + + return html` + + ` + } +} +``` diff --git a/docs/framework/lit/reference/functions/createQueriesController.md b/docs/framework/lit/reference/functions/createQueriesController.md new file mode 100644 index 00000000000..db5e74399cb --- /dev/null +++ b/docs/framework/lit/reference/functions/createQueriesController.md @@ -0,0 +1,90 @@ +--- +id: createQueriesController +title: createQueriesController +--- + +# Function: createQueriesController() + +```ts +function createQueriesController( + host, + options, +queryClient?): QueriesResultAccessor; +``` + +Defined in: [packages/lit-query/src/createQueriesController.ts:615](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueriesController.ts#L615) + +Creates a Lit reactive controller that subscribes the host to multiple +queries. + +The returned accessor is callable and also exposes `current` and `destroy`. +When `options` or `options.queries` is a function, it is re-read during host +updates so the query list can follow reactive host state. + +If `queryClient` is omitted, the controller resolves the client from the +nearest connected `QueryClientProvider`. + +## Type Parameters + +### TQueryOptions + +`TQueryOptions` *extends* `any`[] + +### TCombinedResult + +`TCombinedResult` = `CreateQueriesResults`\<`TQueryOptions`\> + +## Parameters + +### host + +`ReactiveControllerHost` + +The Lit reactive controller host that owns the queries +subscription. + +### options + +[`Accessor`](../type-aliases/Accessor.md)\<[`CreateQueriesControllerOptions`](../type-aliases/CreateQueriesControllerOptions.md)\<`TQueryOptions`, `TCombinedResult`\>\> + +Queries controller options, or a getter that returns options. + +### queryClient? + +`QueryClient` + +Optional explicit query client. Provide this for +controllers that should not resolve a client from Lit context. + +## Returns + +[`QueriesResultAccessor`](../type-aliases/QueriesResultAccessor.md)\<`TCombinedResult`\> + +An accessor for the latest query results, or the value returned by +`combine`. + +## Example + +```ts +import { LitElement, html } from 'lit' +import { createQueriesController } from '@tanstack/lit-query' + +class DashboardView extends LitElement { + private readonly dashboard = createQueriesController(this, { + queries: [ + { queryKey: ['stats'], queryFn: fetchStats }, + { queryKey: ['projects'], queryFn: fetchProjects }, + ], + combine: ([stats, projects]) => ({ + stats: stats.data, + projects: projects.data ?? [], + isPending: stats.isPending || projects.isPending, + }), + }) + + render() { + const dashboard = this.dashboard() + return html`

Projects: ${dashboard.projects.length}

` + } +} +``` diff --git a/docs/framework/lit/reference/functions/createQueryController.md b/docs/framework/lit/reference/functions/createQueryController.md new file mode 100644 index 00000000000..11e52770bbd --- /dev/null +++ b/docs/framework/lit/reference/functions/createQueryController.md @@ -0,0 +1,97 @@ +--- +id: createQueryController +title: createQueryController +--- + +# Function: createQueryController() + +```ts +function createQueryController( + host, + options, +queryClient?): QueryResultAccessor; +``` + +Defined in: [packages/lit-query/src/createQueryController.ts:319](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueryController.ts#L319) + +Creates a Lit reactive controller that subscribes the host to a single query. + +The returned accessor is callable and also exposes `current`, `refetch`, +`suspense`, and `destroy`. When `options` is a function, it is re-read during +host updates so query keys and options can follow reactive host state. + +If `queryClient` is omitted, the controller resolves the client from the +nearest connected `QueryClientProvider`. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `Error` + +### TData + +`TData` = `TQueryFnData` + +### TQueryData + +`TQueryData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +## Parameters + +### host + +`ReactiveControllerHost` + +The Lit reactive controller host that owns the query +subscription. + +### options + +[`Accessor`](../type-aliases/Accessor.md)\<[`CreateQueryOptions`](../type-aliases/CreateQueryOptions.md)\<`TQueryFnData`, `TError`, `TData`, `TQueryData`, `TQueryKey`\>\> + +Query observer options, or a getter that returns options. + +### queryClient? + +`QueryClient` + +Optional explicit query client. Provide this for +controllers that should not resolve a client from Lit context. + +## Returns + +[`QueryResultAccessor`](../type-aliases/QueryResultAccessor.md)\<`TData`, `TError`\> + +An accessor for the latest query result with query helper methods. + +## Example + +```ts +import { LitElement, html } from 'lit' +import { createQueryController } from '@tanstack/lit-query' + +class TodosView extends LitElement { + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: async () => fetch('/api/todos').then((r) => r.json()), + }) + + render() { + const query = this.todos() + + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error` + + return html`
    ${query.data.map((todo) => html`
  • ${todo.title}
  • `)}
` + } +} +``` diff --git a/docs/framework/lit/reference/functions/getDefaultQueryClient.md b/docs/framework/lit/reference/functions/getDefaultQueryClient.md new file mode 100644 index 00000000000..4c9ff19db48 --- /dev/null +++ b/docs/framework/lit/reference/functions/getDefaultQueryClient.md @@ -0,0 +1,22 @@ +--- +id: getDefaultQueryClient +title: getDefaultQueryClient +--- + +# Function: getDefaultQueryClient() + +```ts +function getDefaultQueryClient(): QueryClient | undefined; +``` + +Defined in: [packages/lit-query/src/context.ts:72](https://github.com/TanStack/query/blob/main/packages/lit-query/src/context.ts#L72) + +Returns the registered default `QueryClient`, if exactly one default client is +available. + +## Returns + +`QueryClient` \| `undefined` + +The default query client, or `undefined` when there is no registered +client or more than one registered client. diff --git a/docs/framework/lit/reference/functions/infiniteQueryOptions.md b/docs/framework/lit/reference/functions/infiniteQueryOptions.md new file mode 100644 index 00000000000..3ac26702f12 --- /dev/null +++ b/docs/framework/lit/reference/functions/infiniteQueryOptions.md @@ -0,0 +1,63 @@ +--- +id: infiniteQueryOptions +title: infiniteQueryOptions +--- + +# Function: infiniteQueryOptions() + +```ts +function infiniteQueryOptions(options): InfiniteQueryObserverOptions; +``` + +Defined in: [packages/lit-query/src/infiniteQueryOptions.ts:26](https://github.com/TanStack/query/blob/main/packages/lit-query/src/infiniteQueryOptions.ts#L26) + +Preserves and types infinite query options for reuse across Lit Query APIs. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `Error` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`, `unknown`\> + +### TQueryKey + +`TQueryKey` *extends* readonly `unknown`[] = readonly `unknown`[] + +### TPageParam + +`TPageParam` = `unknown` + +## Parameters + +### options + +`InfiniteQueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +Infinite query options to preserve. + +## Returns + +`InfiniteQueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryKey`, `TPageParam`\> + +The same options object. + +## Example + +```ts +import { infiniteQueryOptions } from '@tanstack/lit-query' + +const projectsOptions = infiniteQueryOptions({ + queryKey: ['projects'], + queryFn: ({ pageParam }) => fetchProjects(pageParam), + initialPageParam: 0, + getNextPageParam: (lastPage) => lastPage.nextCursor, +}) +``` diff --git a/docs/framework/lit/reference/functions/mutationOptions.md b/docs/framework/lit/reference/functions/mutationOptions.md new file mode 100644 index 00000000000..966fb849098 --- /dev/null +++ b/docs/framework/lit/reference/functions/mutationOptions.md @@ -0,0 +1,57 @@ +--- +id: mutationOptions +title: mutationOptions +--- + +# Function: mutationOptions() + +```ts +function mutationOptions(options): MutationObserverOptions; +``` + +Defined in: [packages/lit-query/src/mutationOptions.ts:22](https://github.com/TanStack/query/blob/main/packages/lit-query/src/mutationOptions.ts#L22) + +Preserves and types mutation options for reuse across Lit Query APIs. + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `Error` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` + +## Parameters + +### options + +`MutationObserverOptions`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> + +Mutation options to preserve. + +## Returns + +`MutationObserverOptions`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> + +The same options object. + +## Example + +```ts +import { mutationOptions } from '@tanstack/lit-query' + +const addTodoOptions = mutationOptions({ + mutationKey: ['add-todo'], + mutationFn: (title: string) => addTodo(title), +}) +``` diff --git a/docs/framework/lit/reference/functions/queryOptions.md b/docs/framework/lit/reference/functions/queryOptions.md new file mode 100644 index 00000000000..2c43233ca26 --- /dev/null +++ b/docs/framework/lit/reference/functions/queryOptions.md @@ -0,0 +1,147 @@ +--- +id: queryOptions +title: queryOptions +--- + +# Function: queryOptions() + +## Call Signature + +```ts +function queryOptions(options): Omit, "queryFn"> & object & object; +``` + +Defined in: [packages/lit-query/src/queryOptions.ts:94](https://github.com/TanStack/query/blob/main/packages/lit-query/src/queryOptions.ts#L94) + +Brands query options so the `queryKey` carries the query function data and +error types across TanStack Query APIs. + +### 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`\> + +Query options to preserve and brand. + +### Returns + +`Omit`\<`QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`, `never`\>, `"queryFn"`\> & `object` & `object` + +The same options object with a typed `queryKey`. + +### Example + +```ts +import { queryOptions } from '@tanstack/lit-query' + +const todosOptions = queryOptions({ + queryKey: ['todos'], + queryFn: fetchTodos, + initialData: [], +}) +``` + +## Call Signature + +```ts +function queryOptions(options): OmitKeyof, "queryFn"> & object & object; +``` + +Defined in: [packages/lit-query/src/queryOptions.ts:112](https://github.com/TanStack/query/blob/main/packages/lit-query/src/queryOptions.ts#L112) + +Brands query options so the `queryKey` carries the query function data and +error types across TanStack Query APIs. + +### 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`\> + +Query options to preserve and brand. + +### Returns + +`OmitKeyof`\<`QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`, `never`\>, `"queryFn"`\> & `object` & `object` + +The same options object with a typed `queryKey`. + +## Call Signature + +```ts +function queryOptions(options): QueryObserverOptions & object & object; +``` + +Defined in: [packages/lit-query/src/queryOptions.ts:130](https://github.com/TanStack/query/blob/main/packages/lit-query/src/queryOptions.ts#L130) + +Brands query options so the `queryKey` carries the query function data and +error types across TanStack Query APIs. + +### 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`\> + +Query options to preserve and brand. + +### Returns + +`QueryObserverOptions`\<`TQueryFnData`, `TError`, `TData`, `TQueryFnData`, `TQueryKey`, `never`\> & `object` & `object` + +The same options object with a typed `queryKey`. diff --git a/docs/framework/lit/reference/functions/registerDefaultQueryClient.md b/docs/framework/lit/reference/functions/registerDefaultQueryClient.md new file mode 100644 index 00000000000..d3174f600ca --- /dev/null +++ b/docs/framework/lit/reference/functions/registerDefaultQueryClient.md @@ -0,0 +1,30 @@ +--- +id: registerDefaultQueryClient +title: registerDefaultQueryClient +--- + +# Function: registerDefaultQueryClient() + +```ts +function registerDefaultQueryClient(client): void; +``` + +Defined in: [packages/lit-query/src/context.ts:32](https://github.com/TanStack/query/blob/main/packages/lit-query/src/context.ts#L32) + +Registers a `QueryClient` as a process-local fallback for APIs that resolve a +client without an explicit argument. + +`QueryClientProvider` calls this automatically while it is connected. Prefer +passing an explicit client or rendering under a provider when possible. + +## Parameters + +### client + +`QueryClient` + +The query client to register as the current default. + +## Returns + +`void` diff --git a/docs/framework/lit/reference/functions/resolveQueryClient.md b/docs/framework/lit/reference/functions/resolveQueryClient.md new file mode 100644 index 00000000000..cfb0ba226ac --- /dev/null +++ b/docs/framework/lit/reference/functions/resolveQueryClient.md @@ -0,0 +1,29 @@ +--- +id: resolveQueryClient +title: resolveQueryClient +--- + +# Function: resolveQueryClient() + +```ts +function resolveQueryClient(explicit?): QueryClient; +``` + +Defined in: [packages/lit-query/src/context.ts:118](https://github.com/TanStack/query/blob/main/packages/lit-query/src/context.ts#L118) + +Resolves an explicit `QueryClient` or falls back to `useQueryClient`. + +## Parameters + +### explicit? + +`QueryClient` + +Optional client supplied by the caller. + +## Returns + +`QueryClient` + +The explicit client when provided, otherwise the current default +client. diff --git a/docs/framework/lit/reference/functions/unregisterDefaultQueryClient.md b/docs/framework/lit/reference/functions/unregisterDefaultQueryClient.md new file mode 100644 index 00000000000..8c82f497c35 --- /dev/null +++ b/docs/framework/lit/reference/functions/unregisterDefaultQueryClient.md @@ -0,0 +1,29 @@ +--- +id: unregisterDefaultQueryClient +title: unregisterDefaultQueryClient +--- + +# Function: unregisterDefaultQueryClient() + +```ts +function unregisterDefaultQueryClient(client): void; +``` + +Defined in: [packages/lit-query/src/context.ts:45](https://github.com/TanStack/query/blob/main/packages/lit-query/src/context.ts#L45) + +Unregisters a client previously registered with +`registerDefaultQueryClient`. + +`QueryClientProvider` calls this automatically when it disconnects. + +## Parameters + +### client + +`QueryClient` + +The query client registration to release. + +## Returns + +`void` diff --git a/docs/framework/lit/reference/functions/useIsFetching.md b/docs/framework/lit/reference/functions/useIsFetching.md new file mode 100644 index 00000000000..6cfd72bb4e5 --- /dev/null +++ b/docs/framework/lit/reference/functions/useIsFetching.md @@ -0,0 +1,67 @@ +--- +id: useIsFetching +title: useIsFetching +--- + +# Function: useIsFetching() + +```ts +function useIsFetching( + host, + filters, + queryClient?): IsFetchingAccessor; +``` + +Defined in: [packages/lit-query/src/useIsFetching.ts:147](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useIsFetching.ts#L147) + +Creates a Lit reactive controller that tracks how many matching queries are +currently fetching. + +When `filters` is a function, it is re-read during host updates so the count +can follow reactive host state. If `queryClient` is omitted, the controller +resolves the client from the nearest connected `QueryClientProvider`. + +## Parameters + +### host + +`ReactiveControllerHost` + +The Lit reactive controller host that owns the cache +subscription. + +### filters + +[`Accessor`](../type-aliases/Accessor.md)\<`QueryFilters`\\> = `{}` + +Query filters, or a getter that returns query filters. + +### queryClient? + +`QueryClient` + +Optional explicit query client. Provide this for +controllers that should not resolve a client from Lit context. + +## Returns + +[`IsFetchingAccessor`](../type-aliases/IsFetchingAccessor.md) + +An accessor for the current number of matching fetching queries. + +## Example + +```ts +import { LitElement, html } from 'lit' +import { useIsFetching } from '@tanstack/lit-query' + +class TodosStatus extends LitElement { + private readonly todosFetching = useIsFetching(this, { + queryKey: ['todos'], + }) + + render() { + return html`${this.todosFetching()} active todo fetches` + } +} +``` diff --git a/docs/framework/lit/reference/functions/useIsMutating.md b/docs/framework/lit/reference/functions/useIsMutating.md new file mode 100644 index 00000000000..1991ee25266 --- /dev/null +++ b/docs/framework/lit/reference/functions/useIsMutating.md @@ -0,0 +1,67 @@ +--- +id: useIsMutating +title: useIsMutating +--- + +# Function: useIsMutating() + +```ts +function useIsMutating( + host, + filters, + queryClient?): IsMutatingAccessor; +``` + +Defined in: [packages/lit-query/src/useIsMutating.ts:147](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useIsMutating.ts#L147) + +Creates a Lit reactive controller that tracks how many matching mutations are +currently pending. + +When `filters` is a function, it is re-read during host updates so the count +can follow reactive host state. If `queryClient` is omitted, the controller +resolves the client from the nearest connected `QueryClientProvider`. + +## Parameters + +### host + +`ReactiveControllerHost` + +The Lit reactive controller host that owns the cache +subscription. + +### filters + +[`Accessor`](../type-aliases/Accessor.md)\<`MutationFilters`\<`unknown`, `Error`, `unknown`, `unknown`\>\> = `{}` + +Mutation filters, or a getter that returns mutation filters. + +### queryClient? + +`QueryClient` + +Optional explicit query client. Provide this for +controllers that should not resolve a client from Lit context. + +## Returns + +[`IsMutatingAccessor`](../type-aliases/IsMutatingAccessor.md) + +An accessor for the current number of matching pending mutations. + +## Example + +```ts +import { LitElement, html } from 'lit' +import { useIsMutating } from '@tanstack/lit-query' + +class MutationStatus extends LitElement { + private readonly savesPending = useIsMutating(this, { + mutationKey: ['save-project'], + }) + + render() { + return html`${this.savesPending()} saves pending` + } +} +``` diff --git a/docs/framework/lit/reference/functions/useMutationState.md b/docs/framework/lit/reference/functions/useMutationState.md new file mode 100644 index 00000000000..f12f55d38b2 --- /dev/null +++ b/docs/framework/lit/reference/functions/useMutationState.md @@ -0,0 +1,75 @@ +--- +id: useMutationState +title: useMutationState +--- + +# Function: useMutationState() + +```ts +function useMutationState( + host, + options, +queryClient?): MutationStateAccessor; +``` + +Defined in: [packages/lit-query/src/useMutationState.ts:187](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useMutationState.ts#L187) + +Creates a Lit reactive controller that selects state from matching mutations +in the mutation cache. + +When `options.filters` is a function, it is re-read during host updates so +the selection can follow reactive host state. If `queryClient` is omitted, +the controller resolves the client from the nearest connected +`QueryClientProvider`. + +## Type Parameters + +### TResult + +`TResult` = `MutationState`\<`unknown`, `unknown`, `unknown`, `unknown`\> + +## Parameters + +### host + +`ReactiveControllerHost` + +The Lit reactive controller host that owns the mutation cache +subscription. + +### options + +[`MutationStateOptions`](../type-aliases/MutationStateOptions.md)\<`TResult`\> = `{}` + +Mutation state filters and optional selector. + +### queryClient? + +`QueryClient` + +Optional explicit query client. Provide this for +controllers that should not resolve a client from Lit context. + +## Returns + +[`MutationStateAccessor`](../type-aliases/MutationStateAccessor.md)\<`TResult`\> + +An accessor for the selected mutation state array. + +## Example + +```ts +import { LitElement, html } from 'lit' +import { useMutationState } from '@tanstack/lit-query' + +class PendingUploads extends LitElement { + private readonly uploads = useMutationState(this, { + filters: { mutationKey: ['upload'], status: 'pending' }, + select: (mutation) => mutation.state.variables as File, + }) + + render() { + return html`${this.uploads().length} uploads pending` + } +} +``` diff --git a/docs/framework/lit/reference/functions/useQueryClient.md b/docs/framework/lit/reference/functions/useQueryClient.md new file mode 100644 index 00000000000..72db893d0d5 --- /dev/null +++ b/docs/framework/lit/reference/functions/useQueryClient.md @@ -0,0 +1,25 @@ +--- +id: useQueryClient +title: useQueryClient +--- + +# Function: useQueryClient() + +```ts +function useQueryClient(): QueryClient; +``` + +Defined in: [packages/lit-query/src/context.ts:98](https://github.com/TanStack/query/blob/main/packages/lit-query/src/context.ts#L98) + +Resolves the current default `QueryClient` registered by a connected +`QueryClientProvider`. + +This helper is useful outside a Lit reactive controller when a single +provider is mounted. It throws if no client is registered or if multiple +clients are mounted and the default would be ambiguous. + +## Returns + +`QueryClient` + +The single registered query client. diff --git a/docs/framework/lit/reference/index.md b/docs/framework/lit/reference/index.md new file mode 100644 index 00000000000..77ef5851377 --- /dev/null +++ b/docs/framework/lit/reference/index.md @@ -0,0 +1,59 @@ +--- +id: "@tanstack/lit-query" +title: "@tanstack/lit-query" +--- + +# @tanstack/lit-query + +## Classes + +- [QueryClientProvider](classes/QueryClientProvider.md) + +## Type Aliases + +- [Accessor](type-aliases/Accessor.md) +- [CreateInfiniteQueryOptions](type-aliases/CreateInfiniteQueryOptions.md) +- [CreateMutationOptions](type-aliases/CreateMutationOptions.md) +- [CreateQueriesControllerOptions](type-aliases/CreateQueriesControllerOptions.md) +- [CreateQueriesInput](type-aliases/CreateQueriesInput.md) +- [CreateQueryOptions](type-aliases/CreateQueryOptions.md) +- [DefinedInitialDataOptions](type-aliases/DefinedInitialDataOptions.md) +- [InfiniteQueryControllerOptions](type-aliases/InfiniteQueryControllerOptions.md) +- [InfiniteQueryResultAccessor](type-aliases/InfiniteQueryResultAccessor.md) +- [IsFetchingAccessor](type-aliases/IsFetchingAccessor.md) +- [IsMutatingAccessor](type-aliases/IsMutatingAccessor.md) +- [MutationControllerOptions](type-aliases/MutationControllerOptions.md) +- [MutationControllerResult](type-aliases/MutationControllerResult.md) +- [MutationResultAccessor](type-aliases/MutationResultAccessor.md) +- [MutationStateAccessor](type-aliases/MutationStateAccessor.md) +- [MutationStateOptions](type-aliases/MutationStateOptions.md) +- [QueriesControllerOptions](type-aliases/QueriesControllerOptions.md) +- [QueriesResultAccessor](type-aliases/QueriesResultAccessor.md) +- [QueryControllerOptions](type-aliases/QueryControllerOptions.md) +- [QueryControllerResult](type-aliases/QueryControllerResult.md) +- [QueryResultAccessor](type-aliases/QueryResultAccessor.md) +- [UndefinedInitialDataOptions](type-aliases/UndefinedInitialDataOptions.md) +- [UnusedSkipTokenOptions](type-aliases/UnusedSkipTokenOptions.md) +- [ValueAccessor](type-aliases/ValueAccessor.md) + +## Variables + +- [queryClientContext](variables/queryClientContext.md) + +## Functions + +- [createInfiniteQueryController](functions/createInfiniteQueryController.md) +- [createMutationController](functions/createMutationController.md) +- [createQueriesController](functions/createQueriesController.md) +- [createQueryController](functions/createQueryController.md) +- [getDefaultQueryClient](functions/getDefaultQueryClient.md) +- [infiniteQueryOptions](functions/infiniteQueryOptions.md) +- [mutationOptions](functions/mutationOptions.md) +- [queryOptions](functions/queryOptions.md) +- [registerDefaultQueryClient](functions/registerDefaultQueryClient.md) +- [resolveQueryClient](functions/resolveQueryClient.md) +- [unregisterDefaultQueryClient](functions/unregisterDefaultQueryClient.md) +- [useIsFetching](functions/useIsFetching.md) +- [useIsMutating](functions/useIsMutating.md) +- [useMutationState](functions/useMutationState.md) +- [useQueryClient](functions/useQueryClient.md) diff --git a/docs/framework/lit/reference/type-aliases/Accessor.md b/docs/framework/lit/reference/type-aliases/Accessor.md new file mode 100644 index 00000000000..95d77dee47a --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/Accessor.md @@ -0,0 +1,30 @@ +--- +id: Accessor +title: Accessor +--- + +# Type Alias: Accessor\ + +```ts +type Accessor = T | () => T; +``` + +Defined in: [packages/lit-query/src/accessor.ts:13](https://github.com/TanStack/query/blob/main/packages/lit-query/src/accessor.ts#L13) + +A value that can be passed directly or read from a zero-argument getter. + +Lit Query APIs read function accessors during host updates, so the getter can +depend on reactive host state. + +## Type Parameters + +### T + +`T` + +## Example + +```ts +const staticKey: Accessor = ['todos'] +const reactiveKey: Accessor = () => ['todos', this.userId] +``` diff --git a/docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md b/docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md new file mode 100644 index 00000000000..ba09bb1126c --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/CreateInfiniteQueryOptions.md @@ -0,0 +1,39 @@ +--- +id: CreateInfiniteQueryOptions +title: CreateInfiniteQueryOptions +--- + +# Type Alias: CreateInfiniteQueryOptions\ + +```ts +type CreateInfiniteQueryOptions = InfiniteQueryObserverOptions; +``` + +Defined in: [packages/lit-query/src/createInfiniteQueryController.ts:27](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createInfiniteQueryController.ts#L27) + +Options accepted by `createInfiniteQueryController`. + +This is the Lit adapter shape for `InfiniteQueryObserverOptions`. Pass it +directly or through an `Accessor` when the options depend on Lit host state. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`\> + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +### TPageParam + +`TPageParam` = `unknown` diff --git a/docs/framework/lit/reference/type-aliases/CreateMutationOptions.md b/docs/framework/lit/reference/type-aliases/CreateMutationOptions.md new file mode 100644 index 00000000000..94d611682b5 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/CreateMutationOptions.md @@ -0,0 +1,35 @@ +--- +id: CreateMutationOptions +title: CreateMutationOptions +--- + +# Type Alias: CreateMutationOptions\ + +```ts +type CreateMutationOptions = MutationObserverOptions; +``` + +Defined in: [packages/lit-query/src/createMutationController.ts:25](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createMutationController.ts#L25) + +Options accepted by `createMutationController`. + +This is the Lit adapter shape for `MutationObserverOptions`. Pass it directly +or through an `Accessor` when the options depend on Lit host state. + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` diff --git a/docs/framework/lit/reference/type-aliases/CreateQueriesControllerOptions.md b/docs/framework/lit/reference/type-aliases/CreateQueriesControllerOptions.md new file mode 100644 index 00000000000..1f69e15b84a --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/CreateQueriesControllerOptions.md @@ -0,0 +1,64 @@ +--- +id: CreateQueriesControllerOptions +title: CreateQueriesControllerOptions +--- + +# Type Alias: CreateQueriesControllerOptions\ + +```ts +type CreateQueriesControllerOptions = object; +``` + +Defined in: [packages/lit-query/src/createQueriesController.ts:194](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueriesController.ts#L194) + +Options accepted by `createQueriesController`. + +`queries` can be a static list or a getter that returns the current list. +`combine` can reshape the array of query results into a single value for the +returned accessor. + +## Type Parameters + +### TQueryOptions + +`TQueryOptions` *extends* `any`[] = `any`[] + +### TCombinedResult + +`TCombinedResult` = `CreateQueriesResults`\<`TQueryOptions`\> + +## Properties + +### combine()? + +```ts +optional combine: (result) => TCombinedResult; +``` + +Defined in: [packages/lit-query/src/createQueriesController.ts:208](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueriesController.ts#L208) + +Optional function that combines the query result array into one value. + +#### Parameters + +##### result + +`CreateQueriesResults`\<`TQueryOptions`\> + +#### Returns + +`TCombinedResult` + +*** + +### queries + +```ts +queries: Accessor< + | readonly [...CreateQueriesOptions] +| readonly [...{ [K in keyof TQueryOptions]: GetCreateQueriesInput }]>; +``` + +Defined in: [packages/lit-query/src/createQueriesController.ts:199](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueriesController.ts#L199) + +Query options to observe, or a getter that returns the current options. diff --git a/docs/framework/lit/reference/type-aliases/CreateQueriesInput.md b/docs/framework/lit/reference/type-aliases/CreateQueriesInput.md new file mode 100644 index 00000000000..0f698f79617 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/CreateQueriesInput.md @@ -0,0 +1,35 @@ +--- +id: CreateQueriesInput +title: CreateQueriesInput +--- + +# Type Alias: CreateQueriesInput\ + +```ts +type CreateQueriesInput = QueryObserverOptions; +``` + +Defined in: [packages/lit-query/src/createQueriesController.ts:30](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueriesController.ts#L30) + +Options for one query inside `createQueriesController`. + +This mirrors `QueryObserverOptions` and is used by the tuple inference that +maps each input query to its corresponding result. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/lit/reference/type-aliases/CreateQueryOptions.md b/docs/framework/lit/reference/type-aliases/CreateQueryOptions.md new file mode 100644 index 00000000000..a967e779ac8 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/CreateQueryOptions.md @@ -0,0 +1,40 @@ +--- +id: CreateQueryOptions +title: CreateQueryOptions +--- + +# Type Alias: CreateQueryOptions\ + +```ts +type CreateQueryOptions = QueryObserverOptions; +``` + +Defined in: [packages/lit-query/src/createQueryController.ts:27](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueryController.ts#L27) + +Options accepted by `createQueryController`. + +This is the Lit adapter shape for `QueryObserverOptions`. It can be passed +directly to `createQueryController`, or wrapped in an `Accessor` when the +options depend on Lit host state. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryData + +`TQueryData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/lit/reference/type-aliases/DefinedInitialDataOptions.md b/docs/framework/lit/reference/type-aliases/DefinedInitialDataOptions.md new file mode 100644 index 00000000000..b369207dbb3 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/DefinedInitialDataOptions.md @@ -0,0 +1,48 @@ +--- +id: DefinedInitialDataOptions +title: DefinedInitialDataOptions +--- + +# Type Alias: DefinedInitialDataOptions\ + +```ts +type DefinedInitialDataOptions = Omit, "queryFn"> & object; +``` + +Defined in: [packages/lit-query/src/queryOptions.ts:16](https://github.com/TanStack/query/blob/main/packages/lit-query/src/queryOptions.ts#L16) + +Query options with `initialData` that guarantees defined query data. + +## 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/lit/reference/type-aliases/InfiniteQueryControllerOptions.md b/docs/framework/lit/reference/type-aliases/InfiniteQueryControllerOptions.md new file mode 100644 index 00000000000..f2b6bbe0b58 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/InfiniteQueryControllerOptions.md @@ -0,0 +1,36 @@ +--- +id: InfiniteQueryControllerOptions +title: InfiniteQueryControllerOptions +--- + +# Type Alias: InfiniteQueryControllerOptions\ + +```ts +type InfiniteQueryControllerOptions = Accessor>; +``` + +Defined in: [packages/lit-query/src/types.ts:41](https://github.com/TanStack/query/blob/main/packages/lit-query/src/types.ts#L41) + +Accessor-wrapped options accepted by `createInfiniteQueryController`. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `InfiniteData`\<`TQueryFnData`\> + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` + +### TPageParam + +`TPageParam` = `unknown` diff --git a/docs/framework/lit/reference/type-aliases/InfiniteQueryResultAccessor.md b/docs/framework/lit/reference/type-aliases/InfiniteQueryResultAccessor.md new file mode 100644 index 00000000000..a45900de25f --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/InfiniteQueryResultAccessor.md @@ -0,0 +1,66 @@ +--- +id: InfiniteQueryResultAccessor +title: InfiniteQueryResultAccessor +--- + +# Type Alias: InfiniteQueryResultAccessor\ + +```ts +type InfiniteQueryResultAccessor = ValueAccessor> & object; +``` + +Defined in: [packages/lit-query/src/createInfiniteQueryController.ts:48](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createInfiniteQueryController.ts#L48) + +Accessor returned by `createInfiniteQueryController`. + +Call the accessor or read its `current` property to get the latest infinite +query result. The attached methods delegate to the active infinite query +observer. + +## Type Declaration + +### destroy() + +```ts +destroy: () => void; +``` + +Removes the controller from its Lit host and unsubscribes observers. + +#### Returns + +`void` + +### fetchNextPage + +```ts +fetchNextPage: InfiniteQueryObserverResult["fetchNextPage"]; +``` + +Fetches the next page for the current infinite query. + +### fetchPreviousPage + +```ts +fetchPreviousPage: InfiniteQueryObserverResult["fetchPreviousPage"]; +``` + +Fetches the previous page for the current infinite query. + +### refetch + +```ts +refetch: InfiniteQueryObserverResult["refetch"]; +``` + +Refetches the current infinite query. + +## Type Parameters + +### TData + +`TData` + +### TError + +`TError` diff --git a/docs/framework/lit/reference/type-aliases/IsFetchingAccessor.md b/docs/framework/lit/reference/type-aliases/IsFetchingAccessor.md new file mode 100644 index 00000000000..99c09c43502 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/IsFetchingAccessor.md @@ -0,0 +1,29 @@ +--- +id: IsFetchingAccessor +title: IsFetchingAccessor +--- + +# Type Alias: IsFetchingAccessor + +```ts +type IsFetchingAccessor = ValueAccessor & object; +``` + +Defined in: [packages/lit-query/src/useIsFetching.ts:17](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useIsFetching.ts#L17) + +Accessor returned by `useIsFetching`. + +Call the accessor or read its `current` property to get the number of +currently fetching queries that match the filters. + +## Type Declaration + +### destroy() + +```ts +destroy: () => void; +``` + +#### Returns + +`void` diff --git a/docs/framework/lit/reference/type-aliases/IsMutatingAccessor.md b/docs/framework/lit/reference/type-aliases/IsMutatingAccessor.md new file mode 100644 index 00000000000..1f0ff1194f7 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/IsMutatingAccessor.md @@ -0,0 +1,29 @@ +--- +id: IsMutatingAccessor +title: IsMutatingAccessor +--- + +# Type Alias: IsMutatingAccessor + +```ts +type IsMutatingAccessor = ValueAccessor & object; +``` + +Defined in: [packages/lit-query/src/useIsMutating.ts:17](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useIsMutating.ts#L17) + +Accessor returned by `useIsMutating`. + +Call the accessor or read its `current` property to get the number of +currently pending mutations that match the filters. + +## Type Declaration + +### destroy() + +```ts +destroy: () => void; +``` + +#### Returns + +`void` diff --git a/docs/framework/lit/reference/type-aliases/MutationControllerOptions.md b/docs/framework/lit/reference/type-aliases/MutationControllerOptions.md new file mode 100644 index 00000000000..6482d82a13a --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/MutationControllerOptions.md @@ -0,0 +1,32 @@ +--- +id: MutationControllerOptions +title: MutationControllerOptions +--- + +# Type Alias: MutationControllerOptions\ + +```ts +type MutationControllerOptions = Accessor>; +``` + +Defined in: [packages/lit-query/src/types.ts:54](https://github.com/TanStack/query/blob/main/packages/lit-query/src/types.ts#L54) + +Accessor-wrapped options accepted by `createMutationController`. + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` diff --git a/docs/framework/lit/reference/type-aliases/MutationControllerResult.md b/docs/framework/lit/reference/type-aliases/MutationControllerResult.md new file mode 100644 index 00000000000..8b63b098fba --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/MutationControllerResult.md @@ -0,0 +1,32 @@ +--- +id: MutationControllerResult +title: MutationControllerResult +--- + +# Type Alias: MutationControllerResult\ + +```ts +type MutationControllerResult = MutationObserverResult; +``` + +Defined in: [packages/lit-query/src/types.ts:64](https://github.com/TanStack/query/blob/main/packages/lit-query/src/types.ts#L64) + +Result object produced by a Lit mutation controller. + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TVariables + +`TVariables` = `void` + +### TOnMutateResult + +`TOnMutateResult` = `unknown` diff --git a/docs/framework/lit/reference/type-aliases/MutationResultAccessor.md b/docs/framework/lit/reference/type-aliases/MutationResultAccessor.md new file mode 100644 index 00000000000..6846aec42aa --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/MutationResultAccessor.md @@ -0,0 +1,91 @@ +--- +id: MutationResultAccessor +title: MutationResultAccessor +--- + +# Type Alias: MutationResultAccessor\ + +```ts +type MutationResultAccessor = ValueAccessor> & object; +``` + +Defined in: [packages/lit-query/src/createMutationController.ts:38](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createMutationController.ts#L38) + +Accessor returned by `createMutationController`. + +Call the accessor or read its `current` property to get the latest mutation +result. The attached methods delegate to the active mutation observer. + +## Type Declaration + +### destroy() + +```ts +destroy: () => void; +``` + +Removes the controller from its Lit host and unsubscribes observers. + +#### Returns + +`void` + +### mutate() + +```ts +mutate: (variables, options?) => void; +``` + +Starts the mutation and swallows the returned promise. + +Throws synchronously if no `QueryClient` can be resolved. + +#### Parameters + +##### variables + +`TVariables` + +##### options? + +`MutateOptions`\<`TData`, `TError`, `TVariables`, `TOnMutateResult`\> + +#### Returns + +`void` + +### mutateAsync + +```ts +mutateAsync: MutationObserverResult["mutate"]; +``` + +Starts the mutation and returns the observer promise. + +Rejects if no `QueryClient` can be resolved. + +### reset + +```ts +reset: MutationObserverResult["reset"]; +``` + +Resets the mutation observer to its idle state. + +## Type Parameters + +### TData + +`TData` + +### TError + +`TError` + +### TVariables + +`TVariables` + +### TOnMutateResult + +`TOnMutateResult` diff --git a/docs/framework/lit/reference/type-aliases/MutationStateAccessor.md b/docs/framework/lit/reference/type-aliases/MutationStateAccessor.md new file mode 100644 index 00000000000..6c9f9298caa --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/MutationStateAccessor.md @@ -0,0 +1,37 @@ +--- +id: MutationStateAccessor +title: MutationStateAccessor +--- + +# Type Alias: MutationStateAccessor\ + +```ts +type MutationStateAccessor = ValueAccessor & object; +``` + +Defined in: [packages/lit-query/src/useMutationState.ts:32](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useMutationState.ts#L32) + +Accessor returned by `useMutationState`. + +Call the accessor or read its `current` property to get the selected state for +matching mutations. + +## Type Declaration + +### destroy() + +```ts +destroy: () => void; +``` + +Removes the controller from its Lit host and unsubscribes observers. + +#### Returns + +`void` + +## Type Parameters + +### TResult + +`TResult` diff --git a/docs/framework/lit/reference/type-aliases/MutationStateOptions.md b/docs/framework/lit/reference/type-aliases/MutationStateOptions.md new file mode 100644 index 00000000000..7ae3228ea66 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/MutationStateOptions.md @@ -0,0 +1,54 @@ +--- +id: MutationStateOptions +title: MutationStateOptions +--- + +# Type Alias: MutationStateOptions\ + +```ts +type MutationStateOptions = object; +``` + +Defined in: [packages/lit-query/src/useMutationState.ts:19](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useMutationState.ts#L19) + +Options accepted by `useMutationState`. + +## Type Parameters + +### TResult + +`TResult` + +## Properties + +### filters? + +```ts +optional filters: Accessor; +``` + +Defined in: [packages/lit-query/src/useMutationState.ts:21](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useMutationState.ts#L21) + +Filters used to select mutations from the mutation cache. + +*** + +### select()? + +```ts +optional select: (mutation) => TResult; +``` + +Defined in: [packages/lit-query/src/useMutationState.ts:23](https://github.com/TanStack/query/blob/main/packages/lit-query/src/useMutationState.ts#L23) + +Maps each matching mutation to the value returned by the accessor. + +#### Parameters + +##### mutation + +`Mutation` + +#### Returns + +`TResult` diff --git a/docs/framework/lit/reference/type-aliases/QueriesControllerOptions.md b/docs/framework/lit/reference/type-aliases/QueriesControllerOptions.md new file mode 100644 index 00000000000..70ef5a77f2b --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/QueriesControllerOptions.md @@ -0,0 +1,24 @@ +--- +id: QueriesControllerOptions +title: QueriesControllerOptions +--- + +# Type Alias: QueriesControllerOptions\ + +```ts +type QueriesControllerOptions = Accessor>; +``` + +Defined in: [packages/lit-query/src/types.ts:74](https://github.com/TanStack/query/blob/main/packages/lit-query/src/types.ts#L74) + +Accessor-wrapped options accepted by `createQueriesController`. + +## Type Parameters + +### TQueryOptions + +`TQueryOptions` *extends* `any`[] = `any`[] + +### TCombinedResult + +`TCombinedResult` = `CreateQueriesResults`\<`TQueryOptions`\> diff --git a/docs/framework/lit/reference/type-aliases/QueriesResultAccessor.md b/docs/framework/lit/reference/type-aliases/QueriesResultAccessor.md new file mode 100644 index 00000000000..c780231ee2f --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/QueriesResultAccessor.md @@ -0,0 +1,37 @@ +--- +id: QueriesResultAccessor +title: QueriesResultAccessor +--- + +# Type Alias: QueriesResultAccessor\ + +```ts +type QueriesResultAccessor = ValueAccessor & object; +``` + +Defined in: [packages/lit-query/src/createQueriesController.ts:217](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueriesController.ts#L217) + +Accessor returned by `createQueriesController`. + +Call the accessor or read its `current` property to get the latest combined +value. + +## Type Declaration + +### destroy() + +```ts +destroy: () => void; +``` + +Removes the controller from its Lit host and unsubscribes observers. + +#### Returns + +`void` + +## Type Parameters + +### TCombinedResult + +`TCombinedResult` diff --git a/docs/framework/lit/reference/type-aliases/QueryControllerOptions.md b/docs/framework/lit/reference/type-aliases/QueryControllerOptions.md new file mode 100644 index 00000000000..104d98c5fb9 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/QueryControllerOptions.md @@ -0,0 +1,36 @@ +--- +id: QueryControllerOptions +title: QueryControllerOptions +--- + +# Type Alias: QueryControllerOptions\ + +```ts +type QueryControllerOptions = Accessor>; +``` + +Defined in: [packages/lit-query/src/types.ts:20](https://github.com/TanStack/query/blob/main/packages/lit-query/src/types.ts#L20) + +Accessor-wrapped options accepted by `createQueryController`. + +## Type Parameters + +### TQueryFnData + +`TQueryFnData` = `unknown` + +### TError + +`TError` = `DefaultError` + +### TData + +`TData` = `TQueryFnData` + +### TQueryData + +`TQueryData` = `TQueryFnData` + +### TQueryKey + +`TQueryKey` *extends* `QueryKey` = `QueryKey` diff --git a/docs/framework/lit/reference/type-aliases/QueryControllerResult.md b/docs/framework/lit/reference/type-aliases/QueryControllerResult.md new file mode 100644 index 00000000000..853212d0536 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/QueryControllerResult.md @@ -0,0 +1,24 @@ +--- +id: QueryControllerResult +title: QueryControllerResult +--- + +# Type Alias: QueryControllerResult\ + +```ts +type QueryControllerResult = QueryObserverResult; +``` + +Defined in: [packages/lit-query/src/types.ts:33](https://github.com/TanStack/query/blob/main/packages/lit-query/src/types.ts#L33) + +Result object produced by a Lit query controller. + +## Type Parameters + +### TData + +`TData` = `unknown` + +### TError + +`TError` = `DefaultError` diff --git a/docs/framework/lit/reference/type-aliases/QueryResultAccessor.md b/docs/framework/lit/reference/type-aliases/QueryResultAccessor.md new file mode 100644 index 00000000000..ce806098556 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/QueryResultAccessor.md @@ -0,0 +1,61 @@ +--- +id: QueryResultAccessor +title: QueryResultAccessor +--- + +# Type Alias: QueryResultAccessor\ + +```ts +type QueryResultAccessor = ValueAccessor> & object; +``` + +Defined in: [packages/lit-query/src/createQueryController.ts:41](https://github.com/TanStack/query/blob/main/packages/lit-query/src/createQueryController.ts#L41) + +Accessor returned by `createQueryController`. + +Call the accessor or read its `current` property to get the latest query +result. The attached methods delegate to the active query observer. + +## Type Declaration + +### destroy() + +```ts +destroy: () => void; +``` + +Removes the controller from its Lit host and unsubscribes observers. + +#### Returns + +`void` + +### refetch + +```ts +refetch: QueryObserverResult["refetch"]; +``` + +Refetches the current query. + +### suspense() + +```ts +suspense: () => Promise>; +``` + +Resolves with an optimistic query result, fetching first when needed. + +#### Returns + +`Promise`\<`QueryObserverResult`\<`TData`, `TError`\>\> + +## Type Parameters + +### TData + +`TData` + +### TError + +`TError` diff --git a/docs/framework/lit/reference/type-aliases/UndefinedInitialDataOptions.md b/docs/framework/lit/reference/type-aliases/UndefinedInitialDataOptions.md new file mode 100644 index 00000000000..75aa69f4139 --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/UndefinedInitialDataOptions.md @@ -0,0 +1,42 @@ +--- +id: UndefinedInitialDataOptions +title: UndefinedInitialDataOptions +--- + +# Type Alias: UndefinedInitialDataOptions\ + +```ts +type UndefinedInitialDataOptions = QueryObserverOptions & object; +``` + +Defined in: [packages/lit-query/src/queryOptions.ts:58](https://github.com/TanStack/query/blob/main/packages/lit-query/src/queryOptions.ts#L58) + +Query options where `initialData` can be omitted or undefined. + +## 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/lit/reference/type-aliases/UnusedSkipTokenOptions.md b/docs/framework/lit/reference/type-aliases/UnusedSkipTokenOptions.md new file mode 100644 index 00000000000..53acb8d9f5c --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/UnusedSkipTokenOptions.md @@ -0,0 +1,40 @@ +--- +id: UnusedSkipTokenOptions +title: UnusedSkipTokenOptions +--- + +# Type Alias: UnusedSkipTokenOptions\ + +```ts +type UnusedSkipTokenOptions = OmitKeyof, "queryFn"> & object; +``` + +Defined in: [packages/lit-query/src/queryOptions.ts:34](https://github.com/TanStack/query/blob/main/packages/lit-query/src/queryOptions.ts#L34) + +Query options where `queryFn` is present and not a `skipToken`. + +## 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/lit/reference/type-aliases/ValueAccessor.md b/docs/framework/lit/reference/type-aliases/ValueAccessor.md new file mode 100644 index 00000000000..642c42f37da --- /dev/null +++ b/docs/framework/lit/reference/type-aliases/ValueAccessor.md @@ -0,0 +1,39 @@ +--- +id: ValueAccessor +title: ValueAccessor +--- + +# Type Alias: ValueAccessor\ + +```ts +type ValueAccessor = () => T & object; +``` + +Defined in: [packages/lit-query/src/accessor.ts:32](https://github.com/TanStack/query/blob/main/packages/lit-query/src/accessor.ts#L32) + +A callable accessor with a `current` property for reading the latest +controller result. + +Controller creators and cache state helpers return this shape so render code +can use either `result()` or `result.current`. + +## Type Declaration + +### current + +```ts +readonly current: T; +``` + +## Type Parameters + +### T + +`T` + +## Example + +```ts +const query = this.todos() +const sameQuery = this.todos.current +``` diff --git a/docs/framework/lit/reference/variables/queryClientContext.md b/docs/framework/lit/reference/variables/queryClientContext.md new file mode 100644 index 00000000000..56395991d49 --- /dev/null +++ b/docs/framework/lit/reference/variables/queryClientContext.md @@ -0,0 +1,18 @@ +--- +id: queryClientContext +title: queryClientContext +--- + +# Variable: queryClientContext + +```ts +const queryClientContext: object; +``` + +Defined in: [packages/lit-query/src/context.ts:11](https://github.com/TanStack/query/blob/main/packages/lit-query/src/context.ts#L11) + +Lit context key used by `QueryClientProvider` and host-bound APIs to share a +`QueryClient` through the DOM tree. + +Most applications use `QueryClientProvider` instead of interacting with this +context directly. diff --git a/docs/framework/lit/typescript.md b/docs/framework/lit/typescript.md new file mode 100644 index 00000000000..6a59b09b1d7 --- /dev/null +++ b/docs/framework/lit/typescript.md @@ -0,0 +1,132 @@ +--- +id: typescript +title: TypeScript +--- + +Lit Query is written in TypeScript and reuses TanStack Query Core's type system. The most important rule is the same as every other adapter: give your query and mutation functions well-defined return types, and the result accessors will infer from them. + +## Query Inference + +```ts +import { LitElement } from 'lit' +import { createQueryController } from '@tanstack/lit-query' + +type Todo = { + id: number + title: string +} + +async function fetchTodos(): Promise { + const response = await fetch('/api/todos') + if (!response.ok) throw new Error('Failed to fetch todos') + return response.json() as Promise +} + +class TodosView extends LitElement { + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodos, + }) + + render() { + const query = this.todos() + // query.data is Todo[] | undefined until success is known. + } +} +``` + +Checking `isSuccess`, `isPending`, `isError`, or `status` narrows the result just like TanStack Query Core result types: + +```ts +const query = this.todos() + +if (query.isSuccess) { + query.data + // Todo[] +} +``` + +## Mutation Inference + +```ts +import { LitElement } from 'lit' +import { createMutationController } from '@tanstack/lit-query' + +type CreateTodoInput = { + title: string +} + +type Todo = { + id: number + title: string +} + +async function addTodo(input: CreateTodoInput): Promise { + const response = await fetch('/api/todos', { + method: 'POST', + body: JSON.stringify(input), + }) + if (!response.ok) throw new Error('Failed to create todo') + return response.json() as Promise +} + +class AddTodoButton extends LitElement { + private readonly mutation = createMutationController(this, { + mutationFn: addTodo, + }) + + private add() { + this.mutation.mutate({ title: 'Learn Lit Query' }) + } +} +``` + +## Extracting Options + +Use [`queryOptions`](./reference/functions/queryOptions.md), [`infiniteQueryOptions`](./reference/functions/infiniteQueryOptions.md), and [`mutationOptions`](./reference/functions/mutationOptions.md) when you want to share typed options between controllers and `QueryClient` calls. + +```ts +import { LitElement } from 'lit' +import { + QueryClient, + createQueryController, + queryOptions, +} from '@tanstack/lit-query' + +function todosOptions() { + return queryOptions({ + queryKey: ['todos'], + queryFn: fetchTodos, + staleTime: 5_000, + }) +} + +const queryClient = new QueryClient() + +class TodosView extends LitElement { + private readonly todos = createQueryController(this, todosOptions()) +} + +void queryClient.prefetchQuery(todosOptions()) +``` + +The branded `queryKey` returned from `queryOptions` also helps APIs like `queryClient.getQueryData` understand the data type. + +## Global Register Types + +Because `@tanstack/lit-query` re-exports TanStack Query Core, module augmentation is written against `@tanstack/lit-query` in Lit apps: + +```ts +import '@tanstack/lit-query' + +type AppQueryKey = ['todos' | 'projects', ...ReadonlyArray] + +declare module '@tanstack/lit-query' { + interface Register { + queryKey: AppQueryKey + mutationKey: AppQueryKey + } +} +``` + +See the generated reference for Lit-specific [option and result types](./reference/index.md). diff --git a/examples/lit/basic/README.md b/examples/lit/basic/README.md new file mode 100644 index 00000000000..6d79ad06c1a --- /dev/null +++ b/examples/lit/basic/README.md @@ -0,0 +1,5 @@ +# Example + +To run this example from the repo root: + +- `pnpm --dir examples/lit/basic run dev` diff --git a/examples/lit/basic/basic-query.html b/examples/lit/basic/basic-query.html new file mode 100644 index 00000000000..19de73348a1 --- /dev/null +++ b/examples/lit/basic/basic-query.html @@ -0,0 +1,12 @@ + + + + + + Lit Query Basic Example + + + + + + diff --git a/examples/lit/basic/config/port.d.ts b/examples/lit/basic/config/port.d.ts new file mode 100644 index 00000000000..12e9b85cc4d --- /dev/null +++ b/examples/lit/basic/config/port.d.ts @@ -0,0 +1 @@ +export const DEMO_PORT: number diff --git a/examples/lit/basic/config/port.js b/examples/lit/basic/config/port.js new file mode 100644 index 00000000000..bff39bff2ee --- /dev/null +++ b/examples/lit/basic/config/port.js @@ -0,0 +1,22 @@ +const DEFAULT_DEMO_PORT = 4173 +const envPort = process.env.DEMO_PORT + +function resolvePort() { + if (!envPort) { + return DEFAULT_DEMO_PORT + } + + const parsedPort = Number.parseInt(envPort, 10) + const isValidPort = + Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65535 + + if (!isValidPort) { + throw new Error( + `Invalid DEMO_PORT "${envPort}". Expected an integer between 1 and 65535.`, + ) + } + + return parsedPort +} + +export const DEMO_PORT = resolvePort() diff --git a/examples/lit/basic/index.html b/examples/lit/basic/index.html new file mode 100644 index 00000000000..a2962e70475 --- /dev/null +++ b/examples/lit/basic/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Lit Query E2E Demo + + + + + + diff --git a/examples/lit/basic/lifecycle-contract.html b/examples/lit/basic/lifecycle-contract.html new file mode 100644 index 00000000000..5d0f5e5e5a1 --- /dev/null +++ b/examples/lit/basic/lifecycle-contract.html @@ -0,0 +1,12 @@ + + + + + + TanStack Lit Query Lifecycle Contract Fixture + + + + + + diff --git a/examples/lit/basic/mutation.html b/examples/lit/basic/mutation.html new file mode 100644 index 00000000000..1d5de500efb --- /dev/null +++ b/examples/lit/basic/mutation.html @@ -0,0 +1,12 @@ + + + + + + Lit Query Mutation Example + + + + + + diff --git a/examples/lit/basic/package.json b/examples/lit/basic/package.json new file mode 100644 index 00000000000..969932dc834 --- /dev/null +++ b/examples/lit/basic/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tanstack/query-example-lit-basic", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/lit-query": "^0.1.0", + "@tanstack/query-core": "^5.99.0", + "lit": "^3.3.1" + }, + "devDependencies": { + "typescript": "5.8.3", + "vite": "^6.4.1" + } +} diff --git a/examples/lit/basic/src/basic-query.ts b/examples/lit/basic/src/basic-query.ts new file mode 100644 index 00000000000..5cc208d0174 --- /dev/null +++ b/examples/lit/basic/src/basic-query.ts @@ -0,0 +1,90 @@ +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createQueryController, +} from '@tanstack/lit-query' +import { fetchTodosFromServer, resetTodoApi } from './todoApi' +import type { TodosResponse } from './todoApi' + +resetTodoApi() + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + }, +}) + +class BasicQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } +} + +customElements.define('basic-query-provider', BasicQueryProvider) + +class BasicQueryExample extends LitElement { + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodosFromServer, + }) + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + render() { + const query = this.todos() + return html` +
+

Basic Query Example

+

+ Status: ${query.status} +

+ + + ${query.isPending + ? html`

Loading...

` + : null} + ${query.isError + ? html`

Error: ${String(query.error)}

` + : null} + +
    + ${(query.data?.items ?? []).map( + (todo) => + html`
  • ${todo.title}
  • `, + )} +
+
+ ` + } +} + +customElements.define('basic-query-example', BasicQueryExample) + +class BasicQueryRoot extends LitElement { + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + render() { + return html` + + + + ` + } +} + +customElements.define('basic-query-root', BasicQueryRoot) diff --git a/examples/lit/basic/src/lifecycle-contract.ts b/examples/lit/basic/src/lifecycle-contract.ts new file mode 100644 index 00000000000..76f0027ba40 --- /dev/null +++ b/examples/lit/basic/src/lifecycle-contract.ts @@ -0,0 +1,223 @@ +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createQueryController, +} from '@tanstack/lit-query' + +type ContractProbeData = { + provider: 'provider-a' | 'provider-b' + payload: string +} + +type ContractTarget = 'orphan' | 'provider-a' | 'provider-b' + +const contractQueryKey = ['lifecycle-contract', 'provider-binding'] as const +let contractConsumerInstanceCount = 0 + +function createContractClient(data: ContractProbeData): QueryClient { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: Number.POSITIVE_INFINITY, + }, + }, + }) + + client.setQueryData(contractQueryKey, data) + return client +} + +const contractClientA = createContractClient({ + provider: 'provider-a', + payload: 'provider-a cache', +}) + +const contractClientB = createContractClient({ + provider: 'provider-b', + payload: 'provider-b cache', +}) + +class ContractProviderA extends QueryClientProvider { + constructor() { + super() + this.client = contractClientA + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } +} + +if (!customElements.get('contract-provider-a')) { + customElements.define('contract-provider-a', ContractProviderA) +} + +class ContractProviderB extends QueryClientProvider { + constructor() { + super() + this.client = contractClientB + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } +} + +if (!customElements.get('contract-provider-b')) { + customElements.define('contract-provider-b', ContractProviderB) +} + +class LifecycleContractConsumer extends LitElement { + private readonly instanceId = ++contractConsumerInstanceCount + + private readonly query = createQueryController( + this, + { + queryKey: contractQueryKey, + queryFn: () => { + throw new Error( + 'Lifecycle contract fixture unexpectedly fetched from queryFn.', + ) + }, + retry: false, + staleTime: Number.POSITIVE_INFINITY, + }, + ) + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + private renderQueryState() { + try { + const query = this.query() + + return html` +
query: ${query.status}
+
+ provider: ${query.data?.provider ?? 'none'} +
+
+ payload: ${query.data?.payload ?? 'none'} +
+
+ error: ${query.error ? String(query.error) : 'none'} +
+ ` + } catch (error) { + const message = error instanceof Error ? error.message : String(error) + + return html` +
query: missing-client
+
provider: none
+
payload: none
+
error: ${message}
+ ` + } + } + + render() { + return html` +
instance: ${this.instanceId}
+ ${this.renderQueryState()} + ` + } +} + +if (!customElements.get('lifecycle-contract-consumer')) { + customElements.define( + 'lifecycle-contract-consumer', + LifecycleContractConsumer, + ) +} + +class LifecycleContractRoot extends LitElement { + static properties = { + currentTarget: { state: true }, + } + + private currentTarget: ContractTarget = 'orphan' + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + override firstUpdated(): void { + this.moveConsumerTo('orphan') + } + + moveConsumerTo(target: ContractTarget): void { + const consumer = this.ensureConsumer() + const destination = this.getContainer(target) + destination.appendChild(consumer) + this.currentTarget = target + this.requestUpdate() + } + + private ensureConsumer(): LifecycleContractConsumer { + const existing = this.querySelector( + 'lifecycle-contract-consumer', + ) + if (existing) { + return existing + } + + return document.createElement( + 'lifecycle-contract-consumer', + ) as LifecycleContractConsumer + } + + private getContainer(target: ContractTarget): HTMLElement { + const selector = + target === 'orphan' + ? '[data-contract-slot="orphan"]' + : target === 'provider-a' + ? 'contract-provider-a' + : 'contract-provider-b' + + const container = this.querySelector(selector) + if (!(container instanceof HTMLElement)) { + throw new Error( + `Lifecycle contract container not found for target "${target}".`, + ) + } + + return container + } + + render() { + return html` +
+

Lifecycle Contract Fixture

+

+ Exercises the same consumer across missing-provider and provider + switching flows. +

+
+ location: ${this.currentTarget} +
+ +
+

Orphan Zone

+
+
+ +
+

Provider A

+ +
+ +
+

Provider B

+ +
+
+ ` + } +} + +if (!customElements.get('lifecycle-contract-root')) { + customElements.define('lifecycle-contract-root', LifecycleContractRoot) +} diff --git a/examples/lit/basic/src/main.ts b/examples/lit/basic/src/main.ts new file mode 100644 index 00000000000..5f56e3be0f6 --- /dev/null +++ b/examples/lit/basic/src/main.ts @@ -0,0 +1,274 @@ +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createMutationController, + createQueryController, + useIsFetching, + useIsMutating, +} from '@tanstack/lit-query' +import { + addTodoOnServer, + failNextFetchRequest, + failNextMutationRequest, + fetchTodosFromServer, + resetTodoApi, +} from './todoApi' +import type { Todo, TodosResponse } from './todoApi' + +resetTodoApi() + +const demoQueryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, +}) + +class DemoQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = demoQueryClient + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } +} + +customElements.define('demo-query-provider', DemoQueryProvider) + +class TanstackLitQueryDemo extends LitElement { + static properties = { + nextTodoTitle: { state: true }, + cacheSeedCount: { state: true }, + } + + private nextTodoTitle = 'Add mutation assertion' + private cacheSeedCount = 0 + + private readonly todosQuery = createQueryController( + this, + { + queryKey: ['todos'], + queryFn: fetchTodosFromServer, + }, + ) + + private readonly createTodoMutation = createMutationController< + Todo, + Error, + string + >(this, { + mutationKey: ['create-todo'], + mutationFn: addTodoOnServer, + onSuccess: (createdTodo) => { + demoQueryClient.setQueryData(['todos'], (existing) => { + if (!existing) { + return { + items: [createdTodo], + requestCount: 0, + source: 'cache', + } + } + + return { + items: [...existing.items, createdTodo], + requestCount: existing.requestCount, + source: 'cache', + } + }) + }, + }) + + private readonly isFetching = useIsFetching(this, { + queryKey: ['todos'], + }) + + private readonly isMutating = useIsMutating(this, { + mutationKey: ['create-todo'], + }) + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + private onTitleInput(event: Event): void { + const target = event.target as HTMLInputElement + this.nextTodoTitle = target.value + } + + private addTodo(): void { + const title = this.nextTodoTitle.trim() + if (!title) { + return + } + + this.createTodoMutation.mutate(title) + this.nextTodoTitle = '' + } + + private async invalidateTodos(): Promise { + await demoQueryClient.invalidateQueries({ queryKey: ['todos'] }) + } + + private seedCacheOnlyTodo(): void { + this.cacheSeedCount += 1 + + const seedTodo: Todo = { + id: 10_000 + this.cacheSeedCount, + title: `Seeded cache todo ${this.cacheSeedCount}`, + } + + demoQueryClient.setQueryData(['todos'], (existing) => { + if (!existing) { + return { + items: [seedTodo], + requestCount: 0, + source: 'cache', + } + } + + return { + items: [...existing.items, seedTodo], + requestCount: existing.requestCount, + source: 'cache', + } + }) + } + + private forceNextFetchFailure(): void { + failNextFetchRequest() + } + + private forceNextMutationFailure(): void { + failNextMutationRequest() + } + + private async resetDemoState(): Promise { + resetTodoApi() + this.cacheSeedCount = 0 + this.nextTodoTitle = 'Add mutation assertion' + + await demoQueryClient.resetQueries({ queryKey: ['todos'] }) + this.createTodoMutation.reset() + } + + render() { + const query = this.todosQuery() + const mutation = this.createTodoMutation() + const todos = query.data?.items ?? [] + + return html` +
+

TanStack Lit Query E2E Demo

+

Verifies integration between Lit, query-core, and this adapter.

+ +
+
query: ${query.status}
+
mutation: ${mutation.status}
+
fetches: ${this.isFetching()}
+
+ mutations: ${this.isMutating()} +
+
+ server-requests: ${query.data?.requestCount ?? 0} +
+
+ source: ${query.data?.source ?? 'none'} +
+
+ +
+ + + + + + +
+ +
+ + + +
+ + ${query.isError + ? html`
${String(query.error)}
` + : null} + ${mutation.isError + ? html`
+ ${String(mutation.error)} +
` + : null} + +
    + ${todos.map( + (todo) => + html`
  • ${todo.id}: ${todo.title}
  • `, + )} +
+
+ ` + } +} + +customElements.define('tanstack-lit-query-demo', TanstackLitQueryDemo) + +class DemoRoot extends LitElement { + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + render() { + return html` + + + + ` + } +} + +customElements.define('demo-root', DemoRoot) diff --git a/examples/lit/basic/src/mutation.ts b/examples/lit/basic/src/mutation.ts new file mode 100644 index 00000000000..422be660656 --- /dev/null +++ b/examples/lit/basic/src/mutation.ts @@ -0,0 +1,139 @@ +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createMutationController, + createQueryController, +} from '@tanstack/lit-query' +import { addTodoOnServer, fetchTodosFromServer, resetTodoApi } from './todoApi' +import type { Todo, TodosResponse } from './todoApi' + +resetTodoApi() + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, +}) + +class MutationExampleProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } +} + +customElements.define('mutation-example-provider', MutationExampleProvider) + +class MutationExample extends LitElement { + static properties = { + nextTitle: { state: true }, + } + + private nextTitle = 'Created from mutation example' + + private readonly todos = createQueryController(this, { + queryKey: ['todos'], + queryFn: fetchTodosFromServer, + }) + + private readonly addTodo = createMutationController( + this, + { + mutationKey: ['create-todo'], + mutationFn: addTodoOnServer, + onSuccess: (created) => { + queryClient.setQueryData(['todos'], (existing) => { + if (!existing) { + return { + items: [created], + requestCount: 0, + source: 'cache', + } + } + + return { + items: [...existing.items, created], + requestCount: existing.requestCount, + source: 'cache', + } + }) + }, + }, + ) + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + private onInput(event: Event): void { + const target = event.target as HTMLInputElement + this.nextTitle = target.value + } + + private submit(): void { + const title = this.nextTitle.trim() + if (!title) return + this.addTodo.mutate(title) + this.nextTitle = '' + } + + render() { + const query = this.todos() + const mutation = this.addTodo() + const items = query.data?.items ?? [] + + return html` +
+

Mutation Example

+

+ Query: ${query.status} +

+

+ Mutation: ${mutation.status} +

+ + + + + +
    + ${items.map( + (todo) => + html`
  • ${todo.title}
  • `, + )} +
+
+ ` + } +} + +customElements.define('mutation-example', MutationExample) + +class MutationExampleRoot extends LitElement { + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + render() { + return html` + + + + ` + } +} + +customElements.define('mutation-example-root', MutationExampleRoot) diff --git a/examples/lit/basic/src/todoApi.ts b/examples/lit/basic/src/todoApi.ts new file mode 100644 index 00000000000..6ea4aa4dac1 --- /dev/null +++ b/examples/lit/basic/src/todoApi.ts @@ -0,0 +1,77 @@ +export type Todo = { + id: number + title: string +} + +export type TodosResponse = { + items: Todo[] + requestCount: number + source: 'server' | 'cache' +} + +let todos: Todo[] = [ + { id: 1, title: 'Ship lit-query alpha' }, + { id: 2, title: 'Write integration checks' }, +] + +let requestCount = 0 +let nextTodoId = 3 +let failNextFetch = false +let failNextMutation = false + +const delay = (ms: number): Promise => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) + +export async function fetchTodosFromServer(): Promise { + await delay(90) + if (failNextFetch) { + failNextFetch = false + throw new Error('Forced fetch failure (test)') + } + requestCount += 1 + + return { + items: todos.map((todo) => ({ ...todo })), + requestCount, + source: 'server', + } +} + +export async function addTodoOnServer(title: string): Promise { + await delay(70) + if (failNextMutation) { + failNextMutation = false + throw new Error('Forced mutation failure (test)') + } + + const nextTodo: Todo = { + id: nextTodoId, + title, + } + + nextTodoId += 1 + todos = [...todos, nextTodo] + + return { ...nextTodo } +} + +export function resetTodoApi(): void { + todos = [ + { id: 1, title: 'Ship lit-query alpha' }, + { id: 2, title: 'Write integration checks' }, + ] + requestCount = 0 + nextTodoId = 3 + failNextFetch = false + failNextMutation = false +} + +export function failNextFetchRequest(): void { + failNextFetch = true +} + +export function failNextMutationRequest(): void { + failNextMutation = true +} diff --git a/examples/lit/basic/tsconfig.json b/examples/lit/basic/tsconfig.json new file mode 100644 index 00000000000..d6e16f3ca50 --- /dev/null +++ b/examples/lit/basic/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "useDefineForClassFields": false + }, + "include": ["src/**/*.ts", "vite.config.ts"] +} diff --git a/examples/lit/basic/vite.config.ts b/examples/lit/basic/vite.config.ts new file mode 100644 index 00000000000..1d3028a8fb5 --- /dev/null +++ b/examples/lit/basic/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import { DEMO_PORT } from './config/port.js' + +export default defineConfig({ + server: { + host: '127.0.0.1', + port: DEMO_PORT, + strictPort: true, + }, + preview: { + host: '127.0.0.1', + port: DEMO_PORT, + strictPort: true, + }, +}) diff --git a/examples/lit/pagination/README.md b/examples/lit/pagination/README.md new file mode 100644 index 00000000000..695072f25d0 --- /dev/null +++ b/examples/lit/pagination/README.md @@ -0,0 +1,5 @@ +# Example + +To run this example from the repo root: + +- `pnpm --dir examples/lit/pagination run dev` diff --git a/examples/lit/pagination/config/ports.d.ts b/examples/lit/pagination/config/ports.d.ts new file mode 100644 index 00000000000..740d57d9863 --- /dev/null +++ b/examples/lit/pagination/config/ports.d.ts @@ -0,0 +1,2 @@ +export const DEMO_PORT: number +export const API_PORT: number diff --git a/examples/lit/pagination/config/ports.js b/examples/lit/pagination/config/ports.js new file mode 100644 index 00000000000..c4afc201ce2 --- /dev/null +++ b/examples/lit/pagination/config/ports.js @@ -0,0 +1,26 @@ +const DEFAULT_DEMO_PORT = 4183 +const DEFAULT_API_PORT = 4184 + +function readPortFromEnv(name, fallback) { + const rawValue = process.env[name] + if (!rawValue) { + return fallback + } + + const parsed = Number.parseInt(rawValue, 10) + const valid = Number.isInteger(parsed) && parsed >= 1 && parsed <= 65_535 + + if (!valid) { + throw new Error( + `Invalid ${name}="${rawValue}". Expected integer in [1, 65535].`, + ) + } + + return parsed +} + +export const DEMO_PORT = readPortFromEnv( + 'PAGINATION_DEMO_PORT', + DEFAULT_DEMO_PORT, +) +export const API_PORT = readPortFromEnv('PAGINATION_API_PORT', DEFAULT_API_PORT) diff --git a/examples/lit/pagination/index.html b/examples/lit/pagination/index.html new file mode 100644 index 00000000000..5d3fe8e8121 --- /dev/null +++ b/examples/lit/pagination/index.html @@ -0,0 +1,12 @@ + + + + + + TanStack Lit Query Pagination Demo + + + + + + diff --git a/examples/lit/pagination/package.json b/examples/lit/pagination/package.json new file mode 100644 index 00000000000..7e4bf0c01d8 --- /dev/null +++ b/examples/lit/pagination/package.json @@ -0,0 +1,20 @@ +{ + "name": "@tanstack/query-example-lit-pagination", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "node ./scripts/dev.mjs", + "build": "tsc --noEmit && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@tanstack/lit-query": "^0.1.0", + "@tanstack/query-core": "^5.99.0", + "lit": "^3.3.1" + }, + "devDependencies": { + "typescript": "5.8.3", + "vite": "^6.4.1" + } +} diff --git a/examples/lit/pagination/scripts/dev.mjs b/examples/lit/pagination/scripts/dev.mjs new file mode 100644 index 00000000000..2a58a8da0fd --- /dev/null +++ b/examples/lit/pagination/scripts/dev.mjs @@ -0,0 +1,80 @@ +import { spawn } from 'node:child_process' +import { once } from 'node:events' +import { API_PORT } from '../config/ports.js' + +const viteCommand = process.platform === 'win32' ? 'vite.cmd' : 'vite' +const cwd = new URL('..', import.meta.url) + +function forwardOutput(prefix, stream, output) { + stream.on('data', (chunk) => { + output.write(`${prefix}${chunk}`) + }) +} + +function start(name, command, args, extraEnv = {}) { + const child = spawn(command, args, { + cwd, + stdio: ['ignore', 'pipe', 'pipe'], + env: { + ...process.env, + ...extraEnv, + }, + }) + + forwardOutput(`[${name}] `, child.stdout, process.stdout) + forwardOutput(`[${name}] `, child.stderr, process.stderr) + + return child +} + +async function stop(child) { + if (!child || child.exitCode !== null) { + return + } + + child.kill('SIGTERM') + await Promise.race([ + once(child, 'exit'), + new Promise((resolve) => setTimeout(resolve, 2000)), + ]) + + if (child.exitCode === null) { + child.kill('SIGKILL') + await Promise.race([ + once(child, 'exit'), + new Promise((resolve) => setTimeout(resolve, 2000)), + ]) + } +} + +async function run() { + const api = start('api', process.execPath, ['./server/index.mjs']) + const web = start('web', viteCommand, [], { + VITE_PAGINATION_API_PORT: String(API_PORT), + }) + + const shutdown = async () => { + await Promise.all([stop(web), stop(api)]) + } + + process.on('SIGINT', shutdown) + process.on('SIGTERM', shutdown) + + const [winner] = await Promise.race([ + once(api, 'exit').then(([code]) => ({ name: 'api', code })), + once(web, 'exit').then(([code]) => ({ name: 'web', code })), + ]) + + await shutdown() + + if (winner.code !== 0 && winner.code !== null) { + process.exitCode = winner.code + } else { + process.exitCode = 1 + } +} + +run().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/examples/lit/pagination/server/index.mjs b/examples/lit/pagination/server/index.mjs new file mode 100644 index 00000000000..b9722992326 --- /dev/null +++ b/examples/lit/pagination/server/index.mjs @@ -0,0 +1,340 @@ +import { createServer } from 'node:http' +import { API_PORT } from '../config/ports.js' + +const PAGE_SIZE = 10 +const TOTAL_PROJECTS = 50 +const JSON_CONTENT_TYPE = 'application/json' + +function createSeedProjects() { + return Array.from({ length: TOTAL_PROJECTS }, (_, index) => ({ + id: index + 1, + name: `Project ${index + 1}`, + owner: `Team ${(index % 5) + 1}`, + isFavorite: false, + })) +} + +let projects = createSeedProjects() +let nextProjectId = projects.length + 1 +let totalRequestCount = 0 +let totalMutationCount = 0 +let failNextMutation = false +const perPageRequestCount = new Map() + +function writeJson(res, status, body) { + const payload = JSON.stringify(body) + res.writeHead(status, { + 'content-type': 'application/json; charset=utf-8', + 'cache-control': 'no-store', + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET,POST,PATCH,OPTIONS', + 'access-control-allow-headers': 'content-type', + 'content-length': Buffer.byteLength(payload), + }) + res.end(payload) +} + +function parsePositiveInt(rawValue, fallback) { + if (rawValue == null || rawValue === '') { + return fallback + } + + const parsed = Number.parseInt(rawValue, 10) + if (!Number.isInteger(parsed) || parsed < 1) { + return undefined + } + + return parsed +} + +function parseNonNegativeInt(rawValue, fallback) { + if (rawValue == null || rawValue === '') { + return fallback + } + + const parsed = Number.parseInt(rawValue, 10) + if (!Number.isInteger(parsed) || parsed < 0) { + return undefined + } + + return parsed +} + +function resetState() { + projects = createSeedProjects() + nextProjectId = projects.length + 1 + totalRequestCount = 0 + totalMutationCount = 0 + failNextMutation = false + perPageRequestCount.clear() +} + +async function sleep(ms) { + if (!ms || ms <= 0) { + return + } + + await new Promise((resolve) => { + setTimeout(resolve, ms) + }) +} + +async function readJsonBody(req) { + const contentType = req.headers['content-type'] + if (!contentType || !contentType.startsWith(JSON_CONTENT_TYPE)) { + return { + ok: false, + status: 415, + error: 'Expected application/json request body', + } + } + + const chunks = [] + for await (const chunk of req) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)) + } + + const raw = Buffer.concat(chunks).toString('utf8') + if (!raw) { + return { + ok: false, + status: 400, + error: 'Request body is required', + } + } + + try { + return { + ok: true, + value: JSON.parse(raw), + } + } catch { + return { + ok: false, + status: 400, + error: 'Request body must be valid JSON', + } + } +} + +function buildProjectsPage(page) { + const totalPages = Math.max(1, Math.ceil(projects.length / PAGE_SIZE)) + const boundedPage = Math.min(page, totalPages) + const startIndex = (boundedPage - 1) * PAGE_SIZE + const endIndex = startIndex + PAGE_SIZE + const items = projects.slice(startIndex, endIndex) + + totalRequestCount += 1 + const pageRequests = (perPageRequestCount.get(boundedPage) ?? 0) + 1 + perPageRequestCount.set(boundedPage, pageRequests) + + return { + page: boundedPage, + pageSize: PAGE_SIZE, + totalPages, + totalProjects: projects.length, + hasMore: boundedPage < totalPages, + projects: items, + requestMeta: { + totalRequestCount, + pageRequestCount: pageRequests, + totalMutationCount, + }, + } +} + +function maybeFailMutation(res) { + if (!failNextMutation) { + return false + } + + failNextMutation = false + writeJson(res, 500, { error: 'Forced mutation failure (test)' }) + return true +} + +function validateProjectName(name) { + if (typeof name !== 'string') { + return 'Project name must be a string' + } + + const trimmed = name.trim() + if (trimmed.length < 3) { + return 'Project name must be at least 3 characters' + } + + if (trimmed.length > 60) { + return 'Project name must be 60 characters or fewer' + } + + return null +} + +function validateOwner(owner) { + if (typeof owner !== 'string') { + return 'Owner must be a string' + } + + const trimmed = owner.trim() + if (!trimmed) { + return 'Owner is required' + } + + if (trimmed.length > 40) { + return 'Owner must be 40 characters or fewer' + } + + return null +} + +const server = createServer(async (req, res) => { + if (req.method === 'OPTIONS') { + res.writeHead(204, { + 'access-control-allow-origin': '*', + 'access-control-allow-methods': 'GET,POST,PATCH,OPTIONS', + 'access-control-allow-headers': 'content-type', + 'cache-control': 'no-store', + }) + res.end() + return + } + + if (!req.url) { + writeJson(res, 400, { error: 'Missing URL' }) + return + } + + const requestUrl = new URL(req.url, `http://127.0.0.1:${API_PORT}`) + + if (requestUrl.pathname === '/api/projects' && req.method === 'GET') { + const page = parsePositiveInt(requestUrl.searchParams.get('page'), 1) + if (!page) { + writeJson(res, 400, { error: 'Invalid page parameter' }) + return + } + + const delayMs = parseNonNegativeInt(requestUrl.searchParams.get('delay'), 0) + if (delayMs === undefined) { + writeJson(res, 400, { error: 'Invalid delay parameter' }) + return + } + + await sleep(delayMs) + + if (requestUrl.searchParams.get('error') === 'true') { + writeJson(res, 500, { + error: 'Forced server error (test)', + page, + }) + return + } + + writeJson(res, 200, buildProjectsPage(page)) + return + } + + if (requestUrl.pathname === '/api/projects' && req.method === 'POST') { + if (maybeFailMutation(res)) { + return + } + + const payload = await readJsonBody(req) + if (!payload.ok) { + writeJson(res, payload.status, { error: payload.error }) + return + } + + const nameError = validateProjectName(payload.value?.name) + if (nameError) { + writeJson(res, 422, { error: nameError }) + return + } + + const ownerError = validateOwner(payload.value?.owner) + if (ownerError) { + writeJson(res, 422, { error: ownerError }) + return + } + + const project = { + id: nextProjectId, + name: payload.value.name.trim(), + owner: payload.value.owner.trim(), + isFavorite: false, + } + + nextProjectId += 1 + totalMutationCount += 1 + projects = [project, ...projects] + + writeJson(res, 201, { + project, + mutationCount: totalMutationCount, + }) + return + } + + const patchMatch = requestUrl.pathname.match(/^\/api\/projects\/(\d+)$/) + if (patchMatch && req.method === 'PATCH') { + if (maybeFailMutation(res)) { + return + } + + const projectId = Number.parseInt(patchMatch[1], 10) + const projectIndex = projects.findIndex( + (project) => project.id === projectId, + ) + if (projectIndex === -1) { + writeJson(res, 404, { error: `Project ${projectId} was not found` }) + return + } + + const payload = await readJsonBody(req) + if (!payload.ok) { + writeJson(res, payload.status, { error: payload.error }) + return + } + + if (typeof payload.value?.isFavorite !== 'boolean') { + writeJson(res, 422, { error: 'isFavorite must be a boolean' }) + return + } + + const nextProject = { + ...projects[projectIndex], + isFavorite: payload.value.isFavorite, + } + + totalMutationCount += 1 + projects = projects.map((project, index) => + index === projectIndex ? nextProject : project, + ) + + writeJson(res, 200, { + project: nextProject, + mutationCount: totalMutationCount, + }) + return + } + + if ( + requestUrl.pathname === '/api/testing/fail-next-mutation' && + req.method === 'POST' + ) { + failNextMutation = true + writeJson(res, 200, { ok: true }) + return + } + + if (requestUrl.pathname === '/api/reset' && req.method === 'POST') { + resetState() + writeJson(res, 200, { ok: true }) + return + } + + writeJson(res, 404, { error: 'Not found' }) +}) + +server.listen(API_PORT, '127.0.0.1', () => { + console.log(`[api] listening on http://127.0.0.1:${API_PORT}`) +}) diff --git a/examples/lit/pagination/src/api.ts b/examples/lit/pagination/src/api.ts new file mode 100644 index 00000000000..8f4e1cd2091 --- /dev/null +++ b/examples/lit/pagination/src/api.ts @@ -0,0 +1,166 @@ +export type Project = { + id: number + name: string + owner: string + isFavorite: boolean +} + +export type ProjectsPageResponse = { + page: number + pageSize: number + totalPages: number + totalProjects: number + hasMore: boolean + projects: Project[] + requestMeta: { + totalRequestCount: number + pageRequestCount: number + totalMutationCount: number + } +} + +export type CreateProjectInput = { + name: string + owner: string +} + +export type ToggleProjectFavoriteInput = { + id: number + isFavorite: boolean +} + +export type ProjectMutationResponse = { + project: Project + mutationCount: number +} + +export type ProjectsQueryKey = readonly ['projects', number, number, boolean] + +const DEFAULT_API_PORT = 4184 +const configuredApiPort = Number.parseInt( + import.meta.env.VITE_PAGINATION_API_PORT ?? String(DEFAULT_API_PORT), + 10, +) +const API_PORT = Number.isInteger(configuredApiPort) + ? configuredApiPort + : DEFAULT_API_PORT +const API_BASE_URL = `http://127.0.0.1:${API_PORT}` + +function buildProjectsUrl( + page: number, + delayMs: number, + forceError: boolean, +): URL { + const url = new URL('/api/projects', API_BASE_URL) + url.searchParams.set('page', String(page)) + + if (delayMs > 0) { + url.searchParams.set('delay', String(delayMs)) + } + + if (forceError) { + url.searchParams.set('error', 'true') + } + + return url +} + +async function readJsonOrThrow( + response: Response, + fallbackMessage: string, +): Promise { + if (response.ok) { + return (await response.json()) as T + } + + const payload = (await response.json().catch(() => null)) as { + error?: string + } | null + + throw new Error( + payload && typeof payload === 'object' && 'error' in payload + ? String(payload.error ?? fallbackMessage) + : fallbackMessage, + ) +} + +async function requestJson( + url: URL, + init: RequestInit, + fallbackMessage: string, +): Promise { + const response = await fetch(url, init) + return readJsonOrThrow(response, fallbackMessage) +} + +export function projectsQueryKey( + page: number, + delayMs: number, + forceError: boolean, +): ProjectsQueryKey { + return ['projects', page, delayMs, forceError] as const +} + +export async function fetchProjectsPage( + page: number, + delayMs: number, + forceError: boolean, +): Promise { + const response = await fetch(buildProjectsUrl(page, delayMs, forceError)) + return readJsonOrThrow( + response, + `Failed to fetch projects page ${page}`, + ) +} + +export async function createProjectOnServer( + input: CreateProjectInput, +): Promise { + return requestJson( + new URL('/api/projects', API_BASE_URL), + { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify(input), + }, + 'Failed to create project', + ) +} + +export async function toggleProjectFavoriteOnServer( + input: ToggleProjectFavoriteInput, +): Promise { + return requestJson( + new URL(`/api/projects/${input.id}`, API_BASE_URL), + { + method: 'PATCH', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ isFavorite: input.isFavorite }), + }, + `Failed to update project ${input.id}`, + ) +} + +export async function armNextProjectMutationFailureOnServer(): Promise { + await requestJson<{ ok: true }>( + new URL('/api/testing/fail-next-mutation', API_BASE_URL), + { + method: 'POST', + }, + 'Failed to arm next mutation failure', + ) +} + +export async function resetProjectsApiState(): Promise { + await requestJson<{ ok: true }>( + new URL('/api/reset', API_BASE_URL), + { + method: 'POST', + }, + 'Failed to reset API state', + ) +} diff --git a/examples/lit/pagination/src/main.ts b/examples/lit/pagination/src/main.ts new file mode 100644 index 00000000000..b51b6f28641 --- /dev/null +++ b/examples/lit/pagination/src/main.ts @@ -0,0 +1,585 @@ +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createMutationController, + createQueryController, + keepPreviousData, +} from '@tanstack/lit-query' +import { + armNextProjectMutationFailureOnServer, + createProjectOnServer, + fetchProjectsPage, + projectsQueryKey, + resetProjectsApiState, + toggleProjectFavoriteOnServer, +} from './api' +import type { + CreateQueryOptions, + MutationResultAccessor, + QueryKey, + QueryResultAccessor, +} from '@tanstack/lit-query' +import type { + CreateProjectInput, + Project, + ProjectsPageResponse, + ToggleProjectFavoriteInput, +} from './api' + +type ProjectsCacheSnapshot = Array<[QueryKey, ProjectsPageResponse | undefined]> +type FavoriteMutationContext = { + snapshots: ProjectsCacheSnapshot +} + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: 5_000, + }, + mutations: { + retry: false, + }, + }, +}) + +class PaginationQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } +} + +customElements.define('pagination-query-provider', PaginationQueryProvider) + +class PaginationDemo extends LitElement { + static properties = { + page: { state: true }, + delayMs: { state: true }, + forceErrorMode: { state: true }, + prefetchStatus: { state: true }, + resetError: { state: true }, + draftName: { state: true }, + draftOwner: { state: true }, + mutationControlStatus: { state: true }, + mutationControlError: { state: true }, + } + + private page = 1 + private delayMs = 250 + private forceErrorMode = false + private prefetchStatus = 'idle' + private resetError: string | undefined + private draftName = 'Platform Rollout' + private draftOwner = 'Team 6' + private mutationControlStatus = 'idle' + private mutationControlError: string | undefined + private lastAutoPrefetchPage = 0 + private readonly projectsQueryOptions: CreateQueryOptions< + ProjectsPageResponse, + Error + > + private readonly projectsQuery: QueryResultAccessor< + ProjectsPageResponse, + Error + > + private readonly createProjectMutation: MutationResultAccessor< + Project, + Error, + CreateProjectInput, + unknown + > + private readonly favoriteMutation: MutationResultAccessor< + Project, + Error, + ToggleProjectFavoriteInput, + FavoriteMutationContext + > + + constructor() { + super() + + this.projectsQueryOptions = { + queryKey: projectsQueryKey(this.page, this.delayMs, this.forceErrorMode), + queryFn: () => + fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode), + placeholderData: keepPreviousData, + } + + this.projectsQuery = createQueryController( + this, + this.projectsQueryOptions, + ) + + this.createProjectMutation = createMutationController< + Project, + Error, + CreateProjectInput + >( + this, + { + mutationKey: ['create-project'], + mutationFn: async (input) => { + const response = await createProjectOnServer(input) + return response.project + }, + onMutate: () => { + this.mutationControlStatus = 'idle' + this.mutationControlError = undefined + }, + onSuccess: async () => { + this.page = 1 + this.lastAutoPrefetchPage = 0 + this.prefetchStatus = 'idle' + this.draftName = '' + this.draftOwner = 'Team 6' + this.syncProjectsQueryOptions() + await queryClient.invalidateQueries({ + queryKey: ['projects'], + refetchType: 'none', + }) + await this.projectsQuery.refetch() + }, + }, + queryClient, + ) + + this.favoriteMutation = createMutationController< + Project, + Error, + ToggleProjectFavoriteInput, + FavoriteMutationContext + >( + this, + { + mutationKey: ['toggle-project-favorite'], + mutationFn: async (input) => { + const response = await toggleProjectFavoriteOnServer(input) + return response.project + }, + onMutate: async (variables) => { + this.mutationControlStatus = 'idle' + this.mutationControlError = undefined + await queryClient.cancelQueries({ queryKey: ['projects'] }) + + const snapshots = queryClient.getQueriesData({ + queryKey: ['projects'], + }) + + for (const [key, existing] of snapshots) { + if (!existing) { + continue + } + + queryClient.setQueryData(key, { + ...existing, + projects: existing.projects.map((project) => + project.id === variables.id + ? { ...project, isFavorite: variables.isFavorite } + : project, + ), + }) + } + + return { snapshots } + }, + onError: (_error, _variables, context) => { + for (const [key, snapshot] of context?.snapshots ?? []) { + queryClient.setQueryData(key, snapshot) + } + }, + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: ['projects'] }) + }, + }, + queryClient, + ) + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + override updated(): void { + this.maybePrefetchNextPage() + } + + private syncProjectsQueryOptions(): void { + this.projectsQueryOptions.queryKey = projectsQueryKey( + this.page, + this.delayMs, + this.forceErrorMode, + ) + this.projectsQueryOptions.queryFn = () => + fetchProjectsPage(this.page, this.delayMs, this.forceErrorMode) + } + + private refetchForCurrentState(): void { + this.syncProjectsQueryOptions() + void this.projectsQuery.refetch() + } + + private async maybePrefetchNextPage(): Promise { + const query = this.projectsQuery() + const currentData = query.data + + if (!currentData || query.isPlaceholderData || !currentData.hasMore) { + return + } + + if (this.lastAutoPrefetchPage === currentData.page) { + return + } + + this.lastAutoPrefetchPage = currentData.page + await this.prefetchPage(currentData.page + 1) + } + + private onDelayInput(event: Event): void { + const target = event.target as HTMLInputElement + const nextValue = Number.parseInt(target.value, 10) + + if (!Number.isInteger(nextValue) || nextValue < 0) { + return + } + + this.delayMs = nextValue + this.refetchForCurrentState() + } + + private onErrorModeToggle(event: Event): void { + const target = event.target as HTMLInputElement + this.forceErrorMode = target.checked + this.refetchForCurrentState() + } + + private onDraftNameInput(event: Event): void { + const target = event.target as HTMLInputElement + this.draftName = target.value + } + + private onDraftOwnerInput(event: Event): void { + const target = event.target as HTMLInputElement + this.draftOwner = target.value + } + + private async prefetchPage(targetPage: number): Promise { + this.prefetchStatus = `pending:${targetPage}` + + try { + await queryClient.prefetchQuery({ + queryKey: projectsQueryKey( + targetPage, + this.delayMs, + this.forceErrorMode, + ), + queryFn: () => + fetchProjectsPage(targetPage, this.delayMs, this.forceErrorMode), + }) + this.prefetchStatus = `ready:${targetPage}` + } catch (error) { + this.prefetchStatus = `error:${String(error)}` + } + } + + private async prefetchNext(): Promise { + const query = this.projectsQuery() + const currentData = query.data + + if (!currentData?.hasMore) { + this.prefetchStatus = 'skipped:no-next-page' + return + } + + await this.prefetchPage(currentData.page + 1) + } + + private goToPreviousPage(): void { + if (this.page > 1) { + this.page -= 1 + this.refetchForCurrentState() + } + } + + private goToNextPage(): void { + const currentData = this.projectsQuery().data + if (!currentData?.hasMore) { + return + } + + this.page += 1 + this.refetchForCurrentState() + } + + private async resetDemoState(): Promise { + this.resetError = undefined + + try { + await resetProjectsApiState() + this.page = 1 + this.delayMs = 250 + this.forceErrorMode = false + this.prefetchStatus = 'idle' + this.resetError = undefined + this.draftName = 'Platform Rollout' + this.draftOwner = 'Team 6' + this.mutationControlStatus = 'idle' + this.mutationControlError = undefined + this.lastAutoPrefetchPage = 0 + this.syncProjectsQueryOptions() + this.createProjectMutation.reset() + this.favoriteMutation.reset() + await queryClient.resetQueries({ queryKey: ['projects'] }) + await this.projectsQuery.refetch() + } catch (error) { + this.resetError = String(error) + } + } + + private submitCreateProject(): void { + const name = this.draftName.trim() + const owner = this.draftOwner.trim() + + if (!name || !owner) { + return + } + + this.createProjectMutation.mutate({ name, owner }) + } + + private toggleFavorite(project: Project): void { + this.favoriteMutation.mutate({ + id: project.id, + isFavorite: !project.isFavorite, + }) + } + + private async armNextMutationFailure(): Promise { + this.mutationControlError = undefined + + try { + await armNextProjectMutationFailureOnServer() + this.mutationControlStatus = 'armed' + } catch (error) { + this.mutationControlStatus = 'error' + this.mutationControlError = String(error) + } + } + + render() { + const query = this.projectsQuery() + const projects = query.data?.projects ?? [] + const hasMore = query.data?.hasMore ?? false + const createProject = this.createProjectMutation() + const favoriteProject = this.favoriteMutation() + + return html` +
+

TanStack Lit Query Pagination Demo

+

+ Pagination + mutation demo with optimistic favorite toggles, + invalidation, and deterministic server failures. +

+ +
+
query: ${query.status}
+
+ isFetching: ${query.isFetching ? 'yes' : 'no'} +
+
+ isPlaceholderData: ${query.isPlaceholderData ? 'yes' : 'no'} +
+
page: ${this.page}
+
+ response-page: ${query.data?.page ?? '-'} +
+
has-more: ${hasMore ? 'yes' : 'no'}
+
+ total-projects: ${query.data?.totalProjects ?? 0} +
+
+ total-requests: ${query.data?.requestMeta.totalRequestCount ?? 0} +
+
+ page-requests: ${query.data?.requestMeta.pageRequestCount ?? 0} +
+
+ total-mutations: ${query.data?.requestMeta.totalMutationCount ?? 0} +
+
+ prefetch: ${this.prefetchStatus} +
+
+ + ${query.isError + ? html`

${String(query.error)}

` + : null} + ${this.resetError + ? html`

${this.resetError}

` + : null} + +
+ + + + + + + + + + +
+ mutation-control: ${this.mutationControlStatus} +
+ ${this.mutationControlError + ? html`
+ ${this.mutationControlError} +
` + : null} +
+ +
+
+ create-mutation: ${createProject.status} +
+ ${createProject.isError + ? html`
+ ${String(createProject.error)} +
` + : null} + + + + + + + + +
+ +
+
+ favorite-mutation: ${favoriteProject.status} +
+ ${favoriteProject.isError + ? html`
+ ${String(favoriteProject.error)} +
` + : null} +
+ +
+ + +
+ +
    + ${projects.map( + (project) => html` +
  • + ${project.id}: ${project.name} (${project.owner}) + ${project.isFavorite ? 'favorite' : 'standard'} + +
  • + `, + )} +
+
+ ` + } +} + +customElements.define('pagination-demo', PaginationDemo) + +class PaginationDemoRoot extends LitElement { + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + render() { + return html` + + + + ` + } +} + +customElements.define('pagination-demo-root', PaginationDemoRoot) diff --git a/examples/lit/pagination/src/vite-env.d.ts b/examples/lit/pagination/src/vite-env.d.ts new file mode 100644 index 00000000000..3d865d7345e --- /dev/null +++ b/examples/lit/pagination/src/vite-env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_PAGINATION_API_PORT?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/examples/lit/pagination/tsconfig.json b/examples/lit/pagination/tsconfig.json new file mode 100644 index 00000000000..d6e16f3ca50 --- /dev/null +++ b/examples/lit/pagination/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "useDefineForClassFields": false + }, + "include": ["src/**/*.ts", "vite.config.ts"] +} diff --git a/examples/lit/pagination/vite.config.ts b/examples/lit/pagination/vite.config.ts new file mode 100644 index 00000000000..371006b81cf --- /dev/null +++ b/examples/lit/pagination/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vite' +import { DEMO_PORT } from './config/ports.js' + +export default defineConfig({ + server: { + host: '127.0.0.1', + port: DEMO_PORT, + strictPort: true, + }, + preview: { + host: '127.0.0.1', + port: DEMO_PORT, + strictPort: true, + }, +}) diff --git a/examples/lit/ssr/README.md b/examples/lit/ssr/README.md new file mode 100644 index 00000000000..3c538d78f13 --- /dev/null +++ b/examples/lit/ssr/README.md @@ -0,0 +1,5 @@ +# Example + +To run this example from the repo root: + +- `pnpm --dir examples/lit/ssr run dev` diff --git a/examples/lit/ssr/config/ports.d.ts b/examples/lit/ssr/config/ports.d.ts new file mode 100644 index 00000000000..23d3eea7080 --- /dev/null +++ b/examples/lit/ssr/config/ports.d.ts @@ -0,0 +1,5 @@ +export const SSR_HOST: string +export const SSR_PORT: number +export const SSR_CONNECT_HOST: string +export const SSR_BASE_URL: string +export const SSR_PUBLIC_ORIGIN: string diff --git a/examples/lit/ssr/config/ports.js b/examples/lit/ssr/config/ports.js new file mode 100644 index 00000000000..a2872fdd9e4 --- /dev/null +++ b/examples/lit/ssr/config/ports.js @@ -0,0 +1,63 @@ +const DEFAULT_SSR_HOST = '127.0.0.1' +const DEFAULT_SSR_PORT = 4174 + +function normalizeUrlHost(host) { + if (host.includes(':') && !host.startsWith('[') && !host.endsWith(']')) { + return `[${host}]` + } + + return host +} + +function resolvePort(name, fallback) { + const value = process.env[name] + if (!value) { + return fallback + } + + const parsedPort = Number.parseInt(value, 10) + const isValidPort = + Number.isInteger(parsedPort) && parsedPort > 0 && parsedPort <= 65_535 + + if (!isValidPort) { + throw new Error( + `Invalid ${name} "${value}". Expected an integer between 1 and 65535.`, + ) + } + + return parsedPort +} + +export const SSR_PORT = resolvePort('SSR_PORT', DEFAULT_SSR_PORT) +export const SSR_HOST = process.env.SSR_HOST ?? DEFAULT_SSR_HOST + +function resolveConnectHost(host) { + if (host === '0.0.0.0') { + return '127.0.0.1' + } + + if (host === '::') { + return '[::1]' + } + + return normalizeUrlHost(host) +} + +function resolvePublicOrigin(host, port) { + const explicitOrigin = process.env.SSR_PUBLIC_ORIGIN + if (explicitOrigin) { + const url = new URL(explicitOrigin) + return url.origin + } + + const explicitPublicHost = process.env.SSR_PUBLIC_HOST + const publicHost = explicitPublicHost + ? normalizeUrlHost(explicitPublicHost) + : resolveConnectHost(host) + + return `http://${publicHost}:${port}` +} + +export const SSR_CONNECT_HOST = resolveConnectHost(SSR_HOST) +export const SSR_BASE_URL = `http://${SSR_CONNECT_HOST}:${SSR_PORT}` +export const SSR_PUBLIC_ORIGIN = resolvePublicOrigin(SSR_HOST, SSR_PORT) diff --git a/examples/lit/ssr/index.html b/examples/lit/ssr/index.html new file mode 100644 index 00000000000..7632b49fba0 --- /dev/null +++ b/examples/lit/ssr/index.html @@ -0,0 +1,15 @@ + + + + + + Lit Query SSR Example + + + __SSR_APP_HTML__ + + + + diff --git a/examples/lit/ssr/package.json b/examples/lit/ssr/package.json new file mode 100644 index 00000000000..6b2928b5850 --- /dev/null +++ b/examples/lit/ssr/package.json @@ -0,0 +1,22 @@ +{ + "name": "@tanstack/query-example-lit-ssr", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "node ./scripts/dev.mjs", + "build": "tsc --noEmit && vite build" + }, + "dependencies": { + "@lit-labs/ssr": "^3.3.0", + "@tanstack/lit-query": "^0.1.0", + "@tanstack/query-core": "^5.99.0", + "lit": "^3.3.1" + }, + "devDependencies": { + "@lit-labs/ssr-client": "^1.1.7", + "tsx": "^4.19.0", + "typescript": "5.8.3", + "vite": "^6.4.1" + } +} diff --git a/examples/lit/ssr/scripts/dev.mjs b/examples/lit/ssr/scripts/dev.mjs new file mode 100644 index 00000000000..0280720b806 --- /dev/null +++ b/examples/lit/ssr/scripts/dev.mjs @@ -0,0 +1,51 @@ +import { spawn, spawnSync } from 'node:child_process' +import { once } from 'node:events' +import { dirname, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' + +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm' +const tsxCommand = process.platform === 'win32' ? 'tsx.cmd' : 'tsx' +const cwd = resolve(dirname(fileURLToPath(import.meta.url)), '..') + +function runBuild() { + const result = spawnSync(npmCommand, ['run', 'build'], { + cwd, + stdio: 'inherit', + }) + + if (result.status !== 0) { + process.exit(result.status ?? 1) + } +} + +async function run() { + runBuild() + + const server = spawn(tsxCommand, ['./server/index.mjs'], { + cwd, + stdio: 'inherit', + }) + + const stopServer = (signal) => { + if (server.exitCode === null) { + server.kill(signal) + } + } + + process.on('SIGINT', () => stopServer('SIGINT')) + process.on('SIGTERM', () => stopServer('SIGTERM')) + + const outcome = await Promise.race([ + once(server, 'error').then(([error]) => { + throw error + }), + once(server, 'exit').then(([code]) => ({ code })), + ]) + + process.exitCode = outcome.code ?? 0 +} + +run().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/examples/lit/ssr/server/index.mjs b/examples/lit/ssr/server/index.mjs new file mode 100644 index 00000000000..3c80d93829e --- /dev/null +++ b/examples/lit/ssr/server/index.mjs @@ -0,0 +1,308 @@ +import { createServer } from 'node:http' +import { readFile } from 'node:fs/promises' +import { dirname, extname, resolve, sep } from 'node:path' +import { setTimeout as sleep } from 'node:timers/promises' +import { fileURLToPath } from 'node:url' +import { render } from '@lit-labs/ssr' +import { collectResult } from '@lit-labs/ssr/lib/render-result.js' +import { html } from 'lit' +import { QueryClient, dehydrate } from '@tanstack/lit-query' +import { + SSR_BASE_URL, + SSR_HOST, + SSR_PORT, + SSR_PUBLIC_ORIGIN, +} from '../config/ports.js' +import { + DATA_QUERY_KEY, + DEFAULT_MESSAGE, + QUERY_STALE_TIME, + createDataQueryOptions, +} from '../src/api.js' +import { + getSsrQueryControllerCreationCount, + resetSsrQueryControllerCreationCount, +} from '../src/app.ts' + +const serverDir = dirname(fileURLToPath(import.meta.url)) +const distDir = resolve(serverDir, '../dist') +const templatePath = resolve(distDir, 'index.html') + +const contentTypes = { + '.css': 'text/css; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.svg': 'image/svg+xml; charset=utf-8', +} + +let requestCount = 0 +let failNextDataRequest = false +let nextDataDelayMs = 0 + +const apiCorsHeaders = { + 'access-control-allow-headers': 'content-type', + 'access-control-allow-methods': 'GET,POST,OPTIONS', + 'access-control-allow-origin': '*', +} + +function createBrowserQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: QUERY_STALE_TIME, + }, + }, + }) +} + +function nextDataResponse() { + requestCount += 1 + return { + message: DEFAULT_MESSAGE, + requestCount, + servedAt: new Date().toISOString(), + } +} + +function resetApiState() { + requestCount = 0 + failNextDataRequest = false + nextDataDelayMs = 0 +} + +function parseNonNegativeDelayMs(value) { + const parsedDelayMs = Number.parseInt(value ?? '', 10) + if (!Number.isInteger(parsedDelayMs) || parsedDelayMs < 0) { + return undefined + } + + return parsedDelayMs +} + +async function consumeNextDataDelay() { + const delayMs = nextDataDelayMs + nextDataDelayMs = 0 + + if (delayMs > 0) { + await sleep(delayMs) + } +} + +function consumeFailNextDataRequest() { + if (!failNextDataRequest) { + return false + } + + failNextDataRequest = false + return true +} + +function writeBuffer(res, statusCode, contentType, payload, extraHeaders = {}) { + res.writeHead(statusCode, { + 'cache-control': 'no-store', + 'content-length': payload.byteLength, + 'content-type': contentType, + ...extraHeaders, + }) + res.end(payload) +} + +function writeHtml(res, statusCode, body, extraHeaders = {}) { + const payload = Buffer.from(body) + writeBuffer( + res, + statusCode, + 'text/html; charset=utf-8', + payload, + extraHeaders, + ) +} + +function writeJson(res, statusCode, body) { + const payload = Buffer.from(JSON.stringify(body)) + writeBuffer( + res, + statusCode, + 'application/json; charset=utf-8', + payload, + apiCorsHeaders, + ) +} + +function serializeJsonForHtml(value) { + return JSON.stringify(value).replace(/[<>&\u2028\u2029]/g, (character) => { + switch (character) { + case '<': + return '\\u003c' + case '>': + return '\\u003e' + case '&': + return '\\u0026' + case '\u2028': + return '\\u2028' + case '\u2029': + return '\\u2029' + default: + return character + } + }) +} + +async function readTemplate() { + try { + return await readFile(templatePath, 'utf8') + } catch (error) { + if (error && typeof error === 'object' && 'code' in error) { + throw new Error( + 'Missing built client assets. Run "pnpm --dir examples/lit/ssr run build" from the repo root first.', + ) + } + + throw error + } +} + +async function serveAsset(pathname, res) { + if (!pathname.startsWith('/assets/')) { + return false + } + + const assetPath = resolve(distDir, `.${pathname}`) + const distRootPrefix = `${distDir}${sep}` + if (!assetPath.startsWith(distRootPrefix)) { + return false + } + + try { + const file = await readFile(assetPath) + const contentType = + contentTypes[extname(assetPath)] ?? 'application/octet-stream' + writeBuffer(res, 200, contentType, file) + return true + } catch { + return false + } +} + +async function renderPage(res) { + const queryClient = createBrowserQueryClient() + resetSsrQueryControllerCreationCount(queryClient) + await queryClient.prefetchQuery(createDataQueryOptions(SSR_PUBLIC_ORIGIN)) + + const prefetchedQueryState = queryClient.getQueryState(DATA_QUERY_KEY) + if (prefetchedQueryState?.status !== 'success') { + throw new Error( + 'SSR prefetch did not complete successfully. Refusing to render loading HTML.', + ) + } + + const appHtml = await collectResult( + render( + html``, + ), + ) + + const dehydratedState = dehydrate(queryClient) + const controllerCreationCount = + getSsrQueryControllerCreationCount(queryClient) + if (controllerCreationCount < 1) { + throw new Error('SSR render did not exercise createQueryController.') + } + + const template = await readTemplate() + const htmlDocument = template + .replace('__SSR_APP_HTML__', appHtml) + .replace('__QUERY_STATE_JSON__', serializeJsonForHtml(dehydratedState)) + + writeHtml(res, 200, htmlDocument, { + 'x-ssr-query-controller-created': String(controllerCreationCount), + }) +} + +const server = createServer(async (req, res) => { + const requestUrl = new URL(req.url ?? '/', SSR_BASE_URL) + const method = req.method ?? 'GET' + + if (method === 'OPTIONS' && requestUrl.pathname.startsWith('/api/')) { + res.writeHead(204, { + 'cache-control': 'no-store', + ...apiCorsHeaders, + }) + res.end() + return + } + + if (method === 'GET' && requestUrl.pathname === '/api/data') { + await consumeNextDataDelay() + + if (consumeFailNextDataRequest()) { + writeJson(res, 500, { error: 'Forced data failure (test)' }) + return + } + + writeJson(res, 200, nextDataResponse()) + return + } + + if (method === 'GET' && requestUrl.pathname === '/api/request-count') { + writeJson(res, 200, { count: requestCount }) + return + } + + if (method === 'POST' && requestUrl.pathname === '/api/reset') { + resetApiState() + writeJson(res, 200, { ok: true }) + return + } + + if (method === 'POST' && requestUrl.pathname === '/api/test/fail-next-data') { + failNextDataRequest = true + writeJson(res, 200, { ok: true }) + return + } + + if ( + method === 'POST' && + requestUrl.pathname === '/api/test/delay-next-data' + ) { + const delayMs = parseNonNegativeDelayMs(requestUrl.searchParams.get('ms')) + if (delayMs === undefined) { + writeJson(res, 400, { error: 'Invalid ms query parameter.' }) + return + } + + nextDataDelayMs = delayMs + writeJson(res, 200, { ok: true, delayMs }) + return + } + + if (method === 'GET' && requestUrl.pathname === '/') { + try { + await renderPage(res) + } catch (error) { + console.error('[ssr] render failed:', error) + writeHtml( + res, + 500, + 'SSR render failed.', + ) + } + return + } + + if (method === 'GET' && (await serveAsset(requestUrl.pathname, res))) { + return + } + + writeJson(res, 404, { error: 'Not found' }) +}) + +server.listen(SSR_PORT, SSR_HOST, () => { + console.log( + `[ssr] listening on ${SSR_BASE_URL} (public origin ${SSR_PUBLIC_ORIGIN})`, + ) +}) diff --git a/examples/lit/ssr/src/api.ts b/examples/lit/ssr/src/api.ts new file mode 100644 index 00000000000..eb9656de655 --- /dev/null +++ b/examples/lit/ssr/src/api.ts @@ -0,0 +1,69 @@ +import type { CreateQueryOptions } from '@tanstack/lit-query' + +export const DATA_QUERY_KEY = ['ssr-example-data'] as const +export const DEFAULT_MESSAGE = 'Hello from SSR!' +export const QUERY_STALE_TIME = 30_000 + +export type DataResponse = { + message: string + requestCount: number + servedAt: string +} + +function resolveApiUrl(pathname: string, apiBaseUrl: string): string { + if (!apiBaseUrl) { + return pathname + } + + return new URL(pathname, apiBaseUrl).toString() +} + +async function readJson(response: Response): Promise { + let payload: TResponse | { error?: string } | null = null + + try { + payload = (await response.json()) as TResponse | { error?: string } + } catch { + if (response.ok) { + throw new Error( + `Failed to parse JSON response with status ${response.status}.`, + ) + } + } + + if (!response.ok) { + const errorDetail = + payload && + typeof payload === 'object' && + 'error' in payload && + typeof payload.error === 'string' + ? `: ${payload.error}` + : '' + + throw new Error( + `Request failed with status ${response.status}${errorDetail}`, + ) + } + + return payload as TResponse +} + +export function createDataQueryOptions(apiBaseUrl = '') { + return { + queryKey: DATA_QUERY_KEY, + queryFn: async ({ signal }) => { + const response = await fetch(resolveApiUrl('/api/data', apiBaseUrl), { + signal, + }) + return readJson(response) + }, + retry: false, + staleTime: QUERY_STALE_TIME, + } satisfies CreateQueryOptions< + DataResponse, + Error, + DataResponse, + DataResponse, + typeof DATA_QUERY_KEY + > +} diff --git a/examples/lit/ssr/src/app.ts b/examples/lit/ssr/src/app.ts new file mode 100644 index 00000000000..903bbcb2c9c --- /dev/null +++ b/examples/lit/ssr/src/app.ts @@ -0,0 +1,167 @@ +import { LitElement, css, html } from 'lit' +import { + createQueryController, + type QueryClient, + type QueryResultAccessor, +} from '@tanstack/lit-query' +import { createDataQueryOptions, type DataResponse } from './api.js' + +const ssrQueryControllerCreationCounts = new WeakMap() + +function incrementSsrQueryControllerCreationCount( + queryClient: QueryClient, +): void { + ssrQueryControllerCreationCounts.set( + queryClient, + (ssrQueryControllerCreationCounts.get(queryClient) ?? 0) + 1, + ) +} + +export function resetSsrQueryControllerCreationCount( + queryClient: QueryClient, +): void { + ssrQueryControllerCreationCounts.set(queryClient, 0) +} + +export function getSsrQueryControllerCreationCount( + queryClient: QueryClient, +): number { + return ssrQueryControllerCreationCounts.get(queryClient) ?? 0 +} + +export class SsrApp extends LitElement { + static properties = { + apiBaseUrl: { attribute: 'api-base-url' }, + queryClient: { attribute: false }, + } + + static styles = css` + :host { + color: #1f2937; + display: block; + font-family: + ui-sans-serif, + system-ui, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; + max-width: 32rem; + padding: 1.5rem; + } + + article { + background: #ffffff; + border: 1px solid #d1d5db; + border-radius: 1rem; + box-shadow: 0 12px 40px rgba(15, 23, 42, 0.08); + padding: 1.25rem; + } + + h1 { + font-size: 1.25rem; + margin: 0 0 1rem; + } + + p { + margin: 0.5rem 0; + } + + button { + background: #111827; + border: none; + border-radius: 999px; + color: #ffffff; + cursor: pointer; + font: inherit; + margin-top: 1rem; + padding: 0.65rem 1rem; + } + + button[disabled] { + cursor: wait; + opacity: 0.65; + } + ` + + apiBaseUrl = '' + queryClient?: QueryClient + + private dataQuery?: QueryResultAccessor + + protected override willUpdate(): void { + if (!this.dataQuery && this.queryClient) { + incrementSsrQueryControllerCreationCount(this.queryClient) + this.dataQuery = createQueryController( + this, + createDataQueryOptions(this.apiBaseUrl), + this.queryClient, + ) + } + } + + private readonly handleRefetch = (): void => { + void this.dataQuery?.refetch() + } + + protected override render() { + if (!this.dataQuery) { + return html` +
+

Lit Query SSR

+

Loading...

+
+ ` + } + + const query = this.dataQuery() + + if (query.isPending) { + return html` +
+

Lit Query SSR

+

Loading...

+
+ ` + } + + if (query.isError) { + return html` +
+

Lit Query SSR

+

Error

+

${query.error?.message}

+
+ ` + } + + return html` +
+

Lit Query SSR

+

${query.isFetching ? 'Refreshing' : 'Ready'}

+

${query.data.message}

+

+ Request count: ${query.data.requestCount} +

+

Served at: ${query.data.servedAt}

+ +
+ ` + } +} + +if (!customElements.get('ssr-app')) { + customElements.define('ssr-app', SsrApp) +} + +declare global { + interface HTMLElementTagNameMap { + 'ssr-app': SsrApp + } +} diff --git a/examples/lit/ssr/src/main.ts b/examples/lit/ssr/src/main.ts new file mode 100644 index 00000000000..6c8b52e9e71 --- /dev/null +++ b/examples/lit/ssr/src/main.ts @@ -0,0 +1,63 @@ +import '@lit-labs/ssr-client/lit-element-hydrate-support.js' + +import { QueryClient, hydrate, type DehydratedState } from '@tanstack/lit-query' +import { QUERY_STALE_TIME } from './api.js' + +type HydratableSsrApp = HTMLElement & { + queryClient?: QueryClient +} + +function createQueryClient(): QueryClient { + return new QueryClient({ + defaultOptions: { + queries: { + retry: false, + staleTime: QUERY_STALE_TIME, + }, + }, + }) +} + +function readDehydratedState(): DehydratedState { + const stateElement = document.getElementById('__QUERY_STATE__') + if (!stateElement) { + throw new Error('Missing dehydrated state script.') + } + + const stateText = stateElement.textContent?.trim() ?? 'null' + return JSON.parse(stateText) as DehydratedState +} + +async function bootstrap() { + if (document.readyState === 'loading') { + await new Promise((resolve) => { + document.addEventListener('DOMContentLoaded', () => resolve(), { + once: true, + }) + }) + } + + const appElement = document.querySelector( + 'ssr-app', + ) as HydratableSsrApp | null + if (!appElement) { + throw new Error('Expected the SSR app element to exist before hydration.') + } + + const queryClient = createQueryClient() + queryClient.mount() + hydrate(queryClient, readDehydratedState()) + appElement.queryClient = queryClient + + window.addEventListener( + 'pagehide', + () => { + queryClient.unmount() + }, + { once: true }, + ) + + await import('./app.js') +} + +void bootstrap() diff --git a/examples/lit/ssr/tsconfig.json b/examples/lit/ssr/tsconfig.json new file mode 100644 index 00000000000..84271844ba9 --- /dev/null +++ b/examples/lit/ssr/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "useDefineForClassFields": false + }, + "include": ["config/**/*.d.ts", "src/**/*.ts", "vite.config.ts"] +} diff --git a/examples/lit/ssr/vite.config.ts b/examples/lit/ssr/vite.config.ts new file mode 100644 index 00000000000..e313e6f288d --- /dev/null +++ b/examples/lit/ssr/vite.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite' +import { SSR_PORT } from './config/ports.js' + +export default defineConfig({ + build: { + target: 'es2022', + }, + server: { + host: '127.0.0.1', + port: SSR_PORT, + strictPort: true, + }, + preview: { + host: '127.0.0.1', + port: SSR_PORT, + strictPort: true, + }, +}) diff --git a/integrations/lit-vite/index.html b/integrations/lit-vite/index.html new file mode 100644 index 00000000000..b3974a125e0 --- /dev/null +++ b/integrations/lit-vite/index.html @@ -0,0 +1,13 @@ + + + + + Vite + Lit + + + + + + + + diff --git a/integrations/lit-vite/package.json b/integrations/lit-vite/package.json new file mode 100644 index 00000000000..f5e040bc08a --- /dev/null +++ b/integrations/lit-vite/package.json @@ -0,0 +1,17 @@ +{ + "name": "lit-vite", + "private": true, + "type": "module", + "scripts": { + "build": "tsc --noEmit && vite build" + }, + "dependencies": { + "@tanstack/lit-query": "workspace:*", + "@tanstack/query-core": "workspace:*", + "lit": "^3.3.1", + "vite": "^6.4.1" + }, + "devDependencies": { + "typescript": "5.8.3" + } +} diff --git a/integrations/lit-vite/src/main.ts b/integrations/lit-vite/src/main.ts new file mode 100644 index 00000000000..5da031300e8 --- /dev/null +++ b/integrations/lit-vite/src/main.ts @@ -0,0 +1,56 @@ +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createQueryController, +} from '@tanstack/lit-query' + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) + +class LitQueryProvider extends QueryClientProvider { + constructor() { + super() + this.client = queryClient + } + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } +} + +class LitQueryApp extends LitElement { + private readonly query = createQueryController(this, { + queryKey: ['test'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 100)) + return 'Success' + }, + }) + + protected override createRenderRoot(): HTMLElement | DocumentFragment { + return this + } + + render() { + const query = this.query() + + if (query.isPending) { + return html`
Loading...
` + } + + if (query.isError) { + return html`
An error has occurred!
` + } + + return html`
${query.data}
` + } +} + +customElements.define('lit-query-provider', LitQueryProvider) +customElements.define('lit-query-app', LitQueryApp) diff --git a/integrations/lit-vite/tsconfig.json b/integrations/lit-vite/tsconfig.json new file mode 100644 index 00000000000..f223644e2f2 --- /dev/null +++ b/integrations/lit-vite/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "lib": ["ES2022", "DOM"], + "strict": true, + "noEmit": true, + "isolatedModules": true, + "skipLibCheck": true, + "useDefineForClassFields": false + }, + "include": ["src/**/*.ts"] +} diff --git a/integrations/lit-vite/vite.config.ts b/integrations/lit-vite/vite.config.ts new file mode 100644 index 00000000000..4f1b25a0c3d --- /dev/null +++ b/integrations/lit-vite/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite' + +export default defineConfig({}) diff --git a/knip.json b/knip.json index 2d444d217cc..cde2b9b3b5d 100644 --- a/knip.json +++ b/knip.json @@ -19,6 +19,9 @@ "entry": ["src/v4/**/*.cjs", "src/v5/**/*.cjs"], "ignore": ["**/__testfixtures__/**"] }, + "packages/lit-query": { + "ignore": ["src/tests/**"] + }, "packages/vue-query": { "ignoreDependencies": ["vue2", "vue2.7"] } diff --git a/labeler-config.yml b/labeler-config.yml index d3f7712547c..444b9210acf 100644 --- a/labeler-config.yml +++ b/labeler-config.yml @@ -10,6 +10,9 @@ 'package: eslint-plugin-query': - changed-files: - any-glob-to-any-file: 'packages/eslint-plugin-query/**/*' +'package: lit-query': + - changed-files: + - any-glob-to-any-file: 'packages/lit-query/**/*' 'package: preact-query': - changed-files: - any-glob-to-any-file: 'packages/preact-query/**/*' diff --git a/packages/lit-query/.editorconfig b/packages/lit-query/.editorconfig new file mode 100644 index 00000000000..9d08a1a828a --- /dev/null +++ b/packages/lit-query/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/packages/lit-query/.npmignore b/packages/lit-query/.npmignore new file mode 100644 index 00000000000..f497387664b --- /dev/null +++ b/packages/lit-query/.npmignore @@ -0,0 +1,2 @@ +docs/ +src/tests/ diff --git a/packages/lit-query/.prettierignore b/packages/lit-query/.prettierignore new file mode 100644 index 00000000000..f800743a0af --- /dev/null +++ b/packages/lit-query/.prettierignore @@ -0,0 +1,10 @@ +dist +dist-cjs +node_modules +coverage +.claude +.references +*.tgz +package-lock.json +examples/**/output +examples/**/dist diff --git a/packages/lit-query/README.md b/packages/lit-query/README.md new file mode 100644 index 00000000000..c439074d806 --- /dev/null +++ b/packages/lit-query/README.md @@ -0,0 +1,127 @@ +# @tanstack/lit-query + +Lit adapter for `@tanstack/query-core` using Lit reactive controllers. + +This package is currently experimental and v0.1. Pin exact versions if you use +it in production while the API is early. + +## Install + +```bash +npm install @tanstack/lit-query @tanstack/query-core lit +``` + +For local development in this repository: + +```bash +pnpm install +pnpm --dir packages/lit-query run build +``` + +## Quick Start + +```ts +import { LitElement, html } from 'lit' +import { + QueryClient, + QueryClientProvider, + createQueryController, +} from '@tanstack/lit-query' + +const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, +}) + +class AppProvider extends QueryClientProvider { + constructor() { + super() + this.client = client + } +} +customElements.define('app-provider', AppProvider) + +class UsersView extends LitElement { + private readonly users = createQueryController(this, { + queryKey: ['users'], + queryFn: async () => { + const response = await fetch('/api/users') + return response.json() as Promise> + }, + }) + + render() { + const query = this.users() + if (query.isPending) return html`Loading...` + if (query.isError) return html`Error` + return html`
    + ${query.data?.map((u) => html`
  • ${u.name}
  • `)} +
` + } +} +customElements.define('users-view', UsersView) +``` + +## API Surface + +- `QueryClientProvider`, `useQueryClient`, `resolveQueryClient` +- `createQueryController` +- `createMutationController` +- `createInfiniteQueryController` +- `createQueriesController` +- `useIsFetching`, `useIsMutating`, `useMutationState` +- `queryOptions`, `infiniteQueryOptions`, `mutationOptions` + +## Runnable Examples + +This repo includes runnable Lit examples under the top-level `examples/lit` +directory so they can be surfaced in the docs: + +- `examples/lit/basic`: Vite Lit app covering query and mutation primitives. +- `examples/lit/pagination`: pagination, prefetching, optimistic updates, and error recovery. +- `examples/lit/ssr`: Lit SSR render, dehydrate, and hydrate flow. + +Run an example from the repo root: + +```bash +pnpm --dir examples/lit/basic run dev +pnpm --dir examples/lit/pagination run dev +pnpm --dir examples/lit/ssr run dev +``` + +Open: + +- `http://127.0.0.1:4173/` (basic example) +- `http://127.0.0.1:4183/` (pagination example app) +- `http://127.0.0.1:4174/` (SSR example app) + +Use a different port (optional): + +```bash +DEMO_PORT=4180 pnpm --dir examples/lit/basic run dev +PAGINATION_DEMO_PORT=4181 PAGINATION_API_PORT=4182 pnpm --dir examples/lit/pagination run dev +SSR_PORT=4180 pnpm --dir examples/lit/ssr run dev +SSR_HOST=0.0.0.0 pnpm --dir examples/lit/ssr run dev +``` + +## Integration Smoke + +For the framework build smoke used in CI: + +```bash +pnpm --dir integrations/lit-vite run build +``` + +## Quality Gates + +From the repository root: + +```bash +pnpm --dir packages/lit-query run test:types +pnpm --dir packages/lit-query run test:lib +pnpm --dir packages/lit-query run build +pnpm --dir packages/lit-query run test:build +``` + +## License + +MIT diff --git a/packages/lit-query/eslint.config.js b/packages/lit-query/eslint.config.js new file mode 100644 index 00000000000..c3642e60191 --- /dev/null +++ b/packages/lit-query/eslint.config.js @@ -0,0 +1,52 @@ +// @ts-check + +import js from '@eslint/js' +import vitest from '@vitest/eslint-plugin' +import globals from 'globals' +import tseslint from 'typescript-eslint' + +export default tseslint.config( + { + ignores: [ + 'dist/**', + 'coverage/**', + 'node_modules/**', + '.claude/**', + '.references/**', + '**/dist/**', + '**/dist-cjs/**', + 'examples/**/output/**', + '**/*.d.ts', + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + files: ['**/*.{ts,js,mjs,cjs}'], + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + '@typescript-eslint/no-explicit-any': 'off', + 'no-console': 'off', + }, + }, + { + files: ['src/tests/**/*.{ts,js,mjs}', 'examples/**/e2e/**/*.{js,mjs}'], + plugins: { + vitest, + }, + languageOptions: { + globals: { + ...vitest.environments.env.globals, + }, + }, + rules: { + ...vitest.configs.recommended.rules, + 'vitest/expect-expect': 'off', + }, + }, +) diff --git a/packages/lit-query/package.json b/packages/lit-query/package.json new file mode 100644 index 00000000000..3528f7a5cac --- /dev/null +++ b/packages/lit-query/package.json @@ -0,0 +1,64 @@ +{ + "name": "@tanstack/lit-query", + "version": "0.1.0", + "description": "Lit adapter for TanStack Query Core", + "license": "MIT", + "type": "module", + "main": "dist-cjs/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "sideEffects": false, + "files": [ + "dist", + "dist-cjs", + "src/**/*.ts", + "!src/tests/**/*" + ], + "exports": { + ".": { + "@tanstack/custom-condition": "./src/index.ts", + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist-cjs/index.d.cts", + "default": "./dist-cjs/index.js" + }, + "default": "./dist/index.js" + }, + "./package.json": "./package.json" + }, + "scripts": { + "compile": "node ../../node_modules/typescript/lib/tsc.js --build", + "build:deps": "pnpm --dir ../query-core run build", + "build": "pnpm run build:deps && pnpm run build:esm && pnpm run build:cjs", + "build:esm": "node ../../node_modules/typescript/lib/tsc.js -p tsconfig.build.json", + "build:cjs": "node -e \"require('node:fs').rmSync('dist-cjs', { recursive: true, force: true })\" && node ../../node_modules/typescript/lib/tsc.js -p tsconfig.build.cjs.json && node scripts/write-cjs-package.mjs", + "test:types": "node ../../node_modules/typescript/lib/tsc.js --noEmit", + "test:eslint": "eslint .", + "lint:fix": "eslint . --fix", + "test:lib": "vitest run", + "test:lib:dev": "vitest", + "test:watch": "pnpm run test:lib:dev", + "test:build": "publint --strict && attw --pack && node scripts/check-cjs-types-smoke.mjs", + "measure:bundle": "pnpm run build && node scripts/measure-bundle.mjs", + "measure:bundle:raw": "node scripts/measure-bundle.mjs", + "perf:l3": "pnpm run build && node scripts/l3-stress.mjs", + "perf:l3:raw": "node scripts/l3-stress.mjs" + }, + "dependencies": { + "@lit/context": "^1.1.6", + "@tanstack/query-core": "workspace:*", + "lit": "^3.3.1" + }, + "peerDependencies": { + "@tanstack/query-core": "^5.0.0", + "lit": ">=2.8.0 <4" + }, + "devDependencies": { + "@eslint/js": "^9.36.0", + "globals": "^17.4.0", + "typescript-eslint": "^8.54.0" + } +} diff --git a/packages/lit-query/scripts/check-cjs-types-smoke.mjs b/packages/lit-query/scripts/check-cjs-types-smoke.mjs new file mode 100644 index 00000000000..a8c508d11d7 --- /dev/null +++ b/packages/lit-query/scripts/check-cjs-types-smoke.mjs @@ -0,0 +1,137 @@ +import { execFile as execFileCallback } from 'node:child_process' +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' + +const execFile = promisify(execFileCallback) +const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm' +const pnpmCommand = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm' +const projectDir = resolve(dirname(fileURLToPath(import.meta.url)), '..') +const workspaceRoot = resolve(projectDir, '..', '..') +const typeRootsDir = resolve(workspaceRoot, 'node_modules', '@types') +const tscEntrypoint = resolve( + workspaceRoot, + 'node_modules', + 'typescript', + 'lib', + 'tsc.js', +) + +const tempRoot = await mkdtemp(join(tmpdir(), 'tanstack-lit-query-cjs-smoke-')) +const packDir = resolve(tempRoot, 'pack') +const consumerDir = resolve(tempRoot, 'consumer') + +try { + const tarballPath = await packProject(packDir) + + await writeConsumerFixture(consumerDir) + await installConsumer(consumerDir, tarballPath) + await typecheckConsumer(consumerDir) + + console.log('CommonJS TypeScript smoke test passed.') +} finally { + await rm(tempRoot, { recursive: true, force: true }) +} + +async function packProject(destination) { + await mkdir(destination, { recursive: true }) + + const { stdout } = await execFile( + pnpmCommand, + ['pack', '--json', '--pack-destination', destination], + { + cwd: projectDir, + }, + ) + const packResult = JSON.parse(stdout) + const filename = Array.isArray(packResult) + ? packResult[0]?.filename + : packResult?.filename + + if (typeof filename !== 'string') { + throw new Error(`Unexpected pack output: ${stdout}`) + } + + return resolve(destination, filename) +} + +async function writeConsumerFixture(consumerDirectory) { + await rm(consumerDirectory, { recursive: true, force: true }) + await mkdir(consumerDirectory, { recursive: true }) + + await writeFile( + resolve(consumerDirectory, 'package.json'), + `${JSON.stringify( + { + private: true, + type: 'commonjs', + }, + null, + 2, + )}\n`, + 'utf8', + ) + + await writeFile( + resolve(consumerDirectory, 'index.cts'), + [ + "const pkg = require('@tanstack/lit-query')", + '', + "type CreateQueryOptions = import('@tanstack/lit-query').CreateQueryOptions", + '', + 'const options: CreateQueryOptions = {', + " queryKey: ['cjs-smoke'],", + " queryFn: async () => 'ok',", + '}', + '', + "if (typeof pkg.createQueryController !== 'function') {", + " throw new Error('createQueryController export is missing in CommonJS consumer.')", + '}', + '', + 'void pkg.queryOptions(options)', + '', + ].join('\n'), + 'utf8', + ) + + await writeFile( + resolve(consumerDirectory, 'tsconfig.json'), + `${JSON.stringify( + { + compilerOptions: { + module: 'Node16', + moduleResolution: 'Node16', + target: 'ES2022', + strict: true, + noEmit: true, + types: ['node'], + typeRoots: [typeRootsDir], + }, + include: ['index.cts'], + }, + null, + 2, + )}\n`, + 'utf8', + ) + + await writeFile( + resolve(consumerDirectory, '.npmrc'), + 'package-lock=false\n', + 'utf8', + ) +} + +async function installConsumer(consumerDirectory, tarballPath) { + await execFile(npmCommand, ['install', '--silent', tarballPath], { + cwd: consumerDirectory, + }) +} + +async function typecheckConsumer(consumerDirectory) { + await execFile(process.execPath, [tscEntrypoint, '-p', 'tsconfig.json'], { + cwd: consumerDirectory, + }) +} diff --git a/packages/lit-query/scripts/l3-stress.mjs b/packages/lit-query/scripts/l3-stress.mjs new file mode 100644 index 00000000000..55f52a9a73f --- /dev/null +++ b/packages/lit-query/scripts/l3-stress.mjs @@ -0,0 +1,124 @@ +import { QueryClient } from '@tanstack/query-core' +import { createQueryController } from '../dist/index.js' + +class TestControllerHost { + controllers = new Set() + updatesRequested = 0 + updateComplete = Promise.resolve(true) + + addController(controller) { + this.controllers.add(controller) + } + + removeController(controller) { + this.controllers.delete(controller) + } + + requestUpdate() { + this.updatesRequested += 1 + } + + connect() { + for (const controller of this.controllers) { + controller.hostConnected?.() + } + } + + disconnect() { + for (const controller of this.controllers) { + controller.hostDisconnected?.() + } + } + + update() { + for (const controller of this.controllers) { + controller.hostUpdate?.() + } + + for (const controller of this.controllers) { + controller.hostUpdated?.() + } + } +} + +async function waitFor(assertion, timeoutMs = 3000) { + const startedAt = Date.now() + while (!assertion()) { + if (Date.now() - startedAt > timeoutMs) { + throw new Error(`Timed out waiting for assertion after ${timeoutMs}ms`) + } + await new Promise((resolve) => setTimeout(resolve, 5)) + } +} + +async function run(cycles = 1000) { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + gcTime: 0, + }, + }, + }) + + const queryKey = ['l3-stress'] + const startedAt = Date.now() + const initialHeapMb = process.memoryUsage().heapUsed / (1024 * 1024) + + for (let cycle = 0; cycle < cycles; cycle += 1) { + const host = new TestControllerHost() + const query = createQueryController( + host, + { + queryKey, + gcTime: 0, + queryFn: async () => cycle, + }, + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess, 4000) + + const cacheQuery = client.getQueryCache().find({ queryKey }) + const connectedCount = cacheQuery?.getObserversCount() ?? 0 + if (connectedCount !== 1) { + throw new Error( + `observer_count_connected_invalid:${connectedCount}:cycle:${cycle}`, + ) + } + + host.disconnect() + query.destroy() + + const disconnectedCount = cacheQuery?.getObserversCount() ?? 0 + if (disconnectedCount !== 0) { + throw new Error( + `observer_count_disconnected_invalid:${disconnectedCount}:cycle:${cycle}`, + ) + } + } + + const elapsedMs = Date.now() - startedAt + const finalHeapMb = process.memoryUsage().heapUsed / (1024 * 1024) + const memoryGrowthMb = Number((finalHeapMb - initialHeapMb).toFixed(3)) + + const summary = { + measuredAt: new Date().toISOString(), + cycles, + elapsedMs, + initialHeapMb: Number(initialHeapMb.toFixed(3)), + finalHeapMb: Number(finalHeapMb.toFixed(3)), + memoryGrowthMb, + retainedObserversAfterRun: + client.getQueryCache().find({ queryKey })?.getObserversCount() ?? 0, + } + + console.log(JSON.stringify(summary, null, 2)) +} + +run().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/packages/lit-query/scripts/measure-bundle.mjs b/packages/lit-query/scripts/measure-bundle.mjs new file mode 100644 index 00000000000..b8adec12b17 --- /dev/null +++ b/packages/lit-query/scripts/measure-bundle.mjs @@ -0,0 +1,43 @@ +import { readFile, readdir, stat } from 'node:fs/promises' +import path from 'node:path' +import { gzipSync } from 'node:zlib' +import { fileURLToPath } from 'node:url' + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(scriptDir, '..') +const distDir = path.join(repoRoot, 'dist') +const entryFile = path.join(distDir, 'index.js') + +async function getDirSizeBytes(dirPath) { + let total = 0 + const entries = await readdir(dirPath, { withFileTypes: true }) + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name) + if (entry.isDirectory()) { + total += await getDirSizeBytes(fullPath) + } else if (entry.isFile()) { + total += (await stat(fullPath)).size + } + } + return total +} + +async function run() { + const entry = await readFile(entryFile) + const entryGzip = gzipSync(entry) + const distBytes = await getDirSizeBytes(distDir) + + const output = { + measuredAt: new Date().toISOString(), + entryJsBytes: entry.length, + entryJsGzipBytes: entryGzip.length, + distTotalBytes: distBytes, + } + + console.log(JSON.stringify(output, null, 2)) +} + +run().catch((error) => { + console.error(error) + process.exit(1) +}) diff --git a/packages/lit-query/scripts/write-cjs-package.mjs b/packages/lit-query/scripts/write-cjs-package.mjs new file mode 100644 index 00000000000..dcf076d7832 --- /dev/null +++ b/packages/lit-query/scripts/write-cjs-package.mjs @@ -0,0 +1,73 @@ +import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, relative, resolve } from 'node:path' + +const projectDir = process.cwd() +const esmDir = resolve(projectDir, 'dist') +const outDir = resolve(process.cwd(), 'dist-cjs') +const esmOnlyPackages = new Set(['lit']) +const esmImportTypeRegex = /import type \{([^}]*)\} from (['"])([^'"]+)\2;/g +const esmValueImportRegex = /import \{([^}]*)\} from (['"])([^'"]+)\2;/g +const importTypeExpressionRegex = /import\((['"])([^'"]+)\1\)/g + +await mkdir(outDir, { recursive: true }) +await writeFile( + resolve(outDir, 'package.json'), + `${JSON.stringify({ type: 'commonjs' }, null, 2)}\n`, + 'utf8', +) + +for (const declarationFile of await findDeclarationFiles(esmDir)) { + const source = await readFile(declarationFile, 'utf8') + const relativePath = relative(esmDir, declarationFile) + const outputPath = resolve(outDir, relativePath.replace(/\.d\.ts$/, '.d.cts')) + + await mkdir(dirname(outputPath), { recursive: true }) + await writeFile(outputPath, rewriteDeclaration(source), 'utf8') +} + +async function findDeclarationFiles(rootDir) { + const entries = await readdir(rootDir, { withFileTypes: true }) + const files = [] + + for (const entry of entries) { + const entryPath = resolve(rootDir, entry.name) + + if (entry.isDirectory()) { + files.push(...(await findDeclarationFiles(entryPath))) + continue + } + + if (entry.isFile() && entry.name.endsWith('.d.ts')) { + files.push(entryPath) + } + } + + return files +} + +function rewriteDeclaration(source) { + return source + .replace(/^\/\/# sourceMappingURL=.*$\n?/gm, '') + .replace(/(['"])(\.\.?\/[^'"]+)\.js\1/g, '$1$2.cjs$1') + .replace(esmImportTypeRegex, (match, specifiers, quote, packageName) => { + if (!esmOnlyPackages.has(packageName)) { + return match + } + + return `import type {${specifiers}} from ${quote}${packageName}${quote} with { "resolution-mode": "import" };` + }) + .replace(esmValueImportRegex, (match, specifiers, quote, packageName) => { + if (!esmOnlyPackages.has(packageName)) { + return match + } + + return `import type {${specifiers}} from ${quote}${packageName}${quote} with { "resolution-mode": "import" };` + }) + .replace(importTypeExpressionRegex, (match, quote, packageName) => { + if (packageName !== 'lit-html') { + return match + } + + return `import(${quote}${packageName}${quote}, { with: { "resolution-mode": "import" } })` + }) +} diff --git a/packages/lit-query/src/QueryClientProvider.ts b/packages/lit-query/src/QueryClientProvider.ts new file mode 100644 index 00000000000..b3447aa5df9 --- /dev/null +++ b/packages/lit-query/src/QueryClientProvider.ts @@ -0,0 +1,174 @@ +import { ContextProvider } from '@lit/context' +import type { QueryClient } from '@tanstack/query-core' +import type { TemplateResult } from 'lit' +import { LitElement, html } from 'lit' +import { + createMissingQueryClientError, + queryClientContext, + registerDefaultQueryClient, + unregisterDefaultQueryClient, +} from './context.js' + +/** + * Lit element that provides a `QueryClient` to descendant Lit Query + * controllers through Lit context. + * + * The `client` is a property, not an attribute. When rendering this element in + * a Lit template, bind it with property binding: `.client=${queryClient}`. + * The provider throws if it connects without a client, or if an already + * connected provider has its client cleared. + * + * This class is not registered as a custom element by the package. Applications + * must register either a subclass or the class itself with + * `customElements.define`. + * + * @example + * ```ts + * import { html, LitElement } from 'lit' + * import { QueryClient, QueryClientProvider } from '@tanstack/lit-query' + * + * const queryClient = new QueryClient() + * + * class AppQueryProvider extends QueryClientProvider { + * constructor() { + * super() + * this.client = queryClient + * } + * } + * + * customElements.define('app-query-provider', AppQueryProvider) + * + * class AppRoot extends LitElement { + * render() { + * return html`` + * } + * } + * ``` + * + * @example + * ```ts + * import { html } from 'lit' + * import { QueryClient, QueryClientProvider } from '@tanstack/lit-query' + * + * const queryClient = new QueryClient() + * + * customElements.define('query-client-provider', QueryClientProvider) + * + * const view = html` + * + * + * + * ` + * ``` + */ +export class QueryClientProvider extends LitElement { + /** @internal */ + static properties = { + client: { attribute: false }, + } + + /** + * The `QueryClient` provided to descendant controllers and global fallback + * helpers while this provider is connected. + * + * Bind this as a property in Lit templates with `.client=${queryClient}`. + */ + declare client: QueryClient + + private readonly contextProvider: ContextProvider + + private mountedClient: QueryClient | undefined + + constructor() { + super() + this.contextProvider = new ContextProvider(this, { + context: queryClientContext, + }) + } + + /** @internal */ + connectedCallback(): void { + super.connectedCallback() + const client = this.requireClient() + this.contextProvider.setValue(client) + this.mountClient(client) + } + + /** @internal */ + disconnectedCallback(): void { + this.unmountClient(this.mountedClient) + super.disconnectedCallback() + } + + /** @internal */ + protected willUpdate(changedProperties: Map): void { + if (!changedProperties.has('client')) { + return + } + + const nextClient = this.client + if (!nextClient) { + if (this.isConnected) { + this.unmountClient(this.mountedClient) + // Sentinel: notify active consumers that the provider is now unbound. + this.contextProvider.setValue(undefined as unknown as QueryClient) + throw createMissingQueryClientError() + } + + return + } + + const previousClient = changedProperties.get('client') as + | QueryClient + | undefined + if (previousClient && previousClient !== nextClient && this.isConnected) { + this.unmountClient(previousClient) + } + + this.contextProvider.setValue(nextClient) + + if (this.isConnected) { + this.mountClient(nextClient) + } + } + + /** @internal */ + render(): TemplateResult { + return html`` + } + + private mountClient(client: QueryClient): void { + if (this.mountedClient === client) { + return + } + + if (this.mountedClient) { + this.unmountClient(this.mountedClient) + } + + client.mount() + registerDefaultQueryClient(client) + this.mountedClient = client + } + + private unmountClient(client?: QueryClient): void { + if (!client) { + return + } + + client.unmount() + unregisterDefaultQueryClient(client) + + if (this.mountedClient === client) { + this.mountedClient = undefined + } + } + + private requireClient(): QueryClient { + if (!this.client) { + throw createMissingQueryClientError() + } + + return this.client + } +} diff --git a/packages/lit-query/src/accessor.ts b/packages/lit-query/src/accessor.ts new file mode 100644 index 00000000000..35bc8098620 --- /dev/null +++ b/packages/lit-query/src/accessor.ts @@ -0,0 +1,43 @@ +/** + * A value that can be passed directly or read from a zero-argument getter. + * + * Lit Query APIs read function accessors during host updates, so the getter can + * depend on reactive host state. + * + * @example + * ```ts + * const staticKey: Accessor = ['todos'] + * const reactiveKey: Accessor = () => ['todos', this.userId] + * ``` + */ +export type Accessor = T | (() => T) + +export function readAccessor(value: Accessor): T { + return typeof value === 'function' ? (value as () => T)() : value +} + +/** + * A callable accessor with a `current` property for reading the latest + * controller result. + * + * Controller creators and cache state helpers return this shape so render code + * can use either `result()` or `result.current`. + * + * @example + * ```ts + * const query = this.todos() + * const sameQuery = this.todos.current + * ``` + */ +export type ValueAccessor = (() => T) & { + readonly current: T +} + +export function createValueAccessor(getter: () => T): ValueAccessor { + const accessor = (() => getter()) as ValueAccessor + Object.defineProperty(accessor, 'current', { + get: getter, + enumerable: true, + }) + return accessor +} diff --git a/packages/lit-query/src/context.ts b/packages/lit-query/src/context.ts new file mode 100644 index 00000000000..8e7c8a142f1 --- /dev/null +++ b/packages/lit-query/src/context.ts @@ -0,0 +1,120 @@ +import { createContext } from '@lit/context' +import type { QueryClient } from '@tanstack/query-core' + +/** + * Lit context key used by `QueryClientProvider` and host-bound APIs to share a + * `QueryClient` through the DOM tree. + * + * Most applications use `QueryClientProvider` instead of interacting with this + * context directly. + */ +export const queryClientContext = createContext( + Symbol.for('tanstack-query-client'), +) + +const missingQueryClientMessage = + 'No QueryClient available. Pass one explicitly or render within QueryClientProvider.' +const ambiguousQueryClientMessage = + 'Multiple QueryClients are mounted. Pass one explicitly instead of relying on global QueryClient helpers.' + +const registeredClients = new Map() +let defaultClient: QueryClient | undefined + +/** + * Registers a `QueryClient` as a process-local fallback for APIs that resolve a + * client without an explicit argument. + * + * `QueryClientProvider` calls this automatically while it is connected. Prefer + * passing an explicit client or rendering under a provider when possible. + * + * @param client - The query client to register as the current default. + */ +export function registerDefaultQueryClient(client: QueryClient): void { + registeredClients.set(client, (registeredClients.get(client) ?? 0) + 1) + defaultClient = client +} + +/** + * Unregisters a client previously registered with + * `registerDefaultQueryClient`. + * + * `QueryClientProvider` calls this automatically when it disconnects. + * + * @param client - The query client registration to release. + */ +export function unregisterDefaultQueryClient(client: QueryClient): void { + const count = registeredClients.get(client) + if (count === undefined) { + return + } + + if (count > 1) { + registeredClients.set(client, count - 1) + return + } + + registeredClients.delete(client) + if (defaultClient !== client) { + return + } + + const remaining = [...registeredClients.keys()] + defaultClient = remaining.at(-1) +} + +/** + * Returns the registered default `QueryClient`, if exactly one default client is + * available. + * + * @returns The default query client, or `undefined` when there is no registered + * client or more than one registered client. + */ +export function getDefaultQueryClient(): QueryClient | undefined { + if (registeredClients.size > 1) { + return undefined + } + + return defaultClient +} + +export function createMissingQueryClientError(): Error { + return new Error(missingQueryClientMessage) +} + +function createAmbiguousQueryClientError(): Error { + return new Error(ambiguousQueryClientMessage) +} + +/** + * Resolves the current default `QueryClient` registered by a connected + * `QueryClientProvider`. + * + * This helper is useful outside a Lit reactive controller when a single + * provider is mounted. It throws if no client is registered or if multiple + * clients are mounted and the default would be ambiguous. + * + * @returns The single registered query client. + */ +export function useQueryClient(): QueryClient { + const client = getDefaultQueryClient() + if (client) { + return client + } + + if (registeredClients.size > 1) { + throw createAmbiguousQueryClientError() + } + + throw createMissingQueryClientError() +} + +/** + * Resolves an explicit `QueryClient` or falls back to `useQueryClient`. + * + * @param explicit - Optional client supplied by the caller. + * @returns The explicit client when provided, otherwise the current default + * client. + */ +export function resolveQueryClient(explicit?: QueryClient): QueryClient { + return explicit ?? useQueryClient() +} diff --git a/packages/lit-query/src/controllers/BaseController.ts b/packages/lit-query/src/controllers/BaseController.ts new file mode 100644 index 00000000000..ba303edd417 --- /dev/null +++ b/packages/lit-query/src/controllers/BaseController.ts @@ -0,0 +1,284 @@ +import { ContextEvent } from '@lit/context' +import type { QueryClient } from '@tanstack/query-core' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { + createMissingQueryClientError, + queryClientContext, +} from '../context.js' + +type QueryClientResolutionState = + | 'pre-connect' + | 'awaiting-context' + | 'bound' + | 'missing' + +export abstract class BaseController implements ReactiveController { + protected result: TResult + + private readonly explicitClient?: QueryClient + private contextClient: QueryClient | undefined + private contextUnsubscribe: (() => void) | undefined + + private connected = false + private destroyed = false + private updateQueued = false + private clientChangeQueued = false + private connectionAttempt = 0 + private queryClientResolutionState: QueryClientResolutionState + + protected constructor( + protected readonly host: ReactiveControllerHost, + initialResult: TResult, + queryClient?: QueryClient, + ) { + this.explicitClient = queryClient + this.result = initialResult + this.queryClientResolutionState = queryClient ? 'bound' : 'pre-connect' + + host.addController(this) + } + + hostConnected(): void { + if (this.connected || this.destroyed) { + return + } + + this.connected = true + let contextResolutionAttempt: number | undefined + + if (this.explicitClient) { + this.queryClientResolutionState = 'bound' + } else { + contextResolutionAttempt = ++this.connectionAttempt + this.beginContextResolution() + } + + // Defer onConnected to ensure subclass constructors complete before + // lifecycle callbacks access subclass state. This handles the case where + // addController is called on an already-connected host (e.g., during + // willUpdate), which synchronously triggers hostConnected before + // subclass field initialization. + queueMicrotask(() => { + if (this.connected && !this.destroyed) { + this.onConnected() + } + }) + + if (contextResolutionAttempt !== undefined) { + // Provider-backed controllers on already-connected hosts should finish + // their deferred onConnected pass before a context client binds. + this.queueContextResolution(contextResolutionAttempt) + } + } + + hostDisconnected(): void { + if (!this.connected) { + return + } + + this.connected = false + + if (!this.explicitClient) { + this.connectionAttempt += 1 + this.clearContextClient() + this.updateQueryClientResolutionState('pre-connect') + } + + this.onDisconnected() + } + + hostUpdate(): void { + if (this.destroyed) { + return + } + + this.onHostUpdate() + } + + destroy(): void { + if (this.destroyed) { + return + } + + this.destroyed = true + this.connected = false + this.connectionAttempt += 1 + this.clearContextClient() + this.queryClientResolutionState = this.explicitClient + ? 'bound' + : 'pre-connect' + this.onDisconnected() + + if ('removeController' in this.host) { + this.host.removeController(this) + } + } + + protected tryGetQueryClient(): QueryClient | undefined { + return this.explicitClient ?? this.contextClient + } + + protected getQueryClient(): QueryClient { + const client = this.tryGetQueryClient() + if (!client) { + throw createMissingQueryClientError() + } + + return client + } + + protected setResult(next: TResult): void { + if (Object.is(this.result, next)) { + return + } + + this.result = next + this.queueUpdate() + } + + get current(): TResult { + if (this.queryClientResolutionState === 'missing') { + throw createMissingQueryClientError() + } + + return this.result + } + + protected get connectedState(): boolean { + return this.connected + } + + protected queueUpdate(): void { + if (this.updateQueued) { + return + } + + this.updateQueued = true + queueMicrotask(() => { + this.updateQueued = false + if (!this.destroyed) { + this.host.requestUpdate() + } + }) + } + + private queueQueryClientChanged(): void { + if (this.clientChangeQueued) { + return + } + + this.clientChangeQueued = true + queueMicrotask(() => { + this.clientChangeQueued = false + if (!this.destroyed) { + this.onQueryClientChanged() + } + }) + } + + private beginContextResolution(): void { + this.clearContextClient() + this.updateQueryClientResolutionState('awaiting-context') + } + + private queueContextResolution(attempt: number): void { + queueMicrotask(() => { + if ( + this.destroyed || + !this.connected || + attempt !== this.connectionAttempt || + this.queryClientResolutionState !== 'awaiting-context' + ) { + return + } + + this.dispatchContextRequest(attempt) + this.queueInitialContextResolutionCompletion(attempt) + }) + } + + private dispatchContextRequest(attempt: number): void { + if (!('dispatchEvent' in this.host)) { + return + } + + const contextTarget = this.host as ReactiveControllerHost & EventTarget + contextTarget.dispatchEvent( + new ContextEvent( + queryClientContext, + contextTarget as unknown as Element, + (value, unsubscribe) => { + if ( + this.destroyed || + !this.connected || + attempt !== this.connectionAttempt + ) { + unsubscribe?.() + return + } + + if ( + this.contextUnsubscribe && + this.contextUnsubscribe !== unsubscribe + ) { + this.contextUnsubscribe() + } + + const resolutionChanged = this.updateQueryClientResolutionState( + value === undefined ? 'missing' : 'bound', + ) + const clientChanged = this.contextClient !== value + + this.contextClient = value + this.contextUnsubscribe = unsubscribe + + if (resolutionChanged || clientChanged) { + this.queueUpdate() + this.queueQueryClientChanged() + } + }, + true, + ), + ) + } + + private queueInitialContextResolutionCompletion(attempt: number): void { + queueMicrotask(() => { + if ( + this.destroyed || + !this.connected || + attempt !== this.connectionAttempt || + this.queryClientResolutionState !== 'awaiting-context' + ) { + return + } + + if (this.updateQueryClientResolutionState('missing')) { + this.queueUpdate() + this.queueQueryClientChanged() + } + }) + } + + private clearContextClient(): void { + this.contextUnsubscribe?.() + this.contextUnsubscribe = undefined + this.contextClient = undefined + } + + private updateQueryClientResolutionState( + nextState: QueryClientResolutionState, + ): boolean { + if (this.queryClientResolutionState === nextState) { + return false + } + + this.queryClientResolutionState = nextState + return true + } + + protected abstract onConnected(): void + protected abstract onDisconnected(): void + protected abstract onHostUpdate(): void + protected abstract onQueryClientChanged(): void +} diff --git a/packages/lit-query/src/createInfiniteQueryController.ts b/packages/lit-query/src/createInfiniteQueryController.ts new file mode 100644 index 00000000000..4e41edbf818 --- /dev/null +++ b/packages/lit-query/src/createInfiniteQueryController.ts @@ -0,0 +1,394 @@ +import { + InfiniteQueryObserver, + type DefaultError, + type DefaultedInfiniteQueryObserverOptions, + type InfiniteData, + type InfiniteQueryObserverOptions, + type InfiniteQueryObserverResult, + type QueryKey, +} from '@tanstack/query-core' +import type { QueryClient } from '@tanstack/query-core' +import type { ReactiveControllerHost } from 'lit' +import { + createValueAccessor, + readAccessor, + type Accessor, + type ValueAccessor, +} from './accessor.js' +import { createMissingQueryClientError } from './context.js' +import { BaseController } from './controllers/BaseController.js' + +/** + * Options accepted by `createInfiniteQueryController`. + * + * This is the Lit adapter shape for `InfiniteQueryObserverOptions`. Pass it + * directly or through an `Accessor` when the options depend on Lit host state. + */ +export type CreateInfiniteQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam +> + +/** + * Accessor returned by `createInfiniteQueryController`. + * + * Call the accessor or read its `current` property to get the latest infinite + * query result. The attached methods delegate to the active infinite query + * observer. + */ +export type InfiniteQueryResultAccessor = ValueAccessor< + InfiniteQueryObserverResult +> & { + /** Refetches the current infinite query. */ + refetch: InfiniteQueryObserverResult['refetch'] + /** Fetches the next page for the current infinite query. */ + fetchNextPage: InfiniteQueryObserverResult['fetchNextPage'] + /** Fetches the previous page for the current infinite query. */ + fetchPreviousPage: InfiniteQueryObserverResult< + TData, + TError + >['fetchPreviousPage'] + /** Removes the controller from its Lit host and unsubscribes observers. */ + destroy: () => void +} + +function createPendingInfiniteQueryResult< + TData, + TError, +>(): InfiniteQueryObserverResult { + return { + data: undefined, + dataUpdatedAt: 0, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isError: false, + isFetched: false, + isFetchedAfterMount: false, + isFetching: false, + isInitialLoading: false, + isLoading: false, + isLoadingError: false, + isPaused: false, + isPending: true, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isEnabled: true, + isSuccess: false, + fetchStatus: 'idle', + status: 'pending', + refetch: (() => + Promise.reject( + createMissingQueryClientError(), + )) as InfiniteQueryObserverResult['refetch'], + fetchNextPage: (() => + Promise.reject( + createMissingQueryClientError(), + )) as InfiniteQueryObserverResult['fetchNextPage'], + fetchPreviousPage: (() => + Promise.reject( + createMissingQueryClientError(), + )) as InfiniteQueryObserverResult['fetchPreviousPage'], + hasNextPage: false, + hasPreviousPage: false, + isFetchNextPageError: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchingPreviousPage: false, + promise: Promise.resolve(undefined as never), + } as unknown as InfiniteQueryObserverResult +} + +class InfiniteQueryController< + TQueryFnData, + TError, + TData, + TQueryKey extends QueryKey, + TPageParam, +> extends BaseController> { + private readonly options: Accessor< + CreateInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + > + private observer: + | InfiniteQueryObserver + | undefined + private unsubscribe: (() => void) | undefined + private queryClient: QueryClient | undefined + + constructor( + host: ReactiveControllerHost, + options: Accessor< + CreateInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, + queryClient?: QueryClient, + ) { + super(host, createPendingInfiniteQueryResult(), queryClient) + this.options = options + + if (!queryClient) { + return + } + + if (typeof options === 'function') { + return + } + + const defaulted = this.defaultOptions(queryClient) + const observer = new InfiniteQueryObserver(queryClient, defaulted) + this.queryClient = queryClient + this.observer = observer + this.result = observer.getOptimisticResult(defaulted) + } + + protected onConnected(): void { + if (!this.syncClient()) { + return + } + + this.refreshOptions() + this.subscribe() + this.observer?.updateResult() + if (this.observer) { + this.setResult(this.observer.getCurrentResult()) + } + } + + protected onDisconnected(): void { + this.unsubscribeObserver() + this.syncClient() + } + + protected onHostUpdate(): void { + if (typeof this.options !== 'function') { + return + } + + this.refreshOptions() + } + + protected onQueryClientChanged(): void { + if (!this.syncClient() || !this.connectedState) { + return + } + + this.refreshOptions() + this.subscribe() + this.observer?.updateResult() + if (this.observer) { + this.setResult(this.observer.getCurrentResult()) + } + } + + refetch: InfiniteQueryObserverResult['refetch'] = ( + ...args + ) => { + if (!this.refreshOptions()) { + return Promise.reject(createMissingQueryClientError()) + } + + return this.result.refetch(...args) + } + + fetchNextPage: InfiniteQueryObserverResult['fetchNextPage'] = ( + ...args + ) => { + if (!this.refreshOptions()) { + return Promise.reject(createMissingQueryClientError()) + } + + return this.result.fetchNextPage(...args) + } + + fetchPreviousPage: InfiniteQueryObserverResult< + TData, + TError + >['fetchPreviousPage'] = (...args) => { + if (!this.refreshOptions()) { + return Promise.reject(createMissingQueryClientError()) + } + + return this.result.fetchPreviousPage(...args) + } + + private subscribe(): void { + if (!this.observer) { + return + } + + if (this.unsubscribe) { + return + } + + this.unsubscribe = this.observer.subscribe((next) => { + this.setResult(next) + }) + } + + private unsubscribeObserver(): void { + this.unsubscribe?.() + this.unsubscribe = undefined + } + + private syncClient(): boolean { + const nextClient = this.tryGetQueryClient() + if (!nextClient) { + this.unsubscribeObserver() + this.queryClient = undefined + this.observer = undefined + this.setResult(createPendingInfiniteQueryResult()) + return false + } + + if (nextClient === this.queryClient) { + return true + } + + this.unsubscribeObserver() + this.queryClient = nextClient + const options = this.defaultOptions(this.queryClient) + this.observer = new InfiniteQueryObserver(this.queryClient, options) + this.setResult(this.observer.getOptimisticResult(options)) + return true + } + + private refreshOptions(): boolean { + if (!this.syncClient() || !this.observer || !this.queryClient) { + return false + } + + const options = this.defaultOptions(this.queryClient) + this.observer.setOptions(options) + this.setResult(this.observer.getOptimisticResult(options)) + return true + } + + private defaultOptions( + client = this.queryClient, + ): DefaultedInfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > { + if (!client) { + throw createMissingQueryClientError() + } + + const defaulted = client.defaultQueryOptions( + readAccessor(this.options), + ) as DefaultedInfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + ;(defaulted as { _optimisticResults?: 'optimistic' })._optimisticResults = + 'optimistic' + return defaulted + } +} + +/** + * Creates a Lit reactive controller that subscribes the host to an infinite + * query. + * + * The returned accessor is callable and also exposes `current`, `refetch`, + * `fetchNextPage`, `fetchPreviousPage`, and `destroy`. When `options` is a + * function, it is re-read during host updates so query keys and options can + * follow reactive host state. + * + * If `queryClient` is omitted, the controller resolves the client from the + * nearest connected `QueryClientProvider`. + * + * @param host - The Lit reactive controller host that owns the infinite query + * subscription. + * @param options - Infinite query observer options, or a getter that returns + * options. + * @param queryClient - Optional explicit query client. Provide this for + * controllers that should not resolve a client from Lit context. + * @returns An accessor for the latest infinite query result with page helper + * methods. + * + * @example + * ```ts + * import { LitElement, html } from 'lit' + * import { createInfiniteQueryController } from '@tanstack/lit-query' + * + * class ProjectsView extends LitElement { + * private readonly projects = createInfiniteQueryController(this, { + * queryKey: ['projects'], + * queryFn: ({ pageParam }) => fetchProjects(pageParam), + * initialPageParam: 0, + * getNextPageParam: (lastPage) => lastPage.nextCursor, + * }) + * + * render() { + * const query = this.projects() + * + * return html` + * + * ` + * } + * } + * ``` + */ +export function createInfiniteQueryController< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + host: ReactiveControllerHost, + options: Accessor< + CreateInfiniteQueryOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + > + >, + queryClient?: QueryClient, +): InfiniteQueryResultAccessor { + const controller = new InfiniteQueryController(host, options, queryClient) + + return Object.assign( + createValueAccessor(() => controller.current), + { + refetch: controller.refetch, + fetchNextPage: controller.fetchNextPage, + fetchPreviousPage: controller.fetchPreviousPage, + destroy: () => controller.destroy(), + }, + ) +} diff --git a/packages/lit-query/src/createMutationController.ts b/packages/lit-query/src/createMutationController.ts new file mode 100644 index 00000000000..d3133982ddd --- /dev/null +++ b/packages/lit-query/src/createMutationController.ts @@ -0,0 +1,361 @@ +import { + MutationObserver, + type DefaultError, + type MutateOptions, + type MutationObserverOptions, + type MutationObserverResult, +} from '@tanstack/query-core' +import type { QueryClient } from '@tanstack/query-core' +import type { ReactiveControllerHost } from 'lit' +import { + createValueAccessor, + readAccessor, + type Accessor, + type ValueAccessor, +} from './accessor.js' +import { createMissingQueryClientError } from './context.js' +import { BaseController } from './controllers/BaseController.js' + +/** + * Options accepted by `createMutationController`. + * + * This is the Lit adapter shape for `MutationObserverOptions`. Pass it directly + * or through an `Accessor` when the options depend on Lit host state. + */ +export type CreateMutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> = MutationObserverOptions + +/** + * Accessor returned by `createMutationController`. + * + * Call the accessor or read its `current` property to get the latest mutation + * result. The attached methods delegate to the active mutation observer. + */ +export type MutationResultAccessor = + ValueAccessor< + MutationObserverResult + > & { + /** + * Starts the mutation and swallows the returned promise. + * + * Throws synchronously if no `QueryClient` can be resolved. + */ + mutate: ( + variables: TVariables, + options?: MutateOptions, + ) => void + /** + * Starts the mutation and returns the observer promise. + * + * Rejects if no `QueryClient` can be resolved. + */ + mutateAsync: MutationObserverResult< + TData, + TError, + TVariables, + TOnMutateResult + >['mutate'] + /** Resets the mutation observer to its idle state. */ + reset: MutationObserverResult< + TData, + TError, + TVariables, + TOnMutateResult + >['reset'] + /** Removes the controller from its Lit host and unsubscribes observers. */ + destroy: () => void + } + +function createIdleMutationResult< + TData, + TError, + TVariables, + TOnMutateResult, +>(): MutationObserverResult { + return { + context: undefined, + data: undefined, + error: null, + failureCount: 0, + failureReason: null, + isError: false, + isIdle: true, + isPending: false, + isPaused: false, + isSuccess: false, + status: 'idle', + submittedAt: 0, + variables: undefined, + mutate: (() => + Promise.reject( + createMissingQueryClientError(), + )) as MutationObserverResult< + TData, + TError, + TVariables, + TOnMutateResult + >['mutate'], + reset: (() => undefined) as MutationObserverResult< + TData, + TError, + TVariables, + TOnMutateResult + >['reset'], + } as MutationObserverResult +} + +class MutationController< + TData, + TError, + TVariables, + TOnMutateResult, +> extends BaseController< + MutationObserverResult +> { + private readonly options: Accessor< + CreateMutationOptions + > + private observer: + | MutationObserver + | undefined + private unsubscribe: (() => void) | undefined + private queryClient: QueryClient | undefined + + constructor( + host: ReactiveControllerHost, + options: Accessor< + CreateMutationOptions + >, + queryClient?: QueryClient, + ) { + super(host, createIdleMutationResult(), queryClient) + this.options = options + + if (!queryClient) { + return + } + + if (typeof options === 'function') { + return + } + + const observer = new MutationObserver( + queryClient, + this.defaultOptions(queryClient), + ) + this.queryClient = queryClient + this.observer = observer + this.result = observer.getCurrentResult() + } + + protected onConnected(): void { + if (!this.syncClient()) { + return + } + + this.refreshOptions() + this.subscribe() + if (this.observer) { + this.setResult(this.observer.getCurrentResult()) + } + } + + protected onDisconnected(): void { + this.unsubscribeObserver() + this.syncClient() + } + + protected onHostUpdate(): void { + if (typeof this.options !== 'function') { + return + } + + this.refreshOptions() + } + + protected onQueryClientChanged(): void { + if (!this.syncClient() || !this.connectedState) { + return + } + + this.refreshOptions() + this.subscribe() + if (this.observer) { + this.setResult(this.observer.getCurrentResult()) + } + } + + mutate = ( + variables: TVariables, + mutateOptions?: MutateOptions, + ): void => { + if (!this.syncClient() || !this.observer) { + throw createMissingQueryClientError() + } + + void this.observer.mutate(variables, mutateOptions).catch(() => { + // Intentionally swallow in sync mutate path. + }) + } + + mutateAsync: MutationObserverResult< + TData, + TError, + TVariables, + TOnMutateResult + >['mutate'] = (...args) => { + if (!this.syncClient() || !this.observer) { + return Promise.reject(createMissingQueryClientError()) + } + + return this.observer.mutate(...args) + } + + reset: MutationObserverResult< + TData, + TError, + TVariables, + TOnMutateResult + >['reset'] = () => { + if (!this.syncClient() || !this.observer) { + return + } + + this.observer.reset() + this.setResult(this.observer.getCurrentResult()) + } + + private subscribe(): void { + if (!this.observer) { + return + } + + if (this.unsubscribe) { + return + } + + this.unsubscribe = this.observer.subscribe((next) => { + this.setResult(next) + }) + } + + private unsubscribeObserver(): void { + this.unsubscribe?.() + this.unsubscribe = undefined + } + + private syncClient(): boolean { + const nextClient = this.tryGetQueryClient() + if (!nextClient) { + this.unsubscribeObserver() + this.queryClient = undefined + this.observer = undefined + this.setResult(createIdleMutationResult()) + return false + } + + if (nextClient === this.queryClient) { + return true + } + + this.unsubscribeObserver() + this.queryClient = nextClient + this.observer = new MutationObserver( + this.queryClient, + this.defaultOptions(this.queryClient), + ) + this.setResult(this.observer.getCurrentResult()) + return true + } + + private refreshOptions(): boolean { + if (!this.syncClient() || !this.observer || !this.queryClient) { + return false + } + + this.observer.setOptions(this.defaultOptions()) + this.setResult(this.observer.getCurrentResult()) + return true + } + + private defaultOptions( + client = this.queryClient, + ): MutationObserverOptions { + if (!client) { + throw createMissingQueryClientError() + } + + return client.defaultMutationOptions(readAccessor(this.options)) + } +} + +/** + * Creates a Lit reactive controller that subscribes the host to a mutation. + * + * The returned accessor is callable and also exposes `current`, `mutate`, + * `mutateAsync`, `reset`, and `destroy`. When `options` is a function, it is + * re-read during host updates so mutation options can follow reactive host + * state. + * + * If `queryClient` is omitted, the controller resolves the client from the + * nearest connected `QueryClientProvider`. + * + * @param host - The Lit reactive controller host that owns the mutation + * subscription. + * @param options - Mutation observer options, or a getter that returns options. + * @param queryClient - Optional explicit query client. Provide this for + * controllers that should not resolve a client from Lit context. + * @returns An accessor for the latest mutation result with mutation helper + * methods. + * + * @example + * ```ts + * import { LitElement, html } from 'lit' + * import { createMutationController } from '@tanstack/lit-query' + * + * class AddTodoForm extends LitElement { + * private readonly addTodo = createMutationController(this, { + * mutationFn: (title: string) => + * fetch('/api/todos', { method: 'POST', body: JSON.stringify({ title }) }), + * }) + * + * render() { + * const mutation = this.addTodo() + * + * return html` + * + * ` + * } + * } + * ``` + */ +export function createMutationController< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + host: ReactiveControllerHost, + options: Accessor< + CreateMutationOptions + >, + queryClient?: QueryClient, +): MutationResultAccessor { + const controller = new MutationController(host, options, queryClient) + + return Object.assign( + createValueAccessor(() => controller.current), + { + mutate: controller.mutate, + mutateAsync: controller.mutateAsync, + reset: controller.reset, + destroy: () => controller.destroy(), + }, + ) +} diff --git a/packages/lit-query/src/createQueriesController.ts b/packages/lit-query/src/createQueriesController.ts new file mode 100644 index 00000000000..c686cfa9b40 --- /dev/null +++ b/packages/lit-query/src/createQueriesController.ts @@ -0,0 +1,633 @@ +import { + QueriesObserver, + type DefaultError, + type DefinedQueryObserverResult, + type OmitKeyof, + type QueriesObserverOptions, + type QueryFunction, + type QueryKey, + type QueryObserverOptions, + type QueryObserverResult, + type ThrowOnError, +} from '@tanstack/query-core' +import type { QueryClient } from '@tanstack/query-core' +import type { ReactiveControllerHost } from 'lit' +import { + createValueAccessor, + readAccessor, + type Accessor, + type ValueAccessor, +} from './accessor.js' +import { createMissingQueryClientError } from './context.js' +import { BaseController } from './controllers/BaseController.js' + +/** + * Options for one query inside `createQueriesController`. + * + * This mirrors `QueryObserverOptions` and is used by the tuple inference that + * maps each input query to its corresponding result. + */ +export type CreateQueriesInput< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = QueryObserverOptions + +type CreateQueriesInputForController< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = OmitKeyof, never> + +type MAXIMUM_DEPTH = 20 + +type SkipTokenForCreateQueries = symbol + +type GetCreateQueriesInput = T extends { + queryFnData: infer TQueryFnData + error?: infer TError + data: infer TData +} + ? CreateQueriesInputForController + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? CreateQueriesInputForController + : T extends { data: infer TData; error?: infer TError } + ? CreateQueriesInputForController + : T extends [infer TQueryFnData, infer TError, infer TData] + ? CreateQueriesInputForController + : T extends [infer TQueryFnData, infer TError] + ? CreateQueriesInputForController + : T extends [infer TQueryFnData] + ? CreateQueriesInputForController + : T extends { + queryFn?: + | QueryFunction + | SkipTokenForCreateQueries + select?: (data: any) => infer TData + throwOnError?: ThrowOnError + } + ? CreateQueriesInputForController< + TQueryFnData, + unknown extends TError ? DefaultError : TError, + unknown extends TData ? TQueryFnData : TData, + TQueryKey + > + : CreateQueriesInputForController + +type GetDefinedOrUndefinedCreateQueriesResult< + T, + TData, + TError = unknown, +> = T extends { + initialData?: infer TInitialData +} + ? unknown extends TInitialData + ? QueryObserverResult + : TInitialData extends TData + ? DefinedQueryObserverResult + : TInitialData extends () => infer TInitialDataResult + ? unknown extends TInitialDataResult + ? QueryObserverResult + : TInitialDataResult extends TData + ? DefinedQueryObserverResult + : QueryObserverResult + : QueryObserverResult + : QueryObserverResult + +type GetCreateQueriesResult = T extends { + queryFnData: any + error?: infer TError + data: infer TData +} + ? GetDefinedOrUndefinedCreateQueriesResult + : T extends { queryFnData: infer TQueryFnData; error?: infer TError } + ? GetDefinedOrUndefinedCreateQueriesResult + : T extends { data: infer TData; error?: infer TError } + ? GetDefinedOrUndefinedCreateQueriesResult + : T extends [any, infer TError, infer TData] + ? GetDefinedOrUndefinedCreateQueriesResult + : T extends [infer TQueryFnData, infer TError] + ? GetDefinedOrUndefinedCreateQueriesResult + : T extends [infer TQueryFnData] + ? GetDefinedOrUndefinedCreateQueriesResult + : T extends { + queryFn?: + | QueryFunction + | SkipTokenForCreateQueries + select?: (data: any) => infer TData + throwOnError?: ThrowOnError + } + ? GetDefinedOrUndefinedCreateQueriesResult< + T, + unknown extends TData ? TQueryFnData : TData, + unknown extends TError ? DefaultError : TError + > + : QueryObserverResult + +export type CreateQueriesOptions< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetCreateQueriesInput] + : T extends [infer Head, ...infer Tails] + ? CreateQueriesOptions< + [...Tails], + [...TResults, GetCreateQueriesInput], + [...TDepth, 1] + > + : ReadonlyArray extends T + ? T + : T extends Array< + CreateQueriesInputForController< + infer TQueryFnData, + infer TError, + infer TData, + infer TQueryKey + > + > + ? Array< + CreateQueriesInputForController< + TQueryFnData, + TError, + TData, + TQueryKey + > + > + : Array + +/** + * Tuple of query results inferred from the query inputs passed to + * `createQueriesController`. + */ +export type CreateQueriesResults< + T extends Array, + TResults extends Array = [], + TDepth extends ReadonlyArray = [], +> = TDepth['length'] extends MAXIMUM_DEPTH + ? Array + : T extends [] + ? [] + : T extends [infer Head] + ? [...TResults, GetCreateQueriesResult] + : T extends [infer Head, ...infer Tails] + ? CreateQueriesResults< + [...Tails], + [...TResults, GetCreateQueriesResult], + [...TDepth, 1] + > + : { [K in keyof T]: GetCreateQueriesResult } + +/** + * Options accepted by `createQueriesController`. + * + * `queries` can be a static list or a getter that returns the current list. + * `combine` can reshape the array of query results into a single value for the + * returned accessor. + */ +export type CreateQueriesControllerOptions< + TQueryOptions extends Array = Array, + TCombinedResult = CreateQueriesResults, +> = { + /** Query options to observe, or a getter that returns the current options. */ + queries: Accessor< + | readonly [...CreateQueriesOptions] + | readonly [ + ...{ + [K in keyof TQueryOptions]: GetCreateQueriesInput + }, + ] + > + /** Optional function that combines the query result array into one value. */ + combine?: (result: CreateQueriesResults) => TCombinedResult +} + +/** + * Accessor returned by `createQueriesController`. + * + * Call the accessor or read its `current` property to get the latest combined + * value. + */ +export type QueriesResultAccessor = + ValueAccessor & { + /** Removes the controller from its Lit host and unsubscribes observers. */ + destroy: () => void + } + +function createPendingQueryObserverResult(): QueryObserverResult { + return { + data: undefined, + dataUpdatedAt: 0, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isError: false, + isFetched: false, + isFetchedAfterMount: false, + isFetching: false, + isInitialLoading: false, + isLoading: false, + isLoadingError: false, + isPaused: false, + isPending: true, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isEnabled: true, + isSuccess: false, + fetchStatus: 'idle', + status: 'pending', + refetch: (() => + Promise.reject( + createMissingQueryClientError(), + )) as QueryObserverResult['refetch'], + promise: Promise.resolve(undefined as never), + } as unknown as QueryObserverResult +} + +function createPlaceholderQueryObserverResult( + query: QueryObserverOptions, +): QueryObserverResult { + const initialData = + typeof query.initialData === 'function' + ? query.initialData() + : query.initialData + + if (initialData === undefined) { + return createPendingQueryObserverResult() + } + + const data = query.select ? query.select(initialData) : initialData + const initialDataUpdatedAt = + typeof query.initialDataUpdatedAt === 'function' + ? query.initialDataUpdatedAt() + : query.initialDataUpdatedAt + + return { + ...createPendingQueryObserverResult(), + data, + dataUpdatedAt: initialDataUpdatedAt ?? Date.now(), + isPending: false, + isInitialLoading: false, + isLoading: false, + isSuccess: true, + status: 'success', + promise: Promise.resolve(data as never), + } as QueryObserverResult +} + +function resolveQueriesOptions( + optionsAccessor: Accessor< + CreateQueriesControllerOptions + >, + client: QueryClient, +): { + queries: Array + combine: QueriesObserverOptions['combine'] +} { + const resolvedOptions = readAccessor(optionsAccessor) + const resolvedQueries = readAccessor(resolvedOptions.queries) + const combine = + resolvedOptions.combine as QueriesObserverOptions['combine'] + + return { + queries: resolvedQueries.map((query) => { + const defaulted = client.defaultQueryOptions( + query as QueryObserverOptions, + ) + ;(defaulted as { _optimisticResults?: 'optimistic' })._optimisticResults = + 'optimistic' + return defaulted + }), + combine, + } +} + +class QueriesController< + TQueryOptions extends Array, + TCombinedResult, +> extends BaseController { + private readonly options: Accessor< + CreateQueriesControllerOptions + > + private observer: QueriesObserver | undefined + private unsubscribe: (() => void) | undefined + private queryClient: QueryClient | undefined + private explicitInitializationError: unknown | undefined + private placeholderInitialized = false + private placeholderRetryableFailure = true + + constructor( + host: ReactiveControllerHost, + options: Accessor< + CreateQueriesControllerOptions + >, + queryClient?: QueryClient, + ) { + super(host, [] as unknown as TCombinedResult, queryClient) + this.options = options + + queueMicrotask(() => { + this.placeholderRetryableFailure = false + }) + + if (!queryClient) { + return + } + + if (this.shouldRefreshOnHostUpdate()) { + return + } + + this.tryInitializeExplicitClient(queryClient) + } + + protected onConnected(): void { + if (!this.syncClient()) { + return + } + + this.refreshOptions() + this.subscribe() + } + + protected onDisconnected(): void { + this.unsubscribeObserver() + this.syncClient() + } + + protected onHostUpdate(): void { + if (!this.shouldRefreshOnHostUpdate()) { + return + } + + if (!this.refreshOptions()) { + this.setPlaceholderResult() + } + } + + protected onQueryClientChanged(): void { + if (!this.syncClient() || !this.connectedState) { + return + } + + this.refreshOptions() + this.subscribe() + } + + private subscribe(): void { + if (!this.observer) { + return + } + + if (this.unsubscribe) { + return + } + + this.unsubscribe = this.observer.subscribe((next) => { + const { combine } = this.readResolvedOptions() + this.setResult(this.computeResult(next, combine)) + }) + } + + private tryInitializeExplicitClient(queryClient: QueryClient): boolean { + try { + const { queries, combine } = resolveQueriesOptions( + this.options, + queryClient, + ) + const observer = new QueriesObserver(queryClient, queries, { + combine, + } as QueriesObserverOptions) + this.queryClient = queryClient + this.observer = observer + this.result = this.computeResult(observer.getCurrentResult(), combine) + this.explicitInitializationError = undefined + this.placeholderInitialized = true + return true + } catch (error) { + // Retry after construction completes so late host fields used by + // static queries/combine callbacks can finish initializing first. + this.explicitInitializationError = error + this.queryClient = undefined + this.observer = undefined + return false + } + } + + private retryExplicitInitializationIfNeeded(): boolean { + if (!this.explicitInitializationError || this.shouldRefreshOnHostUpdate()) { + return false + } + + const explicitClient = this.tryGetQueryClient() + if (!explicitClient) { + return false + } + + return this.tryInitializeExplicitClient(explicitClient) + } + + private unsubscribeObserver(): void { + this.unsubscribe?.() + this.unsubscribe = undefined + } + + private syncClient(): boolean { + const nextClient = this.tryGetQueryClient() + if (!nextClient) { + this.unsubscribeObserver() + this.queryClient = undefined + this.observer = undefined + this.setPlaceholderResult() + return false + } + + if (nextClient === this.queryClient) { + return true + } + + this.unsubscribeObserver() + this.queryClient = nextClient + const { queries, combine } = this.readResolvedOptions() + this.observer = new QueriesObserver(this.queryClient, queries, { + combine, + } as QueriesObserverOptions) + this.setResult( + this.computeResult(this.observer.getCurrentResult(), combine), + ) + this.placeholderInitialized = true + return true + } + + private refreshOptions(): boolean { + if (!this.syncClient() || !this.observer) { + return false + } + + const { queries, combine } = this.readResolvedOptions() + + this.observer.setQueries(queries, { + combine, + } as QueriesObserverOptions) + + const [rawResult, getCombinedResult] = this.observer.getOptimisticResult( + queries, + combine, + ) + + this.setResult(getCombinedResult(rawResult)) + return true + } + + private readResolvedOptions(client = this.queryClient): { + queries: Array + combine: QueriesObserverOptions['combine'] + } { + if (!client) { + throw createMissingQueryClientError() + } + + return resolveQueriesOptions( + this.options as Accessor< + CreateQueriesControllerOptions + >, + client, + ) + } + + private shouldRefreshOnHostUpdate(): boolean { + if (typeof this.options === 'function') { + return true + } + + return typeof this.options.queries === 'function' + } + + private computeResult( + rawResult: Array, + combine: QueriesObserverOptions['combine'], + ): TCombinedResult { + return (combine ? combine(rawResult) : rawResult) as TCombinedResult + } + + private static createPlaceholderResult( + optionsAccessor: Accessor< + CreateQueriesControllerOptions + >, + ): TCombinedResult { + const resolvedOptions = readAccessor(optionsAccessor) + const queries = readAccessor(resolvedOptions.queries) + const placeholders = queries.map((query) => + createPlaceholderQueryObserverResult(query as QueryObserverOptions), + ) + return ( + resolvedOptions.combine + ? resolvedOptions.combine(placeholders as never) + : placeholders + ) as TCombinedResult + } + + readCurrent(): TCombinedResult { + if (this.retryExplicitInitializationIfNeeded()) { + return this.current + } + + if (this.explicitInitializationError && !this.placeholderRetryableFailure) { + throw this.explicitInitializationError + } + + if (!this.queryClient && !this.observer && !this.placeholderInitialized) { + try { + // Early reads can happen during class-field initialization, before + // accessors referenced by queries/combine are ready. Retry normally + // after construction finishes and only surface errors after that point. + this.setPlaceholderResult() + } catch (error) { + if (!this.placeholderRetryableFailure) { + throw error + } + } + } + + return this.current + } + + private setPlaceholderResult(): void { + this.result = QueriesController.createPlaceholderResult(this.options) + this.placeholderInitialized = true + } +} + +/** + * Creates a Lit reactive controller that subscribes the host to multiple + * queries. + * + * The returned accessor is callable and also exposes `current` and `destroy`. + * When `options` or `options.queries` is a function, it is re-read during host + * updates so the query list can follow reactive host state. + * + * If `queryClient` is omitted, the controller resolves the client from the + * nearest connected `QueryClientProvider`. + * + * @param host - The Lit reactive controller host that owns the queries + * subscription. + * @param options - Queries controller options, or a getter that returns options. + * @param queryClient - Optional explicit query client. Provide this for + * controllers that should not resolve a client from Lit context. + * @returns An accessor for the latest query results, or the value returned by + * `combine`. + * + * @example + * ```ts + * import { LitElement, html } from 'lit' + * import { createQueriesController } from '@tanstack/lit-query' + * + * class DashboardView extends LitElement { + * private readonly dashboard = createQueriesController(this, { + * queries: [ + * { queryKey: ['stats'], queryFn: fetchStats }, + * { queryKey: ['projects'], queryFn: fetchProjects }, + * ], + * combine: ([stats, projects]) => ({ + * stats: stats.data, + * projects: projects.data ?? [], + * isPending: stats.isPending || projects.isPending, + * }), + * }) + * + * render() { + * const dashboard = this.dashboard() + * return html`

Projects: ${dashboard.projects.length}

` + * } + * } + * ``` + */ +export function createQueriesController< + TQueryOptions extends Array, + TCombinedResult = CreateQueriesResults, +>( + host: ReactiveControllerHost, + options: Accessor< + CreateQueriesControllerOptions + >, + queryClient?: QueryClient, +): QueriesResultAccessor { + const controller = new QueriesController(host, options, queryClient) + + return Object.assign( + createValueAccessor(() => controller.readCurrent()), + { + destroy: () => controller.destroy(), + }, + ) +} diff --git a/packages/lit-query/src/createQueryController.ts b/packages/lit-query/src/createQueryController.ts new file mode 100644 index 00000000000..2ea540e3404 --- /dev/null +++ b/packages/lit-query/src/createQueryController.ts @@ -0,0 +1,342 @@ +import { + QueryObserver, + type DefaultError, + type DefaultedQueryObserverOptions, + type QueryKey, + type QueryObserverOptions, + type QueryObserverResult, +} from '@tanstack/query-core' +import type { QueryClient } from '@tanstack/query-core' +import type { ReactiveControllerHost } from 'lit' +import { + createValueAccessor, + readAccessor, + type Accessor, + type ValueAccessor, +} from './accessor.js' +import { createMissingQueryClientError } from './context.js' +import { BaseController } from './controllers/BaseController.js' + +/** + * Options accepted by `createQueryController`. + * + * This is the Lit adapter shape for `QueryObserverOptions`. It can be passed + * directly to `createQueryController`, or wrapped in an `Accessor` when the + * options depend on Lit host state. + */ +export type CreateQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = QueryObserverOptions + +/** + * Accessor returned by `createQueryController`. + * + * Call the accessor or read its `current` property to get the latest query + * result. The attached methods delegate to the active query observer. + */ +export type QueryResultAccessor = ValueAccessor< + QueryObserverResult +> & { + /** Refetches the current query. */ + refetch: QueryObserverResult['refetch'] + /** Resolves with an optimistic query result, fetching first when needed. */ + suspense: () => Promise> + /** Removes the controller from its Lit host and unsubscribes observers. */ + destroy: () => void +} + +function createPendingQueryResult(): QueryObserverResult< + TData, + TError +> { + return { + data: undefined, + dataUpdatedAt: 0, + error: null, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isError: false, + isFetched: false, + isFetchedAfterMount: false, + isFetching: false, + isInitialLoading: false, + isLoading: false, + isLoadingError: false, + isPaused: false, + isPending: true, + isPlaceholderData: false, + isRefetchError: false, + isRefetching: false, + isStale: true, + isEnabled: true, + isSuccess: false, + fetchStatus: 'idle', + status: 'pending', + refetch: (() => + Promise.reject(createMissingQueryClientError())) as QueryObserverResult< + TData, + TError + >['refetch'], + promise: Promise.resolve(undefined as never), + } as unknown as QueryObserverResult +} + +class QueryController< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey extends QueryKey, +> extends BaseController> { + private readonly options: Accessor< + CreateQueryOptions + > + private observer: + | QueryObserver + | undefined + private unsubscribe: (() => void) | undefined + private queryClient: QueryClient | undefined + + constructor( + host: ReactiveControllerHost, + options: Accessor< + CreateQueryOptions + >, + queryClient?: QueryClient, + ) { + const initialClient = queryClient + super(host, createPendingQueryResult(), queryClient) + this.options = options + + if (!initialClient) { + return + } + + if (typeof options === 'function') { + return + } + + const defaulted = this.defaultOptions(initialClient) + const observer = new QueryObserver(initialClient, defaulted) + this.queryClient = initialClient + this.observer = observer + this.result = observer.getOptimisticResult(defaulted) + } + + protected onConnected(): void { + if (!this.syncClient()) { + return + } + + this.refreshOptions() + this.subscribe() + this.observer?.updateResult() + if (this.observer) { + this.setResult(this.observer.getCurrentResult()) + } + } + + protected onDisconnected(): void { + this.unsubscribeObserver() + this.syncClient() + } + + protected onHostUpdate(): void { + if (typeof this.options !== 'function') { + return + } + + this.refreshOptions() + } + + protected onQueryClientChanged(): void { + if (!this.syncClient()) { + return + } + + if (!this.connectedState) { + return + } + + this.refreshOptions() + this.subscribe() + this.observer?.updateResult() + if (this.observer) { + this.setResult(this.observer.getCurrentResult()) + } + } + + refetch: QueryObserverResult['refetch'] = (...args) => { + if (!this.refreshOptions()) { + return Promise.reject(createMissingQueryClientError()) + } + + return this.result.refetch(...args) + } + + suspense = async (): Promise> => { + if (!this.syncClient() || !this.observer || !this.queryClient) { + throw createMissingQueryClientError() + } + + const options = this.defaultOptions(this.queryClient) + this.observer.setOptions(options) + const optimistic = this.observer.getOptimisticResult(options) + if (options.enabled !== false && optimistic.isStale) { + return this.observer.fetchOptimistic(options) + } + + return optimistic + } + + private subscribe(): void { + if (!this.observer) { + return + } + + if (this.unsubscribe) { + return + } + + this.unsubscribe = this.observer.subscribe((next) => { + this.setResult(next) + }) + } + + private unsubscribeObserver(): void { + this.unsubscribe?.() + this.unsubscribe = undefined + } + + private syncClient(): boolean { + const nextClient = this.tryGetQueryClient() + if (!nextClient) { + this.unsubscribeObserver() + this.queryClient = undefined + this.observer = undefined + this.setResult(createPendingQueryResult()) + return false + } + + if (nextClient === this.queryClient && this.observer) { + return true + } + + this.unsubscribeObserver() + this.queryClient = nextClient + const options = this.defaultOptions() + this.observer = new QueryObserver(this.queryClient, options) + this.setResult(this.observer.getOptimisticResult(options)) + return true + } + + private refreshOptions(): boolean { + if (!this.syncClient() || !this.observer) { + return false + } + + const options = this.defaultOptions(this.queryClient) + this.observer.setOptions(options) + this.setResult(this.observer.getOptimisticResult(options)) + return true + } + + private defaultOptions( + client = this.queryClient, + ): DefaultedQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + > { + const resolvedClient = client ?? this.tryGetQueryClient() + if (!resolvedClient) { + throw createMissingQueryClientError() + } + + this.queryClient = resolvedClient + const defaulted = resolvedClient.defaultQueryOptions( + readAccessor(this.options), + ) as DefaultedQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryData, + TQueryKey + > + ;(defaulted as { _optimisticResults?: 'optimistic' })._optimisticResults = + 'optimistic' + return defaulted + } +} + +/** + * Creates a Lit reactive controller that subscribes the host to a single query. + * + * The returned accessor is callable and also exposes `current`, `refetch`, + * `suspense`, and `destroy`. When `options` is a function, it is re-read during + * host updates so query keys and options can follow reactive host state. + * + * If `queryClient` is omitted, the controller resolves the client from the + * nearest connected `QueryClientProvider`. + * + * @param host - The Lit reactive controller host that owns the query + * subscription. + * @param options - Query observer options, or a getter that returns options. + * @param queryClient - Optional explicit query client. Provide this for + * controllers that should not resolve a client from Lit context. + * @returns An accessor for the latest query result with query helper methods. + * + * @example + * ```ts + * import { LitElement, html } from 'lit' + * import { createQueryController } from '@tanstack/lit-query' + * + * class TodosView extends LitElement { + * private readonly todos = createQueryController(this, { + * queryKey: ['todos'], + * queryFn: async () => fetch('/api/todos').then((r) => r.json()), + * }) + * + * render() { + * const query = this.todos() + * + * if (query.isPending) return html`Loading...` + * if (query.isError) return html`Error` + * + * return html`
    ${query.data.map((todo) => html`
  • ${todo.title}
  • `)}
` + * } + * } + * ``` + */ +export function createQueryController< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + host: ReactiveControllerHost, + options: Accessor< + CreateQueryOptions + >, + queryClient?: QueryClient, +): QueryResultAccessor { + const controller = new QueryController(host, options, queryClient) + + return Object.assign( + createValueAccessor(() => controller.current), + { + refetch: controller.refetch, + suspense: controller.suspense, + destroy: () => controller.destroy(), + }, + ) +} diff --git a/packages/lit-query/src/index.ts b/packages/lit-query/src/index.ts new file mode 100644 index 00000000000..bf87f1dc45b --- /dev/null +++ b/packages/lit-query/src/index.ts @@ -0,0 +1,72 @@ +/* istanbul ignore file */ + +export * from '@tanstack/query-core' + +export type { Accessor, ValueAccessor } from './accessor.js' + +export { + getDefaultQueryClient, + queryClientContext, + registerDefaultQueryClient, + resolveQueryClient, + unregisterDefaultQueryClient, + useQueryClient, +} from './context.js' + +export { QueryClientProvider } from './QueryClientProvider.js' + +export type { + CreateInfiniteQueryOptions, + InfiniteQueryResultAccessor, +} from './createInfiniteQueryController.js' +export { createInfiniteQueryController } from './createInfiniteQueryController.js' + +export type { + CreateMutationOptions, + MutationResultAccessor, +} from './createMutationController.js' +export { createMutationController } from './createMutationController.js' + +export type { + CreateQueriesControllerOptions, + CreateQueriesInput, + QueriesResultAccessor, +} from './createQueriesController.js' +export { createQueriesController } from './createQueriesController.js' + +export type { + CreateQueryOptions, + QueryResultAccessor, +} from './createQueryController.js' +export { createQueryController } from './createQueryController.js' + +export type { IsFetchingAccessor } from './useIsFetching.js' +export { useIsFetching } from './useIsFetching.js' + +export type { IsMutatingAccessor } from './useIsMutating.js' +export { useIsMutating } from './useIsMutating.js' + +export type { + MutationStateAccessor, + MutationStateOptions, +} from './useMutationState.js' +export { useMutationState } from './useMutationState.js' + +export type { + DefinedInitialDataOptions, + UndefinedInitialDataOptions, + UnusedSkipTokenOptions, +} from './queryOptions.js' +export { queryOptions } from './queryOptions.js' + +export { infiniteQueryOptions } from './infiniteQueryOptions.js' +export { mutationOptions } from './mutationOptions.js' + +export type { + InfiniteQueryControllerOptions, + MutationControllerOptions, + MutationControllerResult, + QueriesControllerOptions, + QueryControllerOptions, + QueryControllerResult, +} from './types.js' diff --git a/packages/lit-query/src/infiniteQueryOptions.ts b/packages/lit-query/src/infiniteQueryOptions.ts new file mode 100644 index 00000000000..f4bc8851ebe --- /dev/null +++ b/packages/lit-query/src/infiniteQueryOptions.ts @@ -0,0 +1,48 @@ +import type { + DefaultError, + InfiniteData, + InfiniteQueryObserverOptions, + QueryKey, +} from '@tanstack/query-core' + +/** + * Preserves and types infinite query options for reuse across Lit Query APIs. + * + * @param options - Infinite query options to preserve. + * @returns The same options object. + * + * @example + * ```ts + * import { infiniteQueryOptions } from '@tanstack/lit-query' + * + * const projectsOptions = infiniteQueryOptions({ + * queryKey: ['projects'], + * queryFn: ({ pageParam }) => fetchProjects(pageParam), + * initialPageParam: 0, + * getNextPageParam: (lastPage) => lastPage.nextCursor, + * }) + * ``` + */ +export function infiniteQueryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +>( + options: InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam + >, +): InfiniteQueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryKey, + TPageParam +> { + return options +} diff --git a/packages/lit-query/src/mutationOptions.ts b/packages/lit-query/src/mutationOptions.ts new file mode 100644 index 00000000000..ce5ef1b085e --- /dev/null +++ b/packages/lit-query/src/mutationOptions.ts @@ -0,0 +1,31 @@ +import type { + DefaultError, + MutationObserverOptions, +} from '@tanstack/query-core' + +/** + * Preserves and types mutation options for reuse across Lit Query APIs. + * + * @param options - Mutation options to preserve. + * @returns The same options object. + * + * @example + * ```ts + * import { mutationOptions } from '@tanstack/lit-query' + * + * const addTodoOptions = mutationOptions({ + * mutationKey: ['add-todo'], + * mutationFn: (title: string) => addTodo(title), + * }) + * ``` + */ +export function mutationOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +>( + options: MutationObserverOptions, +): MutationObserverOptions { + return options +} diff --git a/packages/lit-query/src/queryOptions.ts b/packages/lit-query/src/queryOptions.ts new file mode 100644 index 00000000000..c97c6683970 --- /dev/null +++ b/packages/lit-query/src/queryOptions.ts @@ -0,0 +1,143 @@ +import type { + DataTag, + DefaultError, + InitialDataFunction, + NonUndefinedGuard, + OmitKeyof, + QueryFunction, + QueryKey, + QueryObserverOptions, + SkipToken, +} from '@tanstack/query-core' + +/** + * Query options with `initialData` that guarantees defined query data. + */ +export type DefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Omit< + QueryObserverOptions, + 'queryFn' +> & { + initialData: + | NonUndefinedGuard + | (() => NonUndefinedGuard) + queryFn?: QueryFunction +} + +/** + * Query options where `queryFn` is present and not a `skipToken`. + */ +export type UnusedSkipTokenOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = OmitKeyof< + QueryObserverOptions, + 'queryFn' +> & { + queryFn?: Exclude< + QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey + >['queryFn'], + SkipToken | undefined + > +} + +/** + * Query options where `initialData` can be omitted or undefined. + */ +export type UndefinedInitialDataOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = QueryObserverOptions< + TQueryFnData, + TError, + TData, + TQueryFnData, + TQueryKey +> & { + initialData?: + | undefined + | InitialDataFunction> + | NonUndefinedGuard +} + +/** + * Brands query options so the `queryKey` carries the query function data and + * error types across TanStack Query APIs. + * + * @param options - Query options to preserve and brand. + * @returns The same options object with a typed `queryKey`. + * + * @example + * ```ts + * import { queryOptions } from '@tanstack/lit-query' + * + * const todosOptions = queryOptions({ + * queryKey: ['todos'], + * queryFn: fetchTodos, + * initialData: [], + * }) + * ``` + */ +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: DefinedInitialDataOptions, +): DefinedInitialDataOptions & { + queryKey: DataTag +} + +/** + * Brands query options so the `queryKey` carries the query function data and + * error types across TanStack Query APIs. + * + * @param options - Query options to preserve and brand. + * @returns The same options object with a typed `queryKey`. + */ +export function queryOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +>( + options: UnusedSkipTokenOptions, +): UnusedSkipTokenOptions & { + queryKey: DataTag +} + +/** + * Brands query options so the `queryKey` carries the query function data and + * error types across TanStack Query APIs. + * + * @param options - Query options to preserve and brand. + * @returns The same options object with a typed `queryKey`. + */ +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/lit-query/src/tests/base-controller.test.ts b/packages/lit-query/src/tests/base-controller.test.ts new file mode 100644 index 00000000000..8587632fb8d --- /dev/null +++ b/packages/lit-query/src/tests/base-controller.test.ts @@ -0,0 +1,111 @@ +import { describe, expect, it } from 'vitest' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { QueryClient } from '@tanstack/query-core' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { BaseController } from '../controllers/BaseController.js' + +const providerTagName = 'test-query-client-provider-base-controller' +if (!customElements.get(providerTagName)) { + customElements.define(providerTagName, QueryClientProvider) +} + +class RecordingController extends BaseController { + readonly lifecycle: string[] = [] + + constructor(host: ReactiveControllerHost) { + super(host, 'pending') + } + + protected onConnected(): void { + this.lifecycle.push( + `connected:${this.tryGetQueryClient() ? 'client' : 'missing'}`, + ) + } + + protected onDisconnected(): void {} + + protected onHostUpdate(): void {} + + protected onQueryClientChanged(): void { + this.lifecycle.push( + `changed:${this.tryGetQueryClient() ? 'client' : 'missing'}`, + ) + } +} + +class AlreadyConnectedContextHost + extends HTMLElement + implements ReactiveControllerHost +{ + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + if (this.isConnected) { + controller.hostConnected?.() + } + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + + connectedCallback(): void { + for (const controller of this.controllers) { + controller.hostConnected?.() + } + } + + disconnectedCallback(): void { + for (const controller of this.controllers) { + controller.hostDisconnected?.() + } + } + + attachController(): RecordingController { + return new RecordingController(this) + } +} + +const hostTagName = 'test-base-controller-context-host' +if (!customElements.get(hostTagName)) { + customElements.define(hostTagName, AlreadyConnectedContextHost) +} + +describe('BaseController', () => { + it('defers provider resolution on already-connected hosts until after onConnected', async () => { + const client = new QueryClient() + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + + const host = document.createElement( + hostTagName, + ) as AlreadyConnectedContextHost + provider.append(host) + + document.body.append(provider) + await provider.updateComplete + + const controller = host.attachController() + await Promise.resolve() + await Promise.resolve() + + expect(controller.lifecycle).toEqual([ + 'connected:missing', + 'changed:client', + ]) + + controller.destroy() + provider.remove() + await Promise.resolve() + }) +}) diff --git a/packages/lit-query/src/tests/client-switch-controllers.test.ts b/packages/lit-query/src/tests/client-switch-controllers.test.ts new file mode 100644 index 00000000000..3e60bfd88b9 --- /dev/null +++ b/packages/lit-query/src/tests/client-switch-controllers.test.ts @@ -0,0 +1,482 @@ +import { describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { createInfiniteQueryController } from '../createInfiniteQueryController.js' +import { createMutationController } from '../createMutationController.js' +import { createQueriesController } from '../createQueriesController.js' +import { waitFor } from './testHost.js' + +const providerTagName = 'test-query-client-provider-switch' +if (!customElements.get(providerTagName)) { + customElements.define(providerTagName, QueryClientProvider) +} + +class BaseControllerHostElement + extends HTMLElement + implements ReactiveControllerHost +{ + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + + connectedCallback(): void { + for (const controller of this.controllers) { + controller.hostConnected?.() + } + } + + disconnectedCallback(): void { + for (const controller of this.controllers) { + controller.hostDisconnected?.() + } + } +} + +class MutationSwitchHostElement extends BaseControllerHostElement { + mutationCalls = 0 + readonly mutationKey = ['switch-mutation'] as const + + readonly mutation = createMutationController(this, () => ({ + mutationKey: this.mutationKey, + mutationFn: async (value: number) => { + this.mutationCalls += 1 + return value + 1 + }, + })) +} + +const mutationHostTagName = 'test-mutation-switch-host' +if (!customElements.get(mutationHostTagName)) { + customElements.define(mutationHostTagName, MutationSwitchHostElement) +} + +class QueriesSwitchHostElement extends BaseControllerHostElement { + queryCalls = 0 + readonly queryKey = ['switch-queries'] as const + + readonly queries = createQueriesController(this, () => ({ + queries: [ + { + queryKey: this.queryKey, + queryFn: async () => { + this.queryCalls += 1 + return `q-${this.queryCalls}` + }, + retry: false, + }, + ] as const, + combine: (results) => results.map((result) => result.data), + })) +} + +const queriesHostTagName = 'test-queries-switch-host' +if (!customElements.get(queriesHostTagName)) { + customElements.define(queriesHostTagName, QueriesSwitchHostElement) +} + +class InfiniteSwitchHostElement extends BaseControllerHostElement { + pageCalls = 0 + readonly queryKey = ['switch-infinite'] as const + + readonly infinite = createInfiniteQueryController(this, () => ({ + queryKey: this.queryKey, + initialPageParam: 0, + queryFn: async ({ pageParam }) => { + this.pageCalls += 1 + return Number(pageParam) + }, + getNextPageParam: (lastPage: number) => + lastPage < 1 ? lastPage + 1 : undefined, + retry: false, + })) +} + +const infiniteHostTagName = 'test-infinite-switch-host' +if (!customElements.get(infiniteHostTagName)) { + customElements.define(infiniteHostTagName, InfiniteSwitchHostElement) +} + +describe('LQ-003 client-switch coverage across controllers', () => { + it('switches mutation controller to new provider client while connected', async () => { + const clientA = new QueryClient() + const clientB = new QueryClient() + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = clientA + document.body.append(provider) + await provider.updateComplete + + const consumer = document.createElement( + mutationHostTagName, + ) as MutationSwitchHostElement + provider.append(consumer) + + await Promise.resolve() + await Promise.resolve() + await expect(consumer.mutation.mutateAsync(1)).resolves.toBe(2) + + const countAAfterFirst = clientA + .getMutationCache() + .findAll({ mutationKey: consumer.mutationKey }).length + expect(countAAfterFirst).toBeGreaterThan(0) + + provider.client = clientB + await provider.updateComplete + await Promise.resolve() + + await expect(consumer.mutation.mutateAsync(2)).resolves.toBe(3) + + const countAAfterSecond = clientA + .getMutationCache() + .findAll({ mutationKey: consumer.mutationKey }).length + const countBAfterSecond = clientB + .getMutationCache() + .findAll({ mutationKey: consumer.mutationKey }).length + + expect(countAAfterSecond).toBe(countAAfterFirst) + expect(countBAfterSecond).toBeGreaterThan(0) + + consumer.mutation.destroy() + provider.remove() + await Promise.resolve() + }) + + it('switches queries controller to new provider client while connected', async () => { + const clientA = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const clientB = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = clientA + document.body.append(provider) + await provider.updateComplete + + const consumer = document.createElement( + queriesHostTagName, + ) as QueriesSwitchHostElement + provider.append(consumer) + + await Promise.resolve() + await waitFor(() => typeof consumer.queries()[0] === 'string') + + const cacheAEntryBeforeSwitch = clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + expect(cacheAEntryBeforeSwitch?.getObserversCount()).toBe(1) + + provider.client = clientB + await provider.updateComplete + + await waitFor(() => { + const cacheBEntry = clientB + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + return Boolean(cacheBEntry && cacheBEntry.getObserversCount() === 1) + }) + + const cacheAEntryAfterSwitch = clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + expect(cacheAEntryAfterSwitch?.getObserversCount() ?? 0).toBe(0) + + void clientB.invalidateQueries({ queryKey: consumer.queryKey }) + await waitFor(() => consumer.queryCalls >= 2) + + consumer.queries.destroy() + provider.remove() + await Promise.resolve() + }) + + it('switches infinite query controller to new provider client while connected', async () => { + const clientA = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const clientB = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = clientA + document.body.append(provider) + await provider.updateComplete + + const consumer = document.createElement( + infiniteHostTagName, + ) as InfiniteSwitchHostElement + provider.append(consumer) + + await Promise.resolve() + await waitFor(() => consumer.infinite().isSuccess) + expect(consumer.infinite().data?.pages).toEqual([0]) + + const cacheAEntryBeforeSwitch = clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + expect(cacheAEntryBeforeSwitch?.getObserversCount()).toBe(1) + + provider.client = clientB + await provider.updateComplete + + await waitFor(() => { + const cacheBEntry = clientB + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + return Boolean(cacheBEntry && cacheBEntry.getObserversCount() === 1) + }) + await waitFor( + () => + consumer.infinite().isSuccess && + (consumer.infinite().data?.pages.length ?? 0) >= 1, + 4000, + ) + await waitFor(() => consumer.infinite().hasNextPage === true, 4000) + + const cacheAEntryAfterSwitch = clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + expect(cacheAEntryAfterSwitch?.getObserversCount() ?? 0).toBe(0) + + await consumer.infinite.fetchNextPage() + await waitFor(() => consumer.infinite().data?.pages.length === 2, 4000) + expect(consumer.infinite().data?.pages).toEqual([0, 1]) + + consumer.infinite.destroy() + provider.remove() + await Promise.resolve() + }) + + it('reparents mutation controller under a different provider and binds the new nearest client', async () => { + const clientA = new QueryClient() + const clientB = new QueryClient() + + const providerA = document.createElement( + providerTagName, + ) as QueryClientProvider + providerA.client = clientA + const providerB = document.createElement( + providerTagName, + ) as QueryClientProvider + providerB.client = clientB + + const consumer = document.createElement( + mutationHostTagName, + ) as MutationSwitchHostElement + providerA.append(consumer) + + document.body.append(providerA) + await providerA.updateComplete + await Promise.resolve() + await Promise.resolve() + + await expect(consumer.mutation.mutateAsync(1)).resolves.toBe(2) + + consumer.remove() + await new Promise((resolve) => setTimeout(resolve, 0)) + providerA.remove() + + providerB.append(consumer) + document.body.append(providerB) + await providerB.updateComplete + await Promise.resolve() + await Promise.resolve() + + await expect(consumer.mutation.mutateAsync(2)).resolves.toBe(3) + expect( + clientA.getMutationCache().findAll({ mutationKey: consumer.mutationKey }) + .length, + ).toBeGreaterThan(0) + expect( + clientB.getMutationCache().findAll({ mutationKey: consumer.mutationKey }) + .length, + ).toBeGreaterThan(0) + + consumer.mutation.destroy() + providerB.remove() + await Promise.resolve() + }) + + it('reparents queries controller under a different provider without cross-tree leakage', async () => { + const clientA = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const clientB = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const providerA = document.createElement( + providerTagName, + ) as QueryClientProvider + providerA.client = clientA + const providerB = document.createElement( + providerTagName, + ) as QueryClientProvider + providerB.client = clientB + + const consumer = document.createElement( + queriesHostTagName, + ) as QueriesSwitchHostElement + providerA.append(consumer) + + document.body.append(providerA) + await providerA.updateComplete + + await waitFor(() => typeof consumer.queries()[0] === 'string') + + consumer.remove() + await waitFor( + () => + (clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount() ?? 0) === 0, + ) + providerA.remove() + + providerB.append(consumer) + document.body.append(providerB) + await providerB.updateComplete + await waitFor( + () => + typeof consumer.queries()[0] === 'string' && consumer.queryCalls >= 2, + ) + + expect( + clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount() ?? 0, + ).toBe(0) + expect( + clientB + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount(), + ).toBe(1) + + consumer.queries.destroy() + providerA.remove() + providerB.remove() + await Promise.resolve() + }) + + it('reparents infinite query controller under a different provider and binds the new nearest client', async () => { + const clientA = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const clientB = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const providerA = document.createElement( + providerTagName, + ) as QueryClientProvider + providerA.client = clientA + const providerB = document.createElement( + providerTagName, + ) as QueryClientProvider + providerB.client = clientB + + const consumer = document.createElement( + infiniteHostTagName, + ) as InfiniteSwitchHostElement + providerA.append(consumer) + + document.body.append(providerA) + await providerA.updateComplete + + await waitFor(() => consumer.infinite().isSuccess) + + consumer.remove() + await waitFor( + () => + (clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount() ?? 0) === 0, + ) + providerA.remove() + + providerB.append(consumer) + document.body.append(providerB) + await providerB.updateComplete + await waitFor( + () => + consumer.infinite().isSuccess && + (consumer.infinite().data?.pages.length ?? 0) >= 1 && + consumer.pageCalls >= 2, + ) + + expect( + clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount() ?? 0, + ).toBe(0) + expect( + clientB + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount(), + ).toBe(1) + + consumer.infinite.destroy() + providerA.remove() + providerB.remove() + await Promise.resolve() + }) +}) diff --git a/packages/lit-query/src/tests/context-provider.test.ts b/packages/lit-query/src/tests/context-provider.test.ts new file mode 100644 index 00000000000..8428e0b75f1 --- /dev/null +++ b/packages/lit-query/src/tests/context-provider.test.ts @@ -0,0 +1,213 @@ +import { describe, expect, it, vi } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import { createQueryController } from '../createQueryController.js' +import { + getDefaultQueryClient, + resolveQueryClient, + useQueryClient, +} from '../index.js' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { + TestElementHost, + waitFor, + waitForMissingQueryClient, +} from './testHost.js' + +const tagName = 'test-query-client-provider' +if (!customElements.get(tagName)) { + customElements.define(tagName, QueryClientProvider) +} + +class ProviderContextConsumerElement extends TestElementHost { + readonly query = createQueryController(this, { + queryKey: ['provider-context-consumer'] as const, + queryFn: async () => 'ok', + retry: false, + }) +} + +const consumerTagName = 'test-query-client-provider-consumer' +if (!customElements.get(consumerTagName)) { + customElements.define(consumerTagName, ProviderContextConsumerElement) +} + +describe('QueryClientProvider/context', () => { + it('registers and unregisters the default query client for public helpers', async () => { + const client = new QueryClient() + const provider = document.createElement(tagName) as QueryClientProvider + provider.client = client + + document.body.append(provider) + await provider.updateComplete + + expect(useQueryClient()).toBe(client) + expect(resolveQueryClient()).toBe(client) + + provider.remove() + await Promise.resolve() + + expect(() => useQueryClient()).toThrowError(/No QueryClient available/) + }) + + it('prefers an explicit client in resolveQueryClient', () => { + const explicit = new QueryClient() + expect(resolveQueryClient(explicit)).toBe(explicit) + }) + + it('keeps the default client registered until the last provider using it disconnects', async () => { + const client = new QueryClient() + const providerA = document.createElement(tagName) as QueryClientProvider + const providerB = document.createElement(tagName) as QueryClientProvider + providerA.client = client + providerB.client = client + + document.body.append(providerA) + document.body.append(providerB) + await providerA.updateComplete + await providerB.updateComplete + + expect(useQueryClient()).toBe(client) + + providerB.remove() + await Promise.resolve() + + expect(useQueryClient()).toBe(client) + + providerA.remove() + await Promise.resolve() + + expect(() => useQueryClient()).toThrowError(/No QueryClient available/) + }) + + it('throws when multiple different providers make global lookup ambiguous', async () => { + const clientA = new QueryClient() + const clientB = new QueryClient() + const providerA = document.createElement(tagName) as QueryClientProvider + const providerB = document.createElement(tagName) as QueryClientProvider + providerA.client = clientA + providerB.client = clientB + + document.body.append(providerA) + document.body.append(providerB) + await providerA.updateComplete + await providerB.updateComplete + + expect(getDefaultQueryClient()).toBeUndefined() + expect(() => useQueryClient()).toThrowError( + /Multiple QueryClients are mounted/, + ) + expect(() => resolveQueryClient()).toThrowError( + /Multiple QueryClients are mounted/, + ) + + providerB.remove() + await Promise.resolve() + + expect(getDefaultQueryClient()).toBe(clientA) + expect(useQueryClient()).toBe(clientA) + + providerA.remove() + await Promise.resolve() + }) + + it('requires an explicit client before connect', () => { + const provider = document.createElement(tagName) as QueryClientProvider + expect(() => provider.connectedCallback()).toThrowError( + /No QueryClient available/, + ) + }) + + it('S8: provider swap while disconnected preserves mount/unmount contract', async () => { + const clientA = new QueryClient() + const clientB = new QueryClient() + + const mountA = vi.spyOn(clientA, 'mount') + const unmountA = vi.spyOn(clientA, 'unmount') + const mountB = vi.spyOn(clientB, 'mount') + const unmountB = vi.spyOn(clientB, 'unmount') + + const provider = document.createElement(tagName) as QueryClientProvider + provider.client = clientA + + document.body.append(provider) + await provider.updateComplete + + expect(mountA).toHaveBeenCalledTimes(1) + expect(unmountA).toHaveBeenCalledTimes(0) + expect(mountB).toHaveBeenCalledTimes(0) + expect(unmountB).toHaveBeenCalledTimes(0) + + provider.remove() + await Promise.resolve() + + expect(unmountA).toHaveBeenCalledTimes(1) + expect(mountB).toHaveBeenCalledTimes(0) + + provider.client = clientB + await provider.updateComplete + + expect(unmountA).toHaveBeenCalledTimes(1) + expect(mountB).toHaveBeenCalledTimes(0) + + document.body.append(provider) + await provider.updateComplete + + expect(mountA).toHaveBeenCalledTimes(1) + expect(unmountA).toHaveBeenCalledTimes(1) + expect(mountB).toHaveBeenCalledTimes(1) + expect(unmountB).toHaveBeenCalledTimes(0) + + provider.remove() + await Promise.resolve() + + expect(unmountB).toHaveBeenCalledTimes(1) + + mountA.mockRestore() + unmountA.mockRestore() + mountB.mockRestore() + unmountB.mockRestore() + }) + + it('LC-PROVIDER-01: invalid connected client updates tear down the mounted client before surfacing the error', async () => { + const client = new QueryClient() + const mount = vi.spyOn(client, 'mount') + const unmount = vi.spyOn(client, 'unmount') + + const provider = document.createElement(tagName) as QueryClientProvider + const consumer = document.createElement( + consumerTagName, + ) as ProviderContextConsumerElement + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + await waitFor(() => consumer.query().isSuccess) + + expect(mount).toHaveBeenCalledTimes(1) + expect(unmount).toHaveBeenCalledTimes(0) + expect(consumer.query().data).toBe('ok') + + provider.client = undefined as unknown as QueryClient + await expect(provider.updateComplete).rejects.toThrow( + /No QueryClient available/, + ) + expect(unmount).toHaveBeenCalledTimes(1) + expect(getDefaultQueryClient()).toBeUndefined() + expect(() => useQueryClient()).toThrowError(/No QueryClient available/) + await waitForMissingQueryClient(() => consumer.query()) + await expect(consumer.query.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + + consumer.query.destroy() + provider.remove() + await Promise.resolve() + + expect(unmount).toHaveBeenCalledTimes(1) + + mount.mockRestore() + unmount.mockRestore() + }) +}) diff --git a/packages/lit-query/src/tests/counters-and-state.test.ts b/packages/lit-query/src/tests/counters-and-state.test.ts new file mode 100644 index 00000000000..3a28b5a5161 --- /dev/null +++ b/packages/lit-query/src/tests/counters-and-state.test.ts @@ -0,0 +1,648 @@ +import { describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { createMutationController } from '../createMutationController.js' +import { createQueryController } from '../createQueryController.js' +import { useIsFetching } from '../useIsFetching.js' +import { useIsMutating } from '../useIsMutating.js' +import { useMutationState } from '../useMutationState.js' +import { + TestControllerHost, + TestElementHost, + waitFor, + waitForMissingQueryClient, +} from './testHost.js' + +const providerTagName = 'test-query-client-provider-counters' +if (!customElements.get(providerTagName)) { + customElements.define(providerTagName, QueryClientProvider) +} + +let explicitCountersClient: QueryClient | undefined + +class ContextCountersHostElement extends TestElementHost { + readonly queryKey = ['context-counters', 'query'] as const + readonly mutationKey = ['context-counters', 'mutation'] as const + + private resolveQuery: (() => void) | undefined + private resolveMutation: (() => void) | undefined + + readonly query = createQueryController( + this, + { + queryKey: this.queryKey, + queryFn: () => + new Promise((resolve) => { + this.resolveQuery = () => resolve('query-ok') + }), + retry: false, + }, + explicitCountersClient, + ) + + readonly mutation = createMutationController( + this, + { + mutationKey: this.mutationKey, + mutationFn: () => + new Promise((resolve) => { + this.resolveMutation = () => resolve('mutation-ok') + }), + }, + explicitCountersClient, + ) + + readonly isFetching = useIsFetching( + this, + { queryKey: this.queryKey }, + explicitCountersClient, + ) + + readonly isMutating = useIsMutating( + this, + { mutationKey: this.mutationKey }, + explicitCountersClient, + ) + + readonly mutationStatuses = useMutationState( + this, + { + filters: { mutationKey: this.mutationKey }, + select: (mutation) => mutation.state.status, + }, + explicitCountersClient, + ) + + resolvePendingQuery(): void { + this.resolveQuery?.() + } + + resolvePendingMutation(): void { + this.resolveMutation?.() + } +} + +const contextCountersTagName = 'test-context-counters-host' +if (!customElements.get(contextCountersTagName)) { + customElements.define(contextCountersTagName, ContextCountersHostElement) +} + +describe('useIsFetching/useIsMutating/useMutationState', () => { + it('LC-COUNTERS-01: pre-connect placeholders stay zero/empty until a provider binds', async () => { + const consumer = document.createElement( + contextCountersTagName, + ) as ContextCountersHostElement + + expect(consumer.query().status).toBe('pending') + expect(consumer.isFetching()).toBe(0) + expect(consumer.isMutating()).toBe(0) + expect(consumer.mutationStatuses()).toEqual([]) + + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }) + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor(() => consumer.isFetching() === 1) + consumer.resolvePendingQuery() + await waitFor(() => consumer.query().isSuccess) + await waitFor(() => consumer.isFetching() === 0) + + consumer.mutation.mutate() + await waitFor(() => consumer.isMutating() === 1) + consumer.resolvePendingMutation() + await waitFor(() => consumer.isMutating() === 0) + await waitFor(() => consumer.mutationStatuses().includes('success')) + + consumer.query.destroy() + consumer.mutation.destroy() + consumer.isFetching.destroy() + consumer.isMutating.destroy() + consumer.mutationStatuses.destroy() + provider.remove() + await Promise.resolve() + }) + + it('LC-COUNTERS-02: explicit client takes precedence over provider context', async () => { + const explicitClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }) + const providerClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }) + explicitCountersClient = explicitClient + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = providerClient + + const consumer = document.createElement( + contextCountersTagName, + ) as ContextCountersHostElement + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor(() => consumer.isFetching() === 1) + consumer.resolvePendingQuery() + await waitFor(() => consumer.query().isSuccess) + + consumer.mutation.mutate() + await waitFor(() => consumer.isMutating() === 1) + consumer.resolvePendingMutation() + await waitFor(() => consumer.isMutating() === 0) + + expect( + explicitClient.getQueryCache().find({ queryKey: consumer.queryKey }), + ).toBeDefined() + expect( + providerClient.getQueryCache().find({ queryKey: consumer.queryKey }), + ).toBeUndefined() + expect( + explicitClient + .getMutationCache() + .findAll({ mutationKey: consumer.mutationKey }).length, + ).toBeGreaterThan(0) + expect( + providerClient + .getMutationCache() + .findAll({ mutationKey: consumer.mutationKey }).length, + ).toBe(0) + + consumer.query.destroy() + consumer.mutation.destroy() + consumer.isFetching.destroy() + consumer.isMutating.destroy() + consumer.mutationStatuses.destroy() + provider.remove() + explicitCountersClient = undefined + await Promise.resolve() + }) + + it('tracks fetch/mutate counters and mutation state', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + + const query = createQueryController( + host, + { + queryKey: ['counter-test'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 40)) + return 'done' + }, + }, + client, + ) + + const mutation = createMutationController( + host, + { + mutationFn: async (value: number) => { + await new Promise((resolve) => setTimeout(resolve, 40)) + return value + 10 + }, + }, + client, + ) + + const isFetching = useIsFetching(host, {}, client) + const isMutating = useIsMutating(host, {}, client) + const mutationStatuses = useMutationState( + host, + { + select: (item) => item.state.status, + }, + client, + ) + + host.connect() + host.update() + + await waitFor(() => isFetching() === 1) + await waitFor(() => query().isSuccess) + await waitFor(() => isFetching() === 0) + + mutation.mutate(1) + await waitFor(() => isMutating() === 1) + await waitFor(() => isMutating() === 0) + await waitFor(() => mutationStatuses().includes('success')) + }) + + it('S1: useIsFetching tracks filters and filter reactivity', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let resolveA: (() => void) | undefined + let resolveB: (() => void) | undefined + let activeFilter: { queryKey?: readonly string[] } = { + queryKey: ['fetch-a'], + } + + createQueryController( + host, + { + queryKey: ['fetch-a'], + queryFn: () => + new Promise((resolve) => { + resolveA = () => resolve('a') + }), + }, + client, + ) + + createQueryController( + host, + { + queryKey: ['fetch-b'], + queryFn: () => + new Promise((resolve) => { + resolveB = () => resolve('b') + }), + }, + client, + ) + + const isFetchingAll = useIsFetching(host, {}, client) + const isFetchingFiltered = useIsFetching(host, () => activeFilter, client) + + host.connect() + host.update() + + await waitFor(() => isFetchingAll() === 2) + await waitFor(() => isFetchingFiltered() === 1) + + activeFilter = { queryKey: ['fetch-b'] } + host.update() + await waitFor(() => isFetchingFiltered() === 1) + + resolveA?.() + await waitFor(() => isFetchingAll() === 1) + await waitFor(() => isFetchingFiltered() === 1) + + resolveB?.() + await waitFor(() => isFetchingAll() === 0) + await waitFor(() => isFetchingFiltered() === 0) + }) + + it('S2: useIsMutating tracks mutation filters and reactivity', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + let resolveA: (() => void) | undefined + let resolveB: (() => void) | undefined + let activeFilter: { mutationKey?: readonly string[] } = { + mutationKey: ['mut-a'], + } + + const mutationA = createMutationController( + host, + { + mutationKey: ['mut-a'], + mutationFn: () => + new Promise((resolve) => { + resolveA = () => resolve(1) + }), + }, + client, + ) + + const mutationB = createMutationController( + host, + { + mutationKey: ['mut-b'], + mutationFn: () => + new Promise((resolve) => { + resolveB = () => resolve(2) + }), + }, + client, + ) + + const isMutatingAll = useIsMutating(host, {}, client) + const isMutatingFiltered = useIsMutating(host, () => activeFilter, client) + + host.connect() + host.update() + + mutationA.mutate() + mutationB.mutate() + + await waitFor(() => isMutatingAll() === 2) + await waitFor(() => isMutatingFiltered() === 1) + + activeFilter = { mutationKey: ['mut-b'] } + host.update() + await waitFor(() => isMutatingFiltered() === 1) + + resolveA?.() + await waitFor(() => isMutatingAll() === 1) + await waitFor(() => isMutatingFiltered() === 1) + + resolveB?.() + await waitFor(() => isMutatingAll() === 0) + await waitFor(() => isMutatingFiltered() === 0) + }) + + it('S3: useMutationState selects and filters by mutation key/status', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + let activeFilter: { mutationKey?: readonly string[] } = { + mutationKey: ['state-a'], + } + + const mutationA = createMutationController( + host, + { + mutationKey: ['state-a'], + mutationFn: async () => 'ok', + }, + client, + ) + + const mutationB = createMutationController( + host, + { + mutationKey: ['state-b'], + mutationFn: async () => { + throw new Error('state-b-failure') + }, + }, + client, + ) + + const mutationStatuses = useMutationState( + host, + { + filters: () => activeFilter, + select: (item) => item.state.status, + }, + client, + ) + + host.connect() + host.update() + + await expect(mutationA.mutateAsync(undefined)).resolves.toBe('ok') + await expect(mutationB.mutateAsync(undefined)).rejects.toThrow( + 'state-b-failure', + ) + + await waitFor( + () => + mutationStatuses().length === 1 && mutationStatuses()[0] === 'success', + ) + + activeFilter = { mutationKey: ['state-b'] } + host.update() + + await waitFor( + () => + mutationStatuses().length === 1 && mutationStatuses()[0] === 'error', + ) + }) + + it('S4: useMutationState refreshes when the select closure changes on host update', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + let label = 'before' + + const mutation = createMutationController( + host, + { + mutationKey: ['state-select-reactivity'], + mutationFn: async () => 'ok', + }, + client, + ) + + const mutationLabels = useMutationState( + host, + { + filters: { + mutationKey: ['state-select-reactivity'], + }, + select: () => label, + }, + client, + ) + + host.connect() + host.update() + + await expect(mutation.mutateAsync(undefined)).resolves.toBe('ok') + await waitFor( + () => mutationLabels().length === 1 && mutationLabels()[0] === 'before', + ) + + label = 'after' + host.update() + + await waitFor( + () => mutationLabels().length === 1 && mutationLabels()[0] === 'after', + ) + + mutation.destroy() + mutationLabels.destroy() + }) + + it('LC-COUNTERS-03: read-only helpers fail after handshake and recover under a provider', async () => { + const consumer = document.createElement( + contextCountersTagName, + ) as ContextCountersHostElement + + expect(consumer.query().status).toBe('pending') + expect(consumer.isFetching()).toBe(0) + expect(consumer.isMutating()).toBe(0) + expect(consumer.mutationStatuses()).toEqual([]) + + document.body.append(consumer) + await waitForMissingQueryClient(() => consumer.query()) + + expect(() => consumer.isFetching()).toThrow(/No QueryClient available/) + expect(() => consumer.isMutating()).toThrow(/No QueryClient available/) + expect(() => consumer.mutationStatuses()).toThrow( + /No QueryClient available/, + ) + + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }) + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor(() => consumer.isFetching() === 1) + consumer.resolvePendingQuery() + await waitFor(() => consumer.query().isSuccess) + await waitFor(() => consumer.isFetching() === 0) + + consumer.query.destroy() + consumer.mutation.destroy() + consumer.isFetching.destroy() + consumer.isMutating.destroy() + consumer.mutationStatuses.destroy() + provider.remove() + await Promise.resolve() + }) + + it('ALREADYCONN-COUNTERS-01: read-only helpers on already-connected host with explicit client do not throw', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + mutations: { + retry: false, + }, + }, + }) + + const producerHost = new TestControllerHost() + let resolveQuery: (() => void) | undefined + let resolveMutation: (() => void) | undefined + + createQueryController( + producerHost, + { + queryKey: ['already-connected-counters-query'], + queryFn: () => + new Promise((resolve) => { + resolveQuery = () => resolve('query-ok') + }), + retry: false, + }, + client, + ) + + const producerMutation = createMutationController( + producerHost, + { + mutationKey: ['already-connected-counters-mutation'], + mutationFn: () => + new Promise((resolve) => { + resolveMutation = () => resolve('mutation-ok') + }), + }, + client, + ) + + producerHost.connect() + producerHost.update() + await waitFor(() => client.isFetching() === 1) + + producerMutation.mutate() + await waitFor(() => client.isMutating() === 1) + + class AlreadyConnectedHost implements ReactiveControllerHost { + private readonly controllers = new Set() + private isConnected = true + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + if (this.isConnected) { + controller.hostConnected?.() + } + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + const host = new AlreadyConnectedHost() + const isFetching = useIsFetching(host, {}, client) + const isMutating = useIsMutating(host, {}, client) + const mutationStatuses = useMutationState( + host, + { + filters: { mutationKey: ['already-connected-counters-mutation'] }, + select: (mutation) => mutation.state.status, + }, + client, + ) + + await Promise.resolve() + await Promise.resolve() + + await waitFor(() => isFetching() === 1) + await waitFor(() => isMutating() === 1) + await waitFor(() => mutationStatuses().includes('pending')) + + resolveQuery?.() + resolveMutation?.() + + await waitFor(() => isFetching() === 0) + await waitFor(() => isMutating() === 0) + await waitFor(() => mutationStatuses().includes('success')) + + isFetching.destroy() + isMutating.destroy() + mutationStatuses.destroy() + producerMutation.destroy() + }) +}) diff --git a/packages/lit-query/src/tests/infinite-and-options.test.ts b/packages/lit-query/src/tests/infinite-and-options.test.ts new file mode 100644 index 00000000000..cd3418f69fd --- /dev/null +++ b/packages/lit-query/src/tests/infinite-and-options.test.ts @@ -0,0 +1,423 @@ +import { describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { createInfiniteQueryController } from '../createInfiniteQueryController.js' +import { createMutationController } from '../createMutationController.js' +import { createQueryController } from '../createQueryController.js' +import { infiniteQueryOptions } from '../infiniteQueryOptions.js' +import { mutationOptions } from '../mutationOptions.js' +import { queryOptions } from '../queryOptions.js' +import { + TestControllerHost, + TestElementHost, + waitFor, + waitForMissingQueryClient, +} from './testHost.js' + +const providerTagName = 'test-query-client-provider-infinite' +if (!customElements.get(providerTagName)) { + customElements.define(providerTagName, QueryClientProvider) +} + +let explicitInfiniteClient: QueryClient | undefined + +class ContextInfiniteHostElement extends TestElementHost { + readonly queryKey = ['context-infinite'] as const + + readonly infinite = createInfiniteQueryController( + this, + { + queryKey: this.queryKey, + initialPageParam: 0, + queryFn: async ({ pageParam }) => Number(pageParam), + getNextPageParam: (lastPage) => (lastPage < 1 ? lastPage + 1 : undefined), + getPreviousPageParam: (firstPage) => + firstPage > -1 ? firstPage - 1 : undefined, + retry: false, + }, + explicitInfiniteClient, + ) +} + +const contextInfiniteTagName = 'test-context-infinite-host' +if (!customElements.get(contextInfiniteTagName)) { + customElements.define(contextInfiniteTagName, ContextInfiniteHostElement) +} + +describe('createInfiniteQueryController', () => { + it('LC-INF-01: first provider connection resolves from the pre-connect placeholder state', async () => { + const consumer = document.createElement( + contextInfiniteTagName, + ) as ContextInfiniteHostElement + + expect(consumer.infinite().status).toBe('pending') + await expect(consumer.infinite.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + await expect(consumer.infinite.fetchNextPage()).rejects.toThrow( + /No QueryClient available/, + ) + + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor(() => consumer.infinite().isSuccess) + expect(consumer.infinite().data?.pages).toEqual([0]) + + consumer.infinite.destroy() + provider.remove() + await Promise.resolve() + }) + + it('LC-INF-02: explicit client takes precedence over provider context', async () => { + const explicitClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const providerClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + explicitInfiniteClient = explicitClient + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = providerClient + + const consumer = document.createElement( + contextInfiniteTagName, + ) as ContextInfiniteHostElement + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor(() => consumer.infinite().isSuccess) + expect(consumer.infinite().data?.pages).toEqual([0]) + expect( + explicitClient.getQueryCache().find({ queryKey: consumer.queryKey }), + ).toBeDefined() + expect( + providerClient.getQueryCache().find({ queryKey: consumer.queryKey }), + ).toBeUndefined() + + consumer.infinite.destroy() + provider.remove() + explicitInfiniteClient = undefined + await Promise.resolve() + }) + + it('M14: supports initial page, fetchNextPage, and fetchPreviousPage', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + const infinite = createInfiniteQueryController( + host, + { + queryKey: ['m14', 'infinite'], + initialPageParam: 0, + queryFn: async ({ pageParam }) => Number(pageParam), + getNextPageParam: (lastPage) => + lastPage < 1 ? lastPage + 1 : undefined, + getPreviousPageParam: (firstPage) => + firstPage > -1 ? firstPage - 1 : undefined, + }, + client, + ) + + host.connect() + host.update() + + await waitFor(() => infinite().isSuccess) + expect(infinite().data?.pages).toEqual([0]) + + await infinite.fetchNextPage() + await waitFor(() => (infinite().data?.pages.length ?? 0) === 2) + expect(infinite().data?.pages).toEqual([0, 1]) + + await infinite.fetchPreviousPage() + await waitFor(() => (infinite().data?.pages.length ?? 0) === 3) + expect(infinite().data?.pages).toEqual([-1, 0, 1]) + }) + + it('INFEDGE-01: next-page failure preserves prior pages consistently', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + const infinite = createInfiniteQueryController( + host, + { + queryKey: ['infedge-01'], + initialPageParam: 0, + queryFn: async ({ pageParam }) => { + const page = Number(pageParam) + if (page === 1) { + throw new Error('next-page-failed') + } + return page + }, + getNextPageParam: (lastPage) => + lastPage < 1 ? lastPage + 1 : undefined, + }, + client, + ) + + host.connect() + host.update() + + await waitFor(() => infinite().isSuccess) + expect(infinite().data?.pages).toEqual([0]) + + const nextPageResult = await infinite.fetchNextPage() + expect(nextPageResult.isFetchNextPageError).toBe(true) + expect(nextPageResult.error).toBeInstanceOf(Error) + await waitFor(() => infinite().isFetchNextPageError) + expect(infinite().data?.pages).toEqual([0]) + }) + + it('LC-INF-03: missing provider fails deterministically and imperative methods align', async () => { + const consumer = document.createElement( + contextInfiniteTagName, + ) as ContextInfiniteHostElement + const placeholderResult = consumer.infinite() + + expect(placeholderResult.status).toBe('pending') + + document.body.append(consumer) + + expect(() => consumer.infinite()).not.toThrow() + await waitForMissingQueryClient(() => consumer.infinite()) + + await expect(consumer.infinite.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + await expect(consumer.infinite.fetchNextPage()).rejects.toThrow( + /No QueryClient available/, + ) + await expect(placeholderResult.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + await expect(placeholderResult.fetchNextPage()).rejects.toThrow( + /No QueryClient available/, + ) + await expect(placeholderResult.fetchPreviousPage()).rejects.toThrow( + /No QueryClient available/, + ) + + consumer.infinite.destroy() + consumer.remove() + await Promise.resolve() + }) + + it('ALREADYCONN-INF-01: infinite query controller on already-connected host with explicit client does not throw', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + client.setQueryData(['already-connected-infinite'], { + pages: [0], + pageParams: [0], + }) + + class AlreadyConnectedHost implements ReactiveControllerHost { + private readonly controllers = new Set() + private isConnected = true + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + if (this.isConnected) { + controller.hostConnected?.() + } + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + const host = new AlreadyConnectedHost() + const infinite = createInfiniteQueryController( + host, + { + queryKey: ['already-connected-infinite'], + initialPageParam: 0, + queryFn: async ({ pageParam }) => Number(pageParam), + getNextPageParam: (lastPage) => + lastPage < 1 ? lastPage + 1 : undefined, + staleTime: 30_000, + }, + client, + ) + + await Promise.resolve() + await Promise.resolve() + + expect(infinite().isSuccess).toBe(true) + expect(infinite().data?.pages).toEqual([0]) + + infinite.destroy() + }) + + it('LC-INF-04: explicit-client infinite accessors defer until host fields are initialized', () => { + const client = new QueryClient() + + class DeferredExplicitInfiniteHost implements ReactiveControllerHost { + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + readonly infinite = createInfiniteQueryController( + this, + () => ({ + queryKey: ['deferred-explicit-infinite', this.id] as const, + initialPageParam: 0, + queryFn: async ({ pageParam }) => Number(pageParam), + getNextPageParam: (lastPage) => + lastPage < 1 ? lastPage + 1 : undefined, + retry: false, + }), + client, + ) + + readonly firstRead = this.infinite() + readonly id = 'alpha' + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + expect(() => new DeferredExplicitInfiniteHost()).not.toThrow() + + const host = new DeferredExplicitInfiniteHost() + expect(host.infinite().status).toBe('pending') + + host.infinite.destroy() + }) +}) + +describe('options helpers integration', () => { + it('OPT-01: queryOptions integrates with createQueryController', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const host = new TestControllerHost() + + const query = createQueryController( + host, + queryOptions({ + queryKey: ['opt-01', 'query'] as const, + queryFn: async () => 'query-ok', + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + expect(query().data).toBe('query-ok') + }) + + it('OPT-01: mutationOptions integrates with createMutationController', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + + const mutation = createMutationController( + host, + mutationOptions({ + mutationFn: async (value: number) => value + 10, + }), + client, + ) + + host.connect() + host.update() + await expect(mutation.mutateAsync(5)).resolves.toBe(15) + expect(mutation().isSuccess).toBe(true) + }) + + it('OPT-01: infiniteQueryOptions integrates with createInfiniteQueryController', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const host = new TestControllerHost() + + const infinite = createInfiniteQueryController( + host, + infiniteQueryOptions({ + queryKey: ['opt-01', 'infinite'], + initialPageParam: 0, + queryFn: async ({ pageParam }) => Number(pageParam), + getNextPageParam: (lastPage) => + lastPage < 1 ? lastPage + 1 : undefined, + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => infinite().isSuccess) + expect(infinite().data?.pages).toEqual([0]) + }) +}) diff --git a/packages/lit-query/src/tests/mutation-controller.test.ts b/packages/lit-query/src/tests/mutation-controller.test.ts new file mode 100644 index 00000000000..6353cc5eed5 --- /dev/null +++ b/packages/lit-query/src/tests/mutation-controller.test.ts @@ -0,0 +1,472 @@ +import { describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { createMutationController } from '../createMutationController.js' +import { + TestControllerHost, + TestElementHost, + waitFor, + waitForMissingQueryClient, +} from './testHost.js' + +const providerTagName = 'test-query-client-provider-mutation' +if (!customElements.get(providerTagName)) { + customElements.define(providerTagName, QueryClientProvider) +} + +let explicitMutationClient: QueryClient | undefined + +class ContextMutationHostElement extends TestElementHost { + readonly mutationKey = ['context-mutation'] as const + + readonly mutation = createMutationController( + this, + { + mutationKey: this.mutationKey, + mutationFn: async (value: number) => value + 1, + }, + explicitMutationClient, + ) +} + +const contextMutationTagName = 'test-context-mutation-host' +if (!customElements.get(contextMutationTagName)) { + customElements.define(contextMutationTagName, ContextMutationHostElement) +} + +describe('createMutationController', () => { + it('LC-MUT-01: first provider connection resolves from the pre-connect placeholder state', async () => { + const consumer = document.createElement( + contextMutationTagName, + ) as ContextMutationHostElement + + expect(consumer.mutation().isIdle).toBe(true) + expect(() => consumer.mutation.mutate(1)).toThrowError( + /No QueryClient available/, + ) + await expect(consumer.mutation.mutateAsync(1)).rejects.toThrow( + /No QueryClient available/, + ) + + const client = new QueryClient() + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await Promise.resolve() + + await expect(consumer.mutation.mutateAsync(1)).resolves.toBe(2) + expect(consumer.mutation().isSuccess).toBe(true) + + consumer.mutation.destroy() + provider.remove() + await Promise.resolve() + }) + + it('LC-MUT-02: explicit client takes precedence over provider context', async () => { + const explicitClient = new QueryClient() + const providerClient = new QueryClient() + explicitMutationClient = explicitClient + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = providerClient + + const consumer = document.createElement( + contextMutationTagName, + ) as ContextMutationHostElement + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await Promise.resolve() + + await expect(consumer.mutation.mutateAsync(2)).resolves.toBe(3) + + expect( + explicitClient + .getMutationCache() + .findAll({ mutationKey: consumer.mutationKey }).length, + ).toBeGreaterThan(0) + expect( + providerClient + .getMutationCache() + .findAll({ mutationKey: consumer.mutationKey }).length, + ).toBe(0) + + consumer.mutation.destroy() + provider.remove() + explicitMutationClient = undefined + await Promise.resolve() + }) + + it('supports mutate and mutateAsync paths', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + + const mutation = createMutationController( + host, + { + mutationFn: async (value: number) => value + 1, + }, + client, + ) + + host.connect() + host.update() + + const result = await mutation.mutateAsync(1) + expect(result).toBe(2) + expect(mutation().isSuccess).toBe(true) + expect(mutation().data).toBe(2) + + mutation.mutate(2) + await waitFor(() => mutation().data === 3) + expect(mutation().isSuccess).toBe(true) + }) + + it('M9: mutation state transitions cover idle/pending/success/error', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + + const mutation = createMutationController( + host, + { + mutationFn: async (value: number) => { + await new Promise((resolve) => setTimeout(resolve, 10)) + if (value < 0) { + throw new Error('negative-not-allowed') + } + return value + 1 + }, + }, + client, + ) + + host.connect() + host.update() + + expect(mutation().isIdle).toBe(true) + + const successPromise = mutation.mutateAsync(10) + await waitFor(() => mutation().isPending) + await expect(successPromise).resolves.toBe(11) + await waitFor(() => mutation().isSuccess) + expect(mutation().data).toBe(11) + + const errorPromise = mutation.mutateAsync(-1) + await waitFor(() => mutation().isPending) + await expect(errorPromise).rejects.toThrow('negative-not-allowed') + await waitFor(() => mutation().isError) + expect(mutation().error).toBeInstanceOf(Error) + }) + + it('M10: reset clears mutation state back to idle baseline', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + + const mutation = createMutationController( + host, + { + mutationFn: async () => { + throw new Error('reset-target') + }, + }, + client, + ) + + host.connect() + host.update() + + await expect(mutation.mutateAsync(undefined)).rejects.toThrow( + 'reset-target', + ) + await waitFor(() => mutation().isError) + expect(mutation().error).toBeInstanceOf(Error) + + mutation.reset() + expect(mutation().isIdle).toBe(true) + expect(mutation().isPaused).toBe(false) + expect(mutation().isError).toBe(false) + expect(mutation().error).toBeNull() + expect(mutation().data).toBeUndefined() + }) + + it('M11: mutate is non-throwing while mutateAsync rejects on error', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + + const mutation = createMutationController( + host, + { + mutationFn: async (value: number) => { + if (value < 0) { + throw new Error('negative-not-allowed') + } + + return value + 1 + }, + }, + client, + ) + + host.connect() + host.update() + + expect(() => mutation.mutate(-1)).not.toThrow() + await waitFor(() => mutation().isError) + expect(mutation().error).toBeInstanceOf(Error) + + await expect(mutation.mutateAsync(-1)).rejects.toThrow( + 'negative-not-allowed', + ) + }) + + it('M12: mutation callback order and call counts are deterministic', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + const callbackEvents: string[] = [] + + const mutation = createMutationController( + host, + { + mutationFn: async (value: number) => { + await new Promise((resolve) => setTimeout(resolve, 5)) + if (value < 0) { + throw new Error('callback-order-failure') + } + return value + 1 + }, + onSuccess: (_data, value) => { + callbackEvents.push(`success:${value}`) + }, + onError: (_error, value) => { + callbackEvents.push(`error:${value}`) + }, + onSettled: (_data, _error, value) => { + callbackEvents.push(`settled:${value}`) + }, + }, + client, + ) + + host.connect() + host.update() + + await expect(mutation.mutateAsync(1)).resolves.toBe(2) + await expect(mutation.mutateAsync(-1)).rejects.toThrow( + 'callback-order-failure', + ) + + expect(callbackEvents).toEqual([ + 'success:1', + 'settled:1', + 'error:-1', + 'settled:-1', + ]) + }) + + it('AREACT-02: refreshed mutation callbacks use latest closures', async () => { + const client = new QueryClient() + const host = new TestControllerHost() + const callbackEvents: string[] = [] + let version = 'v1' + + const mutation = createMutationController( + host, + () => ({ + mutationFn: async (value: number) => { + if (value < 0) { + throw new Error('freshness-failure') + } + return value + 1 + }, + onSuccess: () => { + callbackEvents.push(`success:${version}`) + }, + onError: () => { + callbackEvents.push(`error:${version}`) + }, + onSettled: () => { + callbackEvents.push(`settled:${version}`) + }, + }), + client, + ) + + host.connect() + host.update() + + await expect(mutation.mutateAsync(1)).resolves.toBe(2) + expect(callbackEvents.slice(0, 2)).toEqual(['success:v1', 'settled:v1']) + + version = 'v2' + host.update() + + await expect(mutation.mutateAsync(-1)).rejects.toThrow('freshness-failure') + expect(callbackEvents.slice(2)).toEqual(['error:v2', 'settled:v2']) + }) + + it('LC-MUT-03: missing provider becomes a deterministic missing-client state', async () => { + const consumer = document.createElement( + contextMutationTagName, + ) as ContextMutationHostElement + const placeholderResult = consumer.mutation() + + expect(placeholderResult.isIdle).toBe(true) + expect(placeholderResult.isPaused).toBe(false) + + document.body.append(consumer) + + expect(() => consumer.mutation()).not.toThrow() + await waitForMissingQueryClient(() => consumer.mutation()) + + expect(() => consumer.mutation.mutate(1)).toThrow( + /No QueryClient available/, + ) + await expect(consumer.mutation.mutateAsync(1)).rejects.toThrow( + /No QueryClient available/, + ) + await expect(placeholderResult.mutate(1)).rejects.toThrow( + /No QueryClient available/, + ) + + expect(() => consumer.mutation.reset()).not.toThrow() + expect(() => placeholderResult.reset()).not.toThrow() + + consumer.mutation.destroy() + consumer.remove() + await Promise.resolve() + }) + + it('LC-MUT-04: later valid provider adoption recovers without reconstruction', async () => { + const consumer = document.createElement( + contextMutationTagName, + ) as ContextMutationHostElement + + document.body.append(consumer) + await waitForMissingQueryClient(() => consumer.mutation()) + + const client = new QueryClient() + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await Promise.resolve() + + await expect(consumer.mutation.mutateAsync(1)).resolves.toBe(2) + expect(consumer.mutation().isSuccess).toBe(true) + + consumer.mutation.destroy() + provider.remove() + await Promise.resolve() + }) + + it('ALREADYCONN-MUT-01: mutation controller on already-connected host with explicit client does not throw', async () => { + // Regression test for SSR hydration scenario where controller is created + // during willUpdate on an already-connected host. + const client = new QueryClient() + + // Create a host that simulates Lit's behavior: addController calls + // hostConnected immediately if the host is already connected + class AlreadyConnectedHost { + private readonly controllers = new Set<{ + hostConnected?: () => void + }>() + private isConnected = true + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: { hostConnected?: () => void }): void { + this.controllers.add(controller) + if (this.isConnected) { + controller.hostConnected?.() + } + } + + removeController(controller: { hostConnected?: () => void }): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + const host = new AlreadyConnectedHost() + + // This should NOT throw even though hostConnected runs during construction + const mutation = createMutationController( + host, + { + mutationKey: ['already-connected-mutation-test'], + mutationFn: async (value: number) => value * 2, + }, + client, + ) + + // Wait for the deferred onConnected to complete + await Promise.resolve() + await Promise.resolve() + + // Mutation controller should work correctly + expect(mutation().isIdle).toBe(true) + await expect(mutation.mutateAsync(5)).resolves.toBe(10) + expect(mutation().isSuccess).toBe(true) + + mutation.destroy() + }) + + it('LC-MUT-05: explicit-client mutation accessors defer until host fields are initialized', () => { + const client = new QueryClient() + + class DeferredExplicitMutationHost implements ReactiveControllerHost { + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + readonly mutation = createMutationController( + this, + () => ({ + mutationKey: ['deferred-explicit-mutation', this.id] as const, + mutationFn: async (value: number) => value + this.offset, + }), + client, + ) + + readonly firstRead = this.mutation() + readonly id = 'alpha' + readonly offset = 1 + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + expect(() => new DeferredExplicitMutationHost()).not.toThrow() + + const host = new DeferredExplicitMutationHost() + expect(host.mutation().isIdle).toBe(true) + + host.mutation.destroy() + }) +}) diff --git a/packages/lit-query/src/tests/queries-controller.test.ts b/packages/lit-query/src/tests/queries-controller.test.ts new file mode 100644 index 00000000000..1fd9c536007 --- /dev/null +++ b/packages/lit-query/src/tests/queries-controller.test.ts @@ -0,0 +1,705 @@ +import { describe, expect, it } from 'vitest' +import { QueryClient } from '@tanstack/query-core' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { createQueriesController } from '../createQueriesController.js' +import { queryOptions } from '../queryOptions.js' +import { + TestControllerHost, + TestElementHost, + waitFor, + waitForMissingQueryClient, +} from './testHost.js' + +const providerTagName = 'test-query-client-provider-queries' +if (!customElements.get(providerTagName)) { + customElements.define(providerTagName, QueryClientProvider) +} + +let explicitQueriesClient: QueryClient | undefined + +class ContextQueriesHostElement extends TestElementHost { + readonly queryKeys = [ + ['context-queries', 'alpha'] as const, + ['context-queries', 'beta'] as const, + ] + + readonly queries = createQueriesController( + this, + { + queries: this.queryKeys.map((queryKey) => ({ + queryKey, + queryFn: async () => queryKey[1], + retry: false, + })), + combine: (results) => + results.map((result) => ({ + status: result.status, + data: result.data, + })), + }, + explicitQueriesClient, + ) +} + +const contextQueriesTagName = 'test-context-queries-host' +if (!customElements.get(contextQueriesTagName)) { + customElements.define(contextQueriesTagName, ContextQueriesHostElement) +} + +class RawContextQueriesHostElement extends TestElementHost { + readonly queryKeys = [ + ['raw-context-queries', 'alpha'] as const, + ['raw-context-queries', 'beta'] as const, + ] + + readonly queries = createQueriesController(this, { + queries: this.queryKeys.map((queryKey) => ({ + queryKey, + queryFn: async () => queryKey[1], + retry: false, + })), + }) +} + +const rawContextQueriesTagName = 'test-raw-context-queries-host' +if (!customElements.get(rawContextQueriesTagName)) { + customElements.define(rawContextQueriesTagName, RawContextQueriesHostElement) +} + +class DeferredFieldsQueriesHost implements ReactiveControllerHost { + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + readonly queries = createQueriesController(this, () => ({ + queries: this.ids.map((id) => ({ + queryKey: ['deferred-fields-queries', id] as const, + queryFn: async () => id, + retry: false, + })), + combine: (results) => results.map((result) => result.status), + })) + + readonly firstRead = this.queries() + readonly ids = ['alpha', 'beta'] as const + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } +} + +describe('createQueriesController', () => { + it('LC-QUERIES-01: first provider connection resolves from the pre-connect placeholder state', async () => { + const consumer = document.createElement( + contextQueriesTagName, + ) as ContextQueriesHostElement + + expect(consumer.queries()).toEqual([ + { status: 'pending', data: undefined }, + { status: 'pending', data: undefined }, + ]) + + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor( + () => + consumer.queries()[0]?.status === 'success' && + consumer.queries()[1]?.status === 'success', + ) + expect(consumer.queries().map((item) => item.data)).toEqual([ + 'alpha', + 'beta', + ]) + + consumer.queries.destroy() + provider.remove() + await Promise.resolve() + }) + + it('LC-QUERIES-02: explicit client takes precedence over provider context', async () => { + const explicitClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const providerClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + explicitQueriesClient = explicitClient + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = providerClient + + const consumer = document.createElement( + contextQueriesTagName, + ) as ContextQueriesHostElement + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor( + () => + consumer.queries()[0]?.status === 'success' && + consumer.queries()[1]?.status === 'success', + ) + + expect( + explicitClient.getQueryCache().find({ queryKey: consumer.queryKeys[0]! }), + ).toBeDefined() + expect( + providerClient.getQueryCache().find({ queryKey: consumer.queryKeys[0]! }), + ).toBeUndefined() + + consumer.queries.destroy() + provider.remove() + explicitQueriesClient = undefined + await Promise.resolve() + }) + + it('combines multiple query results', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + + const queries = createQueriesController( + host, + { + queries: [ + { + queryKey: ['q1'], + queryFn: async () => 'alpha', + }, + { + queryKey: ['q2'], + queryFn: async () => 'beta', + }, + ] as const, + combine: (results) => results.map((result) => result.data), + }, + client, + ) + + host.connect() + host.update() + + await waitFor(() => queries().every((value) => typeof value === 'string')) + expect(queries()).toEqual(['alpha', 'beta']) + }) + + it('M13: supports dynamic add/remove and keeps partial failure stability', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let includeThird = false + let includeFailing = true + + const queries = createQueriesController( + host, + () => ({ + queries: [ + { + queryKey: ['m13', 'alpha'] as const, + queryFn: async () => 'alpha', + }, + ...(includeFailing + ? [ + { + queryKey: ['m13', 'failing'] as const, + queryFn: async () => { + throw new Error('m13-fail') + }, + }, + ] + : []), + ...(includeThird + ? [ + { + queryKey: ['m13', 'gamma'] as const, + queryFn: async () => 'gamma', + }, + ] + : []), + ] as const, + combine: (results) => + results.map((result) => ({ + status: result.status, + data: result.data, + error: result.error instanceof Error ? result.error.message : null, + })), + }), + client, + ) + + host.connect() + host.update() + + await waitFor(() => queries().length === 2) + await waitFor( + () => + queries()[0]?.status === 'success' && queries()[1]?.status === 'error', + ) + expect(queries()[0]).toMatchObject({ status: 'success', data: 'alpha' }) + expect(queries()[1]).toMatchObject({ status: 'error', error: 'm13-fail' }) + + includeThird = true + host.update() + await waitFor(() => queries().length === 3) + await waitFor( + () => + queries()[0]?.status === 'success' && + queries()[1]?.status === 'error' && + queries()[2]?.status === 'success', + ) + expect(queries()[2]).toMatchObject({ status: 'success', data: 'gamma' }) + + includeFailing = false + host.update() + await waitFor(() => queries().length === 2) + await waitFor( + () => + queries()[0]?.status === 'success' && + queries()[1]?.status === 'success', + ) + expect(queries().map((item) => item.data)).toEqual(['alpha', 'gamma']) + }) + + it('CQS-ADV-01: reordering queries preserves documented result order mapping', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let order: Array<'first' | 'second'> = ['first', 'second'] + + const queries = createQueriesController( + host, + () => ({ + queries: order.map((id) => ({ + queryKey: ['cqs-adv-01', id] as const, + queryFn: async () => id, + })), + combine: (results) => results.map((result) => result.data), + }), + client, + ) + + host.connect() + host.update() + + await waitFor(() => queries().every((value) => typeof value === 'string')) + expect(queries()).toEqual(['first', 'second']) + + order = ['second', 'first'] + host.update() + await waitFor(() => queries()[0] === 'second' && queries()[1] === 'first') + expect(queries()).toEqual(['second', 'first']) + }) + + it('CQS-ADV-02: duplicate query keys return stable per-index results by contract', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let callCount = 0 + + const queries = createQueriesController( + host, + { + queries: [ + { + queryKey: ['dup-key'] as const, + queryFn: async () => { + callCount += 1 + return 'shared-value' + }, + }, + { + queryKey: ['dup-key'] as const, + queryFn: async () => { + callCount += 1 + return 'shared-value' + }, + }, + ] as const, + combine: (results) => + results.map((result) => ({ + status: result.status, + data: result.data, + })), + }, + client, + ) + + host.connect() + host.update() + + await waitFor( + () => + queries().length === 2 && + queries()[0]?.status === 'success' && + queries()[1]?.status === 'success', + ) + expect(queries()[0]?.data).toBe('shared-value') + expect(queries()[1]?.data).toBe('shared-value') + expect(callCount).toBeGreaterThan(0) + }) + + it('LC-QUERIES-03: missing provider fails after handshake and later provider adoption recovers', async () => { + const consumer = document.createElement( + contextQueriesTagName, + ) as ContextQueriesHostElement + + expect(consumer.queries()).toEqual([ + { status: 'pending', data: undefined }, + { status: 'pending', data: undefined }, + ]) + + document.body.append(consumer) + + expect(() => consumer.queries()).not.toThrow() + await waitForMissingQueryClient(() => consumer.queries()) + + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor( + () => + consumer.queries()[0]?.status === 'success' && + consumer.queries()[1]?.status === 'success', + ) + expect(consumer.queries().map((item) => item.data)).toEqual([ + 'alpha', + 'beta', + ]) + + consumer.queries.destroy() + provider.remove() + await Promise.resolve() + }) + + it('LC-QUERIES-04: raw query results reject placeholder refetch after missing-client handshake', async () => { + const consumer = document.createElement( + rawContextQueriesTagName, + ) as RawContextQueriesHostElement + + const firstQuery = consumer.queries()[0] + expect(firstQuery?.status).toBe('pending') + + document.body.append(consumer) + + await waitForMissingQueryClient(() => consumer.queries()) + await expect(firstQuery?.refetch()).rejects.toThrow( + 'No QueryClient available. Pass one explicitly or render within QueryClientProvider.', + ) + + consumer.queries.destroy() + consumer.remove() + await Promise.resolve() + }) + + it('LC-QUERIES-05: constructor defers placeholder accessors until host fields are initialized', () => { + expect(() => new DeferredFieldsQueriesHost()).not.toThrow() + + const host = new DeferredFieldsQueriesHost() + expect(host.queries()).toEqual(['pending', 'pending']) + }) + + it('LC-QUERIES-06: placeholder combine materializes defined initialData before a client is available', () => { + const host = new TestControllerHost() + const queries = createQueriesController(host, { + queries: [ + queryOptions({ + queryKey: ['placeholder-initial-data'] as const, + queryFn: async () => ({ id: 4, name: 'Marie' }), + initialData: { id: 0, name: 'Seed' }, + }), + ] as const, + combine: (result) => result[0].data.name, + }) + + expect(queries()).toBe('Seed') + + queries.destroy() + }) + + it('LC-QUERIES-07: explicit-client constructor defers dynamic accessors until host fields are initialized', () => { + const client = new QueryClient() + + class DeferredExplicitQueriesHost implements ReactiveControllerHost { + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + readonly queries = createQueriesController( + this, + () => ({ + queries: this.ids.map((id) => ({ + queryKey: ['deferred-explicit-queries', id] as const, + queryFn: async () => id, + retry: false, + })), + combine: (results) => results.map((result) => result.status), + }), + client, + ) + + readonly firstRead = this.queries() + readonly ids = ['alpha', 'beta'] as const + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + expect(() => new DeferredExplicitQueriesHost()).not.toThrow() + + const host = new DeferredExplicitQueriesHost() + expect(host.queries()).toEqual(['pending', 'pending']) + + host.queries.destroy() + }) + + it('LC-QUERIES-08: explicit-client constructor defers static combine callbacks until host fields are initialized', () => { + const client = new QueryClient() + + class DeferredExplicitCombineQueriesHost implements ReactiveControllerHost { + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + readonly queries = createQueriesController( + this, + { + queries: [ + { + queryKey: ['deferred-explicit-combine-queries', 'alpha'] as const, + queryFn: async () => 'alpha', + retry: false, + }, + ] as const, + combine: (results) => + this.ids.map((id, index) => `${id}:${results[index]?.status}`), + }, + client, + ) + + readonly firstRead = this.queries() + readonly ids = ['alpha'] as const + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + expect(() => new DeferredExplicitCombineQueriesHost()).not.toThrow() + + const host = new DeferredExplicitCombineQueriesHost() + expect(host.queries()).toEqual(['alpha:pending']) + + host.queries.destroy() + }) + + it('LC-QUERIES-09: explicit-client constructor re-surfaces permanent static combine errors after initialization', async () => { + const client = new QueryClient() + + class InvalidExplicitCombineQueriesHost implements ReactiveControllerHost { + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + readonly queries = createQueriesController( + this, + { + queries: [ + { + queryKey: ['invalid-explicit-combine-queries', 'alpha'] as const, + queryFn: async () => 'alpha', + retry: false, + }, + ] as const, + combine: () => { + throw new Error('invalid combine') + }, + }, + client, + ) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + expect(() => new InvalidExplicitCombineQueriesHost()).not.toThrow() + + const host = new InvalidExplicitCombineQueriesHost() + await Promise.resolve() + + expect(() => host.queries()).toThrow('invalid combine') + }) + + it('ALREADYCONN-QUERIES-01: queries controller on already-connected host with explicit client does not throw', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + client.setQueryData(['already-connected-queries', 'alpha'], 'alpha') + client.setQueryData(['already-connected-queries', 'beta'], 'beta') + + class AlreadyConnectedHost implements ReactiveControllerHost { + private readonly controllers = new Set() + private isConnected = true + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + if (this.isConnected) { + controller.hostConnected?.() + } + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + const host = new AlreadyConnectedHost() + const queries = createQueriesController( + host, + { + queries: [ + { + queryKey: ['already-connected-queries', 'alpha'] as const, + queryFn: async () => 'fetched-alpha', + staleTime: 30_000, + }, + { + queryKey: ['already-connected-queries', 'beta'] as const, + queryFn: async () => 'fetched-beta', + staleTime: 30_000, + }, + ] as const, + combine: (results) => + results.map((result) => ({ + status: result.status, + data: result.data, + })), + }, + client, + ) + + await Promise.resolve() + await Promise.resolve() + + expect(queries()).toEqual([ + { status: 'success', data: 'alpha' }, + { status: 'success', data: 'beta' }, + ]) + + queries.destroy() + }) +}) diff --git a/packages/lit-query/src/tests/query-controller.test.ts b/packages/lit-query/src/tests/query-controller.test.ts new file mode 100644 index 00000000000..bc618ff830e --- /dev/null +++ b/packages/lit-query/src/tests/query-controller.test.ts @@ -0,0 +1,1326 @@ +import { describe, expect, it } from 'vitest' +import { keepPreviousData, QueryClient } from '@tanstack/query-core' +import type { ReactiveController, ReactiveControllerHost } from 'lit' +import { QueryClientProvider } from '../QueryClientProvider.js' +import { createQueryController } from '../createQueryController.js' +import { + TestControllerHost, + waitFor, + waitForMissingQueryClient, +} from './testHost.js' + +const providerTagName = 'test-query-client-provider-query' +if (!customElements.get(providerTagName)) { + customElements.define(providerTagName, QueryClientProvider) +} + +class QueryConsumerHostElement + extends HTMLElement + implements ReactiveControllerHost +{ + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + readonly queryKey = ['query-controller', 'provider-switch'] as const + queryCalls = 0 + + readonly query = createQueryController(this, () => ({ + queryKey: this.queryKey, + queryFn: async () => { + this.queryCalls += 1 + return `value-${this.queryCalls}` + }, + retry: false, + })) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + + connectedCallback(): void { + for (const controller of this.controllers) { + controller.hostConnected?.() + } + } + + disconnectedCallback(): void { + for (const controller of this.controllers) { + controller.hostDisconnected?.() + } + } +} + +const consumerTagName = 'test-query-consumer-host' +if (!customElements.get(consumerTagName)) { + customElements.define(consumerTagName, QueryConsumerHostElement) +} + +describe('createQueryController', () => { + it('M1: does not request update after destroy when microtask flushes', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + const query = createQueryController( + host, + { + queryKey: ['query-controller', 'm1'], + queryFn: async () => 'done', + }, + client, + ) + + host.connect() + query.destroy() + + await Promise.resolve() + await Promise.resolve() + + expect(host.updatesRequested).toBe(0) + }) + + it('M2: returns observer count to baseline after 100 lifecycle cycles', async () => { + const queryKey = ['query-controller', 'm2'] as const + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + for (let cycle = 0; cycle < 100; cycle += 1) { + const host = new TestControllerHost() + const query = createQueryController( + host, + { + queryKey, + queryFn: async () => cycle, + }, + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + + const cacheQuery = client.getQueryCache().find({ queryKey }) + expect(cacheQuery?.getObserversCount()).toBe(1) + + host.disconnect() + query.destroy() + expect(cacheQuery?.getObserversCount() ?? 0).toBe(0) + } + }) + + it('fetches and updates query state', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let callCount = 0 + + const query = createQueryController( + host, + { + queryKey: ['user', 'fetches-and-updates'], + queryFn: async () => { + callCount += 1 + return { id: 1, name: 'Ada' } + }, + }, + client, + ) + + host.connect() + host.update() + + await waitFor(() => query().isSuccess) + + expect(query().data).toEqual({ id: 1, name: 'Ada' }) + expect(callCount).toBe(1) + expect(host.updatesRequested).toBeGreaterThan(0) + }) + + it('M4: transitions from pending to success with expected contract', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + const query = createQueryController( + host, + { + queryKey: ['query-controller', 'm4'], + queryFn: async () => { + await new Promise((resolve) => setTimeout(resolve, 10)) + return 'ok' + }, + }, + client, + ) + + expect(query().status).toBe('pending') + expect(query().isSuccess).toBe(false) + + host.connect() + host.update() + + await waitFor(() => query().isSuccess) + expect(query().status).toBe('success') + expect(query().data).toBe('ok') + }) + + it('M6: does not fetch when enabled=false and fetches after enabling', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let callCount = 0 + let enabled = false + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', 'm6'], + enabled, + queryFn: async () => { + callCount += 1 + return 'enabled-result' + }, + }), + client, + ) + + host.connect() + host.update() + + await new Promise((resolve) => setTimeout(resolve, 25)) + expect(callCount).toBe(0) + expect(query().isSuccess).toBe(false) + + enabled = true + host.update() + await waitFor(() => query().isSuccess) + + expect(callCount).toBe(1) + expect(query().data).toBe('enabled-result') + }) + + it('M7: remount with gcTime=0 has no observer leak and refetches', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const queryKey = ['query-controller', 'm7'] as const + let callCount = 0 + + const firstHost = new TestControllerHost() + const firstQuery = createQueryController( + firstHost, + { + queryKey, + gcTime: 0, + queryFn: async () => { + callCount += 1 + return `value-${callCount}` + }, + }, + client, + ) + + firstHost.connect() + firstHost.update() + await waitFor(() => firstQuery().isSuccess) + + const firstCacheEntry = client.getQueryCache().find({ queryKey }) + expect(firstCacheEntry?.getObserversCount()).toBe(1) + expect(callCount).toBe(1) + + firstHost.disconnect() + firstQuery.destroy() + + await waitFor(() => { + // With gcTime:0, cache entry may be immediately removed after last observer unmounts. + const entry = client.getQueryCache().find({ queryKey }) + return !entry || entry.getObserversCount() === 0 + }) + + const secondHost = new TestControllerHost() + const secondQuery = createQueryController( + secondHost, + { + queryKey, + gcTime: 0, + queryFn: async () => { + callCount += 1 + return `value-${callCount}` + }, + }, + client, + ) + + secondHost.connect() + secondHost.update() + await waitFor(() => secondQuery().isSuccess) + await waitFor(() => secondQuery().data === 'value-2') + + const secondCacheEntry = client.getQueryCache().find({ queryKey }) + expect(secondCacheEntry?.getObserversCount()).toBe(1) + expect(callCount).toBe(2) + expect(secondQuery().data).toBe('value-2') + }) + + it('M8: applies latest accessor key/options on updates and refetch', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let keyId = 1 + const seenKeys: number[] = [] + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', keyId] as const, + queryFn: async ({ queryKey }) => { + const id = queryKey[1] as number + seenKeys.push(id) + return `user-${id}` + }, + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + expect(query().data).toBe('user-1') + + keyId = 2 + host.update() + await waitFor(() => query().isSuccess && query().data === 'user-2') + + await query.refetch() + expect(query().data).toBe('user-2') + expect(seenKeys.includes(1)).toBe(true) + expect(seenKeys.includes(2)).toBe(true) + }) + + it('QSEM-01: refetchOnMount follows stale-vs-fresh policy', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + let staleCalls = 0 + const staleKey = ['query-controller', 'qsem-01', 'stale'] as const + + const staleHostA = new TestControllerHost() + const staleQueryA = createQueryController( + staleHostA, + { + queryKey: staleKey, + staleTime: 0, + refetchOnMount: true, + queryFn: async () => { + staleCalls += 1 + return `stale-${staleCalls}` + }, + }, + client, + ) + + staleHostA.connect() + staleHostA.update() + await waitFor(() => staleQueryA().isSuccess) + expect(staleCalls).toBe(1) + staleHostA.disconnect() + staleQueryA.destroy() + + const staleHostB = new TestControllerHost() + const staleQueryB = createQueryController( + staleHostB, + { + queryKey: staleKey, + staleTime: 0, + refetchOnMount: true, + queryFn: async () => { + staleCalls += 1 + return `stale-${staleCalls}` + }, + }, + client, + ) + + staleHostB.connect() + staleHostB.update() + await waitFor(() => staleCalls >= 2) + expect(staleQueryB().isSuccess).toBe(true) + staleHostB.disconnect() + staleQueryB.destroy() + + let freshCalls = 0 + const freshKey = ['query-controller', 'qsem-01', 'fresh'] as const + + const freshHostA = new TestControllerHost() + const freshQueryA = createQueryController( + freshHostA, + { + queryKey: freshKey, + staleTime: Number.POSITIVE_INFINITY, + refetchOnMount: true, + queryFn: async () => { + freshCalls += 1 + return `fresh-${freshCalls}` + }, + }, + client, + ) + + freshHostA.connect() + freshHostA.update() + await waitFor(() => freshQueryA().isSuccess) + expect(freshCalls).toBe(1) + freshHostA.disconnect() + freshQueryA.destroy() + + const freshHostB = new TestControllerHost() + const freshQueryB = createQueryController( + freshHostB, + { + queryKey: freshKey, + staleTime: Number.POSITIVE_INFINITY, + refetchOnMount: true, + queryFn: async () => { + freshCalls += 1 + return `fresh-${freshCalls}` + }, + }, + client, + ) + + freshHostB.connect() + freshHostB.update() + await waitFor(() => freshQueryB().isSuccess) + await new Promise((resolve) => setTimeout(resolve, 25)) + expect(freshCalls).toBe(1) + expect(freshQueryB().data).toBe('fresh-1') + }) + + it('QSEM-02: select transforms data and select-throw surfaces as error', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let shouldThrow = false + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', 'qsem-02'], + queryFn: async () => ({ value: 2 }), + select: (payload: { value: number }) => { + if (shouldThrow) { + throw new Error('select-failed') + } + + return payload.value * 10 + }, + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + expect(query().data).toBe(20) + + shouldThrow = true + await query.refetch() + await waitFor(() => query().isError) + expect(query().error).toBeInstanceOf(Error) + expect((query().error as Error).message).toContain('select-failed') + }) + + it('S5: keepPreviousData preserves prior data during key transitions', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let keyId = 1 + let resolveSecond: ((value: string) => void) | undefined + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', 's5', keyId] as const, + queryFn: async ({ queryKey }) => { + const id = queryKey[2] as number + if (id === 1) { + return 'value-1' + } + + return new Promise((resolve) => { + resolveSecond = resolve + }) + }, + placeholderData: keepPreviousData, + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess && query().data === 'value-1') + + keyId = 2 + host.update() + + await waitFor(() => query().isFetching) + await waitFor(() => query().isPlaceholderData) + expect(query().data).toBe('value-1') + + resolveSecond?.('value-2') + await waitFor(() => query().isSuccess && query().data === 'value-2') + expect(query().isPlaceholderData).toBe(false) + }) + + it('QSEM-03: invalidation triggers refetch and updates result state', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + const queryKey = ['query-controller', 'qsem-03'] as const + let callCount = 0 + + const query = createQueryController( + host, + { + queryKey, + queryFn: async () => { + callCount += 1 + return `v${callCount}` + }, + }, + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + expect(query().data).toBe('v1') + expect(callCount).toBe(1) + + void client.invalidateQueries({ queryKey }) + await waitFor(() => callCount >= 2) + await waitFor(() => query().data === 'v2') + expect(query().isSuccess).toBe(true) + }) + + it('CANCEL-02: stale older response does not overwrite newer key result', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let keyId = 'old' + let resolveOld: ((value: string) => void) | undefined + let resolveNew: ((value: string) => void) | undefined + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', 'cancel-02', keyId] as const, + queryFn: async ({ queryKey }) => { + const id = queryKey[2] as string + return new Promise((resolve) => { + if (id === 'old') { + resolveOld = resolve + } else { + resolveNew = resolve + } + }) + }, + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => typeof resolveOld === 'function') + + keyId = 'new' + host.update() + await waitFor(() => typeof resolveNew === 'function') + + resolveNew?.('new-value') + await waitFor(() => query().data === 'new-value') + + resolveOld?.('old-value') + await new Promise((resolve) => setTimeout(resolve, 20)) + + expect(query().data).toBe('new-value') + expect(query().isSuccess).toBe(true) + }) + + it('CANCEL-01: queryFn receives AbortSignal and prior request is aborted on key switch', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let keyId: 'old' | 'new' = 'old' + let oldSignal: AbortSignal | undefined + let resolveOld: ((value: string) => void) | undefined + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', 'cancel-01', keyId] as const, + queryFn: async ({ signal, queryKey }) => { + const id = queryKey[2] as 'old' | 'new' + if (id === 'old') { + oldSignal = signal + return new Promise((resolve) => { + resolveOld = resolve + signal.addEventListener('abort', () => resolve('old-aborted'), { + once: true, + }) + }) + } + + return 'new-success' + }, + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => typeof oldSignal !== 'undefined') + + expect(oldSignal).toBeInstanceOf(AbortSignal) + expect(oldSignal?.aborted).toBe(false) + + keyId = 'new' + host.update() + await waitFor(() => query().data === 'new-success') + + expect(oldSignal?.aborted).toBe(true) + resolveOld?.('old-late') + await new Promise((resolve) => setTimeout(resolve, 20)) + expect(query().data).toBe('new-success') + }) + + it('S6: rapid key churn maintains stable final state without duplicate observers', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let keyId = 0 + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', 's6', keyId] as const, + queryFn: async ({ queryKey }) => { + const id = queryKey[2] as number + await new Promise((resolve) => + setTimeout(resolve, Math.max(1, 20 - id)), + ) + return `result-${id}` + }, + }), + client, + ) + + host.connect() + host.update() + + for (let i = 1; i <= 20; i += 1) { + keyId = i + host.update() + } + + await waitFor(() => query().isSuccess && query().data === 'result-20') + + const latestCacheEntry = client + .getQueryCache() + .find({ queryKey: ['query-controller', 's6', 20] }) + expect(latestCacheEntry?.getObserversCount()).toBe(1) + }) + + it('LIFE-01: disconnect while in-flight does not process detached updates', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let resolveFetch: ((value: string) => void) | undefined + + const query = createQueryController( + host, + { + queryKey: ['query-controller', 'life-01'], + queryFn: () => + new Promise((resolve) => { + resolveFetch = resolve + }), + }, + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isFetching) + + host.disconnect() + await Promise.resolve() + const updatesAfterDisconnect = host.updatesRequested + + resolveFetch?.('late-value') + await new Promise((resolve) => setTimeout(resolve, 20)) + + expect(host.updatesRequested).toBe(updatesAfterDisconnect) + }) + + it('LIFE-02: reconnect after in-flight settle yields correct snapshot', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let resolveFetch: ((value: string) => void) | undefined + + const query = createQueryController( + host, + { + queryKey: ['query-controller', 'life-02'], + queryFn: () => + new Promise((resolve) => { + resolveFetch = resolve + }), + }, + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isFetching) + + host.disconnect() + resolveFetch?.('reconnected-value') + await new Promise((resolve) => setTimeout(resolve, 20)) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + expect(query().data).toBe('reconnected-value') + }) + + it('AREACT-01: latest select closure is used after host updates', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let multiplier = 1 + + const query = createQueryController( + host, + () => ({ + queryKey: ['query-controller', 'areact-01'], + queryFn: async () => 2, + select: (value: number) => value * multiplier, + }), + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + expect(query().data).toBe(2) + + multiplier = 3 + host.update() + await waitFor(() => query().data === 6) + + multiplier = 4 + host.update() + await query.refetch() + expect(query().data).toBe(8) + }) + + it('M3: switches provider client while connected with a single active observer', async () => { + const clientA = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const clientB = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = clientA + + const consumer = document.createElement( + consumerTagName, + ) as QueryConsumerHostElement + provider.append(consumer) + document.body.append(provider) + + await provider.updateComplete + await consumer.updateComplete + await waitFor(() => consumer.query().isSuccess && consumer.queryCalls >= 1) + + const oldCacheQueryBeforeSwitch = clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + expect(oldCacheQueryBeforeSwitch?.getObserversCount()).toBe(1) + + provider.client = clientB + await provider.updateComplete + + await waitFor(() => { + const newCacheQuery = clientB + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + return Boolean(newCacheQuery && newCacheQuery.getObserversCount() === 1) + }) + + const oldCacheQueryAfterSwitch = clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + const newCacheQueryAfterSwitch = clientB + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + + expect(oldCacheQueryAfterSwitch?.getObserversCount() ?? 0).toBe(0) + expect(newCacheQueryAfterSwitch?.getObserversCount()).toBe(1) + + void clientB.invalidateQueries({ queryKey: consumer.queryKey }) + await waitFor(() => consumer.queryCalls >= 2) + + consumer.query.destroy() + provider.remove() + await Promise.resolve() + }) + + it('M5: tracks retry failure metadata before eventual success', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + let attempts = 0 + + const query = createQueryController( + host, + { + queryKey: ['query-controller', 'm5'], + retry: 2, + retryDelay: 30, + queryFn: async () => { + attempts += 1 + await new Promise((resolve) => setTimeout(resolve, 5)) + if (attempts < 3) { + throw new Error(`attempt-${attempts}`) + } + return 'success' + }, + }, + client, + ) + + host.connect() + host.update() + + await waitFor(() => query().failureCount >= 1) + expect(query().failureReason).toBeInstanceOf(Error) + expect(query().isPending || query().isError).toBe(true) + + await waitFor(() => query().isSuccess) + expect(query().data).toBe('success') + expect(attempts).toBe(3) + }) + + it('is reconnect-idempotent without duplicate subscriptions', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const host = new TestControllerHost() + const queryKey = ['todos', 'reconnect-idempotent'] as const + + const query = createQueryController( + host, + { + queryKey, + queryFn: async () => ['a', 'b'], + }, + client, + ) + + host.connect() + host.update() + await waitFor(() => query().isSuccess) + + const cacheQuery = client.getQueryCache().find({ queryKey }) + expect(cacheQuery).toBeDefined() + expect(cacheQuery?.getObserversCount()).toBe(1) + + host.disconnect() + expect(cacheQuery?.getObserversCount()).toBe(0) + + host.connect() + host.update() + await waitFor(() => cacheQuery?.getObserversCount() === 1) + + host.connect() + host.update() + + expect(cacheQuery?.getObserversCount()).toBe(1) + }) + + it('M17: no-explicit-client constructor path is safe before provider resolution', async () => { + const consumer = document.createElement( + consumerTagName, + ) as QueryConsumerHostElement + + expect(consumer.query().status).toBe('pending') + + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + provider.append(consumer) + + document.body.append(provider) + await provider.updateComplete + await consumer.updateComplete + + await waitFor(() => consumer.query().isSuccess) + expect(consumer.query().data).toBeDefined() + expect(consumer.queryCalls).toBeGreaterThan(0) + + consumer.query.destroy() + provider.remove() + await Promise.resolve() + }) + + it('LC-QUERY-01: provider-backed first connection does not spuriously throw during handshake', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + + const consumer = document.createElement( + consumerTagName, + ) as QueryConsumerHostElement + provider.append(consumer) + + expect(() => consumer.query()).not.toThrow() + + document.body.append(provider) + + expect(() => consumer.query()).not.toThrow() + + await provider.updateComplete + await consumer.updateComplete + await waitFor(() => consumer.query().isSuccess) + + expect(consumer.query().data).toBeDefined() + + consumer.query.destroy() + provider.remove() + await Promise.resolve() + }) + + it('throws after the initial placeholder phase when no provider is available', async () => { + const consumer = document.createElement( + consumerTagName, + ) as QueryConsumerHostElement + + expect(consumer.query().status).toBe('pending') + + document.body.append(consumer) + + expect(() => consumer.query()).not.toThrow() + await waitForMissingQueryClient(() => consumer.query()) + + await expect(consumer.query.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + await expect(consumer.query.suspense()).rejects.toThrow( + /No QueryClient available/, + ) + + consumer.query.destroy() + consumer.remove() + await Promise.resolve() + }) + + it('LC-QUERY-03: wrapper and result-object imperative methods share the missing-client contract', async () => { + const consumer = document.createElement( + consumerTagName, + ) as QueryConsumerHostElement + const placeholderResult = consumer.query() + + document.body.append(consumer) + await waitForMissingQueryClient(() => consumer.query()) + + await expect(consumer.query.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + await expect(placeholderResult.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + + consumer.query.destroy() + consumer.remove() + await Promise.resolve() + }) + + it('LC-QUERY-04: reconnect outside any provider clears stale provider-derived client state', async () => { + const client = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const provider = document.createElement( + providerTagName, + ) as QueryClientProvider + provider.client = client + + const consumer = document.createElement( + consumerTagName, + ) as QueryConsumerHostElement + provider.append(consumer) + document.body.append(provider) + + await provider.updateComplete + await consumer.updateComplete + await waitFor(() => consumer.query().isSuccess) + + expect( + client + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount(), + ).toBe(1) + + provider.removeChild(consumer) + await waitFor( + () => + (client + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount() ?? 0) === 0, + ) + await new Promise((resolve) => setTimeout(resolve, 0)) + consumer.connectedCallback() + + await expect(consumer.query.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + + consumer.query.destroy() + consumer.remove() + provider.remove() + await Promise.resolve() + }) + + it('LC-QUERY-05: reconnect under a different provider rebinds cleanly with later recovery', async () => { + const clientA = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const clientB = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, + }) + const providerA = document.createElement( + providerTagName, + ) as QueryClientProvider + providerA.client = clientA + const providerB = document.createElement( + providerTagName, + ) as QueryClientProvider + providerB.client = clientB + + const consumer = document.createElement( + consumerTagName, + ) as QueryConsumerHostElement + providerA.append(consumer) + + document.body.append(providerA) + + await providerA.updateComplete + await consumer.updateComplete + await waitFor(() => consumer.query().isSuccess) + + providerA.removeChild(consumer) + await waitFor( + () => + (clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount() ?? 0) === 0, + ) + consumer.connectedCallback() + await expect(consumer.query.refetch()).rejects.toThrow( + /No QueryClient available/, + ) + + consumer.disconnectedCallback() + providerB.append(consumer) + document.body.append(providerB) + await providerB.updateComplete + await waitFor(() => consumer.query().isSuccess && consumer.queryCalls >= 2) + + expect( + clientA + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount() ?? 0, + ).toBe(0) + expect( + clientB + .getQueryCache() + .find({ queryKey: consumer.queryKey }) + ?.getObserversCount(), + ).toBe(1) + expect(consumer.query().data).toBe(`value-${consumer.queryCalls}`) + + consumer.query.destroy() + providerA.remove() + providerB.remove() + await Promise.resolve() + }) + + it('reuses hydrated data on an already-connected host without an eager refetch', async () => { + const client = new QueryClient({ + defaultOptions: { queries: { retry: false, staleTime: 30_000 } }, + }) + let queryFnCalls = 0 + + client.setQueryData(['already-connected-test'], 'hydrated-value') + + // Simulate Lit's synchronous hostConnected call on already-connected hosts. + class AlreadyConnectedHost implements ReactiveControllerHost { + private readonly controllers = new Set() + private isConnected = true + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + if (this.isConnected) { + controller.hostConnected?.() + } + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + const host = new AlreadyConnectedHost() + + const query = createQueryController( + host, + { + queryKey: ['already-connected-test'], + queryFn: async () => { + queryFnCalls += 1 + return 'fetched-value' + }, + staleTime: 30_000, + }, + client, + ) + + await Promise.resolve() + await Promise.resolve() + + expect(query().data).toBe('hydrated-value') + expect(query().isSuccess).toBe(true) + await new Promise((resolve) => setTimeout(resolve, 50)) + expect(queryFnCalls).toBe(0) + + await query.refetch() + await waitFor(() => query().data === 'fetched-value') + expect(queryFnCalls).toBe(1) + + query.destroy() + }) + + it('defers explicit-client query accessors until host fields are initialized', () => { + const client = new QueryClient() + + class DeferredExplicitQueryHost implements ReactiveControllerHost { + private readonly controllers = new Set() + + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + readonly query = createQueryController( + this, + () => ({ + queryKey: ['deferred-explicit-query', this.id] as const, + queryFn: async () => this.id, + retry: false, + }), + client, + ) + + readonly firstRead = this.query() + readonly id = 'alpha' + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + } + + expect(() => new DeferredExplicitQueryHost()).not.toThrow() + + const host = new DeferredExplicitQueryHost() + expect(host.query().status).toBe('pending') + + host.query.destroy() + }) +}) diff --git a/packages/lit-query/src/tests/testHost.ts b/packages/lit-query/src/tests/testHost.ts new file mode 100644 index 00000000000..186433fe467 --- /dev/null +++ b/packages/lit-query/src/tests/testHost.ts @@ -0,0 +1,117 @@ +import type { ReactiveController, ReactiveControllerHost } from 'lit' + +export class TestControllerHost implements ReactiveControllerHost { + private readonly controllers = new Set() + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + + connect(): void { + for (const controller of this.controllers) { + controller.hostConnected?.() + } + } + + disconnect(): void { + for (const controller of this.controllers) { + controller.hostDisconnected?.() + } + } + + update(): void { + for (const controller of this.controllers) { + controller.hostUpdate?.() + } + + for (const controller of this.controllers) { + controller.hostUpdated?.() + } + } +} + +export class TestElementHost + extends HTMLElement + implements ReactiveControllerHost +{ + protected readonly controllers = new Set() + updatesRequested = 0 + readonly updateComplete: Promise = Promise.resolve(true) + + addController(controller: ReactiveController): void { + this.controllers.add(controller) + } + + removeController(controller: ReactiveController): void { + this.controllers.delete(controller) + } + + requestUpdate(): void { + this.updatesRequested += 1 + } + + connectedCallback(): void { + for (const controller of this.controllers) { + controller.hostConnected?.() + } + } + + disconnectedCallback(): void { + for (const controller of this.controllers) { + controller.hostDisconnected?.() + } + } + + flushHostUpdate(): void { + for (const controller of this.controllers) { + controller.hostUpdate?.() + } + + for (const controller of this.controllers) { + controller.hostUpdated?.() + } + } +} + +export async function waitFor( + assertion: () => boolean, + timeoutMs = 2000, +): Promise { + const startedAt = Date.now() + while (!assertion()) { + if (Date.now() - startedAt > timeoutMs) { + throw new Error(`Timed out waiting for assertion after ${timeoutMs}ms`) + } + await new Promise((resolve) => setTimeout(resolve, 10)) + } +} + +function isMissingQueryClientError(error: unknown): boolean { + return ( + error instanceof Error && /No QueryClient available/.test(error.message) + ) +} + +export async function waitForMissingQueryClient( + read: () => unknown, + timeoutMs = 2000, +): Promise { + await waitFor(() => { + try { + read() + return false + } catch (error) { + return isMissingQueryClientError(error) + } + }, timeoutMs) +} diff --git a/packages/lit-query/src/tests/type-inference.test.ts b/packages/lit-query/src/tests/type-inference.test.ts new file mode 100644 index 00000000000..e8c6c3c525d --- /dev/null +++ b/packages/lit-query/src/tests/type-inference.test.ts @@ -0,0 +1,210 @@ +import { + dataTagSymbol, + QueryClient, + type DefinedQueryObserverResult, + type QueryObserverResult, +} from '@tanstack/query-core' +import { describe, expectTypeOf, it } from 'vitest' +import { createMutationController } from '../createMutationController.js' +import { createQueriesController } from '../createQueriesController.js' +import { createInfiniteQueryController } from '../createInfiniteQueryController.js' +import { createQueryController } from '../createQueryController.js' +import { infiniteQueryOptions } from '../infiniteQueryOptions.js' +import { mutationOptions } from '../mutationOptions.js' +import { queryOptions } from '../queryOptions.js' +import { TestControllerHost } from './testHost.js' + +describe('type inference', () => { + it('L1: createQueriesController preserves tuple/combine inference', () => { + const client = new QueryClient() + const host = new TestControllerHost() + const expectTupleResult = ( + value: [QueryObserverResult, QueryObserverResult], + ) => value + const expectDefinedInitialDataTuple = ( + value: [DefinedQueryObserverResult<{ id: number; name: string }>], + ) => value + const expectMappedQueriesResult = ( + value: [ + ...Array>, + QueryObserverResult, + ], + ) => value + + const tupleResult = createQueriesController( + host, + { + queries: [ + { + queryKey: ['type-inference', 'tuple-number'] as const, + queryFn: async () => 1, + }, + { + queryKey: ['type-inference', 'tuple-string'] as const, + queryFn: async () => 'x', + }, + ] as const, + }, + client, + ) + + const tupleData = expectTupleResult(tupleResult()) + expectTypeOf(tupleData[0].data).toEqualTypeOf() + expectTypeOf(tupleData[1].data).toEqualTypeOf() + + const combinedResult = createQueriesController( + host, + { + queries: [ + { + queryKey: ['type-inference', 'combined-number'] as const, + queryFn: async () => 7, + }, + { + queryKey: ['type-inference', 'combined-string'] as const, + queryFn: async () => 'ok', + }, + ] as const, + combine: (result) => ({ + first: result[0].data, + second: result[1].data, + }), + }, + client, + ) + + expectTypeOf(combinedResult().first).toEqualTypeOf() + expectTypeOf(combinedResult().second).toEqualTypeOf() + + const definedInitialDataResult = createQueriesController( + host, + { + queries: [ + queryOptions({ + queryKey: ['type-inference', 'defined-initial-data'] as const, + queryFn: async () => ({ id: 4, name: 'Marie' }), + initialData: { id: 0, name: 'Seed' }, + }), + ] as const, + }, + client, + ) + + const definedInitialDataTuple = expectDefinedInitialDataTuple( + definedInitialDataResult(), + ) + expectTypeOf(definedInitialDataTuple[0].data).toEqualTypeOf<{ + id: number + name: string + }>() + + const definedInitialDataCombined = createQueriesController( + host, + { + queries: [ + queryOptions({ + queryKey: [ + 'type-inference', + 'defined-initial-data-combine', + ] as const, + queryFn: async () => ({ id: 5, name: 'Katherine' }), + initialData: { id: 1, name: 'Init' }, + }), + ] as const, + combine: (result) => result[0].data.name, + }, + client, + ) + + const definedInitialDataCombinedValue: string = definedInitialDataCombined() + expectTypeOf(definedInitialDataCombinedValue).toEqualTypeOf() + + const numberQueries = [1, 2, 3].map((value) => + queryOptions({ + queryKey: ['type-inference', 'mapped-number', value] as const, + queryFn: async () => value, + }), + ) + const mappedQueriesResult = createQueriesController( + host, + { + queries: [ + ...numberQueries, + queryOptions({ + queryKey: ['type-inference', 'mapped-boolean'] as const, + queryFn: async () => true, + }), + ], + }, + client, + ) + + const mappedQueriesData = expectMappedQueriesResult(mappedQueriesResult()) + expectTypeOf(mappedQueriesData[0].data).toEqualTypeOf< + number | boolean | undefined + >() + }) + + it('L2: helper option generics preserve controller inference', () => { + const client = new QueryClient() + const host = new TestControllerHost() + + const query = createQueryController( + host, + queryOptions({ + queryKey: ['type-inference', 'query'] as const, + queryFn: async () => ({ id: 1, name: 'Ada' }), + }), + client, + ) + expectTypeOf(query().data).toEqualTypeOf< + { id: number; name: string } | undefined + >() + + const mutation = createMutationController( + host, + mutationOptions({ + mutationFn: async (input: { id: number }) => input.id.toString(), + }), + client, + ) + expectTypeOf(mutation().data).toEqualTypeOf() + expectTypeOf(mutation().variables).toEqualTypeOf< + { id: number } | undefined + >() + + const queryOpts = queryOptions({ + queryKey: ['type-inference', 'query-options'] as const, + queryFn: async () => ({ id: 2, name: 'Grace' }), + }) + expectTypeOf(queryOpts.queryKey[dataTagSymbol]).toEqualTypeOf<{ + id: number + name: string + }>() + const cachedData = client.getQueryData(queryOpts.queryKey) + expectTypeOf(cachedData).toEqualTypeOf< + { id: number; name: string } | undefined + >() + const updatedData = client.setQueryData(queryOpts.queryKey, { + id: 3, + name: 'Lin', + }) + expectTypeOf(updatedData).toEqualTypeOf< + { id: number; name: string } | undefined + >() + + const infinite = createInfiniteQueryController( + host, + infiniteQueryOptions({ + queryKey: ['type-inference', 'infinite'] as const, + initialPageParam: 0, + queryFn: async () => ({ page: 1 }), + getNextPageParam: (lastPage) => lastPage.page + 1, + }), + client, + ) + expectTypeOf(infinite().data?.pages).toEqualTypeOf< + Array<{ page: number }> | undefined + >() + }) +}) diff --git a/packages/lit-query/src/types.ts b/packages/lit-query/src/types.ts new file mode 100644 index 00000000000..4eca18ed4e1 --- /dev/null +++ b/packages/lit-query/src/types.ts @@ -0,0 +1,77 @@ +import type { + DefaultError, + InfiniteData, + MutationObserverResult, + QueryKey, + QueryObserverResult, +} from '@tanstack/query-core' +import type { Accessor } from './accessor.js' +import type { CreateInfiniteQueryOptions } from './createInfiniteQueryController.js' +import type { CreateMutationOptions } from './createMutationController.js' +import type { + CreateQueriesControllerOptions, + CreateQueriesResults, +} from './createQueriesController.js' +import type { CreateQueryOptions } from './createQueryController.js' + +/** + * Accessor-wrapped options accepted by `createQueryController`. + */ +export type QueryControllerOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = TQueryFnData, + TQueryData = TQueryFnData, + TQueryKey extends QueryKey = QueryKey, +> = Accessor< + CreateQueryOptions +> + +/** + * Result object produced by a Lit query controller. + */ +export type QueryControllerResult< + TData = unknown, + TError = DefaultError, +> = QueryObserverResult + +/** + * Accessor-wrapped options accepted by `createInfiniteQueryController`. + */ +export type InfiniteQueryControllerOptions< + TQueryFnData = unknown, + TError = DefaultError, + TData = InfiniteData, + TQueryKey extends QueryKey = QueryKey, + TPageParam = unknown, +> = Accessor< + CreateInfiniteQueryOptions +> + +/** + * Accessor-wrapped options accepted by `createMutationController`. + */ +export type MutationControllerOptions< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> = Accessor> + +/** + * Result object produced by a Lit mutation controller. + */ +export type MutationControllerResult< + TData = unknown, + TError = DefaultError, + TVariables = void, + TOnMutateResult = unknown, +> = MutationObserverResult + +/** + * Accessor-wrapped options accepted by `createQueriesController`. + */ +export type QueriesControllerOptions< + TQueryOptions extends Array = Array, + TCombinedResult = CreateQueriesResults, +> = Accessor> diff --git a/packages/lit-query/src/useIsFetching.ts b/packages/lit-query/src/useIsFetching.ts new file mode 100644 index 00000000000..76abd9e408f --- /dev/null +++ b/packages/lit-query/src/useIsFetching.ts @@ -0,0 +1,159 @@ +import type { QueryClient, QueryFilters } from '@tanstack/query-core' +import type { ReactiveControllerHost } from 'lit' +import { + createValueAccessor, + readAccessor, + type Accessor, + type ValueAccessor, +} from './accessor.js' +import { BaseController } from './controllers/BaseController.js' + +/** + * Accessor returned by `useIsFetching`. + * + * Call the accessor or read its `current` property to get the number of + * currently fetching queries that match the filters. + */ +export type IsFetchingAccessor = ValueAccessor & { destroy: () => void } + +class IsFetchingController extends BaseController { + private queryClient: QueryClient | undefined + private unsubscribe: (() => void) | undefined + + constructor( + host: ReactiveControllerHost, + private readonly filters: Accessor = {}, + queryClient?: QueryClient, + ) { + super(host, 0, queryClient) + + if (!queryClient) { + return + } + + this.queryClient = queryClient + this.result = this.computeValue() + } + + protected onConnected(): void { + if (!this.syncClient()) { + this.setResult(0) + return + } + + this.subscribe() + this.setResult(this.computeValue()) + } + + protected onDisconnected(): void { + this.unsubscribe?.() + this.unsubscribe = undefined + this.syncClient() + } + + protected onHostUpdate(): void { + if (typeof this.filters !== 'function') { + return + } + + this.setResult(this.syncClient() ? this.computeValue() : 0) + } + + protected onQueryClientChanged(): void { + if (!this.syncClient()) { + this.setResult(0) + return + } + + if (this.connectedState) { + this.subscribe() + this.setResult(this.computeValue()) + } + } + + private syncClient(): boolean { + const nextClient = this.tryGetQueryClient() + if (!nextClient) { + this.unsubscribe?.() + this.unsubscribe = undefined + this.queryClient = undefined + return false + } + + if (nextClient === this.queryClient) { + return true + } + + this.unsubscribe?.() + this.unsubscribe = undefined + this.queryClient = nextClient + return true + } + + private subscribe(): void { + if (!this.queryClient) { + return + } + + if (this.unsubscribe) { + return + } + + this.unsubscribe = this.queryClient.getQueryCache().subscribe(() => { + this.setResult(this.computeValue()) + }) + } + + private computeValue(): number { + if (!this.queryClient) { + return 0 + } + + return this.queryClient.isFetching(readAccessor(this.filters)) + } +} + +/** + * Creates a Lit reactive controller that tracks how many matching queries are + * currently fetching. + * + * When `filters` is a function, it is re-read during host updates so the count + * can follow reactive host state. If `queryClient` is omitted, the controller + * resolves the client from the nearest connected `QueryClientProvider`. + * + * @param host - The Lit reactive controller host that owns the cache + * subscription. + * @param filters - Query filters, or a getter that returns query filters. + * @param queryClient - Optional explicit query client. Provide this for + * controllers that should not resolve a client from Lit context. + * @returns An accessor for the current number of matching fetching queries. + * + * @example + * ```ts + * import { LitElement, html } from 'lit' + * import { useIsFetching } from '@tanstack/lit-query' + * + * class TodosStatus extends LitElement { + * private readonly todosFetching = useIsFetching(this, { + * queryKey: ['todos'], + * }) + * + * render() { + * return html`${this.todosFetching()} active todo fetches` + * } + * } + * ``` + */ +export function useIsFetching( + host: ReactiveControllerHost, + filters: Accessor = {}, + queryClient?: QueryClient, +): IsFetchingAccessor { + const controller = new IsFetchingController(host, filters, queryClient) + return Object.assign( + createValueAccessor(() => controller.current), + { + destroy: () => controller.destroy(), + }, + ) +} diff --git a/packages/lit-query/src/useIsMutating.ts b/packages/lit-query/src/useIsMutating.ts new file mode 100644 index 00000000000..154ad4e5e6a --- /dev/null +++ b/packages/lit-query/src/useIsMutating.ts @@ -0,0 +1,159 @@ +import type { MutationFilters, QueryClient } from '@tanstack/query-core' +import type { ReactiveControllerHost } from 'lit' +import { + createValueAccessor, + readAccessor, + type Accessor, + type ValueAccessor, +} from './accessor.js' +import { BaseController } from './controllers/BaseController.js' + +/** + * Accessor returned by `useIsMutating`. + * + * Call the accessor or read its `current` property to get the number of + * currently pending mutations that match the filters. + */ +export type IsMutatingAccessor = ValueAccessor & { destroy: () => void } + +class IsMutatingController extends BaseController { + private queryClient: QueryClient | undefined + private unsubscribe: (() => void) | undefined + + constructor( + host: ReactiveControllerHost, + private readonly filters: Accessor = {}, + queryClient?: QueryClient, + ) { + super(host, 0, queryClient) + + if (!queryClient) { + return + } + + this.queryClient = queryClient + this.result = this.computeValue() + } + + protected onConnected(): void { + if (!this.syncClient()) { + this.setResult(0) + return + } + + this.subscribe() + this.setResult(this.computeValue()) + } + + protected onDisconnected(): void { + this.unsubscribe?.() + this.unsubscribe = undefined + this.syncClient() + } + + protected onHostUpdate(): void { + if (typeof this.filters !== 'function') { + return + } + + this.setResult(this.syncClient() ? this.computeValue() : 0) + } + + protected onQueryClientChanged(): void { + if (!this.syncClient()) { + this.setResult(0) + return + } + + if (this.connectedState) { + this.subscribe() + this.setResult(this.computeValue()) + } + } + + private syncClient(): boolean { + const nextClient = this.tryGetQueryClient() + if (!nextClient) { + this.unsubscribe?.() + this.unsubscribe = undefined + this.queryClient = undefined + return false + } + + if (nextClient === this.queryClient) { + return true + } + + this.unsubscribe?.() + this.unsubscribe = undefined + this.queryClient = nextClient + return true + } + + private subscribe(): void { + if (!this.queryClient) { + return + } + + if (this.unsubscribe) { + return + } + + this.unsubscribe = this.queryClient.getMutationCache().subscribe(() => { + this.setResult(this.computeValue()) + }) + } + + private computeValue(): number { + if (!this.queryClient) { + return 0 + } + + return this.queryClient.isMutating(readAccessor(this.filters)) + } +} + +/** + * Creates a Lit reactive controller that tracks how many matching mutations are + * currently pending. + * + * When `filters` is a function, it is re-read during host updates so the count + * can follow reactive host state. If `queryClient` is omitted, the controller + * resolves the client from the nearest connected `QueryClientProvider`. + * + * @param host - The Lit reactive controller host that owns the cache + * subscription. + * @param filters - Mutation filters, or a getter that returns mutation filters. + * @param queryClient - Optional explicit query client. Provide this for + * controllers that should not resolve a client from Lit context. + * @returns An accessor for the current number of matching pending mutations. + * + * @example + * ```ts + * import { LitElement, html } from 'lit' + * import { useIsMutating } from '@tanstack/lit-query' + * + * class MutationStatus extends LitElement { + * private readonly savesPending = useIsMutating(this, { + * mutationKey: ['save-project'], + * }) + * + * render() { + * return html`${this.savesPending()} saves pending` + * } + * } + * ``` + */ +export function useIsMutating( + host: ReactiveControllerHost, + filters: Accessor = {}, + queryClient?: QueryClient, +): IsMutatingAccessor { + const controller = new IsMutatingController(host, filters, queryClient) + return Object.assign( + createValueAccessor(() => controller.current), + { + destroy: () => controller.destroy(), + }, + ) +} diff --git a/packages/lit-query/src/useMutationState.ts b/packages/lit-query/src/useMutationState.ts new file mode 100644 index 00000000000..f3bc477db0a --- /dev/null +++ b/packages/lit-query/src/useMutationState.ts @@ -0,0 +1,201 @@ +import type { + Mutation, + MutationFilters, + MutationState, + QueryClient, +} from '@tanstack/query-core' +import type { ReactiveControllerHost } from 'lit' +import { + createValueAccessor, + readAccessor, + type Accessor, + type ValueAccessor, +} from './accessor.js' +import { BaseController } from './controllers/BaseController.js' + +/** + * Options accepted by `useMutationState`. + */ +export type MutationStateOptions = { + /** Filters used to select mutations from the mutation cache. */ + filters?: Accessor + /** Maps each matching mutation to the value returned by the accessor. */ + select?: (mutation: Mutation) => TResult +} + +/** + * Accessor returned by `useMutationState`. + * + * Call the accessor or read its `current` property to get the selected state for + * matching mutations. + */ +export type MutationStateAccessor = ValueAccessor & { + /** Removes the controller from its Lit host and unsubscribes observers. */ + destroy: () => void +} + +class MutationStateController extends BaseController { + private queryClient: QueryClient | undefined + private unsubscribe: (() => void) | undefined + + constructor( + host: ReactiveControllerHost, + private readonly options: MutationStateOptions, + queryClient?: QueryClient, + ) { + super(host, [], queryClient) + + if (!queryClient) { + return + } + + this.queryClient = queryClient + this.result = this.computeState() + } + + protected onConnected(): void { + if (!this.syncClient()) { + this.setResult([]) + return + } + + this.subscribe() + this.setResult(this.computeState()) + } + + protected onDisconnected(): void { + this.unsubscribe?.() + this.unsubscribe = undefined + this.syncClient() + } + + protected onHostUpdate(): void { + if (!this.shouldRefreshOnHostUpdate()) { + return + } + + this.setResult(this.syncClient() ? this.computeState() : []) + } + + protected onQueryClientChanged(): void { + if (!this.syncClient()) { + this.setResult([]) + return + } + + if (this.connectedState) { + this.subscribe() + this.setResult(this.computeState()) + } + } + + private syncClient(): boolean { + const nextClient = this.tryGetQueryClient() + if (!nextClient) { + this.unsubscribe?.() + this.unsubscribe = undefined + this.queryClient = undefined + return false + } + + if (nextClient === this.queryClient) { + return true + } + + this.unsubscribe?.() + this.unsubscribe = undefined + this.queryClient = nextClient + return true + } + + private subscribe(): void { + if (!this.queryClient) { + return + } + + if (this.unsubscribe) { + return + } + + this.unsubscribe = this.queryClient.getMutationCache().subscribe(() => { + this.setResult(this.computeState()) + }) + } + + private shouldRefreshOnHostUpdate(): boolean { + return ( + typeof this.options.filters === 'function' || + typeof this.options.select === 'function' + ) + } + + private computeState(): TResult[] { + if (!this.queryClient) { + return [] + } + + const filters = this.options.filters + ? readAccessor(this.options.filters) + : undefined + + const select = this.options.select + const mutations = this.queryClient.getMutationCache().findAll(filters) + + return mutations.map((mutation) => { + if (select) { + return select(mutation) + } + + return mutation.state as TResult + }) + } +} + +/** + * Creates a Lit reactive controller that selects state from matching mutations + * in the mutation cache. + * + * When `options.filters` is a function, it is re-read during host updates so + * the selection can follow reactive host state. If `queryClient` is omitted, + * the controller resolves the client from the nearest connected + * `QueryClientProvider`. + * + * @param host - The Lit reactive controller host that owns the mutation cache + * subscription. + * @param options - Mutation state filters and optional selector. + * @param queryClient - Optional explicit query client. Provide this for + * controllers that should not resolve a client from Lit context. + * @returns An accessor for the selected mutation state array. + * + * @example + * ```ts + * import { LitElement, html } from 'lit' + * import { useMutationState } from '@tanstack/lit-query' + * + * class PendingUploads extends LitElement { + * private readonly uploads = useMutationState(this, { + * filters: { mutationKey: ['upload'], status: 'pending' }, + * select: (mutation) => mutation.state.variables as File, + * }) + * + * render() { + * return html`${this.uploads().length} uploads pending` + * } + * } + * ``` + */ +export function useMutationState< + TResult = MutationState, +>( + host: ReactiveControllerHost, + options: MutationStateOptions = {}, + queryClient?: QueryClient, +): MutationStateAccessor { + const controller = new MutationStateController(host, options, queryClient) + return Object.assign( + createValueAccessor(() => controller.current), + { + destroy: () => controller.destroy(), + }, + ) +} diff --git a/packages/lit-query/tsconfig.build.cjs.json b/packages/lit-query/tsconfig.build.cjs.json new file mode 100644 index 00000000000..f6a332575d4 --- /dev/null +++ b/packages/lit-query/tsconfig.build.cjs.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.build.json", + "compilerOptions": { + "customConditions": null, + "module": "CommonJS", + "moduleResolution": "Node", + "outDir": "dist-cjs", + "declaration": false, + "declarationMap": false + }, + "exclude": ["src/tests/**/*.ts", "dist", "dist-cjs", "node_modules"] +} diff --git a/packages/lit-query/tsconfig.build.json b/packages/lit-query/tsconfig.build.json new file mode 100644 index 00000000000..5a1dd43717c --- /dev/null +++ b/packages/lit-query/tsconfig.build.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "incremental": false, + "composite": false, + "customConditions": [], + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "src", + "outDir": "dist" + }, + "include": ["src/**/*.ts"], + "exclude": ["src/tests/**/*.ts", "dist", "node_modules"] +} diff --git a/packages/lit-query/tsconfig.json b/packages/lit-query/tsconfig.json new file mode 100644 index 00000000000..9608d648e8c --- /dev/null +++ b/packages/lit-query/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "strict": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "isolatedModules": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noUncheckedIndexedAccess": true, + "forceConsistentCasingInFileNames": true, + "emitDeclarationOnly": false, + "rootDir": ".", + "outDir": "dist-ts" + }, + "include": ["src/**/*.ts", "*.config.*", "package.json"], + "exclude": ["dist", "node_modules"], + "references": [{ "path": "../query-core" }] +} diff --git a/packages/lit-query/vitest.config.ts b/packages/lit-query/vitest.config.ts new file mode 100644 index 00000000000..0ab94757be4 --- /dev/null +++ b/packages/lit-query/vitest.config.ts @@ -0,0 +1,26 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + // fix from https://github.com/vitest-dev/vitest/issues/6992#issuecomment-2509408660 + resolve: { + conditions: ['@tanstack/custom-condition'], + }, + environments: { + ssr: { + resolve: { + conditions: ['@tanstack/custom-condition'], + }, + }, + }, + test: { + dir: './src', + watch: false, + environment: 'jsdom', + include: ['tests/**/*.test.ts'], + coverage: { + enabled: false, + }, + typecheck: { enabled: true }, + restoreMocks: true, + }, +}) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82cb34c9228..c21c2185b7c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,7 +53,7 @@ importers: version: 0.3.1(typescript@5.9.3) '@tanstack/vite-config': specifier: 0.4.3 - version: 0.4.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 0.4.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@testing-library/jest-dom': specifier: ^6.8.0 version: 6.9.1 @@ -68,10 +68,10 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitest/coverage-istanbul': specifier: 4.0.6 - version: 4.0.6(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 4.0.6(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@vitest/eslint-plugin': specifier: ^1.4.0 - version: 1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) esbuild-plugin-file-path-extensions: specifier: ^2.1.4 version: 2.1.4 @@ -122,7 +122,7 @@ importers: version: 0.2.15 tsup: specifier: ^8.4.0 - version: 8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3) + version: 8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) typescript: specifier: 5.9.3 version: 5.9.3 @@ -149,10 +149,10 @@ importers: version: typescript@6.0.1-rc vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vitest: specifier: ^4.0.18 - version: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) examples/angular/auto-refetching: dependencies: @@ -183,7 +183,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -223,7 +223,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -269,7 +269,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -312,7 +312,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -352,7 +352,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -395,7 +395,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -435,7 +435,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -478,7 +478,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -521,7 +521,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -564,7 +564,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -604,7 +604,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -615,6 +615,72 @@ importers: specifier: 5.8.3 version: 5.8.3 + examples/lit/basic: + dependencies: + '@tanstack/lit-query': + specifier: ^0.1.0 + version: link:../../../packages/lit-query + '@tanstack/query-core': + specifier: ^5.99.0 + version: link:../../../packages/query-core + lit: + specifier: ^3.3.1 + version: 3.3.2 + devDependencies: + typescript: + specifier: 5.8.3 + version: 5.8.3 + vite: + specifier: ^6.4.1 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + examples/lit/pagination: + dependencies: + '@tanstack/lit-query': + specifier: ^0.1.0 + version: link:../../../packages/lit-query + '@tanstack/query-core': + specifier: ^5.99.0 + version: link:../../../packages/query-core + lit: + specifier: ^3.3.1 + version: 3.3.2 + devDependencies: + typescript: + specifier: 5.8.3 + version: 5.8.3 + vite: + specifier: ^6.4.1 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + examples/lit/ssr: + dependencies: + '@lit-labs/ssr': + specifier: ^3.3.0 + version: 3.3.1 + '@tanstack/lit-query': + specifier: ^0.1.0 + version: link:../../../packages/lit-query + '@tanstack/query-core': + specifier: ^5.99.0 + version: link:../../../packages/query-core + lit: + specifier: ^3.3.1 + version: 3.3.2 + devDependencies: + '@lit-labs/ssr-client': + specifier: ^1.1.7 + version: 1.1.8 + tsx: + specifier: ^4.19.0 + version: 4.21.0 + typescript: + specifier: 5.8.3 + version: 5.8.3 + vite: + specifier: ^6.4.1 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + examples/preact/simple: dependencies: '@tanstack/preact-query': @@ -626,7 +692,7 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.10.2 - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) eslint: specifier: ^9.36.0 version: 9.39.4(jiti@2.6.1) @@ -638,7 +704,7 @@ importers: version: 5.9.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/algolia: dependencies: @@ -669,13 +735,13 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/auto-refetching: dependencies: @@ -737,13 +803,13 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/basic-graphql-request: dependencies: @@ -768,10 +834,10 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/chat: dependencies: @@ -790,10 +856,10 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.0.14 - version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) tailwindcss: specifier: ^4.0.14 version: 4.2.2 @@ -802,7 +868,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/default-query-function: dependencies: @@ -821,13 +887,13 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/devtools-panel: dependencies: @@ -846,13 +912,13 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/eslint-legacy: dependencies: @@ -886,13 +952,13 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/eslint-plugin-demo: dependencies: @@ -1094,13 +1160,13 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/optimistic-updates-cache: dependencies: @@ -1203,13 +1269,13 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/prefetching: dependencies: @@ -1344,13 +1410,13 @@ importers: version: 1.2.3 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/rick-morty: dependencies: @@ -1375,10 +1441,10 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.1.13 - version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) tailwindcss: specifier: ^4.1.13 version: 4.2.2 @@ -1387,7 +1453,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/shadow-dom: dependencies: @@ -1412,13 +1478,13 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/simple: dependencies: @@ -1437,13 +1503,13 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/star-wars: dependencies: @@ -1468,10 +1534,10 @@ importers: devDependencies: '@tailwindcss/vite': specifier: ^4.1.13 - version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) tailwindcss: specifier: ^4.1.13 version: 4.2.2 @@ -1480,7 +1546,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/react/suspense: dependencies: @@ -1505,13 +1571,13 @@ importers: devDependencies: '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/solid/astro: dependencies: @@ -1520,16 +1586,16 @@ importers: version: 0.9.8(prettier@3.8.1)(typescript@5.8.3) '@astrojs/node': specifier: ^9.1.3 - version: 9.5.5(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3)) + version: 9.5.5(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3)) '@astrojs/solid-js': specifier: ^5.0.7 - version: 5.1.3(@testing-library/jest-dom@6.9.1)(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(solid-js@1.9.12)(terser@5.46.1)(yaml@2.8.3) + version: 5.1.3(@testing-library/jest-dom@6.9.1)(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(solid-js@1.9.12)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3))(tailwindcss@3.4.19(yaml@2.8.3)) + version: 6.0.2(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3)) '@astrojs/vercel': specifier: ^8.1.3 - version: 8.2.11(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(rollup@4.60.1)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3)) + version: 8.2.11(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(rollup@4.60.1)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3)) '@tanstack/solid-query': specifier: ^5.100.8 version: link:../../../packages/solid-query @@ -1538,13 +1604,13 @@ importers: version: link:../../../packages/solid-query-devtools astro: specifier: ^5.5.6 - version: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3) + version: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3) solid-js: specifier: ^1.9.7 version: 1.9.12 tailwindcss: specifier: ^3.4.7 - version: 3.4.19(yaml@2.8.3) + version: 3.4.19(tsx@4.21.0)(yaml@2.8.3) typescript: specifier: 5.8.3 version: 5.8.3 @@ -1566,10 +1632,10 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) examples/solid/basic-graphql-request: dependencies: @@ -1594,10 +1660,10 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) examples/solid/default-query-function: dependencies: @@ -1616,10 +1682,10 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) examples/solid/offline: dependencies: @@ -1647,10 +1713,10 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) examples/solid/simple: dependencies: @@ -1672,10 +1738,10 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) examples/solid/solid-start-streaming: dependencies: @@ -1687,7 +1753,7 @@ importers: version: 0.15.4(solid-js@1.9.12) '@solidjs/start': specifier: ^1.1.3 - version: 1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/solid-query': specifier: ^5.100.8 version: link:../../../packages/solid-query @@ -1699,7 +1765,7 @@ importers: version: 1.9.12 vinxi: specifier: ^0.5.3 - version: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/auto-refetching: dependencies: @@ -1712,13 +1778,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@sveltejs/kit': specifier: ^2.42.2 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) svelte: specifier: ^5.39.3 version: 5.55.1 @@ -1730,7 +1796,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/basic: dependencies: @@ -1749,13 +1815,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@sveltejs/kit': specifier: ^2.42.2 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) svelte: specifier: ^5.39.3 version: 5.55.1 @@ -1767,7 +1833,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/load-more-infinite-scroll: dependencies: @@ -1780,13 +1846,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@sveltejs/kit': specifier: ^2.42.2 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) svelte: specifier: ^5.39.3 version: 5.55.1 @@ -1798,7 +1864,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/optimistic-updates: dependencies: @@ -1811,13 +1877,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@sveltejs/kit': specifier: ^2.42.2 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) svelte: specifier: ^5.39.3 version: 5.55.1 @@ -1829,7 +1895,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/playground: dependencies: @@ -1842,13 +1908,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@sveltejs/kit': specifier: ^2.42.2 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) svelte: specifier: ^5.39.3 version: 5.55.1 @@ -1860,7 +1926,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/simple: dependencies: @@ -1873,7 +1939,7 @@ importers: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tsconfig/svelte': specifier: ^5.0.4 version: 5.0.8 @@ -1888,7 +1954,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/ssr: dependencies: @@ -1901,13 +1967,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@sveltejs/kit': specifier: ^2.42.2 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) svelte: specifier: ^5.39.3 version: 5.55.1 @@ -1919,7 +1985,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/svelte/star-wars: dependencies: @@ -1932,16 +1998,16 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^6.1.0 - version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@sveltejs/kit': specifier: ^2.42.2 - version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tailwindcss/vite': specifier: ^4.1.13 - version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) svelte: specifier: ^5.39.3 version: 5.55.1 @@ -1956,7 +2022,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/vue/basic: dependencies: @@ -1972,13 +2038,13 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.1 - version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/vue/dependent-queries: dependencies: @@ -1991,13 +2057,13 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.1 - version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/vue/persister: dependencies: @@ -2022,13 +2088,13 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.1 - version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) examples/vue/simple: dependencies: @@ -2044,13 +2110,13 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.1 - version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) integrations/angular-cli-20: dependencies: @@ -2087,7 +2153,7 @@ importers: devDependencies: '@angular/build': specifier: ^20.0.0 - version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3) + version: 20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3) '@angular/cli': specifier: ^20.0.0 version: 20.3.22(@types/node@22.19.15)(chokidar@4.0.3) @@ -2098,6 +2164,25 @@ importers: specifier: ~5.8.2 version: 5.8.3 + integrations/lit-vite: + dependencies: + '@tanstack/lit-query': + specifier: workspace:* + version: link:../../packages/lit-query + '@tanstack/query-core': + specifier: workspace:* + version: link:../../packages/query-core + lit: + specifier: ^3.3.1 + version: 3.3.2 + vite: + specifier: ^6.4.1 + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + devDependencies: + typescript: + specifier: 5.8.3 + version: 5.8.3 + integrations/react-next-14: dependencies: '@tanstack/react-query': @@ -2195,7 +2280,7 @@ importers: version: link:../../packages/react-query-devtools '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) react: specifier: ^19.0.0 version: 19.2.4 @@ -2204,7 +2289,7 @@ importers: version: 19.2.4(react@19.2.4) vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) integrations/react-webpack-4: dependencies: @@ -2296,16 +2381,16 @@ importers: version: 1.9.12 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) integrations/svelte-vite: devDependencies: '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/svelte-query': specifier: workspace:* version: link:../../packages/svelte-query @@ -2317,7 +2402,7 @@ importers: version: 5.55.1 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) integrations/vue-vite: dependencies: @@ -2330,13 +2415,13 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^5.2.1 - version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) typescript: specifier: 5.8.3 version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vue-tsc: specifier: ^2.2.8 version: 2.2.12(typescript@5.8.3) @@ -2373,13 +2458,13 @@ importers: version: 7.8.2 vite-plugin-dts: specifier: 4.2.3 - version: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite-plugin-externalize-deps: specifier: ^0.9.0 - version: 0.9.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 0.9.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: '@tanstack/query-devtools': specifier: workspace:* @@ -2451,6 +2536,28 @@ importers: specifier: ^5.0.0 version: 5.0.2 + packages/lit-query: + dependencies: + '@lit/context': + specifier: ^1.1.6 + version: 1.1.6 + '@tanstack/query-core': + specifier: workspace:* + version: link:../query-core + lit: + specifier: ^3.3.1 + version: 3.3.2 + devDependencies: + '@eslint/js': + specifier: ^9.36.0 + version: 9.39.4 + globals: + specifier: ^17.4.0 + version: 17.5.0 + typescript-eslint: + specifier: 8.58.1 + version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + packages/preact-query: dependencies: '@tanstack/query-core': @@ -2459,7 +2566,7 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.10.2 - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/query-persist-client-core': specifier: workspace:* version: link:../query-persist-client-core @@ -2499,7 +2606,7 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.10.2 - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/preact-query': specifier: workspace:* version: link:../preact-query @@ -2530,7 +2637,7 @@ importers: devDependencies: '@preact/preset-vite': specifier: ^2.10.2 - version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/preact-query': specifier: workspace:* version: link:../preact-query @@ -2586,7 +2693,7 @@ importers: version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) npm-run-all2: specifier: ^5.0.0 version: 5.0.2 @@ -2649,10 +2756,10 @@ importers: version: 2.2.6 tsup-preset-solid: specifier: ^2.2.0 - version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3)) + version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/query-persist-client-core: dependencies: @@ -2715,7 +2822,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) cpy-cli: specifier: ^5.0.0 version: 5.0.0 @@ -2749,7 +2856,7 @@ importers: version: 19.2.14 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) npm-run-all2: specifier: ^5.0.0 version: 5.0.2 @@ -2767,7 +2874,7 @@ importers: version: 19.2.14 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) next: specifier: ^16.0.1 version: 16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0) @@ -2798,7 +2905,7 @@ importers: version: 19.2.14 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) npm-run-all2: specifier: ^5.0.0 version: 5.0.2 @@ -2826,10 +2933,10 @@ importers: version: 1.9.12 tsup-preset-solid: specifier: ^2.2.0 - version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3)) + version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/solid-query-devtools: dependencies: @@ -2851,10 +2958,10 @@ importers: version: 1.9.12 tsup-preset-solid: specifier: ^2.2.0 - version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3)) + version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/solid-query-persist-client: dependencies: @@ -2879,10 +2986,10 @@ importers: version: 1.9.12 tsup-preset-solid: specifier: ^2.2.0 - version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3)) + version: 2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)) vite-plugin-solid: specifier: ^2.11.6 - version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) packages/svelte-query: dependencies: @@ -2895,13 +3002,13 @@ importers: version: 2.5.7(svelte@5.55.1)(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/query-test-utils': specifier: workspace:* version: link:../query-test-utils '@testing-library/svelte': specifier: ^5.2.8 - version: 5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@typescript-eslint/parser': specifier: 8.58.1 version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -2932,13 +3039,13 @@ importers: version: 2.5.7(svelte@5.55.1)(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/svelte-query': specifier: workspace:* version: link:../svelte-query '@testing-library/svelte': specifier: ^5.2.8 - version: 5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@typescript-eslint/parser': specifier: 8.58.1 version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -2966,7 +3073,7 @@ importers: version: 2.5.7(svelte@5.55.1)(typescript@5.9.3) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + version: 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tanstack/query-test-utils': specifier: workspace:* version: link:../query-test-utils @@ -2975,7 +3082,7 @@ importers: version: link:../svelte-query '@testing-library/svelte': specifier: ^5.2.8 - version: 5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))) + version: 5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))) '@typescript-eslint/parser': specifier: 8.58.1 version: 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -3012,7 +3119,7 @@ importers: version: link:../query-test-utils '@vitejs/plugin-vue': specifier: ^5.2.4 - version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3)) '@vue/composition-api': specifier: 1.7.2 version: 1.7.2(vue@3.5.31(typescript@5.9.3)) @@ -3040,7 +3147,7 @@ importers: version: link:../vue-query '@vitejs/plugin-vue': specifier: ^5.2.4 - version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) + version: 5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3)) eslint-plugin-vue: specifier: ^10.5.0 version: 10.8.0(@stylistic/eslint-plugin@5.10.0(eslint@9.39.4(jiti@2.6.1)))(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.4(jiti@2.6.1))(vue-eslint-parser@10.4.0(eslint@9.39.4(jiti@2.6.1))) @@ -3049,7 +3156,7 @@ importers: version: 5.8.3 vite: specifier: ^6.4.1 - version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + version: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vue: specifier: ^3.4.27 version: 3.5.31(typescript@5.8.3) @@ -5357,6 +5464,22 @@ packages: '@inquirer/prompts': '>= 3 < 8' listr2: 9.0.1 + '@lit-labs/ssr-client@1.1.8': + resolution: {integrity: sha512-PjGh81oKsoI64m3IDjTqqjhC7dr2uC/o0jrllUb5gRAyp/RlAHxapgJrjq9kWz97faCHLQ8jUlTi6tGm+8fgyA==} + + '@lit-labs/ssr-dom-shim@1.5.1': + resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} + + '@lit-labs/ssr@3.3.1': + resolution: {integrity: sha512-JlF1PempxvzrGEpRFrF+Ki0MHzR3HA51SK8Zv0cFpW9p0bPW4k0FeCwrElCu371UEpXF7RcaE2wgYaE1az0XKg==} + engines: {node: '>=13.9.0'} + + '@lit/context@1.1.6': + resolution: {integrity: sha512-M26qDE6UkQbZA2mQ3RjJ3Gzd8TxP+/0obMgE5HfkfLhEEyYE3Bui4A5XHiGPjy0MUGAyxB3QgVuw2ciS0kHn6A==} + + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} + '@lmdb/lmdb-darwin-arm64@3.4.2': resolution: {integrity: sha512-NK80WwDoODyPaSazKbzd3NEJ3ygePrkERilZshxBViBARNz21rmediktGHExoj9n5t9+ChlgLlxecdFKLCuCKg==} cpu: [arm64] @@ -6180,6 +6303,9 @@ packages: resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} engines: {node: '>= 10.0.0'} + '@parse5/tools@0.3.0': + resolution: {integrity: sha512-zxRyTHkqb7WQMV8kTNBKWb1BeOFUKXBXTBWuxg9H9hfvQB3IwP6Iw2U75Ia5eyRxPNltmY7E8YAlz6zWwUnjKg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -7603,9 +7729,23 @@ packages: vitest: optional: true + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + '@vitest/expect@4.1.2': resolution: {integrity: sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==} + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.4.1 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/mocker@4.1.2': resolution: {integrity: sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==} peerDependencies: @@ -7617,18 +7757,33 @@ packages: vite: optional: true + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + '@vitest/pretty-format@4.1.2': resolution: {integrity: sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==} + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + '@vitest/runner@4.1.2': resolution: {integrity: sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==} + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + '@vitest/snapshot@4.1.2': resolution: {integrity: sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==} + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + '@vitest/spy@4.1.2': resolution: {integrity: sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==} + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + '@vitest/utils@4.1.2': resolution: {integrity: sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==} @@ -8713,6 +8868,10 @@ packages: ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -8748,6 +8907,10 @@ packages: charenc@0.0.2: resolution: {integrity: sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -9270,6 +9433,10 @@ packages: cyclist@1.0.2: resolution: {integrity: sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==} + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -9361,6 +9528,10 @@ packages: dedent-js@1.0.1: resolution: {integrity: sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-equal@2.2.3: resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} engines: {node: '>= 0.4'} @@ -10213,6 +10384,10 @@ packages: picomatch: optional: true + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + fetch-retry@4.1.1: resolution: {integrity: sha512-e6eB7zN6UBSwGVwrbWVH+gdLnkW9WwHhmq2YDK1Sh30pzx1onRVGBvogTlUeWxwTa+L86NYdo4hFkh7O8ZjSnA==} @@ -10358,6 +10533,10 @@ packages: engines: {node: '>=18.3.0'} hasBin: true + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -10558,6 +10737,10 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} + engines: {node: '>=18'} + globalthis@1.0.4: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} @@ -11788,6 +11971,15 @@ packages: resolution: {integrity: sha512-SL0JY3DaxylDuo/MecFeiC+7pedM0zia33zl0vcjgwcq1q1FWWF1To9EIauPbl8GbMCU0R2e0uJ8bZunhYKD2g==} engines: {node: '>=20.0.0'} + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} + + lit-html@3.3.2: + resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==} + + lit@3.3.2: + resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} + lmdb@3.4.2: resolution: {integrity: sha512-nwVGUfTBUwJKXd6lRV8pFNfnrCC1+l49ESJRM19t/tFb/97QfJEixe5DYRvug5JO7DSFKoKaVy7oGMt5rVqZvg==} hasBin: true @@ -11889,6 +12081,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -12403,7 +12598,7 @@ packages: resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + resolution: {integrity: sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=} msgpackr-extract@3.0.3: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} @@ -12574,6 +12769,11 @@ packages: resolution: {integrity: sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg==} engines: {node: '>= 0.10.5'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + node-emoji@2.2.0: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} @@ -12594,6 +12794,10 @@ packages: encoding: optional: true + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-forge@1.4.0: resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} engines: {node: '>= 6.13.0'} @@ -13142,6 +13346,10 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pbkdf2@3.1.5: resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==} engines: {node: '>= 0.10'} @@ -14964,10 +15172,22 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyrainbow@3.1.0: resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} @@ -15110,6 +15330,11 @@ packages: typescript: optional: true + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tty-browserify@0.0.0: resolution: {integrity: sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==} @@ -15589,6 +15814,11 @@ packages: resolution: {integrity: sha512-82Qm+EG/b2PRFBvXBbz1lgWBGcd9totIL6SJhnrZYfakjloTVG9+5l6gfO6dbCCtztm5pqWFzLY0qpZ3H3ww/w==} hasBin: true + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-dts@4.2.3: resolution: {integrity: sha512-O5NalzHANQRwVw1xj8KQun3Bv8OSDAlNJXrnqoAz10BOuW8FVvY5g4ygj+DlJZL5mtSPuMu9vd3OfrdW5d4k6w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -15680,6 +15910,34 @@ packages: vite: optional: true + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^22.15.3 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vitest@4.1.2: resolution: {integrity: sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -16461,7 +16719,7 @@ snapshots: '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 - '@angular/build@20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(typescript@5.8.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(yaml@2.8.3)': + '@angular/build@20.3.22(@angular/compiler-cli@20.3.18(@angular/compiler@20.3.18)(typescript@5.8.3))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@types/node@22.19.15)(chokidar@4.0.3)(jiti@2.6.1)(lightningcss@1.32.0)(postcss@8.5.8)(tailwindcss@4.2.2)(terser@5.46.1)(tslib@2.8.1)(tsx@4.21.0)(typescript@5.8.3)(vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(yaml@2.8.3)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2003.22(chokidar@4.0.3) @@ -16471,7 +16729,7 @@ snapshots: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 '@inquirer/confirm': 5.1.14(@types/node@22.19.15) - '@vitejs/plugin-basic-ssl': 2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@vitejs/plugin-basic-ssl': 2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) beasties: 0.3.5 browserslist: 4.28.2 esbuild: 0.27.4 @@ -16491,7 +16749,7 @@ snapshots: tinyglobby: 0.2.14 tslib: 2.8.1 typescript: 5.8.3 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) watchpack: 2.4.4 optionalDependencies: '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) @@ -16499,7 +16757,7 @@ snapshots: lmdb: 3.4.2 postcss: 8.5.8 tailwindcss: 4.2.2 - vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vitest: 3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - chokidar @@ -16710,10 +16968,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/node@9.5.5(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3))': + '@astrojs/node@9.5.5(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3))': dependencies: '@astrojs/internal-helpers': 0.7.6 - astro: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3) + astro: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3) send: 1.2.1 server-destroy: 1.0.1 transitivePeerDependencies: @@ -16723,11 +16981,11 @@ snapshots: dependencies: prismjs: 1.30.0 - '@astrojs/solid-js@5.1.3(@testing-library/jest-dom@6.9.1)(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(solid-js@1.9.12)(terser@5.46.1)(yaml@2.8.3)': + '@astrojs/solid-js@5.1.3(@testing-library/jest-dom@6.9.1)(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(solid-js@1.9.12)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)': dependencies: solid-js: 1.9.12 - vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vite-plugin-solid: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-solid: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@testing-library/jest-dom' - '@types/node' @@ -16743,13 +17001,13 @@ snapshots: - tsx - yaml - '@astrojs/tailwind@6.0.2(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3))(tailwindcss@3.4.19(yaml@2.8.3))': + '@astrojs/tailwind@6.0.2(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3))(tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3))': dependencies: - astro: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3) + astro: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3) autoprefixer: 10.4.27(postcss@8.5.8) postcss: 8.5.8 postcss-load-config: 4.0.2(postcss@8.5.8) - tailwindcss: 3.4.19(yaml@2.8.3) + tailwindcss: 3.4.19(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - ts-node @@ -16765,14 +17023,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/vercel@8.2.11(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(rollup@4.60.1)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3))': + '@astrojs/vercel@8.2.11(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(rollup@4.60.1)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3))': dependencies: '@astrojs/internal-helpers': 0.7.4 - '@vercel/analytics': 1.6.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3)) + '@vercel/analytics': 1.6.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3)) '@vercel/functions': 2.2.13 '@vercel/nft': 0.30.3(rollup@4.60.1) '@vercel/routing-utils': 5.3.3 - astro: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3) + astro: 5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3) esbuild: 0.27.4 tinyglobby: 0.2.15 transitivePeerDependencies: @@ -19283,6 +19541,36 @@ snapshots: transitivePeerDependencies: - '@types/node' + '@lit-labs/ssr-client@1.1.8': + dependencies: + '@lit/reactive-element': 2.1.2 + lit: 3.3.2 + lit-html: 3.3.2 + + '@lit-labs/ssr-dom-shim@1.5.1': {} + + '@lit-labs/ssr@3.3.1': + dependencies: + '@lit-labs/ssr-client': 1.1.8 + '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit/reactive-element': 2.1.2 + '@parse5/tools': 0.3.0 + '@types/node': 22.19.15 + enhanced-resolve: 5.20.1 + lit: 3.3.2 + lit-element: 4.2.2 + lit-html: 3.3.2 + node-fetch: 3.3.2 + parse5: 7.3.0 + + '@lit/context@1.1.6': + dependencies: + '@lit/reactive-element': 2.1.2 + + '@lit/reactive-element@2.1.2': + dependencies: + '@lit-labs/ssr-dom-shim': 1.5.1 + '@lmdb/lmdb-darwin-arm64@3.4.2': optional: true @@ -19917,6 +20205,10 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6 + '@parse5/tools@0.3.0': + dependencies: + parse5: 7.3.0 + '@pkgjs/parseargs@0.11.0': optional: true @@ -19936,19 +20228,19 @@ snapshots: '@poppinss/exception@1.2.3': {} - '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@preact/preset-vite@2.10.5(@babel/core@7.29.0)(preact@10.29.0)(rollup@4.60.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) - '@prefresh/vite': 2.4.12(preact@10.29.0)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@prefresh/vite': 2.4.12(preact@10.29.0)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) babel-plugin-transform-hook-names: 1.0.2(@babel/core@7.29.0) debug: 4.4.3 magic-string: 0.30.21 picocolors: 1.1.1 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vite-prerender-plugin: 0.5.13(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-prerender-plugin: 0.5.13(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) zimmerframe: 1.1.4 transitivePeerDependencies: - preact @@ -19963,7 +20255,7 @@ snapshots: '@prefresh/utils@1.2.1': {} - '@prefresh/vite@2.4.12(preact@10.29.0)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@prefresh/vite@2.4.12(preact@10.29.0)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@prefresh/babel-plugin': 0.5.3 @@ -19971,7 +20263,7 @@ snapshots: '@prefresh/utils': 1.2.1 '@rollup/pluginutils': 4.2.1 preact: 10.29.0 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -20648,11 +20940,11 @@ snapshots: dependencies: solid-js: 1.9.12 - '@solidjs/start@1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@solidjs/start@1.3.2(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@tanstack/server-functions-plugin': 1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) - '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@tanstack/server-functions-plugin': 1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vinxi/server-components': 0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) cookie-es: 2.0.0 defu: 6.1.4 error-stack-parser: 2.1.4 @@ -20664,8 +20956,8 @@ snapshots: source-map-js: 1.2.1 terracotta: 1.1.0(solid-js@1.9.12) tinyglobby: 0.2.15 - vinxi: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vite-plugin-solid: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vinxi: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-solid: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@testing-library/jest-dom' - solid-js @@ -20697,15 +20989,15 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/adapter-auto@6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))': + '@sveltejs/adapter-auto@6.1.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -20717,16 +21009,16 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.55.1 - vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: typescript: 5.8.3 optional: true - '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@standard-schema/spec': 1.1.0 '@sveltejs/acorn-typescript': 1.0.9(acorn@8.16.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@types/cookie': 0.6.0 acorn: 8.16.0 cookie: 0.6.0 @@ -20738,7 +21030,7 @@ snapshots: set-cookie-parser: 3.1.0 sirv: 3.0.2 svelte: 5.55.1 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) optionalDependencies: typescript: 5.8.3 @@ -20753,49 +21045,49 @@ snapshots: transitivePeerDependencies: - typescript - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) debug: 4.4.3 svelte: 5.55.1 - vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color optional: true - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) debug: 4.4.3 svelte: 5.55.1 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.55.1 - vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color optional: true - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) debug: 4.4.3 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.55.1 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color @@ -20882,14 +21174,14 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 - '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@tailwindcss/vite@4.2.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@tailwindcss/node': 4.2.2 '@tailwindcss/oxide': 4.2.2 tailwindcss: 4.2.2 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@tanstack/directive-functions-plugin@1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@tanstack/directive-functions-plugin@1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.0 @@ -20898,7 +21190,7 @@ snapshots: '@tanstack/router-utils': 1.161.6 babel-dead-code-elimination: 1.0.12 tiny-invariant: 1.3.3 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color @@ -20943,7 +21235,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@tanstack/server-functions-plugin@1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/code-frame': 7.26.2 '@babel/core': 7.29.0 @@ -20952,7 +21244,7 @@ snapshots: '@babel/template': 7.28.6 '@babel/traverse': 7.29.0 '@babel/types': 7.29.0 - '@tanstack/directive-functions-plugin': 1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@tanstack/directive-functions-plugin': 1.121.21(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) babel-dead-code-elimination: 1.0.12 tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -20967,13 +21259,13 @@ snapshots: transitivePeerDependencies: - typescript - '@tanstack/vite-config@0.4.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@tanstack/vite-config@0.4.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: rollup-plugin-preserve-directives: 0.4.0(rollup@4.60.1) - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vite-plugin-dts: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) - vite-plugin-externalize-deps: 0.10.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) - vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-plugin-dts: 4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vite-plugin-externalize-deps: 0.10.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + vite-tsconfig-paths: 5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - '@types/node' - rollup @@ -21057,14 +21349,14 @@ snapshots: dependencies: svelte: 5.55.1 - '@testing-library/svelte@5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))': + '@testing-library/svelte@5.3.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/svelte-core': 1.0.0(svelte@5.55.1) svelte: 5.55.1 optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@tsconfig/svelte@5.0.8': {} @@ -21274,6 +21566,22 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/type-utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + '@typescript-eslint/visitor-keys': 8.58.1 + eslint: 9.39.4(jiti@2.6.1) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.1-rc) + typescript: 6.0.1-rc + transitivePeerDependencies: + - supports-color + '@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/scope-manager': 8.58.1 @@ -21298,6 +21606,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': + dependencies: + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + typescript: 6.0.1-rc + transitivePeerDependencies: + - supports-color + '@typescript-eslint/project-service@8.58.1(typescript@5.8.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@5.8.3) @@ -21316,6 +21636,15 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/project-service@8.58.1(typescript@6.0.1-rc)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.1-rc) + '@typescript-eslint/types': 8.58.1 + debug: 4.4.3 + typescript: 6.0.1-rc + transitivePeerDependencies: + - supports-color + '@typescript-eslint/rule-tester@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -21343,6 +21672,10 @@ snapshots: dependencies: typescript: 5.9.3 + '@typescript-eslint/tsconfig-utils@8.58.1(typescript@6.0.1-rc)': + dependencies: + typescript: 6.0.1-rc + '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@typescript-eslint/types': 8.58.1 @@ -21367,6 +21700,18 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/type-utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': + dependencies: + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.6.1) + ts-api-utils: 2.5.0(typescript@6.0.1-rc) + typescript: 6.0.1-rc + transitivePeerDependencies: + - supports-color + '@typescript-eslint/types@8.58.1': {} '@typescript-eslint/typescript-estree@8.58.1(typescript@5.8.3)': @@ -21399,6 +21744,21 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/typescript-estree@8.58.1(typescript@6.0.1-rc)': + dependencies: + '@typescript-eslint/project-service': 8.58.1(typescript@6.0.1-rc) + '@typescript-eslint/tsconfig-utils': 8.58.1(typescript@6.0.1-rc) + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/visitor-keys': 8.58.1 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@6.0.1-rc) + typescript: 6.0.1-rc + transitivePeerDependencies: + - supports-color + '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.8.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) @@ -21421,6 +21781,17 @@ snapshots: transitivePeerDependencies: - supports-color + '@typescript-eslint/utils@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.6.1)) + '@typescript-eslint/scope-manager': 8.58.1 + '@typescript-eslint/types': 8.58.1 + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) + eslint: 9.39.4(jiti@2.6.1) + typescript: 6.0.1-rc + transitivePeerDependencies: + - supports-color + '@typescript-eslint/visitor-keys@8.58.1': dependencies: '@typescript-eslint/types': 8.58.1 @@ -21499,9 +21870,9 @@ snapshots: '@urql/core': 5.2.0(graphql@16.13.2) wonka: 6.3.6 - '@vercel/analytics@1.6.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3))': + '@vercel/analytics@1.6.1(@sveltejs/kit@2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(next@16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0))(react@19.2.4)(svelte@5.55.1)(vue@3.5.31(typescript@5.8.3))': optionalDependencies: - '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@sveltejs/kit': 2.55.0(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.55.1)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))(svelte@5.55.1)(typescript@5.8.3)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) next: 16.2.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.90.0) react: 19.2.4 svelte: 5.55.1 @@ -21581,7 +21952,7 @@ snapshots: untun: 0.1.3 uqr: 0.1.2 - '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@vinxi/plugin-directives@0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/parser': 7.29.2 acorn: 8.16.0 @@ -21592,24 +21963,24 @@ snapshots: magicast: 0.2.11 recast: 0.23.11 tslib: 2.8.1 - vinxi: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vinxi: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@vinxi/server-components@0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@vinxi/server-components@0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) acorn: 8.16.0 acorn-loose: 8.5.2 acorn-typescript: 1.4.13(acorn@8.16.0) astring: 1.9.0 magicast: 0.2.11 recast: 0.23.11 - vinxi: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vinxi: 0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitejs/plugin-basic-ssl@2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@vitejs/plugin-basic-ssl@2.1.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@vitejs/plugin-react@4.7.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -21617,21 +21988,21 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3))': + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.8.3))': dependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.31(typescript@5.8.3) - '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.31(typescript@5.9.3))': dependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) vue: 3.5.31(typescript@5.9.3) - '@vitest/coverage-istanbul@4.0.6(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))': + '@vitest/coverage-istanbul@4.0.6(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@istanbuljs/schema': 0.1.3 debug: 4.4.3 @@ -21642,11 +22013,11 @@ snapshots: istanbul-reports: 3.2.0 magicast: 0.3.5 tinyrainbow: 3.1.0 - vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color - '@vitest/eslint-plugin@1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)))': + '@vitest/eslint-plugin@1.6.14(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))': dependencies: '@typescript-eslint/scope-manager': 8.58.1 '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) @@ -21654,10 +22025,19 @@ snapshots: optionalDependencies: '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3) typescript: 5.9.3 - vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vitest: 4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) transitivePeerDependencies: - supports-color + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + optional: true + '@vitest/expect@4.1.2': dependencies: '@standard-schema/spec': 1.1.0 @@ -21667,24 +22047,53 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3))': + '@vitest/mocker@3.2.4(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.14(@types/node@22.19.15)(typescript@5.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + optional: true + + '@vitest/mocker@4.1.2(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))': dependencies: '@vitest/spy': 4.1.2 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.12.14(@types/node@22.19.15)(typescript@5.9.3) - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + optional: true '@vitest/pretty-format@4.1.2': dependencies: tinyrainbow: 3.1.0 + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + optional: true + '@vitest/runner@4.1.2': dependencies: '@vitest/utils': 4.1.2 pathe: 2.0.3 + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + optional: true + '@vitest/snapshot@4.1.2': dependencies: '@vitest/pretty-format': 4.1.2 @@ -21692,8 +22101,20 @@ snapshots: magic-string: 0.30.21 pathe: 2.0.3 + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + optional: true + '@vitest/spy@4.1.2': {} + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + optional: true + '@vitest/utils@4.1.2': dependencies: '@vitest/pretty-format': 4.1.2 @@ -22445,7 +22866,7 @@ snapshots: astring@1.9.0: {} - astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(typescript@5.8.3)(yaml@2.8.3): + astro@5.18.1(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@1.21.7)(lightningcss@1.32.0)(rollup@4.60.1)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(typescript@5.8.3)(yaml@2.8.3): dependencies: '@astrojs/compiler': 2.13.1 '@astrojs/internal-helpers': 0.7.6 @@ -22502,8 +22923,8 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.5(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1) vfile: 6.0.3 - vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) xxhash-wasm: 1.1.0 yargs-parser: 21.1.1 yocto-spinner: 0.2.3 @@ -23184,6 +23605,15 @@ snapshots: ccount@2.0.1: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + optional: true + chai@6.2.2: {} chalk@2.4.2: @@ -23211,6 +23641,9 @@ snapshots: charenc@0.0.2: {} + check-error@2.1.3: + optional: true + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -23838,6 +24271,8 @@ snapshots: cyclist@1.0.2: {} + data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: dependencies: whatwg-mimetype: 4.0.0 @@ -23899,6 +24334,9 @@ snapshots: dedent-js@1.0.1: {} + deep-eql@5.0.2: + optional: true + deep-equal@2.2.3: dependencies: array-buffer-byte-length: 1.0.2 @@ -25099,6 +25537,11 @@ snapshots: optionalDependencies: picomatch: 4.0.4 + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + fetch-retry@4.1.1: {} fflate@0.8.2: {} @@ -25258,6 +25701,10 @@ snapshots: dependencies: fd-package-json: 2.0.0 + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + forwarded@0.2.0: {} fraction.js@5.3.4: {} @@ -25465,6 +25912,8 @@ snapshots: globals@16.5.0: {} + globals@17.5.0: {} + globalthis@1.0.4: dependencies: define-properties: 1.2.1 @@ -26879,6 +27328,22 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.2 + lit-element@4.2.2: + dependencies: + '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.2 + + lit-html@3.3.2: + dependencies: + '@types/trusted-types': 2.0.7 + + lit@3.3.2: + dependencies: + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.2 + lmdb@3.4.2: dependencies: msgpackr: 1.11.9 @@ -26992,6 +27457,9 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: + optional: true + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -28149,6 +28617,8 @@ snapshots: dependencies: minimatch: 3.1.5 + node-domexception@1.0.0: {} + node-emoji@2.2.0: dependencies: '@sindresorhus/is': 4.6.0 @@ -28169,6 +28639,12 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + node-forge@1.4.0: {} node-gyp-build-optional-packages@5.2.2: @@ -28908,6 +29384,9 @@ snapshots: pathe@2.0.3: {} + pathval@2.0.1: + optional: true + pbkdf2@3.1.5: dependencies: create-hash: 1.2.0 @@ -29007,20 +29486,22 @@ snapshots: optionalDependencies: postcss: 8.5.8 - postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3): + postcss-load-config@6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 1.21.7 postcss: 8.5.8 + tsx: 4.21.0 yaml: 2.8.3 - postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(yaml@2.8.3): + postcss-load-config@6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3): dependencies: lilconfig: 3.1.3 optionalDependencies: jiti: 2.6.1 postcss: 8.5.8 + tsx: 4.21.0 yaml: 2.8.3 postcss-media-query-parser@0.2.3: {} @@ -30883,7 +31364,7 @@ snapshots: tagged-tag@1.0.0: {} - tailwindcss@3.4.19(yaml@2.8.3): + tailwindcss@3.4.19(tsx@4.21.0)(yaml@2.8.3): dependencies: '@alloc/quick-lru': 5.2.0 arg: 5.0.2 @@ -30902,7 +31383,7 @@ snapshots: postcss: 8.5.8 postcss-import: 15.1.0(postcss@8.5.8) postcss-js: 4.1.0(postcss@8.5.8) - postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(yaml@2.8.3) + postcss-load-config: 6.0.1(jiti@1.21.7)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) postcss-nested: 6.2.0(postcss@8.5.8) postcss-selector-parser: 6.1.2 resolve: 1.22.11 @@ -31074,8 +31555,17 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tinypool@1.1.1: + optional: true + + tinyrainbow@2.0.0: + optional: true + tinyrainbow@3.1.0: {} + tinyspy@4.0.4: + optional: true + tldts-core@6.1.86: {} tldts-core@7.0.27: {} @@ -31156,6 +31646,10 @@ snapshots: dependencies: typescript: 5.9.3 + ts-api-utils@2.5.0(typescript@6.0.1-rc): + dependencies: + typescript: 6.0.1-rc + ts-declaration-location@1.0.7(typescript@5.9.3): dependencies: picomatch: 4.0.4 @@ -31187,16 +31681,16 @@ snapshots: tslib@2.8.1: {} - tsup-preset-solid@2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3)): + tsup-preset-solid@2.2.0(esbuild@0.27.4)(solid-js@1.9.12)(tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3)): dependencies: esbuild-plugin-solid: 0.5.0(esbuild@0.27.4)(solid-js@1.9.12) - tsup: 8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3) + tsup: 8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) transitivePeerDependencies: - esbuild - solid-js - supports-color - tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(typescript@5.9.3)(yaml@2.8.3): + tsup@8.5.1(@microsoft/api-extractor@7.47.7(@types/node@22.19.15))(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.4) cac: 6.7.14 @@ -31207,7 +31701,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(yaml@2.8.3) + postcss-load-config: 6.0.1(jiti@2.6.1)(postcss@8.5.8)(tsx@4.21.0)(yaml@2.8.3) resolve-from: 5.0.0 rollup: 4.60.1 source-map: 0.7.6 @@ -31225,6 +31719,13 @@ snapshots: - tsx - yaml + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + tty-browserify@0.0.0: {} tuf-js@4.1.0: @@ -31344,6 +31845,17 @@ snapshots: transitivePeerDependencies: - supports-color + typescript-eslint@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc): + dependencies: + '@typescript-eslint/eslint-plugin': 8.58.1(@typescript-eslint/parser@8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + '@typescript-eslint/parser': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + '@typescript-eslint/typescript-estree': 8.58.1(typescript@6.0.1-rc) + '@typescript-eslint/utils': 8.58.1(eslint@9.39.4(jiti@2.6.1))(typescript@6.0.1-rc) + eslint: 9.39.4(jiti@2.6.1) + typescript: 6.0.1-rc + transitivePeerDependencies: + - supports-color + typescript@5.3.3: {} typescript@5.4.2: {} @@ -31704,7 +32216,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3): + vinxi@0.5.11(@types/node@22.19.15)(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: '@babel/core': 7.29.0 '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) @@ -31738,7 +32250,7 @@ snapshots: unctx: 2.5.0 unenv: 1.10.0 unstorage: 1.17.5(@vercel/functions@2.2.13)(db0@0.3.4)(idb-keyval@6.2.2)(ioredis@5.10.1) - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) zod: 4.3.6 transitivePeerDependencies: - '@azure/app-configuration' @@ -31786,7 +32298,29 @@ snapshots: - xml2js - yaml - vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-node@3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + optional: true + + vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@microsoft/api-extractor': 7.47.7(@types/node@22.19.15) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) @@ -31799,13 +32333,13 @@ snapshots: magic-string: 0.30.21 typescript: 5.9.3 optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-dts@4.2.3(@types/node@22.19.15)(rollup@4.60.1)(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@microsoft/api-extractor': 7.47.7(@types/node@22.19.15) '@rollup/pluginutils': 5.3.0(rollup@4.60.1) @@ -31818,21 +32352,21 @@ snapshots: magic-string: 0.30.21 typescript: 6.0.1-rc optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - '@types/node' - rollup - supports-color - vite-plugin-externalize-deps@0.10.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-externalize-deps@0.10.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-plugin-externalize-deps@0.9.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-externalize-deps@0.9.0(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-plugin-solid@2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-solid@2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 @@ -31840,14 +32374,14 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.12 solid-refresh: 0.6.3(solid-js@1.9.12) - vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: - supports-color - vite-plugin-solid@2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-plugin-solid@2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.12)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 @@ -31855,14 +32389,14 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.12 solid-refresh: 0.6.3(solid-js@1.9.12) - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) - vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vitefu: 1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) optionalDependencies: '@testing-library/jest-dom': 6.9.1 transitivePeerDependencies: - supports-color - vite-prerender-plugin@0.5.13(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-prerender-plugin@0.5.13(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: kolorist: 1.8.0 magic-string: 0.30.21 @@ -31870,31 +32404,31 @@ snapshots: simple-code-frame: 1.3.0 source-map: 0.7.6 stack-trace: 1.0.0-pre2 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@5.9.3)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@5.9.3) optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript - vite-tsconfig-paths@5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vite-tsconfig-paths@5.1.4(typescript@6.0.1-rc)(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: debug: 4.4.3 globrex: 0.1.2 tsconfck: 3.1.6(typescript@6.0.1-rc) optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) transitivePeerDependencies: - supports-color - typescript - vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3): + vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.4) @@ -31909,9 +32443,10 @@ snapshots: lightningcss: 1.32.0 sass: 1.90.0 terser: 5.46.1 + tsx: 4.21.0 yaml: 2.8.3 - vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3): + vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): dependencies: esbuild: 0.27.4 fdir: 6.5.0(picomatch@4.0.4) @@ -31926,20 +32461,65 @@ snapshots: lightningcss: 1.32.0 sass: 1.90.0 terser: 5.46.1 + tsx: 4.21.0 yaml: 2.8.3 - vitefu@1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vitefu@1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@1.21.7)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) - vitefu@1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vitefu@1.1.2(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): optionalDependencies: - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + + vitest@3.2.4(@types/debug@4.1.13)(@types/node@22.19.15)(jiti@2.6.1)(jsdom@27.4.0)(lightningcss@1.32.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(msw@2.12.14(@types/node@22.19.15)(typescript@5.8.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + vite-node: 3.2.4(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.13 + '@types/node': 22.19.15 + jsdom: 27.4.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + optional: true - vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)): + vitest@4.1.2(@types/node@22.19.15)(jsdom@27.4.0)(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)): dependencies: '@vitest/expect': 4.1.2 - '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3)) + '@vitest/mocker': 4.1.2(msw@2.12.14(@types/node@22.19.15)(typescript@5.9.3))(vite@6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)) '@vitest/pretty-format': 4.1.2 '@vitest/runner': 4.1.2 '@vitest/snapshot': 4.1.2 @@ -31956,7 +32536,7 @@ snapshots: tinyexec: 1.0.4 tinyglobby: 0.2.15 tinyrainbow: 3.1.0 - vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(yaml@2.8.3) + vite: 6.4.1(@types/node@22.19.15)(jiti@2.6.1)(lightningcss@1.32.0)(sass@1.90.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.15 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c5f243dfaab..34482c7ec4f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -8,6 +8,7 @@ packages: - 'examples/angular/*' - 'examples/react/*' - 'examples/preact/*' + - 'examples/lit/*' - 'examples/solid/*' - 'examples/svelte/*' - 'examples/vue/*' diff --git a/scripts/generate-docs.ts b/scripts/generate-docs.ts index 6d613f3bb73..f0a6ada4c8b 100644 --- a/scripts/generate-docs.ts +++ b/scripts/generate-docs.ts @@ -1,46 +1,184 @@ -import { resolve } from 'node:path' +import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' -import { generateReferenceDocs } from '@tanstack/typedoc-config' const __dirname = fileURLToPath(new URL('.', import.meta.url)) +const require = createRequire(import.meta.url) +const typedocConfigPackageJson = + require.resolve('@tanstack/typedoc-config/package.json') +const typedocConfigDir = dirname(typedocConfigPackageJson) +const typedocConfigRequire = createRequire(typedocConfigPackageJson) +const TypeDoc = await import(typedocConfigRequire.resolve('typedoc')) -await generateReferenceDocs({ - packages: [ - { - name: 'angular-query-experimental', - entryPoints: [ - resolve( - __dirname, - '../packages/angular-query-experimental/src/index.ts', - ), - ], - tsconfig: resolve( - __dirname, - '../packages/angular-query-experimental/tsconfig.json', - ), - outputDir: resolve(__dirname, '../docs/framework/angular/reference'), - exclude: ['./packages/query-core/**/*'], - }, - { - name: 'svelte-query', - entryPoints: [ - resolve(__dirname, '../packages/svelte-query/src/index.ts'), - ], - tsconfig: resolve(__dirname, '../packages/svelte-query/tsconfig.json'), - outputDir: resolve(__dirname, '../docs/framework/svelte/reference'), - exclude: ['./packages/query-core/**/*'], - }, - { - name: 'preact-query', - entryPoints: [ - resolve(__dirname, '../packages/preact-query/src/index.ts'), - ], - tsconfig: resolve(__dirname, '../packages/preact-query/tsconfig.json'), - outputDir: resolve(__dirname, '../docs/framework/preact/reference'), - exclude: ['./packages/query-core/**/*'], - }, - ], -}) +type PackageReferenceDocsConfig = { + entryPoints: Array + tsconfig: string + outputDir: string + exclude?: Array + excludeExternals?: boolean + simplifyLitQueriesControllerTypes?: boolean + trimGeneratedMarkdown?: boolean +} + +type TypeDocReflectionWithSignatures = { + name: string + children?: Array + signatures?: Array<{ + typeParameters?: Array<{ + name: string + default?: unknown + }> + }> +} + +function simplifyLitQueriesControllerTypes( + project: TypeDocReflectionWithSignatures, +) { + const stack: Array = [project] + + for (const reflection of stack) { + stack.push(...(reflection.children ?? [])) + + if (reflection.name !== 'createQueriesController') { + continue + } + + for (const signature of reflection.signatures ?? []) { + const combinedResult = signature.typeParameters?.find( + (typeParameter) => typeParameter.name === 'TCombinedResult', + ) + + if (!combinedResult?.default) { + continue + } + + const queryOptionsType = TypeDoc.ReferenceType.createBrokenReference( + 'TQueryOptions', + project, + undefined, + ) + queryOptionsType.refersToTypeParameter = true + + // CreateQueriesResults is internal; render it as plain text, not a link. + const queriesResultsType = TypeDoc.ReferenceType.createBrokenReference( + 'CreateQueriesResults', + project, + undefined, + ) + queriesResultsType.typeArguments = [queryOptionsType] + + combinedResult.default = queriesResultsType + } + } +} + +async function trimTrailingWhitespaceInMarkdown(outputDir: string) { + const entries = await readdir(outputDir, { withFileTypes: true }) + + await Promise.all( + entries.map(async (entry) => { + const path = resolve(outputDir, entry.name) + + if (entry.isDirectory()) { + await trimTrailingWhitespaceInMarkdown(path) + return + } + + if (!entry.isFile() || !path.endsWith('.md')) { + return + } + + const markdown = await readFile(path, 'utf8') + const trimmed = markdown.replace(/[ \t]+$/gm, '') + + if (trimmed !== markdown) { + await writeFile(path, trimmed) + } + }), + ) +} + +async function generatePackageReferenceDocs(pkg: PackageReferenceDocsConfig) { + const outputDir = pkg.outputDir + await rm(outputDir, { recursive: true, force: true }) + await mkdir(outputDir, { recursive: true }) + + const app = await TypeDoc.Application.bootstrapWithPlugins({ + plugin: [ + 'typedoc-plugin-markdown', + 'typedoc-plugin-frontmatter', + resolve(typedocConfigDir, './src/typedoc-custom-settings.js'), + ], + hideGenerator: true, + readme: 'none', + entryFileName: 'index', + hideBreadcrumbs: true, + hidePageHeader: true, + useCodeBlocks: true, + excludePrivate: true, + excludeInternal: true, + excludeExternals: pkg.excludeExternals, + sourceLinkTemplate: + 'https://github.com/TanStack/query/blob/{gitRevision}/{path}#L{line}', + gitRevision: 'main', + entryPoints: pkg.entryPoints, + tsconfig: pkg.tsconfig, + exclude: pkg.exclude, + out: outputDir, + }) + + const project = await app.convert() + + if (project) { + if (pkg.simplifyLitQueriesControllerTypes) { + simplifyLitQueriesControllerTypes(project) + } + + await app.generateOutputs(project) + + if (pkg.trimGeneratedMarkdown) { + await trimTrailingWhitespaceInMarkdown(outputDir) + } + } +} + +for (const pkg of [ + { + entryPoints: [ + resolve(__dirname, '../packages/angular-query-experimental/src/index.ts'), + ], + tsconfig: resolve( + __dirname, + '../packages/angular-query-experimental/tsconfig.json', + ), + outputDir: resolve(__dirname, '../docs/framework/angular/reference'), + exclude: ['./packages/query-core/**/*'], + }, + { + entryPoints: [resolve(__dirname, '../packages/svelte-query/src/index.ts')], + tsconfig: resolve(__dirname, '../packages/svelte-query/tsconfig.json'), + outputDir: resolve(__dirname, '../docs/framework/svelte/reference'), + exclude: ['./packages/query-core/**/*'], + }, + { + entryPoints: [resolve(__dirname, '../packages/preact-query/src/index.ts')], + tsconfig: resolve(__dirname, '../packages/preact-query/tsconfig.json'), + outputDir: resolve(__dirname, '../docs/framework/preact/reference'), + exclude: ['./packages/query-core/**/*'], + }, + { + entryPoints: [resolve(__dirname, '../packages/lit-query/src/index.ts')], + tsconfig: resolve(__dirname, '../packages/lit-query/tsconfig.json'), + outputDir: resolve(__dirname, '../docs/framework/lit/reference'), + exclude: ['./packages/query-core/**/*'], + excludeExternals: true, + simplifyLitQueriesControllerTypes: true, + trimGeneratedMarkdown: true, + }, +] satisfies Array) { + await generatePackageReferenceDocs(pkg) +} console.log('\n✅ All markdown files have been processed!')