react-native-onyx 3.0.96 → 3.0.98

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/dist/Onyx.js CHANGED
@@ -240,11 +240,15 @@ function merge(key, changes) {
240
240
  return mergeQueuePromise[key];
241
241
  }
242
242
  mergeQueue[key] = [changes];
243
- mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((existingValue) => {
243
+ mergeQueuePromise[key] = OnyxUtils_1.default.get(key).then((valueFromGet) => {
244
244
  // Calls to Onyx.set after a merge will terminate the current merge process and clear the merge queue
245
245
  if (mergeQueue[key] == null) {
246
246
  return Promise.resolve();
247
247
  }
248
+ // Other writers (notably Onyx.update's mergeCollection path, which doesn't participate in mergeQueue)
249
+ // can land between get() resolving and this callback running. Applying the delta on top of the value
250
+ // captured back then and broadcasting it would overwrite those writes wholesale, so re-read the cache.
251
+ const existingValue = OnyxCache_1.default.hasCacheForKey(key) ? OnyxCache_1.default.get(key) : valueFromGet;
248
252
  try {
249
253
  const validChanges = mergeQueue[key].filter((change) => {
250
254
  const { isCompatible, existingValueType, newValueType, isEmptyArrayCoercion } = utils_1.default.checkCompatibilityWithExistingValue(change, existingValue);
@@ -12,6 +12,8 @@ declare const METHOD: {
12
12
  readonly MULTI_SET: "multiset";
13
13
  readonly CLEAR: "clear";
14
14
  };
15
+ /** Test-only: clears the disk-pressure log throttle so each test observes its own alert. */
16
+ declare function resetDiskPressureLogThrottle(): void;
15
17
  type OnyxMethod = ValueOf<typeof METHOD>;
16
18
  declare function getSnapshotKey(): OnyxKey | null;
17
19
  /**
@@ -143,6 +145,9 @@ declare function reportStorageQuota(error?: Error): Promise<void>;
143
145
  * - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level
144
146
  * circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making
145
147
  * progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
148
+ * - DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither
149
+ * retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with
150
+ * a single throttled alert + quota snapshot per burst.
146
151
  * - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message +
147
152
  * provider) once so it's visible, then bounded retry without eviction.
148
153
  */
@@ -291,6 +296,7 @@ declare const OnyxUtils: {
291
296
  getCollectionDataAndSendAsObject: typeof getCollectionDataAndSendAsObject;
292
297
  remove: typeof remove;
293
298
  reportStorageQuota: typeof reportStorageQuota;
299
+ resetDiskPressureLogThrottle: typeof resetDiskPressureLogThrottle;
294
300
  retryOperation: typeof retryOperation;
295
301
  broadcastUpdate: typeof broadcastUpdate;
296
302
  hasPendingMergeForKey: typeof hasPendingMergeForKey;
package/dist/OnyxUtils.js CHANGED
@@ -60,6 +60,14 @@ const METHOD = {
60
60
  };
61
61
  // Max number of retries for failed storage operations
62
62
  const MAX_STORAGE_OPERATION_RETRY_ATTEMPTS = 5;
63
+ /** Minimum interval between disk-pressure alerts. One disk-pressure burst fails every queued operation
64
+ * with the identical error, so per-operation logging would amplify the very storm it reports. */
65
+ const DISK_PRESSURE_LOG_INTERVAL_MS = 60000;
66
+ let lastDiskPressureLogTime = 0;
67
+ /** Test-only: clears the disk-pressure log throttle so each test observes its own alert. */
68
+ function resetDiskPressureLogThrottle() {
69
+ lastDiskPressureLogTime = 0;
70
+ }
63
71
  // Key/value store of Onyx key and arrays of values to merge
64
72
  let mergeQueue = {};
65
73
  let mergeQueuePromise = {};
@@ -659,6 +667,9 @@ function reportStorageQuota(error) {
659
667
  * - CAPACITY: evicts the least recently accessed evictable key and retries, under a session-level
660
668
  * circuit breaker (see lib/StorageCircuitBreaker.ts) that halts the loop once eviction stops making
661
669
  * progress or failures storm — the per-operation budget alone cannot stop a session-wide storm.
670
+ * - DISK_PRESSURE: the device disk itself is full (or the database files are unreadable), so neither
671
+ * retries nor in-DB eviction can free space — the write is dropped (cache stays authoritative) with
672
+ * a single throttled alert + quota snapshot per burst.
662
673
  * - UNKNOWN: the provider couldn't classify it — log the full error shape (name + message +
663
674
  * provider) once so it's visible, then bounded retry without eviction.
664
675
  */
@@ -674,6 +685,18 @@ function retryOperation(error, onyxMethod, defaultParams, retryAttempt, inFlight
674
685
  StorageCircuitBreaker_1.default.recordProbeFailure();
675
686
  return Promise.resolve();
676
687
  }
688
+ // DISK_PRESSURE: the device disk is full, so neither retries nor eviction can succeed until the OS
689
+ // frees space. Drop the write (cache stays authoritative) and log one alert + quota snapshot per
690
+ // interval — the snapshot's free-disk bytes let telemetry confirm (or rule out) disk pressure.
691
+ if (errorClass === errors_1.StorageErrorClass.DISK_PRESSURE) {
692
+ const now = Date.now();
693
+ if (now - lastDiskPressureLogTime < DISK_PRESSURE_LOG_INTERVAL_MS) {
694
+ return Promise.resolve();
695
+ }
696
+ lastDiskPressureLogTime = now;
697
+ Logger.logAlert(`Disk-pressure storage error; skipping retries. provider: ${storage_1.default.getStorageProvider().name}. message: ${error === null || error === void 0 ? void 0 : error.message}. onyxMethod: ${onyxMethod.name}.`);
698
+ return reportStorageQuota(error);
699
+ }
677
700
  Logger.logInfo(`Failed to save to storage. Error: ${error}. class: ${errorClass}. onyxMethod: ${onyxMethod.name}. retryAttempt: ${currentRetryAttempt}/${MAX_STORAGE_OPERATION_RETRY_ATTEMPTS}`);
678
701
  if (errorClass === errors_1.StorageErrorClass.INVALID_DATA) {
679
702
  Logger.logAlert(`Attempted to set invalid data set in Onyx. Please ensure all data is serializable. Error: ${error}`);
@@ -1510,6 +1533,7 @@ const OnyxUtils = {
1510
1533
  getCollectionDataAndSendAsObject,
1511
1534
  remove,
1512
1535
  reportStorageQuota,
1536
+ resetDiskPressureLogThrottle,
1513
1537
  retryOperation,
1514
1538
  broadcastUpdate,
1515
1539
  hasPendingMergeForKey,
@@ -3,6 +3,7 @@ declare const StorageMock: {
3
3
  classifyError: jest.Mock<import("type-fest").ValueOf<{
4
4
  readonly TRANSIENT: "transient";
5
5
  readonly CAPACITY: "capacity";
6
+ readonly DISK_PRESSURE: "diskPressure";
6
7
  readonly INVALID_DATA: "invalidData";
7
8
  readonly FATAL: "fatal";
8
9
  readonly UNKNOWN: "unknown";
@@ -15,6 +15,10 @@ declare const StorageErrorClass: {
15
15
  readonly TRANSIENT: "transient";
16
16
  /** Quota exceeded / disk full. Owner: operation layer — evict and retry. */
17
17
  readonly CAPACITY: "capacity";
18
+ /** Filesystem-level failure around the database files (device disk full, or the files cannot be
19
+ * created/read). Owner: operation layer — skip retries and eviction (neither can free OS-level
20
+ * space) and log one throttled alert + quota snapshot per burst. */
21
+ readonly DISK_PRESSURE: "diskPressure";
18
22
  /** Non-serializable payload. Never retriable — the same data will always fail. */
19
23
  readonly INVALID_DATA: "invalidData";
20
24
  /** Backing-store corruption. Owner: connection layer — budgeted heal, then give up. */
@@ -19,6 +19,10 @@ const StorageErrorClass = {
19
19
  TRANSIENT: 'transient',
20
20
  /** Quota exceeded / disk full. Owner: operation layer — evict and retry. */
21
21
  CAPACITY: 'capacity',
22
+ /** Filesystem-level failure around the database files (device disk full, or the files cannot be
23
+ * created/read). Owner: operation layer — skip retries and eviction (neither can free OS-level
24
+ * space) and log one throttled alert + quota snapshot per burst. */
25
+ DISK_PRESSURE: 'diskPressure',
22
26
  /** Non-serializable payload. Never retriable — the same data will always fail. */
23
27
  INVALID_DATA: 'invalidData',
24
28
  /** Backing-store corruption. Owner: connection layer — budgeted heal, then give up. */
@@ -244,15 +244,20 @@ const provider = {
244
244
  if (!provider.store) {
245
245
  throw new Error('Store is not initialized!');
246
246
  }
247
- return Promise.all([provider.store.executeAsync('PRAGMA page_size;'), provider.store.executeAsync('PRAGMA page_count;'), (0, react_native_device_info_1.getFreeDiskStorage)()]).then(([pageSizeResult, pageCountResult, bytesRemaining]) => {
247
+ // The PRAGMAs need the SQLite connection; getFreeDiskStorage() is filesystem-level. Degrade
248
+ // bytesUsed to -1 instead of failing, so the free-disk bytes always get logged.
249
+ const bytesUsedPromise = Promise.all([provider.store.executeAsync('PRAGMA page_size;'), provider.store.executeAsync('PRAGMA page_count;')])
250
+ .then(([pageSizeResult, pageCountResult]) => {
248
251
  var _a, _b, _c, _d, _e, _f;
249
252
  const pageSize = (_c = (_b = (_a = pageSizeResult.rows) === null || _a === void 0 ? void 0 : _a.item(0)) === null || _b === void 0 ? void 0 : _b.page_size) !== null && _c !== void 0 ? _c : 0;
250
253
  const pageCount = (_f = (_e = (_d = pageCountResult.rows) === null || _d === void 0 ? void 0 : _d.item(0)) === null || _e === void 0 ? void 0 : _e.page_count) !== null && _f !== void 0 ? _f : 0;
251
- return {
252
- bytesUsed: pageSize * pageCount,
253
- bytesRemaining,
254
- };
255
- });
254
+ return pageSize * pageCount;
255
+ })
256
+ .catch(() => -1);
257
+ return Promise.all([bytesUsedPromise, (0, react_native_device_info_1.getFreeDiskStorage)()]).then(([bytesUsed, bytesRemaining]) => ({
258
+ bytesUsed,
259
+ bytesRemaining,
260
+ }));
256
261
  },
257
262
  };
258
263
  exports.default = provider;
@@ -16,6 +16,9 @@ function classifySQLiteError(error) {
16
16
  if (message.includes('database or disk is full')) {
17
17
  return errors_1.StorageErrorClass.CAPACITY;
18
18
  }
19
+ if (message.includes('disk i/o error') || message.includes('unable to open database file')) {
20
+ return errors_1.StorageErrorClass.DISK_PRESSURE;
21
+ }
19
22
  return errors_1.StorageErrorClass.UNKNOWN;
20
23
  }
21
24
  exports.default = classifySQLiteError;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-onyx",
3
- "version": "3.0.96",
3
+ "version": "3.0.98",
4
4
  "author": "Expensify, Inc.",
5
5
  "homepage": "https://expensify.com",
6
6
  "description": "State management for React Native",