@choksheak/react-utils 0.2.4 → 0.2.6

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 (48) hide show
  1. package/README.md +2 -0
  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/package.json +32 -7
  7. package/{sharedQuery.js → sharedQuery.cjs} +106 -43
  8. package/sharedQuery.d.mts +52 -5
  9. package/sharedQuery.d.ts +51 -5
  10. package/sharedQuery.min.cjs +2 -0
  11. package/sharedQuery.min.cjs.map +1 -0
  12. package/sharedQuery.min.mjs +2 -0
  13. package/sharedQuery.min.mjs.map +1 -0
  14. package/sharedQuery.mjs +106 -43
  15. package/sharedState.d.mts +3 -0
  16. package/sharedState.d.ts +3 -1
  17. package/sharedState.min.cjs +2 -0
  18. package/sharedState.min.cjs.map +1 -0
  19. package/sharedState.min.mjs +2 -0
  20. package/sharedState.min.mjs.map +1 -0
  21. package/utils/storage.min.cjs +2 -0
  22. package/utils/storage.min.cjs.map +1 -0
  23. package/utils/storage.min.mjs +2 -0
  24. package/utils/storage.min.mjs.map +1 -0
  25. package/utils/stringify.min.cjs +2 -0
  26. package/utils/stringify.min.cjs.map +1 -0
  27. package/utils/stringify.min.mjs +2 -0
  28. package/utils/stringify.min.mjs.map +1 -0
  29. package/utils/useDeepMemo.min.cjs +2 -0
  30. package/utils/useDeepMemo.min.cjs.map +1 -0
  31. package/utils/useDeepMemo.min.mjs +2 -0
  32. package/utils/useDeepMemo.min.mjs.map +1 -0
  33. package/index.d.mts +0 -2
  34. package/index.d.ts +0 -0
  35. package/index.js +0 -4
  36. package/index.mjs +0 -12
  37. package/src/fetcher.ts +0 -14
  38. package/src/index.ts +0 -4
  39. package/src/sharedQuery.ts +0 -588
  40. package/src/sharedState.ts +0 -232
  41. package/src/utils/storage.ts +0 -197
  42. package/src/utils/stringify.ts +0 -49
  43. package/src/utils/useDeepMemo.ts +0 -19
  44. /package/{fetcher.js → fetcher.cjs} +0 -0
  45. /package/{sharedState.js → sharedState.cjs} +0 -0
  46. /package/utils/{storage.js → storage.cjs} +0 -0
  47. /package/utils/{stringify.js → stringify.cjs} +0 -0
  48. /package/utils/{useDeepMemo.js → useDeepMemo.cjs} +0 -0
@@ -1,232 +0,0 @@
1
- import { MS_PER_DAY } from "@choksheak/ts-utils/timeConstants";
2
- import { useSyncExternalStore } from "react";
3
-
4
- import {
5
- getStorageAdapter,
6
- StorageAdapter,
7
- StorageOptions,
8
- } from "./utils/storage";
9
-
10
- /**
11
- * The "shared state" is a React state that shares the same value across the
12
- * entire app. It is largely similar to the jotai library, but is much smaller
13
- * and much easier to work with when used for persisting in local storage or
14
- * indexed db, with an automatic expiration for persisted values.
15
- *
16
- * This library aims to be the shortest, simplest, easiest to use, and most
17
- * efficient implementation of shared global React states with seamless support
18
- * for client persistence.
19
- */
20
-
21
- /************************************************************************/
22
- /* Global config */
23
- /************************************************************************/
24
-
25
- /**
26
- * These values are configurable by the user at any time.
27
- */
28
- export const SharedStateDefaults = {
29
- /**
30
- * Default duration for storage expiration in milliseconds. Initial value
31
- * is 30 days, but can be changed to anything.
32
- */
33
- storeExpiryMs: 30 * MS_PER_DAY,
34
-
35
- /**
36
- * True to load from storage only on mount, false (default) to load in
37
- * the top level scope when the shared state is defined.
38
- */
39
- lazyLoad: false,
40
- };
41
-
42
- /************************************************************************/
43
- /* Shared state implementation */
44
- /************************************************************************/
45
-
46
- type Subscriber<T> = (next: T, prev: T) => void;
47
-
48
- class PubSubStore {
49
- private dataByKey = new Map<string, unknown>();
50
- private subscribersByKey = new Map<string, Set<Subscriber<unknown>>>();
51
-
52
- // This could be undefined if T includes undefined, else it should never
53
- // return undefined because the user has to always specify the default value.
54
- public get<T>(key: string): T {
55
- return this.dataByKey.get(key) as T;
56
- }
57
-
58
- public set<T>(key: string, value: T): void {
59
- const prev = this.dataByKey.get(key) as T;
60
-
61
- // Do nothing if no change.
62
- if (prev === value) return;
63
-
64
- this.dataByKey.set(key, value);
65
- this.notify(key, value, prev);
66
- }
67
-
68
- public delete(key: string): void {
69
- this.dataByKey.delete(key);
70
- }
71
-
72
- public setNoNotify<T>(key: string, value: T): void {
73
- this.dataByKey.set(key, value);
74
- }
75
-
76
- // Subscribe to changes of a specific key
77
- public subscribe<T>(key: string, subscriber: Subscriber<T>): () => void {
78
- let subscribers = this.subscribersByKey.get(key);
79
- if (!subscribers) {
80
- subscribers = new Set();
81
- this.subscribersByKey.set(key, subscribers);
82
- }
83
-
84
- // Type coercion: we store Subscriber<unknown>
85
- subscribers.add(subscriber as Subscriber<unknown>);
86
-
87
- // Return unsubscribe function
88
- return () => {
89
- subscribers.delete(subscriber as Subscriber<unknown>);
90
- if (subscribers.size === 0) this.subscribersByKey.delete(key);
91
- };
92
- }
93
-
94
- private notify<T>(key: string, next: T, prev: T): void {
95
- const subscribers = this.subscribersByKey.get(key);
96
- if (!subscribers) return;
97
-
98
- // Make copy to protect against mutation during iteration.
99
- const copy = Array.from(subscribers.values());
100
- for (const subscriber of copy) {
101
- try {
102
- subscriber(next, prev);
103
- } catch (e) {
104
- console.error(`Error invoking subscriber:`, e);
105
- }
106
- }
107
- }
108
- }
109
-
110
- const pubSubStore = new PubSubStore();
111
-
112
- const STORAGE_KEY = "state";
113
-
114
- export type SharedStateOptions<T> = {
115
- lazyLoad?: boolean;
116
- } & StorageOptions<T>;
117
-
118
- let stateKey = 0;
119
-
120
- export class SharedState<T> {
121
- private readonly pubSubKey: string;
122
- private readonly storageAdapter: StorageAdapter<T> | null;
123
- public readonly setValueBounded: (next: T | ((prev: T) => T)) => void;
124
- public initDone = false;
125
-
126
- public constructor(
127
- private readonly defaultValue: T,
128
- options?: SharedStateOptions<T>,
129
- ) {
130
- this.pubSubKey = String(stateKey++);
131
-
132
- // Use max of 1 storage adapter per shared state.
133
- this.storageAdapter = getStorageAdapter(
134
- options,
135
- SharedStateDefaults.storeExpiryMs,
136
- );
137
-
138
- // Same as this.setValue, but binding the `this` reference.
139
- this.setValueBounded = (arg) => this.setValue(arg);
140
-
141
- // Always set the default value first to avoid returning undefineds if that
142
- // is not part of T.
143
- pubSubStore.setNoNotify(this.pubSubKey, defaultValue);
144
-
145
- const lazyLoad = options?.lazyLoad;
146
- const lazy =
147
- lazyLoad !== undefined ? lazyLoad : SharedStateDefaults.lazyLoad;
148
-
149
- if (!lazy) {
150
- void this.initDefaultValueOnce();
151
- }
152
- }
153
-
154
- // `subscribe` function required by useSyncExternalStore
155
- public subscribe(subscriber: () => void): () => void {
156
- return pubSubStore.subscribe(this.pubSubKey, () => subscriber());
157
- }
158
-
159
- // Get the value of the shared state.
160
- // `getSnapshot` function — must return same ref if value unchanged
161
- public getSnapshot(): T {
162
- return pubSubStore.get(this.pubSubKey);
163
- }
164
-
165
- // Set the value of the shared state.
166
- public setValue(next: T | ((prev: T) => T)): void {
167
- if (typeof next === "function") {
168
- const prev = pubSubStore.get<T>(this.pubSubKey);
169
- next = (next as (p: T) => T)(prev);
170
- }
171
-
172
- pubSubStore.set(this.pubSubKey, next);
173
- void this.storageAdapter?.save(STORAGE_KEY, next);
174
- }
175
-
176
- // Remove the value of the shared state. Actually this just sets the value
177
- // back to the given default value.
178
- public delete() {
179
- pubSubStore.set(this.pubSubKey, this.defaultValue);
180
- void this.storageAdapter?.save(STORAGE_KEY, this.defaultValue);
181
- }
182
-
183
- // Initial default handling
184
- public async initDefaultValueOnce() {
185
- if (this.initDone) return;
186
- this.initDone = true;
187
-
188
- const v = await this.storageAdapter?.load(STORAGE_KEY);
189
- pubSubStore.set(this.pubSubKey, v !== undefined ? v : this.defaultValue);
190
- }
191
- }
192
-
193
- /**
194
- * Create a new shared state object. Put this code at the top level scope:
195
- *
196
- * const myState = sharedState<number>(0);
197
- */
198
- export function sharedState<T>(
199
- defaultValue: T,
200
- options?: SharedStateOptions<T>,
201
- ): SharedState<T> {
202
- return new SharedState(defaultValue, options);
203
- }
204
-
205
- /**
206
- * React hook for subscribing to a key in the global store.
207
- * const [my, setMy] = useSharedState(myState);
208
- */
209
- export function useSharedState<T>(
210
- state: SharedState<T>,
211
- ): [T, (next: T | ((prev: T) => T)) => void] {
212
- state.initDefaultValueOnce();
213
-
214
- const value = useSyncExternalStore(
215
- (onStoreChange) => state.subscribe(onStoreChange),
216
- () => state.getSnapshot(),
217
- // getServerSnapshot is needed to prevent SSR dev errors in nextjs.
218
- () => state.getSnapshot(),
219
- );
220
-
221
- return [value, state.setValueBounded];
222
- }
223
-
224
- /** E.g. const my = useSharedStateValue(myState) */
225
- export function useSharedStateValue<T>(state: SharedState<T>) {
226
- return useSharedState(state)[0];
227
- }
228
-
229
- /** E.g. const setMy = useSharedStateSetter(myState) */
230
- export function useSharedStateSetter<T>(state: SharedState<T>) {
231
- return useSharedState(state)[1];
232
- }
@@ -1,197 +0,0 @@
1
- import { kvStore } from "@choksheak/ts-utils/kvStore";
2
-
3
- export type StorageAdapter<T> = {
4
- save: (key: string, value: T) => Promise<void> | void;
5
- load: (key: string) => Promise<T | undefined> | T | undefined;
6
- delete: (key: string) => Promise<void> | void;
7
- clear: () => Promise<void> | void;
8
- };
9
-
10
- export type StorageOptions<T> = {
11
- // If specified, this will be used and all other fields will be ignored.
12
- store?: StorageAdapter<T>;
13
-
14
- // Storage key in local storage for this data.
15
- localStorageKey?: string;
16
-
17
- // Storage key in indexed db for this data.
18
- indexedDbKey?: string;
19
-
20
- // How long to wait before the value is considered expired.
21
- storeExpiryMs?: number;
22
- };
23
-
24
- export function getStorageAdapter<T>(
25
- options: StorageOptions<T> | undefined,
26
- defaultStoreExpiryMs: number,
27
- ): StorageAdapter<T> | null {
28
- const { localStorageKey, indexedDbKey, storeExpiryMs, store } = options ?? {};
29
-
30
- if (store) {
31
- return store;
32
- }
33
-
34
- const expiryMs = storeExpiryMs || defaultStoreExpiryMs;
35
-
36
- if (localStorageKey) {
37
- return new LocalStorageAdapter<T>(localStorageKey, expiryMs);
38
- }
39
-
40
- if (indexedDbKey) {
41
- return new IndexedDbAdapter<T>(indexedDbKey, expiryMs);
42
- }
43
-
44
- return null;
45
- }
46
-
47
- /************************************************************************/
48
- /* Storage adapters */
49
- /************************************************************************/
50
-
51
- type ValueWithExpiration<T> = { value: T; expiresAt: number };
52
-
53
- /** Save state to memory only - not persisted. */
54
- export class InMemoryStorageAdapter<T> implements StorageAdapter<T> {
55
- private readonly cache = new Map<string, ValueWithExpiration<T>>();
56
-
57
- public constructor(
58
- public readonly keyPrefix: string,
59
- public readonly expiryMs: number,
60
- ) {}
61
-
62
- public save(key: string, value: T): void {
63
- const fullKey = this.keyPrefix + ":" + key;
64
- this.cache.set(fullKey, {
65
- value,
66
- expiresAt: Date.now() + this.expiryMs,
67
- });
68
- }
69
-
70
- public load(key: string): T | undefined {
71
- const fullKey = this.keyPrefix + ":" + key;
72
- const cached = this.cache.get(fullKey);
73
-
74
- if (cached && Date.now() < cached.expiresAt) {
75
- return cached.value;
76
- }
77
-
78
- return undefined;
79
- }
80
-
81
- public delete(key: string): void {
82
- const fullKey = this.keyPrefix + ":" + key;
83
- this.cache.delete(fullKey);
84
- }
85
-
86
- public clear(): void {
87
- this.cache.clear();
88
- }
89
- }
90
-
91
- /** Save state to local storage. */
92
- export class LocalStorageAdapter<T> implements StorageAdapter<T> {
93
- public constructor(
94
- public readonly keyPrefix: string,
95
- public readonly expiryMs: number,
96
- ) {}
97
-
98
- public save(key: string, value: T): void {
99
- if (typeof window === "undefined") return;
100
-
101
- const fullKey = this.keyPrefix + ":" + key;
102
- window.localStorage.setItem(
103
- fullKey,
104
- JSON.stringify({
105
- value,
106
- expiresAt: this.expiryMs ? Date.now() + this.expiryMs : 0,
107
- } satisfies ValueWithExpiration<T>),
108
- );
109
- }
110
-
111
- public load(key: string): T | undefined {
112
- if (typeof window === "undefined") return undefined;
113
-
114
- const fullKey = this.keyPrefix + ":" + key;
115
- const raw = window.localStorage.getItem(fullKey);
116
- if (!raw) return undefined;
117
-
118
- try {
119
- const box = JSON.parse(raw) as ValueWithExpiration<T>;
120
- if (!box || (box.expiresAt && Date.now() >= box.expiresAt)) {
121
- window.localStorage.removeItem(fullKey);
122
- return undefined;
123
- }
124
-
125
- return box.value;
126
- } catch {
127
- // Ignore error.
128
- window.localStorage.removeItem(fullKey);
129
- return undefined;
130
- }
131
- }
132
-
133
- public delete(key: string): void {
134
- if (typeof window === "undefined") return;
135
-
136
- const fullKey = this.keyPrefix + ":" + key;
137
- window.localStorage.removeItem(fullKey);
138
- }
139
-
140
- public clear(): void {
141
- if (typeof window === "undefined") return;
142
-
143
- for (const key of Object.keys(window.localStorage)) {
144
- if (key.startsWith(this.keyPrefix)) {
145
- window.localStorage.removeItem(key);
146
- }
147
- }
148
- }
149
- }
150
-
151
- /** Save state to indexed db. */
152
- export class IndexedDbAdapter<T> implements StorageAdapter<T> {
153
- public constructor(
154
- public readonly keyPrefix: string,
155
- public readonly expiryMs: number,
156
- ) {}
157
-
158
- public async save(key: string, value: T): Promise<void> {
159
- if (typeof window === "undefined") return;
160
-
161
- const fullKey = this.keyPrefix + ":" + key;
162
- await kvStore.set(fullKey, JSON.stringify(value), this.expiryMs);
163
- }
164
-
165
- public async load(key: string): Promise<T | undefined> {
166
- if (typeof window === "undefined") return undefined;
167
-
168
- const fullKey = this.keyPrefix + ":" + key;
169
- const raw = await kvStore.get<string>(fullKey);
170
- if (raw === undefined) return undefined;
171
-
172
- try {
173
- return JSON.parse(raw);
174
- } catch {
175
- // Ignore error.
176
- await kvStore.delete(fullKey);
177
- return undefined;
178
- }
179
- }
180
-
181
- public async delete(key: string): Promise<void> {
182
- if (typeof window === "undefined") return;
183
-
184
- const fullKey = this.keyPrefix + ":" + key;
185
- await kvStore.delete(fullKey);
186
- }
187
-
188
- public async clear(): Promise<void> {
189
- if (typeof window === "undefined") return;
190
-
191
- await kvStore.forEach(async (k) => {
192
- if (k.startsWith(this.keyPrefix)) {
193
- await kvStore.delete(k);
194
- }
195
- });
196
- }
197
- }
@@ -1,49 +0,0 @@
1
- /**
2
- * JSON stringify a value of any type, and returns an unsafe string. The string
3
- * is "unsafe" because you might not be able to restore the original object
4
- * using the serialized JSON. This is specifically used for query key generation
5
- * and is not suitable for more general uses.
6
- *
7
- * Note: The given value should not contain any functions because functions
8
- * cannot be serialized and deserialized properly.
9
- */
10
- export function stringifyDeterministicForKeys(params: unknown): string {
11
- const cache = new Set();
12
-
13
- return JSON.stringify(params, (_, value) => {
14
- if (typeof value === "function") {
15
- throw new Error(`Functions should not be passed into query parameters`);
16
- }
17
-
18
- if (!value || typeof value !== "object" || Array.isArray(value)) {
19
- return value;
20
- }
21
-
22
- if (cache.has(value)) {
23
- // Circular reference detected! Return a placeholder string.
24
- return "[Circular]";
25
- }
26
-
27
- cache.add(value);
28
-
29
- // Don't crash on circular references, but don't mark the top level object
30
- // as circular because we want to return as much string as possible.
31
- if (value !== params) {
32
- try {
33
- JSON.stringify(value);
34
- } catch {
35
- return "[Unserializable]";
36
- }
37
- }
38
-
39
- return Object.keys(value)
40
- .sort()
41
- .reduce(
42
- (result: Record<string, unknown>, key: string) => {
43
- result[key] = value[key];
44
- return result;
45
- },
46
- {} as Record<string, unknown>,
47
- );
48
- });
49
- }
@@ -1,19 +0,0 @@
1
- import isEqual from "lodash/isEqual";
2
- import { useMemo, useState } from "react";
3
-
4
- /** Provides a stable value for T based on deep equality. */
5
- export function useDeepMemo<T>(value: T): T {
6
- const [state, setState] = useState<T>(value);
7
-
8
- return useMemo(() => {
9
- if (state === value || isEqual(state, value)) {
10
- return state;
11
- }
12
-
13
- // We check equality, so this should be safe.
14
- // eslint-disable-next-line react-hooks/set-state-in-render
15
- setState(value);
16
-
17
- return value;
18
- }, [state, value]);
19
- }
File without changes
File without changes
File without changes
File without changes
File without changes