use-storage-persisted-state 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -3
- package/dist/index.d.mts +16 -5
- package/dist/index.d.ts +16 -5
- package/dist/index.js +49 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +47 -6
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# use-storage-persisted-state
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/use-storage-persisted-state)
|
|
4
|
+
[](https://www.npmjs.com/package/use-storage-persisted-state)
|
|
5
|
+
[](LICENSE)
|
|
6
|
+
[](https://bundlephobia.com/package/use-storage-persisted-state)
|
|
7
|
+
[](https://bundlephobia.com/package/use-storage-persisted-state)
|
|
8
|
+
|
|
3
9
|
A robust, type-safe React hook for persisting state backed by `localStorage`, `sessionStorage`, or memory.
|
|
4
10
|
|
|
5
11
|
`useStoragePersistedState` works like `useState`, but it automatically persists your state to the browser and keeps it synchronized across all components, tabs, and even direct localStorage changes, or manual changes in DevTools.
|
|
@@ -139,6 +145,10 @@ const user = readStoragePersistedState<{ name: string } | null>(
|
|
|
139
145
|
setStoragePersistedState("user_profile", { name: "Alice" });
|
|
140
146
|
```
|
|
141
147
|
|
|
148
|
+
### More examples
|
|
149
|
+
|
|
150
|
+
More type-checked usage examples live in `examples/`-folder.
|
|
151
|
+
|
|
142
152
|
## Advanced usage
|
|
143
153
|
|
|
144
154
|
### Data schema migration with custom codec
|
|
@@ -245,6 +255,7 @@ Follow this checklist to publish a new version.
|
|
|
245
255
|
### Release checklist
|
|
246
256
|
|
|
247
257
|
1. Bump the version: `npm version patch|minor|major` (this creates a git tag).
|
|
248
|
-
2.
|
|
249
|
-
3.
|
|
250
|
-
4.
|
|
258
|
+
2. Push the changes and tag: `git push && git push --tags`
|
|
259
|
+
3. Run release checks and build the package:`npm run prepublishOnly`
|
|
260
|
+
4. Verify the tarball contents: `npm pack --dry-run`
|
|
261
|
+
5. Publish: `npm publish` (might need `npm login` first).
|
package/dist/index.d.mts
CHANGED
|
@@ -18,6 +18,7 @@ interface Codec<T> {
|
|
|
18
18
|
* { codec: JsonCodec }
|
|
19
19
|
* );
|
|
20
20
|
* ```
|
|
21
|
+
* any type is needed to allow objects and arrays of any shape.
|
|
21
22
|
*/
|
|
22
23
|
declare const JsonCodec: Codec<any>;
|
|
23
24
|
/**
|
|
@@ -28,8 +29,10 @@ declare const JsonCodec: Codec<any>;
|
|
|
28
29
|
* const [name, setName] = useStoragePersistedState("name", "Guest");
|
|
29
30
|
* // Automatically uses StringCodec because defaultValue is a string
|
|
30
31
|
* ```
|
|
32
|
+
*
|
|
33
|
+
* any type is needed to allow string enums.
|
|
31
34
|
*/
|
|
32
|
-
declare const StringCodec: Codec<
|
|
35
|
+
declare const StringCodec: Codec<any>;
|
|
33
36
|
/**
|
|
34
37
|
* Codec for boolean values. Stores as "true" or "false" strings.
|
|
35
38
|
*
|
|
@@ -49,8 +52,10 @@ declare const BooleanCodec: Codec<boolean>;
|
|
|
49
52
|
* const [count, setCount] = useStoragePersistedState("count", 0);
|
|
50
53
|
* // Automatically uses NumberCodec because defaultValue is a number
|
|
51
54
|
* ```
|
|
55
|
+
*
|
|
56
|
+
* any type is needed to allow number enums.
|
|
52
57
|
*/
|
|
53
|
-
declare const NumberCodec: Codec<
|
|
58
|
+
declare const NumberCodec: Codec<any>;
|
|
54
59
|
/**
|
|
55
60
|
* Infers the appropriate codec based on the default value's type.
|
|
56
61
|
* Used internally when the user doesn't provide an explicit codec.
|
|
@@ -100,13 +105,13 @@ declare function useStoragePersistedState<T>(key: string, defaultValue: null | u
|
|
|
100
105
|
*
|
|
101
106
|
* Returns the provided defaultValue when the key is missing or parsing fails.
|
|
102
107
|
*/
|
|
103
|
-
declare function
|
|
108
|
+
declare function getStoragePersistedState<T>(key: string, defaultValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): T;
|
|
104
109
|
/**
|
|
105
110
|
* Read a persisted value from storage using an explicit codec.
|
|
106
111
|
*
|
|
107
112
|
* Use this overload when defaultValue is null or undefined.
|
|
108
113
|
*/
|
|
109
|
-
declare function
|
|
114
|
+
declare function getStoragePersistedState<T>(key: string, defaultValue: null | undefined, options: StoragePersistedStateOptions<T> & {
|
|
110
115
|
codec: Codec<T>;
|
|
111
116
|
}): T | null;
|
|
112
117
|
/**
|
|
@@ -123,5 +128,11 @@ declare function setStoragePersistedState<T>(key: string, newValue: Exclude<T, n
|
|
|
123
128
|
declare function setStoragePersistedState<T>(key: string, newValue: T | ((prev: T | null) => T), options: StoragePersistedStateOptions<T> & {
|
|
124
129
|
codec: Codec<T>;
|
|
125
130
|
}): void;
|
|
131
|
+
/**
|
|
132
|
+
* Remove a persisted value from storage and notify active hooks for the same key.
|
|
133
|
+
*
|
|
134
|
+
* Defaults to localStorage when no storageType is provided.
|
|
135
|
+
*/
|
|
136
|
+
declare function removeStoragePersistedState(key: string, storageType?: StorageType): void;
|
|
126
137
|
|
|
127
|
-
export { BooleanCodec, type Codec, JsonCodec, NumberCodec, type StoragePersistedStateOptions, type StorageType, StringCodec, inferCodec, readStoragePersistedState, setStoragePersistedState, useStoragePersistedState };
|
|
138
|
+
export { BooleanCodec, type Codec, JsonCodec, NumberCodec, type StoragePersistedStateOptions, type StorageType, StringCodec, getStoragePersistedState, inferCodec, getStoragePersistedState as readStoragePersistedState, removeStoragePersistedState, setStoragePersistedState, useStoragePersistedState };
|
package/dist/index.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ interface Codec<T> {
|
|
|
18
18
|
* { codec: JsonCodec }
|
|
19
19
|
* );
|
|
20
20
|
* ```
|
|
21
|
+
* any type is needed to allow objects and arrays of any shape.
|
|
21
22
|
*/
|
|
22
23
|
declare const JsonCodec: Codec<any>;
|
|
23
24
|
/**
|
|
@@ -28,8 +29,10 @@ declare const JsonCodec: Codec<any>;
|
|
|
28
29
|
* const [name, setName] = useStoragePersistedState("name", "Guest");
|
|
29
30
|
* // Automatically uses StringCodec because defaultValue is a string
|
|
30
31
|
* ```
|
|
32
|
+
*
|
|
33
|
+
* any type is needed to allow string enums.
|
|
31
34
|
*/
|
|
32
|
-
declare const StringCodec: Codec<
|
|
35
|
+
declare const StringCodec: Codec<any>;
|
|
33
36
|
/**
|
|
34
37
|
* Codec for boolean values. Stores as "true" or "false" strings.
|
|
35
38
|
*
|
|
@@ -49,8 +52,10 @@ declare const BooleanCodec: Codec<boolean>;
|
|
|
49
52
|
* const [count, setCount] = useStoragePersistedState("count", 0);
|
|
50
53
|
* // Automatically uses NumberCodec because defaultValue is a number
|
|
51
54
|
* ```
|
|
55
|
+
*
|
|
56
|
+
* any type is needed to allow number enums.
|
|
52
57
|
*/
|
|
53
|
-
declare const NumberCodec: Codec<
|
|
58
|
+
declare const NumberCodec: Codec<any>;
|
|
54
59
|
/**
|
|
55
60
|
* Infers the appropriate codec based on the default value's type.
|
|
56
61
|
* Used internally when the user doesn't provide an explicit codec.
|
|
@@ -100,13 +105,13 @@ declare function useStoragePersistedState<T>(key: string, defaultValue: null | u
|
|
|
100
105
|
*
|
|
101
106
|
* Returns the provided defaultValue when the key is missing or parsing fails.
|
|
102
107
|
*/
|
|
103
|
-
declare function
|
|
108
|
+
declare function getStoragePersistedState<T>(key: string, defaultValue: Exclude<T, null | undefined>, options?: StoragePersistedStateOptions<T>): T;
|
|
104
109
|
/**
|
|
105
110
|
* Read a persisted value from storage using an explicit codec.
|
|
106
111
|
*
|
|
107
112
|
* Use this overload when defaultValue is null or undefined.
|
|
108
113
|
*/
|
|
109
|
-
declare function
|
|
114
|
+
declare function getStoragePersistedState<T>(key: string, defaultValue: null | undefined, options: StoragePersistedStateOptions<T> & {
|
|
110
115
|
codec: Codec<T>;
|
|
111
116
|
}): T | null;
|
|
112
117
|
/**
|
|
@@ -123,5 +128,11 @@ declare function setStoragePersistedState<T>(key: string, newValue: Exclude<T, n
|
|
|
123
128
|
declare function setStoragePersistedState<T>(key: string, newValue: T | ((prev: T | null) => T), options: StoragePersistedStateOptions<T> & {
|
|
124
129
|
codec: Codec<T>;
|
|
125
130
|
}): void;
|
|
131
|
+
/**
|
|
132
|
+
* Remove a persisted value from storage and notify active hooks for the same key.
|
|
133
|
+
*
|
|
134
|
+
* Defaults to localStorage when no storageType is provided.
|
|
135
|
+
*/
|
|
136
|
+
declare function removeStoragePersistedState(key: string, storageType?: StorageType): void;
|
|
126
137
|
|
|
127
|
-
export { BooleanCodec, type Codec, JsonCodec, NumberCodec, type StoragePersistedStateOptions, type StorageType, StringCodec, inferCodec, readStoragePersistedState, setStoragePersistedState, useStoragePersistedState };
|
|
138
|
+
export { BooleanCodec, type Codec, JsonCodec, NumberCodec, type StoragePersistedStateOptions, type StorageType, StringCodec, getStoragePersistedState, inferCodec, getStoragePersistedState as readStoragePersistedState, removeStoragePersistedState, setStoragePersistedState, useStoragePersistedState };
|
package/dist/index.js
CHANGED
|
@@ -26,8 +26,10 @@ __export(index_exports, {
|
|
|
26
26
|
JsonCodec: () => JsonCodec,
|
|
27
27
|
NumberCodec: () => NumberCodec,
|
|
28
28
|
StringCodec: () => StringCodec,
|
|
29
|
+
getStoragePersistedState: () => getStoragePersistedState,
|
|
29
30
|
inferCodec: () => inferCodec,
|
|
30
|
-
readStoragePersistedState: () =>
|
|
31
|
+
readStoragePersistedState: () => getStoragePersistedState,
|
|
32
|
+
removeStoragePersistedState: () => removeStoragePersistedState,
|
|
31
33
|
setStoragePersistedState: () => setStoragePersistedState,
|
|
32
34
|
useStoragePersistedState: () => useStoragePersistedState
|
|
33
35
|
});
|
|
@@ -54,8 +56,22 @@ var JsonCodec = {
|
|
|
54
56
|
}
|
|
55
57
|
};
|
|
56
58
|
var StringCodec = {
|
|
57
|
-
encode: (value) =>
|
|
58
|
-
|
|
59
|
+
encode: (value) => {
|
|
60
|
+
if (value == null) return null;
|
|
61
|
+
if (typeof value === "string") return value;
|
|
62
|
+
console.error(
|
|
63
|
+
`StringCodec encode expected a string but got ${typeof value}.`
|
|
64
|
+
);
|
|
65
|
+
return String(value);
|
|
66
|
+
},
|
|
67
|
+
decode: (value) => {
|
|
68
|
+
if (value == null) return null;
|
|
69
|
+
if (typeof value === "string") return value;
|
|
70
|
+
console.error(
|
|
71
|
+
`StringCodec decode expected a string but got ${typeof value}.`
|
|
72
|
+
);
|
|
73
|
+
return String(value);
|
|
74
|
+
}
|
|
59
75
|
};
|
|
60
76
|
var BooleanCodec = {
|
|
61
77
|
encode: (value) => value ? "true" : "false",
|
|
@@ -64,15 +80,30 @@ var BooleanCodec = {
|
|
|
64
80
|
var NumberCodec = {
|
|
65
81
|
encode: (value) => {
|
|
66
82
|
if (value === null) return null;
|
|
83
|
+
if (typeof value !== "number") {
|
|
84
|
+
console.error(
|
|
85
|
+
`NumberCodec encode expected a number but got ${typeof value}.`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
67
88
|
return String(value);
|
|
68
89
|
},
|
|
69
90
|
decode: (value) => {
|
|
70
|
-
if (value === null
|
|
91
|
+
if (value === null) return null;
|
|
71
92
|
if (value === "NaN") return NaN;
|
|
72
93
|
if (value === "Infinity") return Infinity;
|
|
73
94
|
if (value === "-Infinity") return -Infinity;
|
|
95
|
+
if (value === "") {
|
|
96
|
+
console.warn("NumberCodec decode received an empty string.");
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
74
99
|
const parsed = Number(value);
|
|
75
|
-
|
|
100
|
+
if (Number.isNaN(parsed)) {
|
|
101
|
+
console.warn(
|
|
102
|
+
`NumberCodec decode received an invalid number string "${value}".`
|
|
103
|
+
);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
return parsed;
|
|
76
107
|
}
|
|
77
108
|
};
|
|
78
109
|
function inferCodec(defaultValue) {
|
|
@@ -416,7 +447,7 @@ function resolveCodec(key, valueHint, options) {
|
|
|
416
447
|
}
|
|
417
448
|
return inferCodec(valueHint);
|
|
418
449
|
}
|
|
419
|
-
function
|
|
450
|
+
function getStoragePersistedState(key, defaultValue, options = {}) {
|
|
420
451
|
const syncManager = getSyncManager(options.storageType);
|
|
421
452
|
const adapter = syncManager.storage;
|
|
422
453
|
const codec = resolveCodec(key, defaultValue, options);
|
|
@@ -464,14 +495,26 @@ function setStoragePersistedState(key, newValueOrFn, options = {}) {
|
|
|
464
495
|
console.error(`Error setting storage key "${key}":`, error);
|
|
465
496
|
}
|
|
466
497
|
}
|
|
498
|
+
function removeStoragePersistedState(key, storageType) {
|
|
499
|
+
const syncManager = getSyncManager(storageType);
|
|
500
|
+
const adapter = syncManager.storage;
|
|
501
|
+
try {
|
|
502
|
+
adapter.removeItem(key);
|
|
503
|
+
syncManager.notify(key);
|
|
504
|
+
} catch (error) {
|
|
505
|
+
console.error(`Error removing storage key "${key}":`, error);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
467
508
|
// Annotate the CommonJS export names for ESM import in node:
|
|
468
509
|
0 && (module.exports = {
|
|
469
510
|
BooleanCodec,
|
|
470
511
|
JsonCodec,
|
|
471
512
|
NumberCodec,
|
|
472
513
|
StringCodec,
|
|
514
|
+
getStoragePersistedState,
|
|
473
515
|
inferCodec,
|
|
474
516
|
readStoragePersistedState,
|
|
517
|
+
removeStoragePersistedState,
|
|
475
518
|
setStoragePersistedState,
|
|
476
519
|
useStoragePersistedState
|
|
477
520
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["export { useStoragePersistedState } from \"./useStoragePersistedState\";\nexport type {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\nexport type { Codec } from \"./codecs\";\nexport {\n JsonCodec,\n StringCodec,\n BooleanCodec,\n NumberCodec,\n inferCodec,\n} from \"./codecs\";\nexport {\n readStoragePersistedState,\n setStoragePersistedState,\n} from \"./storagePersistedState\";\n","\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const JsonCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n */\nexport const StringCodec: Codec<string | null> = {\n encode: (value) => value ?? null,\n decode: (value) => value ?? null,\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n */\nexport const NumberCodec: Codec<number | null> = {\n encode: (value) => {\n if (value === null) return null;\n return String(value);\n },\n decode: (value) => {\n if (value === null || value === \"\") return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n const parsed = Number(value);\n return isNaN(parsed) ? null : parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["export { useStoragePersistedState } from \"./useStoragePersistedState\";\nexport type {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\nexport type { Codec } from \"./codecs\";\nexport {\n JsonCodec,\n StringCodec,\n BooleanCodec,\n NumberCodec,\n inferCodec,\n} from \"./codecs\";\nexport {\n getStoragePersistedState,\n getStoragePersistedState as readStoragePersistedState,\n setStoragePersistedState,\n removeStoragePersistedState,\n} from \"./storagePersistedState\";\n","\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n * any type is needed to allow objects and arrays of any shape.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const JsonCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n *\n * any type is needed to allow string enums.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const StringCodec: Codec<any> = {\n encode: (value) => {\n if (value == null) return null;\n if (typeof value === \"string\") return value;\n console.error(\n `StringCodec encode expected a string but got ${typeof value}.`,\n );\n return String(value);\n },\n decode: (value) => {\n if (value == null) return null;\n if (typeof value === \"string\") return value;\n console.error(\n `StringCodec decode expected a string but got ${typeof value}.`,\n );\n return String(value);\n },\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n *\n * any type is needed to allow number enums.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const NumberCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n if (typeof value !== \"number\") {\n console.error(\n `NumberCodec encode expected a number but got ${typeof value}.`,\n );\n }\n return String(value);\n },\n decode: (value) => {\n if (value === null) return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n if (value === \"\") {\n console.warn(\"NumberCodec decode received an empty string.\");\n return null;\n }\n const parsed = Number(value);\n if (Number.isNaN(parsed)) {\n console.warn(\n `NumberCodec decode received an invalid number string \"${value}\".`,\n );\n return null;\n }\n return parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function getStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function getStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function getStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n\n/**\n * Remove a persisted value from storage and notify active hooks for the same key.\n *\n * Defaults to localStorage when no storageType is provided.\n */\nexport function removeStoragePersistedState(\n key: string,\n storageType?: StorageType,\n) {\n const syncManager = getSyncManager(storageType);\n const adapter = syncManager.storage;\n\n try {\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -23,8 +23,22 @@ var JsonCodec = {
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
var StringCodec = {
|
|
26
|
-
encode: (value) =>
|
|
27
|
-
|
|
26
|
+
encode: (value) => {
|
|
27
|
+
if (value == null) return null;
|
|
28
|
+
if (typeof value === "string") return value;
|
|
29
|
+
console.error(
|
|
30
|
+
`StringCodec encode expected a string but got ${typeof value}.`
|
|
31
|
+
);
|
|
32
|
+
return String(value);
|
|
33
|
+
},
|
|
34
|
+
decode: (value) => {
|
|
35
|
+
if (value == null) return null;
|
|
36
|
+
if (typeof value === "string") return value;
|
|
37
|
+
console.error(
|
|
38
|
+
`StringCodec decode expected a string but got ${typeof value}.`
|
|
39
|
+
);
|
|
40
|
+
return String(value);
|
|
41
|
+
}
|
|
28
42
|
};
|
|
29
43
|
var BooleanCodec = {
|
|
30
44
|
encode: (value) => value ? "true" : "false",
|
|
@@ -33,15 +47,30 @@ var BooleanCodec = {
|
|
|
33
47
|
var NumberCodec = {
|
|
34
48
|
encode: (value) => {
|
|
35
49
|
if (value === null) return null;
|
|
50
|
+
if (typeof value !== "number") {
|
|
51
|
+
console.error(
|
|
52
|
+
`NumberCodec encode expected a number but got ${typeof value}.`
|
|
53
|
+
);
|
|
54
|
+
}
|
|
36
55
|
return String(value);
|
|
37
56
|
},
|
|
38
57
|
decode: (value) => {
|
|
39
|
-
if (value === null
|
|
58
|
+
if (value === null) return null;
|
|
40
59
|
if (value === "NaN") return NaN;
|
|
41
60
|
if (value === "Infinity") return Infinity;
|
|
42
61
|
if (value === "-Infinity") return -Infinity;
|
|
62
|
+
if (value === "") {
|
|
63
|
+
console.warn("NumberCodec decode received an empty string.");
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
43
66
|
const parsed = Number(value);
|
|
44
|
-
|
|
67
|
+
if (Number.isNaN(parsed)) {
|
|
68
|
+
console.warn(
|
|
69
|
+
`NumberCodec decode received an invalid number string "${value}".`
|
|
70
|
+
);
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
return parsed;
|
|
45
74
|
}
|
|
46
75
|
};
|
|
47
76
|
function inferCodec(defaultValue) {
|
|
@@ -385,7 +414,7 @@ function resolveCodec(key, valueHint, options) {
|
|
|
385
414
|
}
|
|
386
415
|
return inferCodec(valueHint);
|
|
387
416
|
}
|
|
388
|
-
function
|
|
417
|
+
function getStoragePersistedState(key, defaultValue, options = {}) {
|
|
389
418
|
const syncManager = getSyncManager(options.storageType);
|
|
390
419
|
const adapter = syncManager.storage;
|
|
391
420
|
const codec = resolveCodec(key, defaultValue, options);
|
|
@@ -433,13 +462,25 @@ function setStoragePersistedState(key, newValueOrFn, options = {}) {
|
|
|
433
462
|
console.error(`Error setting storage key "${key}":`, error);
|
|
434
463
|
}
|
|
435
464
|
}
|
|
465
|
+
function removeStoragePersistedState(key, storageType) {
|
|
466
|
+
const syncManager = getSyncManager(storageType);
|
|
467
|
+
const adapter = syncManager.storage;
|
|
468
|
+
try {
|
|
469
|
+
adapter.removeItem(key);
|
|
470
|
+
syncManager.notify(key);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
console.error(`Error removing storage key "${key}":`, error);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
436
475
|
export {
|
|
437
476
|
BooleanCodec,
|
|
438
477
|
JsonCodec,
|
|
439
478
|
NumberCodec,
|
|
440
479
|
StringCodec,
|
|
480
|
+
getStoragePersistedState,
|
|
441
481
|
inferCodec,
|
|
442
|
-
readStoragePersistedState,
|
|
482
|
+
getStoragePersistedState as readStoragePersistedState,
|
|
483
|
+
removeStoragePersistedState,
|
|
443
484
|
setStoragePersistedState,
|
|
444
485
|
useStoragePersistedState
|
|
445
486
|
};
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const JsonCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n */\nexport const StringCodec: Codec<string | null> = {\n encode: (value) => value ?? null,\n decode: (value) => value ?? null,\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n */\nexport const NumberCodec: Codec<number | null> = {\n encode: (value) => {\n if (value === null) return null;\n return String(value);\n },\n decode: (value) => {\n if (value === null || value === \"\") return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n const parsed = Number(value);\n return isNaN(parsed) ? null : parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function readStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/useStoragePersistedState.ts","../src/codecs.ts","../src/storage.ts","../src/storagePersistedState.ts"],"sourcesContent":["\"use client\";\n\nimport { useCallback, useMemo, useRef } from \"react\";\nimport { useSyncExternalStore } from \"use-sync-external-store/shim\";\nimport { Codec, inferCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\n\nexport type StorageType = \"localStorage\" | \"sessionStorage\" | \"memory\";\n\n/**\n * Options for the useStoragePersistedState hook\n */\nexport interface StoragePersistedStateOptions<T> {\n /**\n * Explicit codec for when defaultValue is null or undefined, for complex types, or special cases (e.g., data migration on read).\n * If not provided, codec is inferred from defaultValue type.\n */\n codec?: Codec<T>;\n /**\n * Storage type to use: 'localStorage' (default), 'sessionStorage', or 'memory'.\n */\n storageType?: StorageType;\n /**\n * Enable cross-tab synchronization via the StorageEvent. Note: disabling this will not stop other tabs from updating localStorage, but this hook will not automatically respond to those changes.\n * defaults to true.\n */\n crossTabSync?: boolean;\n /**\n * Polling interval in milliseconds for detecting storage changes made outside of React (e.g., DevTools, direct localStorage manipulation). Set to null to disable polling.\n * defaults to 2000ms.\n */\n pollingIntervalMs?: number | null;\n}\n\n// Overload 1: Default provided, T inferred\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): [T, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): [T | null, (newValue: T | ((prev: T) => T)) => void, () => void];\n\n// Implementation\nexport function useStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager =\n options.storageType === \"sessionStorage\"\n ? sessionStorageSync\n : options.storageType === \"memory\"\n ? memoryStorageSync\n : localStorageSync;\n const adapter = syncManager.storage;\n\n // 1. Determine the codec.\n // If no explicit codec is passed, we try to infer it from defaultValue.\n // If defaultValue is undefined and no codec is passed, we fall back to JSON.\n const codec = useMemo(() => {\n if (options?.codec) return options.codec;\n return inferCodec(defaultValue);\n }, [defaultValue, options?.codec]);\n\n if ((defaultValue === undefined || defaultValue === null) && !options.codec) {\n console.warn(\n `useStorage: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n // Memoize the decoded value to prevent infinite loops in useSyncExternalStore\n // when the codec returns a new object reference (e.g. JSON.parse).\n const lastRaw = useRef<string | null>(null); // string | null, because storage returns null for missing keys\n const lastParsed = useRef<T | undefined>(undefined);\n\n const getSnapshot = useCallback(() => {\n const raw = adapter.getItem(key);\n\n // Return default if storage is missing the key\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n // If raw value matches cache, return cached object.\n if (raw === lastRaw.current) {\n if (lastParsed.current === undefined) {\n return null as T;\n }\n return lastParsed.current as T;\n }\n\n // Decode new raw value\n try {\n const decoded = codec.decode(raw);\n\n lastRaw.current = raw;\n lastParsed.current = decoded;\n return decoded;\n } catch (e) {\n console.error(`Error parsing storage key \"${key}\"`, e);\n return defaultValue as T;\n }\n }, [adapter, key, codec, defaultValue]);\n\n // 2. Subscribe to the external store (Local/SessionStorage + Polling/Events)\n // useSyncExternalStore handles the hydration mismatch automatically by\n // taking a `getServerSnapshot` (returning defaultValue).\n const value = useSyncExternalStore(\n (callback) =>\n syncManager.subscribe(key, callback, {\n crossTabSync: options.crossTabSync,\n pollingIntervalMs: options.pollingIntervalMs,\n }),\n getSnapshot,\n () => defaultValue as T, // Server Snapshot\n );\n\n // 3. Create the Setter\n const setValue = useCallback(\n (newValueOrFn: T | ((prev: T) => T)) => {\n try {\n const currentRaw = adapter.getItem(key);\n const current =\n currentRaw !== null ? codec.decode(currentRaw) : defaultValue;\n\n const newValue =\n newValueOrFn instanceof Function\n ? newValueOrFn(current)\n : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n lastRaw.current = encoded;\n lastParsed.current = newValue;\n\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n // Notify other hooks/tabs\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n },\n [adapter, key, codec, defaultValue, syncManager],\n );\n\n const removeItem = useCallback(() => {\n try {\n lastRaw.current = null;\n lastParsed.current = undefined;\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n }, [adapter, key, syncManager]);\n\n return [value, setValue, removeItem] as const;\n}\n","/**\n * Defines how to serialize/deserialize a value from localStorage.\n * null means the key doesn't exist.\n */\nexport interface Codec<T> {\n encode: (value: T) => string | null;\n decode: (value: string | null) => T;\n}\n\n/**\n * A robust JSON codec that handles parsing errors gracefully.\n * Works with objects, arrays, and other JSON-serializable values.\n *\n * @example\n * ```ts\n * const [user, setUser] = useStoragePersistedState<User | null>(\n * \"user\",\n * null,\n * { codec: JsonCodec }\n * );\n * ```\n * any type is needed to allow objects and arrays of any shape.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const JsonCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n return JSON.stringify(value);\n },\n decode: (value) => {\n if (value === null) return null;\n try {\n return JSON.parse(value);\n } catch (e) {\n console.warn(`LocalStorage parse error for value \"${value}\".`, e);\n throw e;\n }\n },\n};\n\n/**\n * Codec for string values. Stores strings as-is without any transformation.\n *\n * @example\n * ```ts\n * const [name, setName] = useStoragePersistedState(\"name\", \"Guest\");\n * // Automatically uses StringCodec because defaultValue is a string\n * ```\n *\n * any type is needed to allow string enums.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const StringCodec: Codec<any> = {\n encode: (value) => {\n if (value == null) return null;\n if (typeof value === \"string\") return value;\n console.error(\n `StringCodec encode expected a string but got ${typeof value}.`,\n );\n return String(value);\n },\n decode: (value) => {\n if (value == null) return null;\n if (typeof value === \"string\") return value;\n console.error(\n `StringCodec decode expected a string but got ${typeof value}.`,\n );\n return String(value);\n },\n};\n\n/**\n * Codec for boolean values. Stores as \"true\" or \"false\" strings.\n *\n * @example\n * ```ts\n * const [isDark, setIsDark] = useStoragePersistedState(\"darkMode\", false);\n * // Automatically uses BooleanCodec because defaultValue is a boolean\n * ```\n */\nexport const BooleanCodec: Codec<boolean> = {\n encode: (value) => (value ? \"true\" : \"false\"),\n decode: (value) => value === \"true\",\n};\n\n/**\n * Codec for number values. Handles NaN, Infinity, and -Infinity correctly.\n * Returns null for unparseable values (e.g., empty string, invalid number string).\n *\n * @example\n * ```ts\n * const [count, setCount] = useStoragePersistedState(\"count\", 0);\n * // Automatically uses NumberCodec because defaultValue is a number\n * ```\n *\n * any type is needed to allow number enums.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport const NumberCodec: Codec<any> = {\n encode: (value) => {\n if (value === null) return null;\n if (typeof value !== \"number\") {\n console.error(\n `NumberCodec encode expected a number but got ${typeof value}.`,\n );\n }\n return String(value);\n },\n decode: (value) => {\n if (value === null) return null;\n // Handle special numeric values\n if (value === \"NaN\") return NaN;\n if (value === \"Infinity\") return Infinity;\n if (value === \"-Infinity\") return -Infinity;\n if (value === \"\") {\n console.warn(\"NumberCodec decode received an empty string.\");\n return null;\n }\n const parsed = Number(value);\n if (Number.isNaN(parsed)) {\n console.warn(\n `NumberCodec decode received an invalid number string \"${value}\".`,\n );\n return null;\n }\n return parsed;\n },\n};\n\n/**\n * Infers the appropriate codec based on the default value's type.\n * Used internally when the user doesn't provide an explicit codec.\n *\n * - `boolean` → BooleanCodec\n * - `number` → NumberCodec\n * - `string` → StringCodec\n * - `object`, `array`, `undefined`, `null` → JsonCodec\n *\n * @param defaultValue - The default value to infer the codec from\n * @returns The inferred codec for the given value type\n */\nexport function inferCodec<T>(defaultValue: T): Codec<T> {\n const type = typeof defaultValue;\n\n // Type assertions through unknown are needed because we're doing runtime type inference\n // The actual codec returned will match the runtime type of defaultValue\n if (type === \"boolean\") return BooleanCodec as unknown as Codec<T>;\n if (type === \"number\") return NumberCodec as unknown as Codec<T>;\n if (type === \"string\") return StringCodec as unknown as Codec<T>;\n\n // Default to JSON for objects, arrays, or undefined\n return JsonCodec as unknown as Codec<T>;\n}\n","\"use client\";\n\n/**\n * Interface allows swapping LocalStorage for SessionStorage or Async Storage later\n */\nexport interface StorageAdapter {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface SubscribeOptions {\n crossTabSync?: boolean;\n pollingIntervalMs?: number | null;\n}\n\ninterface ListenerOptions {\n crossTabSync: boolean;\n pollingIntervalMs: number | null;\n}\n\ninterface StorageSyncManagerOptions {\n enableCrossTabSync?: boolean;\n enablePolling?: boolean;\n}\n\nclass MemoryStorageAdapter implements StorageAdapter {\n private store = new Map<string, string>();\n\n getItem(key: string) {\n return this.store.has(key) ? this.store.get(key)! : null;\n }\n\n setItem(key: string, value: string) {\n this.store.set(key, value);\n }\n\n removeItem(key: string) {\n this.store.delete(key);\n }\n}\n\nclass FallbackStorageAdapter implements StorageAdapter {\n constructor(\n private primary: StorageAdapter,\n private fallback: StorageAdapter,\n ) {}\n\n getItem(key: string) {\n const fallbackValue = this.fallback.getItem(key);\n if (fallbackValue !== null) return fallbackValue;\n return this.primary.getItem(key);\n }\n\n setItem(key: string, value: string) {\n try {\n this.primary.setItem(key, value);\n this.fallback.removeItem(key);\n } catch {\n this.fallback.setItem(key, value);\n }\n }\n\n removeItem(key: string) {\n try {\n this.primary.removeItem(key);\n } finally {\n this.fallback.removeItem(key);\n }\n }\n}\n\n// Singleton manager ensures we only have ONE listener per key globally\nclass StorageSyncManager {\n private listeners = new Map<string, Map<() => void, ListenerOptions>>();\n private pollingIntervalId: number | null = null;\n private pollingIntervalMsActive: number | null = null;\n private enableCrossTabSync: boolean;\n private enablePolling: boolean;\n\n constructor(\n public readonly storage: StorageAdapter,\n private defaultPollingIntervalMs = 2000,\n options: StorageSyncManagerOptions = {},\n ) {\n this.enableCrossTabSync = options.enableCrossTabSync ?? true;\n this.enablePolling = options.enablePolling ?? true;\n\n if (typeof window !== \"undefined\" && this.enableCrossTabSync) {\n // 1. Cross-tab sync (\"storage\" event is a built-in browser feature that fires\n // when localStorage/sessionStorage changes in ANOTHER tab)\n // We do not remove this listener because this manager is a singleton meant to last\n // the entire application lifecycle. It is effectively cleaned up when the page unloads.\n window.addEventListener(\"storage\", (e) => {\n if (e.key && this.listeners.has(e.key)) {\n this.notifyCrossTab(e.key);\n }\n });\n // 2. Start polling for DevTools or direct window.localStorage changes (robustness)\n // We only poll if there are listeners. Lazy start in subscribe.\n }\n }\n\n subscribe(key: string, callback: () => void, options: SubscribeOptions = {}) {\n if (!this.listeners.has(key)) {\n this.listeners.set(key, new Map());\n }\n const listenerOptions: ListenerOptions = {\n crossTabSync: this.enableCrossTabSync && (options.crossTabSync ?? true),\n pollingIntervalMs: this.enablePolling\n ? this.normalizePollingInterval(options.pollingIntervalMs)\n : null,\n };\n this.listeners.get(key)!.set(callback, listenerOptions);\n\n this.updatePollingInterval();\n\n return () => {\n const listenersForKey = this.listeners.get(key);\n listenersForKey?.delete(callback);\n if (listenersForKey?.size === 0) {\n this.listeners.delete(key);\n }\n this.updatePollingInterval();\n };\n }\n\n // Called when WE change the value in this tab\n notify(key: string) {\n this.notifyListeners(key);\n }\n\n // Keep a cache of values to detect changes during polling\n private snapshotCache = new Map<string, string | null>();\n\n private notifyListeners(\n key: string,\n predicate?: (options: ListenerOptions) => boolean,\n ) {\n this.listeners.get(key)?.forEach((options, cb) => {\n if (!predicate || predicate(options)) {\n cb();\n }\n });\n }\n\n private notifyCrossTab(key: string) {\n this.notifyListeners(key, (options) => options.crossTabSync);\n }\n\n private notifyPolling(key: string) {\n this.notifyListeners(key, (options) => options.pollingIntervalMs !== null);\n }\n\n private normalizePollingInterval(\n pollingIntervalMs: number | null | undefined,\n ) {\n if (pollingIntervalMs === null) return null;\n if (pollingIntervalMs === undefined) return this.defaultPollingIntervalMs;\n if (pollingIntervalMs <= 0 || Number.isNaN(pollingIntervalMs)) {\n return null;\n }\n return pollingIntervalMs;\n }\n\n private getMinPollingIntervalMs() {\n let minInterval: number | null = null;\n this.listeners.forEach((listenersForKey) => {\n listenersForKey.forEach((options) => {\n if (options.pollingIntervalMs === null) return;\n if (minInterval === null || options.pollingIntervalMs < minInterval) {\n minInterval = options.pollingIntervalMs;\n }\n });\n });\n return minInterval;\n }\n\n private updatePollingInterval() {\n if (typeof window === \"undefined\") return;\n if (!this.enablePolling) return;\n\n const nextInterval = this.getMinPollingIntervalMs();\n if (nextInterval === null) {\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n this.pollingIntervalId = null;\n this.pollingIntervalMsActive = null;\n this.snapshotCache.clear();\n return;\n }\n\n if (\n this.pollingIntervalId &&\n this.pollingIntervalMsActive === nextInterval\n ) {\n return;\n }\n\n if (this.pollingIntervalId) {\n clearInterval(this.pollingIntervalId);\n }\n\n this.pollingIntervalId = window.setInterval(() => {\n this.listeners.forEach((listenersForKey, key) => {\n const shouldPoll = Array.from(listenersForKey.values()).some(\n (options) => options.pollingIntervalMs !== null,\n );\n if (!shouldPoll) return;\n\n const currentValue = this.storage.getItem(key);\n const lastValue = this.snapshotCache.get(key);\n\n if (currentValue !== lastValue) {\n this.snapshotCache.set(key, currentValue);\n this.notifyPolling(key);\n }\n });\n }, nextInterval);\n this.pollingIntervalMsActive = nextInterval;\n }\n}\n\n// Lazy-initialized singletons to avoid SSR crashes (window is not defined on server)\nconst localStorageFallback = new MemoryStorageAdapter();\nconst sessionStorageFallback = new MemoryStorageAdapter();\n\nlet _localStorageSync: StorageSyncManager | null = null;\nlet _sessionStorageSync: StorageSyncManager | null = null;\nlet _memoryStorageSync: StorageSyncManager | null = null;\n\nfunction getLocalStorageSync(): StorageSyncManager {\n if (!_localStorageSync) {\n if (typeof window !== \"undefined\") {\n _localStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(window.localStorage, localStorageFallback),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _localStorageSync;\n}\n\nfunction getSessionStorageSync(): StorageSyncManager {\n if (!_sessionStorageSync) {\n if (typeof window !== \"undefined\") {\n _sessionStorageSync = new StorageSyncManager(\n new FallbackStorageAdapter(\n window.sessionStorage,\n sessionStorageFallback,\n ),\n );\n } else {\n // SSR fallback - use memory storage\n return getMemoryStorageSync();\n }\n }\n return _sessionStorageSync;\n}\n\nfunction getMemoryStorageSync(): StorageSyncManager {\n if (!_memoryStorageSync) {\n _memoryStorageSync = new StorageSyncManager(\n new MemoryStorageAdapter(),\n 2000,\n {\n enableCrossTabSync: false,\n enablePolling: false,\n },\n );\n }\n return _memoryStorageSync;\n}\n\n/**\n * Proxy object for localStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const localStorageSync = {\n get storage() {\n return getLocalStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getLocalStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getLocalStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for sessionStorage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const sessionStorageSync = {\n get storage() {\n return getSessionStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getSessionStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getSessionStorageSync().notify(key);\n },\n};\n\n/**\n * Proxy object for memory storage sync manager with lazy initialization.\n * Safe to import in SSR environments.\n */\nexport const memoryStorageSync = {\n get storage() {\n return getMemoryStorageSync().storage;\n },\n subscribe(key: string, callback: () => void, options?: SubscribeOptions) {\n return getMemoryStorageSync().subscribe(key, callback, options);\n },\n notify(key: string) {\n return getMemoryStorageSync().notify(key);\n },\n};\n","\"use client\";\n\nimport { Codec, inferCodec, JsonCodec } from \"./codecs\";\nimport {\n localStorageSync,\n memoryStorageSync,\n sessionStorageSync,\n} from \"./storage\";\nimport {\n StoragePersistedStateOptions,\n StorageType,\n} from \"./useStoragePersistedState\";\n\nfunction getSyncManager(storageType: StorageType | undefined) {\n if (storageType === \"sessionStorage\") return sessionStorageSync;\n if (storageType === \"memory\") return memoryStorageSync;\n return localStorageSync;\n}\n\nfunction resolveCodec<T>(\n key: string,\n valueHint: T,\n options?: StoragePersistedStateOptions<T>,\n): Codec<T> {\n if (options?.codec) return options.codec;\n\n if ((valueHint === undefined || valueHint === null) && !options?.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses undefined or null default without explicit Codec. defaulting to JSON.`,\n );\n }\n\n return inferCodec(valueHint);\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Read a persisted value from storage using the same codec behavior as the hook.\n *\n * Returns the provided defaultValue when the key is missing or parsing fails.\n */\nexport function getStoragePersistedState<T>(\n key: string,\n defaultValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): T;\n\n// Overload 2: Explicit Codec provided, defaultValue can be null or undefined\n/**\n * Read a persisted value from storage using an explicit codec.\n *\n * Use this overload when defaultValue is null or undefined.\n */\nexport function getStoragePersistedState<T>(\n key: string,\n defaultValue: null | undefined,\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): T | null;\n\nexport function getStoragePersistedState<T>(\n key: string,\n defaultValue: T,\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n const codec = resolveCodec(key, defaultValue, options);\n const raw = adapter.getItem(key);\n\n if (raw === null && defaultValue !== undefined) {\n return defaultValue as T;\n }\n\n try {\n return codec.decode(raw);\n } catch (error) {\n console.error(`Error parsing storage key \"${key}\"`, error);\n return defaultValue as T;\n }\n}\n\n// Overload 1: Default provided, T inferred\n/**\n * Set a persisted value in storage and notify active hooks for the same key.\n *\n * Supports functional updates using the current decoded value.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: Exclude<T, null | undefined>,\n options?: StoragePersistedStateOptions<T>,\n): void;\n\n// Overload 2: Explicit Codec provided, newValue can be null or undefined\n/**\n * Set a persisted value in storage using an explicit codec and notify listeners.\n *\n * Use this overload when the new value is null or undefined or when you want custom serialization.\n */\nexport function setStoragePersistedState<T>(\n key: string,\n newValue: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> & { codec: Codec<T> },\n): void;\n\nexport function setStoragePersistedState<T>(\n key: string,\n newValueOrFn: T | ((prev: T | null) => T),\n options: StoragePersistedStateOptions<T> = {},\n) {\n const syncManager = getSyncManager(options.storageType);\n const adapter = syncManager.storage;\n\n let codec: Codec<T>;\n if (!(newValueOrFn instanceof Function)) {\n codec = resolveCodec(key, newValueOrFn, options);\n } else {\n // For functional updates, we cannot infer from newValue\n if (!options.codec) {\n console.warn(\n `storagePersistedState: Key \"${key}\" uses functional update without explicit Codec. defaulting to JSON.`,\n );\n }\n codec = options.codec ?? (JsonCodec as unknown as Codec<T>);\n }\n\n try {\n const currentRaw = adapter.getItem(key);\n const current = currentRaw !== null ? codec.decode(currentRaw) : null;\n\n const newValue =\n newValueOrFn instanceof Function ? newValueOrFn(current) : newValueOrFn;\n\n if (newValue === undefined || newValue === null) {\n adapter.removeItem(key);\n } else {\n const encoded = codec.encode(newValue);\n if (encoded === null || encoded === undefined) {\n adapter.removeItem(key);\n } else {\n adapter.setItem(key, encoded);\n }\n }\n\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error setting storage key \"${key}\":`, error);\n }\n}\n\n/**\n * Remove a persisted value from storage and notify active hooks for the same key.\n *\n * Defaults to localStorage when no storageType is provided.\n */\nexport function removeStoragePersistedState(\n key: string,\n storageType?: StorageType,\n) {\n const syncManager = getSyncManager(storageType);\n const adapter = syncManager.storage;\n\n try {\n adapter.removeItem(key);\n syncManager.notify(key);\n } catch (error) {\n console.error(`Error removing storage key \"${key}\":`, error);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "use-storage-persisted-state",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A robust, type-safe React hook for persisting state in localStorage/sessionStorage with cross-component and cross-tab sync",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mikko Haapanen <mikko.h@iki.fi>",
|