@raf-query/core 0.0.19 → 0.1.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/dist/query.mjs CHANGED
@@ -1,429 +1,119 @@
1
- import { scheduleNotify } from "./notify.mjs";
2
- import { whenOnline } from "./online.mjs";
3
- import { replaceEqualDeep } from "./share.mjs";
4
- import { fulfilled, pending } from "./thenable.mjs";
5
- import { Cause, Effect, Exit, Predicate, Schedule } from "effect";
1
+ import { __exportAll } from "./_virtual/_rolldown/runtime.mjs";
2
+ import { unsafeCast } from "./cast.mjs";
3
+ import { CancelledError as CancelledError$1, TypeId as TypeId$1, changes as changes$1, entryOf, fetch as fetch$1, hasNextPageUnsafe as hasNextPageUnsafe$1, hasPreviousPageUnsafe as hasPreviousPageUnsafe$1, invalidate as invalidate$1, isQuery as isQuery$1, isStaleUnsafe as isStaleUnsafe$1, provide, readUnsafe as readUnsafe$1, reset as reset$1, subscribeUnsafe as subscribeUnsafe$1 } from "./entry.mjs";
4
+ import { Effect } from "effect";
5
+ import { dual } from "effect/Function";
6
6
  //#region src/query.ts
7
- /** Thrown into a request's awaiters when the cache is cleared mid-request. */
8
- var CancelledError = class extends Error {
9
- constructor() {
10
- super("The query was cancelled");
11
- this.name = "CancelledError";
12
- }
13
- };
14
- const noop = () => {};
15
- const identity = (value) => value;
16
- const defaultObserver = {};
17
- function isEffectResult(result) {
18
- return Effect.isEffect(result);
7
+ /**
8
+ * One cache entry, as a data type: a `Query` carries its key, handle and
9
+ * options, and the functions here read it, subscribe to it and run its
10
+ * requests. Get one from `QueryCache.getQuery(factory, input)`.
11
+ *
12
+ * Requests are Effects. The `*Unsafe` functions read and subscribe
13
+ * synchronously, for renders and external stores that can't run an Effect.
14
+ */
15
+ var query_exports = /* @__PURE__ */ __exportAll({
16
+ CancelledError: () => CancelledError,
17
+ TypeId: () => TypeId,
18
+ changes: () => changes,
19
+ fetch: () => fetch,
20
+ fetchNextPage: () => fetchNextPage,
21
+ fetchPreviousPage: () => fetchPreviousPage,
22
+ get: () => get,
23
+ getUnsafe: () => getUnsafe,
24
+ hasNextPageUnsafe: () => hasNextPageUnsafe,
25
+ hasPreviousPageUnsafe: () => hasPreviousPageUnsafe,
26
+ invalidate: () => invalidate,
27
+ isQuery: () => isQuery,
28
+ isStaleUnsafe: () => isStaleUnsafe,
29
+ observerCountUnsafe: () => observerCountUnsafe,
30
+ readUnsafe: () => readUnsafe,
31
+ refetch: () => refetch,
32
+ reset: () => reset,
33
+ subscribeUnsafe: () => subscribeUnsafe
34
+ });
35
+ const TypeId = TypeId$1;
36
+ const CancelledError = CancelledError$1;
37
+ const isQuery = isQuery$1;
38
+ function typed(effect) {
39
+ return unsafeCast(effect);
19
40
  }
20
- function isInfiniteData(data) {
21
- return Predicate.hasProperty(data, "pages") && Predicate.hasProperty(data, "pageParams") && Array.isArray(data.pages) && Array.isArray(data.pageParams);
41
+ function typedState(state) {
42
+ return unsafeCast(state);
22
43
  }
23
- function shouldRetry(retry, failureCount, error) {
24
- if (Predicate.isBoolean(retry)) return retry;
25
- if (Predicate.isNumber(retry)) return failureCount <= retry;
26
- return retry(failureCount, error);
44
+ /**
45
+ * Start a request, or join the one in flight. Fails with the request's
46
+ * error, or a {@link CancelledError} if the entry leaves the cache first.
47
+ * The entry's requests run with this fiber's services from now on, as after
48
+ * a `QueryCache` verb.
49
+ */
50
+ const fetch = dual((args) => isQuery(args[0]), (self, request) => typed(Effect.contextWith((caller) => {
51
+ const entry = entryOf(self);
52
+ provide(entry, caller);
53
+ return fetch$1(entry, request);
54
+ })));
55
+ /** Refetch now; succeeds when the request settles, whatever its outcome. */
56
+ function refetch(self) {
57
+ return entryOf(self).commands.refetch;
27
58
  }
28
- function nextPageParam(options, data) {
29
- if (data.pages.length === 0) return;
30
- return options.getNextPageParam(data.pages.at(-1), data.pages, data.pageParams.at(-1), data.pageParams);
59
+ /** Load the page after the last one; succeeds once it settles. */
60
+ function fetchNextPage(self) {
61
+ return entryOf(self).commands.fetchNextPage;
31
62
  }
32
- function previousPageParam(options, data) {
33
- if (data.pages.length === 0 || !options.getPreviousPageParam) return;
34
- return options.getPreviousPageParam(data.pages[0], data.pages, data.pageParams[0], data.pageParams);
63
+ /** Load the page before the first one; succeeds once it settles. */
64
+ function fetchPreviousPage(self) {
65
+ return entryOf(self).commands.fetchPreviousPage;
35
66
  }
36
- function addPage(data, page, pageParam, direction, maxPages) {
37
- const forward = direction === "forward";
38
- const pages = forward ? [...data.pages, page] : [page, ...data.pages];
39
- const pageParams = forward ? [...data.pageParams, pageParam] : [pageParam, ...data.pageParams];
40
- if (maxPages === void 0 || pages.length <= maxPages) return {
41
- pageParams,
42
- pages
43
- };
44
- return forward ? {
45
- pageParams: pageParams.slice(-maxPages),
46
- pages: pages.slice(-maxPages)
47
- } : {
48
- pageParams: pageParams.slice(0, maxPages),
49
- pages: pages.slice(0, maxPages)
50
- };
67
+ /** Mark stale, and refetch if read; succeeds once that refetch settles. */
68
+ function invalidate(self) {
69
+ return invalidate$1(entryOf(self));
70
+ }
71
+ /**
72
+ * Drop data and errors, back to the state of a new entry. Readers suspend
73
+ * and refetch; succeeds when that refetch settles.
74
+ */
75
+ function reset(self) {
76
+ return reset$1(entryOf(self));
77
+ }
78
+ /** The entry's current state. */
79
+ function get(self) {
80
+ return Effect.sync(() => getUnsafe(self));
81
+ }
82
+ /** {@link get}, synchronously. */
83
+ function getUnsafe(self) {
84
+ return typedState(entryOf(self).state);
85
+ }
86
+ /**
87
+ * The entry's current state, then every change. Counts as a reader while
88
+ * the stream runs, so invalidation and focus refetch it.
89
+ */
90
+ function changes(self) {
91
+ return unsafeCast(changes$1(entryOf(self)));
92
+ }
93
+ /**
94
+ * The promise a render should unwrap. Starts the first request if nothing
95
+ * has, and retries a failed first load once its error has been shown.
96
+ */
97
+ function readUnsafe(self) {
98
+ return unsafeCast(readUnsafe$1(entryOf(self)));
99
+ }
100
+ /** Listen for state changes. Subscribed entries count as read. */
101
+ function subscribeUnsafe(self, listener, options) {
102
+ return subscribeUnsafe$1(entryOf(self), listener, options);
103
+ }
104
+ /** Staleness as of now; `state.isStale` is only kept current while read. */
105
+ function isStaleUnsafe(self) {
106
+ return isStaleUnsafe$1(entryOf(self));
107
+ }
108
+ function hasNextPageUnsafe(self) {
109
+ return hasNextPageUnsafe$1(entryOf(self));
110
+ }
111
+ function hasPreviousPageUnsafe(self) {
112
+ return hasPreviousPageUnsafe$1(entryOf(self));
113
+ }
114
+ /** Readers currently subscribed. */
115
+ function observerCountUnsafe(self) {
116
+ return entryOf(self).listeners.size;
51
117
  }
52
- var QueryEntry = class {
53
- hash;
54
- key;
55
- #errorShown = false;
56
- #errorShownTimer;
57
- #gcTimer;
58
- #handle;
59
- #host;
60
- #inFlight;
61
- #listeners = /* @__PURE__ */ new Map();
62
- #options;
63
- #pending;
64
- #staleTimer;
65
- #state;
66
- constructor(host, key, hash) {
67
- this.#host = host;
68
- this.key = key;
69
- this.hash = hash;
70
- this.#handle = {
71
- input: void 0,
72
- key
73
- };
74
- this.#pending = pending();
75
- this.#state = {
76
- data: void 0,
77
- error: null,
78
- failureCount: 0,
79
- fetchDirection: void 0,
80
- hasData: false,
81
- isFetching: false,
82
- isInvalidated: false,
83
- isStale: true,
84
- promise: this.#pending.promise,
85
- status: "pending",
86
- updatedAt: 0
87
- };
88
- this.#scheduleGc();
89
- }
90
- get handle() {
91
- return this.#handle;
92
- }
93
- get options() {
94
- return this.#options;
95
- }
96
- /** Readers are subscribed; `invalidate` and focus refetch only what is read. */
97
- get isObserved() {
98
- return this.#listeners.size > 0;
99
- }
100
- get observerCount() {
101
- return this.#listeners.size;
102
- }
103
- getState = () => this.#state;
104
- isStaleNow = () => this.#computeStale(this.#state);
105
- subscribe = (listener, observer = defaultObserver) => {
106
- this.#listeners.set(listener, observer);
107
- this.#clearGc();
108
- this.#scheduleStale();
109
- this.#host.observersChanged(this);
110
- return () => {
111
- if (!this.#listeners.delete(listener)) return;
112
- if (this.#listeners.size === 0) {
113
- clearTimeout(this.#staleTimer);
114
- this.#scheduleGc();
115
- }
116
- this.#host.observersChanged(this);
117
- };
118
- };
119
- /** Attach the options and input of the latest factory call. */
120
- setOptions(options, input) {
121
- this.#options = options;
122
- if (this.#handle.input !== input) this.#handle = {
123
- input,
124
- key: this.key
125
- };
126
- }
127
- read = () => {
128
- const state = this.#state;
129
- if (!state.hasData && !state.isFetching) {
130
- if (state.status !== "error" || this.#errorShown) this.fetch().catch(noop);
131
- else this.#markErrorShown();
132
- }
133
- return this.#state.promise;
134
- };
135
- fetch = (request = {}) => {
136
- const options = this.#options;
137
- const inFlight = this.#inFlight;
138
- if (inFlight && !(request.restart && options)) {
139
- if (request.direction) {
140
- const next = () => this.fetch(request);
141
- return inFlight.deferred.promise.then(next, next);
142
- }
143
- return inFlight.deferred.promise;
144
- }
145
- if (!options) return this.#state.promise;
146
- inFlight?.controller.abort();
147
- return this.#start(this.#request(options, request), request.direction);
148
- };
149
- refetch = () => this.fetch().then(noop, noop);
150
- fetchNextPage = () => this.fetch({ direction: "forward" }).then(noop, noop);
151
- fetchPreviousPage = () => this.fetch({ direction: "backward" }).then(noop, noop);
152
- hasNextPage = () => {
153
- const options = this.#options;
154
- const data = this.#state.data;
155
- return options?.kind === "infinite" && isInfiniteData(data) && nextPageParam(options, data) != null;
156
- };
157
- hasPreviousPage = () => {
158
- const options = this.#options;
159
- const data = this.#state.data;
160
- return options?.kind === "infinite" && isInfiniteData(data) && previousPageParam(options, data) != null;
161
- };
162
- invalidate = () => {
163
- this.#set({ isInvalidated: true });
164
- if (!this.isObserved || !this.#options) return Promise.resolve();
165
- return this.fetch({ restart: true }).then(noop, noop);
166
- };
167
- reset = () => {
168
- clearTimeout(this.#errorShownTimer);
169
- this.#errorShownTimer = void 0;
170
- this.#errorShown = false;
171
- this.#pending ??= pending();
172
- this.#set({
173
- data: void 0,
174
- error: null,
175
- failureCount: 0,
176
- hasData: false,
177
- isInvalidated: false,
178
- promise: this.#pending.promise,
179
- status: "pending",
180
- updatedAt: 0
181
- });
182
- if (!this.#options || !this.isObserved && !this.#inFlight) return Promise.resolve();
183
- return this.fetch({ restart: true }).then(noop, noop);
184
- };
185
- /** Write data directly, as if a request had returned it. */
186
- setData(data, updatedAt = Date.now()) {
187
- this.#succeed(data, updatedAt);
188
- }
189
- /** Adopt a request started elsewhere (the server's, during hydration). */
190
- resume(promise) {
191
- if (this.#state.hasData || this.#inFlight) return;
192
- this.#start(Effect.tryPromise({
193
- catch: identity,
194
- try: () => Promise.resolve(promise)
195
- }), void 0).catch(noop);
196
- }
197
- /** Refetch a read, stale entry whose readers want it on this event. */
198
- onWindowEvent(event) {
199
- const option = event === "focus" ? "refetchOnWindowFocus" : "refetchOnReconnect";
200
- const fallback = this.#behavior(option) ?? true;
201
- if ([...this.#listeners.values()].some((observer) => observer[option] ?? fallback) && this.#computeStale(this.#state)) this.fetch().catch(noop);
202
- }
203
- /** Stop timers and abort the in-flight request; the entry is leaving the cache. */
204
- destroy() {
205
- clearTimeout(this.#gcTimer);
206
- clearTimeout(this.#staleTimer);
207
- clearTimeout(this.#errorShownTimer);
208
- const inFlight = this.#inFlight;
209
- this.#inFlight = void 0;
210
- if (inFlight) {
211
- inFlight.controller.abort();
212
- inFlight.deferred.reject(new CancelledError());
213
- }
214
- }
215
- #behavior(name) {
216
- return this.#options?.[name] ?? this.#host.defaults[name];
217
- }
218
- #computeStale(state) {
219
- if (state.isInvalidated || !state.hasData) return true;
220
- const staleTime = this.#behavior("staleTime") ?? 0;
221
- return Date.now() - state.updatedAt >= staleTime;
222
- }
223
- #set(patch) {
224
- const next = {
225
- ...this.#state,
226
- ...patch
227
- };
228
- this.#state = {
229
- ...next,
230
- isStale: this.#computeStale(next)
231
- };
232
- for (const listener of this.#listeners.keys()) scheduleNotify(listener);
233
- this.#host.changed(this);
234
- this.#scheduleStale();
235
- }
236
- #start(request, direction) {
237
- const deferred = this.#inFlight?.deferred ?? pending();
238
- const run = {
239
- controller: new AbortController(),
240
- deferred
241
- };
242
- this.#inFlight = run;
243
- this.#clearGc();
244
- if (!this.#state.hasData && this.#state.status === "error") {
245
- this.#pending = pending();
246
- this.#set({
247
- error: null,
248
- failureCount: 0,
249
- fetchDirection: direction,
250
- isFetching: true,
251
- promise: this.#pending.promise,
252
- status: "pending"
253
- });
254
- } else this.#set({
255
- failureCount: 0,
256
- fetchDirection: direction,
257
- isFetching: true
258
- });
259
- Effect.runPromiseExit(request, { signal: run.controller.signal }).then((exit) => this.#settle(run, exit), noop);
260
- return deferred.promise;
261
- }
262
- #settle(run, exit) {
263
- if (this.#inFlight !== run) return;
264
- this.#inFlight = void 0;
265
- if (Exit.isSuccess(exit)) run.deferred.resolve(this.#succeed(exit.value, Date.now()));
266
- else if (Cause.hasInterruptsOnly(exit.cause)) {
267
- this.#set({
268
- fetchDirection: void 0,
269
- isFetching: false
270
- });
271
- run.deferred.reject(new CancelledError());
272
- } else {
273
- const error = Cause.squash(exit.cause);
274
- this.#fail(error);
275
- run.deferred.reject(error);
276
- }
277
- this.#scheduleGc();
278
- }
279
- #succeed(next, updatedAt) {
280
- const previous = this.#state;
281
- const data = previous.hasData && this.#behavior("structuralSharing") !== false ? replaceEqualDeep(previous.data, next) : next;
282
- const firstLoad = this.#pending;
283
- this.#pending = void 0;
284
- firstLoad?.resolve(data);
285
- const unchanged = previous.hasData && data === previous.data;
286
- this.#set({
287
- data,
288
- error: null,
289
- failureCount: 0,
290
- fetchDirection: void 0,
291
- hasData: true,
292
- isFetching: false,
293
- isInvalidated: false,
294
- promise: firstLoad?.promise ?? (unchanged ? previous.promise : fulfilled(data)),
295
- status: "success",
296
- updatedAt
297
- });
298
- return data;
299
- }
300
- #fail(error) {
301
- this.#host.reportError(error, this);
302
- if (!this.#state.hasData) {
303
- const firstLoad = this.#pending;
304
- this.#pending = void 0;
305
- firstLoad?.reject(error);
306
- this.#errorShown = false;
307
- }
308
- this.#set({
309
- error,
310
- fetchDirection: void 0,
311
- isFetching: false,
312
- status: "error"
313
- });
314
- }
315
- /**
316
- * A failed first load rejects its promise, so the reader throws to an error
317
- * boundary. Every read in that same task (React retries a throwing render
318
- * once, StrictMode renders twice) gets the rejection too; a read in a later
319
- * task — the boundary was reset — starts a new request.
320
- */
321
- #markErrorShown() {
322
- if (this.#errorShownTimer !== void 0) return;
323
- this.#errorShownTimer = setTimeout(() => {
324
- this.#errorShownTimer = void 0;
325
- this.#errorShown = true;
326
- }, 0);
327
- }
328
- #request(options, request) {
329
- if (options.kind === "query") return this.#attempt((signal) => options.queryFn({
330
- queryKey: options.queryKey,
331
- signal
332
- }));
333
- return this.#pagedRequest(options, request);
334
- }
335
- #pagedRequest(options, request) {
336
- const current = isInfiniteData(this.#state.data) ? this.#state.data : void 0;
337
- const fetchPage = (pageParam, direction) => this.#attempt((signal) => options.queryFn({
338
- direction,
339
- pageParam,
340
- queryKey: options.queryKey,
341
- signal
342
- }));
343
- return Effect.gen(function* () {
344
- if (request.direction && current) {
345
- const pageParam = request.direction === "forward" ? nextPageParam(options, current) : previousPageParam(options, current);
346
- if (pageParam == null) return current;
347
- const page = yield* fetchPage(pageParam, request.direction);
348
- return addPage(current, page, pageParam, request.direction, options.maxPages);
349
- }
350
- const count = Math.max(request.pages ?? 1, current?.pages.length ?? 0);
351
- let data = {
352
- pageParams: [],
353
- pages: []
354
- };
355
- let pageParam = current && current.pageParams.length > 0 ? current.pageParams[0] : options.initialPageParam;
356
- for (let index = 0; index < count; index++) {
357
- if (index > 0) {
358
- const next = nextPageParam(options, data);
359
- if (next == null) break;
360
- pageParam = next;
361
- }
362
- const page = yield* fetchPage(pageParam, "forward");
363
- data = addPage(data, page, pageParam, "forward", options.maxPages);
364
- }
365
- return data;
366
- });
367
- }
368
- /** One request: run the `queryFn` with a fresh signal, retrying per policy. */
369
- #attempt(call) {
370
- const once = Effect.suspend(() => {
371
- const controller = new AbortController();
372
- return Effect.try({
373
- catch: identity,
374
- try: () => call(controller.signal)
375
- }).pipe(Effect.flatMap((result) => isEffectResult(result) ? result : Effect.tryPromise({
376
- catch: identity,
377
- try: () => Promise.resolve(result)
378
- })), Effect.onInterrupt(() => Effect.sync(() => controller.abort())));
379
- });
380
- return this.#withRetry(once);
381
- }
382
- #withRetry(once) {
383
- const retry = this.#behavior("retry") ?? (this.#host.isServer ? 0 : 3);
384
- const countFailure = Effect.sync(() => this.#set({ failureCount: this.#state.failureCount + 1 }));
385
- const counted = once.pipe(Effect.tapError(() => countFailure));
386
- const online = this.#host.isServer ? Effect.void : whenOnline;
387
- if (Schedule.isSchedule(retry)) return Effect.suspend(() => {
388
- let first = true;
389
- const each = Effect.suspend(() => {
390
- const wait = first ? Effect.void : online;
391
- first = false;
392
- return wait.pipe(Effect.andThen(counted));
393
- });
394
- return Effect.retry(each, retry);
395
- });
396
- const retryDelay = this.#behavior("retryDelay");
397
- const delayFor = (failureCount, error) => {
398
- if (retryDelay === void 0) return Math.min(1e3 * 2 ** (failureCount - 1), 3e4);
399
- return Predicate.isNumber(retryDelay) ? retryDelay : retryDelay(failureCount, error);
400
- };
401
- const attempt = counted.pipe(Effect.catch((error) => {
402
- const failureCount = this.#state.failureCount;
403
- if (!shouldRetry(retry, failureCount, error)) return Effect.fail(error);
404
- return Effect.sleep(delayFor(failureCount, error)).pipe(Effect.andThen(online), Effect.andThen(Effect.suspend(() => attempt)));
405
- }));
406
- return attempt;
407
- }
408
- #clearGc() {
409
- clearTimeout(this.#gcTimer);
410
- this.#gcTimer = void 0;
411
- }
412
- #scheduleGc() {
413
- this.#clearGc();
414
- if (this.isObserved || this.#inFlight) return;
415
- const gcTime = this.#behavior("gcTime") ?? (this.#host.isServer ? Infinity : 3e5);
416
- if (Number.isFinite(gcTime)) this.#gcTimer = setTimeout(() => this.#host.remove(this), gcTime);
417
- }
418
- #scheduleStale() {
419
- clearTimeout(this.#staleTimer);
420
- const state = this.#state;
421
- if (this.#host.isServer || !this.isObserved || state.isStale) return;
422
- const staleTime = this.#behavior("staleTime") ?? 0;
423
- if (!Number.isFinite(staleTime)) return;
424
- const remaining = state.updatedAt + staleTime - Date.now();
425
- this.#staleTimer = setTimeout(() => this.#set({}), Math.max(remaining, 0) + 1);
426
- }
427
- };
428
118
  //#endregion
429
- export { CancelledError, QueryEntry, isInfiniteData, nextPageParam, previousPageParam };
119
+ export { CancelledError, TypeId, changes, fetch, fetchNextPage, fetchPreviousPage, get, getUnsafe, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isQuery, isStaleUnsafe, observerCountUnsafe, query_exports, readUnsafe, refetch, reset, subscribeUnsafe };
@@ -0,0 +1,47 @@
1
+ import { Context, Effect, Scope } from "effect";
2
+ //#region src/request.ts
3
+ function isEffectResult(result) {
4
+ return Effect.isEffect(result);
5
+ }
6
+ /**
7
+ * One `queryFn` call as an Effect. The signal it gets aborts when the fiber
8
+ * is interrupted; a returned Effect runs in the same fiber, so interrupting
9
+ * the request interrupts it. Plain values settle like promises, a microtask
10
+ * later. A `queryFn` that throws instead of returning is a defect, as in
11
+ * `Effect.sync`.
12
+ */
13
+ function fromQueryFn(call) {
14
+ return Effect.callback((resume, signal) => {
15
+ let result;
16
+ try {
17
+ result = call(signal);
18
+ } catch (error) {
19
+ resume(Effect.die(error));
20
+ return;
21
+ }
22
+ if (isEffectResult(result)) {
23
+ resume(result);
24
+ return;
25
+ }
26
+ Promise.resolve(result).then((value) => resume(Effect.succeed(value)), (error) => resume(Effect.fail(error)));
27
+ });
28
+ }
29
+ /**
30
+ * The services a request runs with: the cache's, under those of the verb
31
+ * that last called for the entry. The caller's `Scope` is left out: a
32
+ * request outlives the call that started it.
33
+ */
34
+ function requestServices(cache, caller) {
35
+ return Context.merge(cache, Context.omit(Scope.Scope)(caller));
36
+ }
37
+ /**
38
+ * A request as it runs: in a `raf-query.request` span, with `services`,
39
+ * which hold what its `queryFn` Effect needs (entries erase that type with
40
+ * the data's). Typed failures become the query's error; defects are logged
41
+ * too, since nothing else reports them.
42
+ */
43
+ function runRequest(request, hash, services) {
44
+ return request.pipe(Effect.tapDefect((defect) => Effect.logError("raf-query: queryFn died", defect)), Effect.withSpan("raf-query.request", { attributes: { "raf_query.hash": hash } }), Effect.provideContext(services));
45
+ }
46
+ //#endregion
47
+ export { fromQueryFn, requestServices, runRequest };
package/dist/retry.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import { Effect, Predicate, Schedule } from "effect";
2
+ //#region src/retry.ts
3
+ /** Doubling from 1 s, at most 30 s. */
4
+ const defaultDelays = Schedule.min([Schedule.exponential("1 second"), Schedule.spaced("30 seconds")]);
5
+ function delays(retryDelay) {
6
+ if (retryDelay === void 0) return defaultDelays;
7
+ if (Predicate.isNumber(retryDelay)) return Schedule.spaced(retryDelay);
8
+ return Schedule.forever.pipe(Schedule.modifyDelay((step) => Effect.succeed(retryDelay(step.attempt, step.input))));
9
+ }
10
+ /**
11
+ * A query's retry options as one `Schedule`, or `undefined` when it doesn't
12
+ * retry. `retryDelay` sets the delays; a count or predicate in `retry` stops
13
+ * the schedule, given the attempt number (1 after the first failure) and the
14
+ * error.
15
+ */
16
+ function retrySchedule(behavior, isServer) {
17
+ const retry = behavior.retry ?? (isServer ? 0 : 3);
18
+ if (Schedule.isSchedule(retry)) return retry;
19
+ if (retry === false || retry === 0) return;
20
+ const schedule = delays(behavior.retryDelay);
21
+ if (retry === true) return schedule;
22
+ return schedule.pipe(Schedule.while((step) => Predicate.isNumber(retry) ? step.attempt <= retry : retry(step.attempt, step.input)));
23
+ }
24
+ //#endregion
25
+ export { retrySchedule };
@@ -21,4 +21,9 @@ interface RejectedStatus {
21
21
  * `reason`) once, when the promise settles.
22
22
  */
23
23
  export type Thenable<T> = Promise<T> & (FulfilledStatus<T> | PendingStatus | RejectedStatus);
24
+ export interface Pending<T> {
25
+ readonly promise: Thenable<T>;
26
+ reject(reason: unknown): void;
27
+ resolve(value: T): void;
28
+ }
24
29
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@raf-query/core",
3
- "version": "0.0.19",
3
+ "version": "0.1.0",
4
4
  "description": "Framework-agnostic core of raf-query, a render-as-you-fetch query library",
5
5
  "keywords": [
6
6
  "query",
@@ -1,18 +0,0 @@
1
- //#region src/all-keyed.d.ts
2
- /** The resolved form of a record passed to {@link allKeyed}. */
3
- export type Settled<TRecord> = { -readonly [TKey in keyof TRecord]: Awaited<TRecord[TKey]>; };
4
- /**
5
- * Ponyfill of the TC39 `Promise.allKeyed` proposal: a record of promises in,
6
- * a promise of the same record out, rejecting on the first failure.
7
- *
8
- * ```ts
9
- * const data = await allKeyed({
10
- * post: qc.ensure(postQuery, { slug }),
11
- * author: qc.ensure(authorQuery, { slug }),
12
- * });
13
- * ```
14
- */
15
- export declare function allKeyed<TRecord extends {
16
- readonly [key: string]: unknown;
17
- }>(record: TRecord): Promise<Settled<TRecord>>;
18
- //#endregion
@@ -1,19 +0,0 @@
1
- //#region src/all-keyed.ts
2
- /**
3
- * Ponyfill of the TC39 `Promise.allKeyed` proposal: a record of promises in,
4
- * a promise of the same record out, rejecting on the first failure.
5
- *
6
- * ```ts
7
- * const data = await allKeyed({
8
- * post: qc.ensure(postQuery, { slug }),
9
- * author: qc.ensure(authorQuery, { slug }),
10
- * });
11
- * ```
12
- */
13
- async function allKeyed(record) {
14
- const keys = Object.keys(record);
15
- const values = await Promise.all(keys.map((key) => record[key]));
16
- return Object.fromEntries(keys.map((key, index) => [key, values[index]]));
17
- }
18
- //#endregion
19
- export { allKeyed };