react-native-nitro-storage 0.9.0 → 0.10.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.
Files changed (77) hide show
  1. package/CHANGELOG.md +101 -29
  2. package/README.md +83 -26
  3. package/android/build.gradle +3 -1
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
  6. package/cpp/bindings/HybridStorage.cpp +106 -283
  7. package/cpp/bindings/HybridStorage.hpp +16 -9
  8. package/docs/api-reference.md +75 -19
  9. package/docs/benchmarks.md +6 -1
  10. package/docs/keychain-lifecycle-testing.md +36 -0
  11. package/docs/recipes.md +1 -2
  12. package/docs/secure-storage.md +45 -9
  13. package/ios/IOSStorageAdapterCpp.mm +391 -175
  14. package/lib/commonjs/capabilities.js +3 -1
  15. package/lib/commonjs/capabilities.js.map +1 -1
  16. package/lib/commonjs/core/durability.js +111 -45
  17. package/lib/commonjs/core/durability.js.map +1 -1
  18. package/lib/commonjs/index.js +15 -5
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.web.js +219 -97
  21. package/lib/commonjs/index.web.js.map +1 -1
  22. package/lib/commonjs/internal.js +3 -11
  23. package/lib/commonjs/internal.js.map +1 -1
  24. package/lib/commonjs/shared.js +62 -2
  25. package/lib/commonjs/shared.js.map +1 -1
  26. package/lib/commonjs/storage-core.js +883 -163
  27. package/lib/commonjs/storage-core.js.map +1 -1
  28. package/lib/commonjs/storage-runtime.js +5 -1
  29. package/lib/commonjs/storage-runtime.js.map +1 -1
  30. package/lib/commonjs/testing.js +13 -0
  31. package/lib/commonjs/testing.js.map +1 -1
  32. package/lib/module/capabilities.js +2 -1
  33. package/lib/module/capabilities.js.map +1 -1
  34. package/lib/module/core/durability.js +111 -45
  35. package/lib/module/core/durability.js.map +1 -1
  36. package/lib/module/index.js +12 -8
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/index.web.js +215 -99
  39. package/lib/module/index.web.js.map +1 -1
  40. package/lib/module/internal.js +2 -9
  41. package/lib/module/internal.js.map +1 -1
  42. package/lib/module/shared.js +60 -2
  43. package/lib/module/shared.js.map +1 -1
  44. package/lib/module/storage-core.js +884 -164
  45. package/lib/module/storage-core.js.map +1 -1
  46. package/lib/module/storage-runtime.js +4 -1
  47. package/lib/module/storage-runtime.js.map +1 -1
  48. package/lib/module/testing.js +8 -1
  49. package/lib/module/testing.js.map +1 -1
  50. package/lib/typescript/capabilities.d.ts +2 -1
  51. package/lib/typescript/capabilities.d.ts.map +1 -1
  52. package/lib/typescript/core/durability.d.ts +8 -4
  53. package/lib/typescript/core/durability.d.ts.map +1 -1
  54. package/lib/typescript/index.d.ts +2 -2
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/index.web.d.ts +3 -29
  57. package/lib/typescript/index.web.d.ts.map +1 -1
  58. package/lib/typescript/internal.d.ts +0 -2
  59. package/lib/typescript/internal.d.ts.map +1 -1
  60. package/lib/typescript/shared.d.ts +19 -0
  61. package/lib/typescript/shared.d.ts.map +1 -1
  62. package/lib/typescript/storage-core.d.ts +15 -6
  63. package/lib/typescript/storage-core.d.ts.map +1 -1
  64. package/lib/typescript/storage-runtime.d.ts +2 -1
  65. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  66. package/lib/typescript/testing.d.ts +1 -1
  67. package/lib/typescript/testing.d.ts.map +1 -1
  68. package/package.json +2 -2
  69. package/src/capabilities.ts +3 -1
  70. package/src/core/durability.ts +140 -43
  71. package/src/index.ts +20 -10
  72. package/src/index.web.ts +328 -166
  73. package/src/internal.ts +0 -14
  74. package/src/shared.ts +85 -0
  75. package/src/storage-core.ts +1198 -232
  76. package/src/storage-runtime.ts +6 -0
  77. package/src/testing.ts +8 -1
@@ -14,24 +14,28 @@ const item = createStorageItem<T>({
14
14
 
15
15
  `StorageItemConfig<T>`:
16
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. |
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)` | Clear one 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
@@ -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 the web entry against the localStorage backend. The `disk:` and `secure:` labels describe web scopes, not native Disk or Secure storage.
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
  ```
@@ -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
- ## Locked Keychain Errors
151
+ ## Secure Storage Error Recovery
152
152
 
153
153
  ```ts
154
- import { isKeychainLockedError } from "react-native-nitro-storage";
154
+ import { isStorageError } from "react-native-nitro-storage";
155
155
 
156
156
  try {
157
157
  refreshTokenItem.get();
158
158
  } catch (error) {
159
- if (isKeychainLockedError(error)) {
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
- The helper recognizes iOS locked Keychain cases and Android invalidated/locked key cases surfaced by the native bridge.
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 persistence. Enable async writes when write throughput is more important than immediate durability:
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
- Call `flushSecureWrites()` before assertions, namespace clears, or any boundary that requires deterministic persistence.
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 an end-to-end auth flow on a locked/unlocked real device when changing biometric or Keychain behavior.
240
+ Also run the [physical-device Keychain lifecycle protocol](keychain-lifecycle-testing.md)
241
+ when changing biometric, Keychain, or error-classification behavior.