@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/CHANGELOG.md +16 -1
- package/README.md +101 -33
- package/dist/_virtual/_rolldown/runtime.mjs +13 -0
- package/dist/cache.d.mts +140 -0
- package/dist/cache.mjs +176 -0
- package/dist/entry.d.mts +113 -0
- package/dist/entry.mjs +421 -0
- package/dist/hydration.d.mts +37 -13
- package/dist/hydration.mjs +78 -17
- package/dist/index.d.mts +7 -7
- package/dist/index.mjs +6 -6
- package/dist/infinite.d.mts +5 -0
- package/dist/infinite.mjs +59 -0
- package/dist/listen.mjs +19 -0
- package/dist/observer.d.mts +57 -39
- package/dist/observer.mjs +215 -133
- package/dist/options.d.mts +50 -14
- package/dist/options.mjs +3 -0
- package/dist/platform.d.mts +27 -0
- package/dist/platform.mjs +44 -0
- package/dist/query.d.mts +77 -49
- package/dist/query.mjs +111 -421
- package/dist/request.mjs +47 -0
- package/dist/retry.mjs +25 -0
- package/dist/thenable.d.mts +5 -0
- package/package.json +1 -1
- package/dist/all-keyed.d.mts +0 -18
- package/dist/all-keyed.mjs +0 -19
- package/dist/client.d.mts +0 -123
- package/dist/client.mjs +0 -264
- package/dist/online.mjs +0 -16
package/dist/entry.d.mts
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { Platform, PlatformEvent } from "./platform.mjs";
|
|
2
|
+
import { QueryKey } from "./key.mjs";
|
|
3
|
+
import { AnyQueryOptions, QueryBehavior, QueryFactory, QueryHandle } from "./options.mjs";
|
|
4
|
+
import { Event } from "./cache.mjs";
|
|
5
|
+
import { Pending, Thenable } from "./thenable.mjs";
|
|
6
|
+
import { FetchRequest, Query, State, SubscribeOptions } from "./query.mjs";
|
|
7
|
+
import { Cause, Clock, Context, Deferred, Effect, Scope, Stream } from "effect";
|
|
8
|
+
declare namespace entry_d_exports {
|
|
9
|
+
export { CacheState, CancelledError, Entry, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, setData, setOptions, start, subscribeUnsafe };
|
|
10
|
+
}
|
|
11
|
+
export declare const TypeId = "~@raf-query/core/Query";
|
|
12
|
+
declare const CancelledError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
13
|
+
readonly _tag: "CancelledError";
|
|
14
|
+
} & Readonly<A>;
|
|
15
|
+
/**
|
|
16
|
+
* The failure of a request that was interrupted rather than settled: its
|
|
17
|
+
* entry was removed or cleared, or an Effect `queryFn` was interrupted.
|
|
18
|
+
*/
|
|
19
|
+
export declare class CancelledError extends CancelledError_base {
|
|
20
|
+
readonly message = "The query was cancelled";
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The state a cache shares with its entries. Entries are created
|
|
24
|
+
* synchronously (a render reads one), so they fork their fibers with the
|
|
25
|
+
* services and into the scope the cache was made with.
|
|
26
|
+
*/
|
|
27
|
+
export interface CacheState {
|
|
28
|
+
readonly clock: Clock.Clock;
|
|
29
|
+
readonly defaults: QueryBehavior;
|
|
30
|
+
readonly entries: Map<string, Entry>;
|
|
31
|
+
readonly isServer: boolean;
|
|
32
|
+
readonly listeners: Set<(event: Event) => void>;
|
|
33
|
+
readonly platform: Platform;
|
|
34
|
+
readonly scope: Scope.Scope;
|
|
35
|
+
readonly services: Context.Context<never>;
|
|
36
|
+
}
|
|
37
|
+
/** The background work of an entry, one fiber per kind. */
|
|
38
|
+
type Task = "errorShown" | "gc" | "request" | "stale";
|
|
39
|
+
interface Tasks {
|
|
40
|
+
cancel(task: Task): void;
|
|
41
|
+
fork(task: Task, effect: Effect.Effect<void>, options?: {
|
|
42
|
+
readonly onlyIfMissing?: boolean;
|
|
43
|
+
}): void;
|
|
44
|
+
}
|
|
45
|
+
interface Commands {
|
|
46
|
+
readonly fetchNextPage: Effect.Effect<void>;
|
|
47
|
+
readonly fetchPreviousPage: Effect.Effect<void>;
|
|
48
|
+
readonly refetch: Effect.Effect<void>;
|
|
49
|
+
}
|
|
50
|
+
/** A cache entry: a {@link Query} and the state behind it. */
|
|
51
|
+
export interface Entry extends Query {
|
|
52
|
+
readonly cache: CacheState;
|
|
53
|
+
/** One Effect per command, so readers can compare them by identity. */
|
|
54
|
+
readonly commands: Commands;
|
|
55
|
+
/** A failed first load was shown; the next read retries it. */
|
|
56
|
+
errorShown: boolean;
|
|
57
|
+
handle: QueryHandle;
|
|
58
|
+
/** Awaited by every caller of the request in flight; outlives restarts. */
|
|
59
|
+
inFlight: Deferred.Deferred<unknown, unknown> | undefined;
|
|
60
|
+
readonly listeners: Map<() => void, SubscribeOptions>;
|
|
61
|
+
options: AnyQueryOptions | undefined;
|
|
62
|
+
/** The first load's promise, until it settles. */
|
|
63
|
+
pending: Pending<unknown> | undefined;
|
|
64
|
+
/** Open while the entry is cached; owns its fibers. */
|
|
65
|
+
readonly scope: Scope.Closeable;
|
|
66
|
+
/**
|
|
67
|
+
* What its requests run with: the cache's services, under those of the
|
|
68
|
+
* latest verb that called for this entry ({@link provide}).
|
|
69
|
+
*/
|
|
70
|
+
services: Context.Context<never>;
|
|
71
|
+
state: State<unknown>;
|
|
72
|
+
readonly tasks: Tasks;
|
|
73
|
+
}
|
|
74
|
+
export declare function isQuery(value: unknown): value is Query;
|
|
75
|
+
export declare function entryOf<TData, TFactory extends QueryFactory, TError>(query: Query<TData, TFactory, TError>): Entry;
|
|
76
|
+
export declare function emit(cache: CacheState, event: Event): void;
|
|
77
|
+
export declare function make(cache: CacheState, key: QueryKey, hash: string): Entry;
|
|
78
|
+
export declare function now(entry: Entry): number;
|
|
79
|
+
/** Readers are subscribed; `invalidate` and focus refetch only what is read. */
|
|
80
|
+
export declare function isObserved(entry: Entry): boolean;
|
|
81
|
+
export declare function isStaleUnsafe(entry: Entry): boolean;
|
|
82
|
+
export declare function hasNextPageUnsafe(entry: Entry): boolean;
|
|
83
|
+
export declare function hasPreviousPageUnsafe(entry: Entry): boolean;
|
|
84
|
+
export declare function subscribeUnsafe(entry: Entry, listener: () => void, options?: SubscribeOptions): () => void;
|
|
85
|
+
export declare function changes(entry: Entry): Stream.Stream<State<unknown>>;
|
|
86
|
+
/**
|
|
87
|
+
* Run this entry's requests with the services of `caller`, a verb's fiber
|
|
88
|
+
* context, over the cache's: every request from now on, including ones a
|
|
89
|
+
* render, a refetch or `invalidate` starts.
|
|
90
|
+
*/
|
|
91
|
+
export declare function provide(entry: Entry, caller: Context.Context<never>): void;
|
|
92
|
+
/** Attach the options and input of the latest factory call. */
|
|
93
|
+
export declare function setOptions(entry: Entry, options: AnyQueryOptions, input: unknown): void;
|
|
94
|
+
/**
|
|
95
|
+
* The promise a render should unwrap. Starts the first request if nothing
|
|
96
|
+
* has, and retries a failed first load once its error has been shown.
|
|
97
|
+
*/
|
|
98
|
+
export declare function readUnsafe(entry: Entry): Thenable<unknown>;
|
|
99
|
+
/** Start a request unless one is in flight, without waiting for it. */
|
|
100
|
+
export declare function start(entry: Entry): void;
|
|
101
|
+
export declare function fetch(entry: Entry, request?: FetchRequest): Effect.Effect<unknown, unknown>;
|
|
102
|
+
export declare function invalidate(entry: Entry): Effect.Effect<void>;
|
|
103
|
+
export declare function reset(entry: Entry): Effect.Effect<void>;
|
|
104
|
+
/** Write data directly, as if a request had returned it. */
|
|
105
|
+
export declare function setData(entry: Entry, data: unknown, updatedAt?: number): void;
|
|
106
|
+
/** Adopt a request started elsewhere (the server's, during hydration). */
|
|
107
|
+
export declare function resume(entry: Entry, promise: PromiseLike<unknown>): void;
|
|
108
|
+
/** Refetch a read, stale entry whose readers want it on this event. */
|
|
109
|
+
export declare function onPlatformEvent(entry: Entry, event: PlatformEvent): void;
|
|
110
|
+
/** Take `entry` out of its cache: interrupt its fibers and fail its awaiters. */
|
|
111
|
+
export declare function remove(entry: Entry): void;
|
|
112
|
+
//#endregion
|
|
113
|
+
export { entry_d_exports };
|
package/dist/entry.mjs
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { whenOnline } from "./platform.mjs";
|
|
2
|
+
import { unsafeCast } from "./cast.mjs";
|
|
3
|
+
import { fetchPages, isInfiniteData, nextPageParam, previousPageParam } from "./infinite.mjs";
|
|
4
|
+
import { listen } from "./listen.mjs";
|
|
5
|
+
import { scheduleNotify } from "./notify.mjs";
|
|
6
|
+
import { fromQueryFn, requestServices, runRequest } from "./request.mjs";
|
|
7
|
+
import { retrySchedule } from "./retry.mjs";
|
|
8
|
+
import { replaceEqualDeep } from "./share.mjs";
|
|
9
|
+
import { fulfilled, pending } from "./thenable.mjs";
|
|
10
|
+
import { Cause, Data, Deferred, Effect, Exit, FiberMap, Pipeable, Predicate, Scope } from "effect";
|
|
11
|
+
//#region src/entry.ts
|
|
12
|
+
const TypeId = "~@raf-query/core/Query";
|
|
13
|
+
/**
|
|
14
|
+
* The failure of a request that was interrupted rather than settled: its
|
|
15
|
+
* entry was removed or cleared, or an Effect `queryFn` was interrupted.
|
|
16
|
+
*/
|
|
17
|
+
var CancelledError = class extends Data.TaggedError("CancelledError") {
|
|
18
|
+
message = "The query was cancelled";
|
|
19
|
+
};
|
|
20
|
+
const Proto = {
|
|
21
|
+
...Pipeable.Prototype,
|
|
22
|
+
[TypeId]: {
|
|
23
|
+
_A: (value) => value,
|
|
24
|
+
_E: (value) => value
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const defaultSubscribeOptions = {};
|
|
28
|
+
function isQuery(value) {
|
|
29
|
+
return Predicate.hasProperty(value, TypeId);
|
|
30
|
+
}
|
|
31
|
+
function entryOf(query) {
|
|
32
|
+
return unsafeCast(query);
|
|
33
|
+
}
|
|
34
|
+
function emit(cache, event) {
|
|
35
|
+
for (const listener of cache.listeners) listener(event);
|
|
36
|
+
}
|
|
37
|
+
/** Fibers keyed by task: forking a task interrupts its previous fiber. */
|
|
38
|
+
function makeTasks(cache, scope) {
|
|
39
|
+
const runSync = Effect.runSyncWith(cache.services);
|
|
40
|
+
const runFork = Effect.runForkWith(cache.services);
|
|
41
|
+
const fibers = runSync(FiberMap.make().pipe(Scope.provide(scope)));
|
|
42
|
+
const run = runSync(FiberMap.runtime(fibers)());
|
|
43
|
+
return {
|
|
44
|
+
cancel(task) {
|
|
45
|
+
if (FiberMap.hasUnsafe(fibers, task)) runFork(FiberMap.remove(fibers, task));
|
|
46
|
+
},
|
|
47
|
+
fork(task, effect, options) {
|
|
48
|
+
run(task, effect, options);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function make(cache, key, hash) {
|
|
53
|
+
const scope = Scope.forkUnsafe(cache.scope);
|
|
54
|
+
const first = pending();
|
|
55
|
+
const entry = Object.assign(Object.create(Proto), {
|
|
56
|
+
cache,
|
|
57
|
+
errorShown: false,
|
|
58
|
+
handle: {
|
|
59
|
+
input: void 0,
|
|
60
|
+
key
|
|
61
|
+
},
|
|
62
|
+
hash,
|
|
63
|
+
inFlight: void 0,
|
|
64
|
+
key,
|
|
65
|
+
listeners: /* @__PURE__ */ new Map(),
|
|
66
|
+
options: void 0,
|
|
67
|
+
pending: first,
|
|
68
|
+
scope,
|
|
69
|
+
services: cache.services,
|
|
70
|
+
state: {
|
|
71
|
+
data: void 0,
|
|
72
|
+
error: null,
|
|
73
|
+
failureCount: 0,
|
|
74
|
+
fetchDirection: void 0,
|
|
75
|
+
hasData: false,
|
|
76
|
+
isFetching: false,
|
|
77
|
+
isInvalidated: false,
|
|
78
|
+
isStale: true,
|
|
79
|
+
promise: first.promise,
|
|
80
|
+
status: "pending",
|
|
81
|
+
updatedAt: 0
|
|
82
|
+
},
|
|
83
|
+
tasks: makeTasks(cache, scope)
|
|
84
|
+
});
|
|
85
|
+
const commands = {
|
|
86
|
+
fetchNextPage: Effect.ignore(fetch(entry, { direction: "forward" })),
|
|
87
|
+
fetchPreviousPage: Effect.ignore(fetch(entry, { direction: "backward" })),
|
|
88
|
+
refetch: Effect.ignore(fetch(entry))
|
|
89
|
+
};
|
|
90
|
+
Object.assign(entry, { commands });
|
|
91
|
+
scheduleGc(entry);
|
|
92
|
+
return entry;
|
|
93
|
+
}
|
|
94
|
+
function now(entry) {
|
|
95
|
+
return entry.cache.clock.currentTimeMillisUnsafe();
|
|
96
|
+
}
|
|
97
|
+
/** Readers are subscribed; `invalidate` and focus refetch only what is read. */
|
|
98
|
+
function isObserved(entry) {
|
|
99
|
+
return entry.listeners.size > 0;
|
|
100
|
+
}
|
|
101
|
+
function behavior(entry, name) {
|
|
102
|
+
return entry.options?.[name] ?? entry.cache.defaults[name];
|
|
103
|
+
}
|
|
104
|
+
function computeStale(entry, state) {
|
|
105
|
+
if (state.isInvalidated || !state.hasData) return true;
|
|
106
|
+
const staleTime = behavior(entry, "staleTime") ?? 0;
|
|
107
|
+
return now(entry) - state.updatedAt >= staleTime;
|
|
108
|
+
}
|
|
109
|
+
function isStaleUnsafe(entry) {
|
|
110
|
+
return computeStale(entry, entry.state);
|
|
111
|
+
}
|
|
112
|
+
function hasNextPageUnsafe(entry) {
|
|
113
|
+
const options = entry.options;
|
|
114
|
+
const data = entry.state.data;
|
|
115
|
+
return options?.kind === "infinite" && isInfiniteData(data) && nextPageParam(options, data) != null;
|
|
116
|
+
}
|
|
117
|
+
function hasPreviousPageUnsafe(entry) {
|
|
118
|
+
const options = entry.options;
|
|
119
|
+
const data = entry.state.data;
|
|
120
|
+
return options?.kind === "infinite" && isInfiniteData(data) && previousPageParam(options, data) != null;
|
|
121
|
+
}
|
|
122
|
+
function subscribeUnsafe(entry, listener, options = defaultSubscribeOptions) {
|
|
123
|
+
entry.listeners.set(listener, options);
|
|
124
|
+
entry.tasks.cancel("gc");
|
|
125
|
+
scheduleStale(entry);
|
|
126
|
+
emit(entry.cache, {
|
|
127
|
+
query: entry,
|
|
128
|
+
type: "observers"
|
|
129
|
+
});
|
|
130
|
+
return () => {
|
|
131
|
+
if (!entry.listeners.delete(listener)) return;
|
|
132
|
+
if (entry.listeners.size === 0) {
|
|
133
|
+
entry.tasks.cancel("stale");
|
|
134
|
+
scheduleGc(entry);
|
|
135
|
+
}
|
|
136
|
+
emit(entry.cache, {
|
|
137
|
+
query: entry,
|
|
138
|
+
type: "observers"
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function changes(entry) {
|
|
143
|
+
return listen((push) => {
|
|
144
|
+
push(entry.state);
|
|
145
|
+
return subscribeUnsafe(entry, () => push(entry.state));
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Run this entry's requests with the services of `caller`, a verb's fiber
|
|
150
|
+
* context, over the cache's: every request from now on, including ones a
|
|
151
|
+
* render, a refetch or `invalidate` starts.
|
|
152
|
+
*/
|
|
153
|
+
function provide(entry, caller) {
|
|
154
|
+
entry.services = requestServices(entry.cache.services, caller);
|
|
155
|
+
}
|
|
156
|
+
/** Attach the options and input of the latest factory call. */
|
|
157
|
+
function setOptions(entry, options, input) {
|
|
158
|
+
entry.options = options;
|
|
159
|
+
if (entry.handle.input !== input) entry.handle = {
|
|
160
|
+
input,
|
|
161
|
+
key: entry.key
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The promise a render should unwrap. Starts the first request if nothing
|
|
166
|
+
* has, and retries a failed first load once its error has been shown.
|
|
167
|
+
*/
|
|
168
|
+
function readUnsafe(entry) {
|
|
169
|
+
const state = entry.state;
|
|
170
|
+
if (!state.hasData && !state.isFetching) {
|
|
171
|
+
if (state.status !== "error" || entry.errorShown) begin(entry, {});
|
|
172
|
+
else markErrorShown(entry);
|
|
173
|
+
}
|
|
174
|
+
return entry.state.promise;
|
|
175
|
+
}
|
|
176
|
+
/** Start a request unless one is in flight, without waiting for it. */
|
|
177
|
+
function start(entry) {
|
|
178
|
+
begin(entry, {});
|
|
179
|
+
}
|
|
180
|
+
function fetch(entry, request = {}) {
|
|
181
|
+
return Effect.suspend(() => {
|
|
182
|
+
const inFlight = entry.inFlight;
|
|
183
|
+
if (inFlight && request.direction && !request.restart) return Deferred.await(inFlight).pipe(Effect.exit, Effect.andThen(fetch(entry, request)));
|
|
184
|
+
return begin(entry, request);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function invalidate(entry) {
|
|
188
|
+
return Effect.suspend(() => {
|
|
189
|
+
set(entry, { isInvalidated: true });
|
|
190
|
+
if (!isObserved(entry) || !entry.options) return Effect.void;
|
|
191
|
+
return Effect.ignore(begin(entry, { restart: true }));
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
function reset(entry) {
|
|
195
|
+
return Effect.suspend(() => {
|
|
196
|
+
entry.tasks.cancel("errorShown");
|
|
197
|
+
entry.errorShown = false;
|
|
198
|
+
entry.pending ??= pending();
|
|
199
|
+
set(entry, {
|
|
200
|
+
data: void 0,
|
|
201
|
+
error: null,
|
|
202
|
+
failureCount: 0,
|
|
203
|
+
hasData: false,
|
|
204
|
+
isInvalidated: false,
|
|
205
|
+
promise: entry.pending.promise,
|
|
206
|
+
status: "pending",
|
|
207
|
+
updatedAt: 0
|
|
208
|
+
});
|
|
209
|
+
if (!entry.options || !isObserved(entry) && !entry.inFlight) return Effect.void;
|
|
210
|
+
return Effect.ignore(begin(entry, { restart: true }));
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Write data directly, as if a request had returned it. */
|
|
214
|
+
function setData(entry, data, updatedAt = now(entry)) {
|
|
215
|
+
succeed(entry, data, updatedAt);
|
|
216
|
+
}
|
|
217
|
+
/** Adopt a request started elsewhere (the server's, during hydration). */
|
|
218
|
+
function resume(entry, promise) {
|
|
219
|
+
if (entry.state.hasData || entry.inFlight) return;
|
|
220
|
+
startRequest(entry, Effect.promise(() => Promise.resolve(promise)), void 0);
|
|
221
|
+
}
|
|
222
|
+
/** Refetch a read, stale entry whose readers want it on this event. */
|
|
223
|
+
function onPlatformEvent(entry, event) {
|
|
224
|
+
const option = event === "focus" ? "refetchOnWindowFocus" : "refetchOnReconnect";
|
|
225
|
+
const fallback = behavior(entry, option) ?? true;
|
|
226
|
+
if ([...entry.listeners.values()].some((options) => options[option] ?? fallback) && isStaleUnsafe(entry)) start(entry);
|
|
227
|
+
}
|
|
228
|
+
/** Take `entry` out of its cache: interrupt its fibers and fail its awaiters. */
|
|
229
|
+
function remove(entry) {
|
|
230
|
+
const cache = entry.cache;
|
|
231
|
+
if (cache.entries.get(entry.hash) !== entry) return;
|
|
232
|
+
cache.entries.delete(entry.hash);
|
|
233
|
+
const inFlight = entry.inFlight;
|
|
234
|
+
entry.inFlight = void 0;
|
|
235
|
+
Effect.runForkWith(cache.services)(Scope.close(entry.scope, Exit.void));
|
|
236
|
+
if (inFlight) Deferred.doneUnsafe(inFlight, Exit.fail(new CancelledError()));
|
|
237
|
+
emit(cache, {
|
|
238
|
+
query: entry,
|
|
239
|
+
type: "removed"
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Start a request now, or join the one in flight. The returned Effect
|
|
244
|
+
* waits for its result.
|
|
245
|
+
*/
|
|
246
|
+
function begin(entry, request) {
|
|
247
|
+
if (Predicate.isTagged(entry.scope.state, "Closed")) return Effect.fail(new CancelledError());
|
|
248
|
+
const options = entry.options;
|
|
249
|
+
const inFlight = entry.inFlight;
|
|
250
|
+
if (inFlight && !(request.restart && options)) return Deferred.await(inFlight);
|
|
251
|
+
if (!options) {
|
|
252
|
+
const promise = entry.state.promise;
|
|
253
|
+
return Effect.promise(() => promise);
|
|
254
|
+
}
|
|
255
|
+
entry.tasks.cancel("request");
|
|
256
|
+
return Deferred.await(startRequest(entry, runRequest(requestEffect(entry, options, request), entry.hash, entry.services), request.direction));
|
|
257
|
+
}
|
|
258
|
+
function set(entry, patch) {
|
|
259
|
+
const next = {
|
|
260
|
+
...entry.state,
|
|
261
|
+
...patch
|
|
262
|
+
};
|
|
263
|
+
entry.state = {
|
|
264
|
+
...next,
|
|
265
|
+
isStale: computeStale(entry, next)
|
|
266
|
+
};
|
|
267
|
+
for (const listener of entry.listeners.keys()) scheduleNotify(listener);
|
|
268
|
+
emit(entry.cache, {
|
|
269
|
+
query: entry,
|
|
270
|
+
type: "updated"
|
|
271
|
+
});
|
|
272
|
+
scheduleStale(entry);
|
|
273
|
+
}
|
|
274
|
+
/** Run `request` as the entry's request fiber, settling into its awaiter. */
|
|
275
|
+
function startRequest(entry, request, direction) {
|
|
276
|
+
const awaiter = entry.inFlight ?? Deferred.makeUnsafe();
|
|
277
|
+
entry.inFlight = awaiter;
|
|
278
|
+
entry.tasks.cancel("gc");
|
|
279
|
+
if (!entry.state.hasData && entry.state.status === "error") {
|
|
280
|
+
entry.pending = pending();
|
|
281
|
+
set(entry, {
|
|
282
|
+
error: null,
|
|
283
|
+
failureCount: 0,
|
|
284
|
+
fetchDirection: direction,
|
|
285
|
+
isFetching: true,
|
|
286
|
+
promise: entry.pending.promise,
|
|
287
|
+
status: "pending"
|
|
288
|
+
});
|
|
289
|
+
} else set(entry, {
|
|
290
|
+
failureCount: 0,
|
|
291
|
+
fetchDirection: direction,
|
|
292
|
+
isFetching: true
|
|
293
|
+
});
|
|
294
|
+
entry.tasks.fork("request", Effect.exit(request).pipe(Effect.flatMap((exit) => Effect.sync(() => settle(entry, awaiter, exit)))));
|
|
295
|
+
return awaiter;
|
|
296
|
+
}
|
|
297
|
+
function settle(entry, awaiter, exit) {
|
|
298
|
+
if (entry.inFlight !== awaiter) return;
|
|
299
|
+
entry.inFlight = void 0;
|
|
300
|
+
if (Exit.isSuccess(exit)) {
|
|
301
|
+
const data = succeed(entry, exit.value, now(entry));
|
|
302
|
+
Deferred.doneUnsafe(awaiter, Exit.succeed(data));
|
|
303
|
+
} else if (Cause.hasInterruptsOnly(exit.cause)) {
|
|
304
|
+
set(entry, {
|
|
305
|
+
fetchDirection: void 0,
|
|
306
|
+
isFetching: false
|
|
307
|
+
});
|
|
308
|
+
Deferred.doneUnsafe(awaiter, Exit.fail(new CancelledError()));
|
|
309
|
+
} else {
|
|
310
|
+
fail(entry, Cause.squash(exit.cause));
|
|
311
|
+
Deferred.doneUnsafe(awaiter, exit);
|
|
312
|
+
}
|
|
313
|
+
scheduleGc(entry);
|
|
314
|
+
}
|
|
315
|
+
function succeed(entry, next, updatedAt) {
|
|
316
|
+
const previous = entry.state;
|
|
317
|
+
const data = previous.hasData && behavior(entry, "structuralSharing") !== false ? replaceEqualDeep(previous.data, next) : next;
|
|
318
|
+
const firstLoad = entry.pending;
|
|
319
|
+
entry.pending = void 0;
|
|
320
|
+
firstLoad?.resolve(data);
|
|
321
|
+
const unchanged = previous.hasData && data === previous.data;
|
|
322
|
+
set(entry, {
|
|
323
|
+
data,
|
|
324
|
+
error: null,
|
|
325
|
+
failureCount: 0,
|
|
326
|
+
fetchDirection: void 0,
|
|
327
|
+
hasData: true,
|
|
328
|
+
isFetching: false,
|
|
329
|
+
isInvalidated: false,
|
|
330
|
+
promise: firstLoad?.promise ?? (unchanged ? previous.promise : fulfilled(data)),
|
|
331
|
+
status: "success",
|
|
332
|
+
updatedAt
|
|
333
|
+
});
|
|
334
|
+
return data;
|
|
335
|
+
}
|
|
336
|
+
function fail(entry, error) {
|
|
337
|
+
emit(entry.cache, {
|
|
338
|
+
error,
|
|
339
|
+
query: entry,
|
|
340
|
+
type: "failed"
|
|
341
|
+
});
|
|
342
|
+
if (!entry.state.hasData) {
|
|
343
|
+
const firstLoad = entry.pending;
|
|
344
|
+
entry.pending = void 0;
|
|
345
|
+
firstLoad?.reject(error);
|
|
346
|
+
entry.errorShown = false;
|
|
347
|
+
}
|
|
348
|
+
set(entry, {
|
|
349
|
+
error,
|
|
350
|
+
fetchDirection: void 0,
|
|
351
|
+
isFetching: false,
|
|
352
|
+
status: "error"
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* A failed first load rejects its promise, so the reader throws to an error
|
|
357
|
+
* boundary. Every read in that same task (React retries a throwing render
|
|
358
|
+
* once, StrictMode renders twice) gets the rejection too; a read in a later
|
|
359
|
+
* task — the boundary was reset — starts a new request.
|
|
360
|
+
*/
|
|
361
|
+
function markErrorShown(entry) {
|
|
362
|
+
entry.tasks.fork("errorShown", Effect.yieldNow.pipe(Effect.andThen(Effect.sync(() => {
|
|
363
|
+
entry.errorShown = true;
|
|
364
|
+
}))), { onlyIfMissing: true });
|
|
365
|
+
}
|
|
366
|
+
function requestEffect(entry, options, request) {
|
|
367
|
+
const queryKey = options.queryKey;
|
|
368
|
+
if (options.kind === "query") return attempt(entry, (signal) => options.queryFn({
|
|
369
|
+
queryKey,
|
|
370
|
+
signal
|
|
371
|
+
}));
|
|
372
|
+
const current = isInfiniteData(entry.state.data) ? entry.state.data : void 0;
|
|
373
|
+
return fetchPages(options, current, request, (pageParam, direction) => attempt(entry, (signal) => options.queryFn({
|
|
374
|
+
direction,
|
|
375
|
+
pageParam,
|
|
376
|
+
queryKey,
|
|
377
|
+
signal
|
|
378
|
+
})));
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* One `queryFn` call, retried per the query's policy. Every failure counts.
|
|
382
|
+
* Its services are erased with its data type; `runRequest` provides them.
|
|
383
|
+
*/
|
|
384
|
+
function attempt(entry, call) {
|
|
385
|
+
const once = fromQueryFn(call).pipe(Effect.tapError(() => Effect.sync(() => set(entry, { failureCount: entry.state.failureCount + 1 }))));
|
|
386
|
+
const schedule = retrySchedule({
|
|
387
|
+
retry: behavior(entry, "retry"),
|
|
388
|
+
retryDelay: behavior(entry, "retryDelay")
|
|
389
|
+
}, entry.cache.isServer);
|
|
390
|
+
if (!schedule) return once;
|
|
391
|
+
const online = entry.cache.isServer ? Effect.void : whenOnline;
|
|
392
|
+
return Effect.suspend(() => {
|
|
393
|
+
let retrying = false;
|
|
394
|
+
const gated = Effect.suspend(() => {
|
|
395
|
+
const wait = retrying ? online : Effect.void;
|
|
396
|
+
retrying = true;
|
|
397
|
+
return Effect.andThen(wait, once);
|
|
398
|
+
});
|
|
399
|
+
return Effect.retry(gated, schedule);
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
function scheduleGc(entry) {
|
|
403
|
+
const gcTime = behavior(entry, "gcTime") ?? (entry.cache.isServer ? Infinity : 3e5);
|
|
404
|
+
if (isObserved(entry) || entry.inFlight || !Number.isFinite(gcTime)) {
|
|
405
|
+
entry.tasks.cancel("gc");
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
entry.tasks.fork("gc", Effect.sleep(gcTime).pipe(Effect.andThen(Effect.sync(() => remove(entry)))));
|
|
409
|
+
}
|
|
410
|
+
function scheduleStale(entry) {
|
|
411
|
+
const state = entry.state;
|
|
412
|
+
const staleTime = behavior(entry, "staleTime") ?? 0;
|
|
413
|
+
if (entry.cache.isServer || !isObserved(entry) || state.isStale || !Number.isFinite(staleTime)) {
|
|
414
|
+
entry.tasks.cancel("stale");
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
const remaining = state.updatedAt + staleTime - now(entry);
|
|
418
|
+
entry.tasks.fork("stale", Effect.sleep(Math.max(remaining, 0) + 1).pipe(Effect.andThen(Effect.sync(() => set(entry, {})))));
|
|
419
|
+
}
|
|
420
|
+
//#endregion
|
|
421
|
+
export { CancelledError, TypeId, changes, emit, entryOf, fetch, hasNextPageUnsafe, hasPreviousPageUnsafe, invalidate, isObserved, isQuery, isStaleUnsafe, make, now, onPlatformEvent, provide, readUnsafe, remove, reset, resume, setData, setOptions, start, subscribeUnsafe };
|
package/dist/hydration.d.mts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { QueryKey } from "./key.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { QueryFactory } from "./options.mjs";
|
|
3
|
+
import "./cache.mjs";
|
|
4
|
+
import "./entry.mjs";
|
|
5
|
+
import { Query, query_d_exports } from "./query.mjs";
|
|
6
|
+
import { Cause, Effect, Scope, Stream } from "effect";
|
|
4
7
|
//#region src/hydration.d.ts
|
|
5
8
|
/**
|
|
6
9
|
* One cache entry in transit. Settled entries carry `data`; entries still
|
|
@@ -17,6 +20,16 @@ export interface DehydratedQuery {
|
|
|
17
20
|
export interface DehydratedState {
|
|
18
21
|
readonly queries: ReadonlyArray<DehydratedQuery>;
|
|
19
22
|
}
|
|
23
|
+
/** The cache now, then entries as they start or settle. */
|
|
24
|
+
export interface DehydratedStream {
|
|
25
|
+
/** The entries to send first. */
|
|
26
|
+
readonly initial: DehydratedState;
|
|
27
|
+
/**
|
|
28
|
+
* Later entries, in batches. Ends once the scope `dehydrateStream` ran in
|
|
29
|
+
* closes, after a last batch of what changed before. One consumer only.
|
|
30
|
+
*/
|
|
31
|
+
readonly updates: Stream.Stream<DehydratedState>;
|
|
32
|
+
}
|
|
20
33
|
export interface DehydrateOptions {
|
|
21
34
|
/**
|
|
22
35
|
* Which entries to send. Default: entries with data or a request in
|
|
@@ -25,10 +38,16 @@ export interface DehydrateOptions {
|
|
|
25
38
|
readonly filter?: (query: Query) => boolean;
|
|
26
39
|
/**
|
|
27
40
|
* Send a pending query's rejection as-is. By default the receiving side
|
|
28
|
-
* gets a
|
|
29
|
-
* error boundary can still retry on the client.
|
|
41
|
+
* gets a {@link RedactedError}, so server errors don't leak into the page;
|
|
42
|
+
* its error boundary can still retry on the client.
|
|
30
43
|
*/
|
|
31
44
|
readonly keepErrors?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Hashes of entries sent already, shared by every dehydration of one
|
|
47
|
+
* transfer (e.g. one request). Those entries are skipped, and the ones
|
|
48
|
+
* sent are added.
|
|
49
|
+
*/
|
|
50
|
+
readonly sent?: Set<string>;
|
|
32
51
|
/** Transform data before it is serialized. */
|
|
33
52
|
readonly serializeData?: (data: unknown) => unknown;
|
|
34
53
|
}
|
|
@@ -36,15 +55,20 @@ export interface HydrateOptions {
|
|
|
36
55
|
/** Reverse `serializeData`. */
|
|
37
56
|
readonly deserializeData?: (data: unknown) => unknown;
|
|
38
57
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
export declare function dehydrateQuery(query: Query, options?: DehydrateOptions): DehydratedQuery;
|
|
43
|
-
/** Snapshot the cache for transfer to another client (server → browser). */
|
|
44
|
-
export declare function dehydrate(client: QueryClient, options?: DehydrateOptions): DehydratedState;
|
|
58
|
+
declare const RedactedError_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => Cause.YieldableError & {
|
|
59
|
+
readonly _tag: "RedactedError";
|
|
60
|
+
} & Readonly<A>;
|
|
45
61
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
62
|
+
* What a dehydrated pending query rejects with when its request failed and
|
|
63
|
+
* `keepErrors` is off: the receiving side learns which query failed, not why.
|
|
48
64
|
*/
|
|
49
|
-
export declare
|
|
65
|
+
export declare class RedactedError extends RedactedError_base<{
|
|
66
|
+
readonly hash: string;
|
|
67
|
+
}> {
|
|
68
|
+
get message(): string;
|
|
69
|
+
}
|
|
70
|
+
/** The default dehydrate filter: entries with data or a request in flight. */
|
|
71
|
+
export declare function canDehydrate<TData, TFactory extends QueryFactory, TError>(query: Query<TData, TFactory, TError>): boolean;
|
|
72
|
+
/** Snapshot one entry for transfer. */
|
|
73
|
+
export declare function dehydrateQuery<TData, TFactory extends QueryFactory, TError>(query: Query<TData, TFactory, TError>, options?: DehydrateOptions): Effect.Effect<DehydratedQuery>;
|
|
50
74
|
//#endregion
|