@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.
Files changed (93) hide show
  1. package/README.md +3 -1
  2. package/fetcher.min.cjs +2 -0
  3. package/fetcher.min.cjs.map +1 -0
  4. package/fetcher.min.mjs +2 -0
  5. package/fetcher.min.mjs.map +1 -0
  6. package/index.min.cjs +2 -0
  7. package/index.min.cjs.map +1 -0
  8. package/index.min.mjs +2 -0
  9. package/index.min.mjs.map +1 -0
  10. package/package.json +38 -5
  11. package/{sharedQuery.js → sharedQuery.cjs} +202 -79
  12. package/sharedQuery.d.mts +73 -9
  13. package/sharedQuery.d.ts +72 -8
  14. package/sharedQuery.min.cjs +2 -0
  15. package/sharedQuery.min.cjs.map +1 -0
  16. package/sharedQuery.min.mjs +2 -0
  17. package/sharedQuery.min.mjs.map +1 -0
  18. package/sharedQuery.mjs +200 -77
  19. package/sharedState.min.cjs +2 -0
  20. package/sharedState.min.cjs.map +1 -0
  21. package/sharedState.min.mjs +2 -0
  22. package/sharedState.min.mjs.map +1 -0
  23. package/utils/storage.min.cjs +2 -0
  24. package/utils/storage.min.cjs.map +1 -0
  25. package/utils/storage.min.mjs +2 -0
  26. package/utils/storage.min.mjs.map +1 -0
  27. package/utils/stringify.min.cjs +2 -0
  28. package/utils/stringify.min.cjs.map +1 -0
  29. package/utils/stringify.min.mjs +2 -0
  30. package/utils/stringify.min.mjs.map +1 -0
  31. package/utils/useDeepMemo.cjs +51 -0
  32. package/utils/useDeepMemo.d.mts +4 -0
  33. package/utils/useDeepMemo.d.ts +2 -0
  34. package/utils/useDeepMemo.min.cjs +2 -0
  35. package/utils/useDeepMemo.min.cjs.map +1 -0
  36. package/utils/useDeepMemo.min.mjs +2 -0
  37. package/utils/useDeepMemo.min.mjs.map +1 -0
  38. package/utils/useDeepMemo.mjs +16 -0
  39. package/docs/.nojekyll +0 -1
  40. package/docs/assets/hierarchy.js +0 -1
  41. package/docs/assets/highlight.css +0 -71
  42. package/docs/assets/icons.js +0 -18
  43. package/docs/assets/icons.svg +0 -1
  44. package/docs/assets/main.js +0 -60
  45. package/docs/assets/navigation.js +0 -1
  46. package/docs/assets/search.js +0 -1
  47. package/docs/assets/style.css +0 -1633
  48. package/docs/classes/sharedQuery.SharedQuery.html +0 -22
  49. package/docs/classes/sharedState.SharedState.html +0 -9
  50. package/docs/classes/utils_storage.InMemoryStorageAdapter.html +0 -9
  51. package/docs/classes/utils_storage.IndexedDbAdapter.html +0 -9
  52. package/docs/classes/utils_storage.LocalStorageAdapter.html +0 -9
  53. package/docs/functions/sharedQuery.sharedQuery.html +0 -9
  54. package/docs/functions/sharedQuery.useSharedQuery.html +0 -6
  55. package/docs/functions/sharedState.sharedState.html +0 -3
  56. package/docs/functions/sharedState.useSharedState.html +0 -3
  57. package/docs/functions/sharedState.useSharedStateSetter.html +0 -2
  58. package/docs/functions/sharedState.useSharedStateValue.html +0 -2
  59. package/docs/functions/utils_storage.getStorageAdapter.html +0 -1
  60. package/docs/functions/utils_stringify.stringifyDeterministicForKeys.html +0 -7
  61. package/docs/hierarchy.html +0 -1
  62. package/docs/index.html +0 -2
  63. package/docs/modules/fetcher.html +0 -1
  64. package/docs/modules/index.html +0 -1
  65. package/docs/modules/sharedQuery.html +0 -1
  66. package/docs/modules/sharedState.html +0 -1
  67. package/docs/modules/utils_storage.html +0 -1
  68. package/docs/modules/utils_stringify.html +0 -1
  69. package/docs/modules.html +0 -1
  70. package/docs/types/sharedQuery.FetchFn.html +0 -1
  71. package/docs/types/sharedQuery.PersistTo.html +0 -1
  72. package/docs/types/sharedQuery.QueryStateValue.html +0 -9
  73. package/docs/types/sharedQuery.SharedQueryOptions.html +0 -1
  74. package/docs/types/sharedQuery.SharedQueryState.html +0 -3
  75. package/docs/types/sharedQuery.UseQueryResult.html +0 -1
  76. package/docs/types/sharedState.SharedStateOptions.html +0 -1
  77. package/docs/types/utils_storage.StorageAdapter.html +0 -5
  78. package/docs/types/utils_storage.StorageOptions.html +0 -5
  79. package/docs/variables/fetcher.fetcher.html +0 -2
  80. package/docs/variables/fetcher.fetcherWithRetries.html +0 -2
  81. package/docs/variables/sharedQuery.SharedQueryDefaults.html +0 -2
  82. package/docs/variables/sharedState.SharedStateDefaults.html +0 -6
  83. package/src/fetcher.ts +0 -14
  84. package/src/index.ts +0 -4
  85. package/src/sharedQuery.ts +0 -507
  86. package/src/sharedState.ts +0 -232
  87. package/src/utils/storage.ts +0 -197
  88. package/src/utils/stringify.ts +0 -49
  89. /package/{fetcher.js → fetcher.cjs} +0 -0
  90. /package/{index.js → index.cjs} +0 -0
  91. /package/{sharedState.js → sharedState.cjs} +0 -0
  92. /package/utils/{storage.js → storage.cjs} +0 -0
  93. /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
- // 100 is not too large, but please tweak accordingly
276
- maxBytes: 1e5
277
- // 100kb before GC
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
- inflightPromises = /* @__PURE__ */ new Map();
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.expiryMs = options?.expiryMs !== void 0 ? options?.expiryMs : SharedQueryDefaults.expiryMs;
293
- this.staleMs = options?.staleMs !== void 0 ? options?.staleMs : SharedQueryDefaults.staleMs;
294
- this.refetchOnStale = Boolean(options?.refetchOnStale);
295
- const persistTo = options.persistTo ?? SharedQueryDefaults.persistTo;
296
- if (persistTo === "localStorage") {
297
- options.localStorageKey = options.queryName;
298
- } else if (persistTo === "indexedDb") {
299
- options.indexedDbKey = options.queryName;
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
- console.log(
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
- console.log(`refetchOnStale for ${queryKey}`);
407
+ this.log(`refetchOnStale for ${queryKey}`);
337
408
  void this.dedupedFetch(queryKey, ...args);
338
409
  }
339
- console.log(`Returning stale data for ${queryKey}`);
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 inflightPromise = this.inflightPromises.get(queryKey);
346
- if (inflightPromise) {
347
- console.log(`Deduplicating inflight fetch for ${queryKey}`);
348
- return inflightPromise;
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
- this.inflightPromises.set(queryKey, promise);
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
- console.log(`Start fetching ${queryKey}`);
427
+ this.log(`Start fetching ${queryKey}`);
356
428
  try {
357
429
  const data = await this.queryFn(...args);
358
- console.log(`Successfully fetched ${queryKey}`);
430
+ this.log(`Successfully fetched ${queryKey}`);
359
431
  this.setData(queryKey, data, Date.now());
360
432
  return data;
361
433
  } catch (e) {
362
- console.error(`Failed to fetch ${queryKey}: ${e}`);
434
+ this.log(`Failed to fetch ${queryKey}: ${e}`);
363
435
  throw e;
364
436
  } finally {
365
- this.inflightPromises.delete(queryKey);
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
- async setData(queryKey, data, dataUpdatedMs = Date.now()) {
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
- /** Do a new fetch even when the data is already cached. */
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(args) {
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.inflightPromises.delete(queryKey);
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
- this.inflightPromises.clear();
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
- console.log(
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
- console.log(
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
- console.log(
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
- console.log(
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 = query.getQueryKey(args);
487
- const execute = useCallback(async () => {
488
- console.log(`Begin executing shared query ${queryKey}`);
489
- setQueryState((prev) => {
490
- const clone = { ...prev };
491
- clone[queryKey] = {
492
- ...clone[queryKey] ?? {},
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
- return clone;
497
- });
498
- try {
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
- clone[queryKey] = {
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
- return clone;
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
- clone[queryKey] = {
617
+ setQueryStateValue((prev) => ({
521
618
  // Keep old data.
522
- ...clone[queryKey] ?? {},
619
+ ...prev ?? {},
523
620
  lastUpdatedMs: now,
524
621
  loading: false,
525
- error,
622
+ error: e,
526
623
  errorUpdatedMs: now
527
- };
528
- return clone;
529
- });
624
+ }));
625
+ }
530
626
  }
531
- }
532
- }, [queryKey]);
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 useMemo(() => {
541
- const state = queryState[queryKey] ?? DEFAULT_QUERY_STATE_ENTRY;
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
- return query.updateFromSource(...args);
650
+ void execute(true);
546
651
  },
547
652
  setData: (data, dataUpdatedMs) => {
548
- query.setData(queryKey, data, dataUpdatedMs ?? Date.now());
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
- query.deleteData(args);
663
+ setQueryState((prev) => {
664
+ const clone = { ...prev };
665
+ delete clone[queryKey];
666
+ return clone;
667
+ });
552
668
  }
553
669
  };
554
- }, [query, queryKey, queryState]);
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