react-native-nitro-storage 0.3.1 → 0.4.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 +334 -34
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +26 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +4 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +90 -18
- package/cpp/bindings/HybridStorage.cpp +214 -23
- package/cpp/bindings/HybridStorage.hpp +31 -3
- package/cpp/core/NativeStorageAdapter.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.hpp +17 -0
- package/ios/IOSStorageAdapterCpp.mm +140 -10
- package/lib/commonjs/index.js +555 -288
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +750 -309
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +25 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/index.js +537 -287
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +732 -308
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +24 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +4 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +41 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +45 -4
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +10 -0
- package/lib/typescript/storage-hooks.d.ts.map +1 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +4 -0
- package/package.json +5 -3
- package/src/Storage.nitro.ts +4 -0
- package/src/index.ts +704 -324
- package/src/index.web.ts +929 -346
- package/src/internal.ts +28 -0
- package/src/storage-hooks.ts +48 -0
package/README.md
CHANGED
|
@@ -14,11 +14,37 @@ Synchronous Memory, Disk, and Secure storage in one unified API — powered by [
|
|
|
14
14
|
- **Biometric storage** — hardware-backed biometric protection on iOS & Android
|
|
15
15
|
- **Auth storage factory** — `createSecureAuthStorage` for multi-token auth flows
|
|
16
16
|
- **Batch operations** — atomic multi-key get/set/remove via native batch APIs
|
|
17
|
+
- **Prefix queries** — fast key/value scans with `storage.getKeysByPrefix` and `storage.getByPrefix`
|
|
18
|
+
- **Versioned writes** — optimistic concurrency with `item.getWithVersion()` and `item.setIfVersion(...)`
|
|
19
|
+
- **Performance metrics** — observe operation timings and aggregate snapshots
|
|
20
|
+
- **Web secure backend override** — plug custom secure storage backend on web
|
|
17
21
|
- **Transactions** — grouped writes with automatic rollback on error
|
|
18
22
|
- **Migrations** — versioned data migrations with `registerMigration` / `migrateToLatest`
|
|
19
23
|
- **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
|
|
20
24
|
- **Cross-platform** — iOS, Android, and web (`localStorage` fallback)
|
|
21
25
|
|
|
26
|
+
## Feature Coverage
|
|
27
|
+
|
|
28
|
+
Every feature in this package is documented with at least one runnable example in this README:
|
|
29
|
+
|
|
30
|
+
- Core item API (`createStorageItem`, `get/set/delete/has/subscribe`) — see Quick Start and Low-level subscription use case
|
|
31
|
+
- Hooks (`useStorage`, `useStorageSelector`, `useSetStorage`) — see Quick Start and Persisted User Preferences
|
|
32
|
+
- Scopes (`Memory`, `Disk`, `Secure`) — see Storage Scopes and multiple use cases
|
|
33
|
+
- Namespaces — see Multi-Tenant / Namespaced Storage
|
|
34
|
+
- TTL expiration + callbacks — see OTP / Temporary Codes
|
|
35
|
+
- Validation + recovery — see Feature Flags with Validation
|
|
36
|
+
- Biometric + access control — see Biometric-protected Secrets
|
|
37
|
+
- Global storage utilities (`clear*`, `has`, `getAll*`, `size`, secure write settings) — see Global utility examples and Storage Snapshots and Cleanup
|
|
38
|
+
- Prefix utilities (`getKeysByPrefix`, `getByPrefix`) — see Prefix Queries and Namespace Inspection
|
|
39
|
+
- Versioned item API (`getWithVersion`, `setIfVersion`) — see Optimistic Versioned Writes
|
|
40
|
+
- Metrics API (`setMetricsObserver`, `getMetricsSnapshot`, `resetMetrics`) — see Storage Metrics Instrumentation
|
|
41
|
+
- Web secure backend override (`setWebSecureStorageBackend`, `getWebSecureStorageBackend`) — see Custom Web Secure Backend
|
|
42
|
+
- Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
|
|
43
|
+
- Transactions — see Transactions and Atomic Balance Transfer
|
|
44
|
+
- Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
|
|
45
|
+
- MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
|
|
46
|
+
- Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
|
|
47
|
+
|
|
22
48
|
## Requirements
|
|
23
49
|
|
|
24
50
|
| Dependency | Version |
|
|
@@ -167,21 +193,35 @@ function createStorageItem<T = undefined>(
|
|
|
167
193
|
| `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
|
|
168
194
|
| `namespace` | `string` | — | Prefix key as `namespace:key` for isolation |
|
|
169
195
|
| `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
|
|
196
|
+
| `biometricLevel` | `BiometricLevel` | `None` | Biometric policy (`BiometryOrPasscode` / `BiometryOnly`) |
|
|
170
197
|
| `accessControl` | `AccessControl` | — | Keychain access control level (native only) |
|
|
171
198
|
|
|
172
199
|
**Returned `StorageItem<T>`:**
|
|
173
200
|
|
|
174
|
-
| Method / Property
|
|
175
|
-
|
|
|
176
|
-
| `get()`
|
|
177
|
-
| `
|
|
178
|
-
| `
|
|
179
|
-
| `
|
|
180
|
-
| `
|
|
181
|
-
| `
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
201
|
+
| Method / Property | Type | Description |
|
|
202
|
+
| ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
|
|
203
|
+
| `get()` | `() => T` | Read current value (synchronous) |
|
|
204
|
+
| `getWithVersion()` | `() => { value: T; version: StorageVersion }` | Read value plus current storage version token |
|
|
205
|
+
| `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
|
|
206
|
+
| `setIfVersion(...)` | `(version: StorageVersion, value: T \| ((prev: T) => T)) => boolean` | Write only if version matches (optimistic concurrency) |
|
|
207
|
+
| `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
|
|
208
|
+
| `has()` | `() => boolean` | Check if a value exists in storage |
|
|
209
|
+
| `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
|
|
210
|
+
| `serialize` | `(v: T) => string` | The item's serializer |
|
|
211
|
+
| `deserialize` | `(v: string) => T` | The item's deserializer |
|
|
212
|
+
| `scope` | `StorageScope` | The item's scope |
|
|
213
|
+
| `key` | `string` | The resolved key (includes namespace prefix) |
|
|
214
|
+
|
|
215
|
+
**Non-React subscription example:**
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const unsubscribe = sessionItem.subscribe(() => {
|
|
219
|
+
console.log("session changed:", sessionItem.get());
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
sessionItem.set("next-session");
|
|
223
|
+
unsubscribe();
|
|
224
|
+
```
|
|
185
225
|
|
|
186
226
|
---
|
|
187
227
|
|
|
@@ -220,21 +260,93 @@ setToken("new-token");
|
|
|
220
260
|
import { storage, StorageScope } from "react-native-nitro-storage";
|
|
221
261
|
```
|
|
222
262
|
|
|
223
|
-
| Method
|
|
224
|
-
|
|
|
225
|
-
| `storage.clear(scope)`
|
|
226
|
-
| `storage.clearAll()`
|
|
227
|
-
| `storage.clearNamespace(ns, scope)`
|
|
228
|
-
| `storage.clearBiometric()`
|
|
229
|
-
| `storage.has(key, scope)`
|
|
230
|
-
| `storage.getAllKeys(scope)`
|
|
231
|
-
| `storage.
|
|
232
|
-
| `storage.
|
|
233
|
-
| `storage.
|
|
234
|
-
| `storage.
|
|
263
|
+
| Method | Description |
|
|
264
|
+
| ---------------------------------------- | ---------------------------------------------------------------------------- |
|
|
265
|
+
| `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
|
|
266
|
+
| `storage.clearAll()` | Clear Memory + Disk + Secure |
|
|
267
|
+
| `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
|
|
268
|
+
| `storage.clearBiometric()` | Remove all biometric-prefixed keys |
|
|
269
|
+
| `storage.has(key, scope)` | Check if a key exists |
|
|
270
|
+
| `storage.getAllKeys(scope)` | Get all key names |
|
|
271
|
+
| `storage.getKeysByPrefix(prefix, scope)` | Get keys that start with a prefix |
|
|
272
|
+
| `storage.getByPrefix(prefix, scope)` | Get raw key-value pairs for keys matching a prefix |
|
|
273
|
+
| `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
|
|
274
|
+
| `storage.size(scope)` | Number of stored keys |
|
|
275
|
+
| `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
|
|
276
|
+
| `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
|
|
277
|
+
| `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
|
|
278
|
+
| `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
|
|
279
|
+
| `storage.setMetricsObserver(observer?)` | Subscribe to per-operation timing events |
|
|
280
|
+
| `storage.getMetricsSnapshot()` | Get aggregate counters/latency stats keyed by operation |
|
|
281
|
+
| `storage.resetMetrics()` | Reset in-memory metrics counters |
|
|
235
282
|
|
|
236
283
|
> `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
|
|
237
284
|
|
|
285
|
+
#### Global utility examples
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import {
|
|
289
|
+
AccessControl,
|
|
290
|
+
storage,
|
|
291
|
+
StorageScope,
|
|
292
|
+
} from "react-native-nitro-storage";
|
|
293
|
+
|
|
294
|
+
storage.has("session", StorageScope.Disk);
|
|
295
|
+
storage.getAllKeys(StorageScope.Disk);
|
|
296
|
+
storage.getKeysByPrefix("user-42:", StorageScope.Disk);
|
|
297
|
+
storage.getByPrefix("user-42:", StorageScope.Disk);
|
|
298
|
+
storage.getAll(StorageScope.Disk);
|
|
299
|
+
storage.size(StorageScope.Disk);
|
|
300
|
+
|
|
301
|
+
storage.clearNamespace("user-42", StorageScope.Disk);
|
|
302
|
+
storage.clearBiometric();
|
|
303
|
+
|
|
304
|
+
storage.setAccessControl(AccessControl.WhenUnlockedThisDeviceOnly);
|
|
305
|
+
storage.setKeychainAccessGroup("group.com.example.shared");
|
|
306
|
+
|
|
307
|
+
storage.clear(StorageScope.Memory);
|
|
308
|
+
storage.clearAll();
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Android secure write mode
|
|
312
|
+
|
|
313
|
+
`storage.setSecureWritesAsync(true)` switches secure writes from synchronous `commit()` to asynchronous `apply()` on Android.
|
|
314
|
+
Use this for non-critical secure writes when lower latency matters more than immediate durability.
|
|
315
|
+
|
|
316
|
+
Call `storage.flushSecureWrites()` when you need deterministic persistence boundaries (for example before namespace clears, process handoff, or strict test assertions).
|
|
317
|
+
|
|
318
|
+
```ts
|
|
319
|
+
import { storage } from "react-native-nitro-storage";
|
|
320
|
+
|
|
321
|
+
storage.setSecureWritesAsync(true);
|
|
322
|
+
|
|
323
|
+
// ...multiple secure writes happen (including coalesced item writes)
|
|
324
|
+
|
|
325
|
+
storage.flushSecureWrites(); // deterministic durability boundary
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
#### Custom web secure backend
|
|
329
|
+
|
|
330
|
+
By default, web Secure scope uses `localStorage` with `__secure_` key prefixing. You can replace it with a custom backend (for example encrypted IndexedDB adapter).
|
|
331
|
+
|
|
332
|
+
```ts
|
|
333
|
+
import {
|
|
334
|
+
getWebSecureStorageBackend,
|
|
335
|
+
setWebSecureStorageBackend,
|
|
336
|
+
} from "react-native-nitro-storage";
|
|
337
|
+
|
|
338
|
+
setWebSecureStorageBackend({
|
|
339
|
+
getItem: (key) => encryptedStore.get(key) ?? null,
|
|
340
|
+
setItem: (key, value) => encryptedStore.set(key, value),
|
|
341
|
+
removeItem: (key) => encryptedStore.delete(key),
|
|
342
|
+
clear: () => encryptedStore.clear(),
|
|
343
|
+
getAllKeys: () => encryptedStore.keys(),
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
const backend = getWebSecureStorageBackend();
|
|
347
|
+
console.log("custom backend active:", backend !== undefined);
|
|
348
|
+
```
|
|
349
|
+
|
|
238
350
|
---
|
|
239
351
|
|
|
240
352
|
### `createSecureAuthStorage<K>(config, options?)`
|
|
@@ -250,7 +362,7 @@ function createSecureAuthStorage<K extends string>(
|
|
|
250
362
|
|
|
251
363
|
- Default namespace: `"auth"`
|
|
252
364
|
- Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
|
|
253
|
-
- Supports per-key TTL, biometric, and access control
|
|
365
|
+
- Supports per-key TTL, biometric level policy, and access control
|
|
254
366
|
|
|
255
367
|
---
|
|
256
368
|
|
|
@@ -398,11 +510,11 @@ enum BiometricLevel {
|
|
|
398
510
|
### Persisted User Preferences
|
|
399
511
|
|
|
400
512
|
```ts
|
|
401
|
-
|
|
513
|
+
type UserPreferences = {
|
|
402
514
|
theme: "light" | "dark" | "system";
|
|
403
515
|
language: string;
|
|
404
516
|
notifications: boolean;
|
|
405
|
-
}
|
|
517
|
+
};
|
|
406
518
|
|
|
407
519
|
const prefsItem = createStorageItem<UserPreferences>({
|
|
408
520
|
key: "prefs",
|
|
@@ -446,21 +558,29 @@ storage.clearNamespace("myapp-auth", StorageScope.Secure);
|
|
|
446
558
|
### Feature Flags with Validation
|
|
447
559
|
|
|
448
560
|
```ts
|
|
449
|
-
|
|
561
|
+
type FeatureFlags = {
|
|
450
562
|
darkMode: boolean;
|
|
451
563
|
betaFeature: boolean;
|
|
452
564
|
maxUploadMb: number;
|
|
453
|
-
}
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
568
|
+
typeof value === "object" && value !== null;
|
|
569
|
+
|
|
570
|
+
const isFeatureFlags = (value: unknown): value is FeatureFlags => {
|
|
571
|
+
if (!isRecord(value)) return false;
|
|
572
|
+
return (
|
|
573
|
+
typeof value.darkMode === "boolean" &&
|
|
574
|
+
typeof value.betaFeature === "boolean" &&
|
|
575
|
+
typeof value.maxUploadMb === "number"
|
|
576
|
+
);
|
|
577
|
+
};
|
|
454
578
|
|
|
455
579
|
const flagsItem = createStorageItem<FeatureFlags>({
|
|
456
580
|
key: "feature-flags",
|
|
457
581
|
scope: StorageScope.Disk,
|
|
458
582
|
defaultValue: { darkMode: false, betaFeature: false, maxUploadMb: 10 },
|
|
459
|
-
validate:
|
|
460
|
-
typeof v === "object" &&
|
|
461
|
-
v !== null &&
|
|
462
|
-
typeof (v as any).darkMode === "boolean" &&
|
|
463
|
-
typeof (v as any).maxUploadMb === "number",
|
|
583
|
+
validate: isFeatureFlags,
|
|
464
584
|
onValidationError: () => ({
|
|
465
585
|
darkMode: false,
|
|
466
586
|
betaFeature: false,
|
|
@@ -471,6 +591,29 @@ const flagsItem = createStorageItem<FeatureFlags>({
|
|
|
471
591
|
});
|
|
472
592
|
```
|
|
473
593
|
|
|
594
|
+
### Biometric-protected Secrets
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
import {
|
|
598
|
+
AccessControl,
|
|
599
|
+
BiometricLevel,
|
|
600
|
+
createStorageItem,
|
|
601
|
+
StorageScope,
|
|
602
|
+
} from "react-native-nitro-storage";
|
|
603
|
+
|
|
604
|
+
const paymentPin = createStorageItem<string>({
|
|
605
|
+
key: "payment-pin",
|
|
606
|
+
scope: StorageScope.Secure,
|
|
607
|
+
defaultValue: "",
|
|
608
|
+
biometricLevel: BiometricLevel.BiometryOnly,
|
|
609
|
+
accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
paymentPin.set("4829");
|
|
613
|
+
const pin = paymentPin.get();
|
|
614
|
+
paymentPin.delete();
|
|
615
|
+
```
|
|
616
|
+
|
|
474
617
|
### Multi-Tenant / Namespaced Storage
|
|
475
618
|
|
|
476
619
|
```ts
|
|
@@ -515,6 +658,40 @@ otpItem.set("482917");
|
|
|
515
658
|
const code = otpItem.get();
|
|
516
659
|
```
|
|
517
660
|
|
|
661
|
+
### Bulk Bootstrap with Batch APIs
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
import {
|
|
665
|
+
createStorageItem,
|
|
666
|
+
getBatch,
|
|
667
|
+
removeBatch,
|
|
668
|
+
setBatch,
|
|
669
|
+
StorageScope,
|
|
670
|
+
} from "react-native-nitro-storage";
|
|
671
|
+
|
|
672
|
+
const firstName = createStorageItem({
|
|
673
|
+
key: "first-name",
|
|
674
|
+
scope: StorageScope.Disk,
|
|
675
|
+
defaultValue: "",
|
|
676
|
+
});
|
|
677
|
+
const lastName = createStorageItem({
|
|
678
|
+
key: "last-name",
|
|
679
|
+
scope: StorageScope.Disk,
|
|
680
|
+
defaultValue: "",
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
setBatch(
|
|
684
|
+
[
|
|
685
|
+
{ item: firstName, value: "Ada" },
|
|
686
|
+
{ item: lastName, value: "Lovelace" },
|
|
687
|
+
],
|
|
688
|
+
StorageScope.Disk,
|
|
689
|
+
);
|
|
690
|
+
|
|
691
|
+
const [first, last] = getBatch([firstName, lastName], StorageScope.Disk);
|
|
692
|
+
removeBatch([firstName, lastName], StorageScope.Disk);
|
|
693
|
+
```
|
|
694
|
+
|
|
518
695
|
### Atomic Balance Transfer
|
|
519
696
|
|
|
520
697
|
```ts
|
|
@@ -555,6 +732,116 @@ const compactItem = createStorageItem<{ id: number; active: boolean }>({
|
|
|
555
732
|
});
|
|
556
733
|
```
|
|
557
734
|
|
|
735
|
+
### Coalesced Secure Writes with Deterministic Flush
|
|
736
|
+
|
|
737
|
+
```ts
|
|
738
|
+
import {
|
|
739
|
+
createStorageItem,
|
|
740
|
+
storage,
|
|
741
|
+
StorageScope,
|
|
742
|
+
} from "react-native-nitro-storage";
|
|
743
|
+
|
|
744
|
+
const sessionToken = createStorageItem<string>({
|
|
745
|
+
key: "session-token",
|
|
746
|
+
scope: StorageScope.Secure,
|
|
747
|
+
defaultValue: "",
|
|
748
|
+
coalesceSecureWrites: true,
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
sessionToken.set("token-v1");
|
|
752
|
+
sessionToken.set("token-v2");
|
|
753
|
+
|
|
754
|
+
// force pending secure writes to native persistence
|
|
755
|
+
storage.flushSecureWrites();
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Storage Snapshots and Cleanup
|
|
759
|
+
|
|
760
|
+
```ts
|
|
761
|
+
import { storage, StorageScope } from "react-native-nitro-storage";
|
|
762
|
+
|
|
763
|
+
const diskKeys = storage.getAllKeys(StorageScope.Disk);
|
|
764
|
+
const diskValues = storage.getAll(StorageScope.Disk);
|
|
765
|
+
const secureCount = storage.size(StorageScope.Secure);
|
|
766
|
+
|
|
767
|
+
if (storage.has("legacy-flag", StorageScope.Disk)) {
|
|
768
|
+
storage.clearNamespace("legacy", StorageScope.Disk);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
storage.clearBiometric();
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### Prefix Queries and Namespace Inspection
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
import { storage, StorageScope } from "react-native-nitro-storage";
|
|
778
|
+
|
|
779
|
+
const userKeys = storage.getKeysByPrefix("user-42:", StorageScope.Disk);
|
|
780
|
+
const userRawEntries = storage.getByPrefix("user-42:", StorageScope.Disk);
|
|
781
|
+
|
|
782
|
+
console.log(userKeys);
|
|
783
|
+
console.log(userRawEntries);
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### Optimistic Versioned Writes
|
|
787
|
+
|
|
788
|
+
```ts
|
|
789
|
+
import { createStorageItem, StorageScope } from "react-native-nitro-storage";
|
|
790
|
+
|
|
791
|
+
const profileItem = createStorageItem({
|
|
792
|
+
key: "profile",
|
|
793
|
+
scope: StorageScope.Disk,
|
|
794
|
+
defaultValue: { name: "Guest" },
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
const snapshot = profileItem.getWithVersion();
|
|
798
|
+
const didWrite = profileItem.setIfVersion(snapshot.version, {
|
|
799
|
+
...snapshot.value,
|
|
800
|
+
name: "Ada",
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
if (!didWrite) {
|
|
804
|
+
// value changed since snapshot; reload and retry
|
|
805
|
+
}
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Storage Metrics Instrumentation
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
import { storage } from "react-native-nitro-storage";
|
|
812
|
+
|
|
813
|
+
storage.setMetricsObserver((event) => {
|
|
814
|
+
console.log(
|
|
815
|
+
`[nitro-storage] ${event.operation} scope=${event.scope} duration=${event.durationMs}ms keys=${event.keysCount}`,
|
|
816
|
+
);
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const metrics = storage.getMetricsSnapshot();
|
|
820
|
+
console.log(metrics["item:get"]?.avgDurationMs);
|
|
821
|
+
|
|
822
|
+
storage.resetMetrics();
|
|
823
|
+
storage.setMetricsObserver(undefined);
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Low-level Subscription (outside React)
|
|
827
|
+
|
|
828
|
+
```ts
|
|
829
|
+
import { createStorageItem, StorageScope } from "react-native-nitro-storage";
|
|
830
|
+
|
|
831
|
+
const notificationsItem = createStorageItem<boolean>({
|
|
832
|
+
key: "notifications-enabled",
|
|
833
|
+
scope: StorageScope.Disk,
|
|
834
|
+
defaultValue: true,
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
const unsubscribe = notificationsItem.subscribe(() => {
|
|
838
|
+
console.log("notifications changed:", notificationsItem.get());
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
notificationsItem.set(false);
|
|
842
|
+
unsubscribe();
|
|
843
|
+
```
|
|
844
|
+
|
|
558
845
|
### Migrating From MMKV
|
|
559
846
|
|
|
560
847
|
```ts
|
|
@@ -587,6 +874,12 @@ import type {
|
|
|
587
874
|
MigrationContext,
|
|
588
875
|
Migration,
|
|
589
876
|
TransactionContext,
|
|
877
|
+
StorageVersion,
|
|
878
|
+
VersionedValue,
|
|
879
|
+
StorageMetricsEvent,
|
|
880
|
+
StorageMetricsObserver,
|
|
881
|
+
StorageMetricSummary,
|
|
882
|
+
WebSecureStorageBackend,
|
|
590
883
|
MMKVLike,
|
|
591
884
|
SecureAuthStorageConfig,
|
|
592
885
|
} from "react-native-nitro-storage";
|
|
@@ -600,7 +893,11 @@ From repository root:
|
|
|
600
893
|
|
|
601
894
|
```bash
|
|
602
895
|
bun run test -- --filter=react-native-nitro-storage
|
|
896
|
+
bun run lint -- --filter=react-native-nitro-storage
|
|
897
|
+
bun run format:check -- --filter=react-native-nitro-storage
|
|
603
898
|
bun run typecheck -- --filter=react-native-nitro-storage
|
|
899
|
+
bun run test:types -- --filter=react-native-nitro-storage
|
|
900
|
+
bun run test:cpp -- --filter=react-native-nitro-storage
|
|
604
901
|
bun run build -- --filter=react-native-nitro-storage
|
|
605
902
|
```
|
|
606
903
|
|
|
@@ -612,7 +909,10 @@ bun run test:coverage # run tests with coverage
|
|
|
612
909
|
bun run lint # eslint (expo-magic rules)
|
|
613
910
|
bun run format:check # prettier check
|
|
614
911
|
bun run typecheck # tsc --noEmit
|
|
615
|
-
bun run
|
|
912
|
+
bun run test:types # public type-level API tests
|
|
913
|
+
bun run test:cpp # C++ binding/core tests
|
|
914
|
+
bun run check:pack # npm pack content guard
|
|
915
|
+
bun run build # bob build
|
|
616
916
|
bun run benchmark # performance benchmarks
|
|
617
917
|
```
|
|
618
918
|
|
package/android/CMakeLists.txt
CHANGED
|
@@ -11,6 +11,8 @@ file(GLOB SOURCES
|
|
|
11
11
|
"../cpp/core/*.cpp"
|
|
12
12
|
"./src/main/cpp/*.cpp"
|
|
13
13
|
)
|
|
14
|
+
# Unit/integration C++ tests define `main()` and must never be linked into the Android shared library.
|
|
15
|
+
list(FILTER SOURCES EXCLUDE REGEX ".*/[^/]*Test\\.cpp$")
|
|
14
16
|
|
|
15
17
|
# 2. Create the library target
|
|
16
18
|
add_library(
|
|
@@ -90,6 +90,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
|
|
|
90
90
|
return fromJavaStringArray(keys);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixDisk(const std::string& prefix) {
|
|
94
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
95
|
+
local_ref<JavaStringArray>(std::string)
|
|
96
|
+
>("getKeysByPrefixDisk");
|
|
97
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
98
|
+
return fromJavaStringArray(keys);
|
|
99
|
+
}
|
|
100
|
+
|
|
93
101
|
size_t AndroidStorageAdapterCpp::sizeDisk() {
|
|
94
102
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeDisk");
|
|
95
103
|
return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
|
|
@@ -166,6 +174,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
|
|
|
166
174
|
return fromJavaStringArray(keys);
|
|
167
175
|
}
|
|
168
176
|
|
|
177
|
+
std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixSecure(const std::string& prefix) {
|
|
178
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
|
|
179
|
+
local_ref<JavaStringArray>(std::string)
|
|
180
|
+
>("getKeysByPrefixSecure");
|
|
181
|
+
auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
|
|
182
|
+
return fromJavaStringArray(keys);
|
|
183
|
+
}
|
|
184
|
+
|
|
169
185
|
size_t AndroidStorageAdapterCpp::sizeSecure() {
|
|
170
186
|
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeSecure");
|
|
171
187
|
return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
|
|
@@ -213,13 +229,21 @@ void AndroidStorageAdapterCpp::clearSecure() {
|
|
|
213
229
|
// --- Config (no-ops on Android; access control / groups are iOS-specific) ---
|
|
214
230
|
|
|
215
231
|
void AndroidStorageAdapterCpp::setSecureAccessControl(int /*level*/) {}
|
|
232
|
+
void AndroidStorageAdapterCpp::setSecureWritesAsync(bool enabled) {
|
|
233
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(jboolean)>("setSecureWritesAsync");
|
|
234
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), enabled);
|
|
235
|
+
}
|
|
216
236
|
void AndroidStorageAdapterCpp::setKeychainAccessGroup(const std::string& /*group*/) {}
|
|
217
237
|
|
|
218
238
|
// --- Biometric ---
|
|
219
239
|
|
|
220
240
|
void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const std::string& value) {
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
setSecureBiometricWithLevel(key, value, 2);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
void AndroidStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
|
|
245
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string, jint)>("setSecureBiometricWithLevel");
|
|
246
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), key, value, level);
|
|
223
247
|
}
|
|
224
248
|
|
|
225
249
|
std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {
|
|
@@ -30,6 +30,7 @@ public:
|
|
|
30
30
|
void deleteDisk(const std::string& key) override;
|
|
31
31
|
bool hasDisk(const std::string& key) override;
|
|
32
32
|
std::vector<std::string> getAllKeysDisk() override;
|
|
33
|
+
std::vector<std::string> getKeysByPrefixDisk(const std::string& prefix) override;
|
|
33
34
|
size_t sizeDisk() override;
|
|
34
35
|
void setDiskBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) override;
|
|
35
36
|
std::vector<std::optional<std::string>> getDiskBatch(const std::vector<std::string>& keys) override;
|
|
@@ -40,6 +41,7 @@ public:
|
|
|
40
41
|
void deleteSecure(const std::string& key) override;
|
|
41
42
|
bool hasSecure(const std::string& key) override;
|
|
42
43
|
std::vector<std::string> getAllKeysSecure() override;
|
|
44
|
+
std::vector<std::string> getKeysByPrefixSecure(const std::string& prefix) override;
|
|
43
45
|
size_t sizeSecure() override;
|
|
44
46
|
void setSecureBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) override;
|
|
45
47
|
std::vector<std::optional<std::string>> getSecureBatch(const std::vector<std::string>& keys) override;
|
|
@@ -49,9 +51,11 @@ public:
|
|
|
49
51
|
void clearSecure() override;
|
|
50
52
|
|
|
51
53
|
void setSecureAccessControl(int level) override;
|
|
54
|
+
void setSecureWritesAsync(bool enabled) override;
|
|
52
55
|
void setKeychainAccessGroup(const std::string& group) override;
|
|
53
56
|
|
|
54
57
|
void setSecureBiometric(const std::string& key, const std::string& value) override;
|
|
58
|
+
void setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) override;
|
|
55
59
|
std::optional<std::string> getSecureBiometric(const std::string& key) override;
|
|
56
60
|
void deleteSecureBiometric(const std::string& key) override;
|
|
57
61
|
bool hasSecureBiometric(const std::string& key) override;
|