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.
- package/README.md +414 -256
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +98 -11
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +15 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +130 -33
- package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
- package/cpp/bindings/HybridStorage.cpp +121 -12
- package/cpp/bindings/HybridStorage.hpp +10 -0
- package/cpp/core/NativeStorageAdapter.hpp +15 -0
- package/ios/IOSStorageAdapterCpp.hpp +19 -0
- package/ios/IOSStorageAdapterCpp.mm +233 -32
- package/lib/commonjs/Storage.types.js +23 -1
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/index.js +173 -32
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +289 -49
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +10 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/module/Storage.types.js +22 -0
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/index.js +163 -35
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +278 -51
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +8 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/typescript/Storage.nitro.d.ts +10 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/Storage.types.d.ts +20 -0
- package/lib/typescript/Storage.types.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +30 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +40 -7
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +2 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +10 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +10 -0
- package/package.json +4 -1
- package/src/Storage.nitro.ts +11 -2
- package/src/Storage.types.ts +22 -0
- package/src/index.ts +270 -71
- package/src/index.web.ts +431 -90
- package/src/internal.ts +14 -4
- package/src/migration.ts +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,31 @@
|
|
|
1
1
|
# react-native-nitro-storage
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
##
|
|
123
|
+
## Storage Scopes
|
|
126
124
|
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
Memory = 0,
|
|
132
|
-
Disk = 1,
|
|
133
|
-
Secure = 2,
|
|
134
|
-
}
|
|
135
|
-
```
|
|
132
|
+
import { StorageScope } from "react-native-nitro-storage";
|
|
136
133
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
139
|
+
---
|
|
156
140
|
|
|
157
|
-
|
|
158
|
-
- Most app code should use `createStorageItem` + hooks instead of this low-level API.
|
|
141
|
+
## API Reference
|
|
159
142
|
|
|
160
|
-
### `
|
|
143
|
+
### `createStorageItem<T>(config)`
|
|
161
144
|
|
|
162
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
195
|
+
const [value, setValue] = useStorage(item);
|
|
197
196
|
```
|
|
198
197
|
|
|
199
|
-
|
|
198
|
+
#### `useStorageSelector(item, selector, isEqual?)`
|
|
200
199
|
|
|
201
|
-
-
|
|
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
|
-
|
|
202
|
+
```ts
|
|
203
|
+
const [theme, setSettings] = useStorageSelector(settingsItem, (s) => s.theme);
|
|
204
|
+
```
|
|
213
205
|
|
|
214
|
-
|
|
206
|
+
#### `useSetStorage(item)`
|
|
215
207
|
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
): [T, (value: T | ((prev: T) => T)) => void]
|
|
211
|
+
const setToken = useSetStorage(tokenItem);
|
|
212
|
+
setToken("new-token");
|
|
222
213
|
```
|
|
223
214
|
|
|
224
|
-
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### `storage` — Global Utilities
|
|
225
218
|
|
|
226
219
|
```ts
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
236
|
+
> `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
|
|
237
237
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
): (value: T | ((prev: T) => T)) => void
|
|
242
|
-
```
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
### `createSecureAuthStorage<K>(config, options?)`
|
|
243
241
|
|
|
244
|
-
|
|
242
|
+
One-liner factory for authentication flows. Creates multiple `StorageItem<string>` entries in Secure scope.
|
|
245
243
|
|
|
246
244
|
```ts
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
items: readonly StorageBatchSetItem<T>[],
|
|
273
|
-
scope: StorageScope
|
|
274
|
-
): void;
|
|
282
|
+
---
|
|
275
283
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
337
|
+
// apply all pending migrations (runs once per scope)
|
|
338
|
+
migrateToLatest(StorageScope.Disk);
|
|
303
339
|
```
|
|
304
340
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
-
|
|
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
|
-
|
|
345
|
+
---
|
|
313
346
|
|
|
314
|
-
|
|
315
|
-
- `registerMigration`: throws when version is already registered.
|
|
316
|
-
- `migrateToLatest`: throws on invalid scope.
|
|
347
|
+
### MMKV Migration
|
|
317
348
|
|
|
318
|
-
|
|
349
|
+
Drop-in helper for migrating from `react-native-mmkv`.
|
|
319
350
|
|
|
320
351
|
```ts
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
368
|
+
### Enums
|
|
344
369
|
|
|
345
|
-
|
|
346
|
-
- Rethrows any error thrown by the transaction callback after rollback.
|
|
370
|
+
#### `AccessControl`
|
|
347
371
|
|
|
348
|
-
|
|
372
|
+
Controls keychain item access requirements (iOS Keychain / Android Keystore). No-op on web.
|
|
349
373
|
|
|
350
374
|
```ts
|
|
351
|
-
|
|
375
|
+
enum AccessControl {
|
|
376
|
+
WhenUnlocked = 0,
|
|
377
|
+
AfterFirstUnlock = 1,
|
|
378
|
+
WhenPasscodeSetThisDeviceOnly = 2,
|
|
379
|
+
WhenUnlockedThisDeviceOnly = 3,
|
|
380
|
+
AfterFirstUnlockThisDeviceOnly = 4,
|
|
381
|
+
}
|
|
382
|
+
```
|
|
352
383
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
384
|
+
#### `BiometricLevel`
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
enum BiometricLevel {
|
|
388
|
+
None = 0,
|
|
389
|
+
BiometryOrPasscode = 1,
|
|
390
|
+
BiometryOnly = 2,
|
|
391
|
+
}
|
|
356
392
|
```
|
|
357
393
|
|
|
358
|
-
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Use Cases
|
|
397
|
+
|
|
398
|
+
### Persisted User Preferences
|
|
359
399
|
|
|
360
400
|
```ts
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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
|
-
|
|
417
|
+
### Auth Token Management
|
|
378
418
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
442
|
+
// logout
|
|
443
|
+
storage.clearNamespace("myapp-auth", StorageScope.Secure);
|
|
444
|
+
```
|
|
385
445
|
|
|
386
|
-
###
|
|
446
|
+
### Feature Flags with Validation
|
|
387
447
|
|
|
388
448
|
```ts
|
|
389
|
-
|
|
390
|
-
|
|
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:
|
|
393
|
-
validate: (v): v is
|
|
394
|
-
|
|
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
|
-
###
|
|
474
|
+
### Multi-Tenant / Namespaced Storage
|
|
399
475
|
|
|
400
476
|
```ts
|
|
401
|
-
|
|
402
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
518
|
+
### Atomic Balance Transfer
|
|
409
519
|
|
|
410
520
|
```ts
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
###
|
|
543
|
+
### Custom Binary Codec
|
|
418
544
|
|
|
419
545
|
```ts
|
|
420
|
-
|
|
421
|
-
|
|
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
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
571
|
+
// run once at app startup
|
|
572
|
+
migrateFromMMKV(mmkv, usernameItem, true); // true = delete from MMKV after
|
|
432
573
|
```
|
|
433
574
|
|
|
434
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
607
|
+
Inside `packages/react-native-nitro-storage`:
|
|
452
608
|
|
|
453
609
|
```bash
|
|
454
|
-
bun run test
|
|
455
|
-
bun run test:coverage
|
|
456
|
-
bun run
|
|
457
|
-
bun run
|
|
458
|
-
bun run
|
|
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
|