@choksheak/react-utils 0.2.3 → 0.2.5
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/README.md +3 -1
- package/fetcher.min.cjs +2 -0
- package/fetcher.min.cjs.map +1 -0
- package/fetcher.min.mjs +2 -0
- package/fetcher.min.mjs.map +1 -0
- package/index.min.cjs +2 -0
- package/index.min.cjs.map +1 -0
- package/index.min.mjs +2 -0
- package/index.min.mjs.map +1 -0
- package/package.json +38 -5
- package/{sharedQuery.js → sharedQuery.cjs} +202 -79
- package/sharedQuery.d.mts +73 -9
- package/sharedQuery.d.ts +72 -8
- package/sharedQuery.min.cjs +2 -0
- package/sharedQuery.min.cjs.map +1 -0
- package/sharedQuery.min.mjs +2 -0
- package/sharedQuery.min.mjs.map +1 -0
- package/sharedQuery.mjs +200 -77
- package/sharedState.min.cjs +2 -0
- package/sharedState.min.cjs.map +1 -0
- package/sharedState.min.mjs +2 -0
- package/sharedState.min.mjs.map +1 -0
- package/utils/storage.min.cjs +2 -0
- package/utils/storage.min.cjs.map +1 -0
- package/utils/storage.min.mjs +2 -0
- package/utils/storage.min.mjs.map +1 -0
- package/utils/stringify.min.cjs +2 -0
- package/utils/stringify.min.cjs.map +1 -0
- package/utils/stringify.min.mjs +2 -0
- package/utils/stringify.min.mjs.map +1 -0
- package/utils/useDeepMemo.cjs +51 -0
- package/utils/useDeepMemo.d.mts +4 -0
- package/utils/useDeepMemo.d.ts +2 -0
- package/utils/useDeepMemo.min.cjs +2 -0
- package/utils/useDeepMemo.min.cjs.map +1 -0
- package/utils/useDeepMemo.min.mjs +2 -0
- package/utils/useDeepMemo.min.mjs.map +1 -0
- package/utils/useDeepMemo.mjs +16 -0
- package/docs/.nojekyll +0 -1
- package/docs/assets/hierarchy.js +0 -1
- package/docs/assets/highlight.css +0 -71
- package/docs/assets/icons.js +0 -18
- package/docs/assets/icons.svg +0 -1
- package/docs/assets/main.js +0 -60
- package/docs/assets/navigation.js +0 -1
- package/docs/assets/search.js +0 -1
- package/docs/assets/style.css +0 -1633
- package/docs/classes/sharedQuery.SharedQuery.html +0 -22
- package/docs/classes/sharedState.SharedState.html +0 -9
- package/docs/classes/utils_storage.InMemoryStorageAdapter.html +0 -9
- package/docs/classes/utils_storage.IndexedDbAdapter.html +0 -9
- package/docs/classes/utils_storage.LocalStorageAdapter.html +0 -9
- package/docs/functions/sharedQuery.sharedQuery.html +0 -9
- package/docs/functions/sharedQuery.useSharedQuery.html +0 -6
- package/docs/functions/sharedState.sharedState.html +0 -3
- package/docs/functions/sharedState.useSharedState.html +0 -3
- package/docs/functions/sharedState.useSharedStateSetter.html +0 -2
- package/docs/functions/sharedState.useSharedStateValue.html +0 -2
- package/docs/functions/utils_storage.getStorageAdapter.html +0 -1
- package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +0 -7
- package/docs/hierarchy.html +0 -1
- package/docs/index.html +0 -2
- package/docs/modules/fetcher.html +0 -1
- package/docs/modules/index.html +0 -1
- package/docs/modules/sharedQuery.html +0 -1
- package/docs/modules/sharedState.html +0 -1
- package/docs/modules/utils_storage.html +0 -1
- package/docs/modules/utils_stringify.html +0 -1
- package/docs/modules.html +0 -1
- package/docs/types/sharedQuery.FetchFn.html +0 -1
- package/docs/types/sharedQuery.PersistTo.html +0 -1
- package/docs/types/sharedQuery.QueryStateValue.html +0 -9
- package/docs/types/sharedQuery.SharedQueryOptions.html +0 -1
- package/docs/types/sharedQuery.SharedQueryState.html +0 -3
- package/docs/types/sharedQuery.UseQueryResult.html +0 -1
- package/docs/types/sharedState.SharedStateOptions.html +0 -1
- package/docs/types/utils_storage.StorageAdapter.html +0 -5
- package/docs/types/utils_storage.StorageOptions.html +0 -5
- package/docs/variables/fetcher.fetcher.html +0 -2
- package/docs/variables/fetcher.fetcherWithRetries.html +0 -2
- package/docs/variables/sharedQuery.SharedQueryDefaults.html +0 -2
- package/docs/variables/sharedState.SharedStateDefaults.html +0 -6
- package/src/fetcher.ts +0 -14
- package/src/index.ts +0 -4
- package/src/sharedQuery.ts +0 -507
- package/src/sharedState.ts +0 -232
- package/src/utils/storage.ts +0 -197
- package/src/utils/stringify.ts +0 -49
- /package/{fetcher.js → fetcher.cjs} +0 -0
- /package/{index.js → index.cjs} +0 -0
- /package/{sharedState.js → sharedState.cjs} +0 -0
- /package/utils/{storage.js → storage.cjs} +0 -0
- /package/utils/{stringify.js → stringify.cjs} +0 -0
package/sharedQuery.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/sharedQuery.ts
|
|
2
2
|
import { MS_PER_DAY as MS_PER_DAY2 } from "@choksheak/ts-utils/timeConstants";
|
|
3
3
|
import getByteSize from "object-sizeof";
|
|
4
|
-
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { useCallback, useEffect, useMemo as useMemo2, useRef } from "react";
|
|
5
5
|
|
|
6
6
|
// src/sharedState.ts
|
|
7
7
|
import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
|
|
@@ -266,18 +266,39 @@ function stringifyDeterministicForKeys(params) {
|
|
|
266
266
|
});
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
+
// src/utils/useDeepMemo.ts
|
|
270
|
+
import isEqual from "lodash/isEqual";
|
|
271
|
+
import { useMemo, useState } from "react";
|
|
272
|
+
function useDeepMemo(value) {
|
|
273
|
+
const [state, setState] = useState(value);
|
|
274
|
+
return useMemo(() => {
|
|
275
|
+
if (state === value || isEqual(state, value)) {
|
|
276
|
+
return state;
|
|
277
|
+
}
|
|
278
|
+
setState(value);
|
|
279
|
+
return value;
|
|
280
|
+
}, [state, value]);
|
|
281
|
+
}
|
|
282
|
+
|
|
269
283
|
// src/sharedQuery.ts
|
|
270
284
|
var SharedQueryDefaults = {
|
|
285
|
+
// Refetch on every page load.
|
|
271
286
|
staleMs: 0,
|
|
287
|
+
// Keep in cache for a long time.
|
|
272
288
|
expiryMs: 30 * MS_PER_DAY2,
|
|
289
|
+
// Always refetch automatically if the data is stale.
|
|
290
|
+
refetchOnStale: true,
|
|
291
|
+
// No default persistence configured.
|
|
273
292
|
persistTo: void 0,
|
|
293
|
+
// 100 is not too large, but please tweak accordingly.
|
|
274
294
|
maxSize: 100,
|
|
275
|
-
//
|
|
276
|
-
maxBytes: 1e5
|
|
277
|
-
//
|
|
295
|
+
// 100kb limit before discarding old records.
|
|
296
|
+
maxBytes: 1e5,
|
|
297
|
+
// Default to log to console.
|
|
298
|
+
log: (...args) => console.log("[sharedQuery]", ...args)
|
|
278
299
|
};
|
|
279
300
|
var SharedQuery = class {
|
|
280
|
-
|
|
301
|
+
inflightQueries = /* @__PURE__ */ new Map();
|
|
281
302
|
queryState;
|
|
282
303
|
queryName;
|
|
283
304
|
queryFn;
|
|
@@ -286,22 +307,27 @@ var SharedQuery = class {
|
|
|
286
307
|
refetchOnStale;
|
|
287
308
|
maxSize;
|
|
288
309
|
maxBytes;
|
|
310
|
+
log;
|
|
289
311
|
constructor(options) {
|
|
290
312
|
this.queryName = options.queryName;
|
|
291
313
|
this.queryFn = options.queryFn;
|
|
292
|
-
this.
|
|
293
|
-
this.
|
|
294
|
-
this.refetchOnStale =
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
314
|
+
this.staleMs = options?.staleMs ?? SharedQueryDefaults.staleMs;
|
|
315
|
+
this.expiryMs = options?.expiryMs ?? SharedQueryDefaults.expiryMs;
|
|
316
|
+
this.refetchOnStale = options?.refetchOnStale ?? SharedQueryDefaults.refetchOnStale;
|
|
317
|
+
if (!options.localStorageKey && !options.indexedDbKey) {
|
|
318
|
+
const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
|
|
319
|
+
if (persistTo === "localStorage") {
|
|
320
|
+
options.localStorageKey = options.queryName;
|
|
321
|
+
} else if (persistTo === "indexedDb") {
|
|
322
|
+
options.indexedDbKey = options.queryName;
|
|
323
|
+
}
|
|
300
324
|
}
|
|
301
325
|
this.maxSize = options?.maxSize ?? SharedQueryDefaults.maxSize;
|
|
302
326
|
this.maxBytes = options?.maxBytes ?? SharedQueryDefaults.maxBytes;
|
|
327
|
+
this.log = options?.log ?? SharedQueryDefaults.log;
|
|
303
328
|
this.queryState = sharedState(
|
|
304
329
|
{},
|
|
330
|
+
// defaultValue
|
|
305
331
|
{
|
|
306
332
|
...options,
|
|
307
333
|
// Fallback to use expiryMs for the storeExpiryMs.
|
|
@@ -309,9 +335,56 @@ var SharedQuery = class {
|
|
|
309
335
|
}
|
|
310
336
|
);
|
|
311
337
|
}
|
|
338
|
+
/** Returns a key to identify requests based on the given args. */
|
|
312
339
|
getQueryKey(args) {
|
|
313
340
|
return this.queryName + ":" + stringifyDeterministicForKeys(args);
|
|
314
341
|
}
|
|
342
|
+
/**
|
|
343
|
+
* Get the AbortController to abort the inflight query (if any).
|
|
344
|
+
*
|
|
345
|
+
* Note that once you get the controller, you can also get the signal easily
|
|
346
|
+
* using `controller.signal`. So if you need both the controller and the
|
|
347
|
+
* signal, just use this function.
|
|
348
|
+
*/
|
|
349
|
+
getAbortController(args) {
|
|
350
|
+
const queryKey = this.getQueryKey(args);
|
|
351
|
+
return this.getAbortControllerByKey(queryKey);
|
|
352
|
+
}
|
|
353
|
+
getAbortControllerByKey(queryKey) {
|
|
354
|
+
return this.inflightQueries.get(queryKey)?.abortController ?? null;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Get the AbortSignal to check for query abortions.
|
|
358
|
+
*
|
|
359
|
+
* Example:
|
|
360
|
+
* ```
|
|
361
|
+
* const getUserQuery = sharedQuery({
|
|
362
|
+
* queryName: "getUser",
|
|
363
|
+
* queryFn: (userId: string) => {
|
|
364
|
+
* // Get the signal for this current execution.
|
|
365
|
+
* const signal = getUserQuery.getAbortSignal([userId]);
|
|
366
|
+
*
|
|
367
|
+
* // Pass the signal to fetch so that it can be aborted.
|
|
368
|
+
* const response = await fetch(`/users/${userId}`, { signal });
|
|
369
|
+
*
|
|
370
|
+
* // Check for errors.
|
|
371
|
+
* if (!response.ok) {
|
|
372
|
+
* throw new Error(response.statusText);
|
|
373
|
+
* }
|
|
374
|
+
*
|
|
375
|
+
* // Return the data.
|
|
376
|
+
* return await response.json();
|
|
377
|
+
* },
|
|
378
|
+
* });
|
|
379
|
+
* ```
|
|
380
|
+
*/
|
|
381
|
+
getAbortSignal(args) {
|
|
382
|
+
const queryKey = this.getQueryKey(args);
|
|
383
|
+
return this.getAbortSignalByKey(queryKey);
|
|
384
|
+
}
|
|
385
|
+
getAbortSignalByKey(queryKey) {
|
|
386
|
+
return this.getAbortControllerByKey(queryKey)?.signal ?? null;
|
|
387
|
+
}
|
|
315
388
|
isStale(dataUpdatedMs) {
|
|
316
389
|
if (this.staleMs === 0) return true;
|
|
317
390
|
const ageMs = Date.now() - dataUpdatedMs;
|
|
@@ -327,46 +400,53 @@ var SharedQuery = class {
|
|
|
327
400
|
const cached = this.queryState.getSnapshot()?.[queryKey];
|
|
328
401
|
if (cached?.data && !this.isExpired(cached.dataUpdatedMs ?? 0)) {
|
|
329
402
|
if (!this.isStale(cached.dataUpdatedMs ?? 0)) {
|
|
330
|
-
|
|
331
|
-
`Return fresh data ${queryKey} from cache without fetching`
|
|
332
|
-
);
|
|
403
|
+
this.log(`Return fresh data ${queryKey} from cache without fetching`);
|
|
333
404
|
return cached.data;
|
|
334
405
|
}
|
|
335
406
|
if (this.refetchOnStale) {
|
|
336
|
-
|
|
407
|
+
this.log(`refetchOnStale for ${queryKey}`);
|
|
337
408
|
void this.dedupedFetch(queryKey, ...args);
|
|
338
409
|
}
|
|
339
|
-
|
|
410
|
+
this.log(`Returning stale data for ${queryKey}`);
|
|
340
411
|
return cached.data;
|
|
341
412
|
}
|
|
342
413
|
return await this.dedupedFetch(queryKey, ...args);
|
|
343
414
|
}
|
|
344
415
|
dedupedFetch(queryKey, ...args) {
|
|
345
|
-
const
|
|
346
|
-
if (
|
|
347
|
-
|
|
348
|
-
return
|
|
416
|
+
const inflightQuery = this.inflightQueries.get(queryKey);
|
|
417
|
+
if (inflightQuery) {
|
|
418
|
+
this.log(`Deduplicating inflight fetch for ${queryKey}`);
|
|
419
|
+
return inflightQuery.promise;
|
|
349
420
|
}
|
|
350
421
|
const promise = this.startFetching(queryKey, args);
|
|
351
|
-
|
|
422
|
+
const abortController = new AbortController();
|
|
423
|
+
this.inflightQueries.set(queryKey, { promise, abortController });
|
|
352
424
|
return promise;
|
|
353
425
|
}
|
|
354
426
|
async startFetching(queryKey, args) {
|
|
355
|
-
|
|
427
|
+
this.log(`Start fetching ${queryKey}`);
|
|
356
428
|
try {
|
|
357
429
|
const data = await this.queryFn(...args);
|
|
358
|
-
|
|
430
|
+
this.log(`Successfully fetched ${queryKey}`);
|
|
359
431
|
this.setData(queryKey, data, Date.now());
|
|
360
432
|
return data;
|
|
361
433
|
} catch (e) {
|
|
362
|
-
|
|
434
|
+
this.log(`Failed to fetch ${queryKey}: ${e}`);
|
|
363
435
|
throw e;
|
|
364
436
|
} finally {
|
|
365
|
-
this.
|
|
437
|
+
this.inflightQueries.delete(queryKey);
|
|
366
438
|
}
|
|
367
439
|
}
|
|
440
|
+
/** Get the entire stored entry for a query key. */
|
|
441
|
+
getQueryValue(queryKey) {
|
|
442
|
+
return this.queryState.getSnapshot()?.[queryKey];
|
|
443
|
+
}
|
|
444
|
+
/** Get the current stored data for a query key. */
|
|
445
|
+
getData(queryKey) {
|
|
446
|
+
return this.queryState.getSnapshot()?.[queryKey]?.data;
|
|
447
|
+
}
|
|
368
448
|
/** Set the data directly if the user obtained it from somewhere else. */
|
|
369
|
-
|
|
449
|
+
setData(queryKey, data, dataUpdatedMs = Date.now()) {
|
|
370
450
|
const entry = {
|
|
371
451
|
lastUpdatedMs: Date.now(),
|
|
372
452
|
loading: false,
|
|
@@ -376,24 +456,35 @@ var SharedQuery = class {
|
|
|
376
456
|
this.queryState.setValue((prev) => ({ ...prev, [queryKey]: entry }));
|
|
377
457
|
this.enforceSizeLimit();
|
|
378
458
|
}
|
|
379
|
-
/**
|
|
459
|
+
/**
|
|
460
|
+
* Do a new fetch even when the data is already cached, but don't fetch if
|
|
461
|
+
* another fetch is already inflight.
|
|
462
|
+
*/
|
|
380
463
|
async updateFromSource(...args) {
|
|
381
464
|
const key = this.getQueryKey(args);
|
|
382
465
|
return await this.dedupedFetch(key, ...args);
|
|
383
466
|
}
|
|
384
467
|
/** Delete the cached data for one set of arguments. */
|
|
385
|
-
deleteData(
|
|
386
|
-
const queryKey = this.getQueryKey(args);
|
|
468
|
+
deleteData(queryKey) {
|
|
387
469
|
const record = this.queryState.getSnapshot();
|
|
388
470
|
const clone = { ...record };
|
|
389
471
|
delete clone[queryKey];
|
|
390
472
|
this.queryState.setValue(clone);
|
|
391
|
-
this.
|
|
473
|
+
const inflight = this.inflightQueries.get(queryKey);
|
|
474
|
+
if (inflight) {
|
|
475
|
+
this.inflightQueries.delete(queryKey);
|
|
476
|
+
inflight.abortController.abort("Deleted");
|
|
477
|
+
}
|
|
392
478
|
}
|
|
393
479
|
/** Delete all currently cached data & all inflight promises. */
|
|
394
480
|
clear() {
|
|
395
481
|
this.queryState.delete();
|
|
396
|
-
|
|
482
|
+
for (const { abortController } of Array.from(
|
|
483
|
+
this.inflightQueries.values()
|
|
484
|
+
)) {
|
|
485
|
+
abortController.abort("Cleared");
|
|
486
|
+
}
|
|
487
|
+
this.inflightQueries.clear();
|
|
397
488
|
}
|
|
398
489
|
/**
|
|
399
490
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
@@ -414,7 +505,7 @@ var SharedQuery = class {
|
|
|
414
505
|
if (this.maxSize) {
|
|
415
506
|
const numToCut = oldSize - this.maxSize;
|
|
416
507
|
if (numToCut > 0) {
|
|
417
|
-
|
|
508
|
+
this.log(
|
|
418
509
|
`enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
|
|
419
510
|
);
|
|
420
511
|
newState = { ...newState };
|
|
@@ -433,7 +524,7 @@ var SharedQuery = class {
|
|
|
433
524
|
const currentByteSize = needUpdate ? getByteSize(newState) : oldByteSize;
|
|
434
525
|
let bytesToCut = currentByteSize - this.maxBytes;
|
|
435
526
|
if (bytesToCut > 0) {
|
|
436
|
-
|
|
527
|
+
this.log(
|
|
437
528
|
`enforceSizeLimit for ${this.queryName} needed due to byte size ${currentByteSize.toLocaleString()} > ${this.maxBytes.toLocaleString()}`
|
|
438
529
|
);
|
|
439
530
|
if (!needUpdate) {
|
|
@@ -458,12 +549,12 @@ var SharedQuery = class {
|
|
|
458
549
|
}
|
|
459
550
|
if (needUpdate) {
|
|
460
551
|
this.queryState.setValue(newState);
|
|
461
|
-
|
|
552
|
+
this.log(
|
|
462
553
|
`Trimmed data for ${this.queryName}: size=(${oldSize} -> ${Object.keys(newState).length}) (limit=${this.maxSize}), byteSize=(${oldByteSize.toLocaleString()} -> ${getByteSize(newState).toLocaleString()}) (limit=${this.maxBytes.toLocaleString()})`
|
|
463
554
|
);
|
|
464
555
|
return;
|
|
465
556
|
}
|
|
466
|
-
|
|
557
|
+
this.log(
|
|
467
558
|
`No need to trim data for ${this.queryName}: size=${oldSize} (limit=${this.maxSize}), byteSize=${oldByteSize.toLocaleString()} (limit=${this.maxBytes.toLocaleString()})`
|
|
468
559
|
);
|
|
469
560
|
}
|
|
@@ -481,77 +572,109 @@ var DEFAULT_QUERY_STATE_ENTRY = {
|
|
|
481
572
|
loading: true
|
|
482
573
|
};
|
|
483
574
|
function useSharedQuery(query, args = []) {
|
|
575
|
+
const stableArgs = useDeepMemo(args);
|
|
484
576
|
const [queryState, setQueryState] = useSharedState(query.queryState);
|
|
485
577
|
const isMounted = useRef(true);
|
|
486
|
-
const queryKey =
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
578
|
+
const queryKey = useMemo2(
|
|
579
|
+
() => query.getQueryKey(stableArgs),
|
|
580
|
+
[query, stableArgs]
|
|
581
|
+
);
|
|
582
|
+
const queryStateValue = queryState[queryKey];
|
|
583
|
+
const setQueryStateValue = useCallback(
|
|
584
|
+
(arg) => {
|
|
585
|
+
setQueryState((prev) => {
|
|
586
|
+
const clone = { ...prev };
|
|
587
|
+
clone[queryKey] = typeof arg === "function" ? arg(clone[queryKey]) : arg;
|
|
588
|
+
return clone;
|
|
589
|
+
});
|
|
590
|
+
},
|
|
591
|
+
[queryKey, setQueryState]
|
|
592
|
+
);
|
|
593
|
+
const execute = useCallback(
|
|
594
|
+
async (forceRefresh) => {
|
|
595
|
+
query.log(`Begin executing shared query ${queryKey}`);
|
|
596
|
+
setQueryStateValue((prev) => ({
|
|
597
|
+
// Keep old data and error.
|
|
598
|
+
...prev ?? {},
|
|
493
599
|
lastUpdatedMs: Date.now(),
|
|
494
600
|
loading: true
|
|
495
|
-
};
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const data = await query.getCachedOrFetch(...args);
|
|
500
|
-
if (isMounted.current) {
|
|
501
|
-
setQueryState((prev) => {
|
|
502
|
-
const clone = { ...prev };
|
|
601
|
+
}));
|
|
602
|
+
try {
|
|
603
|
+
const data = await (forceRefresh ? query.updateFromSource(...stableArgs) : query.getCachedOrFetch(...stableArgs));
|
|
604
|
+
if (isMounted.current) {
|
|
503
605
|
const now = Date.now();
|
|
504
|
-
|
|
606
|
+
setQueryStateValue({
|
|
505
607
|
// Don't keep old error.
|
|
506
608
|
lastUpdatedMs: now,
|
|
507
609
|
loading: false,
|
|
508
610
|
data,
|
|
509
611
|
dataUpdatedMs: now
|
|
510
|
-
};
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
} catch (e) {
|
|
515
|
-
const error = e instanceof Error ? e : new Error(`An unknown error occurred during fetch: ${e}`);
|
|
516
|
-
if (isMounted.current) {
|
|
517
|
-
setQueryState((prev) => {
|
|
518
|
-
const clone = { ...prev };
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
} catch (e) {
|
|
615
|
+
if (isMounted.current) {
|
|
519
616
|
const now = Date.now();
|
|
520
|
-
|
|
617
|
+
setQueryStateValue((prev) => ({
|
|
521
618
|
// Keep old data.
|
|
522
|
-
...
|
|
619
|
+
...prev ?? {},
|
|
523
620
|
lastUpdatedMs: now,
|
|
524
621
|
loading: false,
|
|
525
|
-
error,
|
|
622
|
+
error: e,
|
|
526
623
|
errorUpdatedMs: now
|
|
527
|
-
};
|
|
528
|
-
|
|
529
|
-
});
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
530
626
|
}
|
|
531
|
-
}
|
|
532
|
-
|
|
627
|
+
},
|
|
628
|
+
[query, queryKey, setQueryStateValue, stableArgs]
|
|
629
|
+
);
|
|
533
630
|
useEffect(() => {
|
|
534
631
|
isMounted.current = true;
|
|
535
|
-
void execute();
|
|
632
|
+
void execute(false);
|
|
536
633
|
return () => {
|
|
537
634
|
isMounted.current = false;
|
|
538
635
|
};
|
|
539
636
|
}, [execute]);
|
|
540
|
-
return
|
|
541
|
-
const state =
|
|
637
|
+
return useMemo2(() => {
|
|
638
|
+
const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
542
639
|
return {
|
|
543
640
|
...state,
|
|
641
|
+
// This works only if the user-given queryFn supports abort. If not,
|
|
642
|
+
// this function doesn't do anything, since we don't have any means to
|
|
643
|
+
// abort the running queryFn.
|
|
644
|
+
abortCurrentQuery: (reason) => {
|
|
645
|
+
const controller = query.getAbortControllerByKey(queryKey);
|
|
646
|
+
controller?.abort(reason);
|
|
647
|
+
return Boolean(controller);
|
|
648
|
+
},
|
|
544
649
|
refetch: () => {
|
|
545
|
-
|
|
650
|
+
void execute(true);
|
|
546
651
|
},
|
|
547
652
|
setData: (data, dataUpdatedMs) => {
|
|
548
|
-
|
|
653
|
+
const now = Date.now();
|
|
654
|
+
setQueryStateValue((prev) => ({
|
|
655
|
+
...prev ?? {},
|
|
656
|
+
data,
|
|
657
|
+
dataUpdatedMs: dataUpdatedMs ?? now,
|
|
658
|
+
lastUpdatedMs: now,
|
|
659
|
+
loading: false
|
|
660
|
+
}));
|
|
549
661
|
},
|
|
550
662
|
deleteData: () => {
|
|
551
|
-
|
|
663
|
+
setQueryState((prev) => {
|
|
664
|
+
const clone = { ...prev };
|
|
665
|
+
delete clone[queryKey];
|
|
666
|
+
return clone;
|
|
667
|
+
});
|
|
552
668
|
}
|
|
553
669
|
};
|
|
554
|
-
}, [
|
|
670
|
+
}, [
|
|
671
|
+
execute,
|
|
672
|
+
query,
|
|
673
|
+
queryKey,
|
|
674
|
+
queryStateValue,
|
|
675
|
+
setQueryState,
|
|
676
|
+
setQueryStateValue
|
|
677
|
+
]);
|
|
555
678
|
}
|
|
556
679
|
export {
|
|
557
680
|
SharedQuery,
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var l=Object.defineProperty;var w=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var m=Object.prototype.hasOwnProperty;var K=(r,e)=>{for(var t in e)l(r,t,{get:e[t],enumerable:!0})},P=(r,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of x(e))!m.call(r,n)&&n!==t&&l(r,n,{get:()=>e[n],enumerable:!(i=w(e,n))||i.enumerable});return r};var A=r=>P(l({},"__esModule",{value:!0}),r);var B={};K(B,{SharedState:()=>d,SharedStateDefaults:()=>f,sharedState:()=>O,useSharedState:()=>h,useSharedStateSetter:()=>M,useSharedStateValue:()=>V});module.exports=A(B);var g=require("@choksheak/ts-utils/timeConstants"),S=require("react");var s=require("@choksheak/ts-utils/kvStore");function T(r,e){let{localStorageKey:t,indexedDbKey:i,storeExpiryMs:n,store:a}=r??{};if(a)return a;let u=n||e;return t?new c(t,u):i?new p(i,u):null}var c=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;window.localStorage.setItem(i,JSON.stringify({value:t,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=window.localStorage.getItem(t);if(i)try{let n=JSON.parse(i);if(!n||n.expiresAt&&Date.now()>=n.expiresAt){window.localStorage.removeItem(t);return}return n.value}catch{window.localStorage.removeItem(t);return}}delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.removeItem(t)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},p=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}async save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;await s.kvStore.set(i,JSON.stringify(t),this.expiryMs)}async load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=await s.kvStore.get(t);if(i!==void 0)try{return JSON.parse(i)}catch{await s.kvStore.delete(t);return}}async delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await s.kvStore.delete(t)}async clear(){typeof window>"u"||await s.kvStore.forEach(async e=>{e.startsWith(this.keyPrefix)&&await s.kvStore.delete(e)})}};var f={storeExpiryMs:30*g.MS_PER_DAY,lazyLoad:!1},b=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let i=this.dataByKey.get(e);i!==t&&(this.dataByKey.set(e,t),this.notify(e,t,i))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let i=this.subscribersByKey.get(e);return i||(i=new Set,this.subscribersByKey.set(e,i)),i.add(t),()=>{i.delete(t),i.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,i){let n=this.subscribersByKey.get(e);if(!n)return;let a=Array.from(n.values());for(let u of a)try{u(t,i)}catch(v){console.error("Error invoking subscriber:",v)}}},o=new b,y="state",k=0,d=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(k++),this.storageAdapter=T(t,f.storeExpiryMs),this.setValueBounded=a=>this.setValue(a),o.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:f.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return o.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return o.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=o.get(this.pubSubKey);e=e(t)}o.set(this.pubSubKey,e),this.storageAdapter?.save(y,e)}delete(){o.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.save(y,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.load(y);o.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function O(r,e){return new d(r,e)}function h(r){return r.initDefaultValueOnce(),[(0,S.useSyncExternalStore)(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function V(r){return h(r)[0]}function M(r){return h(r)[1]}0&&(module.exports={SharedState,SharedStateDefaults,sharedState,useSharedState,useSharedStateSetter,useSharedStateValue});
|
|
2
|
+
//# sourceMappingURL=sharedState.min.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { useSyncExternalStore } from \"react\";\n\nimport {\n getStorageAdapter,\n StorageAdapter,\n StorageOptions,\n} from \"./utils/storage\";\n\n/**\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. It is largely similar to the jotai library, but is much smaller\n * and much easier to work with when used for persisting in local storage or\n * indexed db, with an automatic expiration for persisted values.\n *\n * This library aims to be the shortest, simplest, easiest to use, and most\n * efficient implementation of shared global React states with seamless support\n * for client persistence.\n */\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateDefaults = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nclass PubSubStore {\n private dataByKey = new Map<string, unknown>();\n private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n public get<T>(key: string): T {\n return this.dataByKey.get(key) as T;\n }\n\n public set<T>(key: string, value: T): void {\n const prev = this.dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n this.dataByKey.set(key, value);\n this.notify(key, value, prev);\n }\n\n public delete(key: string): void {\n this.dataByKey.delete(key);\n }\n\n public setNoNotify<T>(key: string, value: T): void {\n this.dataByKey.set(key, value);\n }\n\n // Subscribe to changes of a specific key\n public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = this.subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n this.subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) this.subscribersByKey.delete(key);\n };\n }\n\n private notify<T>(key: string, next: T, prev: T): void {\n const subscribers = this.subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n}\n\nconst pubSubStore = new PubSubStore();\n\nconst STORAGE_KEY = \"state\";\n\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\nexport class SharedState<T> {\n private readonly pubSubKey: string;\n private readonly storageAdapter: StorageAdapter<T> | null;\n public readonly setValueBounded: (next: T | ((prev: T) => T)) => void;\n public initDone = false;\n\n public constructor(\n private readonly defaultValue: T,\n options?: SharedStateOptions<T>,\n ) {\n this.pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n this.storageAdapter = getStorageAdapter(\n options,\n SharedStateDefaults.storeExpiryMs,\n );\n\n // Same as this.setValue, but binding the `this` reference.\n this.setValueBounded = (arg) => this.setValue(arg);\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(this.pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy =\n lazyLoad !== undefined ? lazyLoad : SharedStateDefaults.lazyLoad;\n\n if (!lazy) {\n void this.initDefaultValueOnce();\n }\n }\n\n // `subscribe` function required by useSyncExternalStore\n public subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(this.pubSubKey, () => subscriber());\n }\n\n // Get the value of the shared state.\n // `getSnapshot` function — must return same ref if value unchanged\n public getSnapshot(): T {\n return pubSubStore.get(this.pubSubKey);\n }\n\n // Set the value of the shared state.\n public setValue(next: T | ((prev: T) => T)): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(this.pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(this.pubSubKey, next);\n void this.storageAdapter?.save(STORAGE_KEY, next);\n }\n\n // Remove the value of the shared state. Actually this just sets the value\n // back to the given default value.\n public delete() {\n pubSubStore.set(this.pubSubKey, this.defaultValue);\n void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);\n }\n\n // Initial default handling\n public async initDefaultValueOnce() {\n if (this.initDone) return;\n this.initDone = true;\n\n const v = await this.storageAdapter?.load(STORAGE_KEY);\n pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);\n }\n}\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * const myState = sharedState<number>(0);\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n): SharedState<T> {\n return new SharedState(defaultValue, options);\n}\n\n/**\n * React hook for subscribing to a key in the global store.\n * const [my, setMy] = useSharedState(myState);\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, (next: T | ((prev: T) => T)) => void] {\n state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValueBounded];\n}\n\n/** E.g. const my = useSharedStateValue(myState) */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/** E.g. const setMy = useSharedStateSetter(myState) */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n return useSharedState(state)[1];\n}\n","import { kvStore } from \"@choksheak/ts-utils/kvStore\";\n\nexport type StorageAdapter<T> = {\n save: (key: string, value: T) => Promise<void> | void;\n load: (key: string) => Promise<T | undefined> | T | undefined;\n delete: (key: string) => Promise<void> | void;\n clear: () => Promise<void> | void;\n};\n\nexport type StorageOptions<T> = {\n // If specified, this will be used and all other fields will be ignored.\n store?: StorageAdapter<T>;\n\n // Storage key in local storage for this data.\n localStorageKey?: string;\n\n // Storage key in indexed db for this data.\n indexedDbKey?: string;\n\n // How long to wait before the value is considered expired.\n storeExpiryMs?: number;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};\n\n if (store) {\n return store;\n }\n\n const expiryMs = storeExpiryMs || defaultStoreExpiryMs;\n\n if (localStorageKey) {\n return new LocalStorageAdapter<T>(localStorageKey, expiryMs);\n }\n\n if (indexedDbKey) {\n return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);\n }\n\n return null;\n}\n\n/************************************************************************/\n/* Storage adapters */\n/************************************************************************/\n\ntype ValueWithExpiration<T> = { value: T; expiresAt: number };\n\n/** Save state to memory only - not persisted. */\nexport class InMemoryStorageAdapter<T> implements StorageAdapter<T> {\n private readonly cache = new Map<string, ValueWithExpiration<T>>();\n\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.set(fullKey, {\n value,\n expiresAt: Date.now() + this.expiryMs,\n });\n }\n\n public load(key: string): T | undefined {\n const fullKey = this.keyPrefix + \":\" + key;\n const cached = this.cache.get(fullKey);\n\n if (cached && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n\n return undefined;\n }\n\n public delete(key: string): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.delete(fullKey);\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\n/** Save state to local storage. */\nexport class LocalStorageAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.setItem(\n fullKey,\n JSON.stringify({\n value,\n expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,\n } satisfies ValueWithExpiration<T>),\n );\n }\n\n public load(key: string): T | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = window.localStorage.getItem(fullKey);\n if (!raw) return undefined;\n\n try {\n const box = JSON.parse(raw) as ValueWithExpiration<T>;\n if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n\n return box.value;\n } catch {\n // Ignore error.\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n }\n\n public delete(key: string): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.removeItem(fullKey);\n }\n\n public clear(): void {\n if (typeof window === \"undefined\") return;\n\n for (const key of Object.keys(window.localStorage)) {\n if (key.startsWith(this.keyPrefix)) {\n window.localStorage.removeItem(key);\n }\n }\n }\n}\n\n/** Save state to indexed db. */\nexport class IndexedDbAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public async save(key: string, value: T): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);\n }\n\n public async load(key: string): Promise<T | undefined> {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = await kvStore.get<string>(fullKey);\n if (raw === undefined) return undefined;\n\n try {\n return JSON.parse(raw);\n } catch {\n // Ignore error.\n await kvStore.delete(fullKey);\n return undefined;\n }\n }\n\n public async delete(key: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.delete(fullKey);\n }\n\n public async clear(): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n await kvStore.forEach(async (k) => {\n if (k.startsWith(this.keyPrefix)) {\n await kvStore.delete(k);\n }\n });\n }\n}\n"],"mappings":"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","names":["sharedState_exports","__export","SharedState","SharedStateDefaults","sharedState","useSharedState","useSharedStateSetter","useSharedStateValue","__toCommonJS","import_timeConstants","import_react","import_kvStore","getStorageAdapter","options","defaultStoreExpiryMs","localStorageKey","indexedDbKey","storeExpiryMs","store","expiryMs","LocalStorageAdapter","IndexedDbAdapter","LocalStorageAdapter","keyPrefix","expiryMs","key","value","fullKey","raw","box","IndexedDbAdapter","k","SharedStateDefaults","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","arg","lazyLoad","v","sharedState","useSharedState","state","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{MS_PER_DAY as g}from"@choksheak/ts-utils/timeConstants";import{useSyncExternalStore as S}from"react";import{kvStore as a}from"@choksheak/ts-utils/kvStore";function f(r,e){let{localStorageKey:t,indexedDbKey:i,storeExpiryMs:n,store:o}=r??{};if(o)return o;let u=n||e;return t?new d(t,u):i?new l(i,u):null}var d=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;window.localStorage.setItem(i,JSON.stringify({value:t,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=window.localStorage.getItem(t);if(i)try{let n=JSON.parse(i);if(!n||n.expiresAt&&Date.now()>=n.expiresAt){window.localStorage.removeItem(t);return}return n.value}catch{window.localStorage.removeItem(t);return}}delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.removeItem(t)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},l=class{constructor(e,t){this.keyPrefix=e;this.expiryMs=t}async save(e,t){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;await a.set(i,JSON.stringify(t),this.expiryMs)}async load(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e,i=await a.get(t);if(i!==void 0)try{return JSON.parse(i)}catch{await a.delete(t);return}}async delete(e){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await a.delete(t)}async clear(){typeof window>"u"||await a.forEach(async e=>{e.startsWith(this.keyPrefix)&&await a.delete(e)})}};var b={storeExpiryMs:30*g,lazyLoad:!1},p=class{dataByKey=new Map;subscribersByKey=new Map;get(e){return this.dataByKey.get(e)}set(e,t){let i=this.dataByKey.get(e);i!==t&&(this.dataByKey.set(e,t),this.notify(e,t,i))}delete(e){this.dataByKey.delete(e)}setNoNotify(e,t){this.dataByKey.set(e,t)}subscribe(e,t){let i=this.subscribersByKey.get(e);return i||(i=new Set,this.subscribersByKey.set(e,i)),i.add(t),()=>{i.delete(t),i.size===0&&this.subscribersByKey.delete(e)}}notify(e,t,i){let n=this.subscribersByKey.get(e);if(!n)return;let o=Array.from(n.values());for(let u of o)try{u(t,i)}catch(T){console.error("Error invoking subscriber:",T)}}},s=new p,c="state",v=0,y=class{constructor(e,t){this.defaultValue=e;this.pubSubKey=String(v++),this.storageAdapter=f(t,b.storeExpiryMs),this.setValueBounded=o=>this.setValue(o),s.setNoNotify(this.pubSubKey,e);let i=t?.lazyLoad;(i!==void 0?i:b.lazyLoad)||this.initDefaultValueOnce()}pubSubKey;storageAdapter;setValueBounded;initDone=!1;subscribe(e){return s.subscribe(this.pubSubKey,()=>e())}getSnapshot(){return s.get(this.pubSubKey)}setValue(e){if(typeof e=="function"){let t=s.get(this.pubSubKey);e=e(t)}s.set(this.pubSubKey,e),this.storageAdapter?.save(c,e)}delete(){s.set(this.pubSubKey,this.defaultValue),this.storageAdapter?.save(c,this.defaultValue)}async initDefaultValueOnce(){if(this.initDone)return;this.initDone=!0;let e=await this.storageAdapter?.load(c);s.set(this.pubSubKey,e!==void 0?e:this.defaultValue)}};function O(r,e){return new y(r,e)}function h(r){return r.initDefaultValueOnce(),[S(t=>r.subscribe(t),()=>r.getSnapshot(),()=>r.getSnapshot()),r.setValueBounded]}function V(r){return h(r)[0]}function M(r){return h(r)[1]}export{y as SharedState,b as SharedStateDefaults,O as sharedState,h as useSharedState,M as useSharedStateSetter,V as useSharedStateValue};
|
|
2
|
+
//# sourceMappingURL=sharedState.min.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["import { MS_PER_DAY } from \"@choksheak/ts-utils/timeConstants\";\nimport { useSyncExternalStore } from \"react\";\n\nimport {\n getStorageAdapter,\n StorageAdapter,\n StorageOptions,\n} from \"./utils/storage\";\n\n/**\n * The \"shared state\" is a React state that shares the same value across the\n * entire app. It is largely similar to the jotai library, but is much smaller\n * and much easier to work with when used for persisting in local storage or\n * indexed db, with an automatic expiration for persisted values.\n *\n * This library aims to be the shortest, simplest, easiest to use, and most\n * efficient implementation of shared global React states with seamless support\n * for client persistence.\n */\n\n/************************************************************************/\n/* Global config */\n/************************************************************************/\n\n/**\n * These values are configurable by the user at any time.\n */\nexport const SharedStateDefaults = {\n /**\n * Default duration for storage expiration in milliseconds. Initial value\n * is 30 days, but can be changed to anything.\n */\n storeExpiryMs: 30 * MS_PER_DAY,\n\n /**\n * True to load from storage only on mount, false (default) to load in\n * the top level scope when the shared state is defined.\n */\n lazyLoad: false,\n};\n\n/************************************************************************/\n/* Shared state implementation */\n/************************************************************************/\n\ntype Subscriber<T> = (next: T, prev: T) => void;\n\nclass PubSubStore {\n private dataByKey = new Map<string, unknown>();\n private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();\n\n // This could be undefined if T includes undefined, else it should never\n // return undefined because the user has to always specify the default value.\n public get<T>(key: string): T {\n return this.dataByKey.get(key) as T;\n }\n\n public set<T>(key: string, value: T): void {\n const prev = this.dataByKey.get(key) as T;\n\n // Do nothing if no change.\n if (prev === value) return;\n\n this.dataByKey.set(key, value);\n this.notify(key, value, prev);\n }\n\n public delete(key: string): void {\n this.dataByKey.delete(key);\n }\n\n public setNoNotify<T>(key: string, value: T): void {\n this.dataByKey.set(key, value);\n }\n\n // Subscribe to changes of a specific key\n public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {\n let subscribers = this.subscribersByKey.get(key);\n if (!subscribers) {\n subscribers = new Set();\n this.subscribersByKey.set(key, subscribers);\n }\n\n // Type coercion: we store Subscriber<unknown>\n subscribers.add(subscriber as Subscriber<unknown>);\n\n // Return unsubscribe function\n return () => {\n subscribers.delete(subscriber as Subscriber<unknown>);\n if (subscribers.size === 0) this.subscribersByKey.delete(key);\n };\n }\n\n private notify<T>(key: string, next: T, prev: T): void {\n const subscribers = this.subscribersByKey.get(key);\n if (!subscribers) return;\n\n // Make copy to protect against mutation during iteration.\n const copy = Array.from(subscribers.values());\n for (const subscriber of copy) {\n try {\n subscriber(next, prev);\n } catch (e) {\n console.error(`Error invoking subscriber:`, e);\n }\n }\n }\n}\n\nconst pubSubStore = new PubSubStore();\n\nconst STORAGE_KEY = \"state\";\n\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\nexport class SharedState<T> {\n private readonly pubSubKey: string;\n private readonly storageAdapter: StorageAdapter<T> | null;\n public readonly setValueBounded: (next: T | ((prev: T) => T)) => void;\n public initDone = false;\n\n public constructor(\n private readonly defaultValue: T,\n options?: SharedStateOptions<T>,\n ) {\n this.pubSubKey = String(stateKey++);\n\n // Use max of 1 storage adapter per shared state.\n this.storageAdapter = getStorageAdapter(\n options,\n SharedStateDefaults.storeExpiryMs,\n );\n\n // Same as this.setValue, but binding the `this` reference.\n this.setValueBounded = (arg) => this.setValue(arg);\n\n // Always set the default value first to avoid returning undefineds if that\n // is not part of T.\n pubSubStore.setNoNotify(this.pubSubKey, defaultValue);\n\n const lazyLoad = options?.lazyLoad;\n const lazy =\n lazyLoad !== undefined ? lazyLoad : SharedStateDefaults.lazyLoad;\n\n if (!lazy) {\n void this.initDefaultValueOnce();\n }\n }\n\n // `subscribe` function required by useSyncExternalStore\n public subscribe(subscriber: () => void): () => void {\n return pubSubStore.subscribe(this.pubSubKey, () => subscriber());\n }\n\n // Get the value of the shared state.\n // `getSnapshot` function — must return same ref if value unchanged\n public getSnapshot(): T {\n return pubSubStore.get(this.pubSubKey);\n }\n\n // Set the value of the shared state.\n public setValue(next: T | ((prev: T) => T)): void {\n if (typeof next === \"function\") {\n const prev = pubSubStore.get<T>(this.pubSubKey);\n next = (next as (p: T) => T)(prev);\n }\n\n pubSubStore.set(this.pubSubKey, next);\n void this.storageAdapter?.save(STORAGE_KEY, next);\n }\n\n // Remove the value of the shared state. Actually this just sets the value\n // back to the given default value.\n public delete() {\n pubSubStore.set(this.pubSubKey, this.defaultValue);\n void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);\n }\n\n // Initial default handling\n public async initDefaultValueOnce() {\n if (this.initDone) return;\n this.initDone = true;\n\n const v = await this.storageAdapter?.load(STORAGE_KEY);\n pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);\n }\n}\n\n/**\n * Create a new shared state object. Put this code at the top level scope:\n *\n * const myState = sharedState<number>(0);\n */\nexport function sharedState<T>(\n defaultValue: T,\n options?: SharedStateOptions<T>,\n): SharedState<T> {\n return new SharedState(defaultValue, options);\n}\n\n/**\n * React hook for subscribing to a key in the global store.\n * const [my, setMy] = useSharedState(myState);\n */\nexport function useSharedState<T>(\n state: SharedState<T>,\n): [T, (next: T | ((prev: T) => T)) => void] {\n state.initDefaultValueOnce();\n\n const value = useSyncExternalStore(\n (onStoreChange) => state.subscribe(onStoreChange),\n () => state.getSnapshot(),\n // getServerSnapshot is needed to prevent SSR dev errors in nextjs.\n () => state.getSnapshot(),\n );\n\n return [value, state.setValueBounded];\n}\n\n/** E.g. const my = useSharedStateValue(myState) */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/** E.g. const setMy = useSharedStateSetter(myState) */\nexport function useSharedStateSetter<T>(state: SharedState<T>) {\n return useSharedState(state)[1];\n}\n","import { kvStore } from \"@choksheak/ts-utils/kvStore\";\n\nexport type StorageAdapter<T> = {\n save: (key: string, value: T) => Promise<void> | void;\n load: (key: string) => Promise<T | undefined> | T | undefined;\n delete: (key: string) => Promise<void> | void;\n clear: () => Promise<void> | void;\n};\n\nexport type StorageOptions<T> = {\n // If specified, this will be used and all other fields will be ignored.\n store?: StorageAdapter<T>;\n\n // Storage key in local storage for this data.\n localStorageKey?: string;\n\n // Storage key in indexed db for this data.\n indexedDbKey?: string;\n\n // How long to wait before the value is considered expired.\n storeExpiryMs?: number;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};\n\n if (store) {\n return store;\n }\n\n const expiryMs = storeExpiryMs || defaultStoreExpiryMs;\n\n if (localStorageKey) {\n return new LocalStorageAdapter<T>(localStorageKey, expiryMs);\n }\n\n if (indexedDbKey) {\n return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);\n }\n\n return null;\n}\n\n/************************************************************************/\n/* Storage adapters */\n/************************************************************************/\n\ntype ValueWithExpiration<T> = { value: T; expiresAt: number };\n\n/** Save state to memory only - not persisted. */\nexport class InMemoryStorageAdapter<T> implements StorageAdapter<T> {\n private readonly cache = new Map<string, ValueWithExpiration<T>>();\n\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.set(fullKey, {\n value,\n expiresAt: Date.now() + this.expiryMs,\n });\n }\n\n public load(key: string): T | undefined {\n const fullKey = this.keyPrefix + \":\" + key;\n const cached = this.cache.get(fullKey);\n\n if (cached && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n\n return undefined;\n }\n\n public delete(key: string): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.delete(fullKey);\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\n/** Save state to local storage. */\nexport class LocalStorageAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.setItem(\n fullKey,\n JSON.stringify({\n value,\n expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,\n } satisfies ValueWithExpiration<T>),\n );\n }\n\n public load(key: string): T | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = window.localStorage.getItem(fullKey);\n if (!raw) return undefined;\n\n try {\n const box = JSON.parse(raw) as ValueWithExpiration<T>;\n if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n\n return box.value;\n } catch {\n // Ignore error.\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n }\n\n public delete(key: string): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.removeItem(fullKey);\n }\n\n public clear(): void {\n if (typeof window === \"undefined\") return;\n\n for (const key of Object.keys(window.localStorage)) {\n if (key.startsWith(this.keyPrefix)) {\n window.localStorage.removeItem(key);\n }\n }\n }\n}\n\n/** Save state to indexed db. */\nexport class IndexedDbAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public async save(key: string, value: T): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);\n }\n\n public async load(key: string): Promise<T | undefined> {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = await kvStore.get<string>(fullKey);\n if (raw === undefined) return undefined;\n\n try {\n return JSON.parse(raw);\n } catch {\n // Ignore error.\n await kvStore.delete(fullKey);\n return undefined;\n }\n }\n\n public async delete(key: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.delete(fullKey);\n }\n\n public async clear(): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n await kvStore.forEach(async (k) => {\n if (k.startsWith(this.keyPrefix)) {\n await kvStore.delete(k);\n }\n });\n }\n}\n"],"mappings":"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","names":["MS_PER_DAY","useSyncExternalStore","kvStore","getStorageAdapter","options","defaultStoreExpiryMs","localStorageKey","indexedDbKey","storeExpiryMs","store","expiryMs","LocalStorageAdapter","IndexedDbAdapter","LocalStorageAdapter","keyPrefix","expiryMs","key","value","fullKey","raw","box","IndexedDbAdapter","kvStore","k","SharedStateDefaults","MS_PER_DAY","PubSubStore","key","value","prev","subscriber","subscribers","next","copy","e","pubSubStore","STORAGE_KEY","stateKey","SharedState","defaultValue","options","getStorageAdapter","arg","lazyLoad","v","sharedState","useSharedState","state","useSyncExternalStore","onStoreChange","useSharedStateValue","useSharedStateSetter"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var d=Object.defineProperty;var f=Object.getOwnPropertyDescriptor;var p=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var w=(r,e)=>{for(var i in e)d(r,i,{get:e[i],enumerable:!0})},x=(r,e,i,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of p(e))!y.call(r,n)&&n!==i&&d(r,n,{get:()=>e[n],enumerable:!(t=f(e,n))||t.enumerable});return r};var g=r=>x(d({},"__esModule",{value:!0}),r);var m={};w(m,{InMemoryStorageAdapter:()=>l,IndexedDbAdapter:()=>a,LocalStorageAdapter:()=>s,getStorageAdapter:()=>h});module.exports=g(m);var o=require("@choksheak/ts-utils/kvStore");function h(r,e){let{localStorageKey:i,indexedDbKey:t,storeExpiryMs:n,store:u}=r??{};if(u)return u;let c=n||e;return i?new s(i,c):t?new a(t,c):null}var l=class{constructor(e,i){this.keyPrefix=e;this.expiryMs=i}cache=new Map;save(e,i){let t=this.keyPrefix+":"+e;this.cache.set(t,{value:i,expiresAt:Date.now()+this.expiryMs})}load(e){let i=this.keyPrefix+":"+e,t=this.cache.get(i);if(t&&Date.now()<t.expiresAt)return t.value}delete(e){let i=this.keyPrefix+":"+e;this.cache.delete(i)}clear(){this.cache.clear()}},s=class{constructor(e,i){this.keyPrefix=e;this.expiryMs=i}save(e,i){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.setItem(t,JSON.stringify({value:i,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e,t=window.localStorage.getItem(i);if(t)try{let n=JSON.parse(t);if(!n||n.expiresAt&&Date.now()>=n.expiresAt){window.localStorage.removeItem(i);return}return n.value}catch{window.localStorage.removeItem(i);return}}delete(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;window.localStorage.removeItem(i)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},a=class{constructor(e,i){this.keyPrefix=e;this.expiryMs=i}async save(e,i){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await o.kvStore.set(t,JSON.stringify(i),this.expiryMs)}async load(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e,t=await o.kvStore.get(i);if(t!==void 0)try{return JSON.parse(t)}catch{await o.kvStore.delete(i);return}}async delete(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;await o.kvStore.delete(i)}async clear(){typeof window>"u"||await o.kvStore.forEach(async e=>{e.startsWith(this.keyPrefix)&&await o.kvStore.delete(e)})}};0&&(module.exports={InMemoryStorageAdapter,IndexedDbAdapter,LocalStorageAdapter,getStorageAdapter});
|
|
2
|
+
//# sourceMappingURL=storage.min.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/storage.ts"],"sourcesContent":["import { kvStore } from \"@choksheak/ts-utils/kvStore\";\n\nexport type StorageAdapter<T> = {\n save: (key: string, value: T) => Promise<void> | void;\n load: (key: string) => Promise<T | undefined> | T | undefined;\n delete: (key: string) => Promise<void> | void;\n clear: () => Promise<void> | void;\n};\n\nexport type StorageOptions<T> = {\n // If specified, this will be used and all other fields will be ignored.\n store?: StorageAdapter<T>;\n\n // Storage key in local storage for this data.\n localStorageKey?: string;\n\n // Storage key in indexed db for this data.\n indexedDbKey?: string;\n\n // How long to wait before the value is considered expired.\n storeExpiryMs?: number;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};\n\n if (store) {\n return store;\n }\n\n const expiryMs = storeExpiryMs || defaultStoreExpiryMs;\n\n if (localStorageKey) {\n return new LocalStorageAdapter<T>(localStorageKey, expiryMs);\n }\n\n if (indexedDbKey) {\n return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);\n }\n\n return null;\n}\n\n/************************************************************************/\n/* Storage adapters */\n/************************************************************************/\n\ntype ValueWithExpiration<T> = { value: T; expiresAt: number };\n\n/** Save state to memory only - not persisted. */\nexport class InMemoryStorageAdapter<T> implements StorageAdapter<T> {\n private readonly cache = new Map<string, ValueWithExpiration<T>>();\n\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.set(fullKey, {\n value,\n expiresAt: Date.now() + this.expiryMs,\n });\n }\n\n public load(key: string): T | undefined {\n const fullKey = this.keyPrefix + \":\" + key;\n const cached = this.cache.get(fullKey);\n\n if (cached && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n\n return undefined;\n }\n\n public delete(key: string): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.delete(fullKey);\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\n/** Save state to local storage. */\nexport class LocalStorageAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.setItem(\n fullKey,\n JSON.stringify({\n value,\n expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,\n } satisfies ValueWithExpiration<T>),\n );\n }\n\n public load(key: string): T | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = window.localStorage.getItem(fullKey);\n if (!raw) return undefined;\n\n try {\n const box = JSON.parse(raw) as ValueWithExpiration<T>;\n if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n\n return box.value;\n } catch {\n // Ignore error.\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n }\n\n public delete(key: string): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.removeItem(fullKey);\n }\n\n public clear(): void {\n if (typeof window === \"undefined\") return;\n\n for (const key of Object.keys(window.localStorage)) {\n if (key.startsWith(this.keyPrefix)) {\n window.localStorage.removeItem(key);\n }\n }\n }\n}\n\n/** Save state to indexed db. */\nexport class IndexedDbAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public async save(key: string, value: T): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);\n }\n\n public async load(key: string): Promise<T | undefined> {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = await kvStore.get<string>(fullKey);\n if (raw === undefined) return undefined;\n\n try {\n return JSON.parse(raw);\n } catch {\n // Ignore error.\n await kvStore.delete(fullKey);\n return undefined;\n }\n }\n\n public async delete(key: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.delete(fullKey);\n }\n\n public async clear(): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n await kvStore.forEach(async (k) => {\n if (k.startsWith(this.keyPrefix)) {\n await kvStore.delete(k);\n }\n });\n }\n}\n"],"mappings":"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","names":["storage_exports","__export","InMemoryStorageAdapter","IndexedDbAdapter","LocalStorageAdapter","getStorageAdapter","__toCommonJS","import_kvStore","options","defaultStoreExpiryMs","localStorageKey","indexedDbKey","storeExpiryMs","store","expiryMs","keyPrefix","key","value","fullKey","cached","raw","box","k"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{kvStore as n}from"@choksheak/ts-utils/kvStore";function f(o,e){let{localStorageKey:i,indexedDbKey:t,storeExpiryMs:r,store:d}=o??{};if(d)return d;let l=r||e;return i?new s(i,l):t?new a(t,l):null}var u=class{constructor(e,i){this.keyPrefix=e;this.expiryMs=i}cache=new Map;save(e,i){let t=this.keyPrefix+":"+e;this.cache.set(t,{value:i,expiresAt:Date.now()+this.expiryMs})}load(e){let i=this.keyPrefix+":"+e,t=this.cache.get(i);if(t&&Date.now()<t.expiresAt)return t.value}delete(e){let i=this.keyPrefix+":"+e;this.cache.delete(i)}clear(){this.cache.clear()}},s=class{constructor(e,i){this.keyPrefix=e;this.expiryMs=i}save(e,i){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;window.localStorage.setItem(t,JSON.stringify({value:i,expiresAt:this.expiryMs?Date.now()+this.expiryMs:0}))}load(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e,t=window.localStorage.getItem(i);if(t)try{let r=JSON.parse(t);if(!r||r.expiresAt&&Date.now()>=r.expiresAt){window.localStorage.removeItem(i);return}return r.value}catch{window.localStorage.removeItem(i);return}}delete(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;window.localStorage.removeItem(i)}clear(){if(!(typeof window>"u"))for(let e of Object.keys(window.localStorage))e.startsWith(this.keyPrefix)&&window.localStorage.removeItem(e)}},a=class{constructor(e,i){this.keyPrefix=e;this.expiryMs=i}async save(e,i){if(typeof window>"u")return;let t=this.keyPrefix+":"+e;await n.set(t,JSON.stringify(i),this.expiryMs)}async load(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e,t=await n.get(i);if(t!==void 0)try{return JSON.parse(t)}catch{await n.delete(i);return}}async delete(e){if(typeof window>"u")return;let i=this.keyPrefix+":"+e;await n.delete(i)}async clear(){typeof window>"u"||await n.forEach(async e=>{e.startsWith(this.keyPrefix)&&await n.delete(e)})}};export{u as InMemoryStorageAdapter,a as IndexedDbAdapter,s as LocalStorageAdapter,f as getStorageAdapter};
|
|
2
|
+
//# sourceMappingURL=storage.min.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/storage.ts"],"sourcesContent":["import { kvStore } from \"@choksheak/ts-utils/kvStore\";\n\nexport type StorageAdapter<T> = {\n save: (key: string, value: T) => Promise<void> | void;\n load: (key: string) => Promise<T | undefined> | T | undefined;\n delete: (key: string) => Promise<void> | void;\n clear: () => Promise<void> | void;\n};\n\nexport type StorageOptions<T> = {\n // If specified, this will be used and all other fields will be ignored.\n store?: StorageAdapter<T>;\n\n // Storage key in local storage for this data.\n localStorageKey?: string;\n\n // Storage key in indexed db for this data.\n indexedDbKey?: string;\n\n // How long to wait before the value is considered expired.\n storeExpiryMs?: number;\n};\n\nexport function getStorageAdapter<T>(\n options: StorageOptions<T> | undefined,\n defaultStoreExpiryMs: number,\n): StorageAdapter<T> | null {\n const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};\n\n if (store) {\n return store;\n }\n\n const expiryMs = storeExpiryMs || defaultStoreExpiryMs;\n\n if (localStorageKey) {\n return new LocalStorageAdapter<T>(localStorageKey, expiryMs);\n }\n\n if (indexedDbKey) {\n return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);\n }\n\n return null;\n}\n\n/************************************************************************/\n/* Storage adapters */\n/************************************************************************/\n\ntype ValueWithExpiration<T> = { value: T; expiresAt: number };\n\n/** Save state to memory only - not persisted. */\nexport class InMemoryStorageAdapter<T> implements StorageAdapter<T> {\n private readonly cache = new Map<string, ValueWithExpiration<T>>();\n\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.set(fullKey, {\n value,\n expiresAt: Date.now() + this.expiryMs,\n });\n }\n\n public load(key: string): T | undefined {\n const fullKey = this.keyPrefix + \":\" + key;\n const cached = this.cache.get(fullKey);\n\n if (cached && Date.now() < cached.expiresAt) {\n return cached.value;\n }\n\n return undefined;\n }\n\n public delete(key: string): void {\n const fullKey = this.keyPrefix + \":\" + key;\n this.cache.delete(fullKey);\n }\n\n public clear(): void {\n this.cache.clear();\n }\n}\n\n/** Save state to local storage. */\nexport class LocalStorageAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public save(key: string, value: T): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.setItem(\n fullKey,\n JSON.stringify({\n value,\n expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,\n } satisfies ValueWithExpiration<T>),\n );\n }\n\n public load(key: string): T | undefined {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = window.localStorage.getItem(fullKey);\n if (!raw) return undefined;\n\n try {\n const box = JSON.parse(raw) as ValueWithExpiration<T>;\n if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n\n return box.value;\n } catch {\n // Ignore error.\n window.localStorage.removeItem(fullKey);\n return undefined;\n }\n }\n\n public delete(key: string): void {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n window.localStorage.removeItem(fullKey);\n }\n\n public clear(): void {\n if (typeof window === \"undefined\") return;\n\n for (const key of Object.keys(window.localStorage)) {\n if (key.startsWith(this.keyPrefix)) {\n window.localStorage.removeItem(key);\n }\n }\n }\n}\n\n/** Save state to indexed db. */\nexport class IndexedDbAdapter<T> implements StorageAdapter<T> {\n public constructor(\n public readonly keyPrefix: string,\n public readonly expiryMs: number,\n ) {}\n\n public async save(key: string, value: T): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);\n }\n\n public async load(key: string): Promise<T | undefined> {\n if (typeof window === \"undefined\") return undefined;\n\n const fullKey = this.keyPrefix + \":\" + key;\n const raw = await kvStore.get<string>(fullKey);\n if (raw === undefined) return undefined;\n\n try {\n return JSON.parse(raw);\n } catch {\n // Ignore error.\n await kvStore.delete(fullKey);\n return undefined;\n }\n }\n\n public async delete(key: string): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n const fullKey = this.keyPrefix + \":\" + key;\n await kvStore.delete(fullKey);\n }\n\n public async clear(): Promise<void> {\n if (typeof window === \"undefined\") return;\n\n await kvStore.forEach(async (k) => {\n if (k.startsWith(this.keyPrefix)) {\n await kvStore.delete(k);\n }\n });\n }\n}\n"],"mappings":"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","names":["kvStore","getStorageAdapter","options","defaultStoreExpiryMs","localStorageKey","indexedDbKey","storeExpiryMs","store","expiryMs","LocalStorageAdapter","IndexedDbAdapter","InMemoryStorageAdapter","keyPrefix","key","value","fullKey","cached","raw","box","k"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";var s=Object.defineProperty;var c=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var d=(t,n)=>{for(var o in n)s(t,o,{get:n[o],enumerable:!0})},g=(t,n,o,r)=>{if(n&&typeof n=="object"||typeof n=="function")for(let i of f(n))!y.call(t,i)&&i!==o&&s(t,i,{get:()=>n[i],enumerable:!(r=c(n,i))||r.enumerable});return t};var w=t=>g(s({},"__esModule",{value:!0}),t);var u={};d(u,{stringifyDeterministicForKeys:()=>h});module.exports=w(u);function h(t){let n=new Set;return JSON.stringify(t,(o,r)=>{if(typeof r=="function")throw new Error("Functions should not be passed into query parameters");if(!r||typeof r!="object"||Array.isArray(r))return r;if(n.has(r))return"[Circular]";if(n.add(r),r!==t)try{JSON.stringify(r)}catch{return"[Unserializable]"}return Object.keys(r).sort().reduce((i,e)=>(i[e]=r[e],i),{})})}0&&(module.exports={stringifyDeterministicForKeys});
|
|
2
|
+
//# sourceMappingURL=stringify.min.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/utils/stringify.ts"],"sourcesContent":["/**\n * JSON stringify a value of any type, and returns an unsafe string. The string\n * is \"unsafe\" because you might not be able to restore the original object\n * using the serialized JSON. This is specifically used for query key generation\n * and is not suitable for more general uses.\n *\n * Note: The given value should not contain any functions because functions\n * cannot be serialized and deserialized properly.\n */\nexport function stringifyDeterministicForKeys(params: unknown): string {\n const cache = new Set();\n\n return JSON.stringify(params, (_, value) => {\n if (typeof value === \"function\") {\n throw new Error(`Functions should not be passed into query parameters`);\n }\n\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n return value;\n }\n\n if (cache.has(value)) {\n // Circular reference detected! Return a placeholder string.\n return \"[Circular]\";\n }\n\n cache.add(value);\n\n // Don't crash on circular references, but don't mark the top level object\n // as circular because we want to return as much string as possible.\n if (value !== params) {\n try {\n JSON.stringify(value);\n } catch {\n return \"[Unserializable]\";\n }\n }\n\n return Object.keys(value)\n .sort()\n .reduce(\n (result: Record<string, unknown>, key: string) => {\n result[key] = value[key];\n return result;\n },\n {} as Record<string, unknown>,\n );\n });\n}\n"],"mappings":"yaAAA,IAAAA,EAAA,GAAAC,EAAAD,EAAA,mCAAAE,IAAA,eAAAC,EAAAH,GASO,SAASE,EAA8BE,EAAyB,CACrE,IAAMC,EAAQ,IAAI,IAElB,OAAO,KAAK,UAAUD,EAAQ,CAACE,EAAGC,IAAU,CAC1C,GAAI,OAAOA,GAAU,WACnB,MAAM,IAAI,MAAM,sDAAsD,EAGxE,GAAI,CAACA,GAAS,OAAOA,GAAU,UAAY,MAAM,QAAQA,CAAK,EAC5D,OAAOA,EAGT,GAAIF,EAAM,IAAIE,CAAK,EAEjB,MAAO,aAOT,GAJAF,EAAM,IAAIE,CAAK,EAIXA,IAAUH,EACZ,GAAI,CACF,KAAK,UAAUG,CAAK,CACtB,MAAQ,CACN,MAAO,kBACT,CAGF,OAAO,OAAO,KAAKA,CAAK,EACrB,KAAK,EACL,OACC,CAACC,EAAiCC,KAChCD,EAAOC,CAAG,EAAIF,EAAME,CAAG,EAChBD,GAET,CAAC,CACH,CACJ,CAAC,CACH","names":["stringify_exports","__export","stringifyDeterministicForKeys","__toCommonJS","params","cache","_","value","result","key"]}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
function e(n){let t=new Set;return JSON.stringify(n,(s,r)=>{if(typeof r=="function")throw new Error("Functions should not be passed into query parameters");if(!r||typeof r!="object"||Array.isArray(r))return r;if(t.has(r))return"[Circular]";if(t.add(r),r!==n)try{JSON.stringify(r)}catch{return"[Unserializable]"}return Object.keys(r).sort().reduce((i,o)=>(i[o]=r[o],i),{})})}export{e as stringifyDeterministicForKeys};
|
|
2
|
+
//# sourceMappingURL=stringify.min.mjs.map
|