react-native-nitro-storage 0.1.4 β 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 +432 -345
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +191 -3
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +21 -41
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +181 -29
- package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
- package/app.plugin.js +9 -7
- package/cpp/bindings/HybridStorage.cpp +239 -10
- package/cpp/bindings/HybridStorage.hpp +10 -0
- package/cpp/core/NativeStorageAdapter.hpp +22 -0
- package/ios/IOSStorageAdapterCpp.hpp +25 -0
- package/ios/IOSStorageAdapterCpp.mm +315 -33
- package/lib/commonjs/Storage.types.js +23 -1
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/index.js +680 -68
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +801 -133
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +112 -0
- package/lib/commonjs/internal.js.map +1 -0
- package/lib/module/Storage.types.js +22 -0
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/index.js +660 -71
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +766 -125
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +100 -0
- package/lib/module/internal.js.map +1 -0
- 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 +68 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +79 -13
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +21 -0
- package/lib/typescript/internal.d.ts.map +1 -0
- package/lib/typescript/migration.d.ts +2 -3
- package/lib/typescript/migration.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 +22 -8
- package/src/Storage.nitro.ts +11 -2
- package/src/Storage.types.ts +22 -0
- package/src/index.ts +943 -84
- package/src/index.web.ts +1082 -137
- package/src/internal.ts +144 -0
- package/src/migration.ts +3 -3
package/README.md
CHANGED
|
@@ -1,83 +1,77 @@
|
|
|
1
|
-
# react-native-nitro-storage
|
|
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.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
[](https://opensource.org/licenses/MIT)
|
|
7
|
-
[](https://nitro.margelo.com)
|
|
6
|
+
## Highlights
|
|
8
7
|
|
|
9
|
-
|
|
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)
|
|
10
21
|
|
|
11
|
-
|
|
12
|
-
<img src="./readme/ios.png" alt="iOS Benchmark" height="640" />
|
|
13
|
-
<img src="./readme/android.png" alt="Android Benchmark" height="640" />
|
|
14
|
-
</p>
|
|
22
|
+
## Requirements
|
|
15
23
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
| Dependency | Version |
|
|
25
|
+
| ---------------------------- | ----------- |
|
|
26
|
+
| `react-native` | `>= 0.75.0` |
|
|
27
|
+
| `react-native-nitro-modules` | `>= 0.33.9` |
|
|
28
|
+
| `react` | `>= 18.2.0` |
|
|
19
29
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
## β‘ Why Nitro Storage?
|
|
23
|
-
|
|
24
|
-
### **One Library, Three Storage Types**
|
|
25
|
-
|
|
26
|
-
Stop juggling multiple packages. Get memory state, disk persistence, and secure storage in one unified API.
|
|
27
|
-
|
|
28
|
-
### **Truly Synchronous**
|
|
29
|
-
|
|
30
|
-
Every operationβread, write, deleteβexecutes instantly. No `await`, no `.then()`, no bridge overhead.
|
|
31
|
-
|
|
32
|
-
### **Jotai-Style Atoms**
|
|
33
|
-
|
|
34
|
-
Familiar, elegant API with `createStorageItem` and `useStorage`. Works inside and outside React components.
|
|
35
|
-
|
|
36
|
-
### **Production-Ready**
|
|
37
|
-
|
|
38
|
-
Thread-safe C++ core, comprehensive test coverage, and battle-tested on iOS, Android, and **Web**.
|
|
30
|
+
## Installation
|
|
39
31
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
---
|
|
32
|
+
```bash
|
|
33
|
+
bun add react-native-nitro-storage react-native-nitro-modules
|
|
34
|
+
```
|
|
45
35
|
|
|
46
|
-
|
|
36
|
+
or:
|
|
47
37
|
|
|
48
38
|
```bash
|
|
49
39
|
npm install react-native-nitro-storage react-native-nitro-modules
|
|
50
|
-
# or
|
|
51
|
-
yarn add react-native-nitro-storage react-native-nitro-modules
|
|
52
|
-
# or
|
|
53
|
-
bun add react-native-nitro-storage react-native-nitro-modules
|
|
54
40
|
```
|
|
55
41
|
|
|
56
|
-
###
|
|
42
|
+
### Expo
|
|
57
43
|
|
|
58
44
|
```bash
|
|
59
|
-
|
|
45
|
+
bunx expo install react-native-nitro-storage react-native-nitro-modules
|
|
60
46
|
```
|
|
61
47
|
|
|
62
|
-
Add the plugin to
|
|
48
|
+
Add the config plugin to `app.json`:
|
|
63
49
|
|
|
64
50
|
```json
|
|
65
51
|
{
|
|
66
52
|
"expo": {
|
|
67
|
-
"plugins": [
|
|
53
|
+
"plugins": [
|
|
54
|
+
[
|
|
55
|
+
"react-native-nitro-storage",
|
|
56
|
+
{
|
|
57
|
+
"faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID for secure authentication",
|
|
58
|
+
"addBiometricPermissions": false
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
]
|
|
68
62
|
}
|
|
69
63
|
}
|
|
70
64
|
```
|
|
71
65
|
|
|
66
|
+
> `faceIDPermission` sets `NSFaceIDUsageDescription` only when missing. Android biometric permissions are opt-in via `addBiometricPermissions: true`.
|
|
67
|
+
|
|
72
68
|
Then run:
|
|
73
69
|
|
|
74
70
|
```bash
|
|
75
|
-
|
|
71
|
+
bunx expo prebuild
|
|
76
72
|
```
|
|
77
73
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
### For Bare React Native Projects
|
|
74
|
+
### Bare React Native
|
|
81
75
|
|
|
82
76
|
**iOS:**
|
|
83
77
|
|
|
@@ -85,9 +79,7 @@ The config plugin automatically handles Android initialization. No manual setup
|
|
|
85
79
|
cd ios && pod install
|
|
86
80
|
```
|
|
87
81
|
|
|
88
|
-
**Android
|
|
89
|
-
|
|
90
|
-
Add initialization to your `MainApplication.kt` (or `.java`):
|
|
82
|
+
**Android** β initialize the native adapter in `MainApplication.kt`:
|
|
91
83
|
|
|
92
84
|
```kotlin
|
|
93
85
|
import com.nitrostorage.AndroidStorageAdapter
|
|
@@ -102,433 +94,528 @@ class MainApplication : Application() {
|
|
|
102
94
|
|
|
103
95
|
---
|
|
104
96
|
|
|
105
|
-
##
|
|
97
|
+
## Quick Start
|
|
106
98
|
|
|
107
|
-
```
|
|
108
|
-
import {
|
|
109
|
-
createStorageItem,
|
|
110
|
-
useStorage,
|
|
111
|
-
StorageScope,
|
|
112
|
-
} from "react-native-nitro-storage";
|
|
99
|
+
```ts
|
|
100
|
+
import { createStorageItem, StorageScope, useStorage } from "react-native-nitro-storage";
|
|
113
101
|
|
|
114
|
-
//
|
|
115
|
-
const
|
|
102
|
+
// define a storage item outside of components
|
|
103
|
+
const counterItem = createStorageItem({
|
|
116
104
|
key: "counter",
|
|
117
105
|
scope: StorageScope.Memory,
|
|
118
106
|
defaultValue: 0,
|
|
119
107
|
});
|
|
120
108
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const [count, setCount] = useStorage(counterAtom);
|
|
109
|
+
export function Counter() {
|
|
110
|
+
const [count, setCount] = useStorage(counterItem);
|
|
124
111
|
|
|
125
112
|
return (
|
|
126
|
-
<
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
113
|
+
<Button
|
|
114
|
+
title={`Count: ${count}`}
|
|
115
|
+
onPress={() => setCount((prev) => prev + 1)}
|
|
116
|
+
/>
|
|
130
117
|
);
|
|
131
118
|
}
|
|
132
|
-
|
|
133
|
-
// Use outside React
|
|
134
|
-
counterAtom.set(42);
|
|
135
|
-
const value = counterAtom.get(); // 42, instantly
|
|
136
119
|
```
|
|
137
120
|
|
|
138
121
|
---
|
|
139
122
|
|
|
140
|
-
##
|
|
141
|
-
|
|
142
|
-
### **Memory Storage**
|
|
143
|
-
|
|
144
|
-
_Replaces: Zustand, Jotai, Redux_
|
|
145
|
-
|
|
146
|
-
Fast, in-memory state. **Now Pure JS** - can store complex objects, functions, and React nodes!
|
|
147
|
-
|
|
148
|
-
```typescript
|
|
149
|
-
// Store a function
|
|
150
|
-
const callbackAtom = createStorageItem({
|
|
151
|
-
key: "on-click",
|
|
152
|
-
scope: StorageScope.Memory,
|
|
153
|
-
defaultValue: () => console.log("Clicked!"),
|
|
154
|
-
});
|
|
123
|
+
## Storage Scopes
|
|
155
124
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
```
|
|
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 |
|
|
163
130
|
|
|
164
|
-
|
|
131
|
+
```ts
|
|
132
|
+
import { StorageScope } from "react-native-nitro-storage";
|
|
165
133
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Persisted to disk. Survives app restarts.
|
|
171
|
-
|
|
172
|
-
```typescript
|
|
173
|
-
const settingsAtom = createStorageItem({
|
|
174
|
-
key: "app-settings",
|
|
175
|
-
scope: StorageScope.Disk,
|
|
176
|
-
defaultValue: { theme: "dark", notifications: true },
|
|
177
|
-
});
|
|
134
|
+
StorageScope.Memory; // 0 β ephemeral, fastest
|
|
135
|
+
StorageScope.Disk; // 1 β persistent, fast
|
|
136
|
+
StorageScope.Secure; // 2 β encrypted, slightly slower
|
|
178
137
|
```
|
|
179
138
|
|
|
180
|
-
|
|
181
|
-
**Storage:** UserDefaults (iOS), SharedPreferences (Android)
|
|
139
|
+
---
|
|
182
140
|
|
|
183
|
-
|
|
141
|
+
## API Reference
|
|
184
142
|
|
|
185
|
-
|
|
143
|
+
### `createStorageItem<T>(config)`
|
|
186
144
|
|
|
187
|
-
|
|
145
|
+
The core factory. Creates a reactive storage item that can be used standalone or with hooks.
|
|
188
146
|
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
});
|
|
147
|
+
```ts
|
|
148
|
+
function createStorageItem<T = undefined>(
|
|
149
|
+
config: StorageItemConfig<T>,
|
|
150
|
+
): StorageItem<T>;
|
|
194
151
|
```
|
|
195
152
|
|
|
196
|
-
**
|
|
197
|
-
|
|
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) |
|
|
198
185
|
|
|
199
186
|
---
|
|
200
187
|
|
|
201
|
-
|
|
188
|
+
### React Hooks
|
|
202
189
|
|
|
203
|
-
|
|
190
|
+
#### `useStorage(item)`
|
|
204
191
|
|
|
205
|
-
|
|
206
|
-
Use explicit generics for nullable values. `defaultValue` is optional and defaults to `undefined`.
|
|
192
|
+
Full reactive binding. Re-renders when the value changes.
|
|
207
193
|
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
const userAtom = createStorageItem<User | null>({
|
|
211
|
-
key: "current-user",
|
|
212
|
-
scope: StorageScope.Memory,
|
|
213
|
-
defaultValue: null,
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// β
Optional value - no defaultValue needed
|
|
217
|
-
const tokenAtom = createStorageItem<string | undefined>({
|
|
218
|
-
key: "auth-token",
|
|
219
|
-
scope: StorageScope.Secure,
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// β Avoid - type assertion
|
|
223
|
-
const userAtom = createStorageItem({
|
|
224
|
-
key: "current-user",
|
|
225
|
-
scope: StorageScope.Memory,
|
|
226
|
-
defaultValue: null as User | null, // Not necessary
|
|
227
|
-
});
|
|
194
|
+
```ts
|
|
195
|
+
const [value, setValue] = useStorage(item);
|
|
228
196
|
```
|
|
229
197
|
|
|
230
|
-
|
|
231
|
-
For required values, just specify the default.
|
|
198
|
+
#### `useStorageSelector(item, selector, isEqual?)`
|
|
232
199
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
defaultValue: 0, // Type inferred as number
|
|
238
|
-
});
|
|
200
|
+
Subscribe to a derived slice. Only re-renders when the selected value changes.
|
|
201
|
+
|
|
202
|
+
```ts
|
|
203
|
+
const [theme, setSettings] = useStorageSelector(settingsItem, (s) => s.theme);
|
|
239
204
|
```
|
|
240
205
|
|
|
241
|
-
|
|
206
|
+
#### `useSetStorage(item)`
|
|
242
207
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
serialize: (date) => date.toISOString(),
|
|
249
|
-
deserialize: (str) => new Date(str),
|
|
250
|
-
});
|
|
208
|
+
Write-only hook. Useful when a component needs to update a value but doesn't depend on it.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const setToken = useSetStorage(tokenItem);
|
|
212
|
+
setToken("new-token");
|
|
251
213
|
```
|
|
252
214
|
|
|
253
|
-
|
|
215
|
+
---
|
|
254
216
|
|
|
255
|
-
|
|
256
|
-
interface User {
|
|
257
|
-
id: string;
|
|
258
|
-
name: string;
|
|
259
|
-
preferences: {
|
|
260
|
-
theme: "light" | "dark";
|
|
261
|
-
language: string;
|
|
262
|
-
};
|
|
263
|
-
}
|
|
217
|
+
### `storage` β Global Utilities
|
|
264
218
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
scope: StorageScope.Disk,
|
|
268
|
-
defaultValue: {
|
|
269
|
-
id: "",
|
|
270
|
-
name: "",
|
|
271
|
-
preferences: { theme: "dark", language: "en" },
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
// TypeScript knows the exact shape
|
|
276
|
-
const user = userAtom.get();
|
|
277
|
-
console.log(user.preferences.theme); // β
Type-safe
|
|
219
|
+
```ts
|
|
220
|
+
import { storage, StorageScope } from "react-native-nitro-storage";
|
|
278
221
|
```
|
|
279
222
|
|
|
280
|
-
|
|
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
|
+
|
|
236
|
+
> `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
|
|
237
|
+
|
|
238
|
+
---
|
|
281
239
|
|
|
282
|
-
|
|
240
|
+
### `createSecureAuthStorage<K>(config, options?)`
|
|
283
241
|
|
|
284
|
-
|
|
285
|
-
// In an API interceptor
|
|
286
|
-
axios.interceptors.request.use((config) => {
|
|
287
|
-
const token = tokenAtom.get();
|
|
288
|
-
if (token) {
|
|
289
|
-
config.headers.Authorization = `Bearer ${token}`;
|
|
290
|
-
}
|
|
291
|
-
return config;
|
|
292
|
-
});
|
|
242
|
+
One-liner factory for authentication flows. Creates multiple `StorageItem<string>` entries in Secure scope.
|
|
293
243
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
return next(action);
|
|
300
|
-
};
|
|
244
|
+
```ts
|
|
245
|
+
function createSecureAuthStorage<K extends string>(
|
|
246
|
+
config: SecureAuthStorageConfig<K>,
|
|
247
|
+
options?: { namespace?: string },
|
|
248
|
+
): Record<K, StorageItem<string>>;
|
|
301
249
|
```
|
|
302
250
|
|
|
303
|
-
|
|
251
|
+
- Default namespace: `"auth"`
|
|
252
|
+
- Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
|
|
253
|
+
- Supports per-key TTL, biometric, and access control
|
|
304
254
|
|
|
305
|
-
|
|
306
|
-
const unsubscribe = counterAtom.subscribe(() => {
|
|
307
|
-
console.log("Counter changed:", counterAtom.get());
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// Clean up
|
|
311
|
-
unsubscribe();
|
|
312
|
-
```
|
|
255
|
+
---
|
|
313
256
|
|
|
314
257
|
### Batch Operations
|
|
315
258
|
|
|
316
|
-
|
|
259
|
+
Atomic multi-key operations. Uses native batch APIs for best performance.
|
|
317
260
|
|
|
318
|
-
```
|
|
261
|
+
```ts
|
|
319
262
|
import { getBatch, setBatch, removeBatch } from "react-native-nitro-storage";
|
|
320
263
|
|
|
321
|
-
//
|
|
264
|
+
// Read multiple items at once
|
|
265
|
+
const [a, b, c] = getBatch([itemA, itemB, itemC], StorageScope.Disk);
|
|
266
|
+
|
|
267
|
+
// Write multiple items atomically
|
|
322
268
|
setBatch(
|
|
323
269
|
[
|
|
324
|
-
{ item:
|
|
325
|
-
{ item:
|
|
270
|
+
{ item: itemA, value: "hello" },
|
|
271
|
+
{ item: itemB, value: "world" },
|
|
326
272
|
],
|
|
327
|
-
StorageScope.Disk
|
|
273
|
+
StorageScope.Disk,
|
|
328
274
|
);
|
|
329
275
|
|
|
330
|
-
// Read multiple items
|
|
331
|
-
const [v1, v2] = getBatch([item1, item2], StorageScope.Disk);
|
|
332
|
-
|
|
333
276
|
// Remove multiple items
|
|
334
|
-
removeBatch([
|
|
277
|
+
removeBatch([itemA, itemB], StorageScope.Disk);
|
|
335
278
|
```
|
|
336
279
|
|
|
337
|
-
|
|
280
|
+
> All items in a batch must share the same scope. Items with `validate` or `expiration` automatically use per-item paths to preserve semantics.
|
|
338
281
|
|
|
339
|
-
|
|
282
|
+
---
|
|
340
283
|
|
|
341
|
-
|
|
342
|
-
// Increment counter
|
|
343
|
-
counterAtom.set((prev) => prev + 1);
|
|
344
|
-
```
|
|
284
|
+
### Transactions
|
|
345
285
|
|
|
346
|
-
|
|
286
|
+
Grouped writes with automatic rollback on error.
|
|
347
287
|
|
|
348
|
-
|
|
288
|
+
```ts
|
|
289
|
+
import { runTransaction, StorageScope } from "react-native-nitro-storage";
|
|
349
290
|
|
|
350
|
-
|
|
351
|
-
|
|
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()}`);
|
|
352
295
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
}
|
|
296
|
+
if (balance - 50 < 0) throw new Error("Insufficient funds");
|
|
297
|
+
// if this throws, both writes are rolled back
|
|
298
|
+
});
|
|
357
299
|
```
|
|
358
300
|
|
|
359
|
-
|
|
301
|
+
**TransactionContext methods:**
|
|
360
302
|
|
|
361
|
-
|
|
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 |
|
|
362
311
|
|
|
363
|
-
|
|
364
|
-
import { storage } from "react-native-nitro-storage";
|
|
312
|
+
---
|
|
365
313
|
|
|
366
|
-
|
|
367
|
-
storage.clearAll();
|
|
314
|
+
### Migrations
|
|
368
315
|
|
|
369
|
-
|
|
370
|
-
storage.clear(StorageScope.Memory);
|
|
371
|
-
storage.clear(StorageScope.Disk);
|
|
372
|
-
storage.clear(StorageScope.Secure);
|
|
373
|
-
```
|
|
316
|
+
Versioned, sequential data migrations.
|
|
374
317
|
|
|
375
|
-
|
|
318
|
+
```ts
|
|
319
|
+
import {
|
|
320
|
+
registerMigration,
|
|
321
|
+
migrateToLatest,
|
|
322
|
+
StorageScope,
|
|
323
|
+
} from "react-native-nitro-storage";
|
|
376
324
|
|
|
377
|
-
|
|
325
|
+
registerMigration(1, ({ setRaw }) => {
|
|
326
|
+
setRaw("onboarding-complete", "false");
|
|
327
|
+
});
|
|
378
328
|
|
|
379
|
-
|
|
380
|
-
|
|
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
|
+
});
|
|
381
336
|
|
|
382
|
-
//
|
|
383
|
-
|
|
337
|
+
// apply all pending migrations (runs once per scope)
|
|
338
|
+
migrateToLatest(StorageScope.Disk);
|
|
384
339
|
```
|
|
385
340
|
|
|
386
|
-
|
|
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
|
|
387
344
|
|
|
388
|
-
|
|
345
|
+
---
|
|
389
346
|
|
|
390
|
-
|
|
347
|
+
### MMKV Migration
|
|
391
348
|
|
|
392
|
-
|
|
349
|
+
Drop-in helper for migrating from `react-native-mmkv`.
|
|
393
350
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
| Disk | 45ms | 38ms | **0.083ms** |
|
|
398
|
-
| Secure | 120ms | 95ms | **0.215ms** |
|
|
351
|
+
```ts
|
|
352
|
+
import { migrateFromMMKV } from "react-native-nitro-storage";
|
|
353
|
+
import { MMKV } from "react-native-mmkv";
|
|
399
354
|
|
|
400
|
-
|
|
355
|
+
const mmkv = new MMKV();
|
|
401
356
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
# Navigate to the "Benchmark" tab
|
|
357
|
+
const migrated = migrateFromMMKV(mmkv, myStorageItem, true);
|
|
358
|
+
// true β value found and copied, original deleted from MMKV
|
|
359
|
+
// false β no matching key in MMKV
|
|
406
360
|
```
|
|
407
361
|
|
|
408
|
-
|
|
362
|
+
- Read priority: `getString` β `getNumber` β `getBoolean`
|
|
363
|
+
- Uses `item.set()` so validation still applies
|
|
364
|
+
- Only deletes from MMKV when migration succeeds
|
|
409
365
|
|
|
410
|
-
|
|
366
|
+
---
|
|
411
367
|
|
|
412
|
-
###
|
|
368
|
+
### Enums
|
|
413
369
|
|
|
414
|
-
|
|
370
|
+
#### `AccessControl`
|
|
415
371
|
|
|
416
|
-
|
|
372
|
+
Controls keychain item access requirements (iOS Keychain / Android Keystore). No-op on web.
|
|
417
373
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
374
|
+
```ts
|
|
375
|
+
enum AccessControl {
|
|
376
|
+
WhenUnlocked = 0,
|
|
377
|
+
AfterFirstUnlock = 1,
|
|
378
|
+
WhenPasscodeSetThisDeviceOnly = 2,
|
|
379
|
+
WhenUnlockedThisDeviceOnly = 3,
|
|
380
|
+
AfterFirstUnlockThisDeviceOnly = 4,
|
|
381
|
+
}
|
|
382
|
+
```
|
|
423
383
|
|
|
424
|
-
|
|
384
|
+
#### `BiometricLevel`
|
|
425
385
|
|
|
426
|
-
|
|
386
|
+
```ts
|
|
387
|
+
enum BiometricLevel {
|
|
388
|
+
None = 0,
|
|
389
|
+
BiometryOrPasscode = 1,
|
|
390
|
+
BiometryOnly = 2,
|
|
391
|
+
}
|
|
392
|
+
```
|
|
427
393
|
|
|
428
|
-
|
|
394
|
+
---
|
|
429
395
|
|
|
430
|
-
|
|
431
|
-
- `set(value: T | ((prev: T) => T)): void` - Set new value (synchronous)
|
|
432
|
-
- `delete(): void` - Remove value (synchronous)
|
|
433
|
-
- `subscribe(callback: () => void): () => void` - Subscribe to changes
|
|
396
|
+
## Use Cases
|
|
434
397
|
|
|
435
|
-
###
|
|
398
|
+
### Persisted User Preferences
|
|
436
399
|
|
|
437
|
-
|
|
400
|
+
```ts
|
|
401
|
+
interface UserPreferences {
|
|
402
|
+
theme: "light" | "dark" | "system";
|
|
403
|
+
language: string;
|
|
404
|
+
notifications: boolean;
|
|
405
|
+
}
|
|
438
406
|
|
|
439
|
-
|
|
407
|
+
const prefsItem = createStorageItem<UserPreferences>({
|
|
408
|
+
key: "prefs",
|
|
409
|
+
scope: StorageScope.Disk,
|
|
410
|
+
defaultValue: { theme: "system", language: "en", notifications: true },
|
|
411
|
+
});
|
|
440
412
|
|
|
441
|
-
|
|
413
|
+
// in a component β only re-renders when theme changes
|
|
414
|
+
const [theme, setPrefs] = useStorageSelector(prefsItem, (p) => p.theme);
|
|
415
|
+
```
|
|
442
416
|
|
|
443
|
-
|
|
417
|
+
### Auth Token Management
|
|
444
418
|
|
|
445
|
-
|
|
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
|
+
);
|
|
446
428
|
|
|
447
|
-
|
|
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
|
+
}
|
|
448
441
|
|
|
449
|
-
|
|
450
|
-
-
|
|
442
|
+
// logout
|
|
443
|
+
storage.clearNamespace("myapp-auth", StorageScope.Secure);
|
|
444
|
+
```
|
|
451
445
|
|
|
452
|
-
###
|
|
446
|
+
### Feature Flags with Validation
|
|
453
447
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
448
|
+
```ts
|
|
449
|
+
interface FeatureFlags {
|
|
450
|
+
darkMode: boolean;
|
|
451
|
+
betaFeature: boolean;
|
|
452
|
+
maxUploadMb: number;
|
|
453
|
+
}
|
|
457
454
|
|
|
458
|
-
|
|
455
|
+
const flagsItem = createStorageItem<FeatureFlags>({
|
|
456
|
+
key: "feature-flags",
|
|
457
|
+
scope: StorageScope.Disk,
|
|
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(),
|
|
471
|
+
});
|
|
472
|
+
```
|
|
459
473
|
|
|
460
|
-
|
|
474
|
+
### Multi-Tenant / Namespaced Storage
|
|
475
|
+
|
|
476
|
+
```ts
|
|
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
|
+
}
|
|
461
493
|
|
|
462
|
-
|
|
494
|
+
// clear all data for a specific user
|
|
495
|
+
storage.clearNamespace("user-123", StorageScope.Disk);
|
|
496
|
+
```
|
|
463
497
|
|
|
464
|
-
|
|
465
|
-
- **Secure:** Keychain with `kSecAttrAccessibleWhenUnlocked`
|
|
498
|
+
### OTP / Temporary Codes
|
|
466
499
|
|
|
467
|
-
|
|
500
|
+
```ts
|
|
501
|
+
const otpItem = createStorageItem<string>({
|
|
502
|
+
key: "otp-code",
|
|
503
|
+
scope: StorageScope.Secure,
|
|
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
|
+
},
|
|
509
|
+
});
|
|
468
510
|
|
|
469
|
-
|
|
470
|
-
|
|
511
|
+
// store the code
|
|
512
|
+
otpItem.set("482917");
|
|
471
513
|
|
|
472
|
-
|
|
514
|
+
// later β returns "" if expired
|
|
515
|
+
const code = otpItem.get();
|
|
516
|
+
```
|
|
473
517
|
|
|
474
|
-
|
|
518
|
+
### Atomic Balance Transfer
|
|
475
519
|
|
|
476
|
-
|
|
520
|
+
```ts
|
|
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,
|
|
530
|
+
});
|
|
477
531
|
|
|
478
|
-
|
|
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");
|
|
479
536
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
537
|
+
tx.setItem(fromBalance, from - amount);
|
|
538
|
+
tx.setItem(toBalance, tx.getItem(toBalance) + amount);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
```
|
|
483
542
|
|
|
484
|
-
|
|
485
|
-
npm run typecheck
|
|
543
|
+
### Custom Binary Codec
|
|
486
544
|
|
|
487
|
-
|
|
488
|
-
|
|
545
|
+
```ts
|
|
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
|
+
},
|
|
555
|
+
});
|
|
489
556
|
```
|
|
490
557
|
|
|
491
|
-
|
|
558
|
+
### Migrating From MMKV
|
|
492
559
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
- β
Complex objects
|
|
496
|
-
- β
Subscription/unsubscription
|
|
497
|
-
- β
Memory leak prevention
|
|
498
|
-
- β
Thread safety (C++)
|
|
560
|
+
```ts
|
|
561
|
+
import { MMKV } from "react-native-mmkv";
|
|
499
562
|
|
|
500
|
-
|
|
563
|
+
const mmkv = new MMKV();
|
|
501
564
|
|
|
502
|
-
|
|
565
|
+
const usernameItem = createStorageItem({
|
|
566
|
+
key: "username",
|
|
567
|
+
scope: StorageScope.Disk,
|
|
568
|
+
defaultValue: "",
|
|
569
|
+
});
|
|
503
570
|
|
|
504
|
-
|
|
571
|
+
// run once at app startup
|
|
572
|
+
migrateFromMMKV(mmkv, usernameItem, true); // true = delete from MMKV after
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
---
|
|
505
576
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
+
```
|
|
510
594
|
|
|
511
595
|
---
|
|
512
596
|
|
|
513
|
-
##
|
|
597
|
+
## Dev Commands
|
|
514
598
|
|
|
515
|
-
|
|
516
|
-
| ---------------- | ------------- | ---- | ------------ | ------- | ----------------- |
|
|
517
|
-
| Synchronous | β
| β
| β | β
| β |
|
|
518
|
-
| Memory State | β
| β | β | β
| β |
|
|
519
|
-
| Disk Persistence | β
| β
| β
| β | β |
|
|
520
|
-
| Secure Storage | β
| β | β | β | β
|
|
|
521
|
-
| Type-Safe | β
| β οΈ | β οΈ | β
| β οΈ |
|
|
522
|
-
| Unified API | β
| β | β | β | β |
|
|
523
|
-
| React Hooks | β
| β | β | β
| β |
|
|
524
|
-
| Web Support | β
| β | β
| β
| β |
|
|
599
|
+
From repository root:
|
|
525
600
|
|
|
526
|
-
|
|
601
|
+
```bash
|
|
602
|
+
bun run test -- --filter=react-native-nitro-storage
|
|
603
|
+
bun run typecheck -- --filter=react-native-nitro-storage
|
|
604
|
+
bun run build -- --filter=react-native-nitro-storage
|
|
605
|
+
```
|
|
527
606
|
|
|
528
|
-
|
|
607
|
+
Inside `packages/react-native-nitro-storage`:
|
|
529
608
|
|
|
530
|
-
|
|
609
|
+
```bash
|
|
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
|
|
617
|
+
```
|
|
531
618
|
|
|
532
|
-
|
|
619
|
+
## License
|
|
533
620
|
|
|
534
|
-
|
|
621
|
+
MIT
|