react-native-nitro-storage 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/README.md +89 -22
  3. package/SECURITY.md +2 -2
  4. package/cpp/bindings/HybridStorage.cpp +5 -12
  5. package/cpp/bindings/HybridStorage.hpp +1 -1
  6. package/docs/api-reference.md +11 -1
  7. package/docs/batch-transactions-migrations.md +8 -2
  8. package/docs/benchmarks.md +19 -15
  9. package/docs/web-backends.md +18 -1
  10. package/ios/IOSStorageAdapterCpp.mm +162 -17
  11. package/lib/commonjs/capabilities.js +26 -0
  12. package/lib/commonjs/capabilities.js.map +1 -0
  13. package/lib/commonjs/core/durability.js +137 -0
  14. package/lib/commonjs/core/durability.js.map +1 -0
  15. package/lib/commonjs/core/metrics.js +91 -0
  16. package/lib/commonjs/core/metrics.js.map +1 -0
  17. package/lib/commonjs/index.js +47 -22
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/index.web.js +118 -66
  20. package/lib/commonjs/index.web.js.map +1 -1
  21. package/lib/commonjs/indexeddb-backend.js +73 -23
  22. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  23. package/lib/commonjs/internal.js +27 -4
  24. package/lib/commonjs/internal.js.map +1 -1
  25. package/lib/commonjs/shared.js +2 -2
  26. package/lib/commonjs/shared.js.map +1 -1
  27. package/lib/commonjs/storage-core.js +224 -237
  28. package/lib/commonjs/storage-core.js.map +1 -1
  29. package/lib/commonjs/storage-events.js.map +1 -1
  30. package/lib/commonjs/storage-platform.js +6 -0
  31. package/lib/commonjs/storage-platform.js.map +1 -0
  32. package/lib/commonjs/storage-runtime.js +3 -21
  33. package/lib/commonjs/storage-runtime.js.map +1 -1
  34. package/lib/commonjs/testing.js +19 -4
  35. package/lib/commonjs/testing.js.map +1 -1
  36. package/lib/commonjs/web-backend-contract.js +27 -0
  37. package/lib/commonjs/web-backend-contract.js.map +1 -0
  38. package/lib/module/capabilities.js +21 -0
  39. package/lib/module/capabilities.js.map +1 -0
  40. package/lib/module/core/durability.js +133 -0
  41. package/lib/module/core/durability.js.map +1 -0
  42. package/lib/module/core/metrics.js +87 -0
  43. package/lib/module/core/metrics.js.map +1 -0
  44. package/lib/module/index.js +48 -23
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/index.web.js +103 -64
  47. package/lib/module/index.web.js.map +1 -1
  48. package/lib/module/indexeddb-backend.js +73 -23
  49. package/lib/module/indexeddb-backend.js.map +1 -1
  50. package/lib/module/internal.js +24 -3
  51. package/lib/module/internal.js.map +1 -1
  52. package/lib/module/shared.js +2 -2
  53. package/lib/module/shared.js.map +1 -1
  54. package/lib/module/storage-core.js +225 -238
  55. package/lib/module/storage-core.js.map +1 -1
  56. package/lib/module/storage-events.js.map +1 -1
  57. package/lib/module/storage-platform.js +4 -0
  58. package/lib/module/storage-platform.js.map +1 -0
  59. package/lib/module/storage-runtime.js +3 -21
  60. package/lib/module/storage-runtime.js.map +1 -1
  61. package/lib/module/testing.js +5 -3
  62. package/lib/module/testing.js.map +1 -1
  63. package/lib/module/web-backend-contract.js +22 -0
  64. package/lib/module/web-backend-contract.js.map +1 -0
  65. package/lib/typescript/capabilities.d.ts +11 -0
  66. package/lib/typescript/capabilities.d.ts.map +1 -0
  67. package/lib/typescript/core/durability.d.ts +27 -0
  68. package/lib/typescript/core/durability.d.ts.map +1 -0
  69. package/lib/typescript/core/metrics.d.ts +12 -0
  70. package/lib/typescript/core/metrics.d.ts.map +1 -0
  71. package/lib/typescript/index.d.ts +5 -2
  72. package/lib/typescript/index.d.ts.map +1 -1
  73. package/lib/typescript/index.web.d.ts +8 -4
  74. package/lib/typescript/index.web.d.ts.map +1 -1
  75. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  76. package/lib/typescript/internal.d.ts +3 -0
  77. package/lib/typescript/internal.d.ts.map +1 -1
  78. package/lib/typescript/shared.d.ts +2 -1
  79. package/lib/typescript/shared.d.ts.map +1 -1
  80. package/lib/typescript/storage-core.d.ts +6 -6
  81. package/lib/typescript/storage-core.d.ts.map +1 -1
  82. package/lib/typescript/storage-events.d.ts +2 -2
  83. package/lib/typescript/storage-events.d.ts.map +1 -1
  84. package/lib/typescript/storage-platform.d.ts +12 -0
  85. package/lib/typescript/storage-platform.d.ts.map +1 -0
  86. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  87. package/lib/typescript/testing.d.ts +5 -1
  88. package/lib/typescript/testing.d.ts.map +1 -1
  89. package/lib/typescript/web-backend-contract.d.ts +10 -0
  90. package/lib/typescript/web-backend-contract.d.ts.map +1 -0
  91. package/lib/typescript/web-storage-backend.d.ts +1 -1
  92. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  93. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
  94. package/package.json +4 -3
  95. package/react-native-nitro-storage.podspec +3 -1
  96. package/src/capabilities.ts +36 -0
  97. package/src/core/durability.ts +170 -0
  98. package/src/core/metrics.ts +133 -0
  99. package/src/index.ts +66 -40
  100. package/src/index.web.ts +158 -86
  101. package/src/indexeddb-backend.ts +91 -20
  102. package/src/internal.ts +50 -4
  103. package/src/shared.ts +7 -6
  104. package/src/storage-core.ts +327 -292
  105. package/src/storage-events.ts +2 -1
  106. package/src/storage-platform.ts +16 -0
  107. package/src/storage-runtime.ts +13 -47
  108. package/src/testing.ts +14 -7
  109. package/src/web-backend-contract.ts +34 -0
  110. package/src/web-storage-backend.ts +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,60 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format follows Keep a Changelog and the project adheres to SemVer.
6
6
 
7
+ ## 0.9.0 - 2026-08-20
8
+
9
+ ### Breaking changes
10
+
11
+ - `react-native-nitro-modules` now has a peer range of `>=0.37.0 <0.38.0`.
12
+ Upgrade Nitro Modules and rebuild the native app before using Nitro Storage
13
+ 0.9.0; the previous 0.36.x range is not supported.
14
+ - `SetStorageItem.get()` now returns `Partial<Record<TMember, true>>`. Use
15
+ `has()` for membership checks or handle an indexed value as `true | undefined`
16
+ instead of assuming every member exists.
17
+
18
+ ### Changed
19
+
20
+ - Regenerated the shipped Nitro bindings with Nitro Modules and Nitrogen 0.37.0
21
+ while preserving synchronous JSI storage behavior across native platforms.
22
+ - Native batch reads now preserve missing entries as `undefined`, matching the
23
+ TypeScript contract and allowing stored values that match the old internal
24
+ sentinel string.
25
+ - The standalone package development and type baseline is now React Native
26
+ 0.87.0. The Expo SDK 57 example remains on its supported React Native 0.86.2
27
+ baseline.
28
+
29
+ ## 0.8.0 - 2026-08-12
30
+
31
+ ### Breaking changes
32
+
33
+ - None. `getMetricsSnapshot()` keeps its unscoped, cross-scope aggregate keys.
34
+ Use the new `getScopedMetricsSnapshot()` when per-scope counters are needed.
35
+
36
+ ### Added
37
+
38
+ - Exported `PlatformStorage` and `PlatformScope` types from the native, web,
39
+ and testing entrypoints so shared consumer code can verify platform parity
40
+ without duplicating the package contract.
41
+ - Added `getScopedMetricsSnapshot()` with keys such as `item:set:1`, without
42
+ changing established dashboards that consume `getMetricsSnapshot()`.
43
+
44
+ ### Fixed
45
+
46
+ - **Data-loss prevention:** `storage.import()` now flushes pending coalesced Disk and Secure writes before writing, so a later scheduled flush can never overwrite imported values.
47
+ - **Biometric parity:** promoting a value to biometric storage removes the plain secure copy on iOS and web, matching Android; plain reads can no longer return stale values after promotion.
48
+ - **iOS legacy disk data:** Disk enumeration, size, prefix queries, and clear now cover both the suite domain and legacy `standardUserDefaults` values, so deleted legacy values cannot reappear.
49
+ - **Failure-atomic migrations:** each migration step runs in its own transaction with its version marker; a failed step rolls back its data and marker, and rerunning `migrateToLatest()` retries from the last completed version.
50
+ - **Transaction events:** failed transactions emit exactly one typed `rollback` batch event with pre-rollback and restored raw values.
51
+ - **Atomic memory batch removes:** `removeBatch()` in Memory scope mutates all keys first and emits a single `removeBatch` event.
52
+ - **Stable error classification:** storage error codes now come only from `[nitro-error:<code>]` tags produced by the native and web adapters; message-text scraping was removed and every public code has a producer.
53
+ - **Encoding collisions:** reserved primitive tokens and the native batch missing sentinel are escaped in the stored encoding; legacy reads are preserved and raw API round-trips are unchanged.
54
+
55
+ ### Changed
56
+
57
+ - `getCapabilities().writeBuffering` now reports real per-mode durability: native Secure writes report buffering only while `setSecureWritesAsync(true)` is active on Android, and web backends report buffering only for IndexedDB-based backends.
58
+ - The IndexedDB backend reports affected keys when `flush()` fails and starts a best-effort flush on `pagehide` and hidden visibility changes.
59
+ - `setIfVersion()` is documented as optimistic (no backend-level atomicity); CAS guarantees are covered by race tests.
60
+
7
61
  ## 0.7.0 - 2026-07-30
8
62
 
9
63
  ### Changes
package/README.md CHANGED
@@ -4,9 +4,9 @@
4
4
  [![npm downloads](https://img.shields.io/npm/dm/react-native-nitro-storage?color=22c55e&label=downloads)](https://www.npmjs.com/package/react-native-nitro-storage)
5
5
  [![CI](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/actions/workflows/ci.yml/badge.svg)](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/actions/workflows/ci.yml)
6
6
  [![license](https://img.shields.io/npm/l/react-native-nitro-storage?color=007ec6)](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/blob/main/LICENSE)
7
- [![React Native](https://img.shields.io/badge/react--native-%3E%3D0.75-61dafb)](https://reactnative.dev/)
8
- [![Expo](https://img.shields.io/badge/expo-SDK%2057-000020)](https://docs.expo.dev/versions/latest/)
9
- [![Nitro Modules](https://img.shields.io/badge/nitro--modules-0.36.x-black)](https://nitro.margelo.com/)
7
+ [![React Native](https://img.shields.io/badge/react--native-0.87.0-61dafb)](https://reactnative.dev/docs/0.87/getting-started-without-a-framework)
8
+ [![Expo](https://img.shields.io/badge/expo-SDK%2057%20%28RN%200.86.2%29-000020)](https://docs.expo.dev/versions/v57.0.0/)
9
+ [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.37.0%20%3C0.38.0-black)](https://nitro.margelo.com/)
10
10
  [![TypeScript](https://img.shields.io/badge/typescript-6.0-3178c6)](https://www.typescriptlang.org/)
11
11
 
12
12
  Synchronous Memory, Disk, and Secure storage for React Native, Expo development
@@ -26,6 +26,7 @@ pagination, conflict resolution, or remote synchronization.
26
26
  - [Install](#install)
27
27
  - [Expo Config](#expo-config)
28
28
  - [Quick Start](#quick-start)
29
+ - [Auth Tokens](#auth-tokens)
29
30
  - [Typed Storage Items](#typed-storage-items)
30
31
  - [Item Ergonomics](#item-ergonomics)
31
32
  - [Set Items](#set-items)
@@ -57,12 +58,31 @@ Peer dependencies:
57
58
  | ---------------------------- | ------------------ |
58
59
  | `react` | `>=18.2.0` |
59
60
  | `react-native` | `>=0.75.0` |
60
- | `react-native-nitro-modules` | `>=0.36.4 <0.37.0` |
61
+ | `react-native-nitro-modules` | `>=0.37.0 <0.38.0` |
61
62
 
62
- Nitro peer requirement: `react-native-nitro-modules >=0.36.4 <0.37.0`.
63
+ Nitro peer requirement: `react-native-nitro-modules >=0.37.0 <0.38.0`.
63
64
 
64
- Validated example baseline: Expo SDK 57, React Native 0.86.2, React 19.2.3,
65
- and Nitro Modules 0.36.4.
65
+ The standalone package gate uses React Native `0.87.0` and the Strict
66
+ TypeScript API. The Expo example uses Expo SDK `57.0.15`, React Native
67
+ `0.86.2`, React `19.2.3`, and Nitro Modules `0.37.0`, which is the React Native
68
+ version supported by that Expo SDK. Do not override Expo's React Native version.
69
+
70
+ When upgrading from 0.8.x, upgrade Nitro Modules to the 0.37.x range before
71
+ installing this package, then rebuild the native app so the generated Nitro
72
+ bindings and native runtime use the same major-minor version:
73
+
74
+ ```sh
75
+ bun add react-native-nitro-modules@0.37.0 react-native-nitro-storage@0.9.0
76
+ bunx expo prebuild
77
+ ```
78
+
79
+ `SetStorageItem.get()` now returns a partial membership map because a member can
80
+ be absent. Use `set.has(member)` for membership checks, or handle an indexed
81
+ value as `true | undefined`; code that assigned the result to
82
+ `Record<string, true>` must update its type.
83
+
84
+ Nitro Storage requires an Expo development build or a bare React Native app;
85
+ Expo Go and native Windows, macOS, and tvOS targets are not supported.
66
86
 
67
87
  For Expo development builds:
68
88
 
@@ -129,6 +149,40 @@ const theme = themeItem.get();
129
149
  const raw = storage.getString("settings:theme", StorageScope.Disk);
130
150
  ```
131
151
 
152
+ `storage.getString` / `setString` remain the raw API. Prefer typed items for
153
+ application state.
154
+
155
+ Native storage calls are synchronous JSI operations. Keep values and batches
156
+ small enough for the JavaScript event loop; native and configured web-backend
157
+ failures throw errors. Secure cache fallback is opt-in through
158
+ `fallbackToCacheOnReadError`.
159
+
160
+ ## Auth Tokens
161
+
162
+ Use `createSecureAuthStorage` for access and refresh tokens. `renameFrom`
163
+ copies a legacy key on first read and deletes it, so you do not need a custom
164
+ migration helper.
165
+
166
+ ```ts
167
+ import { createSecureAuthStorage } from "react-native-nitro-storage";
168
+
169
+ const auth = createSecureAuthStorage(
170
+ {
171
+ accessToken: { renameFrom: "authToken" },
172
+ refreshToken: { renameFrom: "refreshToken" },
173
+ },
174
+ { namespace: "auth", fallbackToCacheOnReadError: true },
175
+ );
176
+
177
+ auth.accessToken.set("access-token");
178
+ const current = auth.accessToken.get();
179
+ auth.accessToken.subscribe(() => {});
180
+ ```
181
+
182
+ Keep `getString` facades only when the app owns a storage architecture
183
+ boundary. `createSecureAuthStorage` already namespaces keys, notifies
184
+ subscribers, and migrates legacy keys.
185
+
132
186
  ## Typed Storage Items
133
187
 
134
188
  `createStorageItem<T>()` is the recommended API for application code. It keeps
@@ -183,7 +237,7 @@ const config = diskItem<{ theme: "light" | "dark"; compact: boolean }>({
183
237
  });
184
238
 
185
239
  config.merge({ compact: true }); // shallow object update
186
- config.reset(); // back to the default value
240
+ config.reset(); // deletes the stored key; the next read returns the default value
187
241
  const loginMethod = memoryItem<string | null>({
188
242
  key: "loginMethod",
189
243
  defaultValue: null,
@@ -194,8 +248,9 @@ loginMethod.setOrDelete(maybeMethod); // null/undefined deletes, value sets
194
248
  ## Set Items
195
249
 
196
250
  `createSetItem()` models set-membership state (seen ids, dismissed prompts)
197
- without hand-rolling `Record<string, true>` helpers. Adding an existing member
198
- or deleting an absent one is a no-op, so subscribers do not re-render.
251
+ without hand-rolling membership helpers. Its `get()` result is a partial map,
252
+ because absent members are not stored. Adding an existing member or deleting an
253
+ absent one is a no-op, so subscribers do not re-render.
199
254
 
200
255
  ```ts
201
256
  import { createSetItem, StorageScope } from "react-native-nitro-storage";
@@ -378,7 +433,9 @@ On Android 11 and newer, `BiometricLevel.BiometryOnly` and
378
433
  `BiometricLevel.BiometryOrPasscode` use separate Keystore policies. Android 10
379
434
  and older support `BiometryOrPasscode`; `BiometryOnly` throws
380
435
  `biometric_unavailable` because those releases cannot safely enforce the
381
- biometric-only distinction. Secure existence, discovery, and cleanup operations
436
+ biometric-only distinction. Promoting a value to biometric storage removes the
437
+ plain secure copy on every platform, so plain reads cannot return a stale
438
+ value. Secure existence, discovery, and cleanup operations
382
439
  can also throw when a protected store is locked or its key is invalidated. Catch
383
440
  those failures and use `isKeychainLockedError()` when authentication-aware retry
384
441
  behavior is appropriate.
@@ -388,6 +445,8 @@ behavior is appropriate.
388
445
  `getBatch()` preserves tuple value types, so IDEs infer each result from the
389
446
  matching item. `setBatch()` validates every item/value pair independently,
390
447
  including heterogeneous batches.
448
+ Missing keys use each item's `defaultValue`; the native bridge preserves missing
449
+ entries as `undefined` while reading the batch.
391
450
 
392
451
  ```ts
393
452
  import { getBatch, removeBatch, setBatch } from "react-native-nitro-storage";
@@ -431,10 +490,15 @@ storage.setMetricsObserver((event) => {
431
490
  });
432
491
 
433
492
  const metrics = storage.getMetricsSnapshot();
493
+ const scopedMetrics = storage.getScopedMetricsSnapshot();
434
494
  storage.resetMetrics();
435
495
  unsubscribe();
436
496
  ```
437
497
 
498
+ `getMetricsSnapshot()` aggregates each operation across scopes for backward
499
+ compatibility. `getScopedMetricsSnapshot()` adds the numeric scope suffix for
500
+ per-scope analysis, for example `item:set:1`.
501
+
438
502
  Secure event observer values are redacted by default. Pass
439
503
  `{ redactSecureValues: false }` only in trusted debug tooling where raw values
440
504
  are safe to inspect.
@@ -484,7 +548,11 @@ migrateFromMMKV(mmkvInstance, themeItem);
484
548
  ```
485
549
 
486
550
  `runTransaction(scope, callback)` rolls back every write made through the `tx`
487
- context if the callback throws.
551
+ context if the callback throws, then emits one typed `rollback` batch event.
552
+
553
+ Each migration step runs in its own transaction with its version marker, so a
554
+ failed step leaves the scope on the last completed version and rerunning
555
+ `migrateToLatest()` retries deterministically.
488
556
 
489
557
  ## Web Backends
490
558
 
@@ -495,10 +563,7 @@ import {
495
563
  } from "react-native-nitro-storage";
496
564
  import { createIndexedDBBackend } from "react-native-nitro-storage/indexeddb-backend";
497
565
 
498
- const backend = await createIndexedDBBackend({
499
- dbName: "app-storage",
500
- storeName: "kv",
501
- });
566
+ const backend = await createIndexedDBBackend("app-storage", "kv");
502
567
 
503
568
  setWebDiskStorageBackend(backend);
504
569
  setWebSecureStorageBackend(backend);
@@ -534,12 +599,14 @@ const { storage, memoryItem } = createNitroStorageMock();
534
599
 
535
600
  ## Platform Support
536
601
 
537
- | Platform | Status |
538
- | -------- | -------------------------------------------------- |
539
- | iOS | Memory, Disk, and Keychain-backed Secure storage. |
540
- | Android | Memory, Disk, and Keystore-backed Secure storage. |
541
- | Web | Memory plus configurable Disk and Secure backends. |
542
- | Expo | Development builds with the config plugin. |
602
+ | Platform | Status |
603
+ | ---------------------- | -------------------------------------------------- |
604
+ | iOS | Memory, Disk, and Keychain-backed Secure storage. |
605
+ | Android | Memory, Disk, and Keystore-backed Secure storage. |
606
+ | Web | Memory plus configurable Disk and Secure backends. |
607
+ | Expo development build | Supported with the config plugin. |
608
+ | Expo Go | Not supported for Nitro native modules. |
609
+ | Windows, macOS, tvOS | Not supported by this package. |
543
610
 
544
611
  ## Documentation
545
612
 
package/SECURITY.md CHANGED
@@ -6,8 +6,8 @@ Security fixes are shipped for the latest published `0.x` release line.
6
6
 
7
7
  | Version | Supported |
8
8
  | ------- | --------- |
9
- | `0.5.x` | Yes |
10
- | `<0.5` | No |
9
+ | `0.8.x` | Yes |
10
+ | `<0.8` | No |
11
11
 
12
12
  ## Reporting a Vulnerability
13
13
 
@@ -14,7 +14,6 @@
14
14
  namespace margelo::nitro::NitroStorage {
15
15
 
16
16
  namespace {
17
- constexpr auto kBatchMissingSentinel = "__nitro_storage_batch_missing__::v1";
18
17
  constexpr int kDefaultBiometricLevel = 2;
19
18
  } // namespace
20
19
 
@@ -388,8 +387,8 @@ void HybridStorage::setBatch(const std::vector<std::string>& keys, const std::ve
388
387
  }
389
388
  }
390
389
 
391
- std::vector<std::string> HybridStorage::getBatch(const std::vector<std::string>& keys, double scope) {
392
- std::vector<std::string> results;
390
+ std::vector<std::optional<std::string>> HybridStorage::getBatch(const std::vector<std::string>& keys, double scope) {
391
+ std::vector<std::optional<std::string>> results;
393
392
  results.reserve(keys.size());
394
393
 
395
394
  Scope s = toScope(scope);
@@ -402,7 +401,7 @@ std::vector<std::string> HybridStorage::getBatch(const std::vector<std::string>&
402
401
  if (it != memoryStore_.end()) {
403
402
  results.push_back(it->second);
404
403
  } else {
405
- results.push_back(kBatchMissingSentinel);
404
+ results.push_back(std::nullopt);
406
405
  }
407
406
  }
408
407
  return results;
@@ -418,10 +417,7 @@ std::vector<std::string> HybridStorage::getBatch(const std::vector<std::string>&
418
417
  throw std::runtime_error("NitroStorage: Disk getBatch failed (unknown error)");
419
418
  }
420
419
 
421
- for (const auto& value : values) {
422
- results.push_back(value.has_value() ? *value : std::string(kBatchMissingSentinel));
423
- }
424
- return results;
420
+ return values;
425
421
  }
426
422
  case Scope::Secure: {
427
423
  ensureAdapter();
@@ -434,10 +430,7 @@ std::vector<std::string> HybridStorage::getBatch(const std::vector<std::string>&
434
430
  throw std::runtime_error("NitroStorage: Secure getBatch failed (unknown error)");
435
431
  }
436
432
 
437
- for (const auto& value : values) {
438
- results.push_back(value.has_value() ? *value : std::string(kBatchMissingSentinel));
439
- }
440
- return results;
433
+ return values;
441
434
  }
442
435
  }
443
436
 
@@ -39,7 +39,7 @@ public:
39
39
  std::vector<std::string> getKeysByPrefix(const std::string& prefix, double scope) override;
40
40
  double size(double scope) override;
41
41
  void setBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values, double scope) override;
42
- std::vector<std::string> getBatch(const std::vector<std::string>& keys, double scope) override;
42
+ std::vector<std::optional<std::string>> getBatch(const std::vector<std::string>& keys, double scope) override;
43
43
  void removeBatch(const std::vector<std::string>& keys, double scope) override;
44
44
  void removeByPrefix(const std::string& prefix, double scope) override;
45
45
  std::function<void()> addOnChange(
@@ -152,7 +152,7 @@ storage.setEventObserver((event) => {
152
152
 
153
153
  `setEventObserver()` redacts Secure `oldValue` and `newValue` fields by default. Pass `{ redactSecureValues: false }` only for in-memory debugging paths that never persist logs. Raw `subscribe*()` APIs preserve values for state integrations.
154
154
 
155
- Local batch APIs emit one `type: "batch"` envelope to scope and prefix/namespace listeners. Key subscribers receive the matching per-key change so direct key integrations do not need to unpack batch envelopes. Secure events can include raw secret values; do not log Secure event payloads in production.
155
+ Local batch APIs emit one `type: "batch"` envelope to scope and prefix/namespace listeners. Key subscribers receive the matching per-key change so direct key integrations do not need to unpack batch envelopes. Failed transactions emit one batch envelope with `operation: "rollback"` whose changes carry the pre-rollback and restored raw values. Secure events can include raw secret values; do not log Secure event payloads in production.
156
156
 
157
157
  ## Batch Operations
158
158
 
@@ -280,5 +280,15 @@ Common public types:
280
280
  - `WebSecureStorageBackend`
281
281
  - `WebStorageChangeEvent`
282
282
  - `WebStorageScope`
283
+ - `PlatformStorage`
284
+ - `PlatformScope`
285
+ - `WebBackendCapabilities`
286
+
287
+ `getCapabilities().writeBuffering` describes real per-mode durability:
288
+
289
+ - Native: Disk writes are buffered by the platform (`SharedPreferences.apply()` on Android, `NSUserDefaults` on iOS). Secure writes are buffered only when `setSecureWritesAsync(true)` is active on Android; iOS Keychain writes are synchronous.
290
+ - Web: buffering follows the configured backend; IndexedDB backends are buffered, localStorage backends are synchronous.
291
+
292
+ `describeWebBackendCapabilities(backend)` reports a backend's `buffered`, `flushable`, `closable`, and `subscribable` capabilities from the same typed contract used by the built-in backends.
283
293
 
284
294
  The IndexedDB subpath exports `createIndexedDBBackend()` and `IndexedDBBackendOptions`.
@@ -80,7 +80,7 @@ const snapshot = storage.export(StorageScope.Disk);
80
80
  storage.import(snapshot, StorageScope.Disk);
81
81
  ```
82
82
 
83
- For Memory scope, import is atomic: all keys are written before listeners fire. For Disk and Secure, import delegates to native or web batch paths.
83
+ For Memory scope, import is atomic: all keys are written before listeners fire. Disk and Secure imports flush pending coalesced writes first, so the imported snapshot cannot be overwritten by an earlier pending flush, then delegate to native or web batch paths.
84
84
 
85
85
  Secure exports contain raw secret values. `storage.export(StorageScope.Secure)` requires `{ includeSecureValues: true }`; `storage.exportSecureUnsafe()` is the explicit equivalent. Do not log Secure exports or include them in diagnostics, analytics, crash reports, or support bundles.
86
86
 
@@ -124,7 +124,9 @@ Transaction context methods:
124
124
  - `setItem(item, value)`
125
125
  - `removeItem(item)`
126
126
 
127
- If the callback throws, Nitro Storage restores the keys it changed during that transaction.
127
+ If the callback throws, Nitro Storage restores the keys it changed during that transaction and emits exactly one typed `rollback` batch event so observers and the global event observer can react to the rollback.
128
+
129
+ Memory-scope batch removes are atomic: all keys are deleted before listeners fire, and scope, key, and prefix subscribers receive a single `removeBatch` event.
128
130
 
129
131
  ## Migrations
130
132
 
@@ -157,6 +159,8 @@ migrateToLatest(StorageScope.Disk);
157
159
 
158
160
  Migration context methods work with raw strings. Use item serializers manually when migrating structured data.
159
161
 
162
+ Each migration step runs inside its own transaction together with the version marker write. A step that throws rolls back both its data changes and the version marker, so the scope stays on the last completed version and rerunning `migrateToLatest()` retries deterministically.
163
+
160
164
  ```ts
161
165
  registerMigration(3, (ctx) => {
162
166
  const raw = ctx.getRaw("settings");
@@ -198,3 +202,5 @@ const didWrite = themeItem.setIfVersion(
198
202
  ```
199
203
 
200
204
  `setIfVersion()` returns `false` if another write changed the item after `getWithVersion()`.
205
+
206
+ Compare-and-set is optimistic: the version is read, compared, and written without backend-level atomicity. Two writers on different runtimes can both pass the check between their reads and writes, so use it for single-runtime coordination or last-writer-wins-tolerant flows. There is no native compare-and-swap primitive.
@@ -2,36 +2,40 @@
2
2
 
3
3
  Benchmarks are release checks, not product promises. Use them to catch regressions on the local machine and CI image used by this repo.
4
4
 
5
- Run:
5
+ Run from the repo root:
6
6
 
7
7
  ```sh
8
- bun run benchmark -- --filter=react-native-nitro-storage
8
+ bun run build
9
+ bun run benchmark
9
10
  ```
10
11
 
11
- The benchmark script checks representative synchronous read/write paths and fails when results drift beyond the configured threshold.
12
+ ## Scope: Web Only
13
+
14
+ `benchmark` loads `lib/commonjs/index.web.js` and measures the web entry against the localStorage backend. The `disk:` and `secure:` labels describe web scopes, not native Disk or Secure storage.
15
+
16
+ Native Disk/Secure baselines require a device or simulator run and are not part of this gate. Do not compare these numbers against native storage.
12
17
 
13
18
  ## Interpreting Results
14
19
 
15
20
  - Compare results on the same machine and Node/Bun version.
16
- - Treat large deltas as a prompt to inspect recent storage-runtime, serialization, native bridge, or cache changes.
21
+ - Treat large deltas as a prompt to inspect recent storage-runtime, serialization, cache, or event changes.
17
22
  - Do not compare web backend numbers against native secure storage numbers; they measure different systems.
18
- - Secure storage performance depends on platform state, device lock state, biometric prompts, and Keystore/Keychain behavior.
19
23
 
20
24
  ## Release Checklist
21
25
 
22
26
  Before publishing:
23
27
 
24
28
  ```sh
25
- bun run lint -- --filter=react-native-nitro-storage
26
- bun run format:check -- --filter=react-native-nitro-storage
27
- bun run typecheck -- --filter=react-native-nitro-storage
28
- bun run test:types -- --filter=react-native-nitro-storage
29
- bun run test -- --filter=react-native-nitro-storage
30
- bun run test:cpp -- --filter=react-native-nitro-storage
31
- bun run build -- --filter=react-native-nitro-storage
32
- bun run benchmark -- --filter=react-native-nitro-storage
33
- (cd packages/react-native-nitro-storage && bun run check:pack)
34
- npm publish --dry-run
29
+ bun run codegen:check
30
+ bun run lint:check
31
+ bun run format:check
32
+ bun run typecheck
33
+ bun run test:types
34
+ bun run test
35
+ bun run test:cpp
36
+ bun run build
37
+ bun run benchmark
38
+ bun run --cwd packages/react-native-nitro-storage check:pack
35
39
  ```
36
40
 
37
41
  Keep the dry-publish output in the release notes when validating a version locally.
@@ -38,7 +38,7 @@ Optional methods improve performance and observability:
38
38
 
39
39
  `subscribe(listener)` should report `{ key, newValue }` changes. Use `key: null` when the whole backend is cleared.
40
40
 
41
- `close()` should release backend-owned resources such as database handles or broadcast channels. Nitro Storage calls it when a configured Disk or Secure backend is replaced.
41
+ `close()` should release backend-owned resources such as database handles or broadcast channels. A replaced backend is retired instead of closed on the spot: `flushWebStorageBackends()` flushes it and then closes it, so queued async writes can commit before the connection goes away. Backends without `flush()` are closed immediately on replacement, and a retired backend whose flush fails stays retired for a later explicit retry.
42
42
 
43
43
  ## Disk Backend
44
44
 
@@ -100,6 +100,23 @@ Reads are synchronous because they are served from memory after initial load. Wr
100
100
 
101
101
  The IndexedDB backend exposes `close()` and rejects later synchronous operations after it is closed.
102
102
 
103
+ ### Persistence Lifecycle
104
+
105
+ IndexedDB transactions cannot block a page unload, so in-flight writes may be aborted when the page closes. The backend mitigates this by starting a flush on `pagehide` and on `visibilitychange` (hidden), and `flush()` awaits every pending transaction. When persistence fails, `flush()` throws an error that names the affected keys, and `onError` receives each individual failure.
106
+
107
+ ```ts
108
+ const backend = await createIndexedDBBackend("app-secure", "keyvalue", {
109
+ onError: (error) => {
110
+ console.error("IndexedDB secure storage failed", error);
111
+ },
112
+ });
113
+
114
+ // Before assertions, navigation, or lifecycle boundaries:
115
+ await flushWebStorageBackends();
116
+ ```
117
+
118
+ Treat IndexedDB persistence as best-effort under abrupt termination: keep a durable copy of critical values elsewhere if they must survive an immediate page close.
119
+
103
120
  ## Cross-tab Updates
104
121
 
105
122
  The IndexedDB backend uses `BroadcastChannel` when available. Other tabs receive cache invalidation events and update their in-memory copy.