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.
- package/CHANGELOG.md +54 -0
- package/README.md +89 -22
- package/SECURITY.md +2 -2
- package/cpp/bindings/HybridStorage.cpp +5 -12
- package/cpp/bindings/HybridStorage.hpp +1 -1
- package/docs/api-reference.md +11 -1
- package/docs/batch-transactions-migrations.md +8 -2
- package/docs/benchmarks.md +19 -15
- package/docs/web-backends.md +18 -1
- package/ios/IOSStorageAdapterCpp.mm +162 -17
- package/lib/commonjs/capabilities.js +26 -0
- package/lib/commonjs/capabilities.js.map +1 -0
- package/lib/commonjs/core/durability.js +137 -0
- package/lib/commonjs/core/durability.js.map +1 -0
- package/lib/commonjs/core/metrics.js +91 -0
- package/lib/commonjs/core/metrics.js.map +1 -0
- package/lib/commonjs/index.js +47 -22
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +118 -66
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +73 -23
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/internal.js +27 -4
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +2 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +224 -237
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-events.js.map +1 -1
- package/lib/commonjs/storage-platform.js +6 -0
- package/lib/commonjs/storage-platform.js.map +1 -0
- package/lib/commonjs/storage-runtime.js +3 -21
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +19 -4
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/commonjs/web-backend-contract.js +27 -0
- package/lib/commonjs/web-backend-contract.js.map +1 -0
- package/lib/module/capabilities.js +21 -0
- package/lib/module/capabilities.js.map +1 -0
- package/lib/module/core/durability.js +133 -0
- package/lib/module/core/durability.js.map +1 -0
- package/lib/module/core/metrics.js +87 -0
- package/lib/module/core/metrics.js.map +1 -0
- package/lib/module/index.js +48 -23
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +103 -64
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +73 -23
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/internal.js +24 -3
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +2 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +225 -238
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-events.js.map +1 -1
- package/lib/module/storage-platform.js +4 -0
- package/lib/module/storage-platform.js.map +1 -0
- package/lib/module/storage-runtime.js +3 -21
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +5 -3
- package/lib/module/testing.js.map +1 -1
- package/lib/module/web-backend-contract.js +22 -0
- package/lib/module/web-backend-contract.js.map +1 -0
- package/lib/typescript/capabilities.d.ts +11 -0
- package/lib/typescript/capabilities.d.ts.map +1 -0
- package/lib/typescript/core/durability.d.ts +27 -0
- package/lib/typescript/core/durability.d.ts.map +1 -0
- package/lib/typescript/core/metrics.d.ts +12 -0
- package/lib/typescript/core/metrics.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +5 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +8 -4
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +3 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +2 -1
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +6 -6
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-events.d.ts +2 -2
- package/lib/typescript/storage-events.d.ts.map +1 -1
- package/lib/typescript/storage-platform.d.ts +12 -0
- package/lib/typescript/storage-platform.d.ts.map +1 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +5 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/lib/typescript/web-backend-contract.d.ts +10 -0
- package/lib/typescript/web-backend-contract.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +1 -1
- package/lib/typescript/web-storage-backend.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
- package/package.json +4 -3
- package/react-native-nitro-storage.podspec +3 -1
- package/src/capabilities.ts +36 -0
- package/src/core/durability.ts +170 -0
- package/src/core/metrics.ts +133 -0
- package/src/index.ts +66 -40
- package/src/index.web.ts +158 -86
- package/src/indexeddb-backend.ts +91 -20
- package/src/internal.ts +50 -4
- package/src/shared.ts +7 -6
- package/src/storage-core.ts +327 -292
- package/src/storage-events.ts +2 -1
- package/src/storage-platform.ts +16 -0
- package/src/storage-runtime.ts +13 -47
- package/src/testing.ts +14 -7
- package/src/web-backend-contract.ts +34 -0
- 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
|
[](https://www.npmjs.com/package/react-native-nitro-storage)
|
|
5
5
|
[](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/actions/workflows/ci.yml)
|
|
6
6
|
[](https://github.com/JoaoPauloCMarra/react-native-nitro-storage/blob/main/LICENSE)
|
|
7
|
-
[](https://docs.expo.dev/versions/
|
|
9
|
-
[](https://reactnative.dev/docs/0.87/getting-started-without-a-framework)
|
|
8
|
+
[](https://docs.expo.dev/versions/v57.0.0/)
|
|
9
|
+
[](https://nitro.margelo.com/)
|
|
10
10
|
[](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.
|
|
61
|
+
| `react-native-nitro-modules` | `>=0.37.0 <0.38.0` |
|
|
61
62
|
|
|
62
|
-
Nitro peer requirement: `react-native-nitro-modules >=0.
|
|
63
|
+
Nitro peer requirement: `react-native-nitro-modules >=0.37.0 <0.38.0`.
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
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(); //
|
|
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
|
|
198
|
-
|
|
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.
|
|
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
|
|
538
|
-
|
|
|
539
|
-
| iOS
|
|
540
|
-
| Android
|
|
541
|
-
| Web
|
|
542
|
-
| Expo
|
|
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
|
@@ -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
|
|
392
|
-
std::vector<std::string
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
package/docs/api-reference.md
CHANGED
|
@@ -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.
|
|
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.
|
package/docs/benchmarks.md
CHANGED
|
@@ -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
|
|
8
|
+
bun run build
|
|
9
|
+
bun run benchmark
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
|
|
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,
|
|
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
|
|
26
|
-
bun run
|
|
27
|
-
bun run
|
|
28
|
-
bun run
|
|
29
|
-
bun run test
|
|
30
|
-
bun run test
|
|
31
|
-
bun run
|
|
32
|
-
bun run
|
|
33
|
-
|
|
34
|
-
|
|
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.
|
package/docs/web-backends.md
CHANGED
|
@@ -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.
|
|
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.
|