@kontsedal/olas-core 0.0.1-rc.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.
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/dist/index.cjs +363 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +178 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +178 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +339 -0
- package/dist/index.mjs.map +1 -0
- package/dist/root-BImHnGj1.mjs +3270 -0
- package/dist/root-BImHnGj1.mjs.map +1 -0
- package/dist/root-Bazp5_Ik.cjs +3347 -0
- package/dist/root-Bazp5_Ik.cjs.map +1 -0
- package/dist/testing.cjs +81 -0
- package/dist/testing.cjs.map +1 -0
- package/dist/testing.d.cts +56 -0
- package/dist/testing.d.cts.map +1 -0
- package/dist/testing.d.mts +56 -0
- package/dist/testing.d.mts.map +1 -0
- package/dist/testing.mjs +78 -0
- package/dist/testing.mjs.map +1 -0
- package/dist/types-CAMgqCMz.d.mts +816 -0
- package/dist/types-CAMgqCMz.d.mts.map +1 -0
- package/dist/types-emq_lZd7.d.cts +816 -0
- package/dist/types-emq_lZd7.d.cts.map +1 -0
- package/package.json +47 -0
- package/src/__dev__.d.ts +8 -0
- package/src/controller/define.ts +50 -0
- package/src/controller/index.ts +12 -0
- package/src/controller/instance.ts +499 -0
- package/src/controller/root.ts +160 -0
- package/src/controller/types.ts +195 -0
- package/src/devtools.ts +0 -0
- package/src/emitter.ts +79 -0
- package/src/errors.ts +49 -0
- package/src/forms/field.ts +303 -0
- package/src/forms/form-types.ts +130 -0
- package/src/forms/form.ts +640 -0
- package/src/forms/index.ts +2 -0
- package/src/forms/types.ts +1 -0
- package/src/forms/validators.ts +70 -0
- package/src/index.ts +89 -0
- package/src/query/client.ts +934 -0
- package/src/query/define.ts +154 -0
- package/src/query/entry.ts +322 -0
- package/src/query/focus-online.ts +73 -0
- package/src/query/index.ts +3 -0
- package/src/query/infinite.ts +462 -0
- package/src/query/keys.ts +33 -0
- package/src/query/local.ts +113 -0
- package/src/query/mutation.ts +384 -0
- package/src/query/plugin.ts +135 -0
- package/src/query/types.ts +168 -0
- package/src/query/use.ts +321 -0
- package/src/scope.ts +42 -0
- package/src/selection.ts +146 -0
- package/src/signals/index.ts +3 -0
- package/src/signals/readonly.ts +22 -0
- package/src/signals/runtime.ts +115 -0
- package/src/signals/types.ts +31 -0
- package/src/testing.ts +142 -0
- package/src/timing/debounced.ts +32 -0
- package/src/timing/index.ts +2 -0
- package/src/timing/throttled.ts +46 -0
- package/src/utils.ts +13 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
import type { DevtoolsEmitter } from '../devtools'
|
|
2
|
+
import { dispatchError, type ErrorHandler } from '../errors'
|
|
3
|
+
import { type Signal, signal } from '../signals'
|
|
4
|
+
import { Entry } from './entry'
|
|
5
|
+
import { subscribeReconnect, subscribeWindowFocus } from './focus-online'
|
|
6
|
+
import { InfiniteEntry, type InfiniteQuery, type InfiniteQuerySpec } from './infinite'
|
|
7
|
+
import { stableHash } from './keys'
|
|
8
|
+
import {
|
|
9
|
+
type GcEvent,
|
|
10
|
+
type InvalidateEvent,
|
|
11
|
+
lookupRegisteredQuery,
|
|
12
|
+
type QueryClientPlugin,
|
|
13
|
+
type QueryClientPluginApi,
|
|
14
|
+
type SetDataEvent,
|
|
15
|
+
} from './plugin'
|
|
16
|
+
import type { DehydratedState, Query, QuerySpec, RetryDelay, RetryPolicy, Snapshot } from './types'
|
|
17
|
+
|
|
18
|
+
const DEFAULT_GC_TIME = 5 * 60_000
|
|
19
|
+
|
|
20
|
+
type AnyQuery = Query<any, any> & {
|
|
21
|
+
readonly __spec: QuerySpec<any, any>
|
|
22
|
+
__clients: Set<QueryClient>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type AnyInfiniteQuery = InfiniteQuery<any, any, any> & {
|
|
26
|
+
readonly __spec: InfiniteQuerySpec<any, any, any, any>
|
|
27
|
+
__clients: Set<QueryClient>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class ClientEntry<T> {
|
|
31
|
+
readonly entry: Entry<T>
|
|
32
|
+
/** The result of `spec.key(...args)` — used for hashing/identity. */
|
|
33
|
+
readonly keyArgs: readonly unknown[]
|
|
34
|
+
/** The original args the consumer passed — what the fetcher receives. */
|
|
35
|
+
readonly callArgs: readonly unknown[]
|
|
36
|
+
readonly client: QueryClient
|
|
37
|
+
readonly query: AnyQuery
|
|
38
|
+
private subscriberCount = 0
|
|
39
|
+
private gcTimer: ReturnType<typeof setTimeout> | null = null
|
|
40
|
+
private intervalTimer: ReturnType<typeof setInterval> | null = null
|
|
41
|
+
private unsubFocus: (() => void) | null = null
|
|
42
|
+
private unsubOnline: (() => void) | null = null
|
|
43
|
+
private gcTime: number
|
|
44
|
+
private refetchInterval: number | undefined
|
|
45
|
+
private refetchOnWindowFocus: boolean
|
|
46
|
+
private refetchOnReconnect: boolean
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
client: QueryClient,
|
|
50
|
+
query: AnyQuery,
|
|
51
|
+
callArgs: readonly unknown[],
|
|
52
|
+
keyArgs: readonly unknown[],
|
|
53
|
+
spec: QuerySpec<any, T>,
|
|
54
|
+
hydrated?: { data: T; lastUpdatedAt: number },
|
|
55
|
+
) {
|
|
56
|
+
this.client = client
|
|
57
|
+
this.query = query
|
|
58
|
+
this.callArgs = callArgs
|
|
59
|
+
this.keyArgs = keyArgs
|
|
60
|
+
this.gcTime = spec.gcTime ?? DEFAULT_GC_TIME
|
|
61
|
+
this.refetchInterval = spec.refetchInterval
|
|
62
|
+
this.refetchOnWindowFocus = spec.refetchOnWindowFocus ?? client.refetchOnWindowFocus
|
|
63
|
+
this.refetchOnReconnect = spec.refetchOnReconnect ?? client.refetchOnReconnect
|
|
64
|
+
const fetcherFn = spec.fetcher
|
|
65
|
+
const deps = client.deps as import('../controller/types').AmbientDeps
|
|
66
|
+
const devtools = client.devtools
|
|
67
|
+
const queryKey = this.keyArgs
|
|
68
|
+
this.entry = new Entry<T>({
|
|
69
|
+
fetcher: () => (signal) => fetcherFn({ signal, deps }, ...(callArgs as never[])),
|
|
70
|
+
staleTime: spec.staleTime,
|
|
71
|
+
retry: spec.retry as RetryPolicy | undefined,
|
|
72
|
+
retryDelay: spec.retryDelay as RetryDelay | undefined,
|
|
73
|
+
initialData: hydrated?.data,
|
|
74
|
+
initialUpdatedAt: hydrated?.lastUpdatedAt,
|
|
75
|
+
events:
|
|
76
|
+
__DEV__ && devtools !== undefined
|
|
77
|
+
? {
|
|
78
|
+
onFetchStart: () => devtools.emit({ type: 'cache:fetch-start', queryKey }),
|
|
79
|
+
onFetchSuccess: (durationMs) =>
|
|
80
|
+
devtools.emit({ type: 'cache:fetch-success', queryKey, durationMs }),
|
|
81
|
+
onFetchError: (durationMs, error) =>
|
|
82
|
+
devtools.emit({
|
|
83
|
+
type: 'cache:fetch-error',
|
|
84
|
+
queryKey,
|
|
85
|
+
durationMs,
|
|
86
|
+
error,
|
|
87
|
+
}),
|
|
88
|
+
}
|
|
89
|
+
: undefined,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
acquire(): void {
|
|
94
|
+
this.subscriberCount += 1
|
|
95
|
+
if (this.gcTimer != null) {
|
|
96
|
+
clearTimeout(this.gcTimer)
|
|
97
|
+
this.gcTimer = null
|
|
98
|
+
}
|
|
99
|
+
if (this.subscriberCount === 1) {
|
|
100
|
+
if (this.refetchInterval != null) this.startIntervalTimer()
|
|
101
|
+
if (this.refetchOnWindowFocus) {
|
|
102
|
+
this.unsubFocus = subscribeWindowFocus(() => this.triggerEventRefetch())
|
|
103
|
+
}
|
|
104
|
+
if (this.refetchOnReconnect) {
|
|
105
|
+
this.unsubOnline = subscribeReconnect(() => this.triggerEventRefetch())
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
release(): void {
|
|
111
|
+
this.subscriberCount -= 1
|
|
112
|
+
if (this.subscriberCount <= 0) {
|
|
113
|
+
this.stopIntervalTimer()
|
|
114
|
+
this.stopEventSubscriptions()
|
|
115
|
+
if (this.gcTime === 0) {
|
|
116
|
+
this.client.dropEntry(this)
|
|
117
|
+
} else {
|
|
118
|
+
this.gcTimer = setTimeout(() => {
|
|
119
|
+
this.gcTimer = null
|
|
120
|
+
this.client.dropEntry(this)
|
|
121
|
+
}, this.gcTime)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
hasSubscribers(): boolean {
|
|
127
|
+
return this.subscriberCount > 0
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
startIntervalTimer(): void {
|
|
131
|
+
if (this.refetchInterval == null) return
|
|
132
|
+
if (this.intervalTimer != null) return
|
|
133
|
+
this.intervalTimer = setInterval(() => {
|
|
134
|
+
this.entry.startFetch().catch(() => {
|
|
135
|
+
/* error already captured on entry */
|
|
136
|
+
})
|
|
137
|
+
}, this.refetchInterval)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
stopIntervalTimer(): void {
|
|
141
|
+
if (this.intervalTimer != null) {
|
|
142
|
+
clearInterval(this.intervalTimer)
|
|
143
|
+
this.intervalTimer = null
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
stopEventSubscriptions(): void {
|
|
148
|
+
if (this.unsubFocus != null) {
|
|
149
|
+
this.unsubFocus()
|
|
150
|
+
this.unsubFocus = null
|
|
151
|
+
}
|
|
152
|
+
if (this.unsubOnline != null) {
|
|
153
|
+
this.unsubOnline()
|
|
154
|
+
this.unsubOnline = null
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Schedule a gc timer for an entry that was just created via a non-subscribing
|
|
160
|
+
* path (`prefetch`, `setData`, `invalidate`). Without this, those entries
|
|
161
|
+
* never trigger `release()` and would live until root dispose. Called by
|
|
162
|
+
* `QueryClient.bindEntry` right after creating a fresh entry; `acquire()`
|
|
163
|
+
* (e.g., from a subscriber that arrives shortly after a prefetch) clears it.
|
|
164
|
+
* No-op if the entry already has subscribers or a gc timer pending.
|
|
165
|
+
*/
|
|
166
|
+
scheduleGcIfOrphan(): void {
|
|
167
|
+
if (this.subscriberCount > 0 || this.gcTimer != null) return
|
|
168
|
+
if (this.gcTime === 0) {
|
|
169
|
+
// Defer one microtask so the current caller (e.g. a `setData` that
|
|
170
|
+
// writes then expects to read back in the same tick) sees the entry.
|
|
171
|
+
queueMicrotask(() => {
|
|
172
|
+
if (this.subscriberCount === 0 && this.gcTimer == null) {
|
|
173
|
+
this.client.dropEntry(this)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
this.gcTimer = setTimeout(() => {
|
|
179
|
+
this.gcTimer = null
|
|
180
|
+
this.client.dropEntry(this)
|
|
181
|
+
}, this.gcTime)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Refetch on focus / reconnect, but only if the data is actually stale. */
|
|
185
|
+
private triggerEventRefetch(): void {
|
|
186
|
+
if (!this.entry.isStaleNow()) return
|
|
187
|
+
this.entry.startFetch().catch(() => {
|
|
188
|
+
/* error already captured on entry */
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
dispose(): void {
|
|
193
|
+
if (this.gcTimer != null) {
|
|
194
|
+
clearTimeout(this.gcTimer)
|
|
195
|
+
this.gcTimer = null
|
|
196
|
+
}
|
|
197
|
+
this.stopIntervalTimer()
|
|
198
|
+
this.stopEventSubscriptions()
|
|
199
|
+
this.entry.dispose()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export class InfiniteClientEntry<TPage, TItem, PageParam> {
|
|
204
|
+
readonly entry: InfiniteEntry<TPage, TItem, PageParam>
|
|
205
|
+
readonly keyArgs: readonly unknown[]
|
|
206
|
+
readonly callArgs: readonly unknown[]
|
|
207
|
+
readonly client: QueryClient
|
|
208
|
+
readonly query: AnyInfiniteQuery
|
|
209
|
+
private subscriberCount = 0
|
|
210
|
+
private gcTimer: ReturnType<typeof setTimeout> | null = null
|
|
211
|
+
private intervalTimer: ReturnType<typeof setInterval> | null = null
|
|
212
|
+
private gcTime: number
|
|
213
|
+
private refetchInterval: number | undefined
|
|
214
|
+
|
|
215
|
+
constructor(
|
|
216
|
+
client: QueryClient,
|
|
217
|
+
query: AnyInfiniteQuery,
|
|
218
|
+
callArgs: readonly unknown[],
|
|
219
|
+
keyArgs: readonly unknown[],
|
|
220
|
+
spec: InfiniteQuerySpec<any, PageParam, TPage, TItem>,
|
|
221
|
+
) {
|
|
222
|
+
this.client = client
|
|
223
|
+
this.query = query
|
|
224
|
+
this.callArgs = callArgs
|
|
225
|
+
this.keyArgs = keyArgs
|
|
226
|
+
this.gcTime = spec.gcTime ?? DEFAULT_GC_TIME
|
|
227
|
+
this.refetchInterval = spec.refetchInterval
|
|
228
|
+
const fetcherFn = spec.fetcher
|
|
229
|
+
const deps = client.deps as import('../controller/types').AmbientDeps
|
|
230
|
+
this.entry = new InfiniteEntry<TPage, TItem, PageParam>({
|
|
231
|
+
fetcher: ({ pageParam, signal }) =>
|
|
232
|
+
fetcherFn({ pageParam, signal, deps }, ...(callArgs as never[])),
|
|
233
|
+
initialPageParam: spec.initialPageParam,
|
|
234
|
+
getNextPageParam: spec.getNextPageParam,
|
|
235
|
+
getPreviousPageParam: spec.getPreviousPageParam,
|
|
236
|
+
itemsOf: spec.itemsOf,
|
|
237
|
+
staleTime: spec.staleTime,
|
|
238
|
+
retry: spec.retry as RetryPolicy | undefined,
|
|
239
|
+
retryDelay: spec.retryDelay as RetryDelay | undefined,
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
acquire(): void {
|
|
244
|
+
this.subscriberCount += 1
|
|
245
|
+
if (this.gcTimer != null) {
|
|
246
|
+
clearTimeout(this.gcTimer)
|
|
247
|
+
this.gcTimer = null
|
|
248
|
+
}
|
|
249
|
+
if (this.subscriberCount === 1 && this.refetchInterval != null) {
|
|
250
|
+
this.startIntervalTimer()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
release(): void {
|
|
255
|
+
this.subscriberCount -= 1
|
|
256
|
+
if (this.subscriberCount <= 0) {
|
|
257
|
+
this.stopIntervalTimer()
|
|
258
|
+
if (this.gcTime === 0) {
|
|
259
|
+
this.client.dropInfiniteEntry(
|
|
260
|
+
this as unknown as InfiniteClientEntry<unknown, unknown, unknown>,
|
|
261
|
+
)
|
|
262
|
+
} else {
|
|
263
|
+
this.gcTimer = setTimeout(() => {
|
|
264
|
+
this.gcTimer = null
|
|
265
|
+
this.client.dropInfiniteEntry(
|
|
266
|
+
this as unknown as InfiniteClientEntry<unknown, unknown, unknown>,
|
|
267
|
+
)
|
|
268
|
+
}, this.gcTime)
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private startIntervalTimer(): void {
|
|
274
|
+
if (this.refetchInterval == null || this.intervalTimer != null) return
|
|
275
|
+
this.intervalTimer = setInterval(() => {
|
|
276
|
+
this.entry.startFetch().catch(() => {
|
|
277
|
+
/* error captured on entry */
|
|
278
|
+
})
|
|
279
|
+
}, this.refetchInterval)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private stopIntervalTimer(): void {
|
|
283
|
+
if (this.intervalTimer != null) {
|
|
284
|
+
clearInterval(this.intervalTimer)
|
|
285
|
+
this.intervalTimer = null
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** See `ClientEntry.scheduleGcIfOrphan`. */
|
|
290
|
+
scheduleGcIfOrphan(): void {
|
|
291
|
+
if (this.subscriberCount > 0 || this.gcTimer != null) return
|
|
292
|
+
if (this.gcTime === 0) {
|
|
293
|
+
queueMicrotask(() => {
|
|
294
|
+
if (this.subscriberCount === 0 && this.gcTimer == null) {
|
|
295
|
+
this.client.dropInfiniteEntry(
|
|
296
|
+
this as unknown as InfiniteClientEntry<unknown, unknown, unknown>,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
})
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
this.gcTimer = setTimeout(() => {
|
|
303
|
+
this.gcTimer = null
|
|
304
|
+
this.client.dropInfiniteEntry(
|
|
305
|
+
this as unknown as InfiniteClientEntry<unknown, unknown, unknown>,
|
|
306
|
+
)
|
|
307
|
+
}, this.gcTime)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
dispose(): void {
|
|
311
|
+
if (this.gcTimer != null) {
|
|
312
|
+
clearTimeout(this.gcTimer)
|
|
313
|
+
this.gcTimer = null
|
|
314
|
+
}
|
|
315
|
+
this.stopIntervalTimer()
|
|
316
|
+
this.entry.dispose()
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Per-root entry registry. Owns the keyed `Map<hash, ClientEntry>` per query,
|
|
322
|
+
* GC timers, refetch-interval timers. Subscribers are routed in/out via
|
|
323
|
+
* `acquire` / `release`.
|
|
324
|
+
*/
|
|
325
|
+
export class QueryClient {
|
|
326
|
+
private readonly maps = new Map<AnyQuery, Map<string, ClientEntry<unknown>>>()
|
|
327
|
+
private readonly infiniteMaps = new Map<
|
|
328
|
+
AnyInfiniteQuery,
|
|
329
|
+
Map<string, InfiniteClientEntry<unknown, unknown, unknown>>
|
|
330
|
+
>()
|
|
331
|
+
private readonly touchedQueries = new Set<AnyQuery>()
|
|
332
|
+
private readonly touchedInfiniteQueries = new Set<AnyInfiniteQuery>()
|
|
333
|
+
private readonly hydratedData = new Map<string, { data: unknown; lastUpdatedAt: number }>()
|
|
334
|
+
/** Mutations inflight across the whole root — used by `waitForIdle`. */
|
|
335
|
+
readonly mutationsInflight$: Signal<number> = signal(0)
|
|
336
|
+
private onError: ErrorHandler | undefined
|
|
337
|
+
private disposed = false
|
|
338
|
+
/** Devtools bus, if any — passed by `createRoot`. Used to emit cache events. */
|
|
339
|
+
readonly devtools: DevtoolsEmitter | undefined
|
|
340
|
+
|
|
341
|
+
/** Root-level deps; passed to every `QuerySpec.fetcher` via `FetchCtx`. */
|
|
342
|
+
readonly deps: Record<string, unknown>
|
|
343
|
+
|
|
344
|
+
/** Root-wide defaults for refetch triggers; per-query spec overrides win. Spec §5.9. */
|
|
345
|
+
readonly refetchOnWindowFocus: boolean
|
|
346
|
+
readonly refetchOnReconnect: boolean
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Installed plugins. Fired on every `setData` / `invalidate` / `gc` so
|
|
350
|
+
* cross-tab / persistence-like layers can observe and react. SPEC §13.2.
|
|
351
|
+
*/
|
|
352
|
+
private readonly plugins: QueryClientPlugin[]
|
|
353
|
+
/**
|
|
354
|
+
* Flipped to `true` while a remote-originated write (via
|
|
355
|
+
* `applyRemoteSetData` / `applyRemoteInvalidate`) is being applied. The
|
|
356
|
+
* resulting plugin events carry `isRemote: true` so plugins know to skip
|
|
357
|
+
* rebroadcast.
|
|
358
|
+
*/
|
|
359
|
+
private applyingRemote = false
|
|
360
|
+
|
|
361
|
+
constructor(opts?: {
|
|
362
|
+
onError?: ErrorHandler
|
|
363
|
+
hydrate?: DehydratedState
|
|
364
|
+
devtools?: DevtoolsEmitter
|
|
365
|
+
deps?: Record<string, unknown>
|
|
366
|
+
refetchOnWindowFocus?: boolean
|
|
367
|
+
refetchOnReconnect?: boolean
|
|
368
|
+
plugins?: QueryClientPlugin[]
|
|
369
|
+
}) {
|
|
370
|
+
this.onError = opts?.onError
|
|
371
|
+
this.devtools = opts?.devtools
|
|
372
|
+
this.deps = opts?.deps ?? {}
|
|
373
|
+
this.refetchOnWindowFocus = opts?.refetchOnWindowFocus ?? false
|
|
374
|
+
this.refetchOnReconnect = opts?.refetchOnReconnect ?? false
|
|
375
|
+
this.plugins = opts?.plugins ?? []
|
|
376
|
+
if (opts?.hydrate) this.hydrate(opts.hydrate)
|
|
377
|
+
const api = this.makePluginApi()
|
|
378
|
+
for (const plugin of this.plugins) {
|
|
379
|
+
this.callPlugin(() => plugin.init?.(api))
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Build the `QueryClientPluginApi` view that plugins receive at `init`
|
|
385
|
+
* time. Closes over `this`; safe to hand out — plugins call back through
|
|
386
|
+
* these methods to push remote-originated writes into the local cache.
|
|
387
|
+
*/
|
|
388
|
+
private makePluginApi(): QueryClientPluginApi {
|
|
389
|
+
const self = this
|
|
390
|
+
return {
|
|
391
|
+
applyRemoteSetData(queryId, keyArgs, data) {
|
|
392
|
+
self.applyRemoteSetData(queryId, keyArgs, data)
|
|
393
|
+
},
|
|
394
|
+
applyRemoteInvalidate(queryId, keyArgs) {
|
|
395
|
+
self.applyRemoteInvalidate(queryId, keyArgs)
|
|
396
|
+
},
|
|
397
|
+
subscribedKeys(queryId) {
|
|
398
|
+
return self.subscribedKeysFor(queryId)
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Invoke a plugin callback; route exceptions through `onError`. */
|
|
404
|
+
private callPlugin(fn: () => void): void {
|
|
405
|
+
try {
|
|
406
|
+
fn()
|
|
407
|
+
} catch (err) {
|
|
408
|
+
dispatchError(this.onError, err, {
|
|
409
|
+
kind: 'plugin',
|
|
410
|
+
controllerPath: [],
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
private emitSetData(
|
|
416
|
+
query: AnyQuery | AnyInfiniteQuery,
|
|
417
|
+
keyArgs: readonly unknown[],
|
|
418
|
+
data: unknown,
|
|
419
|
+
kind: 'data' | 'infinite',
|
|
420
|
+
): void {
|
|
421
|
+
if (this.plugins.length === 0) return
|
|
422
|
+
const queryId = query.__spec.queryId
|
|
423
|
+
if (queryId == null) return
|
|
424
|
+
const event: SetDataEvent = {
|
|
425
|
+
queryId,
|
|
426
|
+
keyArgs,
|
|
427
|
+
data,
|
|
428
|
+
kind,
|
|
429
|
+
isRemote: this.applyingRemote,
|
|
430
|
+
}
|
|
431
|
+
for (const plugin of this.plugins) {
|
|
432
|
+
if (plugin.onSetData) {
|
|
433
|
+
const cb = plugin.onSetData
|
|
434
|
+
this.callPlugin(() => cb.call(plugin, event))
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
private emitInvalidate(
|
|
440
|
+
query: AnyQuery | AnyInfiniteQuery,
|
|
441
|
+
keyArgs: readonly unknown[],
|
|
442
|
+
kind: 'data' | 'infinite',
|
|
443
|
+
): void {
|
|
444
|
+
if (this.plugins.length === 0) return
|
|
445
|
+
const queryId = query.__spec.queryId
|
|
446
|
+
if (queryId == null) return
|
|
447
|
+
const event: InvalidateEvent = {
|
|
448
|
+
queryId,
|
|
449
|
+
keyArgs,
|
|
450
|
+
kind,
|
|
451
|
+
isRemote: this.applyingRemote,
|
|
452
|
+
}
|
|
453
|
+
for (const plugin of this.plugins) {
|
|
454
|
+
if (plugin.onInvalidate) {
|
|
455
|
+
const cb = plugin.onInvalidate
|
|
456
|
+
this.callPlugin(() => cb.call(plugin, event))
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private emitGc(
|
|
462
|
+
query: AnyQuery | AnyInfiniteQuery,
|
|
463
|
+
keyArgs: readonly unknown[],
|
|
464
|
+
kind: 'data' | 'infinite',
|
|
465
|
+
): void {
|
|
466
|
+
if (this.plugins.length === 0) return
|
|
467
|
+
const queryId = query.__spec.queryId
|
|
468
|
+
if (queryId == null) return
|
|
469
|
+
const event: GcEvent = { queryId, keyArgs, kind }
|
|
470
|
+
for (const plugin of this.plugins) {
|
|
471
|
+
if (plugin.onGc) {
|
|
472
|
+
const cb = plugin.onGc
|
|
473
|
+
this.callPlugin(() => cb.call(plugin, event))
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Resolve `queryId → live entry-map keys`. Empty array when unknown. */
|
|
479
|
+
private subscribedKeysFor(queryId: string): readonly (readonly unknown[])[] {
|
|
480
|
+
// Defer the registry lookup to avoid an eager circular import — `define.ts`
|
|
481
|
+
// imports `QueryClient` as a type, and we import the registry helper here
|
|
482
|
+
// for runtime use only.
|
|
483
|
+
const query = lookupRegisteredQuery(queryId)
|
|
484
|
+
if (!query) return []
|
|
485
|
+
const out: (readonly unknown[])[] = []
|
|
486
|
+
if (query.__olas === 'query') {
|
|
487
|
+
const map = this.maps.get(query as unknown as AnyQuery)
|
|
488
|
+
if (map) for (const ce of map.values()) out.push(ce.keyArgs)
|
|
489
|
+
} else {
|
|
490
|
+
const map = this.infiniteMaps.get(query as unknown as AnyInfiniteQuery)
|
|
491
|
+
if (map) for (const ce of map.values()) out.push(ce.keyArgs)
|
|
492
|
+
}
|
|
493
|
+
return out
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Apply a remote-originated `setData` for the query identified by
|
|
498
|
+
* `queryId`, scoped to the entry already keyed by `keyArgs` in this
|
|
499
|
+
* client. Goes through the underlying `Entry.setData` so subscribers see
|
|
500
|
+
* the write; plugin `onSetData` fires with `isRemote: true`.
|
|
501
|
+
*
|
|
502
|
+
* Drops silently when:
|
|
503
|
+
* - No query with that id is registered (the receiving tab hasn't
|
|
504
|
+
* imported the module that defined it).
|
|
505
|
+
* - The registered query is an infinite query (cross-tab infinite sync
|
|
506
|
+
* is deferred — see `plugin.ts` `SetDataEvent.kind`).
|
|
507
|
+
* - No local entry exists for that key (the receiving tab isn't
|
|
508
|
+
* subscribed; nothing useful to write to without callArgs for a
|
|
509
|
+
* future refetch).
|
|
510
|
+
*/
|
|
511
|
+
applyRemoteSetData(queryId: string, keyArgs: readonly unknown[], data: unknown): void {
|
|
512
|
+
const query = lookupRegisteredQuery(queryId)
|
|
513
|
+
if (!query) return
|
|
514
|
+
if (query.__olas !== 'query') return // infinite — deferred for v1
|
|
515
|
+
const internal = query as unknown as AnyQuery
|
|
516
|
+
const map = this.maps.get(internal)
|
|
517
|
+
if (!map) return
|
|
518
|
+
const hash = stableHash(keyArgs)
|
|
519
|
+
const entry = map.get(hash)
|
|
520
|
+
if (!entry) return
|
|
521
|
+
this.applyingRemote = true
|
|
522
|
+
try {
|
|
523
|
+
entry.entry.setData(() => data as never)
|
|
524
|
+
this.emitSetData(internal, entry.keyArgs, data, 'data')
|
|
525
|
+
} finally {
|
|
526
|
+
this.applyingRemote = false
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
applyRemoteInvalidate(queryId: string, keyArgs: readonly unknown[]): void {
|
|
531
|
+
const query = lookupRegisteredQuery(queryId)
|
|
532
|
+
if (!query) return
|
|
533
|
+
if (query.__olas !== 'query') return // infinite — deferred for v1
|
|
534
|
+
const internal = query as unknown as AnyQuery
|
|
535
|
+
const map = this.maps.get(internal)
|
|
536
|
+
if (!map) return
|
|
537
|
+
const hash = stableHash(keyArgs)
|
|
538
|
+
const entry = map.get(hash)
|
|
539
|
+
if (!entry) return
|
|
540
|
+
this.applyingRemote = true
|
|
541
|
+
try {
|
|
542
|
+
// Emit AFTER kicking off invalidate so plugins reading entry state see
|
|
543
|
+
// post-invalidation values, mirroring setData's emit-after-write order.
|
|
544
|
+
entry.entry.invalidate().catch((err) => {
|
|
545
|
+
dispatchError(this.onError, err, {
|
|
546
|
+
kind: 'cache',
|
|
547
|
+
controllerPath: [],
|
|
548
|
+
queryKey: entry.keyArgs,
|
|
549
|
+
})
|
|
550
|
+
})
|
|
551
|
+
this.emitInvalidate(internal, entry.keyArgs, 'data')
|
|
552
|
+
} finally {
|
|
553
|
+
this.applyingRemote = false
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
hydrate(state: DehydratedState): void {
|
|
558
|
+
if (state.version !== 1) return
|
|
559
|
+
for (const entry of state.entries) {
|
|
560
|
+
const hash = stableHash(entry.key)
|
|
561
|
+
this.hydratedData.set(hash, {
|
|
562
|
+
data: entry.data,
|
|
563
|
+
lastUpdatedAt: entry.lastUpdatedAt,
|
|
564
|
+
})
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Snapshot every live cache entry (regular + infinite) as a flat list of
|
|
570
|
+
* `DebugCacheEntry`. Exposed via `root.__debug.queryEntries()` for the
|
|
571
|
+
* devtools cache inspector — shows current data and state, not past
|
|
572
|
+
* fetch events. Spec §20.9.
|
|
573
|
+
*/
|
|
574
|
+
queryEntriesSnapshot(): import('../devtools').DebugCacheEntry[] {
|
|
575
|
+
const out: import('../devtools').DebugCacheEntry[] = []
|
|
576
|
+
for (const map of this.maps.values()) {
|
|
577
|
+
for (const ce of map.values()) {
|
|
578
|
+
out.push({
|
|
579
|
+
key: ce.keyArgs as readonly unknown[],
|
|
580
|
+
status: ce.entry.status.peek(),
|
|
581
|
+
data: ce.entry.data.peek(),
|
|
582
|
+
error: ce.entry.error.peek(),
|
|
583
|
+
lastUpdatedAt: ce.entry.lastUpdatedAt.peek(),
|
|
584
|
+
isStale: ce.entry.isStale.peek(),
|
|
585
|
+
isFetching: ce.entry.isFetching.peek(),
|
|
586
|
+
hasPendingMutations: ce.entry.hasPendingMutations.peek(),
|
|
587
|
+
})
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
for (const map of this.infiniteMaps.values()) {
|
|
591
|
+
for (const ce of map.values()) {
|
|
592
|
+
out.push({
|
|
593
|
+
key: ce.keyArgs as readonly unknown[],
|
|
594
|
+
status: ce.entry.status.peek(),
|
|
595
|
+
// Infinite entries carry an array of pages; expose them verbatim.
|
|
596
|
+
data: ce.entry.pages.peek(),
|
|
597
|
+
error: ce.entry.error.peek(),
|
|
598
|
+
lastUpdatedAt: ce.entry.lastUpdatedAt.peek(),
|
|
599
|
+
isStale: ce.entry.isStale.peek(),
|
|
600
|
+
isFetching: ce.entry.isFetching.peek(),
|
|
601
|
+
hasPendingMutations: ce.entry.hasPendingMutations.peek(),
|
|
602
|
+
})
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return out
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
dehydrate(): DehydratedState {
|
|
609
|
+
const entries: DehydratedState['entries'] = []
|
|
610
|
+
for (const map of this.maps.values()) {
|
|
611
|
+
for (const ce of map.values()) {
|
|
612
|
+
if (ce.entry.status.peek() === 'success') {
|
|
613
|
+
entries.push({
|
|
614
|
+
key: ce.keyArgs,
|
|
615
|
+
data: ce.entry.data.peek(),
|
|
616
|
+
lastUpdatedAt: ce.entry.lastUpdatedAt.peek() ?? Date.now(),
|
|
617
|
+
})
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return { version: 1, entries }
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async waitForIdle(): Promise<void> {
|
|
625
|
+
for (let safety = 0; safety < 100; safety++) {
|
|
626
|
+
const tasks: Promise<void>[] = []
|
|
627
|
+
for (const map of this.maps.values()) {
|
|
628
|
+
for (const ce of map.values()) {
|
|
629
|
+
if (ce.entry.isFetching.peek()) {
|
|
630
|
+
tasks.push(waitUntilFalse(ce.entry.isFetching))
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
for (const map of this.infiniteMaps.values()) {
|
|
635
|
+
for (const ce of map.values()) {
|
|
636
|
+
if (ce.entry.isFetching.peek()) {
|
|
637
|
+
tasks.push(waitUntilFalse(ce.entry.isFetching))
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (this.mutationsInflight$.peek() > 0) {
|
|
642
|
+
tasks.push(
|
|
643
|
+
new Promise<void>((resolve) => {
|
|
644
|
+
const unsub = this.mutationsInflight$.subscribe((v) => {
|
|
645
|
+
if (v === 0) {
|
|
646
|
+
unsub()
|
|
647
|
+
resolve()
|
|
648
|
+
}
|
|
649
|
+
})
|
|
650
|
+
}),
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
if (tasks.length === 0) return
|
|
654
|
+
await Promise.all(tasks)
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
bindEntry<Args extends unknown[], T>(query: Query<Args, T>, args: Args): ClientEntry<T> {
|
|
659
|
+
const internal = query as AnyQuery
|
|
660
|
+
let map = this.maps.get(internal)
|
|
661
|
+
if (!map) {
|
|
662
|
+
map = new Map()
|
|
663
|
+
this.maps.set(internal, map)
|
|
664
|
+
this.touchedQueries.add(internal)
|
|
665
|
+
internal.__clients.add(this)
|
|
666
|
+
}
|
|
667
|
+
const keyArgs = internal.__spec.key(...args)
|
|
668
|
+
const hash = stableHash(keyArgs)
|
|
669
|
+
let entry = map.get(hash) as ClientEntry<T> | undefined
|
|
670
|
+
if (!entry) {
|
|
671
|
+
const hydrated = this.hydratedData.get(hash) as { data: T; lastUpdatedAt: number } | undefined
|
|
672
|
+
if (hydrated) this.hydratedData.delete(hash)
|
|
673
|
+
entry = new ClientEntry<T>(this, internal, args, keyArgs, internal.__spec, hydrated)
|
|
674
|
+
map.set(hash, entry as ClientEntry<unknown>)
|
|
675
|
+
// The entry is created without an immediate subscriber (callers like
|
|
676
|
+
// `prefetch`/`setData`/`invalidate` reach `bindEntry` first; subscribing
|
|
677
|
+
// callers then call `acquire()` right after, which clears the gc timer).
|
|
678
|
+
entry.scheduleGcIfOrphan()
|
|
679
|
+
}
|
|
680
|
+
return entry
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
dropEntry(entry: ClientEntry<unknown>): void {
|
|
684
|
+
const map = this.maps.get(entry.query)
|
|
685
|
+
if (!map) return
|
|
686
|
+
const hash = stableHash(entry.keyArgs)
|
|
687
|
+
if (map.get(hash) !== entry) return
|
|
688
|
+
map.delete(hash)
|
|
689
|
+
entry.dispose()
|
|
690
|
+
if (map.size === 0) {
|
|
691
|
+
this.maps.delete(entry.query)
|
|
692
|
+
}
|
|
693
|
+
if (__DEV__) {
|
|
694
|
+
this.devtools?.emit({ type: 'cache:gc', queryKey: entry.keyArgs })
|
|
695
|
+
}
|
|
696
|
+
this.emitGc(entry.query, entry.keyArgs, 'data')
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
invalidate<Args extends unknown[]>(query: Query<Args, any>, args: Args): void {
|
|
700
|
+
const internal = query as AnyQuery
|
|
701
|
+
const map = this.maps.get(internal)
|
|
702
|
+
if (!map) return
|
|
703
|
+
const keyArgs = internal.__spec.key(...args)
|
|
704
|
+
const hash = stableHash(keyArgs)
|
|
705
|
+
const entry = map.get(hash)
|
|
706
|
+
if (!entry) return
|
|
707
|
+
if (__DEV__) {
|
|
708
|
+
this.devtools?.emit({ type: 'cache:invalidated', queryKey: keyArgs })
|
|
709
|
+
}
|
|
710
|
+
entry.entry.invalidate().catch((err) => {
|
|
711
|
+
dispatchError(this.onError, err, {
|
|
712
|
+
kind: 'cache',
|
|
713
|
+
controllerPath: [],
|
|
714
|
+
queryKey: keyArgs,
|
|
715
|
+
})
|
|
716
|
+
})
|
|
717
|
+
this.emitInvalidate(internal, keyArgs, 'data')
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
invalidateAll(query: Query<any, any>): void {
|
|
721
|
+
const internal = query as AnyQuery
|
|
722
|
+
const map = this.maps.get(internal)
|
|
723
|
+
if (!map) return
|
|
724
|
+
for (const [hash, entry] of map) {
|
|
725
|
+
void hash
|
|
726
|
+
if (__DEV__) {
|
|
727
|
+
this.devtools?.emit({ type: 'cache:invalidated', queryKey: entry.keyArgs })
|
|
728
|
+
}
|
|
729
|
+
entry.entry.invalidate().catch((err) => {
|
|
730
|
+
dispatchError(this.onError, err, {
|
|
731
|
+
kind: 'cache',
|
|
732
|
+
controllerPath: [],
|
|
733
|
+
queryKey: entry.keyArgs,
|
|
734
|
+
})
|
|
735
|
+
})
|
|
736
|
+
this.emitInvalidate(internal, entry.keyArgs, 'data')
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
setData<Args extends unknown[], T>(
|
|
741
|
+
query: Query<Args, T>,
|
|
742
|
+
args: Args,
|
|
743
|
+
updater: (prev: T | undefined) => T,
|
|
744
|
+
): Snapshot {
|
|
745
|
+
const entry = this.bindEntry(query, args)
|
|
746
|
+
const snapshot = entry.entry.setData(updater)
|
|
747
|
+
// Read the post-update value to broadcast — plugins want the new state,
|
|
748
|
+
// not the updater function (which would be uncloneable across
|
|
749
|
+
// BroadcastChannel).
|
|
750
|
+
this.emitSetData(entry.query, entry.keyArgs, entry.entry.data.peek(), 'data')
|
|
751
|
+
return snapshot
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
bindInfiniteEntry<Args extends unknown[], TPage, TItem>(
|
|
755
|
+
query: InfiniteQuery<Args, TPage, TItem>,
|
|
756
|
+
args: Args,
|
|
757
|
+
): InfiniteClientEntry<TPage, TItem, unknown> {
|
|
758
|
+
const internal = query as AnyInfiniteQuery
|
|
759
|
+
let map = this.infiniteMaps.get(internal)
|
|
760
|
+
if (!map) {
|
|
761
|
+
map = new Map()
|
|
762
|
+
this.infiniteMaps.set(internal, map)
|
|
763
|
+
this.touchedInfiniteQueries.add(internal)
|
|
764
|
+
internal.__clients.add(this)
|
|
765
|
+
}
|
|
766
|
+
const keyArgs = internal.__spec.key(...args)
|
|
767
|
+
const hash = stableHash(keyArgs)
|
|
768
|
+
let entry = map.get(hash) as InfiniteClientEntry<TPage, TItem, unknown> | undefined
|
|
769
|
+
if (!entry) {
|
|
770
|
+
entry = new InfiniteClientEntry<TPage, TItem, unknown>(
|
|
771
|
+
this,
|
|
772
|
+
internal,
|
|
773
|
+
args,
|
|
774
|
+
keyArgs,
|
|
775
|
+
internal.__spec,
|
|
776
|
+
)
|
|
777
|
+
map.set(hash, entry as InfiniteClientEntry<unknown, unknown, unknown>)
|
|
778
|
+
entry.scheduleGcIfOrphan()
|
|
779
|
+
}
|
|
780
|
+
return entry
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
dropInfiniteEntry(entry: InfiniteClientEntry<unknown, unknown, unknown>): void {
|
|
784
|
+
const map = this.infiniteMaps.get(entry.query)
|
|
785
|
+
if (!map) return
|
|
786
|
+
const hash = stableHash(entry.keyArgs)
|
|
787
|
+
if (map.get(hash) !== entry) return
|
|
788
|
+
map.delete(hash)
|
|
789
|
+
entry.dispose()
|
|
790
|
+
if (map.size === 0) {
|
|
791
|
+
this.infiniteMaps.delete(entry.query)
|
|
792
|
+
}
|
|
793
|
+
this.emitGc(entry.query, entry.keyArgs, 'infinite')
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
invalidateInfinite<Args extends unknown[]>(
|
|
797
|
+
query: InfiniteQuery<Args, any, any>,
|
|
798
|
+
args: Args,
|
|
799
|
+
): void {
|
|
800
|
+
const internal = query as AnyInfiniteQuery
|
|
801
|
+
const map = this.infiniteMaps.get(internal)
|
|
802
|
+
if (!map) return
|
|
803
|
+
const keyArgs = internal.__spec.key(...args)
|
|
804
|
+
const hash = stableHash(keyArgs)
|
|
805
|
+
const entry = map.get(hash)
|
|
806
|
+
if (!entry) return
|
|
807
|
+
entry.entry.invalidate().catch((err) => {
|
|
808
|
+
dispatchError(this.onError, err, {
|
|
809
|
+
kind: 'cache',
|
|
810
|
+
controllerPath: [],
|
|
811
|
+
queryKey: entry.keyArgs,
|
|
812
|
+
})
|
|
813
|
+
})
|
|
814
|
+
this.emitInvalidate(internal, keyArgs, 'infinite')
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
invalidateAllInfinite(query: InfiniteQuery<any, any, any>): void {
|
|
818
|
+
const internal = query as AnyInfiniteQuery
|
|
819
|
+
const map = this.infiniteMaps.get(internal)
|
|
820
|
+
if (!map) return
|
|
821
|
+
for (const entry of map.values()) {
|
|
822
|
+
entry.entry.invalidate().catch((err) => {
|
|
823
|
+
dispatchError(this.onError, err, {
|
|
824
|
+
kind: 'cache',
|
|
825
|
+
controllerPath: [],
|
|
826
|
+
queryKey: entry.keyArgs,
|
|
827
|
+
})
|
|
828
|
+
})
|
|
829
|
+
this.emitInvalidate(internal, entry.keyArgs, 'infinite')
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
setInfiniteData<Args extends unknown[], TPage>(
|
|
834
|
+
query: InfiniteQuery<Args, TPage, any>,
|
|
835
|
+
args: Args,
|
|
836
|
+
updater: (prev: TPage[] | undefined) => TPage[],
|
|
837
|
+
): Snapshot {
|
|
838
|
+
const entry = this.bindInfiniteEntry(query, args)
|
|
839
|
+
const snapshot = entry.entry.setData(updater)
|
|
840
|
+
this.emitSetData(entry.query, entry.keyArgs, entry.entry.pages.peek(), 'infinite')
|
|
841
|
+
return snapshot
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
prefetchInfinite<Args extends unknown[], TPage>(
|
|
845
|
+
query: InfiniteQuery<Args, TPage, any>,
|
|
846
|
+
args: Args,
|
|
847
|
+
): Promise<TPage> {
|
|
848
|
+
const entry = this.bindInfiniteEntry(query, args)
|
|
849
|
+
// Acquire/release wraps the fetch so the entry isn't gc'd mid-flight by
|
|
850
|
+
// the orphan-gc timer scheduled in `bindInfiniteEntry`.
|
|
851
|
+
entry.acquire()
|
|
852
|
+
const promise = (async () => {
|
|
853
|
+
const status = entry.entry.status.peek()
|
|
854
|
+
if (status === 'success' && !entry.entry.isStaleNow()) {
|
|
855
|
+
return entry.entry.pages.peek()[0] as TPage
|
|
856
|
+
}
|
|
857
|
+
return entry.entry.startFetch()
|
|
858
|
+
})()
|
|
859
|
+
return promise.finally(() => entry.release())
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
prefetch<Args extends unknown[], T>(query: Query<Args, T>, args: Args): Promise<T> {
|
|
863
|
+
const entry = this.bindEntry(query, args)
|
|
864
|
+
entry.acquire()
|
|
865
|
+
const promise = (async () => {
|
|
866
|
+
const status = entry.entry.status.peek()
|
|
867
|
+
if (status === 'success' && !entry.entry.isStaleNow()) {
|
|
868
|
+
return entry.entry.data.peek() as T
|
|
869
|
+
}
|
|
870
|
+
if (entry.entry.isFetching.peek()) {
|
|
871
|
+
return entry.entry.firstValue()
|
|
872
|
+
}
|
|
873
|
+
return entry.entry.startFetch()
|
|
874
|
+
})()
|
|
875
|
+
return promise.finally(() => entry.release())
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
inflightCount(): number {
|
|
879
|
+
let count = 0
|
|
880
|
+
for (const [, map] of this.maps) {
|
|
881
|
+
for (const [, entry] of map) {
|
|
882
|
+
if (entry.entry.isFetching.peek()) count++
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return count
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
dispose(): void {
|
|
889
|
+
if (this.disposed) return
|
|
890
|
+
this.disposed = true
|
|
891
|
+
for (const map of this.maps.values()) {
|
|
892
|
+
for (const entry of map.values()) {
|
|
893
|
+
entry.dispose()
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
this.maps.clear()
|
|
897
|
+
for (const map of this.infiniteMaps.values()) {
|
|
898
|
+
for (const entry of map.values()) {
|
|
899
|
+
entry.dispose()
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
this.infiniteMaps.clear()
|
|
903
|
+
for (const q of this.touchedQueries) {
|
|
904
|
+
q.__clients.delete(this)
|
|
905
|
+
}
|
|
906
|
+
this.touchedQueries.clear()
|
|
907
|
+
for (const q of this.touchedInfiniteQueries) {
|
|
908
|
+
q.__clients.delete(this)
|
|
909
|
+
}
|
|
910
|
+
this.touchedInfiniteQueries.clear()
|
|
911
|
+
this.hydratedData.clear()
|
|
912
|
+
for (const plugin of this.plugins) {
|
|
913
|
+
if (plugin.dispose) {
|
|
914
|
+
const cb = plugin.dispose
|
|
915
|
+
this.callPlugin(() => cb.call(plugin))
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function waitUntilFalse(sig: {
|
|
922
|
+
peek(): boolean
|
|
923
|
+
subscribe(h: (v: boolean) => void): () => void
|
|
924
|
+
}): Promise<void> {
|
|
925
|
+
if (!sig.peek()) return Promise.resolve()
|
|
926
|
+
return new Promise<void>((resolve) => {
|
|
927
|
+
const unsub = sig.subscribe((v) => {
|
|
928
|
+
if (!v) {
|
|
929
|
+
unsub()
|
|
930
|
+
resolve()
|
|
931
|
+
}
|
|
932
|
+
})
|
|
933
|
+
})
|
|
934
|
+
}
|