react-native-nitro-storage 0.3.0 → 0.3.2

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 (53) hide show
  1. package/README.md +594 -247
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +102 -11
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +16 -0
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +154 -34
  6. package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
  7. package/cpp/bindings/HybridStorage.cpp +176 -21
  8. package/cpp/bindings/HybridStorage.hpp +29 -2
  9. package/cpp/core/NativeStorageAdapter.hpp +16 -0
  10. package/ios/IOSStorageAdapterCpp.hpp +20 -0
  11. package/ios/IOSStorageAdapterCpp.mm +239 -32
  12. package/lib/commonjs/Storage.types.js +23 -1
  13. package/lib/commonjs/Storage.types.js.map +1 -1
  14. package/lib/commonjs/index.js +292 -75
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/index.web.js +473 -86
  17. package/lib/commonjs/index.web.js.map +1 -1
  18. package/lib/commonjs/internal.js +10 -0
  19. package/lib/commonjs/internal.js.map +1 -1
  20. package/lib/commonjs/storage-hooks.js +36 -0
  21. package/lib/commonjs/storage-hooks.js.map +1 -0
  22. package/lib/module/Storage.types.js +22 -0
  23. package/lib/module/Storage.types.js.map +1 -1
  24. package/lib/module/index.js +264 -75
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/index.web.js +445 -86
  27. package/lib/module/index.web.js.map +1 -1
  28. package/lib/module/internal.js +8 -0
  29. package/lib/module/internal.js.map +1 -1
  30. package/lib/module/storage-hooks.js +30 -0
  31. package/lib/module/storage-hooks.js.map +1 -0
  32. package/lib/typescript/Storage.nitro.d.ts +12 -0
  33. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  34. package/lib/typescript/Storage.types.d.ts +20 -0
  35. package/lib/typescript/Storage.types.d.ts.map +1 -1
  36. package/lib/typescript/index.d.ts +33 -10
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/index.web.d.ts +45 -10
  39. package/lib/typescript/index.web.d.ts.map +1 -1
  40. package/lib/typescript/internal.d.ts +2 -0
  41. package/lib/typescript/internal.d.ts.map +1 -1
  42. package/lib/typescript/storage-hooks.d.ts +10 -0
  43. package/lib/typescript/storage-hooks.d.ts.map +1 -0
  44. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +12 -0
  45. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +12 -0
  46. package/package.json +8 -3
  47. package/src/Storage.nitro.ts +13 -2
  48. package/src/Storage.types.ts +22 -0
  49. package/src/index.ts +382 -123
  50. package/src/index.web.ts +618 -134
  51. package/src/internal.ts +14 -4
  52. package/src/migration.ts +1 -1
  53. package/src/storage-hooks.ts +48 -0
package/README.md CHANGED
@@ -1,12 +1,49 @@
1
1
  # react-native-nitro-storage
2
2
 
3
- Synchronous storage for React Native with a unified API for memory, disk, and secure data.
3
+ The fastest, most complete storage solution for React Native.
4
+ Synchronous Memory, Disk, and Secure storage in one unified API — powered by [Nitro Modules](https://github.com/mrousavy/nitro) and JSI.
5
+
6
+ ## Highlights
7
+
8
+ - **Three storage scopes** — in-memory, persistent disk, and hardware-encrypted secure storage
9
+ - **Synchronous reads & writes** — no `async/await`, no bridge, zero serialization overhead for primitives
10
+ - **React hooks** — `useStorage`, `useStorageSelector`, `useSetStorage` with automatic re-renders
11
+ - **Type-safe** — full TypeScript generics, custom serializers, schema validation with fallback
12
+ - **Namespaces** — isolate keys by feature, user, or tenant with automatic prefixing
13
+ - **TTL expiration** — time-based auto-expiry with optional `onExpired` callback
14
+ - **Biometric storage** — hardware-backed biometric protection on iOS & Android
15
+ - **Auth storage factory** — `createSecureAuthStorage` for multi-token auth flows
16
+ - **Batch operations** — atomic multi-key get/set/remove via native batch APIs
17
+ - **Transactions** — grouped writes with automatic rollback on error
18
+ - **Migrations** — versioned data migrations with `registerMigration` / `migrateToLatest`
19
+ - **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
20
+ - **Cross-platform** — iOS, Android, and web (`localStorage` fallback)
21
+
22
+ ## Feature Coverage
23
+
24
+ Every feature in this package is documented with at least one runnable example in this README:
25
+
26
+ - Core item API (`createStorageItem`, `get/set/delete/has/subscribe`) — see Quick Start and Low-level subscription use case
27
+ - Hooks (`useStorage`, `useStorageSelector`, `useSetStorage`) — see Quick Start and Persisted User Preferences
28
+ - Scopes (`Memory`, `Disk`, `Secure`) — see Storage Scopes and multiple use cases
29
+ - Namespaces — see Multi-Tenant / Namespaced Storage
30
+ - TTL expiration + callbacks — see OTP / Temporary Codes
31
+ - Validation + recovery — see Feature Flags with Validation
32
+ - Biometric + access control — see Biometric-protected Secrets
33
+ - Global storage utilities (`clear*`, `has`, `getAll*`, `size`, secure write settings) — see Global utility examples and Storage Snapshots and Cleanup
34
+ - Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
35
+ - Transactions — see Transactions and Atomic Balance Transfer
36
+ - Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
37
+ - MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
38
+ - Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
4
39
 
5
40
  ## Requirements
6
41
 
7
- - `react-native >= 0.75.0`
8
- - `react-native-nitro-modules >= 0.33.9`
9
- - `react >= 18.2.0`
42
+ | Dependency | Version |
43
+ | ---------------------------- | ----------- |
44
+ | `react-native` | `>= 0.75.0` |
45
+ | `react-native-nitro-modules` | `>= 0.33.9` |
46
+ | `react` | `>= 18.2.0` |
10
47
 
11
48
  ## Installation
12
49
 
@@ -14,13 +51,19 @@ Synchronous storage for React Native with a unified API for memory, disk, and se
14
51
  bun add react-native-nitro-storage react-native-nitro-modules
15
52
  ```
16
53
 
54
+ or:
55
+
56
+ ```bash
57
+ npm install react-native-nitro-storage react-native-nitro-modules
58
+ ```
59
+
17
60
  ### Expo
18
61
 
19
62
  ```bash
20
63
  bunx expo install react-native-nitro-storage react-native-nitro-modules
21
64
  ```
22
65
 
23
- `app.json`:
66
+ Add the config plugin to `app.json`:
24
67
 
25
68
  ```json
26
69
  {
@@ -38,12 +81,9 @@ bunx expo install react-native-nitro-storage react-native-nitro-modules
38
81
  }
39
82
  ```
40
83
 
41
- Notes:
42
-
43
- - If `faceIDPermission` is omitted, the plugin sets a default only when `NSFaceIDUsageDescription` is missing.
44
- - Android biometric permissions are opt-in via `addBiometricPermissions: true`.
84
+ > `faceIDPermission` sets `NSFaceIDUsageDescription` only when missing. Android biometric permissions are opt-in via `addBiometricPermissions: true`.
45
85
 
46
- Then:
86
+ Then run:
47
87
 
48
88
  ```bash
49
89
  bunx expo prebuild
@@ -51,13 +91,13 @@ bunx expo prebuild
51
91
 
52
92
  ### Bare React Native
53
93
 
54
- iOS:
94
+ **iOS:**
55
95
 
56
96
  ```bash
57
97
  cd ios && pod install
58
98
  ```
59
99
 
60
- Android (`MainApplication.kt`):
100
+ **Android** — initialize the native adapter in `MainApplication.kt`:
61
101
 
62
102
  ```kotlin
63
103
  import com.nitrostorage.AndroidStorageAdapter
@@ -70,11 +110,14 @@ class MainApplication : Application() {
70
110
  }
71
111
  ```
72
112
 
113
+ ---
114
+
73
115
  ## Quick Start
74
116
 
75
117
  ```ts
76
118
  import { createStorageItem, StorageScope, useStorage } from "react-native-nitro-storage";
77
119
 
120
+ // define a storage item outside of components
78
121
  const counterItem = createStorageItem({
79
122
  key: "counter",
80
123
  scope: StorageScope.Memory,
@@ -93,369 +136,673 @@ export function Counter() {
93
136
  }
94
137
  ```
95
138
 
96
- ## What Is Exported
97
-
98
- - `StorageScope`
99
- - `storage`
100
- - `createStorageItem`
101
- - `useStorage`
102
- - `useStorageSelector`
103
- - `useSetStorage`
104
- - `getBatch`
105
- - `setBatch`
106
- - `removeBatch`
107
- - `registerMigration`
108
- - `migrateToLatest`
109
- - `runTransaction`
110
- - `migrateFromMMKV`
111
-
112
- Exported types:
113
-
114
- - `Storage`
115
- - `StorageItemConfig<T>`
116
- - `StorageItem<T>`
117
- - `StorageBatchSetItem<T>`
118
- - `Validator<T>`
119
- - `ExpirationConfig`
120
- - `MigrationContext`
121
- - `Migration`
122
- - `TransactionContext`
123
- - `MMKVLike`
139
+ ---
140
+
141
+ ## Storage Scopes
142
+
143
+ | Scope | Backend (iOS) | Backend (Android) | Backend (Web) | Persisted |
144
+ | -------- | ------------------------ | -------------------------- | ------------------------------------------------ | --------- |
145
+ | `Memory` | In-process JS Map | In-process JS Map | In-process JS Map | No |
146
+ | `Disk` | UserDefaults (app suite) | SharedPreferences | `localStorage` | Yes |
147
+ | `Secure` | Keychain (AES-256 GCM) | EncryptedSharedPreferences | `localStorage` (`__secure_` + `__bio_` prefixes) | Yes |
148
+
149
+ ```ts
150
+ import { StorageScope } from "react-native-nitro-storage";
151
+
152
+ StorageScope.Memory; // 0 — ephemeral, fastest
153
+ StorageScope.Disk; // 1 — persistent, fast
154
+ StorageScope.Secure; // 2 — encrypted, slightly slower
155
+ ```
156
+
157
+ ---
124
158
 
125
159
  ## API Reference
126
160
 
127
- ### `StorageScope`
161
+ ### `createStorageItem<T>(config)`
162
+
163
+ The core factory. Creates a reactive storage item that can be used standalone or with hooks.
128
164
 
129
165
  ```ts
130
- enum StorageScope {
131
- Memory = 0,
132
- Disk = 1,
133
- Secure = 2,
134
- }
166
+ function createStorageItem<T = undefined>(
167
+ config: StorageItemConfig<T>,
168
+ ): StorageItem<T>;
135
169
  ```
136
170
 
137
- ### `Storage` (low-level native/web adapter type)
171
+ **Config options:**
172
+
173
+ | Property | Type | Default | Description |
174
+ | ---------------------- | -------------------------------- | -------------- | -------------------------------------------------------------- |
175
+ | `key` | `string` | _required_ | Storage key identifier |
176
+ | `scope` | `StorageScope` | _required_ | Where to store the data |
177
+ | `defaultValue` | `T` | `undefined` | Value returned when no data exists |
178
+ | `serialize` | `(value: T) => string` | JSON fast path | Custom serialization |
179
+ | `deserialize` | `(value: string) => T` | JSON fast path | Custom deserialization |
180
+ | `validate` | `(value: unknown) => value is T` | — | Type guard run on every read |
181
+ | `onValidationError` | `(invalidValue: unknown) => T` | — | Recovery function when validation fails |
182
+ | `expiration` | `{ ttlMs: number }` | — | Time-to-live in milliseconds |
183
+ | `onExpired` | `(key: string) => void` | — | Callback fired when a TTL value expires on read |
184
+ | `readCache` | `boolean` | `false` | Cache deserialized values in JS (avoids repeated native reads) |
185
+ | `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
186
+ | `namespace` | `string` | — | Prefix key as `namespace:key` for isolation |
187
+ | `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
188
+ | `accessControl` | `AccessControl` | — | Keychain access control level (native only) |
189
+
190
+ **Returned `StorageItem<T>`:**
191
+
192
+ | Method / Property | Type | Description |
193
+ | ----------------- | ---------------------------------------- | -------------------------------------------------- |
194
+ | `get()` | `() => T` | Read current value (synchronous) |
195
+ | `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
196
+ | `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
197
+ | `has()` | `() => boolean` | Check if a value exists in storage |
198
+ | `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
199
+ | `serialize` | `(v: T) => string` | The item's serializer |
200
+ | `deserialize` | `(v: string) => T` | The item's deserializer |
201
+ | `scope` | `StorageScope` | The item's scope |
202
+ | `key` | `string` | The resolved key (includes namespace prefix) |
203
+
204
+ **Non-React subscription example:**
138
205
 
139
206
  ```ts
140
- type Storage = {
141
- set(key: string, value: string, scope: number): void;
142
- get(key: string, scope: number): string | undefined;
143
- remove(key: string, scope: number): void;
144
- clear(scope: number): void;
145
- setBatch(keys: string[], values: string[], scope: number): void;
146
- getBatch(keys: string[], scope: number): (string | undefined)[];
147
- removeBatch(keys: string[], scope: number): void;
148
- addOnChange(
149
- scope: number,
150
- callback: (key: string, value: string | undefined) => void
151
- ): () => void;
152
- };
207
+ const unsubscribe = sessionItem.subscribe(() => {
208
+ console.log("session changed:", sessionItem.get());
209
+ });
210
+
211
+ sessionItem.set("next-session");
212
+ unsubscribe();
153
213
  ```
154
214
 
155
- Notes:
215
+ ---
216
+
217
+ ### React Hooks
156
218
 
157
- - Exported for typing/integration use cases.
158
- - Most app code should use `createStorageItem` + hooks instead of this low-level API.
219
+ #### `useStorage(item)`
159
220
 
160
- ### `StorageItemConfig<T>`
221
+ Full reactive binding. Re-renders when the value changes.
161
222
 
162
223
  ```ts
163
- type StorageItemConfig<T> = {
164
- key: string;
165
- scope: StorageScope;
166
- defaultValue?: T;
167
- serialize?: (value: T) => string;
168
- deserialize?: (value: string) => T;
169
- validate?: Validator<T>;
170
- onValidationError?: (invalidValue: unknown) => T;
171
- expiration?: ExpirationConfig;
172
- readCache?: boolean;
173
- coalesceSecureWrites?: boolean;
174
- };
224
+ const [value, setValue] = useStorage(item);
175
225
  ```
176
226
 
177
- ### `StorageItem<T>`
227
+ #### `useStorageSelector(item, selector, isEqual?)`
228
+
229
+ Subscribe to a derived slice. Only re-renders when the selected value changes.
178
230
 
179
231
  ```ts
180
- type StorageItem<T> = {
181
- get: () => T;
182
- set: (value: T | ((prev: T) => T)) => void;
183
- delete: () => void;
184
- subscribe: (callback: () => void) => () => void;
185
- serialize: (value: T) => string;
186
- deserialize: (value: string) => T;
187
- _triggerListeners: () => void;
188
- scope: StorageScope;
189
- key: string;
190
- };
232
+ const [theme, setSettings] = useStorageSelector(settingsItem, (s) => s.theme);
191
233
  ```
192
234
 
193
- ### `createStorageItem<T>(config)`
235
+ #### `useSetStorage(item)`
236
+
237
+ Write-only hook. Useful when a component needs to update a value but doesn't depend on it.
238
+
239
+ ```ts
240
+ const setToken = useSetStorage(tokenItem);
241
+ setToken("new-token");
242
+ ```
243
+
244
+ ---
245
+
246
+ ### `storage` — Global Utilities
194
247
 
195
248
  ```ts
196
- function createStorageItem<T = undefined>(config: StorageItemConfig<T>): StorageItem<T>
249
+ import { storage, StorageScope } from "react-native-nitro-storage";
197
250
  ```
198
251
 
199
- Notes:
252
+ | Method | Description |
253
+ | --------------------------------------- | ---------------------------------------------------------------------------- |
254
+ | `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
255
+ | `storage.clearAll()` | Clear Memory + Disk + Secure |
256
+ | `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
257
+ | `storage.clearBiometric()` | Remove all biometric-prefixed keys |
258
+ | `storage.has(key, scope)` | Check if a key exists |
259
+ | `storage.getAllKeys(scope)` | Get all key names |
260
+ | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
261
+ | `storage.size(scope)` | Number of stored keys |
262
+ | `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
263
+ | `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
264
+ | `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
265
+ | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
266
+
267
+ > `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
268
+
269
+ #### Global utility examples
200
270
 
201
- - `Memory` stores values directly.
202
- - `Disk` and `Secure` store serialized values.
203
- - Default serialization uses a primitive fast path for strings/numbers/booleans/null/undefined and JSON for objects/arrays.
204
- - If `expiration` is enabled, values are wrapped internally and expired lazily on read.
205
- - `readCache` is opt-in for `Disk`/`Secure` and can be enabled per item.
206
- - `coalesceSecureWrites` is opt-in and batches same-tick secure writes per key.
207
- - If `validate` fails on a stored value, fallback is:
208
- 1. `onValidationError(invalidValue)` if provided
209
- 2. `defaultValue` otherwise
210
- - Fallback values are written back only when the source was stored data and the resolved fallback also passes `validate`.
271
+ ```ts
272
+ import { AccessControl, storage, StorageScope } from "react-native-nitro-storage";
273
+
274
+ storage.has("session", StorageScope.Disk);
275
+ storage.getAllKeys(StorageScope.Disk);
276
+ storage.getAll(StorageScope.Disk);
277
+ storage.size(StorageScope.Disk);
278
+
279
+ storage.clearNamespace("user-42", StorageScope.Disk);
280
+ storage.clearBiometric();
281
+
282
+ storage.setAccessControl(AccessControl.WhenUnlockedThisDeviceOnly);
283
+ storage.setKeychainAccessGroup("group.com.example.shared");
284
+
285
+ storage.clear(StorageScope.Memory);
286
+ storage.clearAll();
287
+ ```
211
288
 
212
- Throws:
289
+ #### Android secure write mode
213
290
 
214
- - `Error("expiration.ttlMs must be greater than 0.")` when `expiration.ttlMs <= 0`.
291
+ `storage.setSecureWritesAsync(true)` switches secure writes from synchronous `commit()` to asynchronous `apply()` on Android.
292
+ Use this for non-critical secure writes when lower latency matters more than immediate durability.
215
293
 
216
- ### `useStorage(item)`
294
+ Call `storage.flushSecureWrites()` when you need deterministic persistence boundaries (for example before namespace clears, process handoff, or strict test assertions).
217
295
 
218
296
  ```ts
219
- function useStorage<T>(
220
- item: StorageItem<T>
221
- ): [T, (value: T | ((prev: T) => T)) => void]
297
+ import { storage } from "react-native-nitro-storage";
298
+
299
+ storage.setSecureWritesAsync(true);
300
+
301
+ // ...multiple secure writes happen (including coalesced item writes)
302
+
303
+ storage.flushSecureWrites(); // deterministic durability boundary
222
304
  ```
223
305
 
224
- ### `useStorageSelector(item, selector, isEqual?)`
306
+ ---
307
+
308
+ ### `createSecureAuthStorage<K>(config, options?)`
309
+
310
+ One-liner factory for authentication flows. Creates multiple `StorageItem<string>` entries in Secure scope.
225
311
 
226
312
  ```ts
227
- function useStorageSelector<T, TSelected>(
228
- item: StorageItem<T>,
229
- selector: (value: T) => TSelected,
230
- isEqual?: (prev: TSelected, next: TSelected) => boolean
231
- ): [TSelected, (value: T | ((prev: T) => T)) => void]
313
+ function createSecureAuthStorage<K extends string>(
314
+ config: SecureAuthStorageConfig<K>,
315
+ options?: { namespace?: string },
316
+ ): Record<K, StorageItem<string>>;
232
317
  ```
233
318
 
234
- Use this to subscribe to a derived slice of a storage value and avoid rerenders when that slice does not change.
319
+ - Default namespace: `"auth"`
320
+ - Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
321
+ - Supports per-key TTL, biometric, and access control
322
+
323
+ ---
235
324
 
236
- ### `useSetStorage(item)`
325
+ ### Batch Operations
326
+
327
+ Atomic multi-key operations. Uses native batch APIs for best performance.
237
328
 
238
329
  ```ts
239
- function useSetStorage<T>(
240
- item: StorageItem<T>
241
- ): (value: T | ((prev: T) => T)) => void
330
+ import { getBatch, setBatch, removeBatch } from "react-native-nitro-storage";
331
+
332
+ // Read multiple items at once
333
+ const [a, b, c] = getBatch([itemA, itemB, itemC], StorageScope.Disk);
334
+
335
+ // Write multiple items atomically
336
+ setBatch(
337
+ [
338
+ { item: itemA, value: "hello" },
339
+ { item: itemB, value: "world" },
340
+ ],
341
+ StorageScope.Disk,
342
+ );
343
+
344
+ // Remove multiple items
345
+ removeBatch([itemA, itemB], StorageScope.Disk);
242
346
  ```
243
347
 
244
- ### `storage`
348
+ > All items in a batch must share the same scope. Items with `validate` or `expiration` automatically use per-item paths to preserve semantics.
349
+
350
+ ---
351
+
352
+ ### Transactions
353
+
354
+ Grouped writes with automatic rollback on error.
245
355
 
246
356
  ```ts
247
- const storage: {
248
- clear: (scope: StorageScope) => void;
249
- clearAll: () => void;
250
- };
357
+ import { runTransaction, StorageScope } from "react-native-nitro-storage";
358
+
359
+ runTransaction(StorageScope.Disk, (tx) => {
360
+ const balance = tx.getItem(balanceItem);
361
+ tx.setItem(balanceItem, balance - 50);
362
+ tx.setItem(logItem, `Deducted 50 at ${new Date().toISOString()}`);
363
+
364
+ if (balance - 50 < 0) throw new Error("Insufficient funds");
365
+ // if this throws, both writes are rolled back
366
+ });
251
367
  ```
252
368
 
253
- Behavior:
369
+ **TransactionContext methods:**
254
370
 
255
- - `clear(scope)` clears all keys in a single scope.
256
- - `clearAll()` clears `Memory`, `Disk`, and `Secure`.
371
+ | Method | Description |
372
+ | ---------------------- | --------------------------- |
373
+ | `getItem(item)` | Read a StorageItem's value |
374
+ | `setItem(item, value)` | Write a StorageItem's value |
375
+ | `removeItem(item)` | Delete a StorageItem |
376
+ | `getRaw(key)` | Read raw string by key |
377
+ | `setRaw(key, value)` | Write raw string by key |
378
+ | `removeRaw(key)` | Delete raw key |
257
379
 
258
- ### Batch Operations
380
+ ---
381
+
382
+ ### Migrations
383
+
384
+ Versioned, sequential data migrations.
259
385
 
260
386
  ```ts
261
- type StorageBatchSetItem<T> = {
262
- item: StorageItem<T>;
263
- value: T;
264
- };
387
+ import {
388
+ registerMigration,
389
+ migrateToLatest,
390
+ StorageScope,
391
+ } from "react-native-nitro-storage";
265
392
 
266
- function getBatch(
267
- items: readonly Pick<StorageItem<unknown>, "key" | "scope" | "get" | "deserialize">[],
268
- scope: StorageScope
269
- ): unknown[];
393
+ registerMigration(1, ({ setRaw }) => {
394
+ setRaw("onboarding-complete", "false");
395
+ });
270
396
 
271
- function setBatch<T>(
272
- items: readonly StorageBatchSetItem<T>[],
273
- scope: StorageScope
274
- ): void;
397
+ registerMigration(2, ({ getRaw, setRaw, removeRaw }) => {
398
+ const raw = getRaw("legacy-key");
399
+ if (raw) {
400
+ setRaw("new-key", raw);
401
+ removeRaw("legacy-key");
402
+ }
403
+ });
275
404
 
276
- function removeBatch(
277
- items: readonly Pick<StorageItem<unknown>, "key" | "scope" | "delete">[],
278
- scope: StorageScope
279
- ): void;
405
+ // apply all pending migrations (runs once per scope)
406
+ migrateToLatest(StorageScope.Disk);
280
407
  ```
281
408
 
282
- Rules:
409
+ - Versions must be positive integers, registered in any order, applied ascending
410
+ - Version state is tracked per scope via `__nitro_storage_migration_version__`
411
+ - Duplicate versions throw at registration time
283
412
 
284
- - All items must match the batch `scope`.
285
- - Items using `validate` or `expiration` automatically run via per-item `get()`/`set()` paths to preserve validation and TTL behavior.
286
- - Mixed-scope calls throw:
287
- - `Batch scope mismatch for "<key>": expected <Scope>, received <Scope>.`
413
+ ---
288
414
 
289
- ### Migrations
415
+ ### MMKV Migration
416
+
417
+ Drop-in helper for migrating from `react-native-mmkv`.
290
418
 
291
419
  ```ts
292
- type MigrationContext = {
293
- scope: StorageScope;
294
- getRaw: (key: string) => string | undefined;
295
- setRaw: (key: string, value: string) => void;
296
- removeRaw: (key: string) => void;
297
- };
420
+ import { migrateFromMMKV } from "react-native-nitro-storage";
421
+ import { MMKV } from "react-native-mmkv";
298
422
 
299
- type Migration = (context: MigrationContext) => void;
423
+ const mmkv = new MMKV();
300
424
 
301
- function registerMigration(version: number, migration: Migration): void;
302
- function migrateToLatest(scope?: StorageScope): number;
425
+ const migrated = migrateFromMMKV(mmkv, myStorageItem, true);
426
+ // true → value found and copied, original deleted from MMKV
427
+ // false → no matching key in MMKV
303
428
  ```
304
429
 
305
- Behavior:
430
+ - Read priority: `getString` → `getNumber` → `getBoolean`
431
+ - Uses `item.set()` so validation still applies
432
+ - Only deletes from MMKV when migration succeeds
306
433
 
307
- - Versions must be positive integers.
308
- - Duplicate versions throw.
309
- - Migration version is tracked per scope using key `__nitro_storage_migration_version__`.
310
- - `migrateToLatest` applies pending migrations in ascending version order and returns applied/latest version.
434
+ ---
311
435
 
312
- Throws:
436
+ ### Enums
313
437
 
314
- - `registerMigration`: throws when version is not a positive integer.
315
- - `registerMigration`: throws when version is already registered.
316
- - `migrateToLatest`: throws on invalid scope.
438
+ #### `AccessControl`
317
439
 
318
- ### Transactions
440
+ Controls keychain item access requirements (iOS Keychain / Android Keystore). No-op on web.
319
441
 
320
442
  ```ts
321
- type TransactionContext = {
322
- scope: StorageScope;
323
- getRaw: (key: string) => string | undefined;
324
- setRaw: (key: string, value: string) => void;
325
- removeRaw: (key: string) => void;
326
- getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
327
- setItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "set">, value: T) => void;
328
- removeItem: (item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">) => void;
329
- };
443
+ enum AccessControl {
444
+ WhenUnlocked = 0,
445
+ AfterFirstUnlock = 1,
446
+ WhenPasscodeSetThisDeviceOnly = 2,
447
+ WhenUnlockedThisDeviceOnly = 3,
448
+ AfterFirstUnlockThisDeviceOnly = 4,
449
+ }
450
+ ```
451
+
452
+ #### `BiometricLevel`
330
453
 
331
- function runTransaction<T>(
332
- scope: StorageScope,
333
- transaction: (context: TransactionContext) => T
334
- ): T;
454
+ ```ts
455
+ enum BiometricLevel {
456
+ None = 0,
457
+ BiometryOrPasscode = 1,
458
+ BiometryOnly = 2,
459
+ }
335
460
  ```
336
461
 
337
- Behavior:
462
+ ---
338
463
 
339
- - On exception, it rolls back keys modified in that transaction.
340
- - Rollback is best-effort within process lifetime.
341
- - `setItem`/`removeItem` prefer item methods when available, so validation/TTL/cache semantics stay consistent.
464
+ ## Use Cases
342
465
 
343
- Throws:
466
+ ### Persisted User Preferences
344
467
 
345
- - Throws on invalid scope.
346
- - Rethrows any error thrown by the transaction callback after rollback.
468
+ ```ts
469
+ type UserPreferences = {
470
+ theme: "light" | "dark" | "system";
471
+ language: string;
472
+ notifications: boolean;
473
+ };
347
474
 
348
- ### Validation and Expiration Types
475
+ const prefsItem = createStorageItem<UserPreferences>({
476
+ key: "prefs",
477
+ scope: StorageScope.Disk,
478
+ defaultValue: { theme: "system", language: "en", notifications: true },
479
+ });
480
+
481
+ // in a component — only re-renders when theme changes
482
+ const [theme, setPrefs] = useStorageSelector(prefsItem, (p) => p.theme);
483
+ ```
484
+
485
+ ### Auth Token Management
349
486
 
350
487
  ```ts
351
- type Validator<T> = (value: unknown) => value is T;
488
+ const auth = createSecureAuthStorage(
489
+ {
490
+ accessToken: { ttlMs: 15 * 60_000, biometric: true },
491
+ refreshToken: { ttlMs: 7 * 24 * 60 * 60_000 },
492
+ idToken: {},
493
+ },
494
+ { namespace: "myapp-auth" },
495
+ );
496
+
497
+ // after login
498
+ auth.accessToken.set(response.accessToken);
499
+ auth.refreshToken.set(response.refreshToken);
500
+ auth.idToken.set(response.idToken);
501
+
502
+ // check if token exists and hasn't expired
503
+ if (auth.accessToken.has()) {
504
+ const token = auth.accessToken.get();
505
+ // use token
506
+ } else {
507
+ // refresh or re-login
508
+ }
352
509
 
353
- type ExpirationConfig = {
354
- ttlMs: number;
355
- };
510
+ // logout
511
+ storage.clearNamespace("myapp-auth", StorageScope.Secure);
356
512
  ```
357
513
 
358
- ### MMKV Migration
514
+ ### Feature Flags with Validation
359
515
 
360
516
  ```ts
361
- type MMKVLike = {
362
- getString: (key: string) => string | undefined;
363
- getNumber: (key: string) => number | undefined;
364
- getBoolean: (key: string) => boolean | undefined;
365
- contains: (key: string) => boolean;
366
- delete: (key: string) => void;
367
- getAllKeys: () => string[];
517
+ type FeatureFlags = {
518
+ darkMode: boolean;
519
+ betaFeature: boolean;
520
+ maxUploadMb: number;
521
+ };
522
+
523
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
524
+ typeof value === "object" && value !== null;
525
+
526
+ const isFeatureFlags = (value: unknown): value is FeatureFlags => {
527
+ if (!isRecord(value)) return false;
528
+ return (
529
+ typeof value.darkMode === "boolean" &&
530
+ typeof value.betaFeature === "boolean" &&
531
+ typeof value.maxUploadMb === "number"
532
+ );
368
533
  };
369
534
 
370
- function migrateFromMMKV<T>(
371
- mmkv: MMKVLike,
372
- item: StorageItem<T>,
373
- deleteFromMMKV?: boolean
374
- ): boolean;
535
+ const flagsItem = createStorageItem<FeatureFlags>({
536
+ key: "feature-flags",
537
+ scope: StorageScope.Disk,
538
+ defaultValue: { darkMode: false, betaFeature: false, maxUploadMb: 10 },
539
+ validate: isFeatureFlags,
540
+ onValidationError: () => ({
541
+ darkMode: false,
542
+ betaFeature: false,
543
+ maxUploadMb: 10,
544
+ }),
545
+ expiration: { ttlMs: 60 * 60_000 }, // refresh from server every hour
546
+ onExpired: () => fetchAndStoreFlags(),
547
+ });
548
+ ```
549
+
550
+ ### Biometric-protected Secrets
551
+
552
+ ```ts
553
+ import { AccessControl, createStorageItem, StorageScope } from "react-native-nitro-storage";
554
+
555
+ const paymentPin = createStorageItem<string>({
556
+ key: "payment-pin",
557
+ scope: StorageScope.Secure,
558
+ defaultValue: "",
559
+ biometric: true,
560
+ accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
561
+ });
562
+
563
+ paymentPin.set("4829");
564
+ const pin = paymentPin.get();
565
+ paymentPin.delete();
375
566
  ```
376
567
 
377
- Behavior:
568
+ ### Multi-Tenant / Namespaced Storage
569
+
570
+ ```ts
571
+ function createUserStorage(userId: string) {
572
+ return {
573
+ cart: createStorageItem<string[]>({
574
+ key: "cart",
575
+ scope: StorageScope.Disk,
576
+ defaultValue: [],
577
+ namespace: `user-${userId}`,
578
+ }),
579
+ draft: createStorageItem<string>({
580
+ key: "draft",
581
+ scope: StorageScope.Disk,
582
+ defaultValue: "",
583
+ namespace: `user-${userId}`,
584
+ }),
585
+ };
586
+ }
587
+
588
+ // clear all data for a specific user
589
+ storage.clearNamespace("user-123", StorageScope.Disk);
590
+ ```
591
+
592
+ ### OTP / Temporary Codes
593
+
594
+ ```ts
595
+ const otpItem = createStorageItem<string>({
596
+ key: "otp-code",
597
+ scope: StorageScope.Secure,
598
+ defaultValue: "",
599
+ expiration: { ttlMs: 5 * 60_000 }, // 5 minutes
600
+ onExpired: (key) => {
601
+ console.log(`${key} expired — prompt user to request a new code`);
602
+ },
603
+ });
604
+
605
+ // store the code
606
+ otpItem.set("482917");
607
+
608
+ // later — returns "" if expired
609
+ const code = otpItem.get();
610
+ ```
611
+
612
+ ### Bulk Bootstrap with Batch APIs
613
+
614
+ ```ts
615
+ import {
616
+ createStorageItem,
617
+ getBatch,
618
+ removeBatch,
619
+ setBatch,
620
+ StorageScope,
621
+ } from "react-native-nitro-storage";
622
+
623
+ const firstName = createStorageItem({
624
+ key: "first-name",
625
+ scope: StorageScope.Disk,
626
+ defaultValue: "",
627
+ });
628
+ const lastName = createStorageItem({
629
+ key: "last-name",
630
+ scope: StorageScope.Disk,
631
+ defaultValue: "",
632
+ });
378
633
 
379
- - Returns `true` when a value is found and copied, `false` otherwise.
380
- - Read priority is: `getString` -> `getNumber` -> `getBoolean`.
381
- - Uses `item.set(...)`, so schema validation on the target item still applies.
382
- - If `deleteFromMMKV` is `true`, deletes only when migration succeeds.
634
+ setBatch(
635
+ [
636
+ { item: firstName, value: "Ada" },
637
+ { item: lastName, value: "Lovelace" },
638
+ ],
639
+ StorageScope.Disk,
640
+ );
383
641
 
384
- ## Examples
642
+ const [first, last] = getBatch([firstName, lastName], StorageScope.Disk);
643
+ removeBatch([firstName, lastName], StorageScope.Disk);
644
+ ```
385
645
 
386
- ### Schema Validation + Fallback
646
+ ### Atomic Balance Transfer
387
647
 
388
648
  ```ts
389
- const userIdItem = createStorageItem<number>({
390
- key: "user-id",
649
+ const fromBalance = createStorageItem({
650
+ key: "from",
651
+ scope: StorageScope.Disk,
652
+ defaultValue: 100,
653
+ });
654
+ const toBalance = createStorageItem({
655
+ key: "to",
391
656
  scope: StorageScope.Disk,
392
657
  defaultValue: 0,
393
- validate: (v): v is number => typeof v === "number" && v > 0,
394
- onValidationError: () => 1,
395
658
  });
659
+
660
+ function transfer(amount: number) {
661
+ runTransaction(StorageScope.Disk, (tx) => {
662
+ const from = tx.getItem(fromBalance);
663
+ if (from < amount) throw new Error("Insufficient funds");
664
+
665
+ tx.setItem(fromBalance, from - amount);
666
+ tx.setItem(toBalance, tx.getItem(toBalance) + amount);
667
+ });
668
+ }
396
669
  ```
397
670
 
398
- ### TTL
671
+ ### Custom Binary Codec
399
672
 
400
673
  ```ts
401
- const otpItem = createStorageItem<string | undefined>({
402
- key: "otp",
403
- scope: StorageScope.Secure,
404
- expiration: { ttlMs: 60_000 },
674
+ const compactItem = createStorageItem<{ id: number; active: boolean }>({
675
+ key: "compact",
676
+ scope: StorageScope.Disk,
677
+ defaultValue: { id: 0, active: false },
678
+ serialize: (v) => `${v.id}|${v.active ? "1" : "0"}`,
679
+ deserialize: (v) => {
680
+ const [id, flag] = v.split("|");
681
+ return { id: Number(id), active: flag === "1" };
682
+ },
405
683
  });
406
684
  ```
407
685
 
408
- ### Transaction
686
+ ### Coalesced Secure Writes with Deterministic Flush
409
687
 
410
688
  ```ts
411
- runTransaction(StorageScope.Disk, (tx) => {
412
- tx.setRaw("a", JSON.stringify(1));
413
- tx.setRaw("b", JSON.stringify(2));
689
+ import { createStorageItem, storage, StorageScope } from "react-native-nitro-storage";
690
+
691
+ const sessionToken = createStorageItem<string>({
692
+ key: "session-token",
693
+ scope: StorageScope.Secure,
694
+ defaultValue: "",
695
+ coalesceSecureWrites: true,
414
696
  });
697
+
698
+ sessionToken.set("token-v1");
699
+ sessionToken.set("token-v2");
700
+
701
+ // force pending secure writes to native persistence
702
+ storage.flushSecureWrites();
415
703
  ```
416
704
 
417
- ### Versioned Migrations
705
+ ### Storage Snapshots and Cleanup
418
706
 
419
707
  ```ts
420
- registerMigration(1, ({ setRaw }) => {
421
- setRaw("seed", JSON.stringify({ ready: true }));
708
+ import { storage, StorageScope } from "react-native-nitro-storage";
709
+
710
+ const diskKeys = storage.getAllKeys(StorageScope.Disk);
711
+ const diskValues = storage.getAll(StorageScope.Disk);
712
+ const secureCount = storage.size(StorageScope.Secure);
713
+
714
+ if (storage.has("legacy-flag", StorageScope.Disk)) {
715
+ storage.clearNamespace("legacy", StorageScope.Disk);
716
+ }
717
+
718
+ storage.clearBiometric();
719
+ ```
720
+
721
+ ### Low-level Subscription (outside React)
722
+
723
+ ```ts
724
+ import { createStorageItem, StorageScope } from "react-native-nitro-storage";
725
+
726
+ const notificationsItem = createStorageItem<boolean>({
727
+ key: "notifications-enabled",
728
+ scope: StorageScope.Disk,
729
+ defaultValue: true,
422
730
  });
423
731
 
424
- registerMigration(2, ({ getRaw, setRaw }) => {
425
- const raw = getRaw("seed");
426
- if (!raw) return;
427
- const value = JSON.parse(raw) as { ready: boolean };
428
- setRaw("seed", JSON.stringify({ ...value, migrated: true }));
732
+ const unsubscribe = notificationsItem.subscribe(() => {
733
+ console.log("notifications changed:", notificationsItem.get());
429
734
  });
430
735
 
431
- migrateToLatest(StorageScope.Disk);
736
+ notificationsItem.set(false);
737
+ unsubscribe();
432
738
  ```
433
739
 
434
- ## Scope Semantics
740
+ ### Migrating From MMKV
741
+
742
+ ```ts
743
+ import { MMKV } from "react-native-mmkv";
744
+
745
+ const mmkv = new MMKV();
746
+
747
+ const usernameItem = createStorageItem({
748
+ key: "username",
749
+ scope: StorageScope.Disk,
750
+ defaultValue: "",
751
+ });
752
+
753
+ // run once at app startup
754
+ migrateFromMMKV(mmkv, usernameItem, true); // true = delete from MMKV after
755
+ ```
756
+
757
+ ---
758
+
759
+ ## Exported Types
760
+
761
+ ```ts
762
+ import type {
763
+ Storage,
764
+ StorageItemConfig,
765
+ StorageItem,
766
+ StorageBatchSetItem,
767
+ Validator,
768
+ ExpirationConfig,
769
+ MigrationContext,
770
+ Migration,
771
+ TransactionContext,
772
+ MMKVLike,
773
+ SecureAuthStorageConfig,
774
+ } from "react-native-nitro-storage";
775
+ ```
435
776
 
436
- - `Memory`: in-memory only, not persisted.
437
- - `Disk`: App-scoped UserDefaults suite (iOS), SharedPreferences (Android), `localStorage` (web).
438
- - `Secure`: Keychain (iOS), EncryptedSharedPreferences (Android), `sessionStorage` fallback (web).
777
+ ---
439
778
 
440
779
  ## Dev Commands
441
780
 
442
- From repo root:
781
+ From repository root:
443
782
 
444
783
  ```bash
445
784
  bun run test -- --filter=react-native-nitro-storage
785
+ bun run lint -- --filter=react-native-nitro-storage
786
+ bun run format:check -- --filter=react-native-nitro-storage
446
787
  bun run typecheck -- --filter=react-native-nitro-storage
788
+ bun run test:types -- --filter=react-native-nitro-storage
789
+ bun run test:cpp -- --filter=react-native-nitro-storage
447
790
  bun run build -- --filter=react-native-nitro-storage
448
- bun run benchmark
449
791
  ```
450
792
 
451
- Inside package:
793
+ Inside `packages/react-native-nitro-storage`:
452
794
 
453
795
  ```bash
454
- bun run test
455
- bun run test:coverage
456
- bun run typecheck
457
- bun run build
458
- bun run benchmark
796
+ bun run test # run tests
797
+ bun run test:coverage # run tests with coverage
798
+ bun run lint # eslint (expo-magic rules)
799
+ bun run format:check # prettier check
800
+ bun run typecheck # tsc --noEmit
801
+ bun run test:types # public type-level API tests
802
+ bun run test:cpp # C++ binding/core tests
803
+ bun run check:pack # npm pack content guard
804
+ bun run build # bob build
805
+ bun run benchmark # performance benchmarks
459
806
  ```
460
807
 
461
808
  ## License