react-native-nitro-storage 0.3.0 → 0.3.1

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 (45) hide show
  1. package/README.md +414 -256
  2. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +98 -11
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +15 -0
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +130 -33
  5. package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
  6. package/cpp/bindings/HybridStorage.cpp +121 -12
  7. package/cpp/bindings/HybridStorage.hpp +10 -0
  8. package/cpp/core/NativeStorageAdapter.hpp +15 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +19 -0
  10. package/ios/IOSStorageAdapterCpp.mm +233 -32
  11. package/lib/commonjs/Storage.types.js +23 -1
  12. package/lib/commonjs/Storage.types.js.map +1 -1
  13. package/lib/commonjs/index.js +173 -32
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +289 -49
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/internal.js +10 -0
  18. package/lib/commonjs/internal.js.map +1 -1
  19. package/lib/module/Storage.types.js +22 -0
  20. package/lib/module/Storage.types.js.map +1 -1
  21. package/lib/module/index.js +163 -35
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/index.web.js +278 -51
  24. package/lib/module/index.web.js.map +1 -1
  25. package/lib/module/internal.js +8 -0
  26. package/lib/module/internal.js.map +1 -1
  27. package/lib/typescript/Storage.nitro.d.ts +10 -0
  28. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  29. package/lib/typescript/Storage.types.d.ts +20 -0
  30. package/lib/typescript/Storage.types.d.ts.map +1 -1
  31. package/lib/typescript/index.d.ts +30 -7
  32. package/lib/typescript/index.d.ts.map +1 -1
  33. package/lib/typescript/index.web.d.ts +40 -7
  34. package/lib/typescript/index.web.d.ts.map +1 -1
  35. package/lib/typescript/internal.d.ts +2 -0
  36. package/lib/typescript/internal.d.ts.map +1 -1
  37. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +10 -0
  38. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +10 -0
  39. package/package.json +4 -1
  40. package/src/Storage.nitro.ts +11 -2
  41. package/src/Storage.types.ts +22 -0
  42. package/src/index.ts +270 -71
  43. package/src/index.web.ts +431 -90
  44. package/src/internal.ts +14 -4
  45. package/src/migration.ts +1 -1
package/README.md CHANGED
@@ -1,12 +1,31 @@
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)
4
21
 
5
22
  ## Requirements
6
23
 
7
- - `react-native >= 0.75.0`
8
- - `react-native-nitro-modules >= 0.33.9`
9
- - `react >= 18.2.0`
24
+ | Dependency | Version |
25
+ | ---------------------------- | ----------- |
26
+ | `react-native` | `>= 0.75.0` |
27
+ | `react-native-nitro-modules` | `>= 0.33.9` |
28
+ | `react` | `>= 18.2.0` |
10
29
 
11
30
  ## Installation
12
31
 
@@ -14,13 +33,19 @@ Synchronous storage for React Native with a unified API for memory, disk, and se
14
33
  bun add react-native-nitro-storage react-native-nitro-modules
15
34
  ```
16
35
 
36
+ or:
37
+
38
+ ```bash
39
+ npm install react-native-nitro-storage react-native-nitro-modules
40
+ ```
41
+
17
42
  ### Expo
18
43
 
19
44
  ```bash
20
45
  bunx expo install react-native-nitro-storage react-native-nitro-modules
21
46
  ```
22
47
 
23
- `app.json`:
48
+ Add the config plugin to `app.json`:
24
49
 
25
50
  ```json
26
51
  {
@@ -38,12 +63,9 @@ bunx expo install react-native-nitro-storage react-native-nitro-modules
38
63
  }
39
64
  ```
40
65
 
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`.
66
+ > `faceIDPermission` sets `NSFaceIDUsageDescription` only when missing. Android biometric permissions are opt-in via `addBiometricPermissions: true`.
45
67
 
46
- Then:
68
+ Then run:
47
69
 
48
70
  ```bash
49
71
  bunx expo prebuild
@@ -51,13 +73,13 @@ bunx expo prebuild
51
73
 
52
74
  ### Bare React Native
53
75
 
54
- iOS:
76
+ **iOS:**
55
77
 
56
78
  ```bash
57
79
  cd ios && pod install
58
80
  ```
59
81
 
60
- Android (`MainApplication.kt`):
82
+ **Android** — initialize the native adapter in `MainApplication.kt`:
61
83
 
62
84
  ```kotlin
63
85
  import com.nitrostorage.AndroidStorageAdapter
@@ -70,11 +92,14 @@ class MainApplication : Application() {
70
92
  }
71
93
  ```
72
94
 
95
+ ---
96
+
73
97
  ## Quick Start
74
98
 
75
99
  ```ts
76
100
  import { createStorageItem, StorageScope, useStorage } from "react-native-nitro-storage";
77
101
 
102
+ // define a storage item outside of components
78
103
  const counterItem = createStorageItem({
79
104
  key: "counter",
80
105
  scope: StorageScope.Memory,
@@ -93,369 +118,502 @@ export function Counter() {
93
118
  }
94
119
  ```
95
120
 
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`
121
+ ---
124
122
 
125
- ## API Reference
123
+ ## Storage Scopes
126
124
 
127
- ### `StorageScope`
125
+ | Scope | Backend (iOS) | Backend (Android) | Backend (Web) | Persisted |
126
+ | -------- | ------------------------ | -------------------------- | ------------------------------------------------ | --------- |
127
+ | `Memory` | In-process JS Map | In-process JS Map | In-process JS Map | No |
128
+ | `Disk` | UserDefaults (app suite) | SharedPreferences | `localStorage` | Yes |
129
+ | `Secure` | Keychain (AES-256 GCM) | EncryptedSharedPreferences | `localStorage` (`__secure_` + `__bio_` prefixes) | Yes |
128
130
 
129
131
  ```ts
130
- enum StorageScope {
131
- Memory = 0,
132
- Disk = 1,
133
- Secure = 2,
134
- }
135
- ```
132
+ import { StorageScope } from "react-native-nitro-storage";
136
133
 
137
- ### `Storage` (low-level native/web adapter type)
138
-
139
- ```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
- };
134
+ StorageScope.Memory; // 0 ephemeral, fastest
135
+ StorageScope.Disk; // 1 — persistent, fast
136
+ StorageScope.Secure; // 2 — encrypted, slightly slower
153
137
  ```
154
138
 
155
- Notes:
139
+ ---
156
140
 
157
- - Exported for typing/integration use cases.
158
- - Most app code should use `createStorageItem` + hooks instead of this low-level API.
141
+ ## API Reference
159
142
 
160
- ### `StorageItemConfig<T>`
143
+ ### `createStorageItem<T>(config)`
161
144
 
162
- ```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
- };
175
- ```
176
-
177
- ### `StorageItem<T>`
145
+ The core factory. Creates a reactive storage item that can be used standalone or with hooks.
178
146
 
179
147
  ```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
- };
148
+ function createStorageItem<T = undefined>(
149
+ config: StorageItemConfig<T>,
150
+ ): StorageItem<T>;
191
151
  ```
192
152
 
193
- ### `createStorageItem<T>(config)`
153
+ **Config options:**
154
+
155
+ | Property | Type | Default | Description |
156
+ | ---------------------- | -------------------------------- | -------------- | -------------------------------------------------------------- |
157
+ | `key` | `string` | _required_ | Storage key identifier |
158
+ | `scope` | `StorageScope` | _required_ | Where to store the data |
159
+ | `defaultValue` | `T` | `undefined` | Value returned when no data exists |
160
+ | `serialize` | `(value: T) => string` | JSON fast path | Custom serialization |
161
+ | `deserialize` | `(value: string) => T` | JSON fast path | Custom deserialization |
162
+ | `validate` | `(value: unknown) => value is T` | — | Type guard run on every read |
163
+ | `onValidationError` | `(invalidValue: unknown) => T` | — | Recovery function when validation fails |
164
+ | `expiration` | `{ ttlMs: number }` | — | Time-to-live in milliseconds |
165
+ | `onExpired` | `(key: string) => void` | — | Callback fired when a TTL value expires on read |
166
+ | `readCache` | `boolean` | `false` | Cache deserialized values in JS (avoids repeated native reads) |
167
+ | `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
168
+ | `namespace` | `string` | — | Prefix key as `namespace:key` for isolation |
169
+ | `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
170
+ | `accessControl` | `AccessControl` | — | Keychain access control level (native only) |
171
+
172
+ **Returned `StorageItem<T>`:**
173
+
174
+ | Method / Property | Type | Description |
175
+ | ----------------- | ---------------------------------------- | -------------------------------------------------- |
176
+ | `get()` | `() => T` | Read current value (synchronous) |
177
+ | `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
178
+ | `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
179
+ | `has()` | `() => boolean` | Check if a value exists in storage |
180
+ | `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
181
+ | `serialize` | `(v: T) => string` | The item's serializer |
182
+ | `deserialize` | `(v: string) => T` | The item's deserializer |
183
+ | `scope` | `StorageScope` | The item's scope |
184
+ | `key` | `string` | The resolved key (includes namespace prefix) |
185
+
186
+ ---
187
+
188
+ ### React Hooks
189
+
190
+ #### `useStorage(item)`
191
+
192
+ Full reactive binding. Re-renders when the value changes.
194
193
 
195
194
  ```ts
196
- function createStorageItem<T = undefined>(config: StorageItemConfig<T>): StorageItem<T>
195
+ const [value, setValue] = useStorage(item);
197
196
  ```
198
197
 
199
- Notes:
198
+ #### `useStorageSelector(item, selector, isEqual?)`
200
199
 
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`.
200
+ Subscribe to a derived slice. Only re-renders when the selected value changes.
211
201
 
212
- Throws:
202
+ ```ts
203
+ const [theme, setSettings] = useStorageSelector(settingsItem, (s) => s.theme);
204
+ ```
213
205
 
214
- - `Error("expiration.ttlMs must be greater than 0.")` when `expiration.ttlMs <= 0`.
206
+ #### `useSetStorage(item)`
215
207
 
216
- ### `useStorage(item)`
208
+ Write-only hook. Useful when a component needs to update a value but doesn't depend on it.
217
209
 
218
210
  ```ts
219
- function useStorage<T>(
220
- item: StorageItem<T>
221
- ): [T, (value: T | ((prev: T) => T)) => void]
211
+ const setToken = useSetStorage(tokenItem);
212
+ setToken("new-token");
222
213
  ```
223
214
 
224
- ### `useStorageSelector(item, selector, isEqual?)`
215
+ ---
216
+
217
+ ### `storage` — Global Utilities
225
218
 
226
219
  ```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]
220
+ import { storage, StorageScope } from "react-native-nitro-storage";
232
221
  ```
233
222
 
234
- Use this to subscribe to a derived slice of a storage value and avoid rerenders when that slice does not change.
223
+ | Method | Description |
224
+ | --------------------------------------- | ---------------------------------------------------------------------------- |
225
+ | `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
226
+ | `storage.clearAll()` | Clear Memory + Disk + Secure |
227
+ | `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
228
+ | `storage.clearBiometric()` | Remove all biometric-prefixed keys |
229
+ | `storage.has(key, scope)` | Check if a key exists |
230
+ | `storage.getAllKeys(scope)` | Get all key names |
231
+ | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
232
+ | `storage.size(scope)` | Number of stored keys |
233
+ | `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
234
+ | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
235
235
 
236
- ### `useSetStorage(item)`
236
+ > `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
237
237
 
238
- ```ts
239
- function useSetStorage<T>(
240
- item: StorageItem<T>
241
- ): (value: T | ((prev: T) => T)) => void
242
- ```
238
+ ---
239
+
240
+ ### `createSecureAuthStorage<K>(config, options?)`
243
241
 
244
- ### `storage`
242
+ One-liner factory for authentication flows. Creates multiple `StorageItem<string>` entries in Secure scope.
245
243
 
246
244
  ```ts
247
- const storage: {
248
- clear: (scope: StorageScope) => void;
249
- clearAll: () => void;
250
- };
245
+ function createSecureAuthStorage<K extends string>(
246
+ config: SecureAuthStorageConfig<K>,
247
+ options?: { namespace?: string },
248
+ ): Record<K, StorageItem<string>>;
251
249
  ```
252
250
 
253
- Behavior:
251
+ - Default namespace: `"auth"`
252
+ - Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
253
+ - Supports per-key TTL, biometric, and access control
254
254
 
255
- - `clear(scope)` clears all keys in a single scope.
256
- - `clearAll()` clears `Memory`, `Disk`, and `Secure`.
255
+ ---
257
256
 
258
257
  ### Batch Operations
259
258
 
259
+ Atomic multi-key operations. Uses native batch APIs for best performance.
260
+
260
261
  ```ts
261
- type StorageBatchSetItem<T> = {
262
- item: StorageItem<T>;
263
- value: T;
264
- };
262
+ import { getBatch, setBatch, removeBatch } from "react-native-nitro-storage";
263
+
264
+ // Read multiple items at once
265
+ const [a, b, c] = getBatch([itemA, itemB, itemC], StorageScope.Disk);
266
+
267
+ // Write multiple items atomically
268
+ setBatch(
269
+ [
270
+ { item: itemA, value: "hello" },
271
+ { item: itemB, value: "world" },
272
+ ],
273
+ StorageScope.Disk,
274
+ );
275
+
276
+ // Remove multiple items
277
+ removeBatch([itemA, itemB], StorageScope.Disk);
278
+ ```
265
279
 
266
- function getBatch(
267
- items: readonly Pick<StorageItem<unknown>, "key" | "scope" | "get" | "deserialize">[],
268
- scope: StorageScope
269
- ): unknown[];
280
+ > All items in a batch must share the same scope. Items with `validate` or `expiration` automatically use per-item paths to preserve semantics.
270
281
 
271
- function setBatch<T>(
272
- items: readonly StorageBatchSetItem<T>[],
273
- scope: StorageScope
274
- ): void;
282
+ ---
275
283
 
276
- function removeBatch(
277
- items: readonly Pick<StorageItem<unknown>, "key" | "scope" | "delete">[],
278
- scope: StorageScope
279
- ): void;
284
+ ### Transactions
285
+
286
+ Grouped writes with automatic rollback on error.
287
+
288
+ ```ts
289
+ import { runTransaction, StorageScope } from "react-native-nitro-storage";
290
+
291
+ runTransaction(StorageScope.Disk, (tx) => {
292
+ const balance = tx.getItem(balanceItem);
293
+ tx.setItem(balanceItem, balance - 50);
294
+ tx.setItem(logItem, `Deducted 50 at ${new Date().toISOString()}`);
295
+
296
+ if (balance - 50 < 0) throw new Error("Insufficient funds");
297
+ // if this throws, both writes are rolled back
298
+ });
280
299
  ```
281
300
 
282
- Rules:
301
+ **TransactionContext methods:**
302
+
303
+ | Method | Description |
304
+ | ---------------------- | --------------------------- |
305
+ | `getItem(item)` | Read a StorageItem's value |
306
+ | `setItem(item, value)` | Write a StorageItem's value |
307
+ | `removeItem(item)` | Delete a StorageItem |
308
+ | `getRaw(key)` | Read raw string by key |
309
+ | `setRaw(key, value)` | Write raw string by key |
310
+ | `removeRaw(key)` | Delete raw key |
283
311
 
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>.`
312
+ ---
288
313
 
289
314
  ### Migrations
290
315
 
316
+ Versioned, sequential data migrations.
317
+
291
318
  ```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
- };
319
+ import {
320
+ registerMigration,
321
+ migrateToLatest,
322
+ StorageScope,
323
+ } from "react-native-nitro-storage";
324
+
325
+ registerMigration(1, ({ setRaw }) => {
326
+ setRaw("onboarding-complete", "false");
327
+ });
298
328
 
299
- type Migration = (context: MigrationContext) => void;
329
+ registerMigration(2, ({ getRaw, setRaw, removeRaw }) => {
330
+ const raw = getRaw("legacy-key");
331
+ if (raw) {
332
+ setRaw("new-key", raw);
333
+ removeRaw("legacy-key");
334
+ }
335
+ });
300
336
 
301
- function registerMigration(version: number, migration: Migration): void;
302
- function migrateToLatest(scope?: StorageScope): number;
337
+ // apply all pending migrations (runs once per scope)
338
+ migrateToLatest(StorageScope.Disk);
303
339
  ```
304
340
 
305
- Behavior:
306
-
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.
341
+ - Versions must be positive integers, registered in any order, applied ascending
342
+ - Version state is tracked per scope via `__nitro_storage_migration_version__`
343
+ - Duplicate versions throw at registration time
311
344
 
312
- Throws:
345
+ ---
313
346
 
314
- - `registerMigration`: throws when version is not a positive integer.
315
- - `registerMigration`: throws when version is already registered.
316
- - `migrateToLatest`: throws on invalid scope.
347
+ ### MMKV Migration
317
348
 
318
- ### Transactions
349
+ Drop-in helper for migrating from `react-native-mmkv`.
319
350
 
320
351
  ```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
- };
352
+ import { migrateFromMMKV } from "react-native-nitro-storage";
353
+ import { MMKV } from "react-native-mmkv";
354
+
355
+ const mmkv = new MMKV();
330
356
 
331
- function runTransaction<T>(
332
- scope: StorageScope,
333
- transaction: (context: TransactionContext) => T
334
- ): T;
357
+ const migrated = migrateFromMMKV(mmkv, myStorageItem, true);
358
+ // true value found and copied, original deleted from MMKV
359
+ // false no matching key in MMKV
335
360
  ```
336
361
 
337
- Behavior:
362
+ - Read priority: `getString` → `getNumber` → `getBoolean`
363
+ - Uses `item.set()` so validation still applies
364
+ - Only deletes from MMKV when migration succeeds
338
365
 
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.
366
+ ---
342
367
 
343
- Throws:
368
+ ### Enums
344
369
 
345
- - Throws on invalid scope.
346
- - Rethrows any error thrown by the transaction callback after rollback.
370
+ #### `AccessControl`
347
371
 
348
- ### Validation and Expiration Types
372
+ Controls keychain item access requirements (iOS Keychain / Android Keystore). No-op on web.
349
373
 
350
374
  ```ts
351
- type Validator<T> = (value: unknown) => value is T;
375
+ enum AccessControl {
376
+ WhenUnlocked = 0,
377
+ AfterFirstUnlock = 1,
378
+ WhenPasscodeSetThisDeviceOnly = 2,
379
+ WhenUnlockedThisDeviceOnly = 3,
380
+ AfterFirstUnlockThisDeviceOnly = 4,
381
+ }
382
+ ```
352
383
 
353
- type ExpirationConfig = {
354
- ttlMs: number;
355
- };
384
+ #### `BiometricLevel`
385
+
386
+ ```ts
387
+ enum BiometricLevel {
388
+ None = 0,
389
+ BiometryOrPasscode = 1,
390
+ BiometryOnly = 2,
391
+ }
356
392
  ```
357
393
 
358
- ### MMKV Migration
394
+ ---
395
+
396
+ ## Use Cases
397
+
398
+ ### Persisted User Preferences
359
399
 
360
400
  ```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[];
368
- };
401
+ interface UserPreferences {
402
+ theme: "light" | "dark" | "system";
403
+ language: string;
404
+ notifications: boolean;
405
+ }
406
+
407
+ const prefsItem = createStorageItem<UserPreferences>({
408
+ key: "prefs",
409
+ scope: StorageScope.Disk,
410
+ defaultValue: { theme: "system", language: "en", notifications: true },
411
+ });
369
412
 
370
- function migrateFromMMKV<T>(
371
- mmkv: MMKVLike,
372
- item: StorageItem<T>,
373
- deleteFromMMKV?: boolean
374
- ): boolean;
413
+ // in a component — only re-renders when theme changes
414
+ const [theme, setPrefs] = useStorageSelector(prefsItem, (p) => p.theme);
375
415
  ```
376
416
 
377
- Behavior:
417
+ ### Auth Token Management
378
418
 
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.
419
+ ```ts
420
+ const auth = createSecureAuthStorage(
421
+ {
422
+ accessToken: { ttlMs: 15 * 60_000, biometric: true },
423
+ refreshToken: { ttlMs: 7 * 24 * 60 * 60_000 },
424
+ idToken: {},
425
+ },
426
+ { namespace: "myapp-auth" },
427
+ );
428
+
429
+ // after login
430
+ auth.accessToken.set(response.accessToken);
431
+ auth.refreshToken.set(response.refreshToken);
432
+ auth.idToken.set(response.idToken);
433
+
434
+ // check if token exists and hasn't expired
435
+ if (auth.accessToken.has()) {
436
+ const token = auth.accessToken.get();
437
+ // use token
438
+ } else {
439
+ // refresh or re-login
440
+ }
383
441
 
384
- ## Examples
442
+ // logout
443
+ storage.clearNamespace("myapp-auth", StorageScope.Secure);
444
+ ```
385
445
 
386
- ### Schema Validation + Fallback
446
+ ### Feature Flags with Validation
387
447
 
388
448
  ```ts
389
- const userIdItem = createStorageItem<number>({
390
- key: "user-id",
449
+ interface FeatureFlags {
450
+ darkMode: boolean;
451
+ betaFeature: boolean;
452
+ maxUploadMb: number;
453
+ }
454
+
455
+ const flagsItem = createStorageItem<FeatureFlags>({
456
+ key: "feature-flags",
391
457
  scope: StorageScope.Disk,
392
- defaultValue: 0,
393
- validate: (v): v is number => typeof v === "number" && v > 0,
394
- onValidationError: () => 1,
458
+ defaultValue: { darkMode: false, betaFeature: false, maxUploadMb: 10 },
459
+ validate: (v): v is FeatureFlags =>
460
+ typeof v === "object" &&
461
+ v !== null &&
462
+ typeof (v as any).darkMode === "boolean" &&
463
+ typeof (v as any).maxUploadMb === "number",
464
+ onValidationError: () => ({
465
+ darkMode: false,
466
+ betaFeature: false,
467
+ maxUploadMb: 10,
468
+ }),
469
+ expiration: { ttlMs: 60 * 60_000 }, // refresh from server every hour
470
+ onExpired: () => fetchAndStoreFlags(),
395
471
  });
396
472
  ```
397
473
 
398
- ### TTL
474
+ ### Multi-Tenant / Namespaced Storage
399
475
 
400
476
  ```ts
401
- const otpItem = createStorageItem<string | undefined>({
402
- key: "otp",
477
+ function createUserStorage(userId: string) {
478
+ return {
479
+ cart: createStorageItem<string[]>({
480
+ key: "cart",
481
+ scope: StorageScope.Disk,
482
+ defaultValue: [],
483
+ namespace: `user-${userId}`,
484
+ }),
485
+ draft: createStorageItem<string>({
486
+ key: "draft",
487
+ scope: StorageScope.Disk,
488
+ defaultValue: "",
489
+ namespace: `user-${userId}`,
490
+ }),
491
+ };
492
+ }
493
+
494
+ // clear all data for a specific user
495
+ storage.clearNamespace("user-123", StorageScope.Disk);
496
+ ```
497
+
498
+ ### OTP / Temporary Codes
499
+
500
+ ```ts
501
+ const otpItem = createStorageItem<string>({
502
+ key: "otp-code",
403
503
  scope: StorageScope.Secure,
404
- expiration: { ttlMs: 60_000 },
504
+ defaultValue: "",
505
+ expiration: { ttlMs: 5 * 60_000 }, // 5 minutes
506
+ onExpired: (key) => {
507
+ console.log(`${key} expired — prompt user to request a new code`);
508
+ },
405
509
  });
510
+
511
+ // store the code
512
+ otpItem.set("482917");
513
+
514
+ // later — returns "" if expired
515
+ const code = otpItem.get();
406
516
  ```
407
517
 
408
- ### Transaction
518
+ ### Atomic Balance Transfer
409
519
 
410
520
  ```ts
411
- runTransaction(StorageScope.Disk, (tx) => {
412
- tx.setRaw("a", JSON.stringify(1));
413
- tx.setRaw("b", JSON.stringify(2));
521
+ const fromBalance = createStorageItem({
522
+ key: "from",
523
+ scope: StorageScope.Disk,
524
+ defaultValue: 100,
525
+ });
526
+ const toBalance = createStorageItem({
527
+ key: "to",
528
+ scope: StorageScope.Disk,
529
+ defaultValue: 0,
414
530
  });
531
+
532
+ function transfer(amount: number) {
533
+ runTransaction(StorageScope.Disk, (tx) => {
534
+ const from = tx.getItem(fromBalance);
535
+ if (from < amount) throw new Error("Insufficient funds");
536
+
537
+ tx.setItem(fromBalance, from - amount);
538
+ tx.setItem(toBalance, tx.getItem(toBalance) + amount);
539
+ });
540
+ }
415
541
  ```
416
542
 
417
- ### Versioned Migrations
543
+ ### Custom Binary Codec
418
544
 
419
545
  ```ts
420
- registerMigration(1, ({ setRaw }) => {
421
- setRaw("seed", JSON.stringify({ ready: true }));
546
+ const compactItem = createStorageItem<{ id: number; active: boolean }>({
547
+ key: "compact",
548
+ scope: StorageScope.Disk,
549
+ defaultValue: { id: 0, active: false },
550
+ serialize: (v) => `${v.id}|${v.active ? "1" : "0"}`,
551
+ deserialize: (v) => {
552
+ const [id, flag] = v.split("|");
553
+ return { id: Number(id), active: flag === "1" };
554
+ },
422
555
  });
556
+ ```
557
+
558
+ ### Migrating From MMKV
559
+
560
+ ```ts
561
+ import { MMKV } from "react-native-mmkv";
562
+
563
+ const mmkv = new MMKV();
423
564
 
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 }));
565
+ const usernameItem = createStorageItem({
566
+ key: "username",
567
+ scope: StorageScope.Disk,
568
+ defaultValue: "",
429
569
  });
430
570
 
431
- migrateToLatest(StorageScope.Disk);
571
+ // run once at app startup
572
+ migrateFromMMKV(mmkv, usernameItem, true); // true = delete from MMKV after
432
573
  ```
433
574
 
434
- ## Scope Semantics
575
+ ---
576
+
577
+ ## Exported Types
578
+
579
+ ```ts
580
+ import type {
581
+ Storage,
582
+ StorageItemConfig,
583
+ StorageItem,
584
+ StorageBatchSetItem,
585
+ Validator,
586
+ ExpirationConfig,
587
+ MigrationContext,
588
+ Migration,
589
+ TransactionContext,
590
+ MMKVLike,
591
+ SecureAuthStorageConfig,
592
+ } from "react-native-nitro-storage";
593
+ ```
435
594
 
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).
595
+ ---
439
596
 
440
597
  ## Dev Commands
441
598
 
442
- From repo root:
599
+ From repository root:
443
600
 
444
601
  ```bash
445
602
  bun run test -- --filter=react-native-nitro-storage
446
603
  bun run typecheck -- --filter=react-native-nitro-storage
447
604
  bun run build -- --filter=react-native-nitro-storage
448
- bun run benchmark
449
605
  ```
450
606
 
451
- Inside package:
607
+ Inside `packages/react-native-nitro-storage`:
452
608
 
453
609
  ```bash
454
- bun run test
455
- bun run test:coverage
456
- bun run typecheck
457
- bun run build
458
- bun run benchmark
610
+ bun run test # run tests
611
+ bun run test:coverage # run tests with coverage
612
+ bun run lint # eslint (expo-magic rules)
613
+ bun run format:check # prettier check
614
+ bun run typecheck # tsc --noEmit
615
+ bun run build # tsup build
616
+ bun run benchmark # performance benchmarks
459
617
  ```
460
618
 
461
619
  ## License