@raf-query/core 0.11.0 → 0.12.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +18 -2
  2. package/README.md +28 -18
  3. package/dist/client.d.mts +132 -119
  4. package/dist/client.mjs +59 -162
  5. package/dist/entry.d.mts +67 -148
  6. package/dist/entry.mjs +303 -474
  7. package/dist/features/focus-refetch/focus-refetch.d.mts +15 -0
  8. package/dist/features/focus-refetch/focus-refetch.mjs +18 -0
  9. package/dist/features/polling/polling.d.mts +12 -0
  10. package/dist/features/polling/polling.mjs +35 -0
  11. package/dist/features/reconnect-refetch/reconnect-refetch.d.mts +15 -0
  12. package/dist/features/reconnect-refetch/reconnect-refetch.mjs +14 -0
  13. package/dist/features/shared.mjs +13 -0
  14. package/dist/guards.mjs +23 -0
  15. package/dist/hydration/hydration.d.mts +73 -0
  16. package/dist/hydration/hydration.mjs +128 -0
  17. package/dist/index.d.mts +18 -13
  18. package/dist/index.mjs +15 -13
  19. package/dist/infinite/infinite.d.mts +71 -0
  20. package/dist/infinite/infinite.mjs +88 -0
  21. package/dist/internal.d.mts +13 -0
  22. package/dist/internal.mjs +7 -0
  23. package/dist/keys/keys.d.mts +46 -0
  24. package/dist/keys/keys.mjs +53 -0
  25. package/dist/keys/targets.d.mts +56 -0
  26. package/dist/keys/targets.mjs +85 -0
  27. package/dist/links/retry/retry.d.mts +26 -0
  28. package/dist/links/retry/retry.mjs +51 -0
  29. package/dist/links/timeout/timeout.d.mts +9 -0
  30. package/dist/links/timeout/timeout.mjs +27 -0
  31. package/dist/load.d.mts +124 -0
  32. package/dist/load.mjs +165 -0
  33. package/dist/options.d.mts +112 -338
  34. package/dist/options.mjs +25 -36
  35. package/dist/presets.d.mts +29 -0
  36. package/dist/presets.mjs +25 -0
  37. package/dist/root.d.mts +82 -0
  38. package/dist/structural-sharing/structural-sharing.mjs +32 -0
  39. package/dist/subscribe.d.mts +15 -0
  40. package/dist/subscribe.mjs +29 -0
  41. package/dist/thenable.d.mts +13 -17
  42. package/dist/thenable.mjs +25 -6
  43. package/dist/verbs.d.mts +109 -0
  44. package/dist/verbs.mjs +87 -0
  45. package/package.json +13 -9
  46. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  47. package/dist/all-keyed.d.mts +0 -18
  48. package/dist/all-keyed.mjs +0 -19
  49. package/dist/binding.d.mts +0 -92
  50. package/dist/binding.mjs +0 -186
  51. package/dist/cache.d.mts +0 -164
  52. package/dist/cache.mjs +0 -184
  53. package/dist/controls.d.mts +0 -93
  54. package/dist/controls.mjs +0 -69
  55. package/dist/hydration.d.mts +0 -84
  56. package/dist/hydration.mjs +0 -148
  57. package/dist/infinite.d.mts +0 -5
  58. package/dist/infinite.mjs +0 -103
  59. package/dist/key.d.mts +0 -18
  60. package/dist/key.mjs +0 -36
  61. package/dist/listen.mjs +0 -19
  62. package/dist/notify.mjs +0 -21
  63. package/dist/observer.d.mts +0 -110
  64. package/dist/observer.mjs +0 -300
  65. package/dist/platform.d.mts +0 -27
  66. package/dist/platform.mjs +0 -44
  67. package/dist/query.d.mts +0 -137
  68. package/dist/query.mjs +0 -147
  69. package/dist/request.mjs +0 -47
  70. package/dist/retry.mjs +0 -25
  71. package/dist/share.mjs +0 -40
package/dist/entry.mjs CHANGED
@@ -1,526 +1,355 @@
1
- import { replaceEqualDeep } from "./share.mjs";
2
- import { fetchPages, isInfiniteData, nextPageParam, previousPageParam } from "./infinite.mjs";
3
- import { listen } from "./listen.mjs";
4
- import { scheduleNotify } from "./notify.mjs";
5
- import { whenOnline } from "./platform.mjs";
6
- import { fromQueryFn, requestServices, runRequest } from "./request.mjs";
7
- import { retrySchedule } from "./retry.mjs";
1
+ import { listeners } from "./subscribe.mjs";
2
+ import { isObject } from "./guards.mjs";
3
+ import { hashKey } from "./keys/keys.mjs";
4
+ import { matches } from "./keys/targets.mjs";
5
+ import { optionsOf } from "./options.mjs";
6
+ import { replaceEqualDeep } from "./structural-sharing/structural-sharing.mjs";
8
7
  import { fulfilled, pending } from "./thenable.mjs";
9
- import { Cause, Data, Deferred, Effect, Exit, FiberMap, Pipeable, Predicate, Schedule, Scope } from "effect";
10
8
  //#region src/entry.ts
11
- const TypeId = "~@raf-query/core/Query";
12
- /**
13
- * The failure of a request that was interrupted rather than settled: its
14
- * entry was removed or cleared, or an Effect `queryFn` was interrupted.
15
- */
16
- var CancelledError = class extends Data.TaggedError("CancelledError") {
17
- message = "The query was cancelled";
9
+ const initialState = {
10
+ data: void 0,
11
+ error: void 0,
12
+ fetchingDirection: void 0,
13
+ hasData: false,
14
+ isFetching: false,
15
+ status: "pending",
16
+ updatedAt: 0
18
17
  };
19
- /**
20
- * Why a query with `enabled: false` has no data: `ensure` / `fetch` die
21
- * with it, and on a server its promise rejects with it.
22
- */
23
- var DisabledError = class extends Data.TaggedError("DisabledError") {
24
- get message() {
25
- return `Query ${this.hash} is disabled (enabled: false)`;
26
- }
27
- };
28
- const Proto = {
29
- ...Pipeable.Prototype,
30
- [TypeId]: {
31
- _A: (value) => value,
32
- _E: (value) => value
33
- }
34
- };
35
- const defaultSubscribeOptions = {};
36
- function isQuery(value) {
37
- return Predicate.hasProperty(value, TypeId);
18
+ const fiveMinutes = 3e5;
19
+ function abortError() {
20
+ return new DOMException("The query was cancelled", "AbortError");
21
+ }
22
+ function notify(entry) {
23
+ const client = entry.client;
24
+ const changed = client["~changed"];
25
+ if (changed.size === 0) queueMicrotask(() => {
26
+ const batch = [...changed];
27
+ changed.clear();
28
+ for (const item of batch) {
29
+ item.readers.emit(item.state);
30
+ client["~events"].emit({
31
+ entry: item,
32
+ type: "updated"
33
+ });
34
+ }
35
+ });
36
+ changed.add(entry);
37
+ }
38
+ function setState(entry, patch) {
39
+ entry.state = {
40
+ ...entry.state,
41
+ ...patch
42
+ };
43
+ notify(entry);
38
44
  }
39
- function entryOf(query) {
40
- return query;
45
+ function newPending(entry) {
46
+ const next = pending();
47
+ entry.pendingThenable = next;
48
+ entry.thenable = next.promise;
41
49
  }
42
- function emit(cache, event) {
43
- for (const listener of cache.listeners) listener(event);
50
+ function gcTime(entry) {
51
+ return entry.options?.gcTime ?? entry.client.config.gcTime ?? fiveMinutes;
44
52
  }
45
- /** Fibers keyed by task: forking a task interrupts its previous fiber. */
46
- function makeTasks(cache, scope) {
47
- const runSync = Effect.runSyncWith(cache.services);
48
- const runFork = Effect.runForkWith(cache.services);
49
- const fibers = runSync(FiberMap.make().pipe(Scope.provide(scope)));
50
- const run = runSync(FiberMap.runtime(fibers)());
51
- return {
52
- cancel(task) {
53
- if (FiberMap.hasUnsafe(fibers, task)) runFork(FiberMap.remove(fibers, task));
54
- },
55
- fork(task, effect, options) {
56
- run(task, effect, options);
53
+ function scheduleGc(entry) {
54
+ const client = entry.client;
55
+ client.clock.clearTimeout(entry.gcTimer);
56
+ entry.gcTimer = void 0;
57
+ const time = gcTime(entry);
58
+ if (client.isServer || entry.readers.size > 0 || !Number.isFinite(time)) return;
59
+ entry.gcTimer = client.clock.setTimeout(() => {
60
+ entry.gcTimer = void 0;
61
+ if (entry.run) {
62
+ entry.run.done.promise.then(() => scheduleGc(entry));
63
+ return;
57
64
  }
58
- };
65
+ remove(entry);
66
+ }, time);
59
67
  }
60
- function make(cache, key, hash) {
61
- const scope = Scope.forkUnsafe(cache.scope);
68
+ function create(client, key, hash) {
62
69
  const first = pending();
63
- const entry = Object.assign(Object.create(Proto), {
64
- cache,
65
- errorShown: false,
66
- failureCountBefore: 0,
67
- handle: {
68
- input: void 0,
69
- key
70
- },
70
+ const entry = {
71
+ client,
72
+ factory: void 0,
73
+ gcTimer: void 0,
71
74
  hash,
72
- hydratedMeta: void 0,
73
- inFlight: void 0,
75
+ input: void 0,
74
76
  key,
75
- listeners: /* @__PURE__ */ new Map(),
76
77
  options: void 0,
77
- pending: first,
78
- retriesStopped: false,
79
- returnsEffect: void 0,
80
- scope,
81
- services: cache.services,
82
- signalRead: false,
83
- state: {
84
- data: void 0,
85
- error: null,
86
- failureCount: 0,
87
- fetchDirection: void 0,
88
- hasData: false,
89
- isFetching: false,
90
- isInvalidated: false,
91
- isStale: true,
92
- promise: first.promise,
93
- status: "pending",
94
- updatedAt: 0
95
- },
96
- tasks: makeTasks(cache, scope)
97
- });
98
- const command = (request) => Effect.suspend(() => isDisabled(entry) ? Effect.void : Effect.ignore(fetch(entry, request)));
99
- const commands = {
100
- fetchNextPage: command({ direction: "forward" }),
101
- fetchPreviousPage: command({ direction: "backward" }),
102
- refetch: command()
78
+ pendingThenable: first,
79
+ readers: listeners((size) => {
80
+ client["~events"].emit({
81
+ entry,
82
+ type: "readers"
83
+ });
84
+ if (size === 0) scheduleGc(entry);
85
+ else {
86
+ client.clock.clearTimeout(entry.gcTimer);
87
+ entry.gcTimer = void 0;
88
+ }
89
+ }),
90
+ requestedAt: client["~seq"],
91
+ run: void 0,
92
+ state: initialState,
93
+ thenable: first.promise
103
94
  };
104
- Object.assign(entry, { commands });
95
+ client["~entries"].set(hash, entry);
96
+ client["~events"].emit({
97
+ entry,
98
+ type: "added"
99
+ });
105
100
  scheduleGc(entry);
106
101
  return entry;
107
102
  }
108
- function now(entry) {
109
- return entry.cache.clock.currentTimeMillisUnsafe();
110
- }
111
- /** Readers are subscribed; `invalidate` and focus refetch only what is read. */
112
- function isObserved(entry) {
113
- return entry.listeners.size > 0;
114
- }
115
- function behavior(entry, name) {
116
- return entry.options?.[name] ?? entry.cache.defaults[name];
117
- }
118
- /** The entry's latest factory call returned `enabled: false`: it doesn't run. */
119
- function isDisabled(entry) {
120
- return behavior(entry, "enabled") === false;
121
- }
122
- /** Until a factory call attaches options, `meta` comes from hydration. */
123
- function metaOf(entry) {
124
- return entry.options ? behavior(entry, "meta") : entry.hydratedMeta ?? entry.cache.defaults.meta;
125
- }
126
- function computeStale(entry, state) {
127
- if (state.isInvalidated || !state.hasData) return true;
128
- const staleTime = behavior(entry, "staleTime") ?? 0;
129
- return now(entry) - state.updatedAt >= staleTime;
130
- }
131
- function isStaleUnsafe(entry) {
132
- return computeStale(entry, entry.state);
133
- }
134
- function hasNextPageUnsafe(entry) {
135
- const options = entry.options;
136
- const data = entry.state.data;
137
- return options?.kind === "infinite" && isInfiniteData(data) && nextPageParam(options, data) != null;
138
- }
139
- function hasPreviousPageUnsafe(entry) {
140
- const options = entry.options;
141
- const data = entry.state.data;
142
- return options?.kind === "infinite" && isInfiniteData(data) && previousPageParam(options, data) != null;
143
- }
144
- function subscribeUnsafe(entry, listener, options = defaultSubscribeOptions) {
145
- entry.listeners.set(listener, options);
146
- entry.retriesStopped = false;
147
- entry.tasks.cancel("gc");
148
- scheduleStale(entry);
149
- emit(entry.cache, {
150
- query: entry,
151
- type: "observers"
152
- });
153
- return () => {
154
- if (!entry.listeners.delete(listener)) return;
155
- if (entry.listeners.size === 0) {
156
- entry.tasks.cancel("stale");
157
- scheduleGc(entry);
158
- const inFlight = entry.inFlight;
159
- if (inFlight) scheduleNotify(() => unobserved(entry, inFlight));
160
- }
161
- emit(entry.cache, {
162
- query: entry,
163
- type: "observers"
164
- });
165
- };
166
- }
167
- function changes(entry) {
168
- return listen((push) => {
169
- push(entry.state);
170
- return subscribeUnsafe(entry, () => push(entry.state));
171
- });
103
+ /** The entry for a key, created if missing. Hydration uses it. */
104
+ function entryForKey(client, key) {
105
+ const hash = hashKey(key);
106
+ return client["~entries"].get(hash) ?? create(client, key, hash);
172
107
  }
173
108
  /**
174
- * Run this entry's requests with the services of `caller`, a verb's fiber
175
- * context, over the cache's: every request from now on, including ones a
176
- * render, a refetch or `invalidate` starts.
109
+ * The entry for a factory's input, created if missing, with the factory's
110
+ * current options. Every read goes through here, which links an entry that
111
+ * was only hydrated to its factory.
177
112
  */
178
- function provide(entry, caller) {
179
- entry.services = requestServices(entry.cache.services, caller);
180
- }
181
- /** Attach the options and input of the latest factory call. */
182
- function setOptions(entry, options, input) {
113
+ function entryOf(client, factory, input) {
114
+ const options = optionsOf(factory, input);
115
+ const entry = entryForKey(client, options.queryKey);
116
+ entry.factory = factory;
117
+ entry.input = input;
183
118
  entry.options = options;
184
- if (entry.handle.input !== input) entry.handle = {
185
- input,
186
- key: entry.key
187
- };
188
- }
189
- /**
190
- * The promise a render should unwrap. Starts the first request if nothing
191
- * has, and retries a failed first load once its error has been shown.
192
- */
193
- function readUnsafe(entry) {
194
- const state = entry.state;
195
- if (!state.hasData && !state.isFetching) {
196
- if (state.status !== "error" || entry.errorShown) begin(entry, {});
197
- else markErrorShown(entry);
198
- }
199
- return entry.state.promise;
200
- }
201
- /** Start a request unless one is in flight, without waiting for it. */
202
- function start(entry) {
203
- begin(entry, {});
119
+ scheduleGc(entry);
120
+ return entry;
204
121
  }
205
- /**
206
- * Whether cached data that was asked for again should be refetched. A server
207
- * cache renders once, so a refetch there could only duplicate a request whose
208
- * result is never shown; it refetches only what `invalidate` marked.
209
- */
210
- function needsRefetch(entry) {
211
- return entry.cache.isServer ? entry.state.isInvalidated : isStaleUnsafe(entry);
122
+ /** Whether an invalidation newer than the entry's data covers it. */
123
+ function isInvalidated(entry) {
124
+ return entry.client["~stamps"].some((stamp) => stamp.at > entry.requestedAt && matches(entry, stamp));
212
125
  }
213
- /** Background refetch of cached data that was asked for again. */
214
- function revalidate(entry) {
126
+ /** Whether the entry's data should be refetched before it's trusted. */
127
+ function isStale(entry) {
215
128
  const state = entry.state;
216
- if (state.hasData && !state.isFetching && needsRefetch(entry)) start(entry);
217
- }
218
- function fetch(entry, request = {}) {
219
- return Effect.suspend(() => {
220
- const inFlight = entry.inFlight;
221
- if (inFlight && request.direction && !request.restart) return Deferred.await(inFlight).pipe(Effect.exit, Effect.andThen(fetch(entry, request)));
222
- return begin(entry, request);
223
- });
129
+ if (!state.hasData) return true;
130
+ const staleTime = entry.options?.staleTime ?? entry.client.config.staleTime ?? 0;
131
+ return entry.client.clock.now() - state.updatedAt >= staleTime || isInvalidated(entry);
224
132
  }
225
- function invalidate(entry) {
226
- return Effect.suspend(() => {
227
- set(entry, { isInvalidated: true });
228
- if (!isObserved(entry) || !entry.options || isDisabled(entry)) return Effect.void;
229
- return Effect.ignore(begin(entry, { restart: true }));
230
- });
231
- }
232
- function reset(entry) {
233
- return Effect.suspend(() => {
234
- entry.tasks.cancel("errorShown");
235
- entry.errorShown = false;
236
- entry.pending ??= pending();
237
- set(entry, {
238
- data: void 0,
239
- error: null,
240
- failureCount: 0,
241
- hasData: false,
242
- isInvalidated: false,
243
- promise: entry.pending.promise,
244
- status: "pending",
245
- updatedAt: 0
246
- });
247
- if (!entry.options || isDisabled(entry) || !isObserved(entry) && !entry.inFlight) return Effect.void;
248
- return Effect.ignore(begin(entry, { restart: true }));
249
- });
250
- }
251
- /** Write data directly, as if a request had returned it. */
252
- function setData(entry, data, updatedAt = now(entry)) {
253
- succeed(entry, data, updatedAt);
254
- }
255
- /**
256
- * Adopt a request started elsewhere (the server's, during hydration). Its
257
- * data counts as fetched when the sender got it, not when it arrived.
258
- */
259
- function resume(entry, promise) {
260
- if (entry.state.hasData || entry.inFlight) return;
261
- let updatedAt;
262
- startRequest(entry, Effect.promise(async () => {
263
- const result = await promise;
264
- updatedAt = result.updatedAt;
265
- return result.data;
266
- }), void 0, () => updatedAt);
267
- }
268
- /** Refetch a read, stale entry whose readers want it on this event. */
269
- function onPlatformEvent(entry, event) {
270
- const option = event === "focus" ? "refetchOnWindowFocus" : "refetchOnReconnect";
271
- const fallback = behavior(entry, option) ?? true;
272
- if ([...entry.listeners.values()].some((options) => options[option] ?? fallback) && isStaleUnsafe(entry)) start(entry);
273
- }
274
- /** Take `entry` out of its cache: interrupt its fibers and fail its awaiters. */
275
- function remove(entry) {
276
- const cache = entry.cache;
277
- if (cache.entries.get(entry.hash) !== entry) return;
278
- cache.entries.delete(entry.hash);
279
- const inFlight = entry.inFlight;
280
- entry.inFlight = void 0;
281
- Effect.runForkWith(cache.services)(Scope.close(entry.scope, Exit.void));
282
- if (inFlight) Deferred.doneUnsafe(inFlight, Exit.fail(new CancelledError()));
283
- emit(cache, {
284
- query: entry,
285
- type: "removed"
133
+ /** Write data. `startedAt` orders it against invalidations. */
134
+ function write(entry, data, updatedAt, startedAt) {
135
+ const previous = entry.state;
136
+ const next = (entry.options?.structuralSharing ?? entry.client.config.structuralSharing ?? true) && previous.hasData ? replaceEqualDeep(previous.data, data) : data;
137
+ entry.requestedAt = Math.max(entry.requestedAt, startedAt);
138
+ if (entry.pendingThenable) {
139
+ entry.pendingThenable.resolve(next);
140
+ entry.pendingThenable = void 0;
141
+ } else if (!previous.hasData || next !== previous.data) entry.thenable = fulfilled(next);
142
+ setState(entry, {
143
+ data: next,
144
+ error: void 0,
145
+ hasData: true,
146
+ status: "success",
147
+ updatedAt
286
148
  });
149
+ scheduleGc(entry);
287
150
  }
288
151
  /**
289
- * Start a request now, or join the one in flight. The returned Effect
290
- * waits for its result.
152
+ * Record a failed request. Data the entry had stays readable. Without data,
153
+ * the request in flight has a pending thenable for readers: it rejects.
291
154
  */
292
- function begin(entry, request) {
293
- if (Predicate.isTagged(entry.scope.state, "Closed")) return Effect.fail(new CancelledError());
294
- if (isDisabled(entry)) {
295
- const error = new DisabledError({ hash: entry.hash });
296
- if (entry.cache.isServer) {
297
- const firstLoad = entry.pending;
298
- entry.pending = void 0;
299
- firstLoad?.reject(error);
300
- }
301
- return Effect.die(error);
302
- }
303
- const options = entry.options;
304
- const inFlight = entry.inFlight;
305
- if (inFlight && !(request.restart && options)) return Deferred.await(inFlight);
306
- if (!options) {
307
- const promise = entry.state.promise;
308
- return Effect.promise(() => promise);
155
+ function failWith(entry, error) {
156
+ if (!entry.state.hasData) {
157
+ entry.pendingThenable?.reject(error);
158
+ entry.pendingThenable = void 0;
309
159
  }
310
- entry.tasks.cancel("request");
311
- return Deferred.await(startRequest(entry, runRequest(requestEffect(entry, options, request), entry.hash, entry.services), request.direction));
312
- }
313
- function set(entry, patch) {
314
- const next = {
315
- ...entry.state,
316
- ...patch
317
- };
318
- entry.state = {
319
- ...next,
320
- isStale: computeStale(entry, next)
321
- };
322
- for (const listener of entry.listeners.keys()) scheduleNotify(listener);
323
- emit(entry.cache, {
324
- query: entry,
325
- type: "updated"
160
+ setState(entry, {
161
+ error,
162
+ status: "error"
326
163
  });
327
- scheduleStale(entry);
328
164
  }
329
- /** Run `request` as the entry's request fiber, settling into its awaiter. */
330
- function startRequest(entry, request, direction, updatedAt) {
331
- if (!entry.inFlight) entry.failureCountBefore = entry.state.failureCount;
332
- const awaiter = entry.inFlight ?? Deferred.makeUnsafe();
333
- entry.inFlight = awaiter;
334
- entry.retriesStopped = false;
335
- entry.signalRead = false;
336
- entry.tasks.cancel("gc");
337
- if (!entry.state.hasData && !entry.pending) {
338
- entry.pending = pending();
339
- set(entry, {
340
- error: null,
341
- failureCount: 0,
342
- fetchDirection: direction,
343
- isFetching: true,
344
- promise: entry.pending.promise,
345
- status: "pending"
346
- });
347
- } else set(entry, {
348
- failureCount: 0,
349
- fetchDirection: direction,
350
- isFetching: true
165
+ function finish(entry, run, outcome) {
166
+ entry.run = void 0;
167
+ setState(entry, {
168
+ fetchingDirection: void 0,
169
+ isFetching: false
351
170
  });
352
- entry.tasks.fork("request", Effect.exit(request).pipe(Effect.flatMap((exit) => Effect.sync(() => settle(entry, awaiter, exit, updatedAt?.() ?? now(entry))))));
353
- return awaiter;
354
- }
355
- function settle(entry, awaiter, exit, updatedAt) {
356
- if (entry.inFlight !== awaiter) return;
357
- entry.inFlight = void 0;
358
- if (Exit.isSuccess(exit)) {
359
- const data = succeed(entry, exit.value, updatedAt);
360
- Deferred.doneUnsafe(awaiter, Exit.succeed(data));
361
- } else if (Cause.hasInterruptsOnly(exit.cause)) {
362
- set(entry, {
363
- fetchDirection: void 0,
364
- isFetching: false
365
- });
366
- Deferred.doneUnsafe(awaiter, Exit.fail(new CancelledError()));
367
- } else {
368
- fail(entry, Cause.squash(exit.cause));
369
- Deferred.doneUnsafe(awaiter, exit);
370
- }
171
+ run.done.resolve(outcome);
371
172
  scheduleGc(entry);
372
173
  }
373
- /**
374
- * The last reader left while `awaiter`'s request was in flight. A refetch
375
- * whose work an interrupt cancels (`signalRead`) is interrupted, and the
376
- * entry goes back to its state before the request; its awaiters get the
377
- * cached data. Any other request finishes, so its result is cached, but
378
- * runs no more retries.
379
- */
380
- function unobserved(entry, awaiter) {
381
- if (isObserved(entry) || entry.inFlight !== awaiter) return;
382
- if (!entry.signalRead || !entry.state.hasData) {
383
- entry.retriesStopped = true;
174
+ function isAsyncIterable(value) {
175
+ return isObject(value) && Symbol.asyncIterator in value;
176
+ }
177
+ async function pump(entry, run, chunks, stream) {
178
+ let data = stream.initial;
179
+ try {
180
+ for await (const chunk of chunks) {
181
+ if (entry.run !== run) return;
182
+ data = stream.reduce(data, chunk);
183
+ write(entry, data, entry.client.clock.now(), run.startedAt);
184
+ if (!run.settled) {
185
+ run.settled = true;
186
+ run.done.resolve({ ok: true });
187
+ }
188
+ }
189
+ } catch (error) {
190
+ if (entry.run === run) {
191
+ failWith(entry, error);
192
+ finish(entry, run, {
193
+ error,
194
+ ok: false
195
+ });
196
+ }
384
197
  return;
385
198
  }
386
- entry.inFlight = void 0;
387
- entry.tasks.cancel("request");
388
- set(entry, {
389
- failureCount: entry.failureCountBefore,
390
- fetchDirection: void 0,
391
- isFetching: false
392
- });
393
- Deferred.doneUnsafe(awaiter, Exit.succeed(entry.state.data));
394
- scheduleGc(entry);
199
+ if (entry.run === run) {
200
+ if (!run.settled) write(entry, data, entry.client.clock.now(), run.startedAt);
201
+ finish(entry, run, { ok: true });
202
+ }
395
203
  }
396
- function succeed(entry, next, updatedAt) {
397
- const previous = entry.state;
398
- const data = previous.hasData && behavior(entry, "structuralSharing") !== false ? replaceEqualDeep(previous.data, next) : next;
399
- const firstLoad = entry.pending;
400
- entry.pending = void 0;
401
- firstLoad?.resolve(data);
402
- const unchanged = previous.hasData && data === previous.data;
403
- const isFetching = entry.inFlight !== void 0;
404
- set(entry, {
405
- data,
406
- error: null,
407
- failureCount: 0,
408
- fetchDirection: isFetching ? previous.fetchDirection : void 0,
409
- hasData: true,
410
- isFetching,
411
- isInvalidated: false,
412
- promise: firstLoad?.promise ?? (unchanged ? previous.promise : fulfilled(data)),
413
- status: "success",
414
- updatedAt
204
+ function terminal(op) {
205
+ return new Promise((resolve) => {
206
+ const options = op.options;
207
+ const paged = options["~run"];
208
+ resolve(paged ? paged(op) : options.queryFn({
209
+ meta: op.meta,
210
+ queryKey: op.queryKey,
211
+ signal: op.signal
212
+ }));
415
213
  });
416
- return data;
417
214
  }
418
- function fail(entry, error) {
419
- emit(entry.cache, {
420
- error,
421
- query: entry,
422
- type: "failed"
423
- });
424
- if (!entry.state.hasData) {
425
- const firstLoad = entry.pending;
426
- entry.pending = void 0;
427
- firstLoad?.reject(error);
428
- entry.errorShown = false;
429
- }
430
- set(entry, {
431
- error,
432
- fetchDirection: void 0,
433
- isFetching: false,
434
- status: "error"
435
- });
215
+ function chain(op) {
216
+ const links = op.client.config.links ?? [];
217
+ const call = (index, current) => {
218
+ const link = links[index];
219
+ return link ? link(current, (next) => call(index + 1, next)) : terminal(current);
220
+ };
221
+ return call(0, op);
436
222
  }
437
223
  /**
438
- * A failed first load rejects its promise, so the reader throws to an error
439
- * boundary. Every read in that same task (React retries a throwing render
440
- * once, StrictMode renders twice) gets the rejection too; a read in a later
441
- * task — the boundary was reset — starts a new request.
224
+ * Run the entry's request, or join the one in flight. Resolves with how it
225
+ * ended, never rejects: a removed entry's request ends with an `AbortError`.
226
+ * Whoever waited on a restarted request gets the new one's outcome.
442
227
  */
443
- function markErrorShown(entry) {
444
- entry.tasks.fork("errorShown", Effect.yieldNow.pipe(Effect.andThen(Effect.sync(() => {
445
- entry.errorShown = true;
446
- }))), { onlyIfMissing: true });
447
- }
448
- function requestEffect(entry, options, request) {
449
- const meta = metaOf(entry);
450
- const queryKey = options.queryKey;
451
- if (options.kind === "query") return attempt(entry, (readSignal) => options.queryFn({
452
- meta,
453
- queryKey,
454
- get signal() {
455
- return readSignal();
228
+ function fetchEntry(entry, request = {}) {
229
+ const current = entry.run;
230
+ if (current && !request.restart && request.direction === void 0) return current.done.promise;
231
+ const options = entry.options;
232
+ if (!options) return Promise.resolve({ ok: true });
233
+ current?.controller.abort();
234
+ const client = entry.client;
235
+ const run = {
236
+ controller: new AbortController(),
237
+ done: current && !current.settled ? current.done : Promise.withResolvers(),
238
+ settled: false,
239
+ startedAt: ++client["~seq"]
240
+ };
241
+ entry.run = run;
242
+ if (!entry.state.hasData && !entry.pendingThenable) newPending(entry);
243
+ setState(entry, {
244
+ fetchingDirection: request.direction,
245
+ isFetching: true
246
+ });
247
+ const state = entry.state;
248
+ chain({
249
+ client,
250
+ data: state.data,
251
+ direction: request.direction,
252
+ hasData: state.hasData,
253
+ hash: entry.hash,
254
+ input: entry.input,
255
+ meta: options.meta ?? {},
256
+ options,
257
+ pages: request.pages,
258
+ queryKey: entry.key,
259
+ signal: run.controller.signal
260
+ }).then((result) => {
261
+ if (entry.run !== run) return;
262
+ if (options.stream && isAsyncIterable(result)) {
263
+ pump(entry, run, result, options.stream);
264
+ return;
265
+ }
266
+ write(entry, result, client.clock.now(), run.startedAt);
267
+ finish(entry, run, { ok: true });
268
+ }, (error) => {
269
+ if (entry.run !== run) return;
270
+ failWith(entry, error);
271
+ finish(entry, run, {
272
+ error,
273
+ ok: false
274
+ });
275
+ });
276
+ return run.done.promise;
277
+ }
278
+ /** Take over a request started elsewhere (hydration of a pending entry). */
279
+ function adopt(entry, promise) {
280
+ const run = {
281
+ controller: new AbortController(),
282
+ done: Promise.withResolvers(),
283
+ settled: false,
284
+ startedAt: entry.client["~seq"]
285
+ };
286
+ entry.run = run;
287
+ if (!entry.state.hasData && !entry.pendingThenable) newPending(entry);
288
+ setState(entry, { isFetching: true });
289
+ Promise.resolve(promise).then((sent) => {
290
+ if (entry.run === run) {
291
+ write(entry, sent.data, sent.updatedAt, run.startedAt);
292
+ finish(entry, run, { ok: true });
456
293
  }
457
- }));
458
- const pagesOf = () => isInfiniteData(entry.state.data) ? entry.state.data : void 0;
459
- return fetchPages(options, pagesOf(), request, (pageParam, direction) => attempt(entry, (readSignal) => options.queryFn({
460
- direction,
461
- meta,
462
- pageParam,
463
- queryKey,
464
- get signal() {
465
- return readSignal();
294
+ }, (error) => {
295
+ if (entry.run === run) {
296
+ failWith(entry, error);
297
+ finish(entry, run, {
298
+ error,
299
+ ok: false
300
+ });
466
301
  }
467
- })), pagesOf);
302
+ });
468
303
  }
469
- /**
470
- * One `queryFn` call, retried per the query's policy. Every failure counts.
471
- * `call` reads the signal through `readSignal`, which marks the entry's
472
- * `signalRead`. Its services are erased with its data type; `runRequest`
473
- * provides them.
474
- */
475
- function attempt(entry, call) {
476
- const once = fromQueryFn((signal) => {
477
- const result = call(() => {
478
- entry.signalRead = true;
479
- return signal;
304
+ /** Subscribe to an entry's state. Subscribers are its readers: an entry with none can be gc'd. */
305
+ function subscribeEntry(entry, listener, options) {
306
+ return entry.readers.subscribe(listener, options);
307
+ }
308
+ /** Drop the entry, aborting its request. Waiters get an `AbortError`. */
309
+ function remove(entry) {
310
+ const client = entry.client;
311
+ if (client["~entries"].get(entry.hash) !== entry) return;
312
+ client["~entries"].delete(entry.hash);
313
+ client.clock.clearTimeout(entry.gcTimer);
314
+ const run = entry.run;
315
+ entry.run = void 0;
316
+ if (run) {
317
+ run.controller.abort();
318
+ run.done.resolve({
319
+ error: abortError(),
320
+ ok: false
480
321
  });
481
- entry.returnsEffect = Effect.isEffect(result);
482
- if (entry.returnsEffect) entry.signalRead = true;
483
- return result;
484
- }).pipe(Effect.tapError(() => Effect.sync(() => set(entry, { failureCount: entry.state.failureCount + 1 }))));
485
- const schedule = retrySchedule({
486
- retry: behavior(entry, "retry"),
487
- retryDelay: behavior(entry, "retryDelay")
488
- }, entry.cache.isServer);
489
- if (!schedule) return once;
490
- const online = entry.cache.isServer ? Effect.void : whenOnline;
491
- return Effect.suspend(() => {
492
- let retrying = false;
493
- let lastError;
494
- const unlessStopped = (next) => Effect.suspend(() => entry.retriesStopped ? Effect.fail(lastError) : next);
495
- const gated = Effect.suspend(() => {
496
- if (!retrying) {
497
- retrying = true;
498
- return once;
499
- }
500
- return unlessStopped(Effect.andThen(online, unlessStopped(once)));
501
- }).pipe(Effect.tapError((error) => Effect.sync(() => {
502
- lastError = error;
503
- })));
504
- return Effect.retry(gated, Schedule.while(schedule, () => !entry.retriesStopped));
322
+ }
323
+ client["~events"].emit({
324
+ entry,
325
+ type: "removed"
505
326
  });
506
327
  }
507
- function scheduleGc(entry) {
508
- const gcTime = behavior(entry, "gcTime") ?? (entry.cache.isServer ? Infinity : 3e5);
509
- if (isObserved(entry) || entry.inFlight || !Number.isFinite(gcTime)) {
510
- entry.tasks.cancel("gc");
511
- return;
512
- }
513
- entry.tasks.fork("gc", Effect.sleep(gcTime).pipe(Effect.andThen(Effect.sync(() => remove(entry)))));
328
+ /** Drop data and errors. Read entries (or ones loading) refetch; resolves when they settle. */
329
+ function reset(entry) {
330
+ entry.state = initialState;
331
+ newPending(entry);
332
+ notify(entry);
333
+ if (entry.readers.size > 0 || entry.run) return fetchEntry(entry, { restart: true });
334
+ return Promise.resolve({ ok: true });
514
335
  }
515
- function scheduleStale(entry) {
516
- const state = entry.state;
517
- const staleTime = behavior(entry, "staleTime") ?? 0;
518
- if (entry.cache.isServer || !isObserved(entry) || state.isStale || !Number.isFinite(staleTime)) {
519
- entry.tasks.cancel("stale");
520
- return;
336
+ /**
337
+ * Record an invalidation: one stamp per target, all at one point in the
338
+ * sequence. Entries they cover that have readers refetch now, once however
339
+ * many stamps cover them; the rest find out when they are next read.
340
+ * Resolves when the refetches settle.
341
+ */
342
+ function invalidate(client, makeStamps) {
343
+ const stamps = makeStamps(++client["~seq"]);
344
+ const entries = [...client["~entries"].values()];
345
+ const oldest = Math.min(...entries.map((entry) => entry.requestedAt));
346
+ client["~stamps"] = [...client["~stamps"].filter((kept) => kept.at > oldest), ...stamps];
347
+ const refetches = [];
348
+ for (const entry of entries) if (stamps.some((stamp) => matches(entry, stamp))) {
349
+ notify(entry);
350
+ if (entry.readers.size > 0) refetches.push(fetchEntry(entry, { restart: true }));
521
351
  }
522
- const remaining = state.updatedAt + staleTime - now(entry);
523
- entry.tasks.fork("stale", Effect.sleep(Math.max(remaining, 0) + 1).pipe(Effect.andThen(Effect.sync(() => set(entry, {})))));
352
+ return Promise.all(refetches).then(() => {});
524
353
  }
525
354
  //#endregion
526
- export { CancelledError, DisabledError, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isDisabled, isObserved, isQuery, isStaleUnsafe, make, metaOf, needsRefetch, onPlatformEvent, provide, readUnsafe, remove, reset, resume, revalidate, setData, setOptions, start, subscribeUnsafe };
355
+ export { abortError, adopt, entryForKey, entryOf, fetchEntry, invalidate, isInvalidated, isStale, remove, reset, subscribeEntry, write };