@plq/use-persisted-state 1.4.0 → 1.4.1

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 CHANGED
@@ -225,6 +225,20 @@ persisted_state_hook:example → {"count":0}
225
225
 
226
226
  Storage backends only ever see serialized strings. Anything you persist ends up unencrypted in the underlying storage — do not store secrets or sensitive data (see [SECURITY.md](https://github.com/Akurganow/use-persisted-state/blob/main/SECURITY.md)).
227
227
 
228
+ ### An entry the library cannot read
229
+
230
+ A write replaces the factory's whole entry, so there is nothing safe to store when the entry already
231
+ there is a string that will not parse, or parses to something that is not an object — another
232
+ library writing under the same key, or a write cut short. The setter reports the failure on
233
+ `console.error`, keeps the value you set in memory and writes nothing, leaving every other hook's
234
+ key exactly where it is. The factory's `clear` removes the entry and lets writing resume.
235
+
236
+ This reaches as far as the adapter reports. Web storage holds only strings, so anything under the
237
+ key comes back and is checked. Extension storage holds arbitrary JSON, and the bundled adapters
238
+ report a non-string value as absent — the library then reads the entry as empty and its next write
239
+ **replaces that value**. Storing your own data under a `persisted_state_hook:` key in extension
240
+ storage is not safe, whatever its type.
241
+
228
242
  ### Values JSON cannot carry
229
243
 
230
244
  A few values do not survive `JSON.stringify`. This follows from the format rather than from a choice
@@ -1 +1 @@
1
- {"version":3,"file":"create-async-persisted-state.d.ts","sourceRoot":"","sources":["../src/create-async-persisted-state.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,eAAe,CAAA;AAEtE,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,CAAC,SAAS,YAAY,EACtE,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,GACT,CAAC,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CAqGvC"}
1
+ {"version":3,"file":"create-async-persisted-state.d.ts","sourceRoot":"","sources":["../src/create-async-persisted-state.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,eAAe,CAAA;AAEtE,MAAM,CAAC,OAAO,UAAU,yBAAyB,CAAC,CAAC,SAAS,YAAY,EACtE,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,CAAC,GACT,CAAC,cAAc,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,CA+IvC"}
@@ -1,18 +1,90 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.default = createAsyncPersistedState;
7
40
  const react_1 = require("react");
8
- const use_storage_handler_1 = __importDefault(require("./utils/use-storage-handler"));
41
+ const use_storage_handler_1 = __importStar(require("./utils/use-storage-handler"));
9
42
  const get_new_value_1 = __importDefault(require("./utils/get-new-value"));
10
43
  const get_new_item_1 = __importDefault(require("./utils/get-new-item"));
11
44
  const get_persisted_value_1 = __importDefault(require("./utils/get-persisted-value"));
12
45
  function createAsyncPersistedState(storageKey, storage) {
13
46
  const safeStorageKey = `persisted_state_hook:${storageKey}`;
47
+ // Every hook this factory makes lives in one entry, and storing a value means
48
+ // reading that entry, merging one key into it and writing all of it back, with
49
+ // a suspension point on either side of the merge. Left to overlap, a second
50
+ // writer merges into a snapshot taken before the first one landed and stores
51
+ // it: the first writer's key is gone from storage while its value is still on
52
+ // screen, and nothing reports the disagreement. It is the entry, not the hook,
53
+ // that has to be taken one at a time, so the chain belongs to the factory.
54
+ let entryWrites = Promise.resolve();
14
55
  const clear = () => {
15
- return storage.remove(safeStorageKey);
56
+ // Removing the entry changes it as much as storing does, so it takes its turn
57
+ // in the same chain. Outside it, a write already queued lands after the
58
+ // removal and brings back what was cleared - and "clear this data" is the one
59
+ // request that cannot be allowed to half happen.
60
+ const removal = entryWrites.then(() => storage.remove(safeStorageKey));
61
+ entryWrites = removal.catch(() => undefined);
62
+ return removal;
63
+ };
64
+ const commitEntry = (key, newValue, pendingOwnWrites) => {
65
+ const write = entryWrites.then(async () => {
66
+ const persistedItem = await storage.get(safeStorageKey);
67
+ let newItem;
68
+ try {
69
+ newItem = (0, get_new_item_1.default)(key, persistedItem[safeStorageKey], newValue);
70
+ }
71
+ catch {
72
+ // Refused, and reported where it was refused. A write replaces the whole
73
+ // entry, so an entry that cannot be read is one no write can be built on
74
+ // without dropping every other hook's key; skipping leaves the bytes for
75
+ // a repair to reach. The caller keeps what it set, unpersisted.
76
+ return;
77
+ }
78
+ // Recorded before the write, because the backend may report it before the
79
+ // promise settles.
80
+ (0, use_storage_handler_1.recordOwnWrite)(pendingOwnWrites, newItem);
81
+ await storage.set({ [safeStorageKey]: newItem });
82
+ });
83
+ // The chain has to outlive a failed write, or one backend rejection stops
84
+ // every later write on this entry. The rejection itself is not swallowed: it
85
+ // stays on the promise handed back to the caller that asked for the write.
86
+ entryWrites = write.catch(() => undefined);
87
+ return write;
16
88
  };
17
89
  const usePersistedState = (key, initialValue) => {
18
90
  const [state, setState] = (0, react_1.useState)(initialValue);
@@ -29,17 +101,14 @@ function createAsyncPersistedState(storageKey, storage) {
29
101
  hasAppliedValue.current = true;
30
102
  setState(value);
31
103
  }, []);
32
- // The exact entry this hook last wrote and has not yet seen reported back.
33
- const pendingOwnWrite = (0, react_1.useRef)(null);
104
+ // The entries this hook has written and not yet seen reported back.
105
+ const pendingOwnWrites = (0, react_1.useRef)([]);
34
106
  const setPersistedState = (0, react_1.useCallback)(async (newState) => {
35
107
  const newValue = (0, get_new_value_1.default)(newState, latestValue.current);
108
+ // Applied before the write is even queued: the caller sees its value at
109
+ // once, and only the trip to storage waits its turn.
36
110
  applyValue(newValue);
37
- const persistedItem = await storage.get(safeStorageKey);
38
- const newItem = (0, get_new_item_1.default)(key, persistedItem[safeStorageKey], newValue);
39
- // Recorded before the write, because the backend may report it before the
40
- // promise settles.
41
- pendingOwnWrite.current = newItem;
42
- await storage.set({ [safeStorageKey]: newItem });
111
+ await commitEntry(key, newValue, pendingOwnWrites);
43
112
  }, [key, applyValue]);
44
113
  // As in `useState`, the value the load falls back to is the one given on the
45
114
  // first render. Later identities of an inline object must not reload, or the
@@ -50,7 +119,7 @@ function createAsyncPersistedState(storageKey, storage) {
50
119
  // between the read and the subscription in which a write belongs to neither
51
120
  // and is lost. Reading last covers everything written before the subscription
52
121
  // began, and the listener covers everything after.
53
- (0, use_storage_handler_1.default)(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrite);
122
+ (0, use_storage_handler_1.default)(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrites);
54
123
  (0, react_1.useEffect)(() => {
55
124
  // Two separate questions, and one cell cannot hold both: whether this load
56
125
  // is still the current one, which only the closure it belongs to can answer,
@@ -1 +1 @@
1
- {"version":3,"file":"create-async-persisted-state.js","sourceRoot":"","sources":["../src/create-async-persisted-state.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"create-async-persisted-state.js","sourceRoot":"","sources":["../src/create-async-persisted-state.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"create-persisted-state.d.ts","sourceRoot":"","sources":["../src/create-persisted-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,eAAe,CAAA;AAOtE,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,CAAC,cAAc,EAAE,MAAM,IAAI,CAAC,CAgE/G"}
1
+ {"version":3,"file":"create-persisted-state.d.ts","sourceRoot":"","sources":["../src/create-persisted-state.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAC/C,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,eAAe,CAAA;AAOtE,MAAM,CAAC,OAAO,UAAU,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,CAAC,cAAc,EAAE,MAAM,IAAI,CAAC,CA0E/G"}
@@ -1,11 +1,44 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
5
38
  Object.defineProperty(exports, "__esModule", { value: true });
6
39
  exports.default = createPersistedState;
7
40
  const react_1 = require("react");
8
- const use_storage_handler_1 = __importDefault(require("./utils/use-storage-handler"));
41
+ const use_storage_handler_1 = __importStar(require("./utils/use-storage-handler"));
9
42
  const get_new_value_1 = __importDefault(require("./utils/get-new-value"));
10
43
  const get_new_item_1 = __importDefault(require("./utils/get-new-item"));
11
44
  const get_persisted_value_1 = __importDefault(require("./utils/get-persisted-value"));
@@ -38,17 +71,27 @@ function createPersistedState(storageKey, storage) {
38
71
  renderedKey.current = key;
39
72
  applyValue(readPersisted(key, initialValue));
40
73
  }
41
- // The exact entry this hook last wrote and has not yet seen reported back.
42
- const pendingOwnWrite = (0, react_1.useRef)(null);
74
+ // The entries this hook has written and not yet seen reported back.
75
+ const pendingOwnWrites = (0, react_1.useRef)([]);
43
76
  const setPersistedState = (0, react_1.useCallback)((newState) => {
44
77
  const newValue = (0, get_new_value_1.default)(newState, latestValue.current);
45
78
  applyValue(newValue);
46
79
  const persistedItem = storage.get(safeStorageKey)[safeStorageKey];
47
- const newItem = (0, get_new_item_1.default)(key, persistedItem, newValue);
48
- pendingOwnWrite.current = newItem;
80
+ let newItem;
81
+ try {
82
+ newItem = (0, get_new_item_1.default)(key, persistedItem, newValue);
83
+ }
84
+ catch {
85
+ // Refused, and reported where it was refused. A write replaces the whole
86
+ // entry, so an entry that cannot be read is one no write can be built on
87
+ // without dropping every other hook's key; skipping leaves the bytes for
88
+ // a repair to reach. The caller keeps what it set, unpersisted.
89
+ return;
90
+ }
91
+ (0, use_storage_handler_1.recordOwnWrite)(pendingOwnWrites, newItem);
49
92
  storage.set({ [safeStorageKey]: newItem });
50
93
  }, [key, applyValue]);
51
- (0, use_storage_handler_1.default)(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrite);
94
+ (0, use_storage_handler_1.default)(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrites);
52
95
  return [state, setPersistedState];
53
96
  };
54
97
  return [usePersistedState, clear];
@@ -1 +1 @@
1
- {"version":3,"file":"create-persisted-state.js","sourceRoot":"","sources":["../src/create-persisted-state.ts"],"names":[],"mappings":";;;;;AAWA,uCAgEC;AA1ED,iCAAqD;AAKrD,sFAA2D;AAC3D,0EAA+C;AAC/C,wEAA6C;AAC7C,sFAA2D;AAE3D,SAAwB,oBAAoB,CAAC,UAAkB,EAAE,OAAgB;IAC/E,MAAM,cAAc,GAAG,wBAAwB,UAAU,EAAE,CAAA;IAC3D,MAAM,KAAK,GAAG,GAAS,EAAE;QACvB,OAAO,CAAC,MAAM,CAAC,cAAc,CAAC,CAAA;IAChC,CAAC,CAAA;IAED,MAAM,aAAa,GAAG,CAAI,GAAW,EAAE,YAAe,EAAK,EAAE,CAC3D,IAAA,6BAAiB,EAAI,GAAG,EAAE,YAAY,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,cAAc,CAAC,CAAC,CAAA;IAEtF,MAAM,iBAAiB,GAAG,CAAI,GAAW,EAAE,YAAe,EAAwB,EAAE;QAClF,4EAA4E;QAC5E,8EAA8E;QAC9E,gEAAgE;QAChE,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,IAAA,gBAAQ,EAAI,GAAG,EAAE,CAAC,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAAA;QAE7E,4EAA4E;QAC5E,0EAA0E;QAC1E,gEAAgE;QAChE,MAAM,WAAW,GAAG,IAAA,cAAM,EAAC,KAAK,CAAC,CAAA;QAEjC,MAAM,UAAU,GAAG,IAAA,mBAAW,EAAC,CAAC,KAAQ,EAAQ,EAAE;YAChD,WAAW,CAAC,OAAO,GAAG,KAAK,CAAA;YAE3B,QAAQ,CAAC,KAAK,CAAC,CAAA;QACjB,CAAC,EAAE,EAAE,CAAC,CAAA;QAEN,4EAA4E;QAC5E,8EAA8E;QAC9E,8EAA8E;QAC9E,4EAA4E;QAC5E,wCAAwC;QACxC,MAAM,WAAW,GAAG,IAAA,cAAM,EAAC,GAAG,CAAC,CAAA;QAE/B,IAAI,WAAW,CAAC,OAAO,KAAK,GAAG,EAAE,CAAC;YAChC,WAAW,CAAC,OAAO,GAAG,GAAG,CAAA;YAEzB,UAAU,CAAC,aAAa,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAAA;QAC9C,CAAC;QAED,2EAA2E;QAC3E,MAAM,eAAe,GAAG,IAAA,cAAM,EAAgB,IAAI,CAAC,CAAA;QAEnD,MAAM,iBAAiB,GAAG,IAAA,mBAAW,EACnC,CAAC,QAAiC,EAAQ,EAAE;YAC1C,MAAM,QAAQ,GAAG,IAAA,uBAAW,EAAI,QAAQ,EAAE,WAAW,CAAC,OAAO,CAAC,CAAA;YAE9D,UAAU,CAAC,QAAQ,CAAC,CAAA;YAEpB,MAAM,aAAa,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,cAAc,CAAC,CAAA;YACjE,MAAM,OAAO,GAAG,IAAA,sBAAU,EAAI,GAAG,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAA;YAE3D,eAAe,CAAC,OAAO,GAAG,OAAO,CAAA;YAEjC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,cAAc,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;QAC5C,CAAC,EACD,CAAC,GAAG,EAAE,UAAU,CAAC,CAClB,CAAA;QAED,IAAA,6BAAiB,EAAI,GAAG,EAAE,cAAc,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,eAAe,CAAC,CAAA;QAE7F,OAAO,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAA;IACnC,CAAC,CAAA;IAED,OAAO,CAAC,iBAAiB,EAAE,KAAK,CAAC,CAAA;AACnC,CAAC"}
1
+ {"version":3,"file":"create-persisted-state.js","sourceRoot":"","sources":["../src/create-persisted-state.ts"],"names":[],"mappings":"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"}
@@ -1,2 +1,14 @@
1
- export default function <T>(key: string, persistedItem: string, newValue: T): string;
1
+ /**
2
+ * Merges `newValue` under `key` into the serialized entry a factory shares between all of its
3
+ * hooks, and returns the entry to store.
4
+ *
5
+ * Reports and throws when the entry cannot be merged into, rather than answering with one built
6
+ * from nothing. What this returns is written over the whole entry, so an unreadable entry is the
7
+ * one case with nothing safe to return: every other hook's key would be replaced with nothing,
8
+ * and the bytes a repair still needs would go with them.
9
+ *
10
+ * @throws {SyntaxError} when the entry is not valid JSON.
11
+ * @throws {TypeError} when the entry is valid JSON but not an object of keys.
12
+ */
13
+ export default function getNewItem<T>(key: string, persistedItem: string, newValue: T): string;
2
14
  //# 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":"AAAA,MAAM,CAAC,OAAO,WAAW,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAenF"}
1
+ {"version":3,"file":"get-new-item.d.ts","sourceRoot":"","sources":["../../src/utils/get-new-item.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,GAAG,MAAM,CAwB7F"}
@@ -1,17 +1,37 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = default_1;
4
- function default_1(key, persistedItem, newValue) {
3
+ exports.default = getNewItem;
4
+ const is_1 = require("@plq/is");
5
+ /**
6
+ * Merges `newValue` under `key` into the serialized entry a factory shares between all of its
7
+ * hooks, and returns the entry to store.
8
+ *
9
+ * Reports and throws when the entry cannot be merged into, rather than answering with one built
10
+ * from nothing. What this returns is written over the whole entry, so an unreadable entry is the
11
+ * one case with nothing safe to return: every other hook's key would be replaced with nothing,
12
+ * and the bytes a repair still needs would go with them.
13
+ *
14
+ * @throws {SyntaxError} when the entry is not valid JSON.
15
+ * @throws {TypeError} when the entry is valid JSON but not an object of keys.
16
+ */
17
+ function getNewItem(key, persistedItem, newValue) {
5
18
  let persist;
6
19
  try {
7
20
  persist = persistedItem ? JSON.parse(persistedItem) : {};
8
21
  }
9
22
  catch (err) {
10
- console.error(err);
11
- persist = {};
23
+ console.error("use-persisted-state: Can't write value to storage", err);
24
+ throw err;
12
25
  }
13
- return JSON.stringify(Object.assign(persist, {
14
- [key]: newValue,
15
- }));
26
+ if (!(0, is_1.isObject)(persist)) {
27
+ // A shared backend can leave any JSON under the key, and neither a primitive nor an array
28
+ // survives being merged into: `JSON.stringify` unwraps the first back to itself and keeps the
29
+ // second an array, so the value being set disappears with the property added to it. A stored
30
+ // `null` did not even get that far - it threw out of `Object.assign` and into the caller.
31
+ const err = new TypeError('the stored entry is not an object of keys');
32
+ console.error("use-persisted-state: Can't write value to storage", err);
33
+ throw err;
34
+ }
35
+ return JSON.stringify({ ...persist, [key]: newValue });
16
36
  }
17
37
  //# 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":";;AAAA,4BAeC;AAfD,mBAA4B,GAAW,EAAE,aAAqB,EAAE,QAAW;IACzE,IAAI,OAAiC,CAAA;IAErC,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,GAAG,CAAC,CAAA;QAClB,OAAO,GAAG,EAAE,CAAA;IACd,CAAC;IAED,OAAO,IAAI,CAAC,SAAS,CACnB,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE;QACrB,CAAC,GAAG,CAAC,EAAE,QAAQ;KAChB,CAAC,CACH,CAAA;AACH,CAAC"}
1
+ {"version":3,"file":"get-new-item.js","sourceRoot":"","sources":["../../src/utils/get-new-item.ts"],"names":[],"mappings":";;AAcA,6BAwBC;AAtCD,gCAAkC;AAElC;;;;;;;;;;;GAWG;AACH,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,0FAA0F;QAC1F,8FAA8F;QAC9F,6FAA6F;QAC7F,0FAA0F;QAC1F,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,4 +1,15 @@
1
1
  import type React from 'react';
2
2
  import type { AsyncStorage, Storage } from '../@types/storage';
3
- export default function useStorageHandler<T>(key: string, storageKey: string, applyValue: (value: T) => void, storage: AsyncStorage | Storage, initialValue: T | (() => T), pendingOwnWrite: React.RefObject<string | null>): void;
3
+ /**
4
+ * Remembers an entry the hook is about to write, so the change the backend
5
+ * reports for it can be told apart from someone else's write.
6
+ *
7
+ * Recorded before the write, because a backend may report it before the call
8
+ * settles. One slot was not enough: a backend free to report after its write
9
+ * settles - chrome and browser storage both are - lets the next write overwrite
10
+ * the record of a write still waiting to be reported, and that unsuppressed echo
11
+ * then puts the earlier value back over the later one.
12
+ */
13
+ export declare function recordOwnWrite(pendingOwnWrites: React.RefObject<string[]>, item: string): void;
14
+ 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
15
  //# 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;AAiH7E,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,eAAe,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,IAAI,CAAC,GAC9C,IAAI,CAsBN"}
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;AAkE7E;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,gBAAgB,EAAE,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAM9F;AA+DD,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,CAsBN"}
@@ -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");
@@ -37,13 +38,34 @@ function applyRemoval(change, itemKey, applyValue, latestInitialValue) {
37
38
  return;
38
39
  applyValue(initialValue);
39
40
  }
41
+ // A backend that reports nothing back leaves every record unmatched, so the list
42
+ // needs a ceiling or it grows by one string per write for as long as the hook is
43
+ // mounted. A bound, not a tuning: a backend that does report drains the list on
44
+ // every echo, and one that reports late is what the list exists for.
45
+ const MAX_PENDING_OWN_WRITES = 8;
40
46
  /**
41
- * Reports whether this change is the write the hook itself just made, forgetting
42
- * the record when it is. A backend reports a write to every listener, the one
43
- * that made it included, and applying that echo is the storage-to-state re-sync
44
- * this hook was rebuilt without, arriving by another road: it decodes the entry
45
- * again and hands the caller an equal value with a new identity, plus a render
46
- * for a value it already holds.
47
+ * Remembers an entry the hook is about to write, so the change the backend
48
+ * reports for it can be told apart from someone else's write.
49
+ *
50
+ * Recorded before the write, because a backend may report it before the call
51
+ * settles. One slot was not enough: a backend free to report after its write
52
+ * settles - chrome and browser storage both are - lets the next write overwrite
53
+ * the record of a write still waiting to be reported, and that unsuppressed echo
54
+ * then puts the earlier value back over the later one.
55
+ */
56
+ function recordOwnWrite(pendingOwnWrites, item) {
57
+ const pending = pendingOwnWrites.current;
58
+ if (pending.length >= MAX_PENDING_OWN_WRITES)
59
+ pending.shift();
60
+ pending.push(item);
61
+ }
62
+ /**
63
+ * Reports whether this change is a write the hook itself made, forgetting the
64
+ * record when it is. A backend reports a write to every listener, the one that
65
+ * made it included, and applying that echo is the storage-to-state re-sync this
66
+ * hook was rebuilt without, arriving by another road: it decodes the entry again
67
+ * and hands the caller an equal value with a new identity, plus a render for a
68
+ * value it already holds.
47
69
  *
48
70
  * The price, accepted knowingly: for a value JSON cannot carry, the writer keeps
49
71
  * what it set - NaN - while every other component on the key decodes the null
@@ -56,31 +78,38 @@ function applyRemoval(change, itemKey, applyValue, latestInitialValue) {
56
78
  * hook writing the same bytes is suppressed along with it, and nothing is lost:
57
79
  * it would have replaced a value with an equal one.
58
80
  */
59
- function consumeOwnWriteEcho(change, pendingOwnWrite) {
60
- if (pendingOwnWrite.current === null || change.newValue !== pendingOwnWrite.current)
81
+ function consumeOwnWriteEcho(entry, pendingOwnWrites) {
82
+ const matched = pendingOwnWrites.current.indexOf(entry);
83
+ if (matched === -1)
61
84
  return false;
62
- pendingOwnWrite.current = null;
85
+ // Everything recorded before the reported entry goes with it: a backend applies
86
+ // writes in the order it took them and reports them in that order too, so a
87
+ // record still unmatched by now has no echo left to wait for.
88
+ pendingOwnWrites.current.splice(0, matched + 1);
63
89
  return true;
64
90
  }
65
91
  // Builds the change handler. Not a hook, despite living next to one.
66
- function createStorageHandler(itemKey, storageKey, applyValue, latestInitialValue, pendingOwnWrite) {
92
+ function createStorageHandler(itemKey, storageKey, applyValue, latestInitialValue, pendingOwnWrites) {
67
93
  return (changes) => {
68
94
  for (const [key, change] of Object.entries(changes)) {
69
95
  if (key !== storageKey)
70
96
  continue;
71
- if (consumeOwnWriteEcho(change, pendingOwnWrite))
72
- continue;
97
+ // Ahead of the echo check, which needs an entry to match and a removal has
98
+ // none. A removal is never one of this hook's own writes anyway: only the
99
+ // entries it stores are recorded.
73
100
  if (change.newValue === null || change.newValue === undefined) {
74
101
  applyRemoval(change, itemKey, applyValue, latestInitialValue);
75
102
  continue;
76
103
  }
104
+ if (consumeOwnWriteEcho(change.newValue, pendingOwnWrites))
105
+ continue;
77
106
  const newValue = readStoredValue(itemKey, change.newValue);
78
107
  if (newValue.status === 'stored')
79
108
  applyValue(newValue.value);
80
109
  }
81
110
  };
82
111
  }
83
- function useStorageHandler(key, storageKey, applyValue, storage, initialValue, pendingOwnWrite) {
112
+ function useStorageHandler(key, storageKey, applyValue, storage, initialValue, pendingOwnWrites) {
84
113
  // A removal restores the initial value the hook holds now, the same one the
85
114
  // key-change path reads, so a caller whose default travels with its data gets
86
115
  // that record's default back rather than the mounted one. Tracked through a ref
@@ -90,13 +119,13 @@ function useStorageHandler(key, storageKey, applyValue, storage, initialValue, p
90
119
  const latestInitialValue = (0, react_1.useRef)(initialValue);
91
120
  latestInitialValue.current = initialValue;
92
121
  (0, react_1.useEffect)(() => {
93
- const handleStorage = createStorageHandler(key, storageKey, applyValue, latestInitialValue, pendingOwnWrite);
122
+ const handleStorage = createStorageHandler(key, storageKey, applyValue, latestInitialValue, pendingOwnWrites);
94
123
  storage.onChanged.addListener(handleStorage);
95
124
  return () => {
96
125
  if (storage.onChanged.hasListener(handleStorage)) {
97
126
  storage.onChanged.removeListener(handleStorage);
98
127
  }
99
128
  };
100
- }, [key, storage.onChanged, storageKey, applyValue, pendingOwnWrite]);
129
+ }, [key, storage.onChanged, storageKey, applyValue, pendingOwnWrites]);
101
130
  }
102
131
  //# 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":"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"}
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@plq/use-persisted-state",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "useState hook with persistence in storage",
5
5
  "type": "commonjs",
6
6
  "main": "lib/index.js",
@@ -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,8 +15,55 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
15
15
  ): [PersistedState, () => Promise<void>] {
16
16
  const safeStorageKey = `persisted_state_hook:${storageKey}`
17
17
 
18
+ // Every hook this factory makes lives in one entry, and storing a value means
19
+ // reading that entry, merging one key into it and writing all of it back, with
20
+ // a suspension point on either side of the merge. Left to overlap, a second
21
+ // writer merges into a snapshot taken before the first one landed and stores
22
+ // it: the first writer's key is gone from storage while its value is still on
23
+ // screen, and nothing reports the disagreement. It is the entry, not the hook,
24
+ // that has to be taken one at a time, so the chain belongs to the factory.
25
+ let entryWrites: Promise<unknown> = Promise.resolve()
26
+
18
27
  const clear = (): Promise<void> => {
19
- return storage.remove(safeStorageKey)
28
+ // Removing the entry changes it as much as storing does, so it takes its turn
29
+ // in the same chain. Outside it, a write already queued lands after the
30
+ // removal and brings back what was cleared - and "clear this data" is the one
31
+ // request that cannot be allowed to half happen.
32
+ const removal = entryWrites.then(() => storage.remove(safeStorageKey))
33
+
34
+ entryWrites = removal.catch(() => undefined)
35
+
36
+ return removal
37
+ }
38
+
39
+ const commitEntry = <T>(key: string, newValue: T, pendingOwnWrites: React.RefObject<string[]>): Promise<void> => {
40
+ const write = entryWrites.then(async () => {
41
+ const persistedItem = await storage.get(safeStorageKey)
42
+ let newItem: string
43
+
44
+ try {
45
+ newItem = getNewItem<T>(key, persistedItem[safeStorageKey], newValue)
46
+ } catch {
47
+ // Refused, and reported where it was refused. A write replaces the whole
48
+ // entry, so an entry that cannot be read is one no write can be built on
49
+ // without dropping every other hook's key; skipping leaves the bytes for
50
+ // a repair to reach. The caller keeps what it set, unpersisted.
51
+ return
52
+ }
53
+
54
+ // Recorded before the write, because the backend may report it before the
55
+ // promise settles.
56
+ recordOwnWrite(pendingOwnWrites, newItem)
57
+
58
+ await storage.set({ [safeStorageKey]: newItem })
59
+ })
60
+
61
+ // The chain has to outlive a failed write, or one backend rejection stops
62
+ // every later write on this entry. The rejection itself is not swallowed: it
63
+ // stays on the promise handed back to the caller that asked for the write.
64
+ entryWrites = write.catch(() => undefined)
65
+
66
+ return write
20
67
  }
21
68
 
22
69
  const usePersistedState = <T>(key: string, initialValue: T | (() => T)): UsePersistedState<T> => {
@@ -39,23 +86,18 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
39
86
  setState(value)
40
87
  }, [])
41
88
 
42
- // The exact entry this hook last wrote and has not yet seen reported back.
43
- const pendingOwnWrite = useRef<string | null>(null)
89
+ // The entries this hook has written and not yet seen reported back.
90
+ const pendingOwnWrites = useRef<string[]>([])
44
91
 
45
92
  const setPersistedState = useCallback(
46
93
  async (newState: React.SetStateAction<T>): Promise<void> => {
47
94
  const newValue = getNewValue<T>(newState, latestValue.current)
48
95
 
96
+ // Applied before the write is even queued: the caller sees its value at
97
+ // once, and only the trip to storage waits its turn.
49
98
  applyValue(newValue)
50
99
 
51
- const persistedItem = await storage.get(safeStorageKey)
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 })
100
+ await commitEntry<T>(key, newValue, pendingOwnWrites)
59
101
  },
60
102
  [key, applyValue],
61
103
  )
@@ -70,7 +112,7 @@ export default function createAsyncPersistedState<S extends AsyncStorage>(
70
112
  // between the read and the subscription in which a write belongs to neither
71
113
  // and is lost. Reading last covers everything written before the subscription
72
114
  // began, and the listener covers everything after.
73
- useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrite)
115
+ useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrites)
74
116
 
75
117
  useEffect(() => {
76
118
  // Two separate questions, and one cell cannot hold both: whether this load
@@ -4,7 +4,7 @@ import { useCallback, useRef, useState } from 'react'
4
4
  import type { Storage } from './@types/storage'
5
5
  import type { PersistedState, UsePersistedState } from './@types/hook'
6
6
 
7
- import useStorageHandler from './utils/use-storage-handler'
7
+ import useStorageHandler, { recordOwnWrite } from './utils/use-storage-handler'
8
8
  import getNewValue from './utils/get-new-value'
9
9
  import getNewItem from './utils/get-new-item'
10
10
  import getPersistedValue from './utils/get-persisted-value'
@@ -48,8 +48,8 @@ export default function createPersistedState(storageKey: string, storage: Storag
48
48
  applyValue(readPersisted(key, initialValue))
49
49
  }
50
50
 
51
- // The exact entry this hook last wrote and has not yet seen reported back.
52
- const pendingOwnWrite = useRef<string | null>(null)
51
+ // The entries this hook has written and not yet seen reported back.
52
+ const pendingOwnWrites = useRef<string[]>([])
53
53
 
54
54
  const setPersistedState = useCallback(
55
55
  (newState: React.SetStateAction<T>): void => {
@@ -58,16 +58,26 @@ export default function createPersistedState(storageKey: string, storage: Storag
58
58
  applyValue(newValue)
59
59
 
60
60
  const persistedItem = storage.get(safeStorageKey)[safeStorageKey]
61
- const newItem = getNewItem<T>(key, persistedItem, newValue)
61
+ let newItem: string
62
62
 
63
- pendingOwnWrite.current = newItem
63
+ try {
64
+ newItem = getNewItem<T>(key, persistedItem, newValue)
65
+ } catch {
66
+ // Refused, and reported where it was refused. A write replaces the whole
67
+ // entry, so an entry that cannot be read is one no write can be built on
68
+ // without dropping every other hook's key; skipping leaves the bytes for
69
+ // a repair to reach. The caller keeps what it set, unpersisted.
70
+ return
71
+ }
72
+
73
+ recordOwnWrite(pendingOwnWrites, newItem)
64
74
 
65
75
  storage.set({ [safeStorageKey]: newItem })
66
76
  },
67
77
  [key, applyValue],
68
78
  )
69
79
 
70
- useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrite)
80
+ useStorageHandler<T>(key, safeStorageKey, applyValue, storage, initialValue, pendingOwnWrites)
71
81
 
72
82
  return [state, setPersistedState]
73
83
  }
@@ -1,16 +1,39 @@
1
- export default function <T>(key: string, persistedItem: string, newValue: T): string {
2
- let persist: { [x: string]: unknown }
1
+ import { isObject } from '@plq/is'
2
+
3
+ /**
4
+ * Merges `newValue` under `key` into the serialized entry a factory shares between all of its
5
+ * hooks, and returns the entry to store.
6
+ *
7
+ * Reports and throws when the entry cannot be merged into, rather than answering with one built
8
+ * from nothing. What this returns is written over the whole entry, so an unreadable entry is the
9
+ * one case with nothing safe to return: every other hook's key would be replaced with nothing,
10
+ * and the bytes a repair still needs would go with them.
11
+ *
12
+ * @throws {SyntaxError} when the entry is not valid JSON.
13
+ * @throws {TypeError} when the entry is valid JSON but not an object of keys.
14
+ */
15
+ export default function getNewItem<T>(key: string, persistedItem: string, newValue: T): string {
16
+ let persist: unknown
3
17
 
4
18
  try {
5
19
  persist = persistedItem ? JSON.parse(persistedItem) : {}
6
20
  } catch (err) {
7
- console.error(err)
8
- persist = {}
21
+ console.error("use-persisted-state: Can't write value to storage", err)
22
+
23
+ throw err
24
+ }
25
+
26
+ if (!isObject(persist)) {
27
+ // A shared backend can leave any JSON under the key, and neither a primitive nor an array
28
+ // survives being merged into: `JSON.stringify` unwraps the first back to itself and keeps the
29
+ // second an array, so the value being set disappears with the property added to it. A stored
30
+ // `null` did not even get that far - it threw out of `Object.assign` and into the caller.
31
+ const err = new TypeError('the stored entry is not an object of keys')
32
+
33
+ console.error("use-persisted-state: Can't write value to storage", err)
34
+
35
+ throw err
9
36
  }
10
37
 
11
- return JSON.stringify(
12
- Object.assign(persist, {
13
- [key]: newValue,
14
- }),
15
- )
38
+ return JSON.stringify({ ...persist, [key]: newValue })
16
39
  }
@@ -60,13 +60,37 @@ function applyRemoval<T>(
60
60
  applyValue(initialValue)
61
61
  }
62
62
 
63
+ // A backend that reports nothing back leaves every record unmatched, so the list
64
+ // needs a ceiling or it grows by one string per write for as long as the hook is
65
+ // mounted. A bound, not a tuning: a backend that does report drains the list on
66
+ // every echo, and one that reports late is what the list exists for.
67
+ const MAX_PENDING_OWN_WRITES = 8
68
+
63
69
  /**
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
+ * Remembers an entry the hook is about to write, so the change the backend
71
+ * reports for it can be told apart from someone else's write.
72
+ *
73
+ * Recorded before the write, because a backend may report it before the call
74
+ * settles. One slot was not enough: a backend free to report after its write
75
+ * settles - chrome and browser storage both are - lets the next write overwrite
76
+ * the record of a write still waiting to be reported, and that unsuppressed echo
77
+ * then puts the earlier value back over the later one.
78
+ */
79
+ export function recordOwnWrite(pendingOwnWrites: React.RefObject<string[]>, item: string): void {
80
+ const pending = pendingOwnWrites.current
81
+
82
+ if (pending.length >= MAX_PENDING_OWN_WRITES) pending.shift()
83
+
84
+ pending.push(item)
85
+ }
86
+
87
+ /**
88
+ * Reports whether this change is a write the hook itself made, forgetting the
89
+ * record when it is. A backend reports a write to every listener, the one that
90
+ * made it included, and applying that echo is the storage-to-state re-sync this
91
+ * hook was rebuilt without, arriving by another road: it decodes the entry again
92
+ * and hands the caller an equal value with a new identity, plus a render for a
93
+ * value it already holds.
70
94
  *
71
95
  * The price, accepted knowingly: for a value JSON cannot carry, the writer keeps
72
96
  * what it set - NaN - while every other component on the key decodes the null
@@ -79,10 +103,15 @@ function applyRemoval<T>(
79
103
  * hook writing the same bytes is suppressed along with it, and nothing is lost:
80
104
  * it would have replaced a value with an equal one.
81
105
  */
82
- function consumeOwnWriteEcho(change: StorageChange, pendingOwnWrite: React.RefObject<string | null>): boolean {
83
- if (pendingOwnWrite.current === null || change.newValue !== pendingOwnWrite.current) return false
106
+ function consumeOwnWriteEcho(entry: string, pendingOwnWrites: React.RefObject<string[]>): boolean {
107
+ const matched = pendingOwnWrites.current.indexOf(entry)
108
+
109
+ if (matched === -1) return false
84
110
 
85
- pendingOwnWrite.current = null
111
+ // Everything recorded before the reported entry goes with it: a backend applies
112
+ // writes in the order it took them and reports them in that order too, so a
113
+ // record still unmatched by now has no echo left to wait for.
114
+ pendingOwnWrites.current.splice(0, matched + 1)
86
115
 
87
116
  return true
88
117
  }
@@ -93,19 +122,22 @@ function createStorageHandler<T>(
93
122
  storageKey: string,
94
123
  applyValue: (value: T) => void,
95
124
  latestInitialValue: React.RefObject<T | (() => T)>,
96
- pendingOwnWrite: React.RefObject<string | null>,
125
+ pendingOwnWrites: React.RefObject<string[]>,
97
126
  ) {
98
127
  return (changes: { [key: string]: StorageChange }): void => {
99
128
  for (const [key, change] of Object.entries(changes)) {
100
129
  if (key !== storageKey) continue
101
130
 
102
- if (consumeOwnWriteEcho(change, pendingOwnWrite)) continue
103
-
131
+ // Ahead of the echo check, which needs an entry to match and a removal has
132
+ // none. A removal is never one of this hook's own writes anyway: only the
133
+ // entries it stores are recorded.
104
134
  if (change.newValue === null || change.newValue === undefined) {
105
135
  applyRemoval<T>(change, itemKey, applyValue, latestInitialValue)
106
136
  continue
107
137
  }
108
138
 
139
+ if (consumeOwnWriteEcho(change.newValue, pendingOwnWrites)) continue
140
+
109
141
  const newValue = readStoredValue<T>(itemKey, change.newValue)
110
142
 
111
143
  if (newValue.status === 'stored') applyValue(newValue.value)
@@ -119,7 +151,7 @@ export default function useStorageHandler<T>(
119
151
  applyValue: (value: T) => void,
120
152
  storage: AsyncStorage | Storage,
121
153
  initialValue: T | (() => T),
122
- pendingOwnWrite: React.RefObject<string | null>,
154
+ pendingOwnWrites: React.RefObject<string[]>,
123
155
  ): void {
124
156
  // A removal restores the initial value the hook holds now, the same one the
125
157
  // key-change path reads, so a caller whose default travels with its data gets
@@ -132,7 +164,7 @@ export default function useStorageHandler<T>(
132
164
  latestInitialValue.current = initialValue
133
165
 
134
166
  useEffect(() => {
135
- const handleStorage = createStorageHandler<T>(key, storageKey, applyValue, latestInitialValue, pendingOwnWrite)
167
+ const handleStorage = createStorageHandler<T>(key, storageKey, applyValue, latestInitialValue, pendingOwnWrites)
136
168
 
137
169
  storage.onChanged.addListener(handleStorage)
138
170
 
@@ -141,5 +173,5 @@ export default function useStorageHandler<T>(
141
173
  storage.onChanged.removeListener(handleStorage)
142
174
  }
143
175
  }
144
- }, [key, storage.onChanged, storageKey, applyValue, pendingOwnWrite])
176
+ }, [key, storage.onChanged, storageKey, applyValue, pendingOwnWrites])
145
177
  }