@choksheak/react-utils 0.2.6 → 0.2.7
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/fetcher.cjs +4 -4
- package/fetcher.d.mts +31 -4
- package/fetcher.d.ts +30 -3
- package/fetcher.min.cjs +1 -1
- package/fetcher.min.cjs.map +1 -1
- package/fetcher.min.mjs +1 -1
- package/fetcher.min.mjs.map +1 -1
- package/fetcher.mjs +3 -3
- package/package.json +2 -1
- package/sharedQuery.cjs +45 -16
- package/sharedQuery.d.mts +87 -1
- package/sharedQuery.d.ts +87 -1
- package/sharedQuery.min.cjs +1 -1
- package/sharedQuery.min.cjs.map +1 -1
- package/sharedQuery.min.mjs +1 -1
- package/sharedQuery.min.mjs.map +1 -1
- package/sharedQuery.mjs +45 -16
- package/sharedState.d.mts +57 -5
- package/sharedState.d.ts +57 -5
- package/sharedState.min.cjs.map +1 -1
- package/sharedState.min.mjs.map +1 -1
package/sharedQuery.mjs
CHANGED
|
@@ -300,6 +300,12 @@ var SharedQueryDefaults = {
|
|
|
300
300
|
var SharedQuery = class {
|
|
301
301
|
inflightQueries = /* @__PURE__ */ new Map();
|
|
302
302
|
queryState;
|
|
303
|
+
/**
|
|
304
|
+
* Keep track of the mounted query keys. This handles the case where an
|
|
305
|
+
* earlier-started query returns later, and when it returns, it was already
|
|
306
|
+
* unmounted, thus making it eligible for clean up right after fetch.
|
|
307
|
+
*/
|
|
308
|
+
mountedKeys = /* @__PURE__ */ new Map();
|
|
303
309
|
queryName;
|
|
304
310
|
queryFn;
|
|
305
311
|
expiryMs;
|
|
@@ -486,6 +492,19 @@ var SharedQuery = class {
|
|
|
486
492
|
}
|
|
487
493
|
this.inflightQueries.clear();
|
|
488
494
|
}
|
|
495
|
+
/** Keep track of mounted keys. */
|
|
496
|
+
mount(queryKey) {
|
|
497
|
+
this.mountedKeys.set(queryKey, (this.mountedKeys.get(queryKey) ?? 0) + 1);
|
|
498
|
+
}
|
|
499
|
+
/** Clean up mounted keys. */
|
|
500
|
+
unmount(queryKey) {
|
|
501
|
+
const count = this.mountedKeys.get(queryKey);
|
|
502
|
+
if (count === 1) {
|
|
503
|
+
this.mountedKeys.delete(queryKey);
|
|
504
|
+
} else {
|
|
505
|
+
this.mountedKeys.set(queryKey, (count ?? 0) - 1);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
489
508
|
/**
|
|
490
509
|
* Discard the oldest entries if the size exceeds the limit. The last
|
|
491
510
|
* remaining record cannot be deleted no matter what the limit is.
|
|
@@ -503,21 +522,29 @@ var SharedQuery = class {
|
|
|
503
522
|
let newState = oldState;
|
|
504
523
|
let needUpdate = false;
|
|
505
524
|
if (this.maxSize) {
|
|
506
|
-
|
|
525
|
+
let numToCut = oldSize - this.maxSize;
|
|
507
526
|
if (numToCut > 0) {
|
|
508
527
|
this.log(
|
|
509
528
|
`enforceSizeLimit for ${this.queryName} needed due to size ${oldSize} > ${this.maxSize}`
|
|
510
529
|
);
|
|
511
530
|
newState = { ...newState };
|
|
512
531
|
needUpdate = true;
|
|
513
|
-
Object.entries(newState).sort(
|
|
532
|
+
const entriesByTimeAscending = Object.entries(newState).sort(
|
|
514
533
|
(entry1, entry2) => (
|
|
515
534
|
// Sort descending.
|
|
516
|
-
|
|
535
|
+
entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
|
|
517
536
|
)
|
|
518
|
-
)
|
|
519
|
-
|
|
520
|
-
|
|
537
|
+
);
|
|
538
|
+
for (let i = 0; i < entriesByTimeAscending.length && numToCut > 0; i++) {
|
|
539
|
+
const key = entriesByTimeAscending[i][0];
|
|
540
|
+
if (this.mountedKeys.has(key)) {
|
|
541
|
+
this.log(`Cannot clean up ${key} as it is mounted`);
|
|
542
|
+
} else {
|
|
543
|
+
this.log(`Cleaning up unmounted ${key}`);
|
|
544
|
+
delete newState[key];
|
|
545
|
+
numToCut--;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
521
548
|
}
|
|
522
549
|
}
|
|
523
550
|
if (this.maxBytes && Object.keys(newState).length > 1) {
|
|
@@ -534,16 +561,16 @@ var SharedQuery = class {
|
|
|
534
561
|
const entriesByTimeAscending = Object.entries(newState).sort(
|
|
535
562
|
(entry1, entry2) => entry1[1].lastUpdatedMs - entry2[1].lastUpdatedMs
|
|
536
563
|
);
|
|
537
|
-
for (
|
|
538
|
-
let i = 0;
|
|
539
|
-
// Leave the last record entriesByTimeAscending[len-1] untouched.
|
|
540
|
-
i < entriesByTimeAscending.length - 1 && bytesToCut > 0;
|
|
541
|
-
i++
|
|
542
|
-
) {
|
|
564
|
+
for (let i = 0; i < entriesByTimeAscending.length && bytesToCut > 0; i++) {
|
|
543
565
|
const key = entriesByTimeAscending[i][0];
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
566
|
+
if (this.mountedKeys.has(key)) {
|
|
567
|
+
this.log(`Cannot clean up ${key} as it is mounted`);
|
|
568
|
+
} else {
|
|
569
|
+
this.log(`Cleaning up unmounted ${key}`);
|
|
570
|
+
const thisSize = getByteSize(key) + getByteSize(newState[key]);
|
|
571
|
+
delete newState[key];
|
|
572
|
+
bytesToCut -= thisSize;
|
|
573
|
+
}
|
|
547
574
|
}
|
|
548
575
|
}
|
|
549
576
|
}
|
|
@@ -629,11 +656,13 @@ function useSharedQuery(query, args = []) {
|
|
|
629
656
|
);
|
|
630
657
|
useEffect(() => {
|
|
631
658
|
isMounted.current = true;
|
|
659
|
+
query.mount(queryKey);
|
|
632
660
|
void execute(false);
|
|
633
661
|
return () => {
|
|
634
662
|
isMounted.current = false;
|
|
663
|
+
query.unmount(queryKey);
|
|
635
664
|
};
|
|
636
|
-
}, [execute]);
|
|
665
|
+
}, [execute, query, queryKey]);
|
|
637
666
|
return useMemo2(() => {
|
|
638
667
|
const state = queryStateValue ?? DEFAULT_QUERY_STATE_ENTRY;
|
|
639
668
|
return {
|
package/sharedState.d.mts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { StorageOptions } from './utils/storage.mjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
+
* @packageDocumentation
|
|
5
|
+
*
|
|
4
6
|
* The "shared state" is a React state that shares the same value across the
|
|
5
7
|
* entire app. It is largely similar to the jotai library, but is much smaller
|
|
6
8
|
* and much easier to work with when used for persisting in local storage or
|
|
@@ -10,7 +12,34 @@ import { StorageOptions } from './utils/storage.mjs';
|
|
|
10
12
|
* efficient implementation of shared global React states with seamless support
|
|
11
13
|
* for client persistence.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
15
|
+
* Example:
|
|
16
|
+
* ```
|
|
17
|
+
* import { sharedState, useSharedState } from "@choksheak/react-utils/sharedState";
|
|
18
|
+
*
|
|
19
|
+
* // Create a new shared state in the top level scope.
|
|
20
|
+
* // The first argument is the default value, which is required.
|
|
21
|
+
* // The second argument is an options object, and is optional (defaults
|
|
22
|
+
* // to storing in memory only without any persistence).
|
|
23
|
+
* const usersState = sharedState([], { indexedDbKey: "users" });
|
|
24
|
+
*
|
|
25
|
+
* export const UsersComponent: React.FC = () => {
|
|
26
|
+
* // Get both the value and the setter.
|
|
27
|
+
* const [users, setUsers] = useSharedState(usersState);
|
|
28
|
+
* ...
|
|
29
|
+
* };
|
|
30
|
+
*
|
|
31
|
+
* export const ReadOnlyComponent: React.FC = () => {
|
|
32
|
+
* // Get the value only.
|
|
33
|
+
* const users = useSharedStateValue(usersState);
|
|
34
|
+
* ...
|
|
35
|
+
* };
|
|
36
|
+
*
|
|
37
|
+
* export const WriteOnlyComponent: React.FC = () => {
|
|
38
|
+
* // Get the setter only.
|
|
39
|
+
* const setUsers = useSharedStateSetter(usersState);
|
|
40
|
+
* ...
|
|
41
|
+
* };
|
|
42
|
+
* ```
|
|
14
43
|
*/
|
|
15
44
|
|
|
16
45
|
/************************************************************************/
|
|
@@ -30,9 +59,11 @@ declare const SharedStateDefaults: {
|
|
|
30
59
|
*/
|
|
31
60
|
lazyLoad: boolean;
|
|
32
61
|
};
|
|
62
|
+
/** Options to configure a shared state. */
|
|
33
63
|
type SharedStateOptions<T> = {
|
|
34
64
|
lazyLoad?: boolean;
|
|
35
65
|
} & StorageOptions<T>;
|
|
66
|
+
/** Please use sharedState() instead. */
|
|
36
67
|
declare class SharedState<T> {
|
|
37
68
|
private readonly defaultValue;
|
|
38
69
|
private readonly pubSubKey;
|
|
@@ -49,17 +80,38 @@ declare class SharedState<T> {
|
|
|
49
80
|
/**
|
|
50
81
|
* Create a new shared state object. Put this code at the top level scope:
|
|
51
82
|
*
|
|
52
|
-
*
|
|
83
|
+
* Example:
|
|
84
|
+
* ```
|
|
85
|
+
* const counterState = sharedState<number>(0);
|
|
86
|
+
* ```
|
|
53
87
|
*/
|
|
54
88
|
declare function sharedState<T>(defaultValue: T, options?: SharedStateOptions<T>): SharedState<T>;
|
|
55
89
|
/**
|
|
56
90
|
* React hook for subscribing to a key in the global store.
|
|
57
|
-
*
|
|
91
|
+
*
|
|
92
|
+
* Example:
|
|
93
|
+
* ```
|
|
94
|
+
* const [counter, setCounter] = useSharedState(counterState);
|
|
95
|
+
* ```
|
|
58
96
|
*/
|
|
59
97
|
declare function useSharedState<T>(state: SharedState<T>): [T, (next: T | ((prev: T) => T)) => void];
|
|
60
|
-
/**
|
|
98
|
+
/**
|
|
99
|
+
* Same as useSharedState(), but returns the value only.
|
|
100
|
+
*
|
|
101
|
+
* Example:
|
|
102
|
+
* ```
|
|
103
|
+
* const counter = useSharedStateValue(counterState);
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
61
106
|
declare function useSharedStateValue<T>(state: SharedState<T>): T;
|
|
62
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Same as useSharedState(), but returns the setter only.
|
|
109
|
+
*
|
|
110
|
+
* Example:
|
|
111
|
+
* ```
|
|
112
|
+
* const setCounter = useSharedStateSetter(counterState);
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
63
115
|
declare function useSharedStateSetter<T>(state: SharedState<T>): (next: T | ((prev: T) => T)) => void;
|
|
64
116
|
|
|
65
117
|
export { SharedState, SharedStateDefaults, type SharedStateOptions, sharedState, useSharedState, useSharedStateSetter, useSharedStateValue };
|
package/sharedState.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
2
4
|
* The "shared state" is a React state that shares the same value across the
|
|
3
5
|
* entire app. It is largely similar to the jotai library, but is much smaller
|
|
4
6
|
* and much easier to work with when used for persisting in local storage or
|
|
@@ -8,7 +10,34 @@
|
|
|
8
10
|
* efficient implementation of shared global React states with seamless support
|
|
9
11
|
* for client persistence.
|
|
10
12
|
*
|
|
11
|
-
*
|
|
13
|
+
* Example:
|
|
14
|
+
* ```
|
|
15
|
+
* import { sharedState, useSharedState } from "@choksheak/react-utils/sharedState";
|
|
16
|
+
*
|
|
17
|
+
* // Create a new shared state in the top level scope.
|
|
18
|
+
* // The first argument is the default value, which is required.
|
|
19
|
+
* // The second argument is an options object, and is optional (defaults
|
|
20
|
+
* // to storing in memory only without any persistence).
|
|
21
|
+
* const usersState = sharedState([], { indexedDbKey: "users" });
|
|
22
|
+
*
|
|
23
|
+
* export const UsersComponent: React.FC = () => {
|
|
24
|
+
* // Get both the value and the setter.
|
|
25
|
+
* const [users, setUsers] = useSharedState(usersState);
|
|
26
|
+
* ...
|
|
27
|
+
* };
|
|
28
|
+
*
|
|
29
|
+
* export const ReadOnlyComponent: React.FC = () => {
|
|
30
|
+
* // Get the value only.
|
|
31
|
+
* const users = useSharedStateValue(usersState);
|
|
32
|
+
* ...
|
|
33
|
+
* };
|
|
34
|
+
*
|
|
35
|
+
* export const WriteOnlyComponent: React.FC = () => {
|
|
36
|
+
* // Get the setter only.
|
|
37
|
+
* const setUsers = useSharedStateSetter(usersState);
|
|
38
|
+
* ...
|
|
39
|
+
* };
|
|
40
|
+
* ```
|
|
12
41
|
*/
|
|
13
42
|
import { StorageOptions } from "./utils/storage";
|
|
14
43
|
/************************************************************************/
|
|
@@ -28,9 +57,11 @@ export declare const SharedStateDefaults: {
|
|
|
28
57
|
*/
|
|
29
58
|
lazyLoad: boolean;
|
|
30
59
|
};
|
|
60
|
+
/** Options to configure a shared state. */
|
|
31
61
|
export type SharedStateOptions<T> = {
|
|
32
62
|
lazyLoad?: boolean;
|
|
33
63
|
} & StorageOptions<T>;
|
|
64
|
+
/** Please use sharedState() instead. */
|
|
34
65
|
export declare class SharedState<T> {
|
|
35
66
|
private readonly defaultValue;
|
|
36
67
|
private readonly pubSubKey;
|
|
@@ -47,15 +78,36 @@ export declare class SharedState<T> {
|
|
|
47
78
|
/**
|
|
48
79
|
* Create a new shared state object. Put this code at the top level scope:
|
|
49
80
|
*
|
|
50
|
-
*
|
|
81
|
+
* Example:
|
|
82
|
+
* ```
|
|
83
|
+
* const counterState = sharedState<number>(0);
|
|
84
|
+
* ```
|
|
51
85
|
*/
|
|
52
86
|
export declare function sharedState<T>(defaultValue: T, options?: SharedStateOptions<T>): SharedState<T>;
|
|
53
87
|
/**
|
|
54
88
|
* React hook for subscribing to a key in the global store.
|
|
55
|
-
*
|
|
89
|
+
*
|
|
90
|
+
* Example:
|
|
91
|
+
* ```
|
|
92
|
+
* const [counter, setCounter] = useSharedState(counterState);
|
|
93
|
+
* ```
|
|
56
94
|
*/
|
|
57
95
|
export declare function useSharedState<T>(state: SharedState<T>): [T, (next: T | ((prev: T) => T)) => void];
|
|
58
|
-
/**
|
|
96
|
+
/**
|
|
97
|
+
* Same as useSharedState(), but returns the value only.
|
|
98
|
+
*
|
|
99
|
+
* Example:
|
|
100
|
+
* ```
|
|
101
|
+
* const counter = useSharedStateValue(counterState);
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
59
104
|
export declare function useSharedStateValue<T>(state: SharedState<T>): T;
|
|
60
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Same as useSharedState(), but returns the setter only.
|
|
107
|
+
*
|
|
108
|
+
* Example:
|
|
109
|
+
* ```
|
|
110
|
+
* const setCounter = useSharedStateSetter(counterState);
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
61
113
|
export declare function useSharedStateSetter<T>(state: SharedState<T>): (next: T | ((prev: T) => T)) => void;
|
package/sharedState.min.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["/**\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 * @packageDocumentation\n */\n\nimport { 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/* 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["/**\n * @packageDocumentation\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 * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState([], { indexedDbKey: \"users\" });\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { 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/* 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\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/** Please use sharedState() instead. */\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 * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\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 *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\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/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\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"]}
|
package/sharedState.min.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["/**\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 * @packageDocumentation\n */\n\nimport { 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/* 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"]}
|
|
1
|
+
{"version":3,"sources":["../src/sharedState.ts","../src/utils/storage.ts"],"sourcesContent":["/**\n * @packageDocumentation\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 * Example:\n * ```\n * import { sharedState, useSharedState } from \"@choksheak/react-utils/sharedState\";\n *\n * // Create a new shared state in the top level scope.\n * // The first argument is the default value, which is required.\n * // The second argument is an options object, and is optional (defaults\n * // to storing in memory only without any persistence).\n * const usersState = sharedState([], { indexedDbKey: \"users\" });\n *\n * export const UsersComponent: React.FC = () => {\n * // Get both the value and the setter.\n * const [users, setUsers] = useSharedState(usersState);\n * ...\n * };\n *\n * export const ReadOnlyComponent: React.FC = () => {\n * // Get the value only.\n * const users = useSharedStateValue(usersState);\n * ...\n * };\n *\n * export const WriteOnlyComponent: React.FC = () => {\n * // Get the setter only.\n * const setUsers = useSharedStateSetter(usersState);\n * ...\n * };\n * ```\n */\n\nimport { 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/* 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\n/** Options to configure a shared state. */\nexport type SharedStateOptions<T> = {\n lazyLoad?: boolean;\n} & StorageOptions<T>;\n\nlet stateKey = 0;\n\n/** Please use sharedState() instead. */\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 * Example:\n * ```\n * const counterState = sharedState<number>(0);\n * ```\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 *\n * Example:\n * ```\n * const [counter, setCounter] = useSharedState(counterState);\n * ```\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/**\n * Same as useSharedState(), but returns the value only.\n *\n * Example:\n * ```\n * const counter = useSharedStateValue(counterState);\n * ```\n */\nexport function useSharedStateValue<T>(state: SharedState<T>) {\n return useSharedState(state)[0];\n}\n\n/**\n * Same as useSharedState(), but returns the setter only.\n *\n * Example:\n * ```\n * const setCounter = useSharedStateSetter(counterState);\n * ```\n */\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"]}
|