@pyreon/query 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +236 -0
- package/lib/analysis/index.js.html +5406 -0
- package/lib/index.js +489 -0
- package/lib/index.js.map +1 -0
- package/lib/types/index.d.ts +497 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/index2.d.ts +298 -0
- package/lib/types/index2.d.ts.map +1 -0
- package/package.json +55 -0
- package/src/index.ts +69 -0
- package/src/query-client.ts +59 -0
- package/src/tests/query.test.ts +1768 -0
- package/src/use-infinite-query.ts +138 -0
- package/src/use-is-fetching.ts +44 -0
- package/src/use-mutation.ts +117 -0
- package/src/use-queries.ts +61 -0
- package/src/use-query-error-reset-boundary.ts +95 -0
- package/src/use-query.ts +106 -0
- package/src/use-suspense-query.ts +282 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { onUnmount } from '@pyreon/core'
|
|
2
|
+
import { signal, effect, batch } from '@pyreon/reactivity'
|
|
3
|
+
import type { Signal } from '@pyreon/reactivity'
|
|
4
|
+
import { QueryObserver, InfiniteQueryObserver } from '@tanstack/query-core'
|
|
5
|
+
import type {
|
|
6
|
+
DefaultError,
|
|
7
|
+
InfiniteData,
|
|
8
|
+
InfiniteQueryObserverOptions,
|
|
9
|
+
InfiniteQueryObserverResult,
|
|
10
|
+
QueryKey,
|
|
11
|
+
QueryObserverOptions,
|
|
12
|
+
QueryObserverResult,
|
|
13
|
+
} from '@tanstack/query-core'
|
|
14
|
+
import type { VNodeChild, VNodeChildAtom } from '@pyreon/core'
|
|
15
|
+
import { useQueryClient } from './query-client'
|
|
16
|
+
|
|
17
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Like `UseQueryResult` but `data` is `Signal<TData>` (never undefined).
|
|
21
|
+
* Only use inside a `QuerySuspense` boundary which guarantees the query has
|
|
22
|
+
* succeeded before children are rendered.
|
|
23
|
+
*/
|
|
24
|
+
export interface UseSuspenseQueryResult<TData, TError = DefaultError> {
|
|
25
|
+
result: Signal<QueryObserverResult<TData, TError>>
|
|
26
|
+
/** Always TData — never undefined inside a QuerySuspense boundary. */
|
|
27
|
+
data: Signal<TData>
|
|
28
|
+
error: Signal<TError | null>
|
|
29
|
+
status: Signal<'pending' | 'error' | 'success'>
|
|
30
|
+
isPending: Signal<boolean>
|
|
31
|
+
isFetching: Signal<boolean>
|
|
32
|
+
isError: Signal<boolean>
|
|
33
|
+
isSuccess: Signal<boolean>
|
|
34
|
+
refetch: () => Promise<QueryObserverResult<TData, TError>>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface UseSuspenseInfiniteQueryResult<
|
|
38
|
+
TQueryFnData,
|
|
39
|
+
TError = DefaultError,
|
|
40
|
+
> {
|
|
41
|
+
result: Signal<
|
|
42
|
+
InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>
|
|
43
|
+
>
|
|
44
|
+
/** Always InfiniteData<TQueryFnData> — never undefined inside a QuerySuspense boundary. */
|
|
45
|
+
data: Signal<InfiniteData<TQueryFnData>>
|
|
46
|
+
error: Signal<TError | null>
|
|
47
|
+
status: Signal<'pending' | 'error' | 'success'>
|
|
48
|
+
isFetching: Signal<boolean>
|
|
49
|
+
isFetchingNextPage: Signal<boolean>
|
|
50
|
+
isFetchingPreviousPage: Signal<boolean>
|
|
51
|
+
isError: Signal<boolean>
|
|
52
|
+
isSuccess: Signal<boolean>
|
|
53
|
+
hasNextPage: Signal<boolean>
|
|
54
|
+
hasPreviousPage: Signal<boolean>
|
|
55
|
+
fetchNextPage: () => Promise<
|
|
56
|
+
InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>
|
|
57
|
+
>
|
|
58
|
+
fetchPreviousPage: () => Promise<
|
|
59
|
+
InfiniteQueryObserverResult<InfiniteData<TQueryFnData>, TError>
|
|
60
|
+
>
|
|
61
|
+
refetch: () => Promise<
|
|
62
|
+
QueryObserverResult<InfiniteData<TQueryFnData>, TError>
|
|
63
|
+
>
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── QuerySuspense ──────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
type AnyQueryLike = {
|
|
69
|
+
isPending: Signal<boolean>
|
|
70
|
+
isError: Signal<boolean>
|
|
71
|
+
error: Signal<unknown>
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface QuerySuspenseProps {
|
|
75
|
+
/**
|
|
76
|
+
* A single query result (or array of them) to gate on.
|
|
77
|
+
* Children only render when ALL queries have succeeded.
|
|
78
|
+
*/
|
|
79
|
+
query: AnyQueryLike | AnyQueryLike[]
|
|
80
|
+
/** Rendered while any query is pending. */
|
|
81
|
+
fallback?: VNodeChild
|
|
82
|
+
/** Rendered when any query has errored. Defaults to re-throwing to nearest ErrorBoundary. */
|
|
83
|
+
error?: (err: unknown) => VNodeChild
|
|
84
|
+
children: VNodeChild
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pyreon-native Suspense boundary for queries. Shows `fallback` while any query
|
|
89
|
+
* is pending. On error, renders the `error` fallback or re-throws to the
|
|
90
|
+
* nearest Pyreon `ErrorBoundary`.
|
|
91
|
+
*
|
|
92
|
+
* Pair with `useSuspenseQuery` / `useSuspenseInfiniteQuery` to get non-undefined
|
|
93
|
+
* `data` types inside children.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const userQuery = useSuspenseQuery(() => ({ queryKey: ['user'], queryFn: fetchUser }))
|
|
97
|
+
*
|
|
98
|
+
* h(QuerySuspense, {
|
|
99
|
+
* query: userQuery,
|
|
100
|
+
* fallback: h(Spinner, null),
|
|
101
|
+
* error: (err) => h('p', null, `Failed: ${err}`),
|
|
102
|
+
* }, () => h(UserProfile, { user: userQuery.data() }))
|
|
103
|
+
*/
|
|
104
|
+
export function QuerySuspense(props: QuerySuspenseProps): VNodeChild {
|
|
105
|
+
return (): VNodeChildAtom => {
|
|
106
|
+
const queries = Array.isArray(props.query) ? props.query : [props.query]
|
|
107
|
+
|
|
108
|
+
// Error state — use provided error fallback or re-throw to ErrorBoundary
|
|
109
|
+
for (const q of queries) {
|
|
110
|
+
if (q.isError()) {
|
|
111
|
+
const err = q.error()
|
|
112
|
+
if (props.error) {
|
|
113
|
+
return props.error(err) as VNodeChildAtom
|
|
114
|
+
}
|
|
115
|
+
throw err
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Pending state — show fallback
|
|
120
|
+
if (queries.some((q) => q.isPending())) {
|
|
121
|
+
const fb = props.fallback
|
|
122
|
+
return (
|
|
123
|
+
typeof fb === 'function' ? (fb as () => VNodeChildAtom)() : (fb ?? null)
|
|
124
|
+
) as VNodeChildAtom
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// All success — render children
|
|
128
|
+
const ch = props.children
|
|
129
|
+
return (
|
|
130
|
+
typeof ch === 'function' ? (ch as () => VNodeChildAtom)() : ch
|
|
131
|
+
) as VNodeChildAtom
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── useSuspenseQuery ───────────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Like `useQuery` but `data` is typed as `Signal<TData>` (never undefined).
|
|
139
|
+
* Designed for use inside a `QuerySuspense` boundary, which guarantees
|
|
140
|
+
* children only render after the query succeeds.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* const user = useSuspenseQuery(() => ({ queryKey: ['user', id()], queryFn: fetchUser }))
|
|
144
|
+
*
|
|
145
|
+
* h(QuerySuspense, { query: user, fallback: h(Spinner, null) },
|
|
146
|
+
* () => h(UserCard, { name: user.data().name }),
|
|
147
|
+
* )
|
|
148
|
+
*/
|
|
149
|
+
export function useSuspenseQuery<
|
|
150
|
+
TData = unknown,
|
|
151
|
+
TError = DefaultError,
|
|
152
|
+
TKey extends QueryKey = QueryKey,
|
|
153
|
+
>(
|
|
154
|
+
options: () => QueryObserverOptions<TData, TError, TData, TData, TKey>,
|
|
155
|
+
): UseSuspenseQueryResult<TData, TError> {
|
|
156
|
+
const client = useQueryClient()
|
|
157
|
+
const observer = new QueryObserver<TData, TError, TData, TData, TKey>(
|
|
158
|
+
client,
|
|
159
|
+
options(),
|
|
160
|
+
)
|
|
161
|
+
const initial = observer.getCurrentResult()
|
|
162
|
+
|
|
163
|
+
const resultSig = signal<QueryObserverResult<TData, TError>>(initial)
|
|
164
|
+
const dataSig = signal<TData>(initial.data as TData)
|
|
165
|
+
const errorSig = signal<TError | null>(initial.error ?? null)
|
|
166
|
+
const statusSig = signal<'pending' | 'error' | 'success'>(initial.status)
|
|
167
|
+
const isPending = signal(initial.isPending)
|
|
168
|
+
const isFetching = signal(initial.isFetching)
|
|
169
|
+
const isError = signal(initial.isError)
|
|
170
|
+
const isSuccess = signal(initial.isSuccess)
|
|
171
|
+
|
|
172
|
+
const unsub = observer.subscribe((r) => {
|
|
173
|
+
batch(() => {
|
|
174
|
+
resultSig.set(r)
|
|
175
|
+
if (r.data !== undefined) dataSig.set(r.data as TData)
|
|
176
|
+
errorSig.set(r.error ?? null)
|
|
177
|
+
statusSig.set(r.status)
|
|
178
|
+
isPending.set(r.isPending)
|
|
179
|
+
isFetching.set(r.isFetching)
|
|
180
|
+
isError.set(r.isError)
|
|
181
|
+
isSuccess.set(r.isSuccess)
|
|
182
|
+
})
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
effect(() => {
|
|
186
|
+
observer.setOptions(options())
|
|
187
|
+
})
|
|
188
|
+
onUnmount(() => unsub())
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
result: resultSig,
|
|
192
|
+
data: dataSig,
|
|
193
|
+
error: errorSig,
|
|
194
|
+
status: statusSig,
|
|
195
|
+
isPending,
|
|
196
|
+
isFetching,
|
|
197
|
+
isError,
|
|
198
|
+
isSuccess,
|
|
199
|
+
refetch: () => observer.refetch(),
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ─── useSuspenseInfiniteQuery ───────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Like `useInfiniteQuery` but `data` is typed as `Signal<InfiniteData<TData>>`
|
|
207
|
+
* (never undefined). Use inside a `QuerySuspense` boundary.
|
|
208
|
+
*/
|
|
209
|
+
export function useSuspenseInfiniteQuery<
|
|
210
|
+
TQueryFnData = unknown,
|
|
211
|
+
TError = DefaultError,
|
|
212
|
+
TQueryKey extends QueryKey = QueryKey,
|
|
213
|
+
TPageParam = unknown,
|
|
214
|
+
>(
|
|
215
|
+
options: () => InfiniteQueryObserverOptions<
|
|
216
|
+
TQueryFnData,
|
|
217
|
+
TError,
|
|
218
|
+
InfiniteData<TQueryFnData>,
|
|
219
|
+
TQueryKey,
|
|
220
|
+
TPageParam
|
|
221
|
+
>,
|
|
222
|
+
): UseSuspenseInfiniteQueryResult<TQueryFnData, TError> {
|
|
223
|
+
const client = useQueryClient()
|
|
224
|
+
const observer = new InfiniteQueryObserver<
|
|
225
|
+
TQueryFnData,
|
|
226
|
+
TError,
|
|
227
|
+
InfiniteData<TQueryFnData>,
|
|
228
|
+
TQueryKey,
|
|
229
|
+
TPageParam
|
|
230
|
+
>(client, options())
|
|
231
|
+
const initial = observer.getCurrentResult()
|
|
232
|
+
|
|
233
|
+
const resultSig = signal(initial)
|
|
234
|
+
const dataSig = signal(initial.data as InfiniteData<TQueryFnData>)
|
|
235
|
+
const errorSig = signal<TError | null>(initial.error ?? null)
|
|
236
|
+
const statusSig = signal(initial.status)
|
|
237
|
+
const isFetching = signal(initial.isFetching)
|
|
238
|
+
const isFetchingNextPage = signal(initial.isFetchingNextPage)
|
|
239
|
+
const isFetchingPreviousPage = signal(initial.isFetchingPreviousPage)
|
|
240
|
+
const isError = signal(initial.isError)
|
|
241
|
+
const isSuccess = signal(initial.isSuccess)
|
|
242
|
+
const hasNextPage = signal(initial.hasNextPage)
|
|
243
|
+
const hasPreviousPage = signal(initial.hasPreviousPage)
|
|
244
|
+
|
|
245
|
+
const unsub = observer.subscribe((r) => {
|
|
246
|
+
batch(() => {
|
|
247
|
+
resultSig.set(r)
|
|
248
|
+
if (r.data !== undefined) dataSig.set(r.data)
|
|
249
|
+
errorSig.set(r.error ?? null)
|
|
250
|
+
statusSig.set(r.status)
|
|
251
|
+
isFetching.set(r.isFetching)
|
|
252
|
+
isFetchingNextPage.set(r.isFetchingNextPage)
|
|
253
|
+
isFetchingPreviousPage.set(r.isFetchingPreviousPage)
|
|
254
|
+
isError.set(r.isError)
|
|
255
|
+
isSuccess.set(r.isSuccess)
|
|
256
|
+
hasNextPage.set(r.hasNextPage)
|
|
257
|
+
hasPreviousPage.set(r.hasPreviousPage)
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
effect(() => {
|
|
262
|
+
observer.setOptions(options())
|
|
263
|
+
})
|
|
264
|
+
onUnmount(() => unsub())
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
result: resultSig,
|
|
268
|
+
data: dataSig,
|
|
269
|
+
error: errorSig,
|
|
270
|
+
status: statusSig,
|
|
271
|
+
isFetching,
|
|
272
|
+
isFetchingNextPage,
|
|
273
|
+
isFetchingPreviousPage,
|
|
274
|
+
isError,
|
|
275
|
+
isSuccess,
|
|
276
|
+
hasNextPage,
|
|
277
|
+
hasPreviousPage,
|
|
278
|
+
fetchNextPage: () => observer.fetchNextPage(),
|
|
279
|
+
fetchPreviousPage: () => observer.fetchPreviousPage(),
|
|
280
|
+
refetch: () => observer.refetch(),
|
|
281
|
+
}
|
|
282
|
+
}
|