react-native-nitro-storage 0.4.5 → 0.5.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 +254 -945
- package/SECURITY.md +26 -0
- package/docs/api-reference.md +281 -0
- package/docs/batch-transactions-migrations.md +200 -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 +302 -0
- package/docs/secure-storage.md +190 -0
- package/docs/web-backends.md +141 -0
- package/lib/commonjs/index.js +265 -14
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +220 -11
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-events.js +117 -0
- package/lib/commonjs/storage-events.js.map +1 -0
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/module/index.js +265 -14
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +220 -11
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-events.js +112 -0
- package/lib/module/storage-events.js.map +1 -0
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/typescript/index.d.ts +19 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +19 -2
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-events.d.ts +37 -0
- package/lib/typescript/storage-events.d.ts.map +1 -0
- package/lib/typescript/storage-runtime.d.ts +32 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/package.json +25 -11
- package/src/index.ts +601 -14
- package/src/index.web.ts +535 -22
- package/src/storage-events.ts +184 -0
- package/src/storage-runtime.ts +35 -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.
|
|
@@ -0,0 +1,281 @@
|
|
|
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
|
+
| `subscribeSelector(...)` | Subscribe to a selected value with an equality check. |
|
|
48
|
+
| `serialize(value)` | Serialize a value with the item encoder. |
|
|
49
|
+
| `deserialize(value)` | Deserialize a raw string with the item decoder. |
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const unsubscribe = profileItem.subscribeSelector(
|
|
53
|
+
(profile) => profile.name,
|
|
54
|
+
(name, previousName) => {
|
|
55
|
+
console.log("Profile name changed", { name, previousName });
|
|
56
|
+
},
|
|
57
|
+
{ fireImmediately: true },
|
|
58
|
+
);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## React Hooks
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const [value, setValue] = useStorage(item);
|
|
65
|
+
const [selected, setItem] = useStorageSelector(item, selector, isEqual);
|
|
66
|
+
const setOnly = useSetStorage(item);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
See [react-hooks.md](react-hooks.md).
|
|
70
|
+
|
|
71
|
+
## storage
|
|
72
|
+
|
|
73
|
+
`storage` exposes raw and cross-item utilities:
|
|
74
|
+
|
|
75
|
+
| Method | Purpose |
|
|
76
|
+
| ------------------------------------------------ | ------------------------------------------------------------- |
|
|
77
|
+
| `clear(scope)` | Clear one scope. |
|
|
78
|
+
| `clearAll()` | Clear Memory, Disk, and Secure scopes. |
|
|
79
|
+
| `clearNamespace(namespace, scope)` | Remove keys under `namespace:`. |
|
|
80
|
+
| `subscribe(scope, listener)` | Subscribe to raw scope-level change events. |
|
|
81
|
+
| `subscribeKey(scope, key, listener)` | Subscribe to raw events for one key. |
|
|
82
|
+
| `subscribePrefix(scope, prefix, listener)` | Subscribe to raw events for matching key prefixes. |
|
|
83
|
+
| `subscribeNamespace(namespace, scope, listener)` | Subscribe to raw events for `namespace:` keys. |
|
|
84
|
+
| `setEventObserver(observer)` | Receive all change events for devtools or logging. |
|
|
85
|
+
| `clearBiometric()` | Clear biometric Secure entries. |
|
|
86
|
+
| `has(key, scope)` | Check for a raw key. |
|
|
87
|
+
| `getAllKeys(scope)` | List raw keys. |
|
|
88
|
+
| `getKeysByPrefix(prefix, scope)` | List raw keys with a prefix. |
|
|
89
|
+
| `getByPrefix(prefix, scope)` | Read raw string values by prefix. |
|
|
90
|
+
| `getAll(scope)` | Read all raw string values in a scope. |
|
|
91
|
+
| `size(scope)` | Return approximate scope entry count. |
|
|
92
|
+
| `setAccessControl(accessControl)` | Set the default Secure access control level. |
|
|
93
|
+
| `setSecureWritesAsync(enabled)` | Toggle Android secure writes between sync and async modes. |
|
|
94
|
+
| `setDiskWritesAsync(enabled)` | Toggle coalesced Disk write behavior. |
|
|
95
|
+
| `flushDiskWrites()` | Flush pending Disk writes. |
|
|
96
|
+
| `flushSecureWrites()` | Flush pending Secure writes. |
|
|
97
|
+
| `setKeychainAccessGroup(group)` | Configure iOS Keychain access group. |
|
|
98
|
+
| `setMetricsObserver(observer)` | Receive operation timing events. |
|
|
99
|
+
| `getMetricsSnapshot()` | Read aggregated metrics. |
|
|
100
|
+
| `resetMetrics()` | Clear metrics counters. |
|
|
101
|
+
| `getCapabilities()` | Read runtime storage capabilities. |
|
|
102
|
+
| `getSecurityCapabilities()` | Read secure backend capability metadata. |
|
|
103
|
+
| `getSecureMetadata(key)` | Read secure metadata for one key without returning its value. |
|
|
104
|
+
| `getAllSecureMetadata()` | Read secure metadata for all secure keys without values. |
|
|
105
|
+
| `getString(key, scope)` | Read a raw string. |
|
|
106
|
+
| `setString(key, value, scope)` | Write a raw string. |
|
|
107
|
+
| `deleteString(key, scope)` | Remove a raw key. |
|
|
108
|
+
| `export(scope)` | Snapshot raw strings from one scope. |
|
|
109
|
+
| `import(data, scope)` | Bulk import raw strings. |
|
|
110
|
+
|
|
111
|
+
Raw string APIs bypass item serialization and validation. Prefer `StorageItem<T>` unless you are migrating, exporting/importing, or writing a custom integration.
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const diskSnapshot = storage.export(StorageScope.Disk);
|
|
115
|
+
storage.import(diskSnapshot, StorageScope.Disk);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Secure exports contain raw secret values. Do not log `storage.export(StorageScope.Secure)` output or attach it to diagnostics, analytics, crash reports, or support bundles.
|
|
119
|
+
|
|
120
|
+
## Event Subscriptions
|
|
121
|
+
|
|
122
|
+
Use raw subscriptions when integrating Nitro Storage with state managers, sync engines, debug tooling, or non-React code.
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
const unsubscribe = storage.subscribeNamespace(
|
|
126
|
+
"auth",
|
|
127
|
+
StorageScope.Secure,
|
|
128
|
+
(event) => {
|
|
129
|
+
if (event.type === "batch") {
|
|
130
|
+
console.log(
|
|
131
|
+
"Auth keys changed",
|
|
132
|
+
event.changes.map((change) => change.key),
|
|
133
|
+
);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
console.log("Auth key changed", event.key, event.operation);
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
For whole-app debug tooling, install one observer:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
storage.setEventObserver((event) => {
|
|
146
|
+
if (event.scope !== StorageScope.Secure) {
|
|
147
|
+
console.log(event);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Local batch APIs emit one `type: "batch"` envelope to scope and prefix/namespace listeners. Key subscribers receive the matching per-key change so direct key integrations do not need to unpack batch envelopes. Secure events can include raw secret values; do not log Secure event payloads in production.
|
|
153
|
+
|
|
154
|
+
## Batch Operations
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const values = getBatch([themeItem, localeItem], StorageScope.Disk);
|
|
158
|
+
|
|
159
|
+
setBatch(
|
|
160
|
+
[
|
|
161
|
+
{ item: themeItem, value: "dark" },
|
|
162
|
+
{ item: localeItem, value: "en-US" },
|
|
163
|
+
],
|
|
164
|
+
StorageScope.Disk,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
removeBatch([themeItem, localeItem], StorageScope.Disk);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
See [batch-transactions-migrations.md](batch-transactions-migrations.md).
|
|
171
|
+
|
|
172
|
+
## Transactions
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
runTransaction(StorageScope.Disk, (tx) => {
|
|
176
|
+
const current = tx.getItem(balanceItem);
|
|
177
|
+
tx.setItem(balanceItem, current + 10);
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
If the callback throws, previously changed keys in that transaction are rolled back synchronously.
|
|
182
|
+
|
|
183
|
+
## Migrations
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
registerMigration(2, (ctx) => {
|
|
187
|
+
const oldTheme = ctx.getRaw("theme");
|
|
188
|
+
if (oldTheme === "black") {
|
|
189
|
+
ctx.setRaw("theme", "dark");
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
migrateToLatest(StorageScope.Disk);
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Migration versions are tracked per scope.
|
|
197
|
+
|
|
198
|
+
## Secure Auth Storage
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
const auth = createSecureAuthStorage({
|
|
202
|
+
accessToken: { ttlMs: 15 * 60 * 1000 },
|
|
203
|
+
refreshToken: { accessControl: AccessControl.AfterFirstUnlockThisDeviceOnly },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
auth.accessToken.set("token");
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
The returned object is a typed record of secure string `StorageItem`s.
|
|
210
|
+
|
|
211
|
+
## Web Backend APIs
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
setWebDiskStorageBackend(backend);
|
|
215
|
+
getWebDiskStorageBackend();
|
|
216
|
+
setWebSecureStorageBackend(backend);
|
|
217
|
+
getWebSecureStorageBackend();
|
|
218
|
+
await flushWebStorageBackends();
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
See [web-backends.md](web-backends.md).
|
|
222
|
+
|
|
223
|
+
## Enums
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
enum StorageScope {
|
|
227
|
+
Memory = 0,
|
|
228
|
+
Disk = 1,
|
|
229
|
+
Secure = 2,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
enum BiometricLevel {
|
|
233
|
+
None = 0,
|
|
234
|
+
BiometryOrPasscode = 1,
|
|
235
|
+
BiometryOnly = 2,
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`AccessControl` values:
|
|
240
|
+
|
|
241
|
+
- `WhenUnlocked`
|
|
242
|
+
- `AfterFirstUnlock`
|
|
243
|
+
- `WhenPasscodeSetThisDeviceOnly`
|
|
244
|
+
- `WhenUnlockedThisDeviceOnly`
|
|
245
|
+
- `AfterFirstUnlockThisDeviceOnly`
|
|
246
|
+
|
|
247
|
+
## Exported Types
|
|
248
|
+
|
|
249
|
+
Common public types:
|
|
250
|
+
|
|
251
|
+
- `Storage`
|
|
252
|
+
- `Validator<T>`
|
|
253
|
+
- `ExpirationConfig`
|
|
254
|
+
- `StorageItem<T>`
|
|
255
|
+
- `StorageItemConfig<T>`
|
|
256
|
+
- `StorageBatchSetItem<T>`
|
|
257
|
+
- `StorageVersion`
|
|
258
|
+
- `VersionedValue<T>`
|
|
259
|
+
- `StorageMetricsEvent`
|
|
260
|
+
- `StorageMetricsObserver`
|
|
261
|
+
- `StorageMetricSummary`
|
|
262
|
+
- `StorageChangeEvent`
|
|
263
|
+
- `StorageKeyChangeEvent`
|
|
264
|
+
- `StorageBatchChangeEvent`
|
|
265
|
+
- `StorageChangeOperation`
|
|
266
|
+
- `StorageChangeSource`
|
|
267
|
+
- `StorageEventListener`
|
|
268
|
+
- `MigrationContext`
|
|
269
|
+
- `Migration`
|
|
270
|
+
- `TransactionContext`
|
|
271
|
+
- `SecureAuthStorageConfig<K>`
|
|
272
|
+
- `SecurityCapabilities`
|
|
273
|
+
- `SecureStorageMetadata`
|
|
274
|
+
- `StorageErrorCode`
|
|
275
|
+
- `WebStorageBackend`
|
|
276
|
+
- `WebDiskStorageBackend`
|
|
277
|
+
- `WebSecureStorageBackend`
|
|
278
|
+
- `WebStorageChangeEvent`
|
|
279
|
+
- `WebStorageScope`
|
|
280
|
+
|
|
281
|
+
The IndexedDB subpath exports `createIndexedDBBackend()` and `IndexedDBBackendOptions`.
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
+
Scope and prefix subscriptions receive one batch event for raw batch writes and removes:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const unsubscribe = storage.subscribePrefix(
|
|
51
|
+
StorageScope.Disk,
|
|
52
|
+
"settings:",
|
|
53
|
+
(event) => {
|
|
54
|
+
if (event.type === "batch") {
|
|
55
|
+
event.changes.forEach((change) => {
|
|
56
|
+
console.log(change.key, change.operation);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Batch Removes
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
import { removeBatch, StorageScope } from "react-native-nitro-storage";
|
|
67
|
+
|
|
68
|
+
removeBatch([themeItem, localeItem], StorageScope.Disk);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Raw Import and Export
|
|
72
|
+
|
|
73
|
+
`storage.export(scope)` returns raw strings, and `storage.import(data, scope)` writes raw strings. These APIs do not serialize values.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { storage, StorageScope } from "react-native-nitro-storage";
|
|
77
|
+
|
|
78
|
+
const snapshot = storage.export(StorageScope.Disk);
|
|
79
|
+
|
|
80
|
+
storage.import(snapshot, StorageScope.Disk);
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
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.
|
|
84
|
+
|
|
85
|
+
Secure exports contain raw secret values. Do not log them or include them in diagnostics, analytics, crash reports, or support bundles.
|
|
86
|
+
|
|
87
|
+
## Transactions
|
|
88
|
+
|
|
89
|
+
Use `runTransaction(scope, callback)` when several raw or item writes should roll back together if the callback throws.
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
import { runTransaction, StorageScope } from "react-native-nitro-storage";
|
|
93
|
+
|
|
94
|
+
const fromBalanceItem = createStorageItem({
|
|
95
|
+
key: "account:from",
|
|
96
|
+
scope: StorageScope.Disk,
|
|
97
|
+
defaultValue: 100,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const toBalanceItem = createStorageItem({
|
|
101
|
+
key: "account:to",
|
|
102
|
+
scope: StorageScope.Disk,
|
|
103
|
+
defaultValue: 0,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
runTransaction(StorageScope.Disk, (tx) => {
|
|
107
|
+
const from = tx.getItem(fromBalanceItem);
|
|
108
|
+
|
|
109
|
+
if (from < 25) {
|
|
110
|
+
throw new Error("Insufficient balance");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
tx.setItem(fromBalanceItem, from - 25);
|
|
114
|
+
tx.setItem(toBalanceItem, tx.getItem(toBalanceItem) + 25);
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Transaction context methods:
|
|
119
|
+
|
|
120
|
+
- `getRaw(key)`
|
|
121
|
+
- `setRaw(key, value)`
|
|
122
|
+
- `removeRaw(key)`
|
|
123
|
+
- `getItem(item)`
|
|
124
|
+
- `setItem(item, value)`
|
|
125
|
+
- `removeItem(item)`
|
|
126
|
+
|
|
127
|
+
If the callback throws, Nitro Storage restores the keys it changed during that transaction.
|
|
128
|
+
|
|
129
|
+
## Migrations
|
|
130
|
+
|
|
131
|
+
Register migrations with monotonically increasing versions, then migrate a scope to the latest known version.
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import {
|
|
135
|
+
migrateToLatest,
|
|
136
|
+
registerMigration,
|
|
137
|
+
StorageScope,
|
|
138
|
+
} from "react-native-nitro-storage";
|
|
139
|
+
|
|
140
|
+
registerMigration(1, (ctx) => {
|
|
141
|
+
const oldValue = ctx.getRaw("theme");
|
|
142
|
+
if (oldValue === "black") {
|
|
143
|
+
ctx.setRaw("theme", "dark");
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
registerMigration(2, (ctx) => {
|
|
148
|
+
const token = ctx.getRaw("token");
|
|
149
|
+
if (token) {
|
|
150
|
+
ctx.setRaw("auth:accessToken", token);
|
|
151
|
+
ctx.removeRaw("token");
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
migrateToLatest(StorageScope.Disk);
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Migration context methods work with raw strings. Use item serializers manually when migrating structured data.
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
registerMigration(3, (ctx) => {
|
|
162
|
+
const raw = ctx.getRaw("settings");
|
|
163
|
+
if (!raw) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const settings = JSON.parse(raw) as { mode?: string };
|
|
168
|
+
ctx.setRaw(
|
|
169
|
+
"settings",
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
...settings,
|
|
172
|
+
theme: settings.mode ?? "system",
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Prefix Queries and Cleanup
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const keys = storage.getKeysByPrefix("tenant:42:", StorageScope.Disk);
|
|
182
|
+
const values = storage.getByPrefix("tenant:42:", StorageScope.Disk);
|
|
183
|
+
|
|
184
|
+
storage.clearNamespace("tenant:42", StorageScope.Disk);
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Use namespaces for multi-account or tenant-specific state so cleanup is predictable.
|
|
188
|
+
|
|
189
|
+
## Optimistic Versioned Writes
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const current = themeItem.getWithVersion();
|
|
193
|
+
|
|
194
|
+
const didWrite = themeItem.setIfVersion(
|
|
195
|
+
current.version,
|
|
196
|
+
current.value === "dark" ? "light" : "dark",
|
|
197
|
+
);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
`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).
|