@signalium/query 0.0.2 → 1.0.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 +21 -0
- package/dist/cjs/EntityMap.js +2 -2
- package/dist/cjs/EntityMap.js.map +1 -1
- package/dist/cjs/NetworkManager.js +105 -0
- package/dist/cjs/NetworkManager.js.map +1 -0
- package/dist/cjs/QueryClient.js +390 -76
- package/dist/cjs/QueryClient.js.map +1 -1
- package/dist/cjs/QueryStore.js +295 -3
- package/dist/cjs/QueryStore.js.map +1 -1
- package/dist/cjs/index.js +16 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/parseEntities.js +3 -0
- package/dist/cjs/parseEntities.js.map +1 -1
- package/dist/cjs/proxy.js +19 -0
- package/dist/cjs/proxy.js.map +1 -1
- package/dist/cjs/query.js +40 -2
- package/dist/cjs/query.js.map +1 -1
- package/dist/cjs/stores/async.js +6 -0
- package/dist/cjs/stores/async.js.map +1 -0
- package/dist/cjs/stores/sync.js +7 -0
- package/dist/cjs/stores/sync.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/cjs/type-utils.js +3 -0
- package/dist/cjs/type-utils.js.map +1 -0
- package/dist/cjs/types.js +19 -1
- package/dist/cjs/types.js.map +1 -1
- package/dist/esm/EntityMap.js +3 -3
- package/dist/esm/EntityMap.js.map +1 -1
- package/dist/esm/NetworkManager.d.ts +48 -0
- package/dist/esm/NetworkManager.d.ts.map +1 -0
- package/dist/esm/NetworkManager.js +101 -0
- package/dist/esm/NetworkManager.js.map +1 -0
- package/dist/esm/QueryClient.d.ts +81 -25
- package/dist/esm/QueryClient.d.ts.map +1 -1
- package/dist/esm/QueryClient.js +390 -76
- package/dist/esm/QueryClient.js.map +1 -1
- package/dist/esm/QueryStore.d.ts +64 -2
- package/dist/esm/QueryStore.d.ts.map +1 -1
- package/dist/esm/QueryStore.js +293 -2
- package/dist/esm/QueryStore.js.map +1 -1
- package/dist/esm/index.d.ts +5 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/parseEntities.d.ts.map +1 -1
- package/dist/esm/parseEntities.js +3 -0
- package/dist/esm/parseEntities.js.map +1 -1
- package/dist/esm/proxy.d.ts +6 -0
- package/dist/esm/proxy.d.ts.map +1 -1
- package/dist/esm/proxy.js +18 -0
- package/dist/esm/proxy.js.map +1 -1
- package/dist/esm/query.d.ts +30 -29
- package/dist/esm/query.d.ts.map +1 -1
- package/dist/esm/query.js +39 -3
- package/dist/esm/query.js.map +1 -1
- package/dist/esm/stores/async.d.ts +2 -0
- package/dist/esm/stores/async.d.ts.map +1 -0
- package/dist/esm/stores/async.js +2 -0
- package/dist/esm/stores/async.js.map +1 -0
- package/dist/esm/stores/sync.d.ts +2 -0
- package/dist/esm/stores/sync.d.ts.map +1 -0
- package/dist/esm/stores/sync.js +2 -0
- package/dist/esm/stores/sync.js.map +1 -0
- package/dist/esm/type-utils.d.ts +12 -0
- package/dist/esm/type-utils.d.ts.map +1 -0
- package/dist/esm/type-utils.js +2 -0
- package/dist/esm/type-utils.js.map +1 -0
- package/dist/esm/types.d.ts +62 -5
- package/dist/esm/types.d.ts.map +1 -1
- package/dist/esm/types.js +18 -0
- package/dist/esm/types.js.map +1 -1
- package/index.d.ts +1 -0
- package/package.json +25 -7
- package/stores/async.d.ts +1 -0
- package/stores/async.js +15 -0
- package/stores/sync.d.ts +1 -0
- package/stores/sync.js +15 -0
- package/.turbo/turbo-build.log +0 -12
- package/ENTITY_STORE_DESIGN.md +0 -386
- package/dist/tsconfig.esm.tsbuildinfo +0 -1
- package/src/EntityMap.ts +0 -63
- package/src/QueryClient.ts +0 -482
- package/src/QueryStore.ts +0 -322
- package/src/__tests__/caching-persistence.test.ts +0 -983
- package/src/__tests__/entity-system.test.ts +0 -556
- package/src/__tests__/gc-time.test.ts +0 -327
- package/src/__tests__/mock-fetch.test.ts +0 -186
- package/src/__tests__/parse-entities.test.ts +0 -425
- package/src/__tests__/path-interpolation.test.ts +0 -225
- package/src/__tests__/reactivity.test.ts +0 -424
- package/src/__tests__/refetch-interval.test.ts +0 -262
- package/src/__tests__/rest-query-api.test.ts +0 -568
- package/src/__tests__/stale-time.test.ts +0 -357
- package/src/__tests__/type-to-string.test.ts +0 -129
- package/src/__tests__/utils.ts +0 -258
- package/src/__tests__/validation-edge-cases.test.ts +0 -821
- package/src/errors.ts +0 -124
- package/src/index.ts +0 -7
- package/src/parseEntities.ts +0 -213
- package/src/pathInterpolator.ts +0 -74
- package/src/proxy.ts +0 -257
- package/src/query.ts +0 -164
- package/src/react/__tests__/basic.test.tsx +0 -926
- package/src/react/__tests__/component.test.tsx +0 -984
- package/src/react/__tests__/utils.tsx +0 -71
- package/src/typeDefs.ts +0 -351
- package/src/types.ts +0 -132
- package/src/utils.ts +0 -66
- package/tsconfig.cjs.json +0 -14
- package/tsconfig.esm.json +0 -13
- package/tsconfig.json +0 -20
- package/vitest.config.ts +0 -65
package/src/QueryClient.ts
DELETED
|
@@ -1,482 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Query Client with Entity Caching and Deduplication
|
|
3
|
-
*
|
|
4
|
-
* Features:
|
|
5
|
-
* - Global entity map for deduplication
|
|
6
|
-
* - Entity definitions with cached sub-entity paths
|
|
7
|
-
* - Eager entity discovery and caching
|
|
8
|
-
* - Permanent proxy cache for entities
|
|
9
|
-
* - Response caching for offline access
|
|
10
|
-
* - Signalium-based reactivity for entity updates
|
|
11
|
-
* - Self-contained validator (no external dependencies except Signalium)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { relay, type RelayState, context, DiscriminatedReactivePromise, type Context, Signal, signal } from 'signalium';
|
|
15
|
-
import { hashValue, setReactivePromise } from 'signalium/utils';
|
|
16
|
-
import {
|
|
17
|
-
DiscriminatedQueryResult,
|
|
18
|
-
EntityDef,
|
|
19
|
-
QueryResult,
|
|
20
|
-
ObjectFieldTypeDef,
|
|
21
|
-
ComplexTypeDef,
|
|
22
|
-
RefetchInterval,
|
|
23
|
-
} from './types.js';
|
|
24
|
-
import { parseValue } from './proxy.js';
|
|
25
|
-
import { parseEntities } from './parseEntities.js';
|
|
26
|
-
import { EntityRecord, EntityStore } from './EntityMap.js';
|
|
27
|
-
import { QueryStore } from './QueryStore.js';
|
|
28
|
-
import { ValidatorDef } from './typeDefs.js';
|
|
29
|
-
|
|
30
|
-
export interface QueryContext {
|
|
31
|
-
fetch: typeof fetch;
|
|
32
|
-
evictionMultiplier?: number;
|
|
33
|
-
refetchMultiplier?: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface QueryCacheOptions {
|
|
37
|
-
maxCount?: number;
|
|
38
|
-
gcTime?: number; // milliseconds - only applies to on-disk/persistent storage cleanup
|
|
39
|
-
staleTime?: number;
|
|
40
|
-
refetchInterval?: RefetchInterval;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface QueryDefinition<Params, Result> {
|
|
44
|
-
id: string;
|
|
45
|
-
shape: ObjectFieldTypeDef;
|
|
46
|
-
fetchFn: (context: QueryContext, params: Params) => Promise<Result>;
|
|
47
|
-
|
|
48
|
-
cache?: QueryCacheOptions;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// QueryInstance is now merged into QueryResultImpl below
|
|
52
|
-
|
|
53
|
-
const queryKeyFor = (queryDef: QueryDefinition<any, any>, params: unknown): number => {
|
|
54
|
-
return hashValue([queryDef.id, params]);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
const BASE_TICK_INTERVAL = 1000; // 1 second
|
|
58
|
-
|
|
59
|
-
// Refetch interval manager - uses a fixed 1-second tick
|
|
60
|
-
class RefetchManager {
|
|
61
|
-
private intervalId: NodeJS.Timeout;
|
|
62
|
-
private clock: number = 0; // Increments by 1000ms on each tick
|
|
63
|
-
|
|
64
|
-
// Buckets: Map of actual interval -> Set of query instances
|
|
65
|
-
private buckets = new Map<number, Set<QueryResultImpl<any>>>();
|
|
66
|
-
|
|
67
|
-
constructor(private multiplier: number = 1) {
|
|
68
|
-
// Start the timer immediately and keep it running
|
|
69
|
-
const tickInterval = BASE_TICK_INTERVAL * this.multiplier;
|
|
70
|
-
this.intervalId = setTimeout(() => this.tick(), tickInterval);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
addQuery(instance: QueryResultImpl<any>) {
|
|
74
|
-
const interval = instance.def.cache?.refetchInterval;
|
|
75
|
-
|
|
76
|
-
if (!interval) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const actualInterval = interval * this.multiplier;
|
|
81
|
-
// Add to bucket by actual interval
|
|
82
|
-
let bucket = this.buckets.get(actualInterval);
|
|
83
|
-
if (!bucket) {
|
|
84
|
-
bucket = new Set();
|
|
85
|
-
this.buckets.set(actualInterval, bucket);
|
|
86
|
-
}
|
|
87
|
-
bucket.add(instance);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
removeQuery(query: QueryResultImpl<any>) {
|
|
91
|
-
const interval = query.def.cache?.refetchInterval;
|
|
92
|
-
|
|
93
|
-
if (!interval) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const actualInterval = interval * this.multiplier;
|
|
98
|
-
// Remove from bucket
|
|
99
|
-
const bucket = this.buckets.get(actualInterval);
|
|
100
|
-
if (bucket) {
|
|
101
|
-
bucket.delete(query);
|
|
102
|
-
|
|
103
|
-
if (bucket.size === 0) {
|
|
104
|
-
this.buckets.delete(actualInterval);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private tick() {
|
|
110
|
-
this.clock += BASE_TICK_INTERVAL * this.multiplier;
|
|
111
|
-
|
|
112
|
-
// Only process buckets where clock is aligned with the interval
|
|
113
|
-
for (const [interval, bucket] of this.buckets.entries()) {
|
|
114
|
-
if (this.clock % interval === 0) {
|
|
115
|
-
// Process all queries in this bucket
|
|
116
|
-
for (const query of bucket) {
|
|
117
|
-
// Skip if already fetching - let the current fetch complete
|
|
118
|
-
if (query && !query.isFetching) {
|
|
119
|
-
query.refetch();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const tickInterval = BASE_TICK_INTERVAL * this.multiplier;
|
|
126
|
-
this.intervalId = setTimeout(() => this.tick(), tickInterval);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
destroy(): void {
|
|
130
|
-
clearTimeout(this.intervalId);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
const EVICTION_INTERVAL = 60 * 1000; // 1 minute
|
|
135
|
-
|
|
136
|
-
// Memory eviction manager - uses a single interval with rotating sets to avoid timeout overhead
|
|
137
|
-
class MemoryEvictionManager {
|
|
138
|
-
private intervalId: NodeJS.Timeout;
|
|
139
|
-
private currentFlush = new Set<number>(); // Queries to evict on next tick
|
|
140
|
-
private nextFlush = new Set<number>(); // Queries to evict on tick after next
|
|
141
|
-
|
|
142
|
-
constructor(
|
|
143
|
-
private queryClient: QueryClient,
|
|
144
|
-
private multiplier: number = 1,
|
|
145
|
-
) {
|
|
146
|
-
this.intervalId = setInterval(this.tick, EVICTION_INTERVAL * this.multiplier);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
scheduleEviction(queryKey: number) {
|
|
150
|
-
// Add to nextFlush so it waits at least one full interval
|
|
151
|
-
// This prevents immediate eviction if scheduled right before a tick
|
|
152
|
-
this.nextFlush.add(queryKey);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
cancelEviction(queryKey: number) {
|
|
156
|
-
// Remove from both sets to handle reactivation
|
|
157
|
-
this.currentFlush.delete(queryKey);
|
|
158
|
-
this.nextFlush.delete(queryKey);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private tick = () => {
|
|
162
|
-
if (!this.queryClient) return;
|
|
163
|
-
|
|
164
|
-
// Evict all queries in currentFlush
|
|
165
|
-
for (const queryKey of this.currentFlush) {
|
|
166
|
-
this.queryClient.queryInstances.delete(queryKey);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Rotate: currentFlush becomes nextFlush, nextFlush becomes empty
|
|
170
|
-
this.currentFlush = this.nextFlush;
|
|
171
|
-
this.nextFlush = new Set();
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
destroy(): void {
|
|
175
|
-
clearInterval(this.intervalId);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* QueryResult wraps a DiscriminatedReactivePromise and adds additional functionality
|
|
181
|
-
* like refetch, while forwarding all the base relay properties.
|
|
182
|
-
* This class combines the old QueryInstance and QueryResultImpl into a single entity.
|
|
183
|
-
*/
|
|
184
|
-
export class QueryResultImpl<T> implements QueryResult<T> {
|
|
185
|
-
// Fields from old QueryInstance
|
|
186
|
-
def: QueryDefinition<any, any>;
|
|
187
|
-
initialized: boolean = false;
|
|
188
|
-
isRefetchingSignal: Signal<boolean>;
|
|
189
|
-
updatedAt: number | undefined = undefined;
|
|
190
|
-
|
|
191
|
-
// References for refetch functionality
|
|
192
|
-
private queryClient: QueryClient;
|
|
193
|
-
queryKey: number;
|
|
194
|
-
private params: any;
|
|
195
|
-
private relay: DiscriminatedReactivePromise<T>;
|
|
196
|
-
private relayState: RelayState<any> | undefined = undefined;
|
|
197
|
-
|
|
198
|
-
constructor(def: QueryDefinition<any, any>, queryClient: QueryClient, queryKey: number, params: any) {
|
|
199
|
-
setReactivePromise(this);
|
|
200
|
-
this.def = def;
|
|
201
|
-
this.queryClient = queryClient;
|
|
202
|
-
this.queryKey = queryKey;
|
|
203
|
-
this.params = params;
|
|
204
|
-
this.isRefetchingSignal = signal(false);
|
|
205
|
-
|
|
206
|
-
// Create the relay and handle activation/deactivation
|
|
207
|
-
this.relay = relay<T>(
|
|
208
|
-
state => {
|
|
209
|
-
this.relayState = state;
|
|
210
|
-
// Load from cache first, then fetch fresh data
|
|
211
|
-
this.queryClient.activateQuery(this);
|
|
212
|
-
|
|
213
|
-
if (this.initialized) {
|
|
214
|
-
if (this.isStale()) {
|
|
215
|
-
this.refetch();
|
|
216
|
-
}
|
|
217
|
-
} else {
|
|
218
|
-
this.initialize(state as RelayState<unknown>);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Return deactivation callback
|
|
222
|
-
return {
|
|
223
|
-
update: () => {
|
|
224
|
-
state.setPromise(this.runQuery());
|
|
225
|
-
},
|
|
226
|
-
deactivate: () => {
|
|
227
|
-
// Last subscriber left, deactivate refetch and schedule memory eviction
|
|
228
|
-
if (this.def.cache?.refetchInterval) {
|
|
229
|
-
this.queryClient.refetchManager.removeQuery(this);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// Schedule removal from memory using the global eviction manager
|
|
233
|
-
// This allows quick reactivation from memory if needed again soon
|
|
234
|
-
// Disk cache (if configured) will still be available after eviction
|
|
235
|
-
this.queryClient.memoryEvictionManager.scheduleEviction(this.queryKey);
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
},
|
|
239
|
-
// {
|
|
240
|
-
// equals: false,
|
|
241
|
-
// },
|
|
242
|
-
) as DiscriminatedReactivePromise<T>;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
get value(): T | undefined {
|
|
246
|
-
return this.relay.value;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
get error(): unknown {
|
|
250
|
-
return this.relay.error;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
get isPending(): boolean {
|
|
254
|
-
return this.relay.isPending;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
get isRejected(): boolean {
|
|
258
|
-
return this.relay.isRejected;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
get isResolved(): boolean {
|
|
262
|
-
return this.relay.isResolved;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
get isSettled(): boolean {
|
|
266
|
-
return this.relay.isSettled;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
get isReady(): boolean {
|
|
270
|
-
return this.relay.isReady;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
get isRefetching(): boolean {
|
|
274
|
-
return this.isRefetchingSignal.value;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
get isFetching(): boolean {
|
|
278
|
-
return this.relay.isPending || this.isRefetching;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// TODO: Intimate APIs needed for `useReactive`, this is a code smell and
|
|
282
|
-
// we should find a better way to entangle these more generically
|
|
283
|
-
private get _version(): Signal<number> {
|
|
284
|
-
return (this.relay as any)._version;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
private get _signal(): Signal<T> {
|
|
288
|
-
return (this.relay as any)._signal;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
private get _flags(): number {
|
|
292
|
-
return (this.relay as any)._flags;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Forward Promise methods
|
|
296
|
-
then<TResult1 = T, TResult2 = never>(
|
|
297
|
-
onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null | undefined,
|
|
298
|
-
onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null | undefined,
|
|
299
|
-
): Promise<TResult1 | TResult2> {
|
|
300
|
-
return this.relay.then(onfulfilled, onrejected);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
catch<TResult = never>(
|
|
304
|
-
onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null | undefined,
|
|
305
|
-
): Promise<T | TResult> {
|
|
306
|
-
return this.relay.catch(onrejected);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
finally(onfinally?: (() => void) | null | undefined): Promise<T> {
|
|
310
|
-
return this.relay.finally(onfinally);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Additional methods
|
|
314
|
-
async refetch(): Promise<T> {
|
|
315
|
-
this.isRefetchingSignal.value = true;
|
|
316
|
-
|
|
317
|
-
try {
|
|
318
|
-
const result = await this.runQuery();
|
|
319
|
-
|
|
320
|
-
if (this.relayState) {
|
|
321
|
-
this.relayState.value = result;
|
|
322
|
-
|
|
323
|
-
// Update the version to trigger a re-render for direct React consumers
|
|
324
|
-
// e.g. `useReactive(query, params)`
|
|
325
|
-
this._version.update(v => v + 1);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return result;
|
|
329
|
-
} finally {
|
|
330
|
-
this.isRefetchingSignal.value = false;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Fetches fresh data, updates the cache, and updates updatedAt timestamp
|
|
336
|
-
*/
|
|
337
|
-
async runQuery(): Promise<T> {
|
|
338
|
-
const freshData = await this.def.fetchFn(this.queryClient.getContext(), this.params);
|
|
339
|
-
|
|
340
|
-
// Parse and cache the fresh data
|
|
341
|
-
const entityRefs = new Set<number>();
|
|
342
|
-
const shape = this.def.shape;
|
|
343
|
-
|
|
344
|
-
const parsedData =
|
|
345
|
-
shape instanceof ValidatorDef
|
|
346
|
-
? parseEntities(freshData, shape as ComplexTypeDef, this.queryClient, entityRefs)
|
|
347
|
-
: parseValue(freshData, shape, this.def.id);
|
|
348
|
-
|
|
349
|
-
// Cache the data (synchronous, fire-and-forget)
|
|
350
|
-
this.queryClient.saveQueryData(this.def, this.queryKey, freshData, entityRefs);
|
|
351
|
-
|
|
352
|
-
// Update the timestamp
|
|
353
|
-
this.updatedAt = Date.now();
|
|
354
|
-
|
|
355
|
-
return parsedData as T;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
isStale(): boolean {
|
|
359
|
-
if (this.updatedAt === undefined) {
|
|
360
|
-
return true; // No data yet, needs fetch
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const staleTime = this.def.cache?.staleTime ?? 0;
|
|
364
|
-
return Date.now() - this.updatedAt >= staleTime;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
/**
|
|
368
|
-
* Initialize the query by loading from cache and fetching if stale
|
|
369
|
-
*/
|
|
370
|
-
private async initialize(state: RelayState<unknown>): Promise<void> {
|
|
371
|
-
try {
|
|
372
|
-
this.initialized = true;
|
|
373
|
-
// Load from cache first
|
|
374
|
-
const cached = await this.queryClient.loadCachedQuery(this.def, this.queryKey);
|
|
375
|
-
|
|
376
|
-
if (cached !== undefined) {
|
|
377
|
-
const shape = this.def.shape;
|
|
378
|
-
state.value =
|
|
379
|
-
shape instanceof ValidatorDef
|
|
380
|
-
? parseEntities(cached.value, shape as ComplexTypeDef, this.queryClient, new Set())
|
|
381
|
-
: parseValue(cached.value, shape, this.def.id);
|
|
382
|
-
|
|
383
|
-
// Set the cached timestamp
|
|
384
|
-
this.updatedAt = cached.updatedAt;
|
|
385
|
-
|
|
386
|
-
// Check if data is stale
|
|
387
|
-
if (this.isStale()) {
|
|
388
|
-
// Data is stale, trigger background refetch
|
|
389
|
-
this.refetch();
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
// No cached data, fetch fresh
|
|
393
|
-
state.setPromise(this.runQuery());
|
|
394
|
-
}
|
|
395
|
-
} catch (error) {
|
|
396
|
-
// Relay will handle the error state automatically
|
|
397
|
-
state.setError(error as Error);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
// Make it work with Symbol.toStringTag for Promise detection
|
|
402
|
-
get [Symbol.toStringTag](): string {
|
|
403
|
-
return 'QueryResult';
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
export class QueryClient {
|
|
408
|
-
private entityMap = new EntityStore();
|
|
409
|
-
queryInstances = new Map<number, QueryResultImpl<unknown>>();
|
|
410
|
-
memoryEvictionManager: MemoryEvictionManager;
|
|
411
|
-
refetchManager: RefetchManager;
|
|
412
|
-
|
|
413
|
-
constructor(
|
|
414
|
-
private store: QueryStore,
|
|
415
|
-
private context: QueryContext = { fetch },
|
|
416
|
-
) {
|
|
417
|
-
this.memoryEvictionManager = new MemoryEvictionManager(this, this.context.evictionMultiplier);
|
|
418
|
-
this.refetchManager = new RefetchManager(this.context.refetchMultiplier);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
getContext(): QueryContext {
|
|
422
|
-
return this.context;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
saveQueryData(queryDef: QueryDefinition<any, any>, queryKey: number, data: unknown, entityRefs: Set<number>): void {
|
|
426
|
-
this.store.saveQuery(queryDef, queryKey, data, entityRefs);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
activateQuery(queryInstance: QueryResultImpl<unknown>): void {
|
|
430
|
-
const { def, queryKey } = queryInstance;
|
|
431
|
-
this.store.activateQuery(def, queryKey);
|
|
432
|
-
|
|
433
|
-
this.refetchManager.addQuery(queryInstance);
|
|
434
|
-
this.memoryEvictionManager.cancelEviction(queryKey);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
loadCachedQuery(queryDef: QueryDefinition<any, any>, queryKey: number) {
|
|
438
|
-
return this.store.loadQuery(queryDef, queryKey, this.entityMap);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Loads a query from the document store and returns a QueryResult
|
|
443
|
-
* that triggers fetches and prepopulates with cached data
|
|
444
|
-
*/
|
|
445
|
-
getQuery<Params, Result>(
|
|
446
|
-
queryDef: QueryDefinition<Params, Result>,
|
|
447
|
-
params: Params,
|
|
448
|
-
): DiscriminatedQueryResult<Result> {
|
|
449
|
-
const queryKey = queryKeyFor(queryDef, params);
|
|
450
|
-
|
|
451
|
-
let queryInstance = this.queryInstances.get(queryKey) as QueryResultImpl<Result> | undefined;
|
|
452
|
-
|
|
453
|
-
// Create a new instance if it doesn't exist
|
|
454
|
-
if (queryInstance === undefined) {
|
|
455
|
-
queryInstance = new QueryResultImpl(queryDef, this, queryKey, params);
|
|
456
|
-
|
|
457
|
-
// Store for future use
|
|
458
|
-
this.queryInstances.set(queryKey, queryInstance as QueryResultImpl<unknown>);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return queryInstance as DiscriminatedQueryResult<Result>;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
hydrateEntity(key: number, shape: EntityDef): EntityRecord {
|
|
465
|
-
return this.entityMap.hydratePreloadedEntity(key, shape);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
saveEntity(key: number, obj: Record<string, unknown>, shape: EntityDef, entityRefs?: Set<number>): EntityRecord {
|
|
469
|
-
const record = this.entityMap.setEntity(key, obj, shape);
|
|
470
|
-
|
|
471
|
-
this.store.saveEntity(key, obj, entityRefs);
|
|
472
|
-
|
|
473
|
-
return record;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
destroy(): void {
|
|
477
|
-
this.refetchManager.destroy();
|
|
478
|
-
this.memoryEvictionManager.destroy();
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
export const QueryClientContext: Context<QueryClient | undefined> = context<QueryClient | undefined>(undefined);
|