Skip to content

Commit 5d689b2

Browse files
Merge pull request #10 from JoaoPauloCMarra/v0.4.0
Finalize v0.4.0 release updates
2 parents 428e3c5 + 4c1b1ea commit 5d689b2

23 files changed

Lines changed: 2732 additions & 666 deletions

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ All notable changes to this project are documented in this file.
44

55
The format follows Keep a Changelog and the project adheres to SemVer.
66

7+
## 0.4.0 - 2026-02-25
8+
9+
### Added
10+
11+
- Add prefix query APIs: `storage.getKeysByPrefix(prefix, scope)` and `storage.getByPrefix(prefix, scope)`.
12+
- Add optimistic concurrency APIs on items: `item.getWithVersion()` and `item.setIfVersion(version, value)`.
13+
- Add storage metrics APIs: `storage.setMetricsObserver`, `storage.getMetricsSnapshot`, and `storage.resetMetrics`.
14+
- Add `biometricLevel` item/auth config and native bridge support for `setSecureBiometricWithLevel`.
15+
- Add configurable web Secure backend hooks: `setWebSecureStorageBackend` and `getWebSecureStorageBackend`.
16+
- Add native prefix key retrieval plumbing (`getKeysByPrefix`) across Nitro spec, C++ core/bindings, Android, and iOS.
17+
- Add regression coverage for prefix APIs, versioned APIs, metrics APIs, secure coalescing with access control, cross-tab web updates, and transaction rollback batch behavior.
18+
19+
### Changed
20+
21+
- Optimize non-memory transaction rollback paths to use batch native/web writes and removals.
22+
- Improve batch read semantics by using per-item cache hits and returning each item's default when raw batch data is missing.
23+
- Improve native/web secure write coalescing by preserving optional access control without violating strict optional typing.
24+
- Keep iOS secure keychain cache/index behavior aligned with new prefix query and biometric-level paths.
25+
- Expand README/API docs to cover the new public API surface with concrete TypeScript use-case snippets.
26+
727
## 0.3.2 - 2026-02-22
828

929
### Added

README.md

Lines changed: 141 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ Synchronous Memory, Disk, and Secure storage in one unified API — powered by [
1414
- **Biometric storage** — hardware-backed biometric protection on iOS & Android
1515
- **Auth storage factory**`createSecureAuthStorage` for multi-token auth flows
1616
- **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
1721
- **Transactions** — grouped writes with automatic rollback on error
1822
- **Migrations** — versioned data migrations with `registerMigration` / `migrateToLatest`
1923
- **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
@@ -31,6 +35,10 @@ Every feature in this package is documented with at least one runnable example i
3135
- Validation + recovery — see Feature Flags with Validation
3236
- Biometric + access control — see Biometric-protected Secrets
3337
- 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
3442
- Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
3543
- Transactions — see Transactions and Atomic Balance Transfer
3644
- Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
@@ -185,21 +193,24 @@ function createStorageItem<T = undefined>(
185193
| `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
186194
| `namespace` | `string` || Prefix key as `namespace:key` for isolation |
187195
| `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
196+
| `biometricLevel` | `BiometricLevel` | `None` | Biometric policy (`BiometryOrPasscode` / `BiometryOnly`) |
188197
| `accessControl` | `AccessControl` || Keychain access control level (native only) |
189198

190199
**Returned `StorageItem<T>`:**
191200

192-
| Method / Property | Type | Description |
193-
| ----------------- | ---------------------------------------- | -------------------------------------------------- |
194-
| `get()` | `() => T` | Read current value (synchronous) |
195-
| `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
196-
| `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
197-
| `has()` | `() => boolean` | Check if a value exists in storage |
198-
| `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
199-
| `serialize` | `(v: T) => string` | The item's serializer |
200-
| `deserialize` | `(v: string) => T` | The item's deserializer |
201-
| `scope` | `StorageScope` | The item's scope |
202-
| `key` | `string` | The resolved key (includes namespace prefix) |
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) |
203214

204215
**Non-React subscription example:**
205216

@@ -249,30 +260,41 @@ setToken("new-token");
249260
import { storage, StorageScope } from "react-native-nitro-storage";
250261
```
251262

252-
| Method | Description |
253-
| --------------------------------------- | ---------------------------------------------------------------------------- |
254-
| `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
255-
| `storage.clearAll()` | Clear Memory + Disk + Secure |
256-
| `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
257-
| `storage.clearBiometric()` | Remove all biometric-prefixed keys |
258-
| `storage.has(key, scope)` | Check if a key exists |
259-
| `storage.getAllKeys(scope)` | Get all key names |
260-
| `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
261-
| `storage.size(scope)` | Number of stored keys |
262-
| `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
263-
| `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
264-
| `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
265-
| `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
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 |
266282

267283
> `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
268284
269285
#### Global utility examples
270286

271287
```ts
272-
import { AccessControl, storage, StorageScope } from "react-native-nitro-storage";
288+
import {
289+
AccessControl,
290+
storage,
291+
StorageScope,
292+
} from "react-native-nitro-storage";
273293

274294
storage.has("session", StorageScope.Disk);
275295
storage.getAllKeys(StorageScope.Disk);
296+
storage.getKeysByPrefix("user-42:", StorageScope.Disk);
297+
storage.getByPrefix("user-42:", StorageScope.Disk);
276298
storage.getAll(StorageScope.Disk);
277299
storage.size(StorageScope.Disk);
278300

@@ -303,6 +325,28 @@ storage.setSecureWritesAsync(true);
303325
storage.flushSecureWrites(); // deterministic durability boundary
304326
```
305327

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+
306350
---
307351

308352
### `createSecureAuthStorage<K>(config, options?)`
@@ -318,7 +362,7 @@ function createSecureAuthStorage<K extends string>(
318362

319363
- Default namespace: `"auth"`
320364
- Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
321-
- Supports per-key TTL, biometric, and access control
365+
- Supports per-key TTL, biometric level policy, and access control
322366

323367
---
324368

@@ -550,13 +594,18 @@ const flagsItem = createStorageItem<FeatureFlags>({
550594
### Biometric-protected Secrets
551595

552596
```ts
553-
import { AccessControl, createStorageItem, StorageScope } from "react-native-nitro-storage";
597+
import {
598+
AccessControl,
599+
BiometricLevel,
600+
createStorageItem,
601+
StorageScope,
602+
} from "react-native-nitro-storage";
554603

555604
const paymentPin = createStorageItem<string>({
556605
key: "payment-pin",
557606
scope: StorageScope.Secure,
558607
defaultValue: "",
559-
biometric: true,
608+
biometricLevel: BiometricLevel.BiometryOnly,
560609
accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
561610
});
562611

@@ -686,7 +735,11 @@ const compactItem = createStorageItem<{ id: number; active: boolean }>({
686735
### Coalesced Secure Writes with Deterministic Flush
687736

688737
```ts
689-
import { createStorageItem, storage, StorageScope } from "react-native-nitro-storage";
738+
import {
739+
createStorageItem,
740+
storage,
741+
StorageScope,
742+
} from "react-native-nitro-storage";
690743

691744
const sessionToken = createStorageItem<string>({
692745
key: "session-token",
@@ -718,6 +771,58 @@ if (storage.has("legacy-flag", StorageScope.Disk)) {
718771
storage.clearBiometric();
719772
```
720773

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+
721826
### Low-level Subscription (outside React)
722827

723828
```ts
@@ -769,6 +874,12 @@ import type {
769874
MigrationContext,
770875
Migration,
771876
TransactionContext,
877+
StorageVersion,
878+
VersionedValue,
879+
StorageMetricsEvent,
880+
StorageMetricsObserver,
881+
StorageMetricSummary,
882+
WebSecureStorageBackend,
772883
MMKVLike,
773884
SecureAuthStorageConfig,
774885
} from "react-native-nitro-storage";

packages/react-native-nitro-storage/android/src/main/cpp/AndroidStorageAdapterCpp.cpp

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
9090
return fromJavaStringArray(keys);
9191
}
9292

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+
93101
size_t AndroidStorageAdapterCpp::sizeDisk() {
94102
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeDisk");
95103
return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
@@ -166,6 +174,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
166174
return fromJavaStringArray(keys);
167175
}
168176

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+
169185
size_t AndroidStorageAdapterCpp::sizeSecure() {
170186
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeSecure");
171187
return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
@@ -222,8 +238,12 @@ void AndroidStorageAdapterCpp::setKeychainAccessGroup(const std::string& /*group
222238
// --- Biometric ---
223239

224240
void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const std::string& value) {
225-
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string)>("setSecureBiometric");
226-
method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
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);
227247
}
228248

229249
std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {

0 commit comments

Comments
 (0)