react-native-onyx 3.0.118 → 3.0.120

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/API.md CHANGED
@@ -63,6 +63,19 @@ Any existing collection members not included in the new data will be removed.</p
63
63
  Live RAM-only values and writes that have not reached storage are not included.
64
64
  Treat the returned object and its nested values as read-only.</p>
65
65
  </dd>
66
+ <dt><a href="#get">get(key)</a> ⇒</dt>
67
+ <dd><p>Reads the current value of an Onyx key once, without subscribing. Use <code>useOnyx()</code> or
68
+ <code>Onyx.connectWithoutView()</code> when the value has to stay current.</p>
69
+ <p>The read is served from the cache, which <code>init()</code> fills with the whole store, so it returns what a
70
+ subscription to the same key would deliver. The value is the cached one rather than a copy, so treat
71
+ it as read-only.</p>
72
+ </dd>
73
+ <dt><a href="#multiGet">multiGet(keys)</a> ⇒</dt>
74
+ <dd><p>Reads several Onyx keys at once, without subscribing. Use <code>useOnyx()</code> or <code>Onyx.connectWithoutView()</code> when
75
+ the values have to stay current.</p>
76
+ <p>Values come back in the order of the keys given, and each is what get() returns for its key. To read a
77
+ whole collection, pass the collection key rather than listing its members.</p>
78
+ </dd>
66
79
  </dl>
67
80
 
68
81
  <a name="init"></a>
@@ -280,3 +293,49 @@ Live RAM-only values and writes that have not reached storage are not included.
280
293
  Treat the returned object and its nested values as read-only.
281
294
 
282
295
  **Kind**: global function
296
+ <a name="get"></a>
297
+
298
+ ## get(key) ⇒
299
+ Reads the current value of an Onyx key once, without subscribing. Use `useOnyx()` or
300
+ `Onyx.connectWithoutView()` when the value has to stay current.
301
+
302
+ The read is served from the cache, which `init()` fills with the whole store, so it returns what a
303
+ subscription to the same key would deliver. The value is the cached one rather than a copy, so treat
304
+ it as read-only.
305
+
306
+ **Kind**: global function
307
+ **Returns**: The current value, or `undefined` if the key has none.
308
+
309
+ | Param | Description |
310
+ | --- | --- |
311
+ | key | ONYXKEY to read, either a collection key or a single key |
312
+
313
+ **Example**
314
+ ```js
315
+ const report = await Onyx.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
316
+ const allReports = await Onyx.get(ONYXKEYS.COLLECTION.REPORT);
317
+ ```
318
+ <a name="multiGet"></a>
319
+
320
+ ## multiGet(keys) ⇒
321
+ Reads several Onyx keys at once, without subscribing. Use `useOnyx()` or `Onyx.connectWithoutView()` when
322
+ the values have to stay current.
323
+
324
+ Values come back in the order of the keys given, and each is what get() returns for its key. To read a
325
+ whole collection, pass the collection key rather than listing its members.
326
+
327
+ **Kind**: global function
328
+ **Returns**: The values in the order of their keys, each `undefined` where a key has no value.
329
+
330
+ | Param | Description |
331
+ | --- | --- |
332
+ | keys | ONYXKEYS to read, in any mix of collection keys and single keys |
333
+
334
+ **Example**
335
+ ```js
336
+ const [session, wallet, report] = await Onyx.multiGet([
337
+ ONYXKEYS.SESSION,
338
+ ONYXKEYS.WALLET,
339
+ `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
340
+ ]);
341
+ ```
package/dist/Onyx.d.ts CHANGED
@@ -161,6 +161,42 @@ declare function setCollection<TKey extends CollectionKeyBase>(collectionKey: TK
161
161
  * Treat the returned object and its nested values as read-only.
162
162
  */
163
163
  declare function exportState(): Promise<Record<OnyxKey, OnyxValue<OnyxKey>>>;
164
+ /**
165
+ * Reads the current value of an Onyx key once, without subscribing. Use `useOnyx()` or
166
+ * `Onyx.connectWithoutView()` when the value has to stay current.
167
+ *
168
+ * The read is served from the cache, which `init()` fills with the whole store, so it returns what a
169
+ * subscription to the same key would deliver. The value is the cached one rather than a copy, so treat
170
+ * it as read-only.
171
+ *
172
+ * @example
173
+ * const report = await Onyx.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
174
+ * const allReports = await Onyx.get(ONYXKEYS.COLLECTION.REPORT);
175
+ *
176
+ * @param key ONYXKEY to read, either a collection key or a single key
177
+ * @returns The current value, or `undefined` if the key has none.
178
+ */
179
+ declare function get<TKey extends OnyxKey>(key: TKey): Promise<OnyxValue<TKey>>;
180
+ /**
181
+ * Reads several Onyx keys at once, without subscribing. Use `useOnyx()` or `Onyx.connectWithoutView()` when
182
+ * the values have to stay current.
183
+ *
184
+ * Values come back in the order of the keys given, and each is what get() returns for its key. To read a
185
+ * whole collection, pass the collection key rather than listing its members.
186
+ *
187
+ * @example
188
+ * const [session, wallet, report] = await Onyx.multiGet([
189
+ * ONYXKEYS.SESSION,
190
+ * ONYXKEYS.WALLET,
191
+ * `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
192
+ * ]);
193
+ *
194
+ * @param keys ONYXKEYS to read, in any mix of collection keys and single keys
195
+ * @returns The values in the order of their keys, each `undefined` where a key has no value.
196
+ */
197
+ declare function multiGet<const Keys extends readonly OnyxKey[]>(keys: Keys): Promise<{
198
+ [Index in keyof Keys]: OnyxValue<Keys[Index]>;
199
+ }>;
164
200
  declare const Onyx: {
165
201
  METHOD: {
166
202
  readonly SET: "set";
@@ -182,6 +218,8 @@ declare const Onyx: {
182
218
  clear: typeof clear;
183
219
  exportState: typeof exportState;
184
220
  init: typeof init;
221
+ get: typeof get;
222
+ multiGet: typeof multiGet;
185
223
  registerLogger: typeof Logger.registerLogger;
186
224
  };
187
225
  export default Onyx;
package/dist/Onyx.js CHANGED
@@ -473,6 +473,7 @@ function clear(keysToPreserve = []) {
473
473
  .then(() => storage_1.default.multiSet(defaultKeyValuePairs))
474
474
  .then(() => {
475
475
  DevTools_1.default.clearState(keysToPreserve);
476
+ OnyxSubscriptionManager_1.default.resetCacheMissReports();
476
477
  // Notify the subscribers for each key/value group so they can receive the new values
477
478
  for (const [key, value] of Object.entries(keyValuesToResetIndividually)) {
478
479
  OnyxUtils_1.default.notifyKey(key, value);
@@ -662,6 +663,45 @@ function exportState() {
662
663
  return state;
663
664
  }));
664
665
  }
666
+ /**
667
+ * Reads the current value of an Onyx key once, without subscribing. Use `useOnyx()` or
668
+ * `Onyx.connectWithoutView()` when the value has to stay current.
669
+ *
670
+ * The read is served from the cache, which `init()` fills with the whole store, so it returns what a
671
+ * subscription to the same key would deliver. The value is the cached one rather than a copy, so treat
672
+ * it as read-only.
673
+ *
674
+ * @example
675
+ * const report = await Onyx.get(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`);
676
+ * const allReports = await Onyx.get(ONYXKEYS.COLLECTION.REPORT);
677
+ *
678
+ * @param key ONYXKEY to read, either a collection key or a single key
679
+ * @returns The current value, or `undefined` if the key has none.
680
+ */
681
+ function get(key) {
682
+ return OnyxUtils_1.default.afterInit(() => { var _a; return Promise.resolve(((_a = OnyxSubscriptionManager_1.default.getState(key)) !== null && _a !== void 0 ? _a : undefined)); });
683
+ }
684
+ /**
685
+ * Reads several Onyx keys at once, without subscribing. Use `useOnyx()` or `Onyx.connectWithoutView()` when
686
+ * the values have to stay current.
687
+ *
688
+ * Values come back in the order of the keys given, and each is what get() returns for its key. To read a
689
+ * whole collection, pass the collection key rather than listing its members.
690
+ *
691
+ * @example
692
+ * const [session, wallet, report] = await Onyx.multiGet([
693
+ * ONYXKEYS.SESSION,
694
+ * ONYXKEYS.WALLET,
695
+ * `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
696
+ * ]);
697
+ *
698
+ * @param keys ONYXKEYS to read, in any mix of collection keys and single keys
699
+ * @returns The values in the order of their keys, each `undefined` where a key has no value.
700
+ */
701
+ function multiGet(keys) {
702
+ // map() widens the key tuple to an array, so the per-slot value types survive only through the cast.
703
+ return OnyxUtils_1.default.afterInit(() => Promise.resolve(keys.map((key) => { var _a; return (_a = OnyxSubscriptionManager_1.default.getState(key)) !== null && _a !== void 0 ? _a : undefined; })));
704
+ }
665
705
  const Onyx = {
666
706
  METHOD: OnyxUtils_1.default.METHOD,
667
707
  connect,
@@ -676,6 +716,8 @@ const Onyx = {
676
716
  clear,
677
717
  exportState,
678
718
  init,
719
+ get,
720
+ multiGet,
679
721
  registerLogger: Logger.registerLogger,
680
722
  };
681
723
  exports.default = Onyx;
@@ -48,6 +48,7 @@ declare class OnyxSubscriptionManager {
48
48
  * Wipe all subscriptions. Used by tests and `Onyx.clear()` follow-on.
49
49
  */
50
50
  clearAll(): void;
51
+ resetCacheMissReports(): void;
51
52
  /**
52
53
  * True if there are any subscribers for the given key (exact or parent collection).
53
54
  */
@@ -39,6 +39,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
39
39
  const Logger = __importStar(require("./Logger"));
40
40
  const OnyxCache_1 = __importDefault(require("./OnyxCache"));
41
41
  const OnyxKeys_1 = __importDefault(require("./OnyxKeys"));
42
+ const storage_1 = __importDefault(require("./storage"));
43
+ const reportedCacheMisses = new Set();
44
+ function reportCacheMissIfStorageHasValue(key) {
45
+ var _a;
46
+ if (OnyxKeys_1.default.isRamOnlyKey(key) || !OnyxCache_1.default.getAllKeys().has(key) || OnyxCache_1.default.hasNullishStorageKey(key)) {
47
+ return;
48
+ }
49
+ const reportKey = (_a = OnyxKeys_1.default.getCollectionKey(key)) !== null && _a !== void 0 ? _a : key;
50
+ if (reportedCacheMisses.has(reportKey)) {
51
+ return;
52
+ }
53
+ reportedCacheMisses.add(reportKey);
54
+ storage_1.default.getItem(key)
55
+ .then((value) => {
56
+ if (value === undefined || value === null) {
57
+ return;
58
+ }
59
+ Logger.logHmmm(`Cache held no value for a key storage still has: ${reportKey}`);
60
+ })
61
+ .catch((error) => {
62
+ Logger.logInfo(`Could not check storage for a suspected cache miss on ${reportKey}. Error: ${String(error)}`);
63
+ });
64
+ }
42
65
  /**
43
66
  * OnyxSubscriptionManager is a registry for Onyx subscriptions.
44
67
  * Subscriptions are stored in `keyListeners`, a flat map keyed by OnyxKey.
@@ -58,7 +81,11 @@ class OnyxSubscriptionManager {
58
81
  if (OnyxKeys_1.default.isCollectionKey(key)) {
59
82
  return OnyxCache_1.default.getCollectionData(key);
60
83
  }
61
- return OnyxCache_1.default.get(key);
84
+ const value = OnyxCache_1.default.get(key);
85
+ if (value === undefined) {
86
+ reportCacheMissIfStorageHasValue(key);
87
+ }
88
+ return value;
62
89
  }
63
90
  /**
64
91
  * Subscribe to an exact key. For a collection root key this is "collection mode": the
@@ -164,6 +191,10 @@ class OnyxSubscriptionManager {
164
191
  */
165
192
  clearAll() {
166
193
  this.keyListeners.clear();
194
+ this.resetCacheMissReports();
195
+ }
196
+ resetCacheMissReports() {
197
+ reportedCacheMisses.clear();
167
198
  }
168
199
  /**
169
200
  * True if there are any subscribers for the given key (exact or parent collection).
@@ -122,16 +122,16 @@ declare function reportStorageQuota(error?: Error): Promise<void>;
122
122
  * The connection layer (createStore) owns connection/transport recovery; this operation layer owns
123
123
  * capacity recovery (eviction) so that a given failure is retried by exactly one layer:
124
124
  * - INVALID_DATA: logs an alert and throws (the same data will always fail).
125
- * - TRANSIENT / FATAL: the connection layer already retried (transient) or exhausted its heal budget
126
- * and alerted (fatal). Retrying here would only re-amplify, so we skip the write quietly.
125
+ * - TRANSIENT / FATAL: the connection layer already retried (transient) or spent a heal attempt
126
+ * (fatal). Retrying here would only re-amplify, so we skip the write quietly.
127
127
  * - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level
128
128
  * circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making
129
129
  * progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
130
130
  * - DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither
131
131
  * retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with
132
132
  * a single throttled alert + quota snapshot per burst.
133
- * - UNAVAILABLE: the storage engine does not exist in this environment, so the storage layer has
134
- * already degraded to the in-memory provider. No retry.
133
+ * - UNAVAILABLE: the storage engine does not exist in this environment or its heal budget ran out, so
134
+ * the storage layer has already degraded to the in-memory provider. No retry.
135
135
  * - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message +
136
136
  * provider) once so it's visible, then bounded retry without eviction.
137
137
  */
package/dist/OnyxUtils.js CHANGED
@@ -469,16 +469,16 @@ function reportStorageQuota(error) {
469
469
  * The connection layer (createStore) owns connection/transport recovery; this operation layer owns
470
470
  * capacity recovery (eviction) so that a given failure is retried by exactly one layer:
471
471
  * - INVALID_DATA: logs an alert and throws (the same data will always fail).
472
- * - TRANSIENT / FATAL: the connection layer already retried (transient) or exhausted its heal budget
473
- * and alerted (fatal). Retrying here would only re-amplify, so we skip the write quietly.
472
+ * - TRANSIENT / FATAL: the connection layer already retried (transient) or spent a heal attempt
473
+ * (fatal). Retrying here would only re-amplify, so we skip the write quietly.
474
474
  * - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level
475
475
  * circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making
476
476
  * progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
477
477
  * - DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither
478
478
  * retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with
479
479
  * a single throttled alert + quota snapshot per burst.
480
- * - UNAVAILABLE: the storage engine does not exist in this environment, so the storage layer has
481
- * already degraded to the in-memory provider. No retry.
480
+ * - UNAVAILABLE: the storage engine does not exist in this environment or its heal budget ran out, so
481
+ * the storage layer has already degraded to the in-memory provider. No retry.
482
482
  * - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message +
483
483
  * provider) once so it's visible, then bounded retry without eviction.
484
484
  */
@@ -752,6 +752,9 @@ function initializeWithDefaultKeyStates() {
752
752
  })
753
753
  .catch((error) => {
754
754
  Logger.logAlert(`Failed to load data from storage during init. The app will boot with default key states only. Error: ${error}`);
755
+ if (storage_1.default.classifyError(error) === errors_1.StorageErrorClass.FATAL) {
756
+ storage_1.default.degradeToMemoryOnly(error);
757
+ }
755
758
  // Populate the key index so getAllKeys() returns correct results for default keys.
756
759
  // Without this, subscribers that check getAllKeys() would see an empty set even
757
760
  // though we have default values in cache.
@@ -12,6 +12,7 @@ declare const StorageMock: {
12
12
  getStorageProvider: jest.Mock<import("../providers/types").default<{
13
13
  [x: string]: unknown;
14
14
  }>, [], any>;
15
+ degradeToMemoryOnly: jest.Mock<any, any, any>;
15
16
  getItem: jest.Mock<Promise<unknown>, [key: any], any>;
16
17
  multiGet: jest.Mock<Promise<import("../providers/types").StorageKeyValuePair[]>, [keys: import("../providers/types").StorageKeyList], any>;
17
18
  setItem: jest.Mock<Promise<void>, [key: any, value: unknown], any>;
@@ -53,6 +53,7 @@ const StorageMock = {
53
53
  init,
54
54
  classifyError: jest.fn(classifyError),
55
55
  getStorageProvider: jest.fn(() => MemoryOnlyProvider_1.default),
56
+ degradeToMemoryOnly: jest.fn(),
56
57
  getItem: jest.fn(MemoryOnlyProvider_1.default.getItem),
57
58
  multiGet: jest.fn(MemoryOnlyProvider_1.default.multiGet),
58
59
  setItem: jest.fn(MemoryOnlyProvider_1.default.setItem),
@@ -22,12 +22,12 @@ declare const StorageErrorClass: {
22
22
  readonly DISK_PRESSURE: "diskPressure";
23
23
  /** Non-serializable payload. Never retriable — the same data will always fail. */
24
24
  readonly INVALID_DATA: "invalidData";
25
- /** Backing-store corruption. Owner: connection layer — budgeted heal, then give up. */
25
+ /** Backing-store corruption. Owner: connection layer — budgeted heal, then rethrow as UNAVAILABLE. */
26
26
  readonly FATAL: "fatal";
27
27
  /** The storage engine itself does not exist in this environment (for example `indexedDB` is an
28
- * undeclared global in Chrome for iOS private tabs and in Lockdown Mode). Never retriable — the
29
- * engine cannot appear mid-session. Owner: the storage layer — degrade
30
- * to the in-memory provider. */
28
+ * undeclared global in Chrome for iOS private tabs and in Lockdown Mode), or a FATAL heal budget
29
+ * ran out. Never retriable — the engine cannot recover mid-session. Owner: the storage layer —
30
+ * degrade to the in-memory provider. */
31
31
  readonly UNAVAILABLE: "unavailable";
32
32
  /** Unmatched by the active provider. Owner: operation layer — bounded retry, and log the shape so
33
33
  * recurring cases can be promoted into one of the classes above. */
@@ -26,12 +26,12 @@ const StorageErrorClass = {
26
26
  DISK_PRESSURE: 'diskPressure',
27
27
  /** Non-serializable payload. Never retriable — the same data will always fail. */
28
28
  INVALID_DATA: 'invalidData',
29
- /** Backing-store corruption. Owner: connection layer — budgeted heal, then give up. */
29
+ /** Backing-store corruption. Owner: connection layer — budgeted heal, then rethrow as UNAVAILABLE. */
30
30
  FATAL: 'fatal',
31
31
  /** The storage engine itself does not exist in this environment (for example `indexedDB` is an
32
- * undeclared global in Chrome for iOS private tabs and in Lockdown Mode). Never retriable — the
33
- * engine cannot appear mid-session. Owner: the storage layer — degrade
34
- * to the in-memory provider. */
32
+ * undeclared global in Chrome for iOS private tabs and in Lockdown Mode), or a FATAL heal budget
33
+ * ran out. Never retriable — the engine cannot recover mid-session. Owner: the storage layer —
34
+ * degrade to the in-memory provider. */
35
35
  UNAVAILABLE: 'unavailable',
36
36
  /** Unmatched by the active provider. Owner: operation layer — bounded retry, and log the shape so
37
37
  * recurring cases can be promoted into one of the classes above. */
@@ -1,6 +1,7 @@
1
1
  import type StorageProvider from './providers/types';
2
2
  type Storage = {
3
3
  getStorageProvider: () => StorageProvider<unknown>;
4
+ degradeToMemoryOnly: (error: unknown) => void;
4
5
  } & Omit<StorageProvider<unknown>, 'name' | 'store'>;
5
6
  declare const storage: Storage;
6
7
  export default storage;
@@ -56,6 +56,9 @@ const initPromise = new Promise((resolve) => {
56
56
  * Degrade performance by removing the storage provider and only using cache
57
57
  */
58
58
  function degradePerformance(error) {
59
+ if (provider === MemoryOnlyProvider_1.default) {
60
+ return;
61
+ }
59
62
  let causeMessage = '';
60
63
  if (error.cause instanceof Error) {
61
64
  causeMessage = error.cause.message;
@@ -95,6 +98,7 @@ const storage = {
95
98
  getStorageProvider() {
96
99
  return provider;
97
100
  },
101
+ degradeToMemoryOnly: (error) => degradePerformance(error instanceof Error ? error : new Error(String(error))),
98
102
  /**
99
103
  * Classifies a write error using the platform provider's own classifier. Synchronous and pure —
100
104
  * never wrapped in tryOrDegradePerformance.
@@ -212,7 +216,12 @@ const storage = {
212
216
  if (!InstanceSync_1.default.shouldBeUsed)
213
217
  return;
214
218
  shouldKeepInstancesSync = true;
215
- InstanceSync_1.default.init(onStorageKeysChanged, this);
219
+ InstanceSync_1.default.init((pairs) => {
220
+ if (provider === MemoryOnlyProvider_1.default) {
221
+ return;
222
+ }
223
+ onStorageKeysChanged(pairs);
224
+ }, this);
216
225
  },
217
226
  };
218
227
  exports.default = storage;
@@ -1,7 +1,40 @@
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
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  const errors_1 = require("../../errors");
4
- const isIndexedDBAvailable_1 = require("./isIndexedDBAvailable");
37
+ const errorMessages_1 = __importStar(require("./errorMessages"));
5
38
  /**
6
39
  * Classifies an IndexedDB write failure into the shared storage taxonomy (lib/storage/errors.ts).
7
40
  * Matching is done on the lowercased error name and message. This is the IndexedDB engine's own
@@ -10,7 +43,11 @@ const isIndexedDBAvailable_1 = require("./isIndexedDBAvailable");
10
43
  function classifyIDBError(error) {
11
44
  const { name, message } = (0, errors_1.getErrorParts)(error);
12
45
  // The engine is absent, not broken e.g. private tabs and Lockdown Mode on WebKit.
13
- if (message.includes("can't find variable: indexeddb") || message.includes('indexeddb is not defined') || message.includes(isIndexedDBAvailable_1.INDEXED_DB_UNAVAILABLE_MESSAGE.toLowerCase())) {
46
+ if (message.includes("can't find variable: indexeddb") || message.includes('indexeddb is not defined') || message.includes(errorMessages_1.default.UNAVAILABLE.toLowerCase())) {
47
+ return errors_1.StorageErrorClass.UNAVAILABLE;
48
+ }
49
+ // Reopen budget spent on FATAL errors, so the store is treated as gone for the rest of the session.
50
+ if (name === errorMessages_1.IDBErrorName.HEAL_EXHAUSTED.toLowerCase()) {
14
51
  return errors_1.StorageErrorClass.UNAVAILABLE;
15
52
  }
16
53
  // Non-serializable data passed to IDBObjectStore.put — retrying is futile.
@@ -40,7 +40,8 @@ const IDB = __importStar(require("idb-keyval"));
40
40
  const Logger = __importStar(require("../../../Logger"));
41
41
  const errors_1 = require("../../errors");
42
42
  const classifyError_1 = __importDefault(require("./classifyError"));
43
- const isIndexedDBAvailable_1 = __importStar(require("./isIndexedDBAvailable"));
43
+ const errorMessages_1 = __importStar(require("./errorMessages"));
44
+ const isIndexedDBAvailable_1 = __importDefault(require("./isIndexedDBAvailable"));
44
45
  const HEAL_ATTEMPTS_MAX = 3;
45
46
  // This is a copy of the createStore function from idb-keyval, we need a custom implementation
46
47
  // because we need to create the database manually in order to ensure that the store exists before we use it.
@@ -90,7 +91,7 @@ function createStore(dbName, storeName) {
90
91
  if (dbp)
91
92
  return dbp;
92
93
  if (!(0, isIndexedDBAvailable_1.default)()) {
93
- return Promise.reject(new Error(isIndexedDBAvailable_1.INDEXED_DB_UNAVAILABLE_MESSAGE));
94
+ return Promise.reject(new Error(errorMessages_1.default.UNAVAILABLE));
94
95
  }
95
96
  const request = indexedDB.open(dbName);
96
97
  request.onupgradeneeded = () => request.result.createObjectStore(storeName);
@@ -218,12 +219,17 @@ function createStore(dbName, storeName) {
218
219
  });
219
220
  }
220
221
  if (errorClass === errors_1.StorageErrorClass.FATAL) {
222
+ const errorMessage = error instanceof Error ? error.message : String(error);
221
223
  Logger.logAlert('IDB heal: backing store error — heal budget exhausted, giving up', {
222
224
  dbName,
223
225
  storeName,
226
+ errorMessage,
224
227
  });
228
+ const healExhaustedError = new Error(`${errorMessages_1.default.HEAL_EXHAUSTED}: ${errorMessage}`, { cause: error });
229
+ healExhaustedError.name = errorMessages_1.IDBErrorName.HEAL_EXHAUSTED;
230
+ throw healExhaustedError;
225
231
  }
226
- else if (errorClass === errors_1.StorageErrorClass.UNKNOWN) {
232
+ if (errorClass === errors_1.StorageErrorClass.UNKNOWN) {
227
233
  // UNKNOWN — unexpected at this layer; record it so it's visible. CAPACITY is the
228
234
  // expected propagation path (the operation layer owns its logging, and suppresses it
229
235
  // entirely once the circuit breaker is open), so we do NOT log it here — doing so was a
@@ -0,0 +1,11 @@
1
+ /** Messages of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `UNAVAILABLE` to `StorageErrorClass.UNAVAILABLE`. */
2
+ declare const IDBErrorMessage: {
3
+ readonly UNAVAILABLE: "IndexedDB is not available in this environment";
4
+ readonly HEAL_EXHAUSTED: "IndexedDB heal budget exhausted";
5
+ };
6
+ /** Names of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `HEAL_EXHAUSTED` to `StorageErrorClass.UNAVAILABLE`. */
7
+ declare const IDBErrorName: {
8
+ readonly HEAL_EXHAUSTED: "IDBHealExhaustedError";
9
+ };
10
+ export default IDBErrorMessage;
11
+ export { IDBErrorName };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.IDBErrorName = void 0;
4
+ /** Messages of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `UNAVAILABLE` to `StorageErrorClass.UNAVAILABLE`. */
5
+ const IDBErrorMessage = {
6
+ UNAVAILABLE: 'IndexedDB is not available in this environment',
7
+ HEAL_EXHAUSTED: 'IndexedDB heal budget exhausted',
8
+ };
9
+ /** Names of errors thrown by the IndexedDB provider itself. `classifyIDBError` maps `HEAL_EXHAUSTED` to `StorageErrorClass.UNAVAILABLE`. */
10
+ const IDBErrorName = {
11
+ HEAL_EXHAUSTED: 'IDBHealExhaustedError',
12
+ };
13
+ exports.IDBErrorName = IDBErrorName;
14
+ exports.default = IDBErrorMessage;
@@ -40,7 +40,8 @@ const IDB = __importStar(require("idb-keyval"));
40
40
  const utils_1 = __importDefault(require("../../../utils"));
41
41
  const createStore_1 = __importDefault(require("./createStore"));
42
42
  const classifyError_1 = __importDefault(require("./classifyError"));
43
- const isIndexedDBAvailable_1 = __importStar(require("./isIndexedDBAvailable"));
43
+ const errorMessages_1 = __importDefault(require("./errorMessages"));
44
+ const isIndexedDBAvailable_1 = __importDefault(require("./isIndexedDBAvailable"));
44
45
  const DB_NAME = 'OnyxDB';
45
46
  const STORE_NAME = 'keyvaluepairs';
46
47
  /**
@@ -71,7 +72,7 @@ const provider = {
71
72
  */
72
73
  init() {
73
74
  if (!(0, isIndexedDBAvailable_1.default)()) {
74
- throw new Error(`IDBKeyVal store could not be created: ${isIndexedDBAvailable_1.INDEXED_DB_UNAVAILABLE_MESSAGE}`);
75
+ throw new Error(`IDBKeyVal store could not be created: ${errorMessages_1.default.UNAVAILABLE}`);
75
76
  }
76
77
  const newIdbKeyValStore = (0, createStore_1.default)(DB_NAME, STORE_NAME);
77
78
  if (newIdbKeyValStore == null) {
@@ -1,9 +1,6 @@
1
- /** Matched by `classifyIDBError` so a guarded failure classifies as UNAVAILABLE like an unguarded one. */
2
- declare const INDEXED_DB_UNAVAILABLE_MESSAGE = "indexedDB is not available in this environment";
3
1
  /**
4
2
  * `typeof` instead of `indexedDB === undefined`: on WebKit the global is undeclared in private tabs and
5
3
  * Lockdown Mode, so reading it throws a `ReferenceError` instead of evaluating to `undefined`.
6
4
  */
7
5
  declare function isIndexedDBAvailable(): boolean;
8
6
  export default isIndexedDBAvailable;
9
- export { INDEXED_DB_UNAVAILABLE_MESSAGE };
@@ -1,9 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.INDEXED_DB_UNAVAILABLE_MESSAGE = void 0;
4
- /** Matched by `classifyIDBError` so a guarded failure classifies as UNAVAILABLE like an unguarded one. */
5
- const INDEXED_DB_UNAVAILABLE_MESSAGE = 'indexedDB is not available in this environment';
6
- exports.INDEXED_DB_UNAVAILABLE_MESSAGE = INDEXED_DB_UNAVAILABLE_MESSAGE;
7
3
  /**
8
4
  * `typeof` instead of `indexedDB === undefined`: on WebKit the global is undeclared in private tabs and
9
5
  * Lockdown Mode, so reading it throws a `ReferenceError` instead of evaluating to `undefined`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.118",
3
+ "version": "3.0.120",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",