@raf-query/core 0.1.2 → 0.1.3
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/CHANGELOG.md +7 -1
- package/README.md +2 -0
- package/dist/entry.d.mts +9 -0
- package/dist/entry.mjs +63 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.1.3](https://github.com/KATT/raf-query/compare/0.1.2...0.1.3) (2026-10-12)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
* **core:** cancel requests nobody reads anymore ([#27](https://github.com/KATT/raf-query/issues/27)) ([c3c1e4b](https://github.com/KATT/raf-query/commit/c3c1e4bb9250164b6df17d7d4b7bd4043f908961))
|
|
8
|
+
|
|
3
9
|
## [0.1.2](https://github.com/KATT/raf-query/compare/0.1.1...0.1.2) (2026-10-12)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
- add isFetching(filter?) and useIsFetching(filter?) ([#25](https://github.com/KATT/raf-query/issues/25)) ([70c3b98](https://github.com/KATT/raf-query/commit/70c3b981027f717b1d619d5a2b23822f685c032a))
|
|
8
14
|
|
|
9
15
|
## [0.1.1](https://github.com/KATT/raf-query/compare/0.1.0...0.1.1) (2026-10-12)
|
|
10
16
|
|
package/README.md
CHANGED
|
@@ -644,6 +644,8 @@ Query options, accepted by `queryOptions`, `infiniteQueryOptions` and `config.de
|
|
|
644
644
|
|
|
645
645
|
In the browser, retries pause while `navigator.onLine` is `false`: after the retry delay, the next attempt waits for the window's `online` event. The request stays fetching meanwhile, and cancelling it (`remove`, `clear`, a restart) stops the wait. The first attempt runs regardless.
|
|
646
646
|
|
|
647
|
+
When the last reader of a query unsubscribes during a background refetch, a `queryFn` that read its `signal` (or returned an Effect) is aborted: the entry keeps its data and goes back to how it was before the request, and `fetch` callers waiting on it get the cached data. Other requests, including every first load (a render may be suspended on it), finish so their result is cached, but don't retry unless a reader comes back. Requests nothing has subscribed to yet, like a loader's `prefetch`, keep running.
|
|
648
|
+
|
|
647
649
|
`@raf-query/core`:
|
|
648
650
|
|
|
649
651
|
| Export | Use |
|
package/dist/entry.d.mts
CHANGED
|
@@ -54,6 +54,8 @@ export interface Entry extends Query {
|
|
|
54
54
|
readonly commands: Commands;
|
|
55
55
|
/** A failed first load was shown; the next read retries it. */
|
|
56
56
|
errorShown: boolean;
|
|
57
|
+
/** `failureCount` before the request in flight, restored if it's cancelled. */
|
|
58
|
+
failureCountBefore: number;
|
|
57
59
|
handle: QueryHandle;
|
|
58
60
|
/** Awaited by every caller of the request in flight; outlives restarts. */
|
|
59
61
|
inFlight: Deferred.Deferred<unknown, unknown> | undefined;
|
|
@@ -61,6 +63,8 @@ export interface Entry extends Query {
|
|
|
61
63
|
options: AnyQueryOptions | undefined;
|
|
62
64
|
/** The first load's promise, until it settles. */
|
|
63
65
|
pending: Pending<unknown> | undefined;
|
|
66
|
+
/** The last reader left during the request in flight: it retries no more. */
|
|
67
|
+
retriesStopped: boolean;
|
|
64
68
|
/** Open while the entry is cached; owns its fibers. */
|
|
65
69
|
readonly scope: Scope.Closeable;
|
|
66
70
|
/**
|
|
@@ -68,6 +72,11 @@ export interface Entry extends Query {
|
|
|
68
72
|
* latest verb that called for this entry ({@link provide}).
|
|
69
73
|
*/
|
|
70
74
|
services: Context.Context<never>;
|
|
75
|
+
/**
|
|
76
|
+
* The request in flight read its `signal`, or returned an Effect, so
|
|
77
|
+
* interrupting it cancels its work (TanStack's `abortSignalConsumed`).
|
|
78
|
+
*/
|
|
79
|
+
signalRead: boolean;
|
|
71
80
|
state: State<unknown>;
|
|
72
81
|
readonly tasks: Tasks;
|
|
73
82
|
}
|
package/dist/entry.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { fromQueryFn, requestServices, runRequest } from "./request.mjs";
|
|
|
7
7
|
import { retrySchedule } from "./retry.mjs";
|
|
8
8
|
import { replaceEqualDeep } from "./share.mjs";
|
|
9
9
|
import { fulfilled, pending } from "./thenable.mjs";
|
|
10
|
-
import { Cause, Data, Deferred, Effect, Exit, FiberMap, Pipeable, Predicate, Scope } from "effect";
|
|
10
|
+
import { Cause, Data, Deferred, Effect, Exit, FiberMap, Pipeable, Predicate, Schedule, Scope } from "effect";
|
|
11
11
|
//#region src/entry.ts
|
|
12
12
|
const TypeId = "~@raf-query/core/Query";
|
|
13
13
|
/**
|
|
@@ -55,6 +55,7 @@ function make(cache, key, hash) {
|
|
|
55
55
|
const entry = Object.assign(Object.create(Proto), {
|
|
56
56
|
cache,
|
|
57
57
|
errorShown: false,
|
|
58
|
+
failureCountBefore: 0,
|
|
58
59
|
handle: {
|
|
59
60
|
input: void 0,
|
|
60
61
|
key
|
|
@@ -65,8 +66,10 @@ function make(cache, key, hash) {
|
|
|
65
66
|
listeners: /* @__PURE__ */ new Map(),
|
|
66
67
|
options: void 0,
|
|
67
68
|
pending: first,
|
|
69
|
+
retriesStopped: false,
|
|
68
70
|
scope,
|
|
69
71
|
services: cache.services,
|
|
72
|
+
signalRead: false,
|
|
70
73
|
state: {
|
|
71
74
|
data: void 0,
|
|
72
75
|
error: null,
|
|
@@ -121,6 +124,7 @@ function hasPreviousPageUnsafe(entry) {
|
|
|
121
124
|
}
|
|
122
125
|
function subscribeUnsafe(entry, listener, options = defaultSubscribeOptions) {
|
|
123
126
|
entry.listeners.set(listener, options);
|
|
127
|
+
entry.retriesStopped = false;
|
|
124
128
|
entry.tasks.cancel("gc");
|
|
125
129
|
scheduleStale(entry);
|
|
126
130
|
emit(entry.cache, {
|
|
@@ -132,6 +136,8 @@ function subscribeUnsafe(entry, listener, options = defaultSubscribeOptions) {
|
|
|
132
136
|
if (entry.listeners.size === 0) {
|
|
133
137
|
entry.tasks.cancel("stale");
|
|
134
138
|
scheduleGc(entry);
|
|
139
|
+
const inFlight = entry.inFlight;
|
|
140
|
+
if (inFlight) scheduleNotify(() => unobserved(entry, inFlight));
|
|
135
141
|
}
|
|
136
142
|
emit(entry.cache, {
|
|
137
143
|
query: entry,
|
|
@@ -273,8 +279,11 @@ function set(entry, patch) {
|
|
|
273
279
|
}
|
|
274
280
|
/** Run `request` as the entry's request fiber, settling into its awaiter. */
|
|
275
281
|
function startRequest(entry, request, direction) {
|
|
282
|
+
if (!entry.inFlight) entry.failureCountBefore = entry.state.failureCount;
|
|
276
283
|
const awaiter = entry.inFlight ?? Deferred.makeUnsafe();
|
|
277
284
|
entry.inFlight = awaiter;
|
|
285
|
+
entry.retriesStopped = false;
|
|
286
|
+
entry.signalRead = false;
|
|
278
287
|
entry.tasks.cancel("gc");
|
|
279
288
|
if (!entry.state.hasData && entry.state.status === "error") {
|
|
280
289
|
entry.pending = pending();
|
|
@@ -312,6 +321,29 @@ function settle(entry, awaiter, exit) {
|
|
|
312
321
|
}
|
|
313
322
|
scheduleGc(entry);
|
|
314
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* The last reader left while `awaiter`'s request was in flight. A refetch
|
|
326
|
+
* whose work an interrupt cancels (`signalRead`) is interrupted, and the
|
|
327
|
+
* entry goes back to its state before the request; its awaiters get the
|
|
328
|
+
* cached data. Any other request finishes, so its result is cached, but
|
|
329
|
+
* runs no more retries.
|
|
330
|
+
*/
|
|
331
|
+
function unobserved(entry, awaiter) {
|
|
332
|
+
if (isObserved(entry) || entry.inFlight !== awaiter) return;
|
|
333
|
+
if (!entry.signalRead || !entry.state.hasData) {
|
|
334
|
+
entry.retriesStopped = true;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
entry.inFlight = void 0;
|
|
338
|
+
entry.tasks.cancel("request");
|
|
339
|
+
set(entry, {
|
|
340
|
+
failureCount: entry.failureCountBefore,
|
|
341
|
+
fetchDirection: void 0,
|
|
342
|
+
isFetching: false
|
|
343
|
+
});
|
|
344
|
+
Deferred.doneUnsafe(awaiter, Exit.succeed(entry.state.data));
|
|
345
|
+
scheduleGc(entry);
|
|
346
|
+
}
|
|
315
347
|
function succeed(entry, next, updatedAt) {
|
|
316
348
|
const previous = entry.state;
|
|
317
349
|
const data = previous.hasData && behavior(entry, "structuralSharing") !== false ? replaceEqualDeep(previous.data, next) : next;
|
|
@@ -365,24 +397,37 @@ function markErrorShown(entry) {
|
|
|
365
397
|
}
|
|
366
398
|
function requestEffect(entry, options, request) {
|
|
367
399
|
const queryKey = options.queryKey;
|
|
368
|
-
if (options.kind === "query") return attempt(entry, (
|
|
400
|
+
if (options.kind === "query") return attempt(entry, (readSignal) => options.queryFn({
|
|
369
401
|
queryKey,
|
|
370
|
-
signal
|
|
402
|
+
get signal() {
|
|
403
|
+
return readSignal();
|
|
404
|
+
}
|
|
371
405
|
}));
|
|
372
406
|
const current = isInfiniteData(entry.state.data) ? entry.state.data : void 0;
|
|
373
|
-
return fetchPages(options, current, request, (pageParam, direction) => attempt(entry, (
|
|
407
|
+
return fetchPages(options, current, request, (pageParam, direction) => attempt(entry, (readSignal) => options.queryFn({
|
|
374
408
|
direction,
|
|
375
409
|
pageParam,
|
|
376
410
|
queryKey,
|
|
377
|
-
signal
|
|
411
|
+
get signal() {
|
|
412
|
+
return readSignal();
|
|
413
|
+
}
|
|
378
414
|
})));
|
|
379
415
|
}
|
|
380
416
|
/**
|
|
381
417
|
* One `queryFn` call, retried per the query's policy. Every failure counts.
|
|
382
|
-
*
|
|
418
|
+
* `call` reads the signal through `readSignal`, which marks the entry's
|
|
419
|
+
* `signalRead`. Its services are erased with its data type; `runRequest`
|
|
420
|
+
* provides them.
|
|
383
421
|
*/
|
|
384
422
|
function attempt(entry, call) {
|
|
385
|
-
const once = fromQueryFn(
|
|
423
|
+
const once = fromQueryFn((signal) => {
|
|
424
|
+
const result = call(() => {
|
|
425
|
+
entry.signalRead = true;
|
|
426
|
+
return signal;
|
|
427
|
+
});
|
|
428
|
+
if (Effect.isEffect(result)) entry.signalRead = true;
|
|
429
|
+
return result;
|
|
430
|
+
}).pipe(Effect.tapError(() => Effect.sync(() => set(entry, { failureCount: entry.state.failureCount + 1 }))));
|
|
386
431
|
const schedule = retrySchedule({
|
|
387
432
|
retry: behavior(entry, "retry"),
|
|
388
433
|
retryDelay: behavior(entry, "retryDelay")
|
|
@@ -391,12 +436,18 @@ function attempt(entry, call) {
|
|
|
391
436
|
const online = entry.cache.isServer ? Effect.void : whenOnline;
|
|
392
437
|
return Effect.suspend(() => {
|
|
393
438
|
let retrying = false;
|
|
439
|
+
let lastError;
|
|
440
|
+
const unlessStopped = (next) => Effect.suspend(() => entry.retriesStopped ? Effect.fail(lastError) : next);
|
|
394
441
|
const gated = Effect.suspend(() => {
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
442
|
+
if (!retrying) {
|
|
443
|
+
retrying = true;
|
|
444
|
+
return once;
|
|
445
|
+
}
|
|
446
|
+
return unlessStopped(Effect.andThen(online, unlessStopped(once)));
|
|
447
|
+
}).pipe(Effect.tapError((error) => Effect.sync(() => {
|
|
448
|
+
lastError = error;
|
|
449
|
+
})));
|
|
450
|
+
return Effect.retry(gated, Schedule.while(schedule, () => !entry.retriesStopped));
|
|
400
451
|
});
|
|
401
452
|
}
|
|
402
453
|
function scheduleGc(entry) {
|