@choksheak/react-utils 0.2.0 → 0.2.2
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/docs/assets/navigation.js +1 -1
- package/docs/assets/search.js +1 -1
- package/docs/classes/sharedQuery.SharedQuery.html +10 -8
- package/docs/classes/sharedState.SharedState.html +2 -2
- package/docs/classes/utils_storage.InMemoryStorageAdapter.html +2 -2
- package/docs/classes/utils_storage.IndexedDbAdapter.html +2 -2
- package/docs/classes/utils_storage.LocalStorageAdapter.html +2 -2
- package/docs/functions/sharedQuery.sharedQuery.html +2 -2
- package/docs/functions/sharedQuery.useSharedQuery.html +4 -0
- package/docs/functions/sharedState.sharedState.html +1 -1
- package/docs/functions/sharedState.useSharedState.html +1 -1
- package/docs/functions/sharedState.useSharedStateSetter.html +1 -1
- package/docs/functions/sharedState.useSharedStateValue.html +1 -1
- package/docs/functions/utils_storage.getStorageAdapter.html +1 -1
- package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +1 -1
- package/docs/modules/sharedQuery.html +1 -1
- package/docs/modules/sharedState.html +1 -1
- package/docs/types/sharedQuery.FetchFn.html +1 -1
- package/docs/types/sharedQuery.PersistTo.html +1 -0
- package/docs/types/sharedQuery.QueryStateValue.html +5 -3
- package/docs/types/sharedQuery.SharedQueryOptions.html +1 -1
- package/docs/types/sharedQuery.SharedQueryState.html +2 -2
- package/docs/types/sharedQuery.UseQueryResult.html +1 -0
- package/docs/types/sharedState.SharedStateOptions.html +1 -1
- package/docs/types/utils_storage.StorageAdapter.html +2 -2
- package/docs/types/utils_storage.StorageOptions.html +2 -2
- package/docs/variables/fetcher.fetcher.html +1 -1
- package/docs/variables/fetcher.fetcherWithRetries.html +1 -1
- package/docs/variables/sharedQuery.SharedQueryDefaults.html +2 -0
- package/docs/variables/sharedState.SharedStateDefaults.html +6 -0
- package/package.json +1 -1
- package/sharedQuery.d.mts +48 -33
- package/sharedQuery.d.ts +46 -32
- package/sharedQuery.js +121 -96
- package/sharedQuery.mjs +120 -96
- package/sharedState.d.mts +5 -5
- package/sharedState.d.ts +4 -4
- package/sharedState.js +10 -8
- package/sharedState.mjs +9 -7
- package/src/sharedQuery.ts +181 -131
- package/src/sharedState.ts +10 -7
- package/src/utils/stringify.test.ts +8 -0
- package/src/utils/stringify.ts +1 -1
- package/utils/stringify.js +1 -1
- package/utils/stringify.mjs +1 -1
- package/utils/stringify.test.js +6 -1
- package/utils/stringify.test.mjs +6 -1
- package/docs/classes/sharedState.SharedStateConfig.html +0 -9
- package/docs/functions/sharedQuery.useQuery.html +0 -4
- package/docs/types/sharedQuery.FetchedData.html +0 -3
package/sharedQuery.js
CHANGED
|
@@ -21,15 +21,15 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var sharedQuery_exports = {};
|
|
22
22
|
__export(sharedQuery_exports, {
|
|
23
23
|
SharedQuery: () => SharedQuery,
|
|
24
|
+
SharedQueryDefaults: () => SharedQueryDefaults,
|
|
24
25
|
sharedQuery: () => sharedQuery,
|
|
25
|
-
|
|
26
|
+
useSharedQuery: () => useSharedQuery
|
|
26
27
|
});
|
|
27
28
|
module.exports = __toCommonJS(sharedQuery_exports);
|
|
28
|
-
var
|
|
29
|
+
var import_timeConstants2 = require("@choksheak/ts-utils/timeConstants");
|
|
29
30
|
var import_react2 = require("react");
|
|
30
31
|
|
|
31
32
|
// src/sharedState.ts
|
|
32
|
-
var import_kvStore2 = require("@choksheak/ts-utils/kvStore");
|
|
33
33
|
var import_timeConstants = require("@choksheak/ts-utils/timeConstants");
|
|
34
34
|
var import_react = require("react");
|
|
35
35
|
|
|
@@ -134,17 +134,17 @@ var IndexedDbAdapter = class {
|
|
|
134
134
|
};
|
|
135
135
|
|
|
136
136
|
// src/sharedState.ts
|
|
137
|
-
var
|
|
137
|
+
var SharedStateDefaults = {
|
|
138
138
|
/**
|
|
139
139
|
* Default duration for storage expiration in milliseconds. Initial value
|
|
140
140
|
* is 30 days, but can be changed to anything.
|
|
141
141
|
*/
|
|
142
|
-
|
|
142
|
+
storeExpiryMs: 30 * import_timeConstants.MS_PER_DAY,
|
|
143
143
|
/**
|
|
144
144
|
* True to load from storage only on mount, false (default) to load in
|
|
145
145
|
* the top level scope when the shared state is defined.
|
|
146
146
|
*/
|
|
147
|
-
|
|
147
|
+
lazyLoad: false
|
|
148
148
|
};
|
|
149
149
|
var PubSubStore = class {
|
|
150
150
|
dataByKey = /* @__PURE__ */ new Map();
|
|
@@ -199,11 +199,14 @@ var SharedState = class {
|
|
|
199
199
|
constructor(defaultValue, options) {
|
|
200
200
|
this.defaultValue = defaultValue;
|
|
201
201
|
this.pubSubKey = String(stateKey++);
|
|
202
|
-
this.storageAdapter = getStorageAdapter(
|
|
202
|
+
this.storageAdapter = getStorageAdapter(
|
|
203
|
+
options,
|
|
204
|
+
SharedStateDefaults.storeExpiryMs
|
|
205
|
+
);
|
|
203
206
|
this.setValueBounded = (arg) => this.setValue(arg);
|
|
204
207
|
pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
|
|
205
208
|
const lazyLoad = options?.lazyLoad;
|
|
206
|
-
const lazy = lazyLoad !== void 0 ? lazyLoad :
|
|
209
|
+
const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
|
|
207
210
|
if (!lazy) {
|
|
208
211
|
void this.initDefaultValueOnce();
|
|
209
212
|
}
|
|
@@ -265,7 +268,7 @@ function stringifyDeterministicForKeys(params) {
|
|
|
265
268
|
if (typeof value === "function") {
|
|
266
269
|
throw new Error(`Functions should not be passed into query parameters`);
|
|
267
270
|
}
|
|
268
|
-
if (!value || typeof value !== "object") {
|
|
271
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
269
272
|
return value;
|
|
270
273
|
}
|
|
271
274
|
if (cache.has(value)) {
|
|
@@ -290,14 +293,31 @@ function stringifyDeterministicForKeys(params) {
|
|
|
290
293
|
}
|
|
291
294
|
|
|
292
295
|
// src/sharedQuery.ts
|
|
293
|
-
var
|
|
296
|
+
var SharedQueryDefaults = {
|
|
297
|
+
staleMs: 0,
|
|
298
|
+
expiryMs: 30 * import_timeConstants2.MS_PER_DAY,
|
|
299
|
+
persistTo: void 0
|
|
300
|
+
};
|
|
294
301
|
var SharedQuery = class {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
302
|
+
inflightPromises = /* @__PURE__ */ new Map();
|
|
303
|
+
queryState;
|
|
304
|
+
queryName;
|
|
305
|
+
queryFn;
|
|
306
|
+
expiryMs;
|
|
307
|
+
staleMs;
|
|
308
|
+
refetchOnStale;
|
|
309
|
+
constructor(options) {
|
|
310
|
+
this.queryName = options.queryName;
|
|
311
|
+
this.queryFn = options.queryFn;
|
|
312
|
+
this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : SharedQueryDefaults.expiryMs;
|
|
313
|
+
this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : SharedQueryDefaults.staleMs;
|
|
314
|
+
this.refetchOnStale = Boolean(options?.refetchOnStale);
|
|
315
|
+
const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
|
|
316
|
+
if (persistTo === "localStorage") {
|
|
317
|
+
options.localStorageKey = options.queryName;
|
|
318
|
+
} else if (persistTo === "indexedDb") {
|
|
319
|
+
options.indexedDbKey = options.queryName;
|
|
320
|
+
}
|
|
301
321
|
this.queryState = sharedState(
|
|
302
322
|
{},
|
|
303
323
|
{
|
|
@@ -307,109 +327,101 @@ var SharedQuery = class {
|
|
|
307
327
|
}
|
|
308
328
|
);
|
|
309
329
|
}
|
|
310
|
-
inflightPromises = /* @__PURE__ */ new Map();
|
|
311
|
-
queryState;
|
|
312
|
-
expiryMs;
|
|
313
|
-
staleMs;
|
|
314
|
-
revalidateOnStale;
|
|
315
330
|
getQueryKey(args) {
|
|
316
331
|
return this.queryName + ":" + stringifyDeterministicForKeys(args);
|
|
317
332
|
}
|
|
318
|
-
isStale(
|
|
333
|
+
isStale(dataUpdatedMs) {
|
|
319
334
|
if (this.staleMs === 0) return true;
|
|
320
|
-
const ageMs = Date.now() -
|
|
335
|
+
const ageMs = Date.now() - dataUpdatedMs;
|
|
321
336
|
return ageMs > this.staleMs;
|
|
322
337
|
}
|
|
323
|
-
isExpired(
|
|
338
|
+
isExpired(dataUpdatedMs) {
|
|
324
339
|
if (this.expiryMs === 0) return false;
|
|
325
|
-
const ageMs = Date.now() -
|
|
340
|
+
const ageMs = Date.now() - dataUpdatedMs;
|
|
326
341
|
return ageMs > this.expiryMs;
|
|
327
342
|
}
|
|
328
|
-
|
|
343
|
+
async getCachedOrFetch(...args) {
|
|
344
|
+
const queryKey = this.getQueryKey(args);
|
|
345
|
+
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
346
|
+
if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
|
|
347
|
+
if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
|
|
348
|
+
console.log(
|
|
349
|
+
`Return fresh data ${queryKey} from cache without fetching`
|
|
350
|
+
);
|
|
351
|
+
return cached.data;
|
|
352
|
+
}
|
|
353
|
+
if (this.refetchOnStale) {
|
|
354
|
+
console.log(`refetchOnStale for ${queryKey}`);
|
|
355
|
+
void this.dedupedFetch(queryKey, ...args);
|
|
356
|
+
}
|
|
357
|
+
console.log(`Returning stale data for ${queryKey}`);
|
|
358
|
+
return cached.data;
|
|
359
|
+
}
|
|
360
|
+
return await this.dedupedFetch(queryKey, ...args);
|
|
361
|
+
}
|
|
362
|
+
dedupedFetch(queryKey, ...args) {
|
|
329
363
|
const inflightPromise = this.inflightPromises.get(queryKey);
|
|
330
364
|
if (inflightPromise) {
|
|
331
365
|
console.log(`Deduplicating inflight fetch for ${queryKey}`);
|
|
332
366
|
return inflightPromise;
|
|
333
367
|
}
|
|
368
|
+
const promise = this.startFetching(queryKey, args);
|
|
369
|
+
this.inflightPromises.set(queryKey, promise);
|
|
370
|
+
return promise;
|
|
371
|
+
}
|
|
372
|
+
async startFetching(queryKey, args) {
|
|
334
373
|
console.log(`Start fetching ${queryKey}`);
|
|
335
|
-
|
|
374
|
+
try {
|
|
375
|
+
const data = await this.queryFn(...args);
|
|
336
376
|
console.log(`Successfully fetched ${queryKey}`);
|
|
337
|
-
this.
|
|
338
|
-
return
|
|
339
|
-
}
|
|
377
|
+
this.setData(queryKey, data, Date.now());
|
|
378
|
+
return data;
|
|
379
|
+
} catch (e) {
|
|
340
380
|
console.error(`Failed to fetch ${queryKey}: ${e}`);
|
|
341
381
|
throw e;
|
|
342
|
-
}
|
|
382
|
+
} finally {
|
|
343
383
|
this.inflightPromises.delete(queryKey);
|
|
344
|
-
});
|
|
345
|
-
this.inflightPromises.set(queryKey, promise);
|
|
346
|
-
return promise;
|
|
347
|
-
}
|
|
348
|
-
async getCachedOrFetch(...args) {
|
|
349
|
-
const queryKey = this.getQueryKey(args);
|
|
350
|
-
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
351
|
-
if (cached?.data && !this.isExpired(cached.data)) {
|
|
352
|
-
if (!this.isStale(cached.data)) {
|
|
353
|
-
console.log(
|
|
354
|
-
`Return fresh data ${queryKey} from cache without fetching`
|
|
355
|
-
);
|
|
356
|
-
return cached.data.value;
|
|
357
|
-
}
|
|
358
|
-
if (this.revalidateOnStale) {
|
|
359
|
-
console.log(`revalidateOnStale for ${queryKey}`);
|
|
360
|
-
void this.fetchNoCaching(queryKey, ...args);
|
|
361
|
-
}
|
|
362
|
-
console.log(`Returning stale data for ${queryKey}`);
|
|
363
|
-
return cached.data.value;
|
|
364
384
|
}
|
|
365
|
-
return await this.fetchNoCaching(queryKey, ...args);
|
|
366
385
|
}
|
|
367
|
-
|
|
386
|
+
/** Set the data directly if the user obtained it from somewhere else. */
|
|
387
|
+
async setData(queryKey, data, dataUpdatedMs = Date.now()) {
|
|
368
388
|
const entry = {
|
|
369
|
-
data: { value, updatedMs: Date.now() },
|
|
370
389
|
loading: false,
|
|
371
|
-
|
|
390
|
+
data,
|
|
391
|
+
dataUpdatedMs
|
|
372
392
|
};
|
|
373
393
|
this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
374
394
|
}
|
|
375
395
|
/** Do a new fetch even when the data is already cached. */
|
|
376
396
|
async updateFromSource(...args) {
|
|
377
397
|
const key = this.getQueryKey(args);
|
|
378
|
-
return await this.
|
|
379
|
-
}
|
|
380
|
-
/** Delete the
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
this.inflightPromises.delete(queryKey);
|
|
389
|
-
} else {
|
|
390
|
-
this.clearAll();
|
|
391
|
-
}
|
|
398
|
+
return await this.dedupedFetch(key, ...args);
|
|
399
|
+
}
|
|
400
|
+
/** Delete the cached data for one set of arguments. */
|
|
401
|
+
deleteData(args) {
|
|
402
|
+
const queryKey = this.getQueryKey(args);
|
|
403
|
+
const record = this.queryState.getSnapshot();
|
|
404
|
+
const clone = { ...record };
|
|
405
|
+
delete clone[queryKey];
|
|
406
|
+
this.queryState.setValue(clone);
|
|
407
|
+
this.inflightPromises.delete(queryKey);
|
|
392
408
|
}
|
|
393
|
-
/** Delete
|
|
394
|
-
|
|
409
|
+
/** Delete all currently cached data & all inflight promises. */
|
|
410
|
+
clear() {
|
|
395
411
|
this.queryState.delete();
|
|
396
412
|
this.inflightPromises.clear();
|
|
397
413
|
}
|
|
398
414
|
};
|
|
399
415
|
var seenQueryNames = /* @__PURE__ */ new Set();
|
|
400
|
-
function sharedQuery(
|
|
401
|
-
if (seenQueryNames.has(queryName)) {
|
|
402
|
-
throw new Error(`Duplicate shared query "${queryName}"`);
|
|
416
|
+
function sharedQuery(options) {
|
|
417
|
+
if (seenQueryNames.has(options.queryName)) {
|
|
418
|
+
throw new Error(`Duplicate shared query "${options.queryName}"`);
|
|
403
419
|
}
|
|
404
|
-
seenQueryNames.add(queryName);
|
|
405
|
-
return new SharedQuery(
|
|
420
|
+
seenQueryNames.add(options.queryName);
|
|
421
|
+
return new SharedQuery(options);
|
|
406
422
|
}
|
|
407
|
-
var DEFAULT_QUERY_STATE_ENTRY = {
|
|
408
|
-
|
|
409
|
-
loading: true,
|
|
410
|
-
error: null
|
|
411
|
-
};
|
|
412
|
-
function useQuery(query, args) {
|
|
423
|
+
var DEFAULT_QUERY_STATE_ENTRY = { loading: true };
|
|
424
|
+
function useSharedQuery(query, args = []) {
|
|
413
425
|
const [queryState, setQueryState] = useSharedState(query.queryState);
|
|
414
426
|
const isMounted = (0, import_react2.useRef)(true);
|
|
415
427
|
const queryKey = query.getQueryKey(args);
|
|
@@ -418,25 +430,21 @@ function useQuery(query, args) {
|
|
|
418
430
|
setQueryState((prev) => {
|
|
419
431
|
const clone = { ...prev };
|
|
420
432
|
clone[queryKey] = {
|
|
421
|
-
|
|
422
|
-
loading: true
|
|
423
|
-
error: null
|
|
433
|
+
...clone[queryKey] ?? {},
|
|
434
|
+
loading: true
|
|
424
435
|
};
|
|
425
436
|
return clone;
|
|
426
437
|
});
|
|
427
438
|
try {
|
|
428
|
-
const
|
|
439
|
+
const data = await query.getCachedOrFetch(...args);
|
|
429
440
|
if (isMounted.current) {
|
|
430
441
|
setQueryState((prev) => {
|
|
431
442
|
const clone = { ...prev };
|
|
432
|
-
const now = Date.now();
|
|
433
443
|
clone[queryKey] = {
|
|
434
|
-
|
|
435
|
-
value,
|
|
436
|
-
updatedMs: now
|
|
437
|
-
},
|
|
444
|
+
// Don't keep old error.
|
|
438
445
|
loading: false,
|
|
439
|
-
|
|
446
|
+
data,
|
|
447
|
+
dataUpdatedMs: Date.now()
|
|
440
448
|
};
|
|
441
449
|
return clone;
|
|
442
450
|
});
|
|
@@ -447,9 +455,11 @@ function useQuery(query, args) {
|
|
|
447
455
|
setQueryState((prev) => {
|
|
448
456
|
const clone = { ...prev };
|
|
449
457
|
clone[queryKey] = {
|
|
450
|
-
|
|
458
|
+
// Keep old data.
|
|
459
|
+
...clone[queryKey] ?? {},
|
|
451
460
|
loading: false,
|
|
452
|
-
error
|
|
461
|
+
error,
|
|
462
|
+
errorUpdatedMs: Date.now()
|
|
453
463
|
};
|
|
454
464
|
return clone;
|
|
455
465
|
});
|
|
@@ -463,11 +473,26 @@ function useQuery(query, args) {
|
|
|
463
473
|
isMounted.current = false;
|
|
464
474
|
};
|
|
465
475
|
}, [execute]);
|
|
466
|
-
return
|
|
476
|
+
return (0, import_react2.useMemo)(() => {
|
|
477
|
+
const state = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
478
|
+
return {
|
|
479
|
+
...state,
|
|
480
|
+
refetch: () => {
|
|
481
|
+
return query.updateFromSource(...args);
|
|
482
|
+
},
|
|
483
|
+
setData: (data, dataUpdatedMs) => {
|
|
484
|
+
query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
|
|
485
|
+
},
|
|
486
|
+
deleteData: () => {
|
|
487
|
+
query.deleteData(args);
|
|
488
|
+
}
|
|
489
|
+
};
|
|
490
|
+
}, [query, queryKey, queryState]);
|
|
467
491
|
}
|
|
468
492
|
// Annotate the CommonJS export names for ESM import in node:
|
|
469
493
|
0 && (module.exports = {
|
|
470
494
|
SharedQuery,
|
|
495
|
+
SharedQueryDefaults,
|
|
471
496
|
sharedQuery,
|
|
472
|
-
|
|
497
|
+
useSharedQuery
|
|
473
498
|
});
|
package/sharedQuery.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
// src/sharedQuery.ts
|
|
2
|
-
import {
|
|
3
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { MS_PER_DAY as MS_PER_DAY2 } from "@choksheak/ts-utils/timeConstants";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
4
|
|
|
5
5
|
// src/sharedState.ts
|
|
6
|
-
import { DEFAULT_EXPIRY_DELTA_MS } from "@choksheak/ts-utils/kvStore";
|
|
7
6
|
import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
|
|
8
7
|
import { useSyncExternalStore } from "react";
|
|
9
8
|
|
|
@@ -108,17 +107,17 @@ var IndexedDbAdapter = class {
|
|
|
108
107
|
};
|
|
109
108
|
|
|
110
109
|
// src/sharedState.ts
|
|
111
|
-
var
|
|
110
|
+
var SharedStateDefaults = {
|
|
112
111
|
/**
|
|
113
112
|
* Default duration for storage expiration in milliseconds. Initial value
|
|
114
113
|
* is 30 days, but can be changed to anything.
|
|
115
114
|
*/
|
|
116
|
-
|
|
115
|
+
storeExpiryMs: 30 * MS_PER_DAY,
|
|
117
116
|
/**
|
|
118
117
|
* True to load from storage only on mount, false (default) to load in
|
|
119
118
|
* the top level scope when the shared state is defined.
|
|
120
119
|
*/
|
|
121
|
-
|
|
120
|
+
lazyLoad: false
|
|
122
121
|
};
|
|
123
122
|
var PubSubStore = class {
|
|
124
123
|
dataByKey = /* @__PURE__ */ new Map();
|
|
@@ -173,11 +172,14 @@ var SharedState = class {
|
|
|
173
172
|
constructor(defaultValue, options) {
|
|
174
173
|
this.defaultValue = defaultValue;
|
|
175
174
|
this.pubSubKey = String(stateKey++);
|
|
176
|
-
this.storageAdapter = getStorageAdapter(
|
|
175
|
+
this.storageAdapter = getStorageAdapter(
|
|
176
|
+
options,
|
|
177
|
+
SharedStateDefaults.storeExpiryMs
|
|
178
|
+
);
|
|
177
179
|
this.setValueBounded = (arg) => this.setValue(arg);
|
|
178
180
|
pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
|
|
179
181
|
const lazyLoad = options?.lazyLoad;
|
|
180
|
-
const lazy = lazyLoad !== void 0 ? lazyLoad :
|
|
182
|
+
const lazy = lazyLoad !== void 0 ? lazyLoad : SharedStateDefaults.lazyLoad;
|
|
181
183
|
if (!lazy) {
|
|
182
184
|
void this.initDefaultValueOnce();
|
|
183
185
|
}
|
|
@@ -239,7 +241,7 @@ function stringifyDeterministicForKeys(params) {
|
|
|
239
241
|
if (typeof value === "function") {
|
|
240
242
|
throw new Error(`Functions should not be passed into query parameters`);
|
|
241
243
|
}
|
|
242
|
-
if (!value || typeof value !== "object") {
|
|
244
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
243
245
|
return value;
|
|
244
246
|
}
|
|
245
247
|
if (cache.has(value)) {
|
|
@@ -264,14 +266,31 @@ function stringifyDeterministicForKeys(params) {
|
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
// src/sharedQuery.ts
|
|
267
|
-
var
|
|
269
|
+
var SharedQueryDefaults = {
|
|
270
|
+
staleMs: 0,
|
|
271
|
+
expiryMs: 30 * MS_PER_DAY2,
|
|
272
|
+
persistTo: void 0
|
|
273
|
+
};
|
|
268
274
|
var SharedQuery = class {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
inflightPromises = /* @__PURE__ */ new Map();
|
|
276
|
+
queryState;
|
|
277
|
+
queryName;
|
|
278
|
+
queryFn;
|
|
279
|
+
expiryMs;
|
|
280
|
+
staleMs;
|
|
281
|
+
refetchOnStale;
|
|
282
|
+
constructor(options) {
|
|
283
|
+
this.queryName = options.queryName;
|
|
284
|
+
this.queryFn = options.queryFn;
|
|
285
|
+
this.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : SharedQueryDefaults.expiryMs;
|
|
286
|
+
this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : SharedQueryDefaults.staleMs;
|
|
287
|
+
this.refetchOnStale = Boolean(options?.refetchOnStale);
|
|
288
|
+
const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
|
|
289
|
+
if (persistTo === "localStorage") {
|
|
290
|
+
options.localStorageKey = options.queryName;
|
|
291
|
+
} else if (persistTo === "indexedDb") {
|
|
292
|
+
options.indexedDbKey = options.queryName;
|
|
293
|
+
}
|
|
275
294
|
this.queryState = sharedState(
|
|
276
295
|
{},
|
|
277
296
|
{
|
|
@@ -281,109 +300,101 @@ var SharedQuery = class {
|
|
|
281
300
|
}
|
|
282
301
|
);
|
|
283
302
|
}
|
|
284
|
-
inflightPromises = /* @__PURE__ */ new Map();
|
|
285
|
-
queryState;
|
|
286
|
-
expiryMs;
|
|
287
|
-
staleMs;
|
|
288
|
-
revalidateOnStale;
|
|
289
303
|
getQueryKey(args) {
|
|
290
304
|
return this.queryName + ":" + stringifyDeterministicForKeys(args);
|
|
291
305
|
}
|
|
292
|
-
isStale(
|
|
306
|
+
isStale(dataUpdatedMs) {
|
|
293
307
|
if (this.staleMs === 0) return true;
|
|
294
|
-
const ageMs = Date.now() -
|
|
308
|
+
const ageMs = Date.now() - dataUpdatedMs;
|
|
295
309
|
return ageMs > this.staleMs;
|
|
296
310
|
}
|
|
297
|
-
isExpired(
|
|
311
|
+
isExpired(dataUpdatedMs) {
|
|
298
312
|
if (this.expiryMs === 0) return false;
|
|
299
|
-
const ageMs = Date.now() -
|
|
313
|
+
const ageMs = Date.now() - dataUpdatedMs;
|
|
300
314
|
return ageMs > this.expiryMs;
|
|
301
315
|
}
|
|
302
|
-
|
|
316
|
+
async getCachedOrFetch(...args) {
|
|
317
|
+
const queryKey = this.getQueryKey(args);
|
|
318
|
+
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
319
|
+
if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
|
|
320
|
+
if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
|
|
321
|
+
console.log(
|
|
322
|
+
`Return fresh data ${queryKey} from cache without fetching`
|
|
323
|
+
);
|
|
324
|
+
return cached.data;
|
|
325
|
+
}
|
|
326
|
+
if (this.refetchOnStale) {
|
|
327
|
+
console.log(`refetchOnStale for ${queryKey}`);
|
|
328
|
+
void this.dedupedFetch(queryKey, ...args);
|
|
329
|
+
}
|
|
330
|
+
console.log(`Returning stale data for ${queryKey}`);
|
|
331
|
+
return cached.data;
|
|
332
|
+
}
|
|
333
|
+
return await this.dedupedFetch(queryKey, ...args);
|
|
334
|
+
}
|
|
335
|
+
dedupedFetch(queryKey, ...args) {
|
|
303
336
|
const inflightPromise = this.inflightPromises.get(queryKey);
|
|
304
337
|
if (inflightPromise) {
|
|
305
338
|
console.log(`Deduplicating inflight fetch for ${queryKey}`);
|
|
306
339
|
return inflightPromise;
|
|
307
340
|
}
|
|
341
|
+
const promise = this.startFetching(queryKey, args);
|
|
342
|
+
this.inflightPromises.set(queryKey, promise);
|
|
343
|
+
return promise;
|
|
344
|
+
}
|
|
345
|
+
async startFetching(queryKey, args) {
|
|
308
346
|
console.log(`Start fetching ${queryKey}`);
|
|
309
|
-
|
|
347
|
+
try {
|
|
348
|
+
const data = await this.queryFn(...args);
|
|
310
349
|
console.log(`Successfully fetched ${queryKey}`);
|
|
311
|
-
this.
|
|
312
|
-
return
|
|
313
|
-
}
|
|
350
|
+
this.setData(queryKey, data, Date.now());
|
|
351
|
+
return data;
|
|
352
|
+
} catch (e) {
|
|
314
353
|
console.error(`Failed to fetch ${queryKey}: ${e}`);
|
|
315
354
|
throw e;
|
|
316
|
-
}
|
|
355
|
+
} finally {
|
|
317
356
|
this.inflightPromises.delete(queryKey);
|
|
318
|
-
});
|
|
319
|
-
this.inflightPromises.set(queryKey, promise);
|
|
320
|
-
return promise;
|
|
321
|
-
}
|
|
322
|
-
async getCachedOrFetch(...args) {
|
|
323
|
-
const queryKey = this.getQueryKey(args);
|
|
324
|
-
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
325
|
-
if (cached?.data && !this.isExpired(cached.data)) {
|
|
326
|
-
if (!this.isStale(cached.data)) {
|
|
327
|
-
console.log(
|
|
328
|
-
`Return fresh data ${queryKey} from cache without fetching`
|
|
329
|
-
);
|
|
330
|
-
return cached.data.value;
|
|
331
|
-
}
|
|
332
|
-
if (this.revalidateOnStale) {
|
|
333
|
-
console.log(`revalidateOnStale for ${queryKey}`);
|
|
334
|
-
void this.fetchNoCaching(queryKey, ...args);
|
|
335
|
-
}
|
|
336
|
-
console.log(`Returning stale data for ${queryKey}`);
|
|
337
|
-
return cached.data.value;
|
|
338
357
|
}
|
|
339
|
-
return await this.fetchNoCaching(queryKey, ...args);
|
|
340
358
|
}
|
|
341
|
-
|
|
359
|
+
/** Set the data directly if the user obtained it from somewhere else. */
|
|
360
|
+
async setData(queryKey, data, dataUpdatedMs = Date.now()) {
|
|
342
361
|
const entry = {
|
|
343
|
-
data: { value, updatedMs: Date.now() },
|
|
344
362
|
loading: false,
|
|
345
|
-
|
|
363
|
+
data,
|
|
364
|
+
dataUpdatedMs
|
|
346
365
|
};
|
|
347
366
|
this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
348
367
|
}
|
|
349
368
|
/** Do a new fetch even when the data is already cached. */
|
|
350
369
|
async updateFromSource(...args) {
|
|
351
370
|
const key = this.getQueryKey(args);
|
|
352
|
-
return await this.
|
|
353
|
-
}
|
|
354
|
-
/** Delete the
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
this.inflightPromises.delete(queryKey);
|
|
363
|
-
} else {
|
|
364
|
-
this.clearAll();
|
|
365
|
-
}
|
|
371
|
+
return await this.dedupedFetch(key, ...args);
|
|
372
|
+
}
|
|
373
|
+
/** Delete the cached data for one set of arguments. */
|
|
374
|
+
deleteData(args) {
|
|
375
|
+
const queryKey = this.getQueryKey(args);
|
|
376
|
+
const record = this.queryState.getSnapshot();
|
|
377
|
+
const clone = { ...record };
|
|
378
|
+
delete clone[queryKey];
|
|
379
|
+
this.queryState.setValue(clone);
|
|
380
|
+
this.inflightPromises.delete(queryKey);
|
|
366
381
|
}
|
|
367
|
-
/** Delete
|
|
368
|
-
|
|
382
|
+
/** Delete all currently cached data & all inflight promises. */
|
|
383
|
+
clear() {
|
|
369
384
|
this.queryState.delete();
|
|
370
385
|
this.inflightPromises.clear();
|
|
371
386
|
}
|
|
372
387
|
};
|
|
373
388
|
var seenQueryNames = /* @__PURE__ */ new Set();
|
|
374
|
-
function sharedQuery(
|
|
375
|
-
if (seenQueryNames.has(queryName)) {
|
|
376
|
-
throw new Error(`Duplicate shared query "${queryName}"`);
|
|
389
|
+
function sharedQuery(options) {
|
|
390
|
+
if (seenQueryNames.has(options.queryName)) {
|
|
391
|
+
throw new Error(`Duplicate shared query "${options.queryName}"`);
|
|
377
392
|
}
|
|
378
|
-
seenQueryNames.add(queryName);
|
|
379
|
-
return new SharedQuery(
|
|
393
|
+
seenQueryNames.add(options.queryName);
|
|
394
|
+
return new SharedQuery(options);
|
|
380
395
|
}
|
|
381
|
-
var DEFAULT_QUERY_STATE_ENTRY = {
|
|
382
|
-
|
|
383
|
-
loading: true,
|
|
384
|
-
error: null
|
|
385
|
-
};
|
|
386
|
-
function useQuery(query, args) {
|
|
396
|
+
var DEFAULT_QUERY_STATE_ENTRY = { loading: true };
|
|
397
|
+
function useSharedQuery(query, args = []) {
|
|
387
398
|
const [queryState, setQueryState] = useSharedState(query.queryState);
|
|
388
399
|
const isMounted = useRef(true);
|
|
389
400
|
const queryKey = query.getQueryKey(args);
|
|
@@ -392,25 +403,21 @@ function useQuery(query, args) {
|
|
|
392
403
|
setQueryState((prev) => {
|
|
393
404
|
const clone = { ...prev };
|
|
394
405
|
clone[queryKey] = {
|
|
395
|
-
|
|
396
|
-
loading: true
|
|
397
|
-
error: null
|
|
406
|
+
...clone[queryKey] ?? {},
|
|
407
|
+
loading: true
|
|
398
408
|
};
|
|
399
409
|
return clone;
|
|
400
410
|
});
|
|
401
411
|
try {
|
|
402
|
-
const
|
|
412
|
+
const data = await query.getCachedOrFetch(...args);
|
|
403
413
|
if (isMounted.current) {
|
|
404
414
|
setQueryState((prev) => {
|
|
405
415
|
const clone = { ...prev };
|
|
406
|
-
const now = Date.now();
|
|
407
416
|
clone[queryKey] = {
|
|
408
|
-
|
|
409
|
-
value,
|
|
410
|
-
updatedMs: now
|
|
411
|
-
},
|
|
417
|
+
// Don't keep old error.
|
|
412
418
|
loading: false,
|
|
413
|
-
|
|
419
|
+
data,
|
|
420
|
+
dataUpdatedMs: Date.now()
|
|
414
421
|
};
|
|
415
422
|
return clone;
|
|
416
423
|
});
|
|
@@ -421,9 +428,11 @@ function useQuery(query, args) {
|
|
|
421
428
|
setQueryState((prev) => {
|
|
422
429
|
const clone = { ...prev };
|
|
423
430
|
clone[queryKey] = {
|
|
424
|
-
|
|
431
|
+
// Keep old data.
|
|
432
|
+
...clone[queryKey] ?? {},
|
|
425
433
|
loading: false,
|
|
426
|
-
error
|
|
434
|
+
error,
|
|
435
|
+
errorUpdatedMs: Date.now()
|
|
427
436
|
};
|
|
428
437
|
return clone;
|
|
429
438
|
});
|
|
@@ -437,10 +446,25 @@ function useQuery(query, args) {
|
|
|
437
446
|
isMounted.current = false;
|
|
438
447
|
};
|
|
439
448
|
}, [execute]);
|
|
440
|
-
return
|
|
449
|
+
return useMemo(() => {
|
|
450
|
+
const state = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
451
|
+
return {
|
|
452
|
+
...state,
|
|
453
|
+
refetch: () => {
|
|
454
|
+
return query.updateFromSource(...args);
|
|
455
|
+
},
|
|
456
|
+
setData: (data, dataUpdatedMs) => {
|
|
457
|
+
query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
|
|
458
|
+
},
|
|
459
|
+
deleteData: () => {
|
|
460
|
+
query.deleteData(args);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}, [query, queryKey, queryState]);
|
|
441
464
|
}
|
|
442
465
|
export {
|
|
443
466
|
SharedQuery,
|
|
467
|
+
SharedQueryDefaults,
|
|
444
468
|
sharedQuery,
|
|
445
|
-
|
|
469
|
+
useSharedQuery
|
|
446
470
|
};
|