@plq/use-persisted-state 1.2.0 → 1.4.0
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/LICENSE +1 -1
- package/README.md +168 -43
- package/esm/index.d.mts +13 -0
- package/esm/index.mjs +10 -0
- package/esm/storages/browser-storage.d.mts +1 -0
- package/esm/storages/browser-storage.mjs +5 -0
- package/esm/storages/chrome-storage.d.mts +1 -0
- package/esm/storages/chrome-storage.mjs +5 -0
- package/esm/storages/local-storage.d.mts +1 -0
- package/esm/storages/local-storage.mjs +3 -0
- package/esm/storages/session-storage.d.mts +1 -0
- package/esm/storages/session-storage.mjs +3 -0
- package/lib/@types/storage.d.ts.map +1 -1
- package/lib/create-async-persisted-state.d.ts +2 -2
- package/lib/create-async-persisted-state.d.ts.map +1 -1
- package/lib/create-async-persisted-state.js +63 -13
- package/lib/create-async-persisted-state.js.map +1 -1
- package/lib/create-persisted-state.d.ts +2 -2
- package/lib/create-persisted-state.d.ts.map +1 -1
- package/lib/create-persisted-state.js +32 -14
- package/lib/create-persisted-state.js.map +1 -1
- package/lib/index.d.ts +6 -5
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -3
- package/lib/index.js.map +1 -1
- package/lib/storages/browser-storage.d.ts +2 -2
- package/lib/storages/browser-storage.d.ts.map +1 -1
- package/lib/storages/browser-storage.js +6 -26
- package/lib/storages/browser-storage.js.map +1 -1
- package/lib/storages/chrome-storage.d.ts +2 -2
- package/lib/storages/chrome-storage.d.ts.map +1 -1
- package/lib/storages/chrome-storage.js +10 -27
- package/lib/storages/chrome-storage.js.map +1 -1
- package/lib/storages/local-storage.d.ts +1 -1
- package/lib/storages/local-storage.d.ts.map +1 -1
- package/lib/storages/local-storage.js +3 -1
- package/lib/storages/local-storage.js.map +1 -1
- package/lib/storages/session-storage.d.ts +1 -1
- package/lib/storages/session-storage.d.ts.map +1 -1
- package/lib/storages/session-storage.js +3 -1
- package/lib/storages/session-storage.js.map +1 -1
- package/lib/utils/change-notifier.d.ts +18 -0
- package/lib/utils/change-notifier.d.ts.map +1 -0
- package/lib/utils/change-notifier.js +38 -0
- package/lib/utils/change-notifier.js.map +1 -0
- package/lib/utils/create-web-storage.d.ts +10 -2
- package/lib/utils/create-web-storage.d.ts.map +1 -1
- package/lib/utils/create-web-storage.js +72 -75
- package/lib/utils/create-web-storage.js.map +1 -1
- package/lib/utils/extension-storage.d.ts +38 -0
- package/lib/utils/extension-storage.d.ts.map +1 -0
- package/lib/utils/extension-storage.js +66 -0
- package/lib/utils/extension-storage.js.map +1 -0
- package/lib/utils/get-new-item.d.ts.map +1 -1
- package/lib/utils/get-new-item.js +1 -1
- package/lib/utils/get-new-item.js.map +1 -1
- package/lib/utils/get-new-value.d.ts +1 -1
- package/lib/utils/get-new-value.d.ts.map +1 -1
- package/lib/utils/get-new-value.js +1 -1
- package/lib/utils/get-new-value.js.map +1 -1
- package/lib/utils/get-persisted-value.d.ts +8 -1
- package/lib/utils/get-persisted-value.d.ts.map +1 -1
- package/lib/utils/get-persisted-value.js +20 -6
- package/lib/utils/get-persisted-value.js.map +1 -1
- package/lib/utils/is-async-storage.d.ts +7 -0
- package/lib/utils/is-async-storage.d.ts.map +1 -1
- package/lib/utils/is-async-storage.js +52 -30
- package/lib/utils/is-async-storage.js.map +1 -1
- package/lib/utils/storage-event-router.d.ts +17 -0
- package/lib/utils/storage-event-router.d.ts.map +1 -0
- package/lib/utils/storage-event-router.js +54 -0
- package/lib/utils/storage-event-router.js.map +1 -0
- package/lib/utils/use-storage-handler.d.ts +3 -3
- package/lib/utils/use-storage-handler.d.ts.map +1 -1
- package/lib/utils/use-storage-handler.js +80 -26
- package/lib/utils/use-storage-handler.js.map +1 -1
- package/package.json +105 -44
- package/src/@types/storage.ts +13 -13
- package/src/create-async-persisted-state.ts +81 -18
- package/src/create-persisted-state.ts +48 -22
- package/src/index.ts +12 -7
- package/src/storages/browser-storage.ts +8 -36
- package/src/storages/chrome-storage.ts +24 -46
- package/src/storages/local-storage.ts +3 -1
- package/src/storages/session-storage.ts +3 -1
- package/src/utils/change-notifier.ts +43 -0
- package/src/utils/create-web-storage.ts +74 -77
- package/src/utils/extension-storage.ts +79 -0
- package/src/utils/get-new-item.ts +1 -1
- package/src/utils/get-new-value.ts +1 -1
- package/src/utils/get-persisted-value.ts +23 -8
- package/src/utils/is-async-storage.ts +72 -37
- package/src/utils/storage-event-router.ts +67 -0
- package/src/utils/use-storage-handler.ts +112 -36
- package/storages/browser-storage.d.ts +1 -0
- package/storages/browser-storage.js +11 -0
- package/storages/chrome-storage.d.ts +1 -0
- package/storages/chrome-storage.js +11 -0
- package/storages/local-storage.d.ts +1 -0
- package/storages/local-storage.js +9 -0
- package/storages/session-storage.d.ts +1 -0
- package/storages/session-storage.js +9 -0
- package/lib/utils/is-function.d.ts +0 -2
- package/lib/utils/is-function.d.ts.map +0 -1
- package/lib/utils/is-function.js +0 -9
- package/lib/utils/is-function.js.map +0 -1
- package/lib/utils/is-promise.d.ts +0 -2
- package/lib/utils/is-promise.d.ts.map +0 -1
- package/lib/utils/is-promise.js +0 -10
- package/lib/utils/is-promise.js.map +0 -1
|
@@ -1,62 +1,138 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import { useEffect, useRef } from 'react'
|
|
3
|
+
import type { AsyncStorage, Storage, StorageChange } from '../@types/storage'
|
|
4
|
+
import { isFunction, isObject } from '@plq/is'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
/**
|
|
7
|
+
* One key read out of a serialized entry. `null` is a value a caller can store,
|
|
8
|
+
* so a stored `null` has to stay distinguishable from no value at all —
|
|
9
|
+
* collapsing the two is what dropped a persisted `null` on its way from a change
|
|
10
|
+
* event into state.
|
|
11
|
+
*
|
|
12
|
+
* A key that is not in the entry and an entry that will not parse are both
|
|
13
|
+
* `unavailable`. They differ in why, not in what a caller does next, and the
|
|
14
|
+
* difference that matters is reported where it happens: only a parse failure is
|
|
15
|
+
* a defect worth a diagnostic, an entry holding other keys is routine.
|
|
16
|
+
*/
|
|
17
|
+
type StoredValue<T> = { status: 'stored'; value: T } | { status: 'unavailable' }
|
|
18
|
+
|
|
19
|
+
function readStoredValue<T>(key: string, entry: string): StoredValue<T> {
|
|
20
|
+
let parsed: unknown
|
|
7
21
|
|
|
8
22
|
try {
|
|
9
|
-
|
|
23
|
+
parsed = JSON.parse(entry)
|
|
10
24
|
} catch (err) {
|
|
11
|
-
console.error(
|
|
25
|
+
console.error("use-persisted-state: Can't parse value from storage", err)
|
|
26
|
+
|
|
27
|
+
return { status: 'unavailable' }
|
|
12
28
|
}
|
|
13
29
|
|
|
14
|
-
|
|
30
|
+
// A foreign entry can be any JSON value, and `in` throws on a primitive. This
|
|
31
|
+
// runs inside the adapter's notify loop, where a throw also cuts off every
|
|
32
|
+
// listener queued behind this one.
|
|
33
|
+
if (!isObject(parsed) || !(key in parsed)) return { status: 'unavailable' }
|
|
34
|
+
|
|
35
|
+
return { status: 'stored', value: parsed[key] as T }
|
|
15
36
|
}
|
|
16
37
|
|
|
17
|
-
|
|
38
|
+
// Restores the initial value when the whole entry is removed from the storage.
|
|
39
|
+
function applyRemoval<T>(
|
|
40
|
+
change: StorageChange,
|
|
41
|
+
itemKey: string,
|
|
42
|
+
applyValue: (value: T) => void,
|
|
43
|
+
latestInitialValue: React.RefObject<T | (() => T)>,
|
|
44
|
+
): void {
|
|
45
|
+
if (change.oldValue === null || change.oldValue === undefined) return
|
|
46
|
+
|
|
47
|
+
const oldValue = readStoredValue<T>(itemKey, change.oldValue)
|
|
48
|
+
const declaredInitialValue = latestInitialValue.current
|
|
49
|
+
// Compared against the resolved value: a factory is never equal to what it
|
|
50
|
+
// produces, so comparing the declaration re-applies the initial value on every
|
|
51
|
+
// removal.
|
|
52
|
+
const initialValue = isFunction(declaredInitialValue) ? declaredInitialValue() : declaredInitialValue
|
|
53
|
+
|
|
54
|
+
// An entry with no readable value for the key cannot be shown to have held the
|
|
55
|
+
// initial value, so the fallback is applied rather than skipped. The entry is
|
|
56
|
+
// gone either way, and stale state left in place is worse than a redundant
|
|
57
|
+
// restore.
|
|
58
|
+
if (oldValue.status === 'stored' && oldValue.value === initialValue) return
|
|
59
|
+
|
|
60
|
+
applyValue(initialValue)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Reports whether this change is the write the hook itself just made, forgetting
|
|
65
|
+
* the record when it is. A backend reports a write to every listener, the one
|
|
66
|
+
* that made it included, and applying that echo is the storage-to-state re-sync
|
|
67
|
+
* this hook was rebuilt without, arriving by another road: it decodes the entry
|
|
68
|
+
* again and hands the caller an equal value with a new identity, plus a render
|
|
69
|
+
* for a value it already holds.
|
|
70
|
+
*
|
|
71
|
+
* The price, accepted knowingly: for a value JSON cannot carry, the writer keeps
|
|
72
|
+
* what it set - NaN - while every other component on the key decodes the null
|
|
73
|
+
* that reached storage, so the two disagree until one of them remounts. Applying
|
|
74
|
+
* the echo would settle that disagreement and bring back both the second
|
|
75
|
+
* storage-to-state path and the render per write, so it is not the repair it
|
|
76
|
+
* looks like.
|
|
77
|
+
*
|
|
78
|
+
* Forgetting on the first match keeps a later identical entry a change. Another
|
|
79
|
+
* hook writing the same bytes is suppressed along with it, and nothing is lost:
|
|
80
|
+
* it would have replaced a value with an equal one.
|
|
81
|
+
*/
|
|
82
|
+
function consumeOwnWriteEcho(change: StorageChange, pendingOwnWrite: React.RefObject<string | null>): boolean {
|
|
83
|
+
if (pendingOwnWrite.current === null || change.newValue !== pendingOwnWrite.current) return false
|
|
84
|
+
|
|
85
|
+
pendingOwnWrite.current = null
|
|
86
|
+
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Builds the change handler. Not a hook, despite living next to one.
|
|
91
|
+
function createStorageHandler<T>(
|
|
18
92
|
itemKey: string,
|
|
19
93
|
storageKey: string,
|
|
20
|
-
|
|
21
|
-
|
|
94
|
+
applyValue: (value: T) => void,
|
|
95
|
+
latestInitialValue: React.RefObject<T | (() => T)>,
|
|
96
|
+
pendingOwnWrite: React.RefObject<string | null>,
|
|
22
97
|
) {
|
|
23
98
|
return (changes: { [key: string]: StorageChange }): void => {
|
|
24
|
-
|
|
25
|
-
if (
|
|
26
|
-
key === storageKey
|
|
27
|
-
&& (
|
|
28
|
-
change.newValue === null || change.newValue === undefined
|
|
29
|
-
)
|
|
30
|
-
&& change.oldValue !== null
|
|
31
|
-
&& change.oldValue !== undefined
|
|
32
|
-
) {
|
|
33
|
-
const oldValue = getValue<T>(itemKey, change.oldValue)
|
|
34
|
-
|
|
35
|
-
if (oldValue !== initialValue) setState(isFunction(initialValue) ? initialValue() : initialValue)
|
|
36
|
-
}
|
|
99
|
+
for (const [key, change] of Object.entries(changes)) {
|
|
100
|
+
if (key !== storageKey) continue
|
|
37
101
|
|
|
38
|
-
if (
|
|
39
|
-
key === storageKey
|
|
40
|
-
&& change.newValue !== null
|
|
41
|
-
&& change.newValue !== undefined
|
|
42
|
-
) {
|
|
43
|
-
const newValue = getValue<T>(itemKey, change.newValue)
|
|
102
|
+
if (consumeOwnWriteEcho(change, pendingOwnWrite)) continue
|
|
44
103
|
|
|
45
|
-
|
|
104
|
+
if (change.newValue === null || change.newValue === undefined) {
|
|
105
|
+
applyRemoval<T>(change, itemKey, applyValue, latestInitialValue)
|
|
106
|
+
continue
|
|
46
107
|
}
|
|
47
|
-
|
|
108
|
+
|
|
109
|
+
const newValue = readStoredValue<T>(itemKey, change.newValue)
|
|
110
|
+
|
|
111
|
+
if (newValue.status === 'stored') applyValue(newValue.value)
|
|
112
|
+
}
|
|
48
113
|
}
|
|
49
114
|
}
|
|
50
115
|
|
|
51
|
-
export default function<T>(
|
|
116
|
+
export default function useStorageHandler<T>(
|
|
52
117
|
key: string,
|
|
53
118
|
storageKey: string,
|
|
54
|
-
|
|
119
|
+
applyValue: (value: T) => void,
|
|
55
120
|
storage: AsyncStorage | Storage,
|
|
56
121
|
initialValue: T | (() => T),
|
|
122
|
+
pendingOwnWrite: React.RefObject<string | null>,
|
|
57
123
|
): void {
|
|
124
|
+
// A removal restores the initial value the hook holds now, the same one the
|
|
125
|
+
// key-change path reads, so a caller whose default travels with its data gets
|
|
126
|
+
// that record's default back rather than the mounted one. Tracked through a ref
|
|
127
|
+
// rather than a dependency because an inline object or factory has a new
|
|
128
|
+
// identity every render, which would tear the subscription down and rebuild it;
|
|
129
|
+
// written during render, as the key the hook is rendering for is.
|
|
130
|
+
const latestInitialValue = useRef(initialValue)
|
|
131
|
+
|
|
132
|
+
latestInitialValue.current = initialValue
|
|
133
|
+
|
|
58
134
|
useEffect(() => {
|
|
59
|
-
const handleStorage =
|
|
135
|
+
const handleStorage = createStorageHandler<T>(key, storageKey, applyValue, latestInitialValue, pendingOwnWrite)
|
|
60
136
|
|
|
61
137
|
storage.onChanged.addListener(handleStorage)
|
|
62
138
|
|
|
@@ -65,5 +141,5 @@ export default function<T>(
|
|
|
65
141
|
storage.onChanged.removeListener(handleStorage)
|
|
66
142
|
}
|
|
67
143
|
}
|
|
68
|
-
}, [
|
|
144
|
+
}, [key, storage.onChanged, storageKey, applyValue, pendingOwnWrite])
|
|
69
145
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { local, sync, managed } from '../lib/storages/browser-storage'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Resolution stub for tooling that predates the exports map: TypeScript's
|
|
2
|
+
// node10 resolver and older bundlers look for a real file at this path and
|
|
3
|
+
// would otherwise fail. Node 16+ never reaches this file — the exports map
|
|
4
|
+
// answers first. Each area is re-exported by reference, so both paths hand out
|
|
5
|
+
// the same adapter instance and the listener registry stays single.
|
|
6
|
+
const source = require('../lib/storages/browser-storage.js')
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
9
|
+
exports.local = source.local
|
|
10
|
+
exports.sync = source.sync
|
|
11
|
+
exports.managed = source.managed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { local, sync, managed } from '../lib/storages/chrome-storage'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Resolution stub for tooling that predates the exports map: TypeScript's
|
|
2
|
+
// node10 resolver and older bundlers look for a real file at this path and
|
|
3
|
+
// would otherwise fail. Node 16+ never reaches this file — the exports map
|
|
4
|
+
// answers first. Each area is re-exported by reference, so both paths hand out
|
|
5
|
+
// the same adapter instance and the listener registry stays single.
|
|
6
|
+
const source = require('../lib/storages/chrome-storage.js')
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
9
|
+
exports.local = source.local
|
|
10
|
+
exports.sync = source.sync
|
|
11
|
+
exports.managed = source.managed
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '../lib/storages/local-storage'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Resolution stub for tooling that predates the exports map: TypeScript's
|
|
2
|
+
// node10 resolver and older bundlers look for a real file at this path and
|
|
3
|
+
// would otherwise fail. Node 16+ never reaches this file — the exports map
|
|
4
|
+
// answers first. The lib module's shape is mirrored rather than re-assigned to
|
|
5
|
+
// module.exports, so the declaration beside it stays true for both resolvers.
|
|
6
|
+
const source = require('../lib/storages/local-storage.js')
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
9
|
+
exports.default = source.default
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default } from '../lib/storages/session-storage'
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Resolution stub for tooling that predates the exports map: TypeScript's
|
|
2
|
+
// node10 resolver and older bundlers look for a real file at this path and
|
|
3
|
+
// would otherwise fail. Node 16+ never reaches this file — the exports map
|
|
4
|
+
// answers first. The lib module's shape is mirrored rather than re-assigned to
|
|
5
|
+
// module.exports, so the declaration beside it stays true for both resolvers.
|
|
6
|
+
const source = require('../lib/storages/session-storage.js')
|
|
7
|
+
|
|
8
|
+
Object.defineProperty(exports, '__esModule', { value: true })
|
|
9
|
+
exports.default = source.default
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-function.d.ts","sourceRoot":"","sources":["../../src/utils/is-function.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,QAAQ,CAMpE"}
|
package/lib/utils/is-function.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
function isFunction(value) {
|
|
4
|
-
return !!value
|
|
5
|
-
&& (Object.prototype.toString.call(value) === '[object Function]'
|
|
6
|
-
|| typeof value === 'function');
|
|
7
|
-
}
|
|
8
|
-
exports.default = isFunction;
|
|
9
|
-
//# sourceMappingURL=is-function.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-function.js","sourceRoot":"","sources":["../../src/utils/is-function.ts"],"names":[],"mappings":";;AAAA,SAAwB,UAAU,CAAC,KAAc;IAC/C,OAAO,CAAC,CAAC,KAAK;WACT,CACD,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,mBAAmB;eAC1D,OAAO,KAAK,KAAK,UAAU,CAC/B,CAAA;AACL,CAAC;AAND,6BAMC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-promise.d.ts","sourceRoot":"","sources":["../../src/utils/is-promise.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,OAAO,UAAU,SAAS,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,WAAW,CAAC,CAAC,CAAC,CAOtF"}
|
package/lib/utils/is-promise.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
function isPromise(value) {
|
|
4
|
-
return !!value
|
|
5
|
-
&& (Object.prototype.toString.call(value) === '[object Promise]'
|
|
6
|
-
|| typeof value === 'function')
|
|
7
|
-
&& typeof value.then === 'function';
|
|
8
|
-
}
|
|
9
|
-
exports.default = isPromise;
|
|
10
|
-
//# sourceMappingURL=is-promise.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"is-promise.js","sourceRoot":"","sources":["../../src/utils/is-promise.ts"],"names":[],"mappings":";;AAAA,SAAwB,SAAS,CAAc,KAAc;IAC3D,OAAO,CAAC,CAAC,KAAK;WACT,CACD,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,kBAAkB;eACzD,OAAO,KAAK,KAAK,UAAU,CAC/B;WACE,OAAQ,KAA8B,CAAC,IAAI,KAAK,UAAU,CAAA;AACjE,CAAC;AAPD,4BAOC"}
|