react-native-nitro-storage 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -862
- package/SECURITY.md +26 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
- package/docs/api-reference.md +217 -0
- package/docs/batch-transactions-migrations.md +186 -0
- package/docs/benchmarks.md +37 -0
- package/docs/mmkv-migration.md +80 -0
- package/docs/react-hooks.md +113 -0
- package/docs/recipes.md +281 -0
- package/docs/secure-storage.md +171 -0
- package/docs/web-backends.md +141 -0
- package/ios/IOSStorageAdapterCpp.mm +44 -14
- package/lib/commonjs/index.js +271 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +498 -202
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +129 -7
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +41 -0
- package/lib/commonjs/storage-runtime.js.map +1 -0
- package/lib/commonjs/web-storage-backend.js +90 -0
- package/lib/commonjs/web-storage-backend.js.map +1 -0
- package/lib/module/index.js +263 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +490 -202
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +129 -7
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-runtime.js +36 -0
- package/lib/module/storage-runtime.js.map +1 -0
- package/lib/module/web-storage-backend.js +86 -0
- package/lib/module/web-storage-backend.js.map +1 -0
- package/lib/typescript/index.d.ts +14 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +15 -8
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +6 -2
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +48 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +30 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -0
- package/package.json +21 -8
- package/src/index.ts +330 -20
- package/src/index.web.ts +673 -245
- package/src/indexeddb-backend.ts +147 -6
- package/src/storage-runtime.ts +129 -0
- package/src/web-storage-backend.ts +129 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
## Supported Versions
|
|
4
|
+
|
|
5
|
+
Security fixes are shipped for the latest published `0.x` release line.
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
| ------- | --------- |
|
|
9
|
+
| `0.5.x` | Yes |
|
|
10
|
+
| `<0.5` | No |
|
|
11
|
+
|
|
12
|
+
## Reporting a Vulnerability
|
|
13
|
+
|
|
14
|
+
Report security issues through GitHub Security Advisories with:
|
|
15
|
+
|
|
16
|
+
- affected package version
|
|
17
|
+
- platform and OS version
|
|
18
|
+
- React Native and `react-native-nitro-modules` versions
|
|
19
|
+
- reproduction steps
|
|
20
|
+
- whether the issue affects Memory, Disk, Secure, biometric storage, web backends, or packaging
|
|
21
|
+
|
|
22
|
+
Do not publish proof-of-concept exploit details until a fix is available.
|
|
23
|
+
|
|
24
|
+
## Storage Boundary
|
|
25
|
+
|
|
26
|
+
Native Secure scope delegates encryption to platform storage APIs: iOS Keychain and Android Jetpack Security `EncryptedSharedPreferences`. Web Secure scope is API-compatible but defaults to namespaced `localStorage`; use a custom web secure backend when browser-side storage must meet a stricter threat model.
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
package com.nitrostorage
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import android.security.keystore.KeyPermanentlyInvalidatedException
|
|
5
|
+
import android.security.keystore.UserNotAuthenticatedException
|
|
4
6
|
import android.content.SharedPreferences
|
|
5
7
|
import android.util.Log
|
|
6
8
|
import androidx.security.crypto.EncryptedSharedPreferences
|
|
7
9
|
import androidx.security.crypto.MasterKey
|
|
10
|
+
import java.security.InvalidKeyException
|
|
8
11
|
import java.security.KeyStore
|
|
12
|
+
import java.security.KeyStoreException
|
|
9
13
|
import javax.crypto.AEADBadTagException
|
|
10
14
|
|
|
11
15
|
private fun Throwable.hasCause(type: Class<*>): Boolean {
|
|
@@ -17,6 +21,30 @@ private fun Throwable.hasCause(type: Class<*>): Boolean {
|
|
|
17
21
|
return false
|
|
18
22
|
}
|
|
19
23
|
|
|
24
|
+
private fun Throwable.storageErrorCode(): String? {
|
|
25
|
+
return when {
|
|
26
|
+
hasCause(AEADBadTagException::class.java) -> "storage_corruption"
|
|
27
|
+
hasCause(UserNotAuthenticatedException::class.java) ||
|
|
28
|
+
hasCause(KeyStoreException::class.java) -> "authentication_required"
|
|
29
|
+
hasCause(KeyPermanentlyInvalidatedException::class.java) ||
|
|
30
|
+
hasCause(InvalidKeyException::class.java) -> "key_invalidated"
|
|
31
|
+
else -> null
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private fun Throwable.wrapStorageException(
|
|
36
|
+
defaultMessage: String,
|
|
37
|
+
defaultCode: String? = null,
|
|
38
|
+
): RuntimeException {
|
|
39
|
+
val code = storageErrorCode() ?: defaultCode
|
|
40
|
+
val message = if (code != null) {
|
|
41
|
+
"[nitro-error:$code] $defaultMessage"
|
|
42
|
+
} else {
|
|
43
|
+
defaultMessage
|
|
44
|
+
}
|
|
45
|
+
return RuntimeException(message, this)
|
|
46
|
+
}
|
|
47
|
+
|
|
20
48
|
class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
21
49
|
private val sharedPreferences: SharedPreferences =
|
|
22
50
|
context.getSharedPreferences("NitroStorage", Context.MODE_PRIVATE)
|
|
@@ -44,8 +72,11 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
44
72
|
initializeEncryptedPreferences("NitroStorageBiometric", bioKey)
|
|
45
73
|
} catch (e: Exception) {
|
|
46
74
|
Log.e("NitroStorage", "Biometric storage unavailable: ${e.message}")
|
|
47
|
-
throw
|
|
48
|
-
"
|
|
75
|
+
throw e.wrapStorageException(
|
|
76
|
+
"NitroStorage: Biometric storage is not available on this device. " +
|
|
77
|
+
"Ensure biometric hardware is present and credentials are enrolled.",
|
|
78
|
+
defaultCode = "biometric_unavailable",
|
|
79
|
+
)
|
|
49
80
|
}
|
|
50
81
|
}
|
|
51
82
|
|
|
@@ -83,14 +114,17 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
83
114
|
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
|
|
84
115
|
)
|
|
85
116
|
} catch (retryEx: Exception) {
|
|
86
|
-
throw
|
|
117
|
+
throw retryEx.wrapStorageException(
|
|
118
|
+
"NitroStorage: Unrecoverable storage corruption in $name",
|
|
119
|
+
defaultCode = "storage_corruption",
|
|
120
|
+
)
|
|
87
121
|
}
|
|
88
122
|
}
|
|
89
123
|
else -> {
|
|
90
124
|
// Don't wipe on non-corruption failures (e.g., locked keystore)
|
|
91
|
-
throw
|
|
125
|
+
throw e.wrapStorageException(
|
|
92
126
|
"NitroStorage: Failed to initialize $name (${e::class.simpleName}). " +
|
|
93
|
-
|
|
127
|
+
"This may be a temporary keystore issue. If it persists, clear app data.",
|
|
94
128
|
)
|
|
95
129
|
}
|
|
96
130
|
}
|
|
@@ -136,7 +170,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
136
170
|
editor.commit()
|
|
137
171
|
}
|
|
138
172
|
} catch (e: Exception) {
|
|
139
|
-
throw
|
|
173
|
+
throw e.wrapStorageException(
|
|
174
|
+
"NitroStorage: Failed to write to secure storage: ${e.message}",
|
|
175
|
+
)
|
|
140
176
|
}
|
|
141
177
|
}
|
|
142
178
|
|
|
@@ -303,14 +339,26 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
303
339
|
@JvmStatic
|
|
304
340
|
fun getSecure(key: String): String? {
|
|
305
341
|
val inst = getInstanceOrThrow()
|
|
306
|
-
return
|
|
342
|
+
return try {
|
|
343
|
+
inst.getSecureSafe(inst.encryptedPreferences, key)
|
|
344
|
+
} catch (e: Exception) {
|
|
345
|
+
throw e.wrapStorageException(
|
|
346
|
+
"NitroStorage: Failed to read secure storage: ${e.message}",
|
|
347
|
+
)
|
|
348
|
+
}
|
|
307
349
|
}
|
|
308
350
|
|
|
309
351
|
@JvmStatic
|
|
310
352
|
fun getSecureBatch(keys: Array<String>): Array<String?> {
|
|
311
353
|
val inst = getInstanceOrThrow()
|
|
312
|
-
return
|
|
313
|
-
|
|
354
|
+
return try {
|
|
355
|
+
Array(keys.size) { index ->
|
|
356
|
+
inst.getSecureSafe(inst.encryptedPreferences, keys[index])
|
|
357
|
+
}
|
|
358
|
+
} catch (e: Exception) {
|
|
359
|
+
throw e.wrapStorageException(
|
|
360
|
+
"NitroStorage: Failed to read secure storage batch: ${e.message}",
|
|
361
|
+
)
|
|
314
362
|
}
|
|
315
363
|
}
|
|
316
364
|
|
|
@@ -406,7 +454,10 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
406
454
|
inst.applySecureEditor(editor)
|
|
407
455
|
inst.invalidateSecureKeysCache()
|
|
408
456
|
} catch (e: Exception) {
|
|
409
|
-
throw
|
|
457
|
+
throw e.wrapStorageException(
|
|
458
|
+
"NitroStorage: Biometric storage unavailable on this device",
|
|
459
|
+
defaultCode = "biometric_unavailable",
|
|
460
|
+
)
|
|
410
461
|
}
|
|
411
462
|
}
|
|
412
463
|
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
This page lists the public API surface. For copy-ready workflows, see [recipes.md](recipes.md).
|
|
4
|
+
|
|
5
|
+
## createStorageItem
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const item = createStorageItem<T>({
|
|
9
|
+
key: "theme",
|
|
10
|
+
scope: StorageScope.Disk,
|
|
11
|
+
defaultValue: "system",
|
|
12
|
+
});
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`StorageItemConfig<T>`:
|
|
16
|
+
|
|
17
|
+
| Field | Type | Purpose |
|
|
18
|
+
| ---------------------- | -------------------------------- | ---------------------------------------------------------------- |
|
|
19
|
+
| `key` | `string` | Storage key. Combined with `namespace` when provided. |
|
|
20
|
+
| `scope` | `StorageScope` | Memory, Disk, or Secure. |
|
|
21
|
+
| `defaultValue` | `T` | Value returned when no stored value exists. |
|
|
22
|
+
| `serialize` | `(value: T) => string` | Custom string encoder. Defaults to primitive/JSON serialization. |
|
|
23
|
+
| `deserialize` | `(value: string) => T` | Custom string decoder. |
|
|
24
|
+
| `validate` | `(value: unknown) => value is T` | Runtime guard for stored data. |
|
|
25
|
+
| `onValidationError` | `(invalidValue: unknown) => T` | Replacement value when validation fails. |
|
|
26
|
+
| `expiration` | `{ ttlMs: number }` | Time-to-live for the value. |
|
|
27
|
+
| `onExpired` | `(key: string) => void` | Called when a read detects TTL expiry. |
|
|
28
|
+
| `readCache` | `boolean` | Cache parsed values in memory. |
|
|
29
|
+
| `coalesceDiskWrites` | `boolean` | Buffer Disk writes until the next flush. |
|
|
30
|
+
| `coalesceSecureWrites` | `boolean` | Buffer Secure writes until the next flush. |
|
|
31
|
+
| `namespace` | `string` | Prefix keys as `namespace:key`. |
|
|
32
|
+
| `biometric` | `boolean` | Store through biometric secure storage. |
|
|
33
|
+
| `biometricLevel` | `BiometricLevel` | Require biometric/passcode or biometric-only access. |
|
|
34
|
+
| `accessControl` | `AccessControl` | Platform secure accessibility setting. |
|
|
35
|
+
|
|
36
|
+
`StorageItem<T>`:
|
|
37
|
+
|
|
38
|
+
| Method | Purpose |
|
|
39
|
+
| ------------------------------ | ----------------------------------------------------------- |
|
|
40
|
+
| `get()` | Return the typed value or the default value. |
|
|
41
|
+
| `getWithVersion()` | Return `{ value, version }` for optimistic writes. |
|
|
42
|
+
| `set(value)` | Store a value. Accepts direct values or updater functions. |
|
|
43
|
+
| `setIfVersion(version, value)` | Store only when the current version still matches. |
|
|
44
|
+
| `delete()` | Remove the key. |
|
|
45
|
+
| `has()` | Check whether the key exists. |
|
|
46
|
+
| `subscribe(callback)` | Subscribe to item changes. Returns an unsubscribe function. |
|
|
47
|
+
| `serialize(value)` | Serialize a value with the item encoder. |
|
|
48
|
+
| `deserialize(value)` | Deserialize a raw string with the item decoder. |
|
|
49
|
+
|
|
50
|
+
## React Hooks
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const [value, setValue] = useStorage(item);
|
|
54
|
+
const [selected, setItem] = useStorageSelector(item, selector, isEqual);
|
|
55
|
+
const setOnly = useSetStorage(item);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
See [react-hooks.md](react-hooks.md).
|
|
59
|
+
|
|
60
|
+
## storage
|
|
61
|
+
|
|
62
|
+
`storage` exposes raw and cross-item utilities:
|
|
63
|
+
|
|
64
|
+
| Method | Purpose |
|
|
65
|
+
| ---------------------------------- | ------------------------------------------------------------- |
|
|
66
|
+
| `clear(scope)` | Clear one scope. |
|
|
67
|
+
| `clearAll()` | Clear Memory, Disk, and Secure scopes. |
|
|
68
|
+
| `clearNamespace(namespace, scope)` | Remove keys under `namespace:`. |
|
|
69
|
+
| `clearBiometric()` | Clear biometric Secure entries. |
|
|
70
|
+
| `has(key, scope)` | Check for a raw key. |
|
|
71
|
+
| `getAllKeys(scope)` | List raw keys. |
|
|
72
|
+
| `getKeysByPrefix(prefix, scope)` | List raw keys with a prefix. |
|
|
73
|
+
| `getByPrefix(prefix, scope)` | Read raw string values by prefix. |
|
|
74
|
+
| `getAll(scope)` | Read all raw string values in a scope. |
|
|
75
|
+
| `size(scope)` | Return approximate scope entry count. |
|
|
76
|
+
| `setAccessControl(accessControl)` | Set the default Secure access control level. |
|
|
77
|
+
| `setSecureWritesAsync(enabled)` | Toggle Android secure writes between sync and async modes. |
|
|
78
|
+
| `setDiskWritesAsync(enabled)` | Toggle coalesced Disk write behavior. |
|
|
79
|
+
| `flushDiskWrites()` | Flush pending Disk writes. |
|
|
80
|
+
| `flushSecureWrites()` | Flush pending Secure writes. |
|
|
81
|
+
| `setKeychainAccessGroup(group)` | Configure iOS Keychain access group. |
|
|
82
|
+
| `setMetricsObserver(observer)` | Receive operation timing events. |
|
|
83
|
+
| `getMetricsSnapshot()` | Read aggregated metrics. |
|
|
84
|
+
| `resetMetrics()` | Clear metrics counters. |
|
|
85
|
+
| `getCapabilities()` | Read runtime storage capabilities. |
|
|
86
|
+
| `getSecurityCapabilities()` | Read secure backend capability metadata. |
|
|
87
|
+
| `getSecureMetadata(key)` | Read secure metadata for one key without returning its value. |
|
|
88
|
+
| `getAllSecureMetadata()` | Read secure metadata for all secure keys without values. |
|
|
89
|
+
| `getString(key, scope)` | Read a raw string. |
|
|
90
|
+
| `setString(key, value, scope)` | Write a raw string. |
|
|
91
|
+
| `deleteString(key, scope)` | Remove a raw key. |
|
|
92
|
+
| `import(data, scope)` | Bulk import raw strings. |
|
|
93
|
+
|
|
94
|
+
Raw string APIs bypass item serialization and validation. Prefer `StorageItem<T>` unless you are migrating, importing, or writing a custom integration.
|
|
95
|
+
|
|
96
|
+
## Batch Operations
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const values = getBatch([themeItem, localeItem], StorageScope.Disk);
|
|
100
|
+
|
|
101
|
+
setBatch(
|
|
102
|
+
[
|
|
103
|
+
{ item: themeItem, value: "dark" },
|
|
104
|
+
{ item: localeItem, value: "en-US" },
|
|
105
|
+
],
|
|
106
|
+
StorageScope.Disk,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
removeBatch([themeItem, localeItem], StorageScope.Disk);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [batch-transactions-migrations.md](batch-transactions-migrations.md).
|
|
113
|
+
|
|
114
|
+
## Transactions
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
runTransaction(StorageScope.Disk, (tx) => {
|
|
118
|
+
const current = tx.getItem(balanceItem);
|
|
119
|
+
tx.setItem(balanceItem, current + 10);
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
If the callback throws, previously changed keys in that transaction are rolled back synchronously.
|
|
124
|
+
|
|
125
|
+
## Migrations
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
registerMigration(2, (ctx) => {
|
|
129
|
+
const oldTheme = ctx.getRaw("theme");
|
|
130
|
+
if (oldTheme === "black") {
|
|
131
|
+
ctx.setRaw("theme", "dark");
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
migrateToLatest(StorageScope.Disk);
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Migration versions are tracked per scope.
|
|
139
|
+
|
|
140
|
+
## Secure Auth Storage
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const auth = createSecureAuthStorage({
|
|
144
|
+
accessToken: { ttlMs: 15 * 60 * 1000 },
|
|
145
|
+
refreshToken: { accessControl: AccessControl.AfterFirstUnlockThisDeviceOnly },
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
auth.accessToken.set("token");
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
The returned object is a typed record of secure string `StorageItem`s.
|
|
152
|
+
|
|
153
|
+
## Web Backend APIs
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
setWebDiskStorageBackend(backend);
|
|
157
|
+
getWebDiskStorageBackend();
|
|
158
|
+
setWebSecureStorageBackend(backend);
|
|
159
|
+
getWebSecureStorageBackend();
|
|
160
|
+
await flushWebStorageBackends();
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
See [web-backends.md](web-backends.md).
|
|
164
|
+
|
|
165
|
+
## Enums
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
enum StorageScope {
|
|
169
|
+
Memory = 0,
|
|
170
|
+
Disk = 1,
|
|
171
|
+
Secure = 2,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
enum BiometricLevel {
|
|
175
|
+
None = 0,
|
|
176
|
+
BiometryOrPasscode = 1,
|
|
177
|
+
BiometryOnly = 2,
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
`AccessControl` values:
|
|
182
|
+
|
|
183
|
+
- `WhenUnlocked`
|
|
184
|
+
- `AfterFirstUnlock`
|
|
185
|
+
- `WhenPasscodeSetThisDeviceOnly`
|
|
186
|
+
- `WhenUnlockedThisDeviceOnly`
|
|
187
|
+
- `AfterFirstUnlockThisDeviceOnly`
|
|
188
|
+
|
|
189
|
+
## Exported Types
|
|
190
|
+
|
|
191
|
+
Common public types:
|
|
192
|
+
|
|
193
|
+
- `Storage`
|
|
194
|
+
- `Validator<T>`
|
|
195
|
+
- `ExpirationConfig`
|
|
196
|
+
- `StorageItem<T>`
|
|
197
|
+
- `StorageItemConfig<T>`
|
|
198
|
+
- `StorageBatchSetItem<T>`
|
|
199
|
+
- `StorageVersion`
|
|
200
|
+
- `VersionedValue<T>`
|
|
201
|
+
- `StorageMetricsEvent`
|
|
202
|
+
- `StorageMetricsObserver`
|
|
203
|
+
- `StorageMetricSummary`
|
|
204
|
+
- `MigrationContext`
|
|
205
|
+
- `Migration`
|
|
206
|
+
- `TransactionContext`
|
|
207
|
+
- `SecureAuthStorageConfig<K>`
|
|
208
|
+
- `SecurityCapabilities`
|
|
209
|
+
- `SecureStorageMetadata`
|
|
210
|
+
- `StorageErrorCode`
|
|
211
|
+
- `WebStorageBackend`
|
|
212
|
+
- `WebDiskStorageBackend`
|
|
213
|
+
- `WebSecureStorageBackend`
|
|
214
|
+
- `WebStorageChangeEvent`
|
|
215
|
+
- `WebStorageScope`
|
|
216
|
+
|
|
217
|
+
The IndexedDB subpath exports `createIndexedDBBackend()` and `IndexedDBBackendOptions`.
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# Batch, Transactions, and Migrations
|
|
2
|
+
|
|
3
|
+
Use these APIs when a workflow touches several keys or needs a controlled upgrade path.
|
|
4
|
+
|
|
5
|
+
## Batch Reads
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
createStorageItem,
|
|
10
|
+
getBatch,
|
|
11
|
+
StorageScope,
|
|
12
|
+
} from "react-native-nitro-storage";
|
|
13
|
+
|
|
14
|
+
const themeItem = createStorageItem({
|
|
15
|
+
key: "theme",
|
|
16
|
+
scope: StorageScope.Disk,
|
|
17
|
+
defaultValue: "system",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const localeItem = createStorageItem({
|
|
21
|
+
key: "locale",
|
|
22
|
+
scope: StorageScope.Disk,
|
|
23
|
+
defaultValue: "en-US",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const [theme, locale] = getBatch([themeItem, localeItem], StorageScope.Disk);
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
All items in a batch must use the same scope.
|
|
30
|
+
|
|
31
|
+
## Batch Writes
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { setBatch, StorageScope } from "react-native-nitro-storage";
|
|
35
|
+
|
|
36
|
+
setBatch(
|
|
37
|
+
[
|
|
38
|
+
{ item: themeItem, value: "dark" },
|
|
39
|
+
{ item: localeItem, value: "pt-BR" },
|
|
40
|
+
],
|
|
41
|
+
StorageScope.Disk,
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Memory-scope batch writes are two-phase: all values are written first, then listeners are notified. Items with validation or TTL fall back to per-item writes so those rules still run.
|
|
46
|
+
|
|
47
|
+
## Batch Removes
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
import { removeBatch, StorageScope } from "react-native-nitro-storage";
|
|
51
|
+
|
|
52
|
+
removeBatch([themeItem, localeItem], StorageScope.Disk);
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Raw Import
|
|
56
|
+
|
|
57
|
+
`storage.import(data, scope)` writes raw strings. It does not serialize values.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { storage, StorageScope } from "react-native-nitro-storage";
|
|
61
|
+
|
|
62
|
+
storage.import(
|
|
63
|
+
{
|
|
64
|
+
"flags:newOnboarding": "true",
|
|
65
|
+
"flags:paywall": "control",
|
|
66
|
+
},
|
|
67
|
+
StorageScope.Disk,
|
|
68
|
+
);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
For Memory scope, import is atomic: all keys are written before listeners fire. For Disk and Secure, import delegates to native or web batch paths.
|
|
72
|
+
|
|
73
|
+
## Transactions
|
|
74
|
+
|
|
75
|
+
Use `runTransaction(scope, callback)` when several raw or item writes should roll back together if the callback throws.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { runTransaction, StorageScope } from "react-native-nitro-storage";
|
|
79
|
+
|
|
80
|
+
const fromBalanceItem = createStorageItem({
|
|
81
|
+
key: "account:from",
|
|
82
|
+
scope: StorageScope.Disk,
|
|
83
|
+
defaultValue: 100,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const toBalanceItem = createStorageItem({
|
|
87
|
+
key: "account:to",
|
|
88
|
+
scope: StorageScope.Disk,
|
|
89
|
+
defaultValue: 0,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
runTransaction(StorageScope.Disk, (tx) => {
|
|
93
|
+
const from = tx.getItem(fromBalanceItem);
|
|
94
|
+
|
|
95
|
+
if (from < 25) {
|
|
96
|
+
throw new Error("Insufficient balance");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
tx.setItem(fromBalanceItem, from - 25);
|
|
100
|
+
tx.setItem(toBalanceItem, tx.getItem(toBalanceItem) + 25);
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Transaction context methods:
|
|
105
|
+
|
|
106
|
+
- `getRaw(key)`
|
|
107
|
+
- `setRaw(key, value)`
|
|
108
|
+
- `removeRaw(key)`
|
|
109
|
+
- `getItem(item)`
|
|
110
|
+
- `setItem(item, value)`
|
|
111
|
+
- `removeItem(item)`
|
|
112
|
+
|
|
113
|
+
If the callback throws, Nitro Storage restores the keys it changed during that transaction.
|
|
114
|
+
|
|
115
|
+
## Migrations
|
|
116
|
+
|
|
117
|
+
Register migrations with monotonically increasing versions, then migrate a scope to the latest known version.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import {
|
|
121
|
+
migrateToLatest,
|
|
122
|
+
registerMigration,
|
|
123
|
+
StorageScope,
|
|
124
|
+
} from "react-native-nitro-storage";
|
|
125
|
+
|
|
126
|
+
registerMigration(1, (ctx) => {
|
|
127
|
+
const oldValue = ctx.getRaw("theme");
|
|
128
|
+
if (oldValue === "black") {
|
|
129
|
+
ctx.setRaw("theme", "dark");
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
registerMigration(2, (ctx) => {
|
|
134
|
+
const token = ctx.getRaw("token");
|
|
135
|
+
if (token) {
|
|
136
|
+
ctx.setRaw("auth:accessToken", token);
|
|
137
|
+
ctx.removeRaw("token");
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
migrateToLatest(StorageScope.Disk);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Migration context methods work with raw strings. Use item serializers manually when migrating structured data.
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
registerMigration(3, (ctx) => {
|
|
148
|
+
const raw = ctx.getRaw("settings");
|
|
149
|
+
if (!raw) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const settings = JSON.parse(raw) as { mode?: string };
|
|
154
|
+
ctx.setRaw(
|
|
155
|
+
"settings",
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
...settings,
|
|
158
|
+
theme: settings.mode ?? "system",
|
|
159
|
+
}),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Prefix Queries and Cleanup
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
const keys = storage.getKeysByPrefix("tenant:42:", StorageScope.Disk);
|
|
168
|
+
const values = storage.getByPrefix("tenant:42:", StorageScope.Disk);
|
|
169
|
+
|
|
170
|
+
storage.clearNamespace("tenant:42", StorageScope.Disk);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Use namespaces for multi-account or tenant-specific state so cleanup is predictable.
|
|
174
|
+
|
|
175
|
+
## Optimistic Versioned Writes
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const current = themeItem.getWithVersion();
|
|
179
|
+
|
|
180
|
+
const didWrite = themeItem.setIfVersion(
|
|
181
|
+
current.version,
|
|
182
|
+
current.value === "dark" ? "light" : "dark",
|
|
183
|
+
);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`setIfVersion()` returns `false` if another write changed the item after `getWithVersion()`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
Benchmarks are release checks, not product promises. Use them to catch regressions on the local machine and CI image used by this repo.
|
|
4
|
+
|
|
5
|
+
Run:
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun run benchmark -- --filter=react-native-nitro-storage
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The benchmark script checks representative synchronous read/write paths and fails when results drift beyond the configured threshold.
|
|
12
|
+
|
|
13
|
+
## Interpreting Results
|
|
14
|
+
|
|
15
|
+
- Compare results on the same machine and Node/Bun version.
|
|
16
|
+
- Treat large deltas as a prompt to inspect recent storage-runtime, serialization, native bridge, or cache changes.
|
|
17
|
+
- Do not compare web backend numbers against native secure storage numbers; they measure different systems.
|
|
18
|
+
- Secure storage performance depends on platform state, device lock state, biometric prompts, and Keystore/Keychain behavior.
|
|
19
|
+
|
|
20
|
+
## Release Checklist
|
|
21
|
+
|
|
22
|
+
Before publishing:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
bun run lint -- --filter=react-native-nitro-storage
|
|
26
|
+
bun run format:check -- --filter=react-native-nitro-storage
|
|
27
|
+
bun run typecheck -- --filter=react-native-nitro-storage
|
|
28
|
+
bun run test:types -- --filter=react-native-nitro-storage
|
|
29
|
+
bun run test -- --filter=react-native-nitro-storage
|
|
30
|
+
bun run test:cpp -- --filter=react-native-nitro-storage
|
|
31
|
+
bun run build -- --filter=react-native-nitro-storage
|
|
32
|
+
bun run benchmark -- --filter=react-native-nitro-storage
|
|
33
|
+
bun run --cwd packages/react-native-nitro-storage check:pack
|
|
34
|
+
npm publish --dry-run
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Keep the dry-publish output in the release notes when validating a version locally.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# MMKV Migration
|
|
2
|
+
|
|
3
|
+
Use `migrateFromMMKV(mmkv, item, deleteAfterMigration?)` when an app already stores values with `react-native-mmkv` and you want to move one key at a time into Nitro Storage.
|
|
4
|
+
|
|
5
|
+
The helper reads in this order:
|
|
6
|
+
|
|
7
|
+
1. `mmkv.getString(key)`
|
|
8
|
+
2. `mmkv.getNumber(key)`
|
|
9
|
+
3. `mmkv.getBoolean(key)`
|
|
10
|
+
|
|
11
|
+
It writes through `item.set()`, so custom serialization, validation, TTL behavior, and listeners remain active.
|
|
12
|
+
|
|
13
|
+
## Basic Migration
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import {
|
|
17
|
+
createStorageItem,
|
|
18
|
+
migrateFromMMKV,
|
|
19
|
+
StorageScope,
|
|
20
|
+
} from "react-native-nitro-storage";
|
|
21
|
+
import { MMKV } from "react-native-mmkv";
|
|
22
|
+
|
|
23
|
+
const mmkv = new MMKV();
|
|
24
|
+
|
|
25
|
+
const usernameItem = createStorageItem({
|
|
26
|
+
key: "username",
|
|
27
|
+
scope: StorageScope.Disk,
|
|
28
|
+
defaultValue: "",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const migrated = migrateFromMMKV(mmkv, usernameItem, true);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
`migrated` is `true` when a value was found and written. The third argument deletes the MMKV key after a successful write.
|
|
35
|
+
|
|
36
|
+
## Type Conversion
|
|
37
|
+
|
|
38
|
+
MMKV numbers and booleans are converted through the target item's serializer.
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const launchCountItem = createStorageItem({
|
|
42
|
+
key: "launchCount",
|
|
43
|
+
scope: StorageScope.Disk,
|
|
44
|
+
defaultValue: 0,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
migrateFromMMKV(mmkv, launchCountItem, true);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Custom Serialized Values
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
type Settings = {
|
|
54
|
+
compactMode: boolean;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const settingsItem = createStorageItem<Settings>({
|
|
58
|
+
key: "settings",
|
|
59
|
+
scope: StorageScope.Disk,
|
|
60
|
+
defaultValue: { compactMode: false },
|
|
61
|
+
serialize: JSON.stringify,
|
|
62
|
+
deserialize: JSON.parse,
|
|
63
|
+
validate: (value): value is Settings =>
|
|
64
|
+
typeof value === "object" &&
|
|
65
|
+
value !== null &&
|
|
66
|
+
"compactMode" in value &&
|
|
67
|
+
typeof value.compactMode === "boolean",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
migrateFromMMKV(mmkv, settingsItem, true);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Migration Strategy
|
|
74
|
+
|
|
75
|
+
- Migrate stable keys first: preferences, feature flags, and local settings.
|
|
76
|
+
- Keep secure credentials in Secure scope instead of moving them to Disk scope.
|
|
77
|
+
- Run migration once during startup, before components read the target item.
|
|
78
|
+
- Delete the MMKV key only after you have shipped and observed the migration path.
|
|
79
|
+
|
|
80
|
+
For larger data-shape upgrades, use the versioned migration APIs in [batch-transactions-migrations.md](batch-transactions-migrations.md).
|