@plq/use-persisted-state 1.4.0 → 1.4.2
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 -0
- package/lib/create-async-persisted-state.d.ts.map +1 -1
- package/lib/create-async-persisted-state.js +67 -38
- package/lib/create-async-persisted-state.js.map +1 -1
- package/lib/create-persisted-state.d.ts.map +1 -1
- package/lib/create-persisted-state.js +48 -17
- package/lib/create-persisted-state.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/storages/chrome-storage.d.ts.map +1 -1
- package/lib/storages/chrome-storage.js +6 -15
- package/lib/storages/chrome-storage.js.map +1 -1
- package/lib/storages/local-storage.d.ts.map +1 -1
- package/lib/storages/local-storage.js +1 -2
- package/lib/storages/local-storage.js.map +1 -1
- package/lib/storages/session-storage.d.ts.map +1 -1
- package/lib/storages/session-storage.js +1 -2
- package/lib/storages/session-storage.js.map +1 -1
- package/lib/utils/change-notifier.d.ts +1 -8
- package/lib/utils/change-notifier.d.ts.map +1 -1
- package/lib/utils/change-notifier.js +1 -10
- package/lib/utils/change-notifier.js.map +1 -1
- package/lib/utils/create-web-storage.d.ts +1 -8
- package/lib/utils/create-web-storage.d.ts.map +1 -1
- package/lib/utils/create-web-storage.js +5 -17
- package/lib/utils/create-web-storage.js.map +1 -1
- package/lib/utils/extension-storage.d.ts +2 -12
- package/lib/utils/extension-storage.d.ts.map +1 -1
- package/lib/utils/extension-storage.js +3 -14
- package/lib/utils/extension-storage.js.map +1 -1
- package/lib/utils/get-new-item.d.ts +2 -1
- package/lib/utils/get-new-item.d.ts.map +1 -1
- package/lib/utils/get-new-item.js +13 -7
- package/lib/utils/get-new-item.js.map +1 -1
- package/lib/utils/get-persisted-value.d.ts +1 -7
- package/lib/utils/get-persisted-value.d.ts.map +1 -1
- package/lib/utils/get-persisted-value.js +3 -13
- package/lib/utils/get-persisted-value.js.map +1 -1
- package/lib/utils/is-async-storage.d.ts +1 -7
- package/lib/utils/is-async-storage.d.ts.map +1 -1
- package/lib/utils/is-async-storage.js +6 -25
- package/lib/utils/is-async-storage.js.map +1 -1
- package/lib/utils/storage-event-router.d.ts +1 -5
- package/lib/utils/storage-event-router.d.ts.map +1 -1
- package/lib/utils/storage-event-router.js +3 -14
- package/lib/utils/storage-event-router.js.map +1 -1
- package/lib/utils/use-storage-handler.d.ts +3 -1
- package/lib/utils/use-storage-handler.d.ts.map +1 -1
- package/lib/utils/use-storage-handler.js +28 -46
- package/lib/utils/use-storage-handler.js.map +1 -1
- package/package.json +1 -1
- package/src/create-async-persisted-state.ts +42 -40
- package/src/create-persisted-state.ts +15 -17
- package/src/index.ts +1 -4
- package/src/storages/chrome-storage.ts +6 -18
- package/src/storages/local-storage.ts +1 -2
- package/src/storages/session-storage.ts +1 -2
- package/src/utils/change-notifier.ts +1 -10
- package/src/utils/create-web-storage.ts +5 -17
- package/src/utils/extension-storage.ts +3 -14
- package/src/utils/get-new-item.ts +18 -9
- package/src/utils/get-persisted-value.ts +3 -13
- package/src/utils/is-async-storage.ts +6 -26
- package/src/utils/storage-event-router.ts +3 -14
- package/src/utils/use-storage-handler.ts +33 -58
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension-storage.js","sourceRoot":"","sources":["../../src/utils/extension-storage.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"extension-storage.js","sourceRoot":"","sources":["../../src/utils/extension-storage.ts"],"names":[],"mappings":";;AAaA,sCAIC;AAED,4CAaC;AAED,sCAQC;AAQD,wDAiBC;AAlED,uDAA6E;AAE7E,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,CAAU,CAAA;AAI3D,0GAA0G;AAC1G,SAAS,aAAa,CAAC,IAAY;IACjC,OAAQ,aAAmC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAA;AAC5D,CAAC;AAED,kHAAkH;AAClH,SAAgB,aAAa,CAAC,KAAc;IAC1C,IAAI,KAAK,KAAK,SAAS;QAAE,OAAM;IAE/B,OAAO,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;AACjD,CAAC;AAED,SAAgB,gBAAgB,CAAC,OAAsE;IAGrG,MAAM,MAAM,GAAqC,EAAE,CAAA;IAEnD,KAAK,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,MAAM,CAAC,GAAG,CAAC,GAAG;YACZ,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC;YACxC,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,QAAQ,CAAC;SACzC,CAAA;IACH,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,SAAgB,aAAa,CAAC,KAAiC;IAC7D,MAAM,MAAM,GAA8B,EAAE,CAAA;IAE5C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACjD,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;IACpD,CAAC;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAOD,gHAAgH;AAChH,SAAgB,sBAAsB;IACpC,MAAM,SAAS,GAAuC;QACpD,KAAK,EAAE,IAAA,sCAAoB,GAAE;QAC7B,IAAI,EAAE,IAAA,sCAAoB,GAAE;QAC5B,OAAO,EAAE,IAAA,sCAAoB,GAAE;KAChC,CAAA;IAED,OAAO;QACL,IAAI,CAAC,OAAO,EAAE,IAAI;YAChB,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;gBAAE,OAAM;YAEhC,SAAS,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QAC/B,CAAC;QACD,eAAe,CAAC,IAAI;YAClB,OAAO,SAAS,CAAC,IAAI,CAAC,CAAC,SAAS,CAAA;QAClC,CAAC;KACF,CAAA;AACH,CAAC"}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
|
|
1
|
+
/** Merges `newValue` under `key` into the shared entry, throwing rather than rebuilding one it cannot read. */
|
|
2
|
+
export default function getNewItem<T>(key: string, persistedItem: string, newValue: T): string;
|
|
2
3
|
//# sourceMappingURL=get-new-item.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-new-item.d.ts","sourceRoot":"","sources":["../../src/utils/get-new-item.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"get-new-item.d.ts","sourceRoot":"","sources":["../../src/utils/get-new-item.ts"],"names":[],"mappings":"AAEA,+GAA+G;AAC/G,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAqB7F"}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.default =
|
|
4
|
-
|
|
3
|
+
exports.default = getNewItem;
|
|
4
|
+
const is_1 = require("@plq/is");
|
|
5
|
+
/** Merges `newValue` under `key` into the shared entry, throwing rather than rebuilding one it cannot read. */
|
|
6
|
+
function getNewItem(key, persistedItem, newValue) {
|
|
5
7
|
let persist;
|
|
6
8
|
try {
|
|
7
9
|
persist = persistedItem ? JSON.parse(persistedItem) : {};
|
|
8
10
|
}
|
|
9
11
|
catch (err) {
|
|
10
|
-
console.error(err);
|
|
11
|
-
|
|
12
|
+
console.error("use-persisted-state: Can't write value to storage", err);
|
|
13
|
+
throw err;
|
|
12
14
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
if (!(0, is_1.isObject)(persist)) {
|
|
16
|
+
// Neither a primitive nor an array survives being merged into: the value being set would disappear.
|
|
17
|
+
const err = new TypeError('the stored entry is not an object of keys');
|
|
18
|
+
console.error("use-persisted-state: Can't write value to storage", err);
|
|
19
|
+
throw err;
|
|
20
|
+
}
|
|
21
|
+
return JSON.stringify({ ...persist, [key]: newValue });
|
|
16
22
|
}
|
|
17
23
|
//# sourceMappingURL=get-new-item.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-new-item.js","sourceRoot":"","sources":["../../src/utils/get-new-item.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"get-new-item.js","sourceRoot":"","sources":["../../src/utils/get-new-item.ts"],"names":[],"mappings":";;AAGA,6BAqBC;AAxBD,gCAAkC;AAElC,+GAA+G;AAC/G,SAAwB,UAAU,CAAI,GAAW,EAAE,aAAqB,EAAE,QAAW;IACnF,IAAI,OAAgB,CAAA;IAEpB,IAAI,CAAC;QACH,OAAO,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAC1D,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,mDAAmD,EAAE,GAAG,CAAC,CAAA;QAEvE,MAAM,GAAG,CAAA;IACX,CAAC;IAED,IAAI,CAAC,IAAA,aAAQ,EAAC,OAAO,CAAC,EAAE,CAAC;QACvB,oGAAoG;QACpG,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,2CAA2C,CAAC,CAAA;QAEtE,OAAO,CAAC,KAAK,CAAC,mDAAmD,EAAE,GAAG,CAAC,CAAA;QAEvE,MAAM,GAAG,CAAA;IACX,CAAC;IAED,OAAO,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAA;AACxD,CAAC"}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolves the value a hook starts from: the persisted entry for `key` when the stored payload
|
|
3
|
-
* carries one, the initial value otherwise.
|
|
4
|
-
*
|
|
5
|
-
* Presence is decided by the key rather than by the value, so a persisted `null` comes back as
|
|
6
|
-
* the value the user set instead of being mistaken for an absence and replaced.
|
|
7
|
-
*/
|
|
1
|
+
/** Resolves a hook's starting value by key presence, so a persisted `null` is not mistaken for an absence. */
|
|
8
2
|
export default function getPersistedValue<T>(key: string, initialValue: T | (() => T), persist?: string): T;
|
|
9
3
|
//# sourceMappingURL=get-persisted-value.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-persisted-value.d.ts","sourceRoot":"","sources":["../../src/utils/get-persisted-value.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"get-persisted-value.d.ts","sourceRoot":"","sources":["../../src/utils/get-persisted-value.ts"],"names":[],"mappings":"AAEA,8GAA8G;AAC9G,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,CAAC,CAoB1G"}
|
|
@@ -2,29 +2,19 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = getPersistedValue;
|
|
4
4
|
const is_1 = require("@plq/is");
|
|
5
|
-
/**
|
|
6
|
-
* Resolves the value a hook starts from: the persisted entry for `key` when the stored payload
|
|
7
|
-
* carries one, the initial value otherwise.
|
|
8
|
-
*
|
|
9
|
-
* Presence is decided by the key rather than by the value, so a persisted `null` comes back as
|
|
10
|
-
* the value the user set instead of being mistaken for an absence and replaced.
|
|
11
|
-
*/
|
|
5
|
+
/** Resolves a hook's starting value by key presence, so a persisted `null` is not mistaken for an absence. */
|
|
12
6
|
function getPersistedValue(key, initialValue, persist) {
|
|
13
7
|
let initialPersist;
|
|
14
8
|
try {
|
|
15
9
|
initialPersist = persist ? JSON.parse(persist) : {};
|
|
16
10
|
}
|
|
17
11
|
catch (err) {
|
|
18
|
-
// A shared backend can hold a foreign or truncated entry, so a parse failure
|
|
19
|
-
// and must not stop the component mounting. It is reported rather than swallowed because the
|
|
20
|
-
// fallback discards whatever was persisted, and because the change path logs the same failure.
|
|
12
|
+
// A shared backend can hold a foreign or truncated entry, so a parse failure must not stop the mount.
|
|
21
13
|
console.error("use-persisted-state: Can't parse value from storage", err);
|
|
22
14
|
initialPersist = {};
|
|
23
15
|
}
|
|
24
16
|
let initialOrPersistedValue = (0, is_1.isFunction)(initialValue) ? initialValue() : initialValue;
|
|
25
|
-
// `
|
|
26
|
-
// and `in` throws on one. This runs in the `useState` initializer, where a
|
|
27
|
-
// throw stops the component mounting instead of falling back.
|
|
17
|
+
// `in` throws on a primitive, and this runs in the `useState` initializer, where a throw stops the mount.
|
|
28
18
|
if ((0, is_1.isObject)(initialPersist) && key in initialPersist) {
|
|
29
19
|
initialOrPersistedValue = initialPersist[key];
|
|
30
20
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"get-persisted-value.js","sourceRoot":"","sources":["../../src/utils/get-persisted-value.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"get-persisted-value.js","sourceRoot":"","sources":["../../src/utils/get-persisted-value.ts"],"names":[],"mappings":";;AAGA,oCAoBC;AAvBD,gCAA8C;AAE9C,8GAA8G;AAC9G,SAAwB,iBAAiB,CAAI,GAAW,EAAE,YAA2B,EAAE,OAAgB;IACrG,IAAI,cAAuB,CAAA;IAE3B,IAAI,CAAC;QACH,cAAc,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,sGAAsG;QACtG,OAAO,CAAC,KAAK,CAAC,qDAAqD,EAAE,GAAG,CAAC,CAAA;QAEzE,cAAc,GAAG,EAAE,CAAA;IACrB,CAAC;IAED,IAAI,uBAAuB,GAAG,IAAA,eAAU,EAAC,YAAY,CAAC,CAAC,CAAC,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,YAAY,CAAA;IAEtF,0GAA0G;IAC1G,IAAI,IAAA,aAAQ,EAAC,cAAc,CAAC,IAAI,GAAG,IAAI,cAAc,EAAE,CAAC;QACtD,uBAAuB,GAAG,cAAc,CAAC,GAAG,CAAM,CAAA;IACpD,CAAC;IAED,OAAO,uBAAuB,CAAA;AAChC,CAAC"}
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { AsyncStorage } from '../@types/storage';
|
|
2
|
-
/**
|
|
3
|
-
* Narrows a storage to {@link AsyncStorage}, deciding which hook the entry point builds.
|
|
4
|
-
*
|
|
5
|
-
* Inspection never writes: `set` and `remove` are only checked for shape, never invoked, so
|
|
6
|
-
* `get` alone decides. A candidate pairing an async `get` with a sync `set` satisfies neither
|
|
7
|
-
* storage interface, which is why proving the other two members is not worth a write.
|
|
8
|
-
*/
|
|
2
|
+
/** Narrows a storage to {@link AsyncStorage}. Inspecting never writes: `set` and `remove` are never called. */
|
|
9
3
|
export default function isAsyncStorage(storage: unknown): storage is AsyncStorage;
|
|
10
4
|
//# sourceMappingURL=is-async-storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"is-async-storage.d.ts","sourceRoot":"","sources":["../../src/utils/is-async-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"is-async-storage.d.ts","sourceRoot":"","sources":["../../src/utils/is-async-storage.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAWrD,+GAA+G;AAC/G,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,IAAI,YAAY,CAyChF"}
|
|
@@ -2,17 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.default = isAsyncStorage;
|
|
4
4
|
const is_1 = require("@plq/is");
|
|
5
|
-
// `
|
|
6
|
-
// one satisfies nothing the hook then does with it. `@plq/is` classifies async functions as their
|
|
7
|
-
// own type, so `isFunction` rejects them and both checks are needed to admit a callable member.
|
|
5
|
+
// `@plq/is` classifies async functions as their own type, so `isFunction` alone rejects them.
|
|
8
6
|
const isCallableMember = (member) => (0, is_1.isFunction)(member) || (0, is_1.isAsyncFunction)(member);
|
|
9
|
-
/**
|
|
10
|
-
* Narrows a storage to {@link AsyncStorage}, deciding which hook the entry point builds.
|
|
11
|
-
*
|
|
12
|
-
* Inspection never writes: `set` and `remove` are only checked for shape, never invoked, so
|
|
13
|
-
* `get` alone decides. A candidate pairing an async `get` with a sync `set` satisfies neither
|
|
14
|
-
* storage interface, which is why proving the other two members is not worth a write.
|
|
15
|
-
*/
|
|
7
|
+
/** Narrows a storage to {@link AsyncStorage}. Inspecting never writes: `set` and `remove` are never called. */
|
|
16
8
|
function isAsyncStorage(storage) {
|
|
17
9
|
const candidate = storage;
|
|
18
10
|
if (!candidate) {
|
|
@@ -29,31 +21,20 @@ function isAsyncStorage(storage) {
|
|
|
29
21
|
if (!(0, is_1.isFunction)(get)) {
|
|
30
22
|
return false;
|
|
31
23
|
}
|
|
32
|
-
//
|
|
33
|
-
// that apart from a sync get. Any adapter written that way can only be recognised here, and a
|
|
34
|
-
// third-party one is free to be written that way, so this line stays necessary however the
|
|
35
|
-
// shipped adapters happen to declare themselves. A read is the one probe that leaves the
|
|
36
|
-
// inspected storage as it found it.
|
|
24
|
+
// A plain function returning a promise is a valid `AsyncStorage`, and only a call tells it from a sync get.
|
|
37
25
|
let probe;
|
|
38
26
|
try {
|
|
39
|
-
// Called as a method
|
|
40
|
-
// object of methods reads its own state through `this`, and probing the
|
|
41
|
-
// extracted function would throw on a storage that is perfectly valid.
|
|
27
|
+
// Called as a method: an adapter reading its own state through `this` would throw on a torn-off function.
|
|
42
28
|
probe = get.call(candidate, '');
|
|
43
29
|
}
|
|
44
30
|
catch {
|
|
45
|
-
//
|
|
46
|
-
// a throw takes the import down instead of reaching a component. A storage
|
|
47
|
-
// whose read fails is not provably async, and the sync path raises the same
|
|
48
|
-
// failure at the first read, where it can be handled.
|
|
31
|
+
// Deferred, not swallowed: factories are built at module scope, where a throw takes the import down.
|
|
49
32
|
return false;
|
|
50
33
|
}
|
|
51
34
|
if (!(0, is_1.isPromise)(probe)) {
|
|
52
35
|
return false;
|
|
53
36
|
}
|
|
54
|
-
//
|
|
55
|
-
// The rejection still has to be claimed: unclaimed, it terminates the process on Node 15+,
|
|
56
|
-
// letting a storage that merely fails a read take the consuming application down with it.
|
|
37
|
+
// An unclaimed rejection terminates the process on Node 15+, so the discarded probe still has to be claimed.
|
|
57
38
|
probe.then(undefined, () => undefined);
|
|
58
39
|
return true;
|
|
59
40
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"is-async-storage.js","sourceRoot":"","sources":["../../src/utils/is-async-storage.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"is-async-storage.js","sourceRoot":"","sources":["../../src/utils/is-async-storage.ts"],"names":[],"mappings":";;AAaA,iCAyCC;AAtDD,gCAAgE;AAShE,8FAA8F;AAC9F,MAAM,gBAAgB,GAAG,CAAC,MAAe,EAAW,EAAE,CAAC,IAAA,eAAU,EAAC,MAAM,CAAC,IAAI,IAAA,oBAAe,EAAC,MAAM,CAAC,CAAA;AAEpG,+GAA+G;AAC/G,SAAwB,cAAc,CAAC,OAAgB;IACrD,MAAM,SAAS,GAAG,OAA8C,CAAA;IAEhE,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,SAAS,CAAA;IAEtC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC;QACxD,OAAO,KAAK,CAAA;IACd,CAAC;IAED,8EAA8E;IAC9E,IAAI,IAAA,oBAAe,EAAC,GAAG,CAAC,EAAE,CAAC;QACzB,OAAO,IAAI,CAAA;IACb,CAAC;IAED,IAAI,CAAC,IAAA,eAAU,EAAC,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,4GAA4G;IAC5G,IAAI,KAAc,CAAA;IAElB,IAAI,CAAC;QACH,0GAA0G;QAC1G,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,CAAC,CAAA;IACjC,CAAC;IAAC,MAAM,CAAC;QACP,qGAAqG;QACrG,OAAO,KAAK,CAAA;IACd,CAAC;IAED,IAAI,CAAC,IAAA,cAAS,EAAC,KAAK,CAAC,EAAE,CAAC;QACtB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,6GAA6G;IAC7G,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAEtC,OAAO,IAAI,CAAA;AACb,CAAC"}
|
|
@@ -6,11 +6,7 @@ export interface StorageEventRoute {
|
|
|
6
6
|
[key: string]: StorageChange;
|
|
7
7
|
}): void;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
10
|
-
* Starts routing DOM storage events to `route`. Every adapter shares one
|
|
11
|
-
* subscription on the global object, so creating adapters nobody listens to
|
|
12
|
-
* costs nothing.
|
|
13
|
-
*/
|
|
9
|
+
/** Starts routing DOM storage events to `route`. Every adapter shares one subscription on the global object. */
|
|
14
10
|
export declare function addRoute(route: StorageEventRoute): void;
|
|
15
11
|
/** Stops routing to `route`, releasing the shared subscription with the last one. */
|
|
16
12
|
export declare function removeRoute(route: StorageEventRoute): void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-event-router.d.ts","sourceRoot":"","sources":["../../src/utils/storage-event-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAA;IAC3B,IAAI,CAAC,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,GAAG,IAAI,CAAA;CACtD;
|
|
1
|
+
{"version":3,"file":"storage-event-router.d.ts","sourceRoot":"","sources":["../../src/utils/storage-event-router.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEtD,yEAAyE;AACzE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,UAAU,CAAC,OAAO,CAAA;IAC3B,IAAI,CAAC,OAAO,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,aAAa,CAAA;KAAE,GAAG,IAAI,CAAA;CACtD;AA6BD,gHAAgH;AAChH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAQvD;AAED,qFAAqF;AACrF,wBAAgB,WAAW,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAQ1D"}
|
|
@@ -14,28 +14,17 @@ function routeStorageEvent(event) {
|
|
|
14
14
|
},
|
|
15
15
|
};
|
|
16
16
|
for (const route of routes) {
|
|
17
|
-
//
|
|
18
|
-
// reach only the adapter of that area. An event a consumer synthesized —
|
|
19
|
-
// the sole way to test cross-tab sync under jsdom, and a common way to
|
|
20
|
-
// notify the writing tab in production — often cannot name one: the
|
|
21
|
-
// StorageEvent constructor rejects a mocked storage there. Those still go
|
|
22
|
-
// everywhere, exactly as they did before areas were told apart.
|
|
17
|
+
// An event a consumer synthesized often cannot name an area, so those still reach every route.
|
|
23
18
|
if (event.storageArea == null || event.storageArea === route.storage) {
|
|
24
19
|
route.fire(changes);
|
|
25
20
|
}
|
|
26
21
|
}
|
|
27
22
|
}
|
|
28
|
-
// A runtime can provide
|
|
29
|
-
// of that API: subscribing where nothing can unsubscribe strands a listener and
|
|
30
|
-
// makes releasing it throw, so both halves are required before taking one.
|
|
23
|
+
// A runtime can provide half the DOM event API, and subscribing where nothing can unsubscribe strands a listener.
|
|
31
24
|
function canSubscribe() {
|
|
32
25
|
return typeof globalThis.addEventListener === 'function' && typeof globalThis.removeEventListener === 'function';
|
|
33
26
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Starts routing DOM storage events to `route`. Every adapter shares one
|
|
36
|
-
* subscription on the global object, so creating adapters nobody listens to
|
|
37
|
-
* costs nothing.
|
|
38
|
-
*/
|
|
27
|
+
/** Starts routing DOM storage events to `route`. Every adapter shares one subscription on the global object. */
|
|
39
28
|
function addRoute(route) {
|
|
40
29
|
routes.add(route);
|
|
41
30
|
if (!subscription && canSubscribe()) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"storage-event-router.js","sourceRoot":"","sources":["../../src/utils/storage-event-router.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"storage-event-router.js","sourceRoot":"","sources":["../../src/utils/storage-event-router.ts"],"names":[],"mappings":";;AAoCA,4BAQC;AAGD,kCAQC;AA/CD,MAAM,MAAM,GAAG,IAAI,GAAG,EAAqB,CAAA;AAE3C,IAAI,YAAY,GAA2C,IAAI,CAAA;AAE/D,SAAS,iBAAiB,CAAC,KAAmB;IAC5C,IAAI,CAAC,KAAK,CAAC,GAAG;QAAE,OAAM;IAEtB,MAAM,OAAO,GAAG;QACd,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE;YACX,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,QAAQ,EAAE,KAAK,CAAC,QAAQ;SACzB;KACF,CAAA;IAED,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,+FAA+F;QAC/F,IAAI,KAAK,CAAC,WAAW,IAAI,IAAI,IAAI,KAAK,CAAC,WAAW,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;YACrE,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACrB,CAAC;IACH,CAAC;AACH,CAAC;AAED,kHAAkH;AAClH,SAAS,YAAY;IACnB,OAAO,OAAO,UAAU,CAAC,gBAAgB,KAAK,UAAU,IAAI,OAAO,UAAU,CAAC,mBAAmB,KAAK,UAAU,CAAA;AAClH,CAAC;AAED,gHAAgH;AAChH,SAAgB,QAAQ,CAAC,KAAwB;IAC/C,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAA;IAEjB,IAAI,CAAC,YAAY,IAAI,YAAY,EAAE,EAAE,CAAC;QACpC,YAAY,GAAG,iBAAiB,CAAA;QAEhC,UAAU,CAAC,gBAAgB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;IACtD,CAAC;AACH,CAAC;AAED,qFAAqF;AACrF,SAAgB,WAAW,CAAC,KAAwB;IAClD,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;IAEpB,IAAI,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,YAAY,EAAE,CAAC;QACtC,UAAU,CAAC,mBAAmB,CAAC,SAAS,EAAE,YAAY,CAAC,CAAA;QAEvD,YAAY,GAAG,IAAI,CAAA;IACrB,CAAC;AACH,CAAC"}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
2
|
import type { AsyncStorage, Storage } from '../@types/storage';
|
|
3
|
-
|
|
3
|
+
/** Records an entry before it is written, because a backend may report the change before the write settles. */
|
|
4
|
+
export declare function recordOwnWrite(pendingOwnWrites: React.RefObject<string[]>, item: string): void;
|
|
5
|
+
export default function useStorageHandler<T>(key: string, storageKey: string, applyValue: (value: T) => void, storage: AsyncStorage | Storage, initialValue: T | (() => T), pendingOwnWrites: React.RefObject<string[]>): void;
|
|
4
6
|
//# sourceMappingURL=use-storage-handler.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-storage-handler.d.ts","sourceRoot":"","sources":["../../src/utils/use-storage-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAiB,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"use-storage-handler.d.ts","sourceRoot":"","sources":["../../src/utils/use-storage-handler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAiB,MAAM,mBAAmB,CAAA;AA8C7E,+GAA+G;AAC/G,wBAAgB,cAAc,CAAC,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAM9F;AAwCD,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,CAAC,EACzC,GAAG,EAAE,MAAM,EACX,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,EAC9B,OAAO,EAAE,YAAY,GAAG,OAAO,EAC/B,YAAY,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAC3B,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,GAC1C,IAAI,CAiBN"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.recordOwnWrite = recordOwnWrite;
|
|
3
4
|
exports.default = useStorageHandler;
|
|
4
5
|
const react_1 = require("react");
|
|
5
6
|
const is_1 = require("@plq/is");
|
|
@@ -12,91 +13,72 @@ function readStoredValue(key, entry) {
|
|
|
12
13
|
console.error("use-persisted-state: Can't parse value from storage", err);
|
|
13
14
|
return { status: 'unavailable' };
|
|
14
15
|
}
|
|
15
|
-
//
|
|
16
|
-
// runs inside the adapter's notify loop, where a throw also cuts off every
|
|
17
|
-
// listener queued behind this one.
|
|
16
|
+
// `in` throws on a primitive, and a throw here cuts off every listener queued behind this one.
|
|
18
17
|
if (!(0, is_1.isObject)(parsed) || !(key in parsed))
|
|
19
18
|
return { status: 'unavailable' };
|
|
20
19
|
return { status: 'stored', value: parsed[key] };
|
|
21
20
|
}
|
|
22
|
-
// Restores the initial value when the whole entry is removed from the storage.
|
|
23
21
|
function applyRemoval(change, itemKey, applyValue, latestInitialValue) {
|
|
24
22
|
if (change.oldValue === null || change.oldValue === undefined)
|
|
25
23
|
return;
|
|
26
24
|
const oldValue = readStoredValue(itemKey, change.oldValue);
|
|
27
25
|
const declaredInitialValue = latestInitialValue.current;
|
|
28
|
-
//
|
|
29
|
-
// produces, so comparing the declaration re-applies the initial value on every
|
|
30
|
-
// removal.
|
|
26
|
+
// Resolved before comparing: a factory is never equal to what it produces.
|
|
31
27
|
const initialValue = (0, is_1.isFunction)(declaredInitialValue) ? declaredInitialValue() : declaredInitialValue;
|
|
32
|
-
//
|
|
33
|
-
// initial value, so the fallback is applied rather than skipped. The entry is
|
|
34
|
-
// gone either way, and stale state left in place is worse than a redundant
|
|
35
|
-
// restore.
|
|
28
|
+
// Restore redundantly rather than leave stale state when the entry's old value cannot be read.
|
|
36
29
|
if (oldValue.status === 'stored' && oldValue.value === initialValue)
|
|
37
30
|
return;
|
|
38
31
|
applyValue(initialValue);
|
|
39
32
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
*
|
|
55
|
-
* Forgetting on the first match keeps a later identical entry a change. Another
|
|
56
|
-
* hook writing the same bytes is suppressed along with it, and nothing is lost:
|
|
57
|
-
* it would have replaced a value with an equal one.
|
|
58
|
-
*/
|
|
59
|
-
function consumeOwnWriteEcho(change, pendingOwnWrite) {
|
|
60
|
-
if (pendingOwnWrite.current === null || change.newValue !== pendingOwnWrite.current)
|
|
33
|
+
// A list, not one slot: a write may still be awaiting its echo when the next one starts.
|
|
34
|
+
// Capped, because a backend that never reports back leaves every record unmatched.
|
|
35
|
+
const MAX_PENDING_OWN_WRITES = 8;
|
|
36
|
+
/** Records an entry before it is written, because a backend may report the change before the write settles. */
|
|
37
|
+
function recordOwnWrite(pendingOwnWrites, item) {
|
|
38
|
+
const pending = pendingOwnWrites.current;
|
|
39
|
+
if (pending.length >= MAX_PENDING_OWN_WRITES)
|
|
40
|
+
pending.shift();
|
|
41
|
+
pending.push(item);
|
|
42
|
+
}
|
|
43
|
+
// Applying a hook's own echo would re-decode the entry and re-render for a value it already holds.
|
|
44
|
+
function consumeOwnWriteEcho(entry, pendingOwnWrites) {
|
|
45
|
+
const matched = pendingOwnWrites.current.indexOf(entry);
|
|
46
|
+
if (matched === -1)
|
|
61
47
|
return false;
|
|
62
|
-
|
|
48
|
+
// A backend reports writes in the order it took them, so a record still unmatched has no echo left.
|
|
49
|
+
pendingOwnWrites.current.splice(0, matched + 1);
|
|
63
50
|
return true;
|
|
64
51
|
}
|
|
65
|
-
|
|
66
|
-
function createStorageHandler(itemKey, storageKey, applyValue, latestInitialValue, pendingOwnWrite) {
|
|
52
|
+
function createStorageHandler(itemKey, storageKey, applyValue, latestInitialValue, pendingOwnWrites) {
|
|
67
53
|
return (changes) => {
|
|
68
54
|
for (const [key, change] of Object.entries(changes)) {
|
|
69
55
|
if (key !== storageKey)
|
|
70
56
|
continue;
|
|
71
|
-
|
|
72
|
-
continue;
|
|
57
|
+
// Ahead of the echo check, which needs an entry to match and a removal has none.
|
|
73
58
|
if (change.newValue === null || change.newValue === undefined) {
|
|
74
59
|
applyRemoval(change, itemKey, applyValue, latestInitialValue);
|
|
75
60
|
continue;
|
|
76
61
|
}
|
|
62
|
+
if (consumeOwnWriteEcho(change.newValue, pendingOwnWrites))
|
|
63
|
+
continue;
|
|
77
64
|
const newValue = readStoredValue(itemKey, change.newValue);
|
|
78
65
|
if (newValue.status === 'stored')
|
|
79
66
|
applyValue(newValue.value);
|
|
80
67
|
}
|
|
81
68
|
};
|
|
82
69
|
}
|
|
83
|
-
function useStorageHandler(key, storageKey, applyValue, storage, initialValue,
|
|
84
|
-
// A
|
|
85
|
-
// key-change path reads, so a caller whose default travels with its data gets
|
|
86
|
-
// that record's default back rather than the mounted one. Tracked through a ref
|
|
87
|
-
// rather than a dependency because an inline object or factory has a new
|
|
88
|
-
// identity every render, which would tear the subscription down and rebuild it;
|
|
89
|
-
// written during render, as the key the hook is rendering for is.
|
|
70
|
+
function useStorageHandler(key, storageKey, applyValue, storage, initialValue, pendingOwnWrites) {
|
|
71
|
+
// A ref, not a dependency: an inline initial value changes identity every render and would churn it.
|
|
90
72
|
const latestInitialValue = (0, react_1.useRef)(initialValue);
|
|
91
73
|
latestInitialValue.current = initialValue;
|
|
92
74
|
(0, react_1.useEffect)(() => {
|
|
93
|
-
const handleStorage = createStorageHandler(key, storageKey, applyValue, latestInitialValue,
|
|
75
|
+
const handleStorage = createStorageHandler(key, storageKey, applyValue, latestInitialValue, pendingOwnWrites);
|
|
94
76
|
storage.onChanged.addListener(handleStorage);
|
|
95
77
|
return () => {
|
|
96
78
|
if (storage.onChanged.hasListener(handleStorage)) {
|
|
97
79
|
storage.onChanged.removeListener(handleStorage);
|
|
98
80
|
}
|
|
99
81
|
};
|
|
100
|
-
}, [key, storage.onChanged, storageKey, applyValue,
|
|
82
|
+
}, [key, storage.onChanged, storageKey, applyValue, pendingOwnWrites]);
|
|
101
83
|
}
|
|
102
84
|
//# sourceMappingURL=use-storage-handler.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-storage-handler.js","sourceRoot":"","sources":["../../src/utils/use-storage-handler.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"use-storage-handler.js","sourceRoot":"","sources":["../../src/utils/use-storage-handler.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type React from 'react'
|
|
2
2
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
3
3
|
|
|
4
|
-
import useStorageHandler from './utils/use-storage-handler'
|
|
4
|
+
import useStorageHandler, { recordOwnWrite } from './utils/use-storage-handler'
|
|
5
5
|
import getNewValue from './utils/get-new-value'
|
|
6
6
|
import getNewItem from './utils/get-new-item'
|
|
7
7
|
import getPersistedValue from './utils/get-persisted-value'
|
|
@@ -15,21 +15,48 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
|
|
|
15
15
|
): [PersistedState, () => Promise<void>] {
|
|
16
16
|
const safeStorageKey = `persisted_state_hook:${storageKey}`
|
|
17
17
|
|
|
18
|
+
// Storing is read-merge-write on one shared entry, so overlapping writers would drop each other's keys.
|
|
19
|
+
let entryWrites: Promise<unknown> = Promise.resolve()
|
|
20
|
+
|
|
18
21
|
const clear = (): Promise<void> => {
|
|
19
|
-
|
|
22
|
+
// In the same chain as writes, or a queued write lands after the removal and restores what was cleared.
|
|
23
|
+
const removal = entryWrites.then(() => storage.remove(safeStorageKey))
|
|
24
|
+
|
|
25
|
+
entryWrites = removal.catch(() => undefined)
|
|
26
|
+
|
|
27
|
+
return removal
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const commitEntry = <T>(key: string, newValue: T, pendingOwnWrites: React.RefObject<string[]>): Promise<void> => {
|
|
31
|
+
const write = entryWrites.then(async () => {
|
|
32
|
+
const persistedItem = await storage.get(safeStorageKey)
|
|
33
|
+
let newItem: string
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
newItem = getNewItem<T>(key, persistedItem[safeStorageKey], newValue)
|
|
37
|
+
} catch {
|
|
38
|
+
// A write replaces the whole entry, so an unreadable one is skipped rather than rebuilt without other keys.
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
recordOwnWrite(pendingOwnWrites, newItem)
|
|
43
|
+
|
|
44
|
+
await storage.set({ [safeStorageKey]: newItem })
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
// The chain must outlive a failed write; the rejection still reaches the caller through `write`.
|
|
48
|
+
entryWrites = write.catch(() => undefined)
|
|
49
|
+
|
|
50
|
+
return write
|
|
20
51
|
}
|
|
21
52
|
|
|
22
53
|
const usePersistedState = <T>(key: string, initialValue: T | (() => T)): UsePersistedState<T> => {
|
|
23
54
|
const [state, setState] = useState<T>(initialValue)
|
|
24
55
|
|
|
25
|
-
//
|
|
26
|
-
// not against the one captured by the render that created it. Reading the
|
|
27
|
-
// render closure collapses updates batched into a single event.
|
|
56
|
+
// Updater functions must resolve against the last applied value, not the render closure's.
|
|
28
57
|
const latestValue = useRef(state)
|
|
29
58
|
|
|
30
|
-
//
|
|
31
|
-
// the caller set one must not revert it, and no signal local to the load can
|
|
32
|
-
// answer this: the write it loses to happens outside the load entirely.
|
|
59
|
+
// A load that settles after the caller set a value must not revert it.
|
|
33
60
|
const hasAppliedValue = useRef(false)
|
|
34
61
|
|
|
35
62
|
const applyValue = useCallback((value: T): void => {
|
|
@@ -39,8 +66,7 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
|
|
|
39
66
|
setState(value)
|
|
40
67
|
}, [])
|
|
41
68
|
|
|
42
|
-
|
|
43
|
-
const pendingOwnWrite = useRef<string | null>(null)
|
|
69
|
+
const pendingOwnWrites = useRef<string[]>([])
|
|
44
70
|
|
|
45
71
|
const setPersistedState = useCallback(
|
|
46
72
|
async (newState: React.SetStateAction<T>): Promise<void> => {
|
|
@@ -48,38 +74,19 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
|
|
|
48
74
|
|
|
49
75
|
applyValue(newValue)
|
|
50
76
|
|
|
51
|
-
|
|
52
|
-
const newItem = getNewItem<T>(key, persistedItem[safeStorageKey], newValue)
|
|
53
|
-
|
|
54
|
-
// Recorded before the write, because the backend may report it before the
|
|
55
|
-
// promise settles.
|
|
56
|
-
pendingOwnWrite.current = newItem
|
|
57
|
-
|
|
58
|
-
await storage.set({ [safeStorageKey]: newItem })
|
|
77
|
+
await commitEntry<T>(key, newValue, pendingOwnWrites)
|
|
59
78
|
},
|
|
60
79
|
[key, applyValue],
|
|
61
80
|
)
|
|
62
81
|
|
|
63
|
-
//
|
|
64
|
-
// first render. Later identities of an inline object must not reload, or the
|
|
65
|
-
// effect re-runs on every render and never settles.
|
|
82
|
+
// The first render's initial value: reloading on a later identity would re-run the effect forever.
|
|
66
83
|
const mountInitialValue = useRef(initialValue)
|
|
67
84
|
|
|
68
|
-
// Subscribed before the load
|
|
69
|
-
|
|
70
|
-
// between the read and the subscription in which a write belongs to neither
|
|
71
|
-
// and is lost. Reading last covers everything written before the subscription
|
|
72
|
-
// began, and the listener covers everything after.
|
|
73
|
-
useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrite)
|
|
85
|
+
// Subscribed before the load: effects run in declaration order, and reading first would lose writes in between.
|
|
86
|
+
useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrites)
|
|
74
87
|
|
|
75
88
|
useEffect(() => {
|
|
76
|
-
//
|
|
77
|
-
// is still the current one, which only the closure it belongs to can answer,
|
|
78
|
-
// and whether a value has been applied meanwhile, which outlives it. A load
|
|
79
|
-
// abandoned by a key change would otherwise read the flag its successor had
|
|
80
|
-
// just set, apply the previous key's value and shut the successor out. The
|
|
81
|
-
// second question is also what makes subscribing first safe: a value the
|
|
82
|
-
// listener delivers while the load is in flight is not overwritten by it.
|
|
89
|
+
// Separate from `hasAppliedValue`: only this closure knows whether its own load is still current.
|
|
83
90
|
let isCancelled = false
|
|
84
91
|
|
|
85
92
|
hasAppliedValue.current = false
|
|
@@ -92,12 +99,7 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
|
|
|
92
99
|
|
|
93
100
|
applyValue(getPersistedValue<T>(key, mountInitialValue.current, persist[safeStorageKey]))
|
|
94
101
|
} catch (err) {
|
|
95
|
-
//
|
|
96
|
-
// extension context. Nothing awaits this load, so an unclaimed
|
|
97
|
-
// rejection terminates the process on Node 15+ and surfaces as an
|
|
98
|
-
// uncaught error in the browser. The initial value is already in
|
|
99
|
-
// state, so the mount stands on it and the failure is reported rather
|
|
100
|
-
// than swallowed, as the synchronous path reports its own.
|
|
102
|
+
// Nothing awaits this load, so an unclaimed rejection would surface as an uncaught error.
|
|
101
103
|
console.error("use-persisted-state: Can't read value from storage", err)
|
|
102
104
|
}
|
|
103
105
|
}
|