react-native-nitro-storage 0.3.1 → 0.3.2

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 (37) hide show
  1. package/README.md +199 -10
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +4 -0
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +1 -0
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +36 -13
  6. package/cpp/bindings/HybridStorage.cpp +55 -9
  7. package/cpp/bindings/HybridStorage.hpp +19 -2
  8. package/cpp/core/NativeStorageAdapter.hpp +1 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +1 -0
  10. package/ios/IOSStorageAdapterCpp.mm +7 -1
  11. package/lib/commonjs/index.js +139 -63
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +236 -89
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/storage-hooks.js +36 -0
  16. package/lib/commonjs/storage-hooks.js.map +1 -0
  17. package/lib/module/index.js +121 -60
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/index.web.js +219 -87
  20. package/lib/module/index.web.js.map +1 -1
  21. package/lib/module/storage-hooks.js +30 -0
  22. package/lib/module/storage-hooks.js.map +1 -0
  23. package/lib/typescript/Storage.nitro.d.ts +2 -0
  24. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  25. package/lib/typescript/index.d.ts +3 -3
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/lib/typescript/index.web.d.ts +5 -3
  28. package/lib/typescript/index.web.d.ts.map +1 -1
  29. package/lib/typescript/storage-hooks.d.ts +10 -0
  30. package/lib/typescript/storage-hooks.d.ts.map +1 -0
  31. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  32. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  33. package/package.json +5 -3
  34. package/src/Storage.nitro.ts +2 -0
  35. package/src/index.ts +143 -83
  36. package/src/index.web.ts +255 -112
  37. package/src/storage-hooks.ts +48 -0
package/README.md CHANGED
@@ -19,6 +19,24 @@ Synchronous Memory, Disk, and Secure storage in one unified API — powered by [
19
19
  - **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
20
20
  - **Cross-platform** — iOS, Android, and web (`localStorage` fallback)
21
21
 
22
+ ## Feature Coverage
23
+
24
+ Every feature in this package is documented with at least one runnable example in this README:
25
+
26
+ - Core item API (`createStorageItem`, `get/set/delete/has/subscribe`) — see Quick Start and Low-level subscription use case
27
+ - Hooks (`useStorage`, `useStorageSelector`, `useSetStorage`) — see Quick Start and Persisted User Preferences
28
+ - Scopes (`Memory`, `Disk`, `Secure`) — see Storage Scopes and multiple use cases
29
+ - Namespaces — see Multi-Tenant / Namespaced Storage
30
+ - TTL expiration + callbacks — see OTP / Temporary Codes
31
+ - Validation + recovery — see Feature Flags with Validation
32
+ - Biometric + access control — see Biometric-protected Secrets
33
+ - Global storage utilities (`clear*`, `has`, `getAll*`, `size`, secure write settings) — see Global utility examples and Storage Snapshots and Cleanup
34
+ - Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
35
+ - Transactions — see Transactions and Atomic Balance Transfer
36
+ - Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
37
+ - MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
38
+ - Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
39
+
22
40
  ## Requirements
23
41
 
24
42
  | Dependency | Version |
@@ -183,6 +201,17 @@ function createStorageItem<T = undefined>(
183
201
  | `scope` | `StorageScope` | The item's scope |
184
202
  | `key` | `string` | The resolved key (includes namespace prefix) |
185
203
 
204
+ **Non-React subscription example:**
205
+
206
+ ```ts
207
+ const unsubscribe = sessionItem.subscribe(() => {
208
+ console.log("session changed:", sessionItem.get());
209
+ });
210
+
211
+ sessionItem.set("next-session");
212
+ unsubscribe();
213
+ ```
214
+
186
215
  ---
187
216
 
188
217
  ### React Hooks
@@ -231,10 +260,49 @@ import { storage, StorageScope } from "react-native-nitro-storage";
231
260
  | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
232
261
  | `storage.size(scope)` | Number of stored keys |
233
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 |
234
265
  | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
235
266
 
236
267
  > `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
237
268
 
269
+ #### Global utility examples
270
+
271
+ ```ts
272
+ import { AccessControl, storage, StorageScope } from "react-native-nitro-storage";
273
+
274
+ storage.has("session", StorageScope.Disk);
275
+ storage.getAllKeys(StorageScope.Disk);
276
+ storage.getAll(StorageScope.Disk);
277
+ storage.size(StorageScope.Disk);
278
+
279
+ storage.clearNamespace("user-42", StorageScope.Disk);
280
+ storage.clearBiometric();
281
+
282
+ storage.setAccessControl(AccessControl.WhenUnlockedThisDeviceOnly);
283
+ storage.setKeychainAccessGroup("group.com.example.shared");
284
+
285
+ storage.clear(StorageScope.Memory);
286
+ storage.clearAll();
287
+ ```
288
+
289
+ #### Android secure write mode
290
+
291
+ `storage.setSecureWritesAsync(true)` switches secure writes from synchronous `commit()` to asynchronous `apply()` on Android.
292
+ Use this for non-critical secure writes when lower latency matters more than immediate durability.
293
+
294
+ Call `storage.flushSecureWrites()` when you need deterministic persistence boundaries (for example before namespace clears, process handoff, or strict test assertions).
295
+
296
+ ```ts
297
+ import { storage } from "react-native-nitro-storage";
298
+
299
+ storage.setSecureWritesAsync(true);
300
+
301
+ // ...multiple secure writes happen (including coalesced item writes)
302
+
303
+ storage.flushSecureWrites(); // deterministic durability boundary
304
+ ```
305
+
238
306
  ---
239
307
 
240
308
  ### `createSecureAuthStorage<K>(config, options?)`
@@ -398,11 +466,11 @@ enum BiometricLevel {
398
466
  ### Persisted User Preferences
399
467
 
400
468
  ```ts
401
- interface UserPreferences {
469
+ type UserPreferences = {
402
470
  theme: "light" | "dark" | "system";
403
471
  language: string;
404
472
  notifications: boolean;
405
- }
473
+ };
406
474
 
407
475
  const prefsItem = createStorageItem<UserPreferences>({
408
476
  key: "prefs",
@@ -446,21 +514,29 @@ storage.clearNamespace("myapp-auth", StorageScope.Secure);
446
514
  ### Feature Flags with Validation
447
515
 
448
516
  ```ts
449
- interface FeatureFlags {
517
+ type FeatureFlags = {
450
518
  darkMode: boolean;
451
519
  betaFeature: boolean;
452
520
  maxUploadMb: number;
453
- }
521
+ };
522
+
523
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
524
+ typeof value === "object" && value !== null;
525
+
526
+ const isFeatureFlags = (value: unknown): value is FeatureFlags => {
527
+ if (!isRecord(value)) return false;
528
+ return (
529
+ typeof value.darkMode === "boolean" &&
530
+ typeof value.betaFeature === "boolean" &&
531
+ typeof value.maxUploadMb === "number"
532
+ );
533
+ };
454
534
 
455
535
  const flagsItem = createStorageItem<FeatureFlags>({
456
536
  key: "feature-flags",
457
537
  scope: StorageScope.Disk,
458
538
  defaultValue: { darkMode: false, betaFeature: false, maxUploadMb: 10 },
459
- validate: (v): v is FeatureFlags =>
460
- typeof v === "object" &&
461
- v !== null &&
462
- typeof (v as any).darkMode === "boolean" &&
463
- typeof (v as any).maxUploadMb === "number",
539
+ validate: isFeatureFlags,
464
540
  onValidationError: () => ({
465
541
  darkMode: false,
466
542
  betaFeature: false,
@@ -471,6 +547,24 @@ const flagsItem = createStorageItem<FeatureFlags>({
471
547
  });
472
548
  ```
473
549
 
550
+ ### Biometric-protected Secrets
551
+
552
+ ```ts
553
+ import { AccessControl, createStorageItem, StorageScope } from "react-native-nitro-storage";
554
+
555
+ const paymentPin = createStorageItem<string>({
556
+ key: "payment-pin",
557
+ scope: StorageScope.Secure,
558
+ defaultValue: "",
559
+ biometric: true,
560
+ accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
561
+ });
562
+
563
+ paymentPin.set("4829");
564
+ const pin = paymentPin.get();
565
+ paymentPin.delete();
566
+ ```
567
+
474
568
  ### Multi-Tenant / Namespaced Storage
475
569
 
476
570
  ```ts
@@ -515,6 +609,40 @@ otpItem.set("482917");
515
609
  const code = otpItem.get();
516
610
  ```
517
611
 
612
+ ### Bulk Bootstrap with Batch APIs
613
+
614
+ ```ts
615
+ import {
616
+ createStorageItem,
617
+ getBatch,
618
+ removeBatch,
619
+ setBatch,
620
+ StorageScope,
621
+ } from "react-native-nitro-storage";
622
+
623
+ const firstName = createStorageItem({
624
+ key: "first-name",
625
+ scope: StorageScope.Disk,
626
+ defaultValue: "",
627
+ });
628
+ const lastName = createStorageItem({
629
+ key: "last-name",
630
+ scope: StorageScope.Disk,
631
+ defaultValue: "",
632
+ });
633
+
634
+ setBatch(
635
+ [
636
+ { item: firstName, value: "Ada" },
637
+ { item: lastName, value: "Lovelace" },
638
+ ],
639
+ StorageScope.Disk,
640
+ );
641
+
642
+ const [first, last] = getBatch([firstName, lastName], StorageScope.Disk);
643
+ removeBatch([firstName, lastName], StorageScope.Disk);
644
+ ```
645
+
518
646
  ### Atomic Balance Transfer
519
647
 
520
648
  ```ts
@@ -555,6 +683,60 @@ const compactItem = createStorageItem<{ id: number; active: boolean }>({
555
683
  });
556
684
  ```
557
685
 
686
+ ### Coalesced Secure Writes with Deterministic Flush
687
+
688
+ ```ts
689
+ import { createStorageItem, storage, StorageScope } from "react-native-nitro-storage";
690
+
691
+ const sessionToken = createStorageItem<string>({
692
+ key: "session-token",
693
+ scope: StorageScope.Secure,
694
+ defaultValue: "",
695
+ coalesceSecureWrites: true,
696
+ });
697
+
698
+ sessionToken.set("token-v1");
699
+ sessionToken.set("token-v2");
700
+
701
+ // force pending secure writes to native persistence
702
+ storage.flushSecureWrites();
703
+ ```
704
+
705
+ ### Storage Snapshots and Cleanup
706
+
707
+ ```ts
708
+ import { storage, StorageScope } from "react-native-nitro-storage";
709
+
710
+ const diskKeys = storage.getAllKeys(StorageScope.Disk);
711
+ const diskValues = storage.getAll(StorageScope.Disk);
712
+ const secureCount = storage.size(StorageScope.Secure);
713
+
714
+ if (storage.has("legacy-flag", StorageScope.Disk)) {
715
+ storage.clearNamespace("legacy", StorageScope.Disk);
716
+ }
717
+
718
+ storage.clearBiometric();
719
+ ```
720
+
721
+ ### Low-level Subscription (outside React)
722
+
723
+ ```ts
724
+ import { createStorageItem, StorageScope } from "react-native-nitro-storage";
725
+
726
+ const notificationsItem = createStorageItem<boolean>({
727
+ key: "notifications-enabled",
728
+ scope: StorageScope.Disk,
729
+ defaultValue: true,
730
+ });
731
+
732
+ const unsubscribe = notificationsItem.subscribe(() => {
733
+ console.log("notifications changed:", notificationsItem.get());
734
+ });
735
+
736
+ notificationsItem.set(false);
737
+ unsubscribe();
738
+ ```
739
+
558
740
  ### Migrating From MMKV
559
741
 
560
742
  ```ts
@@ -600,7 +782,11 @@ From repository root:
600
782
 
601
783
  ```bash
602
784
  bun run test -- --filter=react-native-nitro-storage
785
+ bun run lint -- --filter=react-native-nitro-storage
786
+ bun run format:check -- --filter=react-native-nitro-storage
603
787
  bun run typecheck -- --filter=react-native-nitro-storage
788
+ bun run test:types -- --filter=react-native-nitro-storage
789
+ bun run test:cpp -- --filter=react-native-nitro-storage
604
790
  bun run build -- --filter=react-native-nitro-storage
605
791
  ```
606
792
 
@@ -612,7 +798,10 @@ bun run test:coverage # run tests with coverage
612
798
  bun run lint # eslint (expo-magic rules)
613
799
  bun run format:check # prettier check
614
800
  bun run typecheck # tsc --noEmit
615
- bun run build # tsup build
801
+ bun run test:types # public type-level API tests
802
+ bun run test:cpp # C++ binding/core tests
803
+ bun run check:pack # npm pack content guard
804
+ bun run build # bob build
616
805
  bun run benchmark # performance benchmarks
617
806
  ```
618
807
 
@@ -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(
@@ -213,6 +213,10 @@ void AndroidStorageAdapterCpp::clearSecure() {
213
213
  // --- Config (no-ops on Android; access control / groups are iOS-specific) ---
214
214
 
215
215
  void AndroidStorageAdapterCpp::setSecureAccessControl(int /*level*/) {}
216
+ void AndroidStorageAdapterCpp::setSecureWritesAsync(bool enabled) {
217
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(jboolean)>("setSecureWritesAsync");
218
+ method(AndroidStorageAdapterJava::javaClassStatic(), enabled);
219
+ }
216
220
  void AndroidStorageAdapterCpp::setKeychainAccessGroup(const std::string& /*group*/) {}
217
221
 
218
222
  // --- Biometric ---
@@ -49,6 +49,7 @@ public:
49
49
  void clearSecure() override;
50
50
 
51
51
  void setSecureAccessControl(int level) override;
52
+ void setSecureWritesAsync(bool enabled) override;
52
53
  void setKeychainAccessGroup(const std::string& group) override;
53
54
 
54
55
  void setSecureBiometric(const std::string& key, const std::string& value) override;
@@ -33,6 +33,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
33
33
  encryptedPreferences
34
34
  }
35
35
  }
36
+
37
+ @Volatile
38
+ private var secureWritesAsync = false
36
39
 
37
40
  private fun initializeEncryptedPreferences(name: String, key: MasterKey): SharedPreferences {
38
41
  return try {
@@ -90,6 +93,14 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
90
93
  }
91
94
  }
92
95
  }
96
+
97
+ private fun applySecureEditor(editor: SharedPreferences.Editor) {
98
+ if (secureWritesAsync) {
99
+ editor.apply()
100
+ } else {
101
+ editor.commit()
102
+ }
103
+ }
93
104
 
94
105
  companion object {
95
106
  @Volatile
@@ -118,6 +129,11 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
118
129
  return getInstanceOrThrow().context
119
130
  }
120
131
 
132
+ @JvmStatic
133
+ fun setSecureWritesAsync(enabled: Boolean) {
134
+ getInstanceOrThrow().secureWritesAsync = enabled
135
+ }
136
+
121
137
  // --- Disk ---
122
138
 
123
139
  @JvmStatic
@@ -182,21 +198,24 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
182
198
  getInstanceOrThrow().sharedPreferences.edit().clear().apply()
183
199
  }
184
200
 
185
- // --- Secure (uses commit for reliability) ---
201
+ // --- Secure (sync commit by default, async apply when enabled) ---
186
202
 
187
203
  @JvmStatic
188
204
  fun setSecure(key: String, value: String) {
189
- getInstanceOrThrow().encryptedPreferences.edit().putString(key, value).commit()
205
+ val inst = getInstanceOrThrow()
206
+ val editor = inst.encryptedPreferences.edit().putString(key, value)
207
+ inst.applySecureEditor(editor)
190
208
  }
191
209
 
192
210
  @JvmStatic
193
211
  fun setSecureBatch(keys: Array<String>, values: Array<String>) {
194
- val editor = getInstanceOrThrow().encryptedPreferences.edit()
212
+ val inst = getInstanceOrThrow()
213
+ val editor = inst.encryptedPreferences.edit()
195
214
  val count = minOf(keys.size, values.size)
196
215
  for (index in 0 until count) {
197
216
  editor.putString(keys[index], values[index])
198
217
  }
199
- editor.commit()
218
+ inst.applySecureEditor(editor)
200
219
  }
201
220
 
202
221
  @JvmStatic
@@ -215,8 +234,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
215
234
  @JvmStatic
216
235
  fun deleteSecure(key: String) {
217
236
  val inst = getInstanceOrThrow()
218
- inst.encryptedPreferences.edit().remove(key).commit()
219
- inst.biometricPreferences.edit().remove(key).commit()
237
+ inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
238
+ inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
220
239
  }
221
240
 
222
241
  @JvmStatic
@@ -228,8 +247,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
228
247
  secureEditor.remove(key)
229
248
  biometricEditor.remove(key)
230
249
  }
231
- secureEditor.commit()
232
- biometricEditor.commit()
250
+ inst.applySecureEditor(secureEditor)
251
+ inst.applySecureEditor(biometricEditor)
233
252
  }
234
253
 
235
254
  @JvmStatic
@@ -255,15 +274,17 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
255
274
  @JvmStatic
256
275
  fun clearSecure() {
257
276
  val inst = getInstanceOrThrow()
258
- inst.encryptedPreferences.edit().clear().commit()
259
- inst.biometricPreferences.edit().clear().commit()
277
+ inst.applySecureEditor(inst.encryptedPreferences.edit().clear())
278
+ inst.applySecureEditor(inst.biometricPreferences.edit().clear())
260
279
  }
261
280
 
262
281
  // --- Biometric (separate encrypted store, requires recent biometric auth on Android) ---
263
282
 
264
283
  @JvmStatic
265
284
  fun setSecureBiometric(key: String, value: String) {
266
- getInstanceOrThrow().biometricPreferences.edit().putString(key, value).commit()
285
+ val inst = getInstanceOrThrow()
286
+ val editor = inst.biometricPreferences.edit().putString(key, value)
287
+ inst.applySecureEditor(editor)
267
288
  }
268
289
 
269
290
  @JvmStatic
@@ -273,7 +294,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
273
294
 
274
295
  @JvmStatic
275
296
  fun deleteSecureBiometric(key: String) {
276
- getInstanceOrThrow().biometricPreferences.edit().remove(key).commit()
297
+ val inst = getInstanceOrThrow()
298
+ inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
277
299
  }
278
300
 
279
301
  @JvmStatic
@@ -283,7 +305,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
283
305
 
284
306
  @JvmStatic
285
307
  fun clearSecureBiometric() {
286
- getInstanceOrThrow().biometricPreferences.edit().clear().commit()
308
+ val inst = getInstanceOrThrow()
309
+ inst.applySecureEditor(inst.biometricPreferences.edit().clear())
287
310
  }
288
311
  }
289
312
  }
@@ -1,12 +1,14 @@
1
1
  #include "HybridStorage.hpp"
2
2
  #include <stdexcept>
3
3
 
4
+ #ifndef NITRO_STORAGE_DISABLE_PLATFORM_ADAPTER
4
5
  #if __APPLE__
5
6
  #include "../../ios/IOSStorageAdapterCpp.hpp"
6
7
  #elif __ANDROID__
7
8
  #include "../../android/src/main/cpp/AndroidStorageAdapterCpp.hpp"
8
9
  #include <fbjni/fbjni.h>
9
10
  #endif
11
+ #endif
10
12
 
11
13
  namespace margelo::nitro::NitroStorage {
12
14
 
@@ -16,12 +18,14 @@ constexpr auto kBatchMissingSentinel = "__nitro_storage_batch_missing__::v1";
16
18
 
17
19
  HybridStorage::HybridStorage()
18
20
  : HybridObject(TAG), HybridStorageSpec() {
21
+ #ifndef NITRO_STORAGE_DISABLE_PLATFORM_ADAPTER
19
22
  #if __APPLE__
20
23
  nativeAdapter_ = std::make_shared<::NitroStorage::IOSStorageAdapterCpp>();
21
24
  #elif __ANDROID__
22
25
  auto context = ::NitroStorage::AndroidStorageAdapterJava::getContext();
23
26
  nativeAdapter_ = std::make_shared<::NitroStorage::AndroidStorageAdapterCpp>(context);
24
27
  #endif
28
+ #endif
25
29
  }
26
30
 
27
31
  HybridStorage::HybridStorage(std::shared_ptr<::NitroStorage::NativeStorageAdapter> adapter)
@@ -298,8 +302,10 @@ void HybridStorage::setBatch(const std::vector<std::string>& keys, const std::ve
298
302
  break;
299
303
  }
300
304
 
305
+ const auto scopeValue = static_cast<int>(s);
306
+ const auto listeners = copyListenersForScope(scopeValue);
301
307
  for (size_t i = 0; i < keys.size(); ++i) {
302
- notifyListeners(static_cast<int>(s), keys[i], values[i]);
308
+ notifyListeners(listeners, keys[i], values[i]);
303
309
  }
304
310
  }
305
311
 
@@ -392,11 +398,34 @@ void HybridStorage::removeBatch(const std::vector<std::string>& keys, double sco
392
398
  break;
393
399
  }
394
400
 
401
+ const auto scopeValue = static_cast<int>(s);
402
+ const auto listeners = copyListenersForScope(scopeValue);
395
403
  for (const auto& key : keys) {
396
- notifyListeners(static_cast<int>(s), key, std::nullopt);
404
+ notifyListeners(listeners, key, std::nullopt);
397
405
  }
398
406
  }
399
407
 
408
+ void HybridStorage::removeByPrefix(const std::string& prefix, double scope) {
409
+ if (prefix.empty()) {
410
+ return;
411
+ }
412
+
413
+ const auto keys = getAllKeys(scope);
414
+ std::vector<std::string> prefixedKeys;
415
+ prefixedKeys.reserve(keys.size());
416
+ for (const auto& key : keys) {
417
+ if (key.rfind(prefix, 0) == 0) {
418
+ prefixedKeys.push_back(key);
419
+ }
420
+ }
421
+
422
+ if (prefixedKeys.empty()) {
423
+ return;
424
+ }
425
+
426
+ removeBatch(prefixedKeys, scope);
427
+ }
428
+
400
429
  // --- Configuration ---
401
430
 
402
431
  void HybridStorage::setSecureAccessControl(double level) {
@@ -404,6 +433,11 @@ void HybridStorage::setSecureAccessControl(double level) {
404
433
  nativeAdapter_->setSecureAccessControl(static_cast<int>(level));
405
434
  }
406
435
 
436
+ void HybridStorage::setSecureWritesAsync(bool enabled) {
437
+ ensureAdapter();
438
+ nativeAdapter_->setSecureWritesAsync(enabled);
439
+ }
440
+
407
441
  void HybridStorage::setKeychainAccessGroup(const std::string& group) {
408
442
  ensureAdapter();
409
443
  nativeAdapter_->setKeychainAccessGroup(group);
@@ -457,11 +491,7 @@ void HybridStorage::clearSecureBiometric() {
457
491
 
458
492
  // --- Internal ---
459
493
 
460
- void HybridStorage::notifyListeners(
461
- int scope,
462
- const std::string& key,
463
- const std::optional<std::string>& value
464
- ) {
494
+ std::vector<HybridStorage::Listener> HybridStorage::copyListenersForScope(int scope) {
465
495
  std::vector<Listener> listenersCopy;
466
496
  {
467
497
  std::lock_guard<std::mutex> lock(listenersMutex_);
@@ -471,8 +501,15 @@ void HybridStorage::notifyListeners(
471
501
  listenersCopy = it->second;
472
502
  }
473
503
  }
474
-
475
- for (const auto& listener : listenersCopy) {
504
+ return listenersCopy;
505
+ }
506
+
507
+ void HybridStorage::notifyListeners(
508
+ const std::vector<Listener>& listeners,
509
+ const std::string& key,
510
+ const std::optional<std::string>& value
511
+ ) {
512
+ for (const auto& listener : listeners) {
476
513
  try {
477
514
  listener.callback(key, value);
478
515
  } catch (...) {
@@ -481,6 +518,15 @@ void HybridStorage::notifyListeners(
481
518
  }
482
519
  }
483
520
 
521
+ void HybridStorage::notifyListeners(
522
+ int scope,
523
+ const std::string& key,
524
+ const std::optional<std::string>& value
525
+ ) {
526
+ const auto listeners = copyListenersForScope(scope);
527
+ notifyListeners(listeners, key, value);
528
+ }
529
+
484
530
  void HybridStorage::ensureAdapter() const {
485
531
  if (!nativeAdapter_) {
486
532
  throw std::runtime_error("NitroStorage: Native adapter not initialized");
@@ -3,6 +3,7 @@
3
3
  #include "HybridStorageSpec.hpp"
4
4
  #include "../core/NativeStorageAdapter.hpp"
5
5
  #include <unordered_map>
6
+ #include <map>
6
7
  #include <mutex>
7
8
  #include <functional>
8
9
  #include <memory>
@@ -10,6 +11,14 @@
10
11
 
11
12
  namespace margelo::nitro::NitroStorage {
12
13
 
14
+ #ifdef NITRO_STORAGE_USE_ORDERED_MAP_FOR_TESTS
15
+ template <typename Key, typename Value>
16
+ using HybridStorageMap = std::map<Key, Value>;
17
+ #else
18
+ template <typename Key, typename Value>
19
+ using HybridStorageMap = std::unordered_map<Key, Value>;
20
+ #endif
21
+
13
22
  class HybridStorage : public HybridStorageSpec {
14
23
  public:
15
24
  HybridStorage();
@@ -26,11 +35,13 @@ public:
26
35
  void setBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values, double scope) override;
27
36
  std::vector<std::string> getBatch(const std::vector<std::string>& keys, double scope) override;
28
37
  void removeBatch(const std::vector<std::string>& keys, double scope) override;
38
+ void removeByPrefix(const std::string& prefix, double scope) override;
29
39
  std::function<void()> addOnChange(
30
40
  double scope,
31
41
  const std::function<void(const std::string&, const std::optional<std::string>&)>& callback
32
42
  ) override;
33
43
  void setSecureAccessControl(double level) override;
44
+ void setSecureWritesAsync(bool enabled) override;
34
45
  void setKeychainAccessGroup(const std::string& group) override;
35
46
  void setSecureBiometric(const std::string& key, const std::string& value) override;
36
47
  std::optional<std::string> getSecureBiometric(const std::string& key) override;
@@ -50,15 +61,21 @@ private:
50
61
  std::function<void(const std::string&, const std::optional<std::string>&)> callback;
51
62
  };
52
63
 
53
- std::unordered_map<std::string, std::string> memoryStore_;
64
+ HybridStorageMap<std::string, std::string> memoryStore_;
54
65
  std::mutex memoryMutex_;
55
66
 
56
67
  std::shared_ptr<::NitroStorage::NativeStorageAdapter> nativeAdapter_;
57
68
 
58
- std::unordered_map<int, std::vector<Listener>> listeners_;
69
+ HybridStorageMap<int, std::vector<Listener>> listeners_;
59
70
  std::mutex listenersMutex_;
60
71
  size_t nextListenerId_ = 0;
61
72
 
73
+ std::vector<Listener> copyListenersForScope(int scope);
74
+ void notifyListeners(
75
+ const std::vector<Listener>& listeners,
76
+ const std::string& key,
77
+ const std::optional<std::string>& value
78
+ );
62
79
  void notifyListeners(int scope, const std::string& key, const std::optional<std::string>& value);
63
80
  void ensureAdapter() const;
64
81
  Scope toScope(double scopeValue);
@@ -34,6 +34,7 @@ public:
34
34
  virtual void clearSecure() = 0;
35
35
 
36
36
  virtual void setSecureAccessControl(int level) = 0;
37
+ virtual void setSecureWritesAsync(bool enabled) = 0;
37
38
  virtual void setKeychainAccessGroup(const std::string& group) = 0;
38
39
 
39
40
  virtual void setSecureBiometric(const std::string& key, const std::string& value) = 0;
@@ -33,6 +33,7 @@ public:
33
33
  void clearSecure() override;
34
34
 
35
35
  void setSecureAccessControl(int level) override;
36
+ void setSecureWritesAsync(bool enabled) override;
36
37
  void setKeychainAccessGroup(const std::string& group) override;
37
38
 
38
39
  void setSecureBiometric(const std::string& key, const std::string& value) override;
@@ -3,6 +3,7 @@
3
3
  #import <Security/Security.h>
4
4
  #import <LocalAuthentication/LocalAuthentication.h>
5
5
  #include <algorithm>
6
+ #include <unordered_set>
6
7
 
7
8
  namespace NitroStorage {
8
9
 
@@ -222,9 +223,10 @@ bool IOSStorageAdapterCpp::hasSecure(const std::string& key) {
222
223
  std::vector<std::string> IOSStorageAdapterCpp::getAllKeysSecure() {
223
224
  NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
224
225
  std::vector<std::string> keys = keychainAccountsForService(kKeychainService, group);
226
+ std::unordered_set<std::string> seen(keys.begin(), keys.end());
225
227
  const std::vector<std::string> biometricKeys = keychainAccountsForService(kBiometricKeychainService, group);
226
228
  for (const auto& key : biometricKeys) {
227
- if (std::find(keys.begin(), keys.end(), key) == keys.end()) {
229
+ if (seen.insert(key).second) {
228
230
  keys.push_back(key);
229
231
  }
230
232
  }
@@ -288,6 +290,10 @@ void IOSStorageAdapterCpp::setSecureAccessControl(int level) {
288
290
  accessControlLevel_ = level;
289
291
  }
290
292
 
293
+ void IOSStorageAdapterCpp::setSecureWritesAsync(bool /*enabled*/) {
294
+ // iOS writes are synchronous by design; keep behavior unchanged.
295
+ }
296
+
291
297
  void IOSStorageAdapterCpp::setKeychainAccessGroup(const std::string& group) {
292
298
  keychainAccessGroup_ = group;
293
299
  }