@raf-query/core 0.11.1 → 0.13.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 (73) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +28 -18
  3. package/dist/client.d.mts +163 -119
  4. package/dist/client.mjs +90 -162
  5. package/dist/entry.d.mts +73 -147
  6. package/dist/entry.mjs +314 -471
  7. package/dist/features/focus-refetch/focus-refetch.d.mts +17 -0
  8. package/dist/features/focus-refetch/focus-refetch.mjs +20 -0
  9. package/dist/features/polling/polling.d.mts +16 -0
  10. package/dist/features/polling/polling.mjs +39 -0
  11. package/dist/features/reconnect-refetch/reconnect-refetch.d.mts +17 -0
  12. package/dist/features/reconnect-refetch/reconnect-refetch.mjs +16 -0
  13. package/dist/features/shared.mjs +13 -0
  14. package/dist/guards.mjs +23 -0
  15. package/dist/hydration/hydration.d.mts +108 -0
  16. package/dist/hydration/hydration.mjs +142 -0
  17. package/dist/index.d.mts +19 -13
  18. package/dist/index.mjs +16 -13
  19. package/dist/infinite/infinite.d.mts +83 -0
  20. package/dist/infinite/infinite.mjs +120 -0
  21. package/dist/infinite/paging.d.mts +71 -0
  22. package/dist/infinite/paging.mjs +62 -0
  23. package/dist/internal.d.mts +13 -0
  24. package/dist/internal.mjs +7 -0
  25. package/dist/keys/keys.d.mts +54 -0
  26. package/dist/keys/keys.mjs +53 -0
  27. package/dist/keys/targets.d.mts +62 -0
  28. package/dist/keys/targets.mjs +85 -0
  29. package/dist/links/retry/retry.d.mts +33 -0
  30. package/dist/links/retry/retry.mjs +53 -0
  31. package/dist/links/timeout/timeout.d.mts +17 -0
  32. package/dist/links/timeout/timeout.mjs +35 -0
  33. package/dist/load.d.mts +149 -0
  34. package/dist/load.mjs +179 -0
  35. package/dist/options.d.mts +161 -332
  36. package/dist/options.mjs +28 -40
  37. package/dist/presets.d.mts +35 -0
  38. package/dist/presets.mjs +27 -0
  39. package/dist/root.d.mts +86 -0
  40. package/dist/structural-sharing/structural-sharing.mjs +32 -0
  41. package/dist/subscribe.d.mts +15 -0
  42. package/dist/subscribe.mjs +29 -0
  43. package/dist/thenable.d.mts +15 -17
  44. package/dist/thenable.mjs +25 -6
  45. package/dist/verbs.d.mts +150 -0
  46. package/dist/verbs.mjs +115 -0
  47. package/package.json +13 -9
  48. package/dist/_virtual/_rolldown/runtime.mjs +0 -13
  49. package/dist/all-keyed.d.mts +0 -18
  50. package/dist/all-keyed.mjs +0 -19
  51. package/dist/binding.d.mts +0 -92
  52. package/dist/binding.mjs +0 -186
  53. package/dist/cache.d.mts +0 -164
  54. package/dist/cache.mjs +0 -184
  55. package/dist/controls.d.mts +0 -95
  56. package/dist/controls.mjs +0 -70
  57. package/dist/hydration.d.mts +0 -84
  58. package/dist/hydration.mjs +0 -148
  59. package/dist/infinite.d.mts +0 -5
  60. package/dist/infinite.mjs +0 -103
  61. package/dist/key.d.mts +0 -18
  62. package/dist/key.mjs +0 -36
  63. package/dist/listen.mjs +0 -19
  64. package/dist/notify.mjs +0 -21
  65. package/dist/observer.d.mts +0 -110
  66. package/dist/observer.mjs +0 -300
  67. package/dist/platform.d.mts +0 -27
  68. package/dist/platform.mjs +0 -44
  69. package/dist/query.d.mts +0 -137
  70. package/dist/query.mjs +0 -147
  71. package/dist/request.mjs +0 -47
  72. package/dist/retry.mjs +0 -25
  73. package/dist/share.mjs +0 -40
package/dist/entry.mjs CHANGED
@@ -1,526 +1,369 @@
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
  };
18
+ const fiveMinutes = 3e5;
19
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.
20
+ * Make the `AbortError` (a `DOMException`) that a `load` waiting on a removed query rejects with.
21
+ *
22
+ * @see https://raf-query.katt.dev/docs/query-cancellation#when-requests-are-aborted
22
23
  */
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);
24
+ function abortError() {
25
+ return new DOMException("The query was cancelled", "AbortError");
26
+ }
27
+ function notify(entry) {
28
+ const client = entry.client;
29
+ const changed = client["~changed"];
30
+ if (changed.size === 0) queueMicrotask(() => {
31
+ const batch = [...changed];
32
+ changed.clear();
33
+ for (const item of batch) {
34
+ item.readers.emit(item.state);
35
+ client["~events"].emit({
36
+ entry: item,
37
+ type: "updated"
38
+ });
39
+ }
40
+ });
41
+ changed.add(entry);
38
42
  }
39
- function entryOf(query) {
40
- return query;
43
+ function setState(entry, patch) {
44
+ entry.state = {
45
+ ...entry.state,
46
+ ...patch
47
+ };
48
+ notify(entry);
49
+ }
50
+ function newPending(entry) {
51
+ const next = pending();
52
+ entry.pendingThenable = next;
53
+ entry.thenable = next.promise;
41
54
  }
42
- function emit(cache, event) {
43
- for (const listener of cache.listeners) listener(event);
55
+ function gcTime(entry) {
56
+ return entry.options?.gcTime ?? entry.client.config.gcTime ?? fiveMinutes;
44
57
  }
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);
58
+ function scheduleGc(entry) {
59
+ const client = entry.client;
60
+ client.clock.clearTimeout(entry.gcTimer);
61
+ entry.gcTimer = void 0;
62
+ const time = gcTime(entry);
63
+ if (client.isServer || entry.readers.size > 0 || !Number.isFinite(time)) return;
64
+ entry.gcTimer = client.clock.setTimeout(() => {
65
+ entry.gcTimer = void 0;
66
+ if (entry.run) {
67
+ entry.run.done.promise.then(() => scheduleGc(entry));
68
+ return;
57
69
  }
58
- };
70
+ remove(entry);
71
+ }, time);
59
72
  }
60
- function make(cache, key, hash) {
61
- const scope = Scope.forkUnsafe(cache.scope);
73
+ function create(client, key, hash) {
62
74
  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
- },
75
+ const entry = {
76
+ client,
77
+ factory: void 0,
78
+ gcTimer: void 0,
71
79
  hash,
72
- hydratedMeta: void 0,
73
- inFlight: void 0,
80
+ input: void 0,
74
81
  key,
75
- listeners: /* @__PURE__ */ new Map(),
76
82
  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()
83
+ pendingThenable: first,
84
+ readers: listeners((size) => {
85
+ client["~events"].emit({
86
+ entry,
87
+ type: "readers"
88
+ });
89
+ if (size === 0) scheduleGc(entry);
90
+ else {
91
+ client.clock.clearTimeout(entry.gcTimer);
92
+ entry.gcTimer = void 0;
93
+ }
94
+ }),
95
+ requestedAt: client["~seq"],
96
+ run: void 0,
97
+ state: initialState,
98
+ thenable: first.promise
103
99
  };
104
- Object.assign(entry, { commands });
100
+ client["~entries"].set(hash, entry);
101
+ client["~events"].emit({
102
+ entry,
103
+ type: "added"
104
+ });
105
105
  scheduleGc(entry);
106
106
  return entry;
107
107
  }
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
- });
108
+ /** The entry for a key, created if missing. Hydration uses it. */
109
+ function entryForKey(client, key) {
110
+ const hash = hashKey(key);
111
+ return client["~entries"].get(hash) ?? create(client, key, hash);
172
112
  }
173
113
  /**
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.
114
+ * The entry for a factory's input, created if missing, with the factory's
115
+ * current options. Every read goes through here, which links an entry that
116
+ * was only hydrated to its factory.
177
117
  */
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) {
118
+ function entryOf(client, factory, input) {
119
+ const options = optionsOf(factory, input);
120
+ const entry = entryForKey(client, options.queryKey);
121
+ entry.factory = factory;
122
+ entry.input = input;
183
123
  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;
124
+ scheduleGc(entry);
125
+ return entry;
200
126
  }
201
- /** Start a request unless one is in flight, without waiting for it. */
202
- function start(entry) {
203
- begin(entry, {});
127
+ /** Whether an invalidation newer than the entry's data covers it. */
128
+ function isInvalidated(entry) {
129
+ return entry.client["~stamps"].some((stamp) => stamp.at > entry.requestedAt && matches(entry, stamp));
204
130
  }
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);
131
+ /** Whether the entry is a browser-only query (`ssr: false`) on a server client: nothing starts it there. */
132
+ function isBrowserOnly(entry) {
133
+ return entry.client.isServer && entry.options?.ssr === false;
212
134
  }
213
- /** Background refetch of cached data that was asked for again. */
214
- function revalidate(entry) {
135
+ /** Whether the entry's data should be refetched before it's trusted. */
136
+ function isStale(entry) {
215
137
  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
- });
224
- }
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
- });
138
+ if (!state.hasData) return true;
139
+ const staleTime = entry.options?.staleTime ?? entry.client.config.staleTime ?? 0;
140
+ return entry.client.clock.now() - state.updatedAt >= staleTime || isInvalidated(entry);
231
141
  }
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"
142
+ /** Write data. `startedAt` orders it against invalidations. */
143
+ function write(entry, data, updatedAt, startedAt) {
144
+ const previous = entry.state;
145
+ const next = (entry.options?.structuralSharing ?? entry.client.config.structuralSharing ?? true) && previous.hasData ? replaceEqualDeep(previous.data, data) : data;
146
+ entry.requestedAt = Math.max(entry.requestedAt, startedAt);
147
+ if (entry.pendingThenable) {
148
+ entry.pendingThenable.resolve(next);
149
+ entry.pendingThenable = void 0;
150
+ } else if (!previous.hasData || next !== previous.data) entry.thenable = fulfilled(next);
151
+ setState(entry, {
152
+ data: next,
153
+ error: void 0,
154
+ hasData: true,
155
+ status: "success",
156
+ updatedAt
286
157
  });
158
+ scheduleGc(entry);
287
159
  }
288
160
  /**
289
- * Start a request now, or join the one in flight. The returned Effect
290
- * waits for its result.
161
+ * Record a failed request. Data the entry had stays readable. Without data,
162
+ * the request in flight has a pending thenable for readers: it rejects.
291
163
  */
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);
164
+ function failWith(entry, error) {
165
+ if (!entry.state.hasData) {
166
+ entry.pendingThenable?.reject(error);
167
+ entry.pendingThenable = void 0;
309
168
  }
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"
169
+ setState(entry, {
170
+ error,
171
+ status: "error"
326
172
  });
327
- scheduleStale(entry);
328
173
  }
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
174
+ function finish(entry, run, outcome) {
175
+ entry.run = void 0;
176
+ setState(entry, {
177
+ fetchingDirection: void 0,
178
+ isFetching: false
351
179
  });
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
- }
180
+ run.done.resolve(outcome);
371
181
  scheduleGc(entry);
372
182
  }
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;
183
+ function isAsyncIterable(value) {
184
+ return isObject(value) && Symbol.asyncIterator in value;
185
+ }
186
+ async function pump(entry, run, chunks, stream) {
187
+ let data = stream.initial;
188
+ try {
189
+ for await (const chunk of chunks) {
190
+ if (entry.run !== run) return;
191
+ data = stream.reduce(data, chunk);
192
+ write(entry, data, entry.client.clock.now(), run.startedAt);
193
+ if (!run.settled) {
194
+ run.settled = true;
195
+ run.done.resolve({ ok: true });
196
+ }
197
+ }
198
+ } catch (error) {
199
+ if (entry.run === run) {
200
+ failWith(entry, error);
201
+ finish(entry, run, {
202
+ error,
203
+ ok: false
204
+ });
205
+ }
384
206
  return;
385
207
  }
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);
208
+ if (entry.run === run) {
209
+ if (!run.settled) write(entry, data, entry.client.clock.now(), run.startedAt);
210
+ finish(entry, run, { ok: true });
211
+ }
395
212
  }
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
213
+ function terminal(op) {
214
+ return new Promise((resolve) => {
215
+ const options = op.options;
216
+ const paged = options["~run"];
217
+ resolve(paged ? paged(op) : options.queryFn({
218
+ client: op.client,
219
+ meta: op.meta,
220
+ queryKey: op.queryKey,
221
+ signal: op.signal
222
+ }));
415
223
  });
416
- return data;
417
224
  }
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
- });
225
+ function chain(op) {
226
+ const links = op.client.config.links ?? [];
227
+ const call = (index, current) => {
228
+ const link = links[index];
229
+ return link ? link(current, (next) => call(index + 1, next)) : terminal(current);
230
+ };
231
+ return call(0, op);
436
232
  }
437
233
  /**
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.
234
+ * Run the entry's request, or join the one in flight. Resolves with how it
235
+ * ended, never rejects: a removed entry's request ends with an `AbortError`.
236
+ * Whoever waited on a restarted request gets the new one's outcome.
442
237
  */
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();
238
+ function fetchEntry(entry, request = {}) {
239
+ const current = entry.run;
240
+ if (current && !request.restart && request.direction === void 0) return current.done.promise;
241
+ const options = entry.options;
242
+ if (!options || isBrowserOnly(entry)) return Promise.resolve({ ok: true });
243
+ current?.controller.abort();
244
+ const client = entry.client;
245
+ const run = {
246
+ controller: new AbortController(),
247
+ done: current && !current.settled ? current.done : Promise.withResolvers(),
248
+ settled: false,
249
+ startedAt: ++client["~seq"]
250
+ };
251
+ entry.run = run;
252
+ if (!entry.state.hasData && !entry.pendingThenable) newPending(entry);
253
+ setState(entry, {
254
+ fetchingDirection: request.direction,
255
+ isFetching: true
256
+ });
257
+ const state = entry.state;
258
+ chain({
259
+ client,
260
+ data: state.data,
261
+ direction: request.direction,
262
+ hasData: state.hasData,
263
+ hash: entry.hash,
264
+ input: entry.input,
265
+ meta: options.meta ?? {},
266
+ options,
267
+ pages: request.pages,
268
+ queryKey: entry.key,
269
+ signal: run.controller.signal
270
+ }).then((result) => {
271
+ if (entry.run !== run) return;
272
+ if (options.stream && isAsyncIterable(result)) {
273
+ pump(entry, run, result, options.stream);
274
+ return;
456
275
  }
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();
276
+ if (!options["~run"] && entry.requestedAt > run.startedAt) {
277
+ finish(entry, run, { ok: true });
278
+ return;
466
279
  }
467
- })), pagesOf);
468
- }
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;
280
+ write(entry, result, client.clock.now(), run.startedAt);
281
+ finish(entry, run, { ok: true });
282
+ }, (error) => {
283
+ if (entry.run !== run) return;
284
+ failWith(entry, error);
285
+ finish(entry, run, {
286
+ error,
287
+ ok: false
480
288
  });
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));
289
+ });
290
+ return run.done.promise;
291
+ }
292
+ /** Take over a request started elsewhere (hydration of a pending entry). */
293
+ function adopt(entry, promise) {
294
+ const run = {
295
+ controller: new AbortController(),
296
+ done: Promise.withResolvers(),
297
+ settled: false,
298
+ startedAt: entry.client["~seq"]
299
+ };
300
+ entry.run = run;
301
+ if (!entry.state.hasData && !entry.pendingThenable) newPending(entry);
302
+ setState(entry, { isFetching: true });
303
+ Promise.resolve(promise).then((sent) => {
304
+ if (entry.run === run) {
305
+ write(entry, sent.data, sent.updatedAt, run.startedAt);
306
+ finish(entry, run, { ok: true });
307
+ }
308
+ }, (error) => {
309
+ if (entry.run === run) {
310
+ failWith(entry, error);
311
+ finish(entry, run, {
312
+ error,
313
+ ok: false
314
+ });
315
+ }
505
316
  });
506
317
  }
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;
318
+ /** Subscribe to an entry's state. Subscribers are its readers: an entry with none can be gc'd. */
319
+ function subscribeEntry(entry, listener, options) {
320
+ return entry.readers.subscribe(listener, options);
321
+ }
322
+ /** Drop the entry, aborting its request. Waiters get an `AbortError`. */
323
+ function remove(entry) {
324
+ const client = entry.client;
325
+ if (client["~entries"].get(entry.hash) !== entry) return;
326
+ client["~entries"].delete(entry.hash);
327
+ client.clock.clearTimeout(entry.gcTimer);
328
+ const run = entry.run;
329
+ entry.run = void 0;
330
+ if (run) {
331
+ run.controller.abort();
332
+ run.done.resolve({
333
+ error: abortError(),
334
+ ok: false
335
+ });
512
336
  }
513
- entry.tasks.fork("gc", Effect.sleep(gcTime).pipe(Effect.andThen(Effect.sync(() => remove(entry)))));
337
+ client["~events"].emit({
338
+ entry,
339
+ type: "removed"
340
+ });
514
341
  }
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;
342
+ /** Drop data and errors. Read entries (or ones loading) refetch; resolves when they settle. */
343
+ function reset(entry) {
344
+ entry.state = initialState;
345
+ newPending(entry);
346
+ notify(entry);
347
+ if (entry.readers.size > 0 || entry.run) return fetchEntry(entry, { restart: true });
348
+ return Promise.resolve({ ok: true });
349
+ }
350
+ /**
351
+ * Record an invalidation: one stamp per target, all at one point in the
352
+ * sequence. Entries they cover that have readers refetch now, once however
353
+ * many stamps cover them; the rest find out when they are next read.
354
+ * Resolves when the refetches settle.
355
+ */
356
+ function invalidate(client, makeStamps) {
357
+ const stamps = makeStamps(++client["~seq"]);
358
+ const entries = [...client["~entries"].values()];
359
+ const oldest = Math.min(...entries.map((entry) => entry.requestedAt));
360
+ client["~stamps"] = [...client["~stamps"].filter((kept) => kept.at > oldest), ...stamps];
361
+ const refetches = [];
362
+ for (const entry of entries) if (stamps.some((stamp) => matches(entry, stamp))) {
363
+ notify(entry);
364
+ if (entry.readers.size > 0) refetches.push(fetchEntry(entry, { restart: true }));
521
365
  }
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, {})))));
366
+ return Promise.all(refetches).then(() => {});
524
367
  }
525
368
  //#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 };
369
+ export { abortError, adopt, entryForKey, entryOf, fetchEntry, invalidate, isBrowserOnly, isInvalidated, isStale, remove, reset, subscribeEntry, write };