react-native-nitro-storage 0.8.0 → 0.10.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/CHANGELOG.md +107 -25
- package/README.md +140 -27
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
- package/cpp/bindings/HybridStorage.cpp +109 -293
- package/cpp/bindings/HybridStorage.hpp +17 -10
- package/docs/api-reference.md +75 -19
- package/docs/benchmarks.md +6 -1
- package/docs/keychain-lifecycle-testing.md +36 -0
- package/docs/recipes.md +1 -2
- package/docs/secure-storage.md +45 -9
- package/ios/IOSStorageAdapterCpp.mm +391 -175
- package/lib/commonjs/capabilities.js +3 -1
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/core/durability.js +110 -43
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +15 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +219 -97
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +3 -11
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +62 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +859 -145
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +5 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +13 -0
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/capabilities.js +2 -1
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/core/durability.js +110 -43
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +12 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +215 -99
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +2 -9
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +60 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +860 -146
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +4 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +8 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/capabilities.d.ts +2 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +7 -2
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +3 -29
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +0 -2
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +19 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +12 -3
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +2 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
- package/package.json +3 -3
- package/react-native-nitro-storage.podspec +3 -1
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +139 -39
- package/src/index.ts +20 -10
- package/src/index.web.ts +328 -166
- package/src/internal.ts +0 -14
- package/src/shared.ts +85 -0
- package/src/storage-core.ts +1165 -199
- package/src/storage-runtime.ts +6 -0
- package/src/testing.ts +8 -1
package/docs/api-reference.md
CHANGED
|
@@ -14,24 +14,28 @@ const item = createStorageItem<T>({
|
|
|
14
14
|
|
|
15
15
|
`StorageItemConfig<T>`:
|
|
16
16
|
|
|
17
|
-
| Field
|
|
18
|
-
|
|
|
19
|
-
| `key`
|
|
20
|
-
| `scope`
|
|
21
|
-
| `defaultValue`
|
|
22
|
-
| `serialize`
|
|
23
|
-
| `deserialize`
|
|
24
|
-
| `validate`
|
|
25
|
-
| `onValidationError`
|
|
26
|
-
| `expiration`
|
|
27
|
-
| `onExpired`
|
|
28
|
-
| `readCache`
|
|
29
|
-
| `coalesceDiskWrites`
|
|
30
|
-
| `coalesceSecureWrites`
|
|
31
|
-
| `namespace`
|
|
32
|
-
| `biometric`
|
|
33
|
-
| `biometricLevel`
|
|
34
|
-
| `accessControl`
|
|
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` | Reuse raw cache entries for reads, including cached missing values. |
|
|
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
|
+
| `group` | `string` | Register the item for group cleanup and inspection. |
|
|
36
|
+
| `renameFrom` | `string \| readonly string[]` | Copy a legacy key on first read, then remove it. |
|
|
37
|
+
| `fallbackToCacheOnReadError` | `boolean` | Return the last cached value when a backend read fails. |
|
|
38
|
+
| `onReadError` | `(error: unknown) => void` | Observe a backend read failure before fallback or rethrow. |
|
|
35
39
|
|
|
36
40
|
`StorageItem<T>`:
|
|
37
41
|
|
|
@@ -41,6 +45,9 @@ const item = createStorageItem<T>({
|
|
|
41
45
|
| `getWithVersion()` | Return `{ value, version }` for optimistic writes. |
|
|
42
46
|
| `set(value)` | Store a value. Accepts direct values or updater functions. |
|
|
43
47
|
| `setIfVersion(version, value)` | Store only when the current version still matches. |
|
|
48
|
+
| `merge(partial)` | Shallow-merge an object value. |
|
|
49
|
+
| `reset()` | Delete the key so the next read returns the default. |
|
|
50
|
+
| `setOrDelete(value)` | Set a value or delete for `null`/`undefined`. |
|
|
44
51
|
| `delete()` | Remove the key. |
|
|
45
52
|
| `has()` | Check whether the key exists. |
|
|
46
53
|
| `subscribe(callback)` | Subscribe to item changes. Returns an unsubscribe function. |
|
|
@@ -58,6 +65,23 @@ const unsubscribe = profileItem.subscribeSelector(
|
|
|
58
65
|
);
|
|
59
66
|
```
|
|
60
67
|
|
|
68
|
+
## createSetItem
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const flags = createSetItem<"beta" | "compact">({
|
|
72
|
+
key: "flags",
|
|
73
|
+
scope: StorageScope.Memory,
|
|
74
|
+
defaultValue: ["compact"],
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
flags.add("beta");
|
|
78
|
+
flags.has("compact");
|
|
79
|
+
flags.getTyped();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`get()` retains the compatibility shape `Record<string, true>`. Use
|
|
83
|
+
`getTyped()` when a precise member union is useful.
|
|
84
|
+
|
|
61
85
|
## React Hooks
|
|
62
86
|
|
|
63
87
|
```ts
|
|
@@ -74,9 +98,14 @@ See [react-hooks.md](react-hooks.md).
|
|
|
74
98
|
|
|
75
99
|
| Method | Purpose |
|
|
76
100
|
| ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
|
77
|
-
| `clear(scope)`
|
|
101
|
+
| `clear(scope, options?)` | Clear one scope, optionally preserving selected keys. |
|
|
78
102
|
| `clearAll()` | Clear Memory, Disk, and Secure scopes. |
|
|
79
103
|
| `clearNamespace(namespace, scope)` | Remove keys under `namespace:`. |
|
|
104
|
+
| `clearGroup(group)` | Remove registered items in a group across their scopes. |
|
|
105
|
+
| `getGroupItems(group)` | List registered items in a group. |
|
|
106
|
+
| `subscribeExpired(scope, listener)` | Receive item events caused by TTL expiry. |
|
|
107
|
+
| `findDuplicateKeys()` | Find duplicate registered `(scope, key)` definitions. |
|
|
108
|
+
| `getRegisteredKeys()` | List registered `(scope, key)` definitions. |
|
|
80
109
|
| `subscribe(scope, listener)` | Subscribe to raw scope-level change events. |
|
|
81
110
|
| `subscribeKey(scope, key, listener)` | Subscribe to raw events for one key. |
|
|
82
111
|
| `subscribePrefix(scope, prefix, listener)` | Subscribe to raw events for matching key prefixes. |
|
|
@@ -97,6 +126,7 @@ See [react-hooks.md](react-hooks.md).
|
|
|
97
126
|
| `setKeychainAccessGroup(group)` | Configure iOS Keychain access group. |
|
|
98
127
|
| `setMetricsObserver(observer)` | Receive operation timing events. |
|
|
99
128
|
| `getMetricsSnapshot()` | Read aggregated metrics. |
|
|
129
|
+
| `getScopedMetricsSnapshot()` | Read metrics grouped by storage scope. |
|
|
100
130
|
| `resetMetrics()` | Clear metrics counters. |
|
|
101
131
|
| `getCapabilities()` | Read runtime storage capabilities. |
|
|
102
132
|
| `getSecurityCapabilities()` | Read secure backend capability metadata. |
|
|
@@ -170,6 +200,11 @@ setBatch(
|
|
|
170
200
|
removeBatch([themeItem, localeItem], StorageScope.Disk);
|
|
171
201
|
```
|
|
172
202
|
|
|
203
|
+
`getBatch()` reuses enabled raw cache entries, including cached missing values,
|
|
204
|
+
and returns each item's default for a missing raw value without issuing a
|
|
205
|
+
per-item fallback read. Items that need validation, expiration, or migration
|
|
206
|
+
use their item-level read path to preserve those rules.
|
|
207
|
+
|
|
173
208
|
See [batch-transactions-migrations.md](batch-transactions-migrations.md).
|
|
174
209
|
|
|
175
210
|
## Transactions
|
|
@@ -211,6 +246,23 @@ auth.accessToken.set("token");
|
|
|
211
246
|
|
|
212
247
|
The returned object is a typed record of secure string `StorageItem`s.
|
|
213
248
|
|
|
249
|
+
## Storage Error Classification
|
|
250
|
+
|
|
251
|
+
```ts
|
|
252
|
+
if (isStorageError(error, "keychain_locked")) {
|
|
253
|
+
scheduleRetryAfterUnlock();
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`getStorageErrorCode(error)` returns the stable `StorageErrorCode` embedded by
|
|
258
|
+
the native or web adapter. `isStorageError(error, code)` matches one exact code
|
|
259
|
+
without parsing platform message text. See [secure-storage.md](secure-storage.md)
|
|
260
|
+
for recovery semantics.
|
|
261
|
+
|
|
262
|
+
`isKeychainLockedError(error)` is deprecated. It remains available for
|
|
263
|
+
compatibility and returns `true` for `keychain_locked`,
|
|
264
|
+
`authentication_required`, and `key_invalidated`.
|
|
265
|
+
|
|
214
266
|
## Web Backend APIs
|
|
215
267
|
|
|
216
268
|
```ts
|
|
@@ -221,6 +273,10 @@ getWebSecureStorageBackend();
|
|
|
221
273
|
await flushWebStorageBackends();
|
|
222
274
|
```
|
|
223
275
|
|
|
276
|
+
The web entry also exports `describeWebBackendCapabilities(backend)` and
|
|
277
|
+
`isIndexedDBWebBackend(backend)`. The native entry keeps the web backend
|
|
278
|
+
setters, getters, and flush function as typed no-ops for shared code.
|
|
279
|
+
|
|
224
280
|
See [web-backends.md](web-backends.md).
|
|
225
281
|
|
|
226
282
|
## Enums
|
package/docs/benchmarks.md
CHANGED
|
@@ -11,13 +11,18 @@ bun run benchmark
|
|
|
11
11
|
|
|
12
12
|
## Scope: Web Only
|
|
13
13
|
|
|
14
|
-
`benchmark` loads `lib/commonjs/index.web.js` and measures
|
|
14
|
+
`benchmark` loads only this package's `lib/commonjs/index.web.js` entry and measures it against a private localStorage implementation created for that process. The `web:disk-scope:` and `web:secure-scope:` labels describe web scopes, not native Disk or Secure storage.
|
|
15
15
|
|
|
16
16
|
Native Disk/Secure baselines require a device or simulator run and are not part of this gate. Do not compare these numbers against native storage.
|
|
17
17
|
|
|
18
18
|
## Interpreting Results
|
|
19
19
|
|
|
20
|
+
- Each run reports the package name/version, runtime, architecture, warmups,
|
|
21
|
+
sample count, median, and p95. It does not use another package's artifact or
|
|
22
|
+
ambient browser storage.
|
|
20
23
|
- Compare results on the same machine and Node/Bun version.
|
|
24
|
+
- The benchmark uses seven measured samples after two warmups and reports the
|
|
25
|
+
median for throughput. It does not select the best sample.
|
|
21
26
|
- Treat large deltas as a prompt to inspect recent storage-runtime, serialization, cache, or event changes.
|
|
22
27
|
- Do not compare web backend numbers against native secure storage numbers; they measure different systems.
|
|
23
28
|
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Physical-Device Keychain Lifecycle Test
|
|
2
|
+
|
|
3
|
+
Use this protocol for iOS Keychain behavior and stable error classification.
|
|
4
|
+
A simulator cannot prove protected-data behavior.
|
|
5
|
+
|
|
6
|
+
## Preconditions
|
|
7
|
+
|
|
8
|
+
- Use a physical iPhone or iPad with a passcode enabled.
|
|
9
|
+
- Build the example app from the package checkout under test.
|
|
10
|
+
- Do not enter a real token or credential. The probe stores a fixed non-secret
|
|
11
|
+
sentinel under `WhenUnlockedThisDeviceOnly` access control.
|
|
12
|
+
|
|
13
|
+
## Lock And Resume
|
|
14
|
+
|
|
15
|
+
1. Open the example app and find **Keychain Lifecycle Probe**.
|
|
16
|
+
2. Press **Seed** and confirm the status changes to `seeded`.
|
|
17
|
+
3. Press **Arm**.
|
|
18
|
+
4. Lock the device, wait for the screen to turn off, then unlock it and return
|
|
19
|
+
to the example app.
|
|
20
|
+
5. Record both probe rows without logging or inspecting the stored value.
|
|
21
|
+
6. Press **Wipe** after the run.
|
|
22
|
+
|
|
23
|
+
The strongest passing result is:
|
|
24
|
+
|
|
25
|
+
- Lock transition: `keychain_locked`.
|
|
26
|
+
- Resume transition: `readable`.
|
|
27
|
+
|
|
28
|
+
If the lock transition says `readable`, the app sampled before iOS made
|
|
29
|
+
protected data unavailable. Repeat the run; treat repeated readable results as
|
|
30
|
+
inconclusive, not as proof that locked reads work. Any resume result other than
|
|
31
|
+
`readable` fails the recovery contract.
|
|
32
|
+
|
|
33
|
+
## Release Evidence
|
|
34
|
+
|
|
35
|
+
Record the physical model, iOS version, package version, build configuration,
|
|
36
|
+
and the two displayed result codes. Do not capture or publish secure values.
|
package/docs/recipes.md
CHANGED
|
@@ -283,10 +283,9 @@ Use `subscribePrefix()` or `subscribeNamespace()` for targeted integrations. Use
|
|
|
283
283
|
## Capability Checks
|
|
284
284
|
|
|
285
285
|
```ts
|
|
286
|
-
const capabilities = storage.getCapabilities();
|
|
287
286
|
const security = storage.getSecurityCapabilities();
|
|
288
287
|
|
|
289
|
-
if (security.secureStorage !== "available") {
|
|
288
|
+
if (security.secureStorage.encrypted !== "available") {
|
|
290
289
|
console.warn("Secure storage is not available on this runtime");
|
|
291
290
|
}
|
|
292
291
|
```
|
package/docs/secure-storage.md
CHANGED
|
@@ -98,7 +98,7 @@ import { storage } from "react-native-nitro-storage";
|
|
|
98
98
|
|
|
99
99
|
const capabilities = storage.getSecurityCapabilities();
|
|
100
100
|
|
|
101
|
-
if (capabilities.secureStorage === "available") {
|
|
101
|
+
if (capabilities.secureStorage.encrypted === "available") {
|
|
102
102
|
// Secure scope is backed by the configured native or web secure backend.
|
|
103
103
|
}
|
|
104
104
|
```
|
|
@@ -148,35 +148,70 @@ Android secure storage uses encrypted SharedPreferences. Restored encrypted pref
|
|
|
148
148
|
|
|
149
149
|
If you disable `configureAndroidBackup` or maintain custom Android backup XML, add equivalent exclusions for both cloud backup and device transfer.
|
|
150
150
|
|
|
151
|
-
##
|
|
151
|
+
## Secure Storage Error Recovery
|
|
152
152
|
|
|
153
153
|
```ts
|
|
154
|
-
import {
|
|
154
|
+
import { isStorageError } from "react-native-nitro-storage";
|
|
155
155
|
|
|
156
156
|
try {
|
|
157
157
|
refreshTokenItem.get();
|
|
158
158
|
} catch (error) {
|
|
159
|
-
if (
|
|
159
|
+
if (isStorageError(error, "keychain_locked")) {
|
|
160
160
|
// Defer token refresh until the device is unlocked.
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
Recovery depends on the exact stable code:
|
|
166
|
+
|
|
167
|
+
| Code | Meaning | Consumer action |
|
|
168
|
+
| ------------------------- | ----------------------------------------- | --------------------------------------------------- |
|
|
169
|
+
| `keychain_locked` | Protected data is temporarily unavailable | Retry after the device unlocks and the app resumes. |
|
|
170
|
+
| `authentication_required` | The secure item requires user interaction | Start the application's authentication flow. |
|
|
171
|
+
| `key_invalidated` | The platform key can no longer decrypt it | Remove and recreate the affected credential safely. |
|
|
172
|
+
|
|
173
|
+
`isKeychainLockedError()` remains available for compatibility but is
|
|
174
|
+
deprecated. It groups all three codes and must not be used to select retry
|
|
175
|
+
behavior. The package does not block the synchronous JSI call, sleep, or retry
|
|
176
|
+
internally; the application owns lifecycle scheduling and cancellation.
|
|
177
|
+
|
|
178
|
+
Do not enable `fallbackToCacheOnReadError` for access or refresh tokens unless
|
|
179
|
+
the application explicitly accepts stale or revoked credentials. A cached
|
|
180
|
+
value can hide the distinction between temporary unavailability and credential
|
|
181
|
+
recovery.
|
|
166
182
|
|
|
167
183
|
## Android Secure Write Mode
|
|
168
184
|
|
|
169
|
-
Android secure writes default to synchronous
|
|
185
|
+
Android secure writes default to synchronous `SharedPreferences.commit()` for
|
|
186
|
+
the established durability contract. If asynchronous
|
|
187
|
+
`SharedPreferences.apply()` is acceptable, opt into async mode explicitly:
|
|
170
188
|
|
|
171
189
|
```ts
|
|
172
190
|
import { storage } from "react-native-nitro-storage";
|
|
173
191
|
|
|
174
192
|
storage.setSecureWritesAsync(true);
|
|
175
193
|
refreshTokenItem.set("opaque-refresh-token");
|
|
176
|
-
storage.flushSecureWrites();
|
|
177
194
|
```
|
|
178
195
|
|
|
179
|
-
|
|
196
|
+
Coalesced secure item writes remain in a last-write-wins queue until the next
|
|
197
|
+
microtask or an explicit `flushSecureWrites()`. A failed flush throws and keeps
|
|
198
|
+
failed and unattempted writes queued for a later retry. Call
|
|
199
|
+
`flushSecureWrites()` before assertions, namespace clears, or any boundary that
|
|
200
|
+
requires deterministic persistence. `storage.clearBiometric()` is also a
|
|
201
|
+
durability barrier: it flushes pending Secure writes before clearing biometric
|
|
202
|
+
entries, and surfaces native clear failures.
|
|
203
|
+
|
|
204
|
+
## iOS Legacy Disk Migration
|
|
205
|
+
|
|
206
|
+
Older releases tracked observed Disk keys in `standardUserDefaults`. On iOS,
|
|
207
|
+
adapter initialization copies a valid registry into the Nitro suite domain and
|
|
208
|
+
removes each legacy source only after a target readback and synchronization
|
|
209
|
+
check. `NSUserDefaults` is not transactional, so the migration stops on any
|
|
210
|
+
failed synchronization or readback without deleting the source or registry.
|
|
211
|
+
Malformed registries, same-domain or fallback stores, conflicting target
|
|
212
|
+
values, and failed persistence therefore remain available for recovery. The
|
|
213
|
+
migration is retryable on a later initialization; do not delete the registry
|
|
214
|
+
manually while an upgrade is in progress.
|
|
180
215
|
|
|
181
216
|
## Web Secure Backend
|
|
182
217
|
|
|
@@ -202,4 +237,5 @@ bun run test:cpp -- --filter=react-native-nitro-storage
|
|
|
202
237
|
(cd packages/react-native-nitro-storage && bun run check:pack)
|
|
203
238
|
```
|
|
204
239
|
|
|
205
|
-
Also run
|
|
240
|
+
Also run the [physical-device Keychain lifecycle protocol](keychain-lifecycle-testing.md)
|
|
241
|
+
when changing biometric, Keychain, or error-classification behavior.
|