@pyreon/query 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,628 +1,356 @@
1
- import { CancelledError, InfiniteQueryObserver, MutationCache, MutationObserver, QueriesObserver, QueryCache, QueryClient, QueryObserver, defaultShouldDehydrateMutation, defaultShouldDehydrateQuery, dehydrate, hashKey, hydrate, isCancelledError, keepPreviousData } from "@tanstack/query-core";
2
- import { createContext, onMount, onUnmount, popContext, pushContext, useContext } from "@pyreon/core";
3
- import { batch, effect, signal } from "@pyreon/reactivity";
1
+ import { CancelledError, DefaultError, DehydratedState, FetchQueryOptions, InfiniteData, InfiniteQueryObserverOptions, InfiniteQueryObserverResult, InvalidateOptions, InvalidateQueryFilters, MutateFunction, MutationCache, MutationFilters, MutationFilters as MutationFilters$1, MutationObserverOptions, MutationObserverResult, QueryCache, QueryClient, QueryClient as QueryClient$1, QueryClientConfig, QueryFilters, QueryFilters as QueryFilters$1, QueryKey, QueryKey as QueryKey$1, QueryObserverOptions, QueryObserverResult, RefetchOptions, RefetchQueryFilters, defaultShouldDehydrateMutation, defaultShouldDehydrateQuery, dehydrate, hashKey, hydrate, isCancelledError, keepPreviousData } from "@tanstack/query-core";
2
+ import * as _pyreon_core0 from "@pyreon/core";
3
+ import { Props, VNode, VNodeChild } from "@pyreon/core";
4
+ import { Signal } from "@pyreon/reactivity";
4
5
 
5
- //#region src/query-client.ts
6
-
7
- /**
8
- * Provides a QueryClient to all descendant components via context.
9
- * Wrap your app root with this to enable useQuery / useMutation throughout the tree.
10
- *
11
- * @example
12
- * const client = new QueryClient()
13
- * mount(h(QueryClientProvider, { client }, h(App, null)), el)
14
- */
15
- function QueryClientProvider(props) {
16
- pushContext(new Map([[QueryClientContext.id, props.client]]));
17
- onMount(() => {
18
- props.client.mount();
19
- return () => props.client.unmount();
20
- });
21
- onUnmount(() => popContext());
22
- const ch = props.children;
23
- return typeof ch === "function" ? ch() : ch;
6
+ //#region src/query-client.d.ts
7
+ interface QueryClientProviderProps extends Props {
8
+ client: QueryClient$1;
9
+ children?: VNodeChild;
24
10
  }
11
+ declare const QueryClientContext: _pyreon_core0.Context<QueryClient$1 | null>;
25
12
  /**
26
- * Returns the nearest QueryClient provided by <QueryClientProvider>.
27
- * Throws if called outside of one.
28
- */
29
- function useQueryClient() {
30
- const client = useContext(QueryClientContext);
31
- if (!client) throw new Error("[pyreon/query] No QueryClient found. Wrap your app with <QueryClientProvider client={client}>.");
32
- return client;
33
- }
34
-
35
- //#endregion
36
- //#region src/use-infinite-query.ts
13
+ * Provides a QueryClient to all descendant components via context.
14
+ * Wrap your app root with this to enable useQuery / useMutation throughout the tree.
15
+ *
16
+ * @example
17
+ * const client = new QueryClient()
18
+ * mount(h(QueryClientProvider, { client }, h(App, null)), el)
19
+ */
20
+ declare function QueryClientProvider(props: QueryClientProviderProps): VNode;
37
21
  /**
38
- * Subscribe to a paginated / infinite-scroll query.
39
- * Returns fine-grained reactive signals plus `fetchNextPage`, `fetchPreviousPage`,
40
- * `hasNextPage` and `hasPreviousPage`.
41
- *
42
- * @example
43
- * const query = useInfiniteQuery(() => ({
44
- * queryKey: ['posts'],
45
- * queryFn: ({ pageParam }) => fetchPosts(pageParam as number),
46
- * initialPageParam: 0,
47
- * getNextPageParam: (lastPage) => lastPage.nextCursor,
48
- * }))
49
- * // query.data()?.pages — array of pages
50
- * // h('button', { onClick: () => query.fetchNextPage() }, 'Load more')
51
- */
52
- function useInfiniteQuery(options) {
53
- const observer = new InfiniteQueryObserver(useQueryClient(), options());
54
- const initial = observer.getCurrentResult();
55
- const resultSig = signal(initial);
56
- const dataSig = signal(initial.data);
57
- const errorSig = signal(initial.error ?? null);
58
- const statusSig = signal(initial.status);
59
- const isPending = signal(initial.isPending);
60
- const isLoading = signal(initial.isLoading);
61
- const isFetching = signal(initial.isFetching);
62
- const isFetchingNextPage = signal(initial.isFetchingNextPage);
63
- const isFetchingPreviousPage = signal(initial.isFetchingPreviousPage);
64
- const isError = signal(initial.isError);
65
- const isSuccess = signal(initial.isSuccess);
66
- const hasNextPage = signal(initial.hasNextPage);
67
- const hasPreviousPage = signal(initial.hasPreviousPage);
68
- const unsub = observer.subscribe(r => {
69
- batch(() => {
70
- resultSig.set(r);
71
- dataSig.set(r.data);
72
- errorSig.set(r.error ?? null);
73
- statusSig.set(r.status);
74
- isPending.set(r.isPending);
75
- isLoading.set(r.isLoading);
76
- isFetching.set(r.isFetching);
77
- isFetchingNextPage.set(r.isFetchingNextPage);
78
- isFetchingPreviousPage.set(r.isFetchingPreviousPage);
79
- isError.set(r.isError);
80
- isSuccess.set(r.isSuccess);
81
- hasNextPage.set(r.hasNextPage);
82
- hasPreviousPage.set(r.hasPreviousPage);
83
- });
84
- });
85
- effect(() => {
86
- observer.setOptions(options());
87
- });
88
- onUnmount(() => unsub());
89
- return {
90
- result: resultSig,
91
- data: dataSig,
92
- error: errorSig,
93
- status: statusSig,
94
- isPending,
95
- isLoading,
96
- isFetching,
97
- isFetchingNextPage,
98
- isFetchingPreviousPage,
99
- isError,
100
- isSuccess,
101
- hasNextPage,
102
- hasPreviousPage,
103
- fetchNextPage: () => observer.fetchNextPage(),
104
- fetchPreviousPage: () => observer.fetchPreviousPage(),
105
- refetch: () => observer.refetch()
106
- };
107
- }
108
-
22
+ * Returns the nearest QueryClient provided by <QueryClientProvider>.
23
+ * Throws if called outside of one.
24
+ */
25
+ declare function useQueryClient(): QueryClient$1;
109
26
  //#endregion
110
- //#region src/use-is-fetching.ts
111
- /**
112
- * Returns a signal that tracks how many queries are currently in-flight.
113
- * Useful for global loading indicators.
114
- *
115
- * @example
116
- * const fetching = useIsFetching()
117
- * // h('span', null, () => fetching() > 0 ? 'Loading…' : '')
118
- */
119
- function useIsFetching(filters) {
120
- const client = useQueryClient();
121
- const count = signal(client.isFetching(filters));
122
- const unsub = client.getQueryCache().subscribe(() => {
123
- count.set(client.isFetching(filters));
124
- });
125
- onUnmount(() => unsub());
126
- return count;
27
+ //#region src/use-infinite-query.d.ts
28
+ interface UseInfiniteQueryResult<TQueryFnData, TError = DefaultError> {
29
+ /** Raw signal full observer result. */
30
+ result: Signal<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
31
+ data: Signal<InfiniteData<TQueryFnData> | undefined>;
32
+ error: Signal<TError | null>;
33
+ status: Signal<'pending' | 'error' | 'success'>;
34
+ isPending: Signal<boolean>;
35
+ isLoading: Signal<boolean>;
36
+ isFetching: Signal<boolean>;
37
+ isFetchingNextPage: Signal<boolean>;
38
+ isFetchingPreviousPage: Signal<boolean>;
39
+ isError: Signal<boolean>;
40
+ isSuccess: Signal<boolean>;
41
+ hasNextPage: Signal<boolean>;
42
+ hasPreviousPage: Signal<boolean>;
43
+ fetchNextPage: () => Promise<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
44
+ fetchPreviousPage: () => Promise<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
45
+ refetch: () => Promise<QueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
127
46
  }
128
47
  /**
129
- * Returns a signal that tracks how many mutations are currently in-flight.
130
- *
131
- * @example
132
- * const mutating = useIsMutating()
133
- * // h('span', null, () => mutating() > 0 ? 'Saving…' : '')
134
- */
135
- function useIsMutating(filters) {
136
- const client = useQueryClient();
137
- const count = signal(client.isMutating(filters));
138
- const unsub = client.getMutationCache().subscribe(() => {
139
- count.set(client.isMutating(filters));
140
- });
141
- onUnmount(() => unsub());
142
- return count;
143
- }
144
-
48
+ * Subscribe to a paginated / infinite-scroll query.
49
+ * Returns fine-grained reactive signals plus `fetchNextPage`, `fetchPreviousPage`,
50
+ * `hasNextPage` and `hasPreviousPage`.
51
+ *
52
+ * @example
53
+ * const query = useInfiniteQuery(() => ({
54
+ * queryKey: ['posts'],
55
+ * queryFn: ({ pageParam }) => fetchPosts(pageParam as number),
56
+ * initialPageParam: 0,
57
+ * getNextPageParam: (lastPage) => lastPage.nextCursor,
58
+ * }))
59
+ * // query.data()?.pages — array of pages
60
+ * // h('button', { onClick: () => query.fetchNextPage() }, 'Load more')
61
+ */
62
+ declare function useInfiniteQuery<TQueryFnData = unknown, TError = DefaultError, TQueryKey extends QueryKey$1 = QueryKey$1, TPageParam = unknown>(options: () => InfiniteQueryObserverOptions<TQueryFnData, TError, InfiniteData<TQueryFnData>, TQueryKey, TPageParam>): UseInfiniteQueryResult<TQueryFnData, TError>;
145
63
  //#endregion
146
- //#region src/use-mutation.ts
64
+ //#region src/use-is-fetching.d.ts
147
65
  /**
148
- * Run a mutation (create / update / delete). Returns reactive signals for
149
- * pending / success / error state plus `mutate` and `mutateAsync` functions.
150
- *
151
- * @example
152
- * const mutation = useMutation({
153
- * mutationFn: (data: CreatePostInput) =>
154
- * fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) }).then(r => r.json()),
155
- * onSuccess: () => client.invalidateQueries({ queryKey: ['posts'] }),
156
- * })
157
- * // h('button', { onClick: () => mutation.mutate({ title: 'New' }) }, 'Create')
158
- */
159
- function useMutation(options) {
160
- const observer = new MutationObserver(useQueryClient(), options);
161
- const initial = observer.getCurrentResult();
162
- const resultSig = signal(initial);
163
- const dataSig = signal(initial.data);
164
- const errorSig = signal(initial.error ?? null);
165
- const statusSig = signal(initial.status);
166
- const isPending = signal(initial.isPending);
167
- const isSuccess = signal(initial.isSuccess);
168
- const isError = signal(initial.isError);
169
- const isIdle = signal(initial.isIdle);
170
- const unsub = observer.subscribe(r => {
171
- batch(() => {
172
- resultSig.set(r);
173
- dataSig.set(r.data);
174
- errorSig.set(r.error ?? null);
175
- statusSig.set(r.status);
176
- isPending.set(r.isPending);
177
- isSuccess.set(r.isSuccess);
178
- isError.set(r.isError);
179
- isIdle.set(r.isIdle);
180
- });
181
- });
182
- onUnmount(() => unsub());
183
- return {
184
- result: resultSig,
185
- data: dataSig,
186
- error: errorSig,
187
- status: statusSig,
188
- isPending,
189
- isSuccess,
190
- isError,
191
- isIdle,
192
- mutate: (vars, callbackOptions) => {
193
- observer.mutate(vars, callbackOptions).catch(() => {});
194
- },
195
- mutateAsync: (vars, callbackOptions) => observer.mutate(vars, callbackOptions),
196
- reset: () => observer.reset()
197
- };
198
- }
199
-
200
- //#endregion
201
- //#region src/use-queries.ts
66
+ * Returns a signal that tracks how many queries are currently in-flight.
67
+ * Useful for global loading indicators.
68
+ *
69
+ * @example
70
+ * const fetching = useIsFetching()
71
+ * // h('span', null, () => fetching() > 0 ? 'Loading…' : '')
72
+ */
73
+ declare function useIsFetching(filters?: QueryFilters$1): Signal<number>;
202
74
  /**
203
- * Subscribe to multiple queries in parallel. Returns a single signal containing
204
- * the array of results — index-aligned with the `queries` array.
205
- *
206
- * `queries` is a reactive function so signal-based keys trigger re-evaluation
207
- * automatically.
208
- *
209
- * @example
210
- * const userIds = signal([1, 2, 3])
211
- * const results = useQueries(() =>
212
- * userIds().map(id => ({
213
- * queryKey: ['user', id],
214
- * queryFn: () => fetchUser(id),
215
- * }))
216
- * )
217
- * // results() — QueryObserverResult[]
218
- * // results()[0].data — first user
219
- */
220
- function useQueries(queries) {
221
- const observer = new QueriesObserver(useQueryClient(), queries());
222
- const resultSig = signal(observer.getCurrentResult());
223
- const unsub = observer.subscribe(results => {
224
- resultSig.set(results);
225
- });
226
- effect(() => {
227
- observer.setQueries(queries());
228
- });
229
- onUnmount(() => {
230
- unsub();
231
- observer.destroy();
232
- });
233
- return resultSig;
234
- }
235
-
75
+ * Returns a signal that tracks how many mutations are currently in-flight.
76
+ *
77
+ * @example
78
+ * const mutating = useIsMutating()
79
+ * // h('span', null, () => mutating() > 0 ? 'Saving…' : '')
80
+ */
81
+ declare function useIsMutating(filters?: MutationFilters$1): Signal<number>;
236
82
  //#endregion
237
- //#region src/use-query.ts
238
- /**
239
- * Subscribe to a query. Returns fine-grained reactive signals for data,
240
- * error and status — each signal only notifies effects that depend on it.
241
- *
242
- * `options` is a function so it can read Pyreon signals — when a signal changes
243
- * (e.g. a reactive query key), the observer is updated and refetches automatically.
244
- *
245
- * @example
246
- * const userId = signal(1)
247
- * const query = useQuery(() => ({
248
- * queryKey: ['user', userId()],
249
- * queryFn: () => fetch(`/api/users/${userId()}`).then(r => r.json()),
250
- * }))
251
- * // In template: () => query.data()?.name
252
- */
253
- function useQuery(options) {
254
- const observer = new QueryObserver(useQueryClient(), options());
255
- const initial = observer.getCurrentResult();
256
- const resultSig = signal(initial);
257
- const dataSig = signal(initial.data);
258
- const errorSig = signal(initial.error ?? null);
259
- const statusSig = signal(initial.status);
260
- const isPending = signal(initial.isPending);
261
- const isLoading = signal(initial.isLoading);
262
- const isFetching = signal(initial.isFetching);
263
- const isError = signal(initial.isError);
264
- const isSuccess = signal(initial.isSuccess);
265
- const unsub = observer.subscribe(r => {
266
- batch(() => {
267
- resultSig.set(r);
268
- dataSig.set(r.data);
269
- errorSig.set(r.error ?? null);
270
- statusSig.set(r.status);
271
- isPending.set(r.isPending);
272
- isLoading.set(r.isLoading);
273
- isFetching.set(r.isFetching);
274
- isError.set(r.isError);
275
- isSuccess.set(r.isSuccess);
276
- });
277
- });
278
- effect(() => {
279
- observer.setOptions(options());
280
- });
281
- onUnmount(() => unsub());
282
- return {
283
- result: resultSig,
284
- data: dataSig,
285
- error: errorSig,
286
- status: statusSig,
287
- isPending,
288
- isLoading,
289
- isFetching,
290
- isError,
291
- isSuccess,
292
- refetch: () => observer.refetch()
293
- };
83
+ //#region src/use-mutation.d.ts
84
+ interface UseMutationResult<TData, TError = DefaultError, TVariables = void, TContext = unknown> {
85
+ /** Raw signal full observer result. Fine-grained accessors below are preferred. */
86
+ result: Signal<MutationObserverResult<TData, TError, TVariables, TContext>>;
87
+ data: Signal<TData | undefined>;
88
+ error: Signal<TError | null>;
89
+ status: Signal<'idle' | 'pending' | 'success' | 'error'>;
90
+ isPending: Signal<boolean>;
91
+ isSuccess: Signal<boolean>;
92
+ isError: Signal<boolean>;
93
+ isIdle: Signal<boolean>;
94
+ /** Fire the mutation (fire-and-forget). Errors are captured in the error signal. */
95
+ mutate: (variables: TVariables, options?: Parameters<MutateFunction<TData, TError, TVariables, TContext>>[1]) => void;
96
+ /** Like mutate but returns a promise — use for try/catch error handling. */
97
+ mutateAsync: MutateFunction<TData, TError, TVariables, TContext>;
98
+ /** Reset the mutation state back to idle. */
99
+ reset: () => void;
294
100
  }
295
-
101
+ /**
102
+ * Run a mutation (create / update / delete). Returns reactive signals for
103
+ * pending / success / error state plus `mutate` and `mutateAsync` functions.
104
+ *
105
+ * @example
106
+ * const mutation = useMutation({
107
+ * mutationFn: (data: CreatePostInput) =>
108
+ * fetch('/api/posts', { method: 'POST', body: JSON.stringify(data) }).then(r => r.json()),
109
+ * onSuccess: () => client.invalidateQueries({ queryKey: ['posts'] }),
110
+ * })
111
+ * // h('button', { onClick: () => mutation.mutate({ title: 'New' }) }, 'Create')
112
+ */
113
+ declare function useMutation<TData = unknown, TError = DefaultError, TVariables = void, TContext = unknown>(options: MutationObserverOptions<TData, TError, TVariables, TContext>): UseMutationResult<TData, TError, TVariables, TContext>;
296
114
  //#endregion
297
- //#region src/use-query-error-reset-boundary.ts
298
-
115
+ //#region src/use-queries.d.ts
116
+ type UseQueriesOptions<TQueryKey extends QueryKey$1 = QueryKey$1> = QueryObserverOptions<unknown, DefaultError, unknown, unknown, TQueryKey>;
299
117
  /**
300
- * Wraps a subtree so that `useQueryErrorResetBoundary()` descendants can reset
301
- * all errored queries within this boundary.
302
- *
303
- * Pair with Pyreon's `ErrorBoundary` to retry failed queries when the user
304
- * dismisses the error fallback:
305
- *
306
- * @example
307
- * h(QueryErrorResetBoundary, null,
308
- * h(ErrorBoundary, {
309
- * fallback: (err, boundaryReset) => {
310
- * const { reset } = useQueryErrorResetBoundary()
311
- * return h('button', {
312
- * onClick: () => { reset(); boundaryReset() },
313
- * }, 'Retry')
314
- * },
315
- * }, h(MyComponent, null)),
316
- * )
317
- */
318
- function QueryErrorResetBoundary(props) {
319
- const client = useQueryClient();
320
- pushContext(new Map([[QueryErrorResetBoundaryContext.id, {
321
- reset: () => {
322
- client.refetchQueries({
323
- predicate: query => query.state.status === "error"
324
- });
325
- }
326
- }]]));
327
- onUnmount(() => popContext());
328
- const ch = props.children;
329
- return typeof ch === "function" ? ch() : ch;
118
+ * Subscribe to multiple queries in parallel. Returns a single signal containing
119
+ * the array of results index-aligned with the `queries` array.
120
+ *
121
+ * `queries` is a reactive function so signal-based keys trigger re-evaluation
122
+ * automatically.
123
+ *
124
+ * @example
125
+ * const userIds = signal([1, 2, 3])
126
+ * const results = useQueries(() =>
127
+ * userIds().map(id => ({
128
+ * queryKey: ['user', id],
129
+ * queryFn: () => fetchUser(id),
130
+ * }))
131
+ * )
132
+ * // results() — QueryObserverResult[]
133
+ * // results()[0].data — first user
134
+ */
135
+ declare function useQueries(queries: () => UseQueriesOptions[]): Signal<QueryObserverResult[]>;
136
+ //#endregion
137
+ //#region src/use-query.d.ts
138
+ interface UseQueryResult<TData, TError = DefaultError> {
139
+ /** Raw signal — the full observer result. Fine-grained accessors below are preferred. */
140
+ result: Signal<QueryObserverResult<TData, TError>>;
141
+ data: Signal<TData | undefined>;
142
+ error: Signal<TError | null>;
143
+ status: Signal<'pending' | 'error' | 'success'>;
144
+ isPending: Signal<boolean>;
145
+ isLoading: Signal<boolean>;
146
+ isFetching: Signal<boolean>;
147
+ isError: Signal<boolean>;
148
+ isSuccess: Signal<boolean>;
149
+ /** Manually trigger a refetch. */
150
+ refetch: () => Promise<QueryObserverResult<TData, TError>>;
330
151
  }
331
152
  /**
332
- * Returns the `reset` function provided by the nearest `QueryErrorResetBoundary`.
333
- * If called outside a boundary, falls back to resetting all errored queries
334
- * on the current `QueryClient`.
335
- *
336
- * @example
337
- * // Inside an ErrorBoundary fallback:
338
- * const { reset } = useQueryErrorResetBoundary()
339
- * h('button', { onClick: () => { reset(); boundaryReset() } }, 'Retry')
340
- */
341
- function useQueryErrorResetBoundary() {
342
- const boundary = useContext(QueryErrorResetBoundaryContext);
343
- const client = useQueryClient();
344
- if (boundary) return boundary;
345
- return {
346
- reset: () => {
347
- client.refetchQueries({
348
- predicate: query => query.state.status === "error"
349
- });
350
- }
351
- };
352
- }
353
-
153
+ * Subscribe to a query. Returns fine-grained reactive signals for data,
154
+ * error and status each signal only notifies effects that depend on it.
155
+ *
156
+ * `options` is a function so it can read Pyreon signals — when a signal changes
157
+ * (e.g. a reactive query key), the observer is updated and refetches automatically.
158
+ *
159
+ * @example
160
+ * const userId = signal(1)
161
+ * const query = useQuery(() => ({
162
+ * queryKey: ['user', userId()],
163
+ * queryFn: () => fetch(`/api/users/${userId()}`).then(r => r.json()),
164
+ * }))
165
+ * // In template: () => query.data()?.name
166
+ */
167
+ declare function useQuery<TData = unknown, TError = DefaultError, TKey extends QueryKey$1 = QueryKey$1>(options: () => QueryObserverOptions<TData, TError, TData, TData, TKey>): UseQueryResult<TData, TError>;
354
168
  //#endregion
355
- //#region src/use-subscription.ts
169
+ //#region src/use-query-error-reset-boundary.d.ts
170
+ interface ErrorResetBoundaryValue {
171
+ reset: () => void;
172
+ }
173
+ interface QueryErrorResetBoundaryProps extends Props {
174
+ children?: VNodeChild;
175
+ }
176
+ /**
177
+ * Wraps a subtree so that `useQueryErrorResetBoundary()` descendants can reset
178
+ * all errored queries within this boundary.
179
+ *
180
+ * Pair with Pyreon's `ErrorBoundary` to retry failed queries when the user
181
+ * dismisses the error fallback:
182
+ *
183
+ * @example
184
+ * h(QueryErrorResetBoundary, null,
185
+ * h(ErrorBoundary, {
186
+ * fallback: (err, boundaryReset) => {
187
+ * const { reset } = useQueryErrorResetBoundary()
188
+ * return h('button', {
189
+ * onClick: () => { reset(); boundaryReset() },
190
+ * }, 'Retry')
191
+ * },
192
+ * }, h(MyComponent, null)),
193
+ * )
194
+ */
195
+ declare function QueryErrorResetBoundary(props: QueryErrorResetBoundaryProps): VNode;
356
196
  /**
357
- * Reactive WebSocket subscription that integrates with TanStack Query.
358
- * Automatically manages connection lifecycle, reconnection, and cleanup.
359
- *
360
- * Use the `onMessage` callback to invalidate or update query cache
361
- * when the server pushes data.
362
- *
363
- * @example
364
- * ```ts
365
- * const sub = useSubscription({
366
- * url: 'wss://api.example.com/ws',
367
- * onMessage: (event, queryClient) => {
368
- * const data = JSON.parse(event.data)
369
- * if (data.type === 'order-updated') {
370
- * queryClient.invalidateQueries({ queryKey: ['orders'] })
371
- * }
372
- * },
373
- * })
374
- * // sub.status() — 'connecting' | 'connected' | 'disconnected' | 'error'
375
- * // sub.send(JSON.stringify({ type: 'subscribe', channel: 'orders' }))
376
- * ```
377
- */
378
- function useSubscription(options) {
379
- const queryClient = useQueryClient();
380
- const status = signal("disconnected");
381
- let ws = null;
382
- let reconnectAttempts = 0;
383
- let reconnectTimer = null;
384
- let intentionalClose = false;
385
- const reconnectEnabled = options.reconnect !== false;
386
- const baseDelay = options.reconnectDelay ?? 1e3;
387
- const maxAttempts = options.maxReconnectAttempts ?? 10;
388
- function getUrl() {
389
- return typeof options.url === "function" ? options.url() : options.url;
390
- }
391
- function isEnabled() {
392
- if (options.enabled === void 0) return true;
393
- return typeof options.enabled === "function" ? options.enabled() : options.enabled;
394
- }
395
- function connect() {
396
- if (ws) {
397
- ws.onopen = null;
398
- ws.onmessage = null;
399
- ws.onclose = null;
400
- ws.onerror = null;
401
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
402
- }
403
- if (!isEnabled()) {
404
- status.set("disconnected");
405
- return;
406
- }
407
- status.set("connecting");
408
- try {
409
- ws = options.protocols ? new WebSocket(getUrl(), options.protocols) : new WebSocket(getUrl());
410
- } catch {
411
- status.set("error");
412
- scheduleReconnect();
413
- return;
414
- }
415
- ws.onopen = event => {
416
- batch(() => {
417
- status.set("connected");
418
- reconnectAttempts = 0;
419
- });
420
- options.onOpen?.(event);
421
- };
422
- ws.onmessage = event => {
423
- options.onMessage(event, queryClient);
424
- };
425
- ws.onclose = event => {
426
- status.set("disconnected");
427
- options.onClose?.(event);
428
- if (!intentionalClose && reconnectEnabled) scheduleReconnect();
429
- };
430
- ws.onerror = event => {
431
- status.set("error");
432
- options.onError?.(event);
433
- };
434
- }
435
- function scheduleReconnect() {
436
- if (!reconnectEnabled) return;
437
- if (maxAttempts > 0 && reconnectAttempts >= maxAttempts) return;
438
- const delay = baseDelay * 2 ** reconnectAttempts;
439
- reconnectAttempts++;
440
- reconnectTimer = setTimeout(() => {
441
- reconnectTimer = null;
442
- if (!intentionalClose && isEnabled()) connect();
443
- }, delay);
444
- }
445
- function send(data) {
446
- if (ws?.readyState === WebSocket.OPEN) ws.send(data);
447
- }
448
- function close() {
449
- intentionalClose = true;
450
- if (reconnectTimer !== null) {
451
- clearTimeout(reconnectTimer);
452
- reconnectTimer = null;
453
- }
454
- if (ws) {
455
- ws.onopen = null;
456
- ws.onmessage = null;
457
- ws.onclose = null;
458
- ws.onerror = null;
459
- if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ws.close();
460
- ws = null;
461
- }
462
- status.set("disconnected");
463
- }
464
- function manualReconnect() {
465
- intentionalClose = false;
466
- reconnectAttempts = 0;
467
- connect();
468
- }
469
- effect(() => {
470
- if (typeof options.url === "function") options.url();
471
- if (typeof options.enabled === "function") options.enabled();
472
- intentionalClose = false;
473
- reconnectAttempts = 0;
474
- connect();
475
- });
476
- onUnmount(() => close());
477
- return {
478
- status,
479
- send,
480
- close,
481
- reconnect: manualReconnect
482
- };
197
+ * Returns the `reset` function provided by the nearest `QueryErrorResetBoundary`.
198
+ * If called outside a boundary, falls back to resetting all errored queries
199
+ * on the current `QueryClient`.
200
+ *
201
+ * @example
202
+ * // Inside an ErrorBoundary fallback:
203
+ * const { reset } = useQueryErrorResetBoundary()
204
+ * h('button', { onClick: () => { reset(); boundaryReset() } }, 'Retry')
205
+ */
206
+ declare function useQueryErrorResetBoundary(): ErrorResetBoundaryValue;
207
+ //#endregion
208
+ //#region src/use-subscription.d.ts
209
+ type SubscriptionStatus = 'connecting' | 'connected' | 'disconnected' | 'error';
210
+ interface UseSubscriptionOptions {
211
+ /** WebSocket URL — can be a signal for reactive URLs */
212
+ url: string | (() => string);
213
+ /** WebSocket sub-protocols */
214
+ protocols?: string | string[];
215
+ /** Called when a message is received — use queryClient to invalidate or update cache */
216
+ onMessage: (event: MessageEvent, queryClient: QueryClient$1) => void;
217
+ /** Called when the connection opens */
218
+ onOpen?: (event: Event) => void;
219
+ /** Called when the connection closes */
220
+ onClose?: (event: CloseEvent) => void;
221
+ /** Called when a connection error occurs */
222
+ onError?: (event: Event) => void;
223
+ /** Whether to automatically reconnect — default: true */
224
+ reconnect?: boolean;
225
+ /** Initial reconnect delay in ms — doubles on each retry, default: 1000 */
226
+ reconnectDelay?: number;
227
+ /** Maximum reconnect attempts default: 10, 0 = unlimited */
228
+ maxReconnectAttempts?: number;
229
+ /** Whether the subscription is enabled default: true */
230
+ enabled?: boolean | (() => boolean);
483
231
  }
484
-
232
+ interface UseSubscriptionResult {
233
+ /** Current connection status */
234
+ status: Signal<SubscriptionStatus>;
235
+ /** Send data through the WebSocket */
236
+ send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void;
237
+ /** Manually close the connection */
238
+ close: () => void;
239
+ /** Manually reconnect */
240
+ reconnect: () => void;
241
+ }
242
+ /**
243
+ * Reactive WebSocket subscription that integrates with TanStack Query.
244
+ * Automatically manages connection lifecycle, reconnection, and cleanup.
245
+ *
246
+ * Use the `onMessage` callback to invalidate or update query cache
247
+ * when the server pushes data.
248
+ *
249
+ * @example
250
+ * ```ts
251
+ * const sub = useSubscription({
252
+ * url: 'wss://api.example.com/ws',
253
+ * onMessage: (event, queryClient) => {
254
+ * const data = JSON.parse(event.data)
255
+ * if (data.type === 'order-updated') {
256
+ * queryClient.invalidateQueries({ queryKey: ['orders'] })
257
+ * }
258
+ * },
259
+ * })
260
+ * // sub.status() — 'connecting' | 'connected' | 'disconnected' | 'error'
261
+ * // sub.send(JSON.stringify({ type: 'subscribe', channel: 'orders' }))
262
+ * ```
263
+ */
264
+ declare function useSubscription(options: UseSubscriptionOptions): UseSubscriptionResult;
485
265
  //#endregion
486
- //#region src/use-suspense-query.ts
266
+ //#region src/use-suspense-query.d.ts
487
267
  /**
488
- * Pyreon-native Suspense boundary for queries. Shows `fallback` while any query
489
- * is pending. On error, renders the `error` fallback or re-throws to the
490
- * nearest Pyreon `ErrorBoundary`.
491
- *
492
- * Pair with `useSuspenseQuery` / `useSuspenseInfiniteQuery` to get non-undefined
493
- * `data` types inside children.
494
- *
495
- * @example
496
- * const userQuery = useSuspenseQuery(() => ({ queryKey: ['user'], queryFn: fetchUser }))
497
- *
498
- * h(QuerySuspense, {
499
- * query: userQuery,
500
- * fallback: h(Spinner, null),
501
- * error: (err) => h('p', null, `Failed: ${err}`),
502
- * }, () => h(UserProfile, { user: userQuery.data() }))
503
- */
504
- function QuerySuspense(props) {
505
- return () => {
506
- const queries = Array.isArray(props.query) ? props.query : [props.query];
507
- for (const q of queries) if (q.isError()) {
508
- const err = q.error();
509
- if (props.error) return props.error(err);
510
- throw err;
511
- }
512
- if (queries.some(q => q.isPending())) {
513
- const fb = props.fallback;
514
- return typeof fb === "function" ? fb() : fb ?? null;
515
- }
516
- const ch = props.children;
517
- return typeof ch === "function" ? ch() : ch;
518
- };
268
+ * Like `UseQueryResult` but `data` is `Signal<TData>` (never undefined).
269
+ * Only use inside a `QuerySuspense` boundary which guarantees the query has
270
+ * succeeded before children are rendered.
271
+ */
272
+ interface UseSuspenseQueryResult<TData, TError = DefaultError> {
273
+ result: Signal<QueryObserverResult<TData, TError>>;
274
+ /** Always TData — never undefined inside a QuerySuspense boundary. */
275
+ data: Signal<TData>;
276
+ error: Signal<TError | null>;
277
+ status: Signal<'pending' | 'error' | 'success'>;
278
+ isPending: Signal<boolean>;
279
+ isFetching: Signal<boolean>;
280
+ isError: Signal<boolean>;
281
+ isSuccess: Signal<boolean>;
282
+ refetch: () => Promise<QueryObserverResult<TData, TError>>;
519
283
  }
520
- /**
521
- * Like `useQuery` but `data` is typed as `Signal<TData>` (never undefined).
522
- * Designed for use inside a `QuerySuspense` boundary, which guarantees
523
- * children only render after the query succeeds.
524
- *
525
- * @example
526
- * const user = useSuspenseQuery(() => ({ queryKey: ['user', id()], queryFn: fetchUser }))
527
- *
528
- * h(QuerySuspense, { query: user, fallback: h(Spinner, null) },
529
- * () => h(UserCard, { name: user.data().name }),
530
- * )
531
- */
532
- function useSuspenseQuery(options) {
533
- const observer = new QueryObserver(useQueryClient(), options());
534
- const initial = observer.getCurrentResult();
535
- const resultSig = signal(initial);
536
- const dataSig = signal(initial.data);
537
- const errorSig = signal(initial.error ?? null);
538
- const statusSig = signal(initial.status);
539
- const isPending = signal(initial.isPending);
540
- const isFetching = signal(initial.isFetching);
541
- const isError = signal(initial.isError);
542
- const isSuccess = signal(initial.isSuccess);
543
- const unsub = observer.subscribe(r => {
544
- batch(() => {
545
- resultSig.set(r);
546
- if (r.data !== void 0) dataSig.set(r.data);
547
- errorSig.set(r.error ?? null);
548
- statusSig.set(r.status);
549
- isPending.set(r.isPending);
550
- isFetching.set(r.isFetching);
551
- isError.set(r.isError);
552
- isSuccess.set(r.isSuccess);
553
- });
554
- });
555
- effect(() => {
556
- observer.setOptions(options());
557
- });
558
- onUnmount(() => unsub());
559
- return {
560
- result: resultSig,
561
- data: dataSig,
562
- error: errorSig,
563
- status: statusSig,
564
- isPending,
565
- isFetching,
566
- isError,
567
- isSuccess,
568
- refetch: () => observer.refetch()
569
- };
284
+ interface UseSuspenseInfiniteQueryResult<TQueryFnData, TError = DefaultError> {
285
+ result: Signal<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
286
+ /** Always InfiniteData<TQueryFnData> never undefined inside a QuerySuspense boundary. */
287
+ data: Signal<InfiniteData<TQueryFnData>>;
288
+ error: Signal<TError | null>;
289
+ status: Signal<'pending' | 'error' | 'success'>;
290
+ isFetching: Signal<boolean>;
291
+ isFetchingNextPage: Signal<boolean>;
292
+ isFetchingPreviousPage: Signal<boolean>;
293
+ isError: Signal<boolean>;
294
+ isSuccess: Signal<boolean>;
295
+ hasNextPage: Signal<boolean>;
296
+ hasPreviousPage: Signal<boolean>;
297
+ fetchNextPage: () => Promise<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
298
+ fetchPreviousPage: () => Promise<InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
299
+ refetch: () => Promise<QueryObserverResult<InfiniteData<TQueryFnData>, TError>>;
570
300
  }
571
- /**
572
- * Like `useInfiniteQuery` but `data` is typed as `Signal<InfiniteData<TData>>`
573
- * (never undefined). Use inside a `QuerySuspense` boundary.
574
- */
575
- function useSuspenseInfiniteQuery(options) {
576
- const observer = new InfiniteQueryObserver(useQueryClient(), options());
577
- const initial = observer.getCurrentResult();
578
- const resultSig = signal(initial);
579
- const dataSig = signal(initial.data);
580
- const errorSig = signal(initial.error ?? null);
581
- const statusSig = signal(initial.status);
582
- const isFetching = signal(initial.isFetching);
583
- const isFetchingNextPage = signal(initial.isFetchingNextPage);
584
- const isFetchingPreviousPage = signal(initial.isFetchingPreviousPage);
585
- const isError = signal(initial.isError);
586
- const isSuccess = signal(initial.isSuccess);
587
- const hasNextPage = signal(initial.hasNextPage);
588
- const hasPreviousPage = signal(initial.hasPreviousPage);
589
- const unsub = observer.subscribe(r => {
590
- batch(() => {
591
- resultSig.set(r);
592
- if (r.data !== void 0) dataSig.set(r.data);
593
- errorSig.set(r.error ?? null);
594
- statusSig.set(r.status);
595
- isFetching.set(r.isFetching);
596
- isFetchingNextPage.set(r.isFetchingNextPage);
597
- isFetchingPreviousPage.set(r.isFetchingPreviousPage);
598
- isError.set(r.isError);
599
- isSuccess.set(r.isSuccess);
600
- hasNextPage.set(r.hasNextPage);
601
- hasPreviousPage.set(r.hasPreviousPage);
602
- });
603
- });
604
- effect(() => {
605
- observer.setOptions(options());
606
- });
607
- onUnmount(() => unsub());
608
- return {
609
- result: resultSig,
610
- data: dataSig,
611
- error: errorSig,
612
- status: statusSig,
613
- isFetching,
614
- isFetchingNextPage,
615
- isFetchingPreviousPage,
616
- isError,
617
- isSuccess,
618
- hasNextPage,
619
- hasPreviousPage,
620
- fetchNextPage: () => observer.fetchNextPage(),
621
- fetchPreviousPage: () => observer.fetchPreviousPage(),
622
- refetch: () => observer.refetch()
623
- };
301
+ type AnyQueryLike = {
302
+ isPending: Signal<boolean>;
303
+ isError: Signal<boolean>;
304
+ error: Signal<unknown>;
305
+ };
306
+ interface QuerySuspenseProps {
307
+ /**
308
+ * A single query result (or array of them) to gate on.
309
+ * Children only render when ALL queries have succeeded.
310
+ */
311
+ query: AnyQueryLike | AnyQueryLike[];
312
+ /** Rendered while any query is pending. */
313
+ fallback?: VNodeChild;
314
+ /** Rendered when any query has errored. Defaults to re-throwing to nearest ErrorBoundary. */
315
+ error?: (err: unknown) => VNodeChild;
316
+ children: VNodeChild;
624
317
  }
625
-
318
+ /**
319
+ * Pyreon-native Suspense boundary for queries. Shows `fallback` while any query
320
+ * is pending. On error, renders the `error` fallback or re-throws to the
321
+ * nearest Pyreon `ErrorBoundary`.
322
+ *
323
+ * Pair with `useSuspenseQuery` / `useSuspenseInfiniteQuery` to get non-undefined
324
+ * `data` types inside children.
325
+ *
326
+ * @example
327
+ * const userQuery = useSuspenseQuery(() => ({ queryKey: ['user'], queryFn: fetchUser }))
328
+ *
329
+ * h(QuerySuspense, {
330
+ * query: userQuery,
331
+ * fallback: h(Spinner, null),
332
+ * error: (err) => h('p', null, `Failed: ${err}`),
333
+ * }, () => h(UserProfile, { user: userQuery.data() }))
334
+ */
335
+ declare function QuerySuspense(props: QuerySuspenseProps): VNodeChild;
336
+ /**
337
+ * Like `useQuery` but `data` is typed as `Signal<TData>` (never undefined).
338
+ * Designed for use inside a `QuerySuspense` boundary, which guarantees
339
+ * children only render after the query succeeds.
340
+ *
341
+ * @example
342
+ * const user = useSuspenseQuery(() => ({ queryKey: ['user', id()], queryFn: fetchUser }))
343
+ *
344
+ * h(QuerySuspense, { query: user, fallback: h(Spinner, null) },
345
+ * () => h(UserCard, { name: user.data().name }),
346
+ * )
347
+ */
348
+ declare function useSuspenseQuery<TData = unknown, TError = DefaultError, TKey extends QueryKey$1 = QueryKey$1>(options: () => QueryObserverOptions<TData, TError, TData, TData, TKey>): UseSuspenseQueryResult<TData, TError>;
349
+ /**
350
+ * Like `useInfiniteQuery` but `data` is typed as `Signal<InfiniteData<TData>>`
351
+ * (never undefined). Use inside a `QuerySuspense` boundary.
352
+ */
353
+ declare function useSuspenseInfiniteQuery<TQueryFnData = unknown, TError = DefaultError, TQueryKey extends QueryKey$1 = QueryKey$1, TPageParam = unknown>(options: () => InfiniteQueryObserverOptions<TQueryFnData, TError, InfiniteData<TQueryFnData>, TQueryKey, TPageParam>): UseSuspenseInfiniteQueryResult<TQueryFnData, TError>;
626
354
  //#endregion
627
- export { CancelledError, MutationCache, QueryCache, QueryClient, QueryClientContext, QueryClientProvider, QueryErrorResetBoundary, QuerySuspense, defaultShouldDehydrateMutation, defaultShouldDehydrateQuery, dehydrate, hashKey, hydrate, isCancelledError, keepPreviousData, useInfiniteQuery, useIsFetching, useIsMutating, useMutation, useQueries, useQuery, useQueryClient, useQueryErrorResetBoundary, useSubscription, useSuspenseInfiniteQuery, useSuspenseQuery };
628
- //# sourceMappingURL=index.d.ts.map
355
+ export { CancelledError, type DehydratedState, type FetchQueryOptions, type InvalidateOptions, type InvalidateQueryFilters, MutationCache, type MutationFilters, QueryCache, QueryClient, type QueryClientConfig, QueryClientContext, QueryClientProvider, type QueryClientProviderProps, QueryErrorResetBoundary, type QueryErrorResetBoundaryProps, type QueryFilters, type QueryKey, QuerySuspense, type QuerySuspenseProps, type RefetchOptions, type RefetchQueryFilters, type SubscriptionStatus, type UseInfiniteQueryResult, type UseMutationResult, type UseQueriesOptions, type UseQueryResult, type UseSubscriptionOptions, type UseSubscriptionResult, type UseSuspenseInfiniteQueryResult, type UseSuspenseQueryResult, defaultShouldDehydrateMutation, defaultShouldDehydrateQuery, dehydrate, hashKey, hydrate, isCancelledError, keepPreviousData, useInfiniteQuery, useIsFetching, useIsMutating, useMutation, useQueries, useQuery, useQueryClient, useQueryErrorResetBoundary, useSubscription, useSuspenseInfiniteQuery, useSuspenseQuery };
356
+ //# sourceMappingURL=index2.d.ts.map