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.
Files changed (44) hide show
  1. package/README.md +334 -34
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +26 -2
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +4 -0
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +90 -18
  6. package/cpp/bindings/HybridStorage.cpp +214 -23
  7. package/cpp/bindings/HybridStorage.hpp +31 -3
  8. package/cpp/core/NativeStorageAdapter.hpp +4 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +17 -0
  10. package/ios/IOSStorageAdapterCpp.mm +140 -10
  11. package/lib/commonjs/index.js +555 -288
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +750 -309
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/internal.js +25 -0
  16. package/lib/commonjs/internal.js.map +1 -1
  17. package/lib/commonjs/storage-hooks.js +36 -0
  18. package/lib/commonjs/storage-hooks.js.map +1 -0
  19. package/lib/module/index.js +537 -287
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/index.web.js +732 -308
  22. package/lib/module/index.web.js.map +1 -1
  23. package/lib/module/internal.js +24 -0
  24. package/lib/module/internal.js.map +1 -1
  25. package/lib/module/storage-hooks.js +30 -0
  26. package/lib/module/storage-hooks.js.map +1 -0
  27. package/lib/typescript/Storage.nitro.d.ts +4 -0
  28. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  29. package/lib/typescript/index.d.ts +41 -4
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/index.web.d.ts +45 -4
  32. package/lib/typescript/index.web.d.ts.map +1 -1
  33. package/lib/typescript/internal.d.ts +1 -0
  34. package/lib/typescript/internal.d.ts.map +1 -1
  35. package/lib/typescript/storage-hooks.d.ts +10 -0
  36. package/lib/typescript/storage-hooks.d.ts.map +1 -0
  37. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +4 -0
  38. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +4 -0
  39. package/package.json +5 -3
  40. package/src/Storage.nitro.ts +4 -0
  41. package/src/index.ts +704 -324
  42. package/src/index.web.ts +929 -346
  43. package/src/internal.ts +28 -0
  44. package/src/storage-hooks.ts +48 -0
@@ -33,6 +33,12 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
33
33
  encryptedPreferences
34
34
  }
35
35
  }
36
+
37
+ @Volatile
38
+ private var secureWritesAsync = false
39
+
40
+ @Volatile
41
+ private var secureKeysCache: Array<String>? = null
36
42
 
37
43
  private fun initializeEncryptedPreferences(name: String, key: MasterKey): SharedPreferences {
38
44
  return try {
@@ -90,6 +96,38 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
90
96
  }
91
97
  }
92
98
  }
99
+
100
+ private fun applySecureEditor(editor: SharedPreferences.Editor) {
101
+ if (secureWritesAsync) {
102
+ editor.apply()
103
+ } else {
104
+ editor.commit()
105
+ }
106
+ }
107
+
108
+ private fun invalidateSecureKeysCache() {
109
+ secureKeysCache = null
110
+ }
111
+
112
+ private fun getSecureKeysCached(): Array<String> {
113
+ val cached = secureKeysCache
114
+ if (cached != null) {
115
+ return cached
116
+ }
117
+
118
+ synchronized(this) {
119
+ val existing = secureKeysCache
120
+ if (existing != null) {
121
+ return existing
122
+ }
123
+ val keys = linkedSetOf<String>()
124
+ keys.addAll(encryptedPreferences.all.keys)
125
+ keys.addAll(biometricPreferences.all.keys)
126
+ val built = keys.toTypedArray()
127
+ secureKeysCache = built
128
+ return built
129
+ }
130
+ }
93
131
 
94
132
  companion object {
95
133
  @Volatile
@@ -118,6 +156,11 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
118
156
  return getInstanceOrThrow().context
119
157
  }
120
158
 
159
+ @JvmStatic
160
+ fun setSecureWritesAsync(enabled: Boolean) {
161
+ getInstanceOrThrow().secureWritesAsync = enabled
162
+ }
163
+
121
164
  // --- Disk ---
122
165
 
123
166
  @JvmStatic
@@ -172,6 +215,13 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
172
215
  return getInstanceOrThrow().sharedPreferences.all.keys.toTypedArray()
173
216
  }
174
217
 
218
+ @JvmStatic
219
+ fun getKeysByPrefixDisk(prefix: String): Array<String> {
220
+ return getInstanceOrThrow().sharedPreferences.all.keys
221
+ .filter { it.startsWith(prefix) }
222
+ .toTypedArray()
223
+ }
224
+
175
225
  @JvmStatic
176
226
  fun sizeDisk(): Int {
177
227
  return getInstanceOrThrow().sharedPreferences.all.size
@@ -182,21 +232,26 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
182
232
  getInstanceOrThrow().sharedPreferences.edit().clear().apply()
183
233
  }
184
234
 
185
- // --- Secure (uses commit for reliability) ---
235
+ // --- Secure (sync commit by default, async apply when enabled) ---
186
236
 
187
237
  @JvmStatic
188
238
  fun setSecure(key: String, value: String) {
189
- getInstanceOrThrow().encryptedPreferences.edit().putString(key, value).commit()
239
+ val inst = getInstanceOrThrow()
240
+ val editor = inst.encryptedPreferences.edit().putString(key, value)
241
+ inst.applySecureEditor(editor)
242
+ inst.invalidateSecureKeysCache()
190
243
  }
191
244
 
192
245
  @JvmStatic
193
246
  fun setSecureBatch(keys: Array<String>, values: Array<String>) {
194
- val editor = getInstanceOrThrow().encryptedPreferences.edit()
247
+ val inst = getInstanceOrThrow()
248
+ val editor = inst.encryptedPreferences.edit()
195
249
  val count = minOf(keys.size, values.size)
196
250
  for (index in 0 until count) {
197
251
  editor.putString(keys[index], values[index])
198
252
  }
199
- editor.commit()
253
+ inst.applySecureEditor(editor)
254
+ inst.invalidateSecureKeysCache()
200
255
  }
201
256
 
202
257
  @JvmStatic
@@ -215,8 +270,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
215
270
  @JvmStatic
216
271
  fun deleteSecure(key: String) {
217
272
  val inst = getInstanceOrThrow()
218
- inst.encryptedPreferences.edit().remove(key).commit()
219
- inst.biometricPreferences.edit().remove(key).commit()
273
+ inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
274
+ inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
275
+ inst.invalidateSecureKeysCache()
220
276
  }
221
277
 
222
278
  @JvmStatic
@@ -228,8 +284,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
228
284
  secureEditor.remove(key)
229
285
  biometricEditor.remove(key)
230
286
  }
231
- secureEditor.commit()
232
- biometricEditor.commit()
287
+ inst.applySecureEditor(secureEditor)
288
+ inst.applySecureEditor(biometricEditor)
289
+ inst.invalidateSecureKeysCache()
233
290
  }
234
291
 
235
292
  @JvmStatic
@@ -241,29 +298,40 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
241
298
  @JvmStatic
242
299
  fun getAllKeysSecure(): Array<String> {
243
300
  val inst = getInstanceOrThrow()
244
- val keys = linkedSetOf<String>()
245
- keys.addAll(inst.encryptedPreferences.all.keys)
246
- keys.addAll(inst.biometricPreferences.all.keys)
247
- return keys.toTypedArray()
301
+ return inst.getSecureKeysCached()
302
+ }
303
+
304
+ @JvmStatic
305
+ fun getKeysByPrefixSecure(prefix: String): Array<String> {
306
+ return getAllKeysSecure().filter { it.startsWith(prefix) }.toTypedArray()
248
307
  }
249
308
 
250
309
  @JvmStatic
251
310
  fun sizeSecure(): Int {
252
- return getAllKeysSecure().size
311
+ return getInstanceOrThrow().getSecureKeysCached().size
253
312
  }
254
313
 
255
314
  @JvmStatic
256
315
  fun clearSecure() {
257
316
  val inst = getInstanceOrThrow()
258
- inst.encryptedPreferences.edit().clear().commit()
259
- inst.biometricPreferences.edit().clear().commit()
317
+ inst.applySecureEditor(inst.encryptedPreferences.edit().clear())
318
+ inst.applySecureEditor(inst.biometricPreferences.edit().clear())
319
+ inst.invalidateSecureKeysCache()
260
320
  }
261
321
 
262
322
  // --- Biometric (separate encrypted store, requires recent biometric auth on Android) ---
263
323
 
264
324
  @JvmStatic
265
325
  fun setSecureBiometric(key: String, value: String) {
266
- getInstanceOrThrow().biometricPreferences.edit().putString(key, value).commit()
326
+ setSecureBiometricWithLevel(key, value, 2)
327
+ }
328
+
329
+ @JvmStatic
330
+ fun setSecureBiometricWithLevel(key: String, value: String, @Suppress("UNUSED_PARAMETER") level: Int) {
331
+ val inst = getInstanceOrThrow()
332
+ val editor = inst.biometricPreferences.edit().putString(key, value)
333
+ inst.applySecureEditor(editor)
334
+ inst.invalidateSecureKeysCache()
267
335
  }
268
336
 
269
337
  @JvmStatic
@@ -273,7 +341,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
273
341
 
274
342
  @JvmStatic
275
343
  fun deleteSecureBiometric(key: String) {
276
- getInstanceOrThrow().biometricPreferences.edit().remove(key).commit()
344
+ val inst = getInstanceOrThrow()
345
+ inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
346
+ inst.invalidateSecureKeysCache()
277
347
  }
278
348
 
279
349
  @JvmStatic
@@ -283,7 +353,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
283
353
 
284
354
  @JvmStatic
285
355
  fun clearSecureBiometric() {
286
- getInstanceOrThrow().biometricPreferences.edit().clear().commit()
356
+ val inst = getInstanceOrThrow()
357
+ inst.applySecureEditor(inst.biometricPreferences.edit().clear())
358
+ inst.invalidateSecureKeysCache()
287
359
  }
288
360
  }
289
361
  }
@@ -1,27 +1,32 @@
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
 
13
15
  namespace {
14
16
  constexpr auto kBatchMissingSentinel = "__nitro_storage_batch_missing__::v1";
17
+ constexpr int kDefaultBiometricLevel = 2;
15
18
  } // namespace
16
19
 
17
20
  HybridStorage::HybridStorage()
18
21
  : HybridObject(TAG), HybridStorageSpec() {
22
+ #ifndef NITRO_STORAGE_DISABLE_PLATFORM_ADAPTER
19
23
  #if __APPLE__
20
24
  nativeAdapter_ = std::make_shared<::NitroStorage::IOSStorageAdapterCpp>();
21
25
  #elif __ANDROID__
22
26
  auto context = ::NitroStorage::AndroidStorageAdapterJava::getContext();
23
27
  nativeAdapter_ = std::make_shared<::NitroStorage::AndroidStorageAdapterCpp>(context);
24
28
  #endif
29
+ #endif
25
30
  }
26
31
 
27
32
  HybridStorage::HybridStorage(std::shared_ptr<::NitroStorage::NativeStorageAdapter> adapter)
@@ -70,7 +75,8 @@ void HybridStorage::set(const std::string& key, const std::string& value, double
70
75
  }
71
76
  break;
72
77
  }
73
-
78
+
79
+ onKeySet(static_cast<int>(s), key);
74
80
  notifyListeners(static_cast<int>(s), key, value);
75
81
  }
76
82
 
@@ -139,7 +145,8 @@ void HybridStorage::remove(const std::string& key, double scope) {
139
145
  }
140
146
  break;
141
147
  }
142
-
148
+
149
+ onKeyRemove(static_cast<int>(s), key);
143
150
  notifyListeners(static_cast<int>(s), key, std::nullopt);
144
151
  }
145
152
 
@@ -175,11 +182,56 @@ std::vector<std::string> HybridStorage::getAllKeys(double scope) {
175
182
  return keys;
176
183
  }
177
184
  case Scope::Disk:
178
- ensureAdapter();
179
- return nativeAdapter_->getAllKeysDisk();
180
- case Scope::Secure:
181
- ensureAdapter();
182
- return nativeAdapter_->getAllKeysSecure();
185
+ case Scope::Secure: {
186
+ const int scopeValue = static_cast<int>(s);
187
+ ensureKeyIndexHydrated(scopeValue);
188
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
189
+ auto indexIt = keyIndex_.find(scopeValue);
190
+ if (indexIt == keyIndex_.end()) {
191
+ return {};
192
+ }
193
+ return toVector(indexIt->second);
194
+ }
195
+ }
196
+ return {};
197
+ }
198
+
199
+ std::vector<std::string> HybridStorage::getKeysByPrefix(const std::string& prefix, double scope) {
200
+ Scope s = toScope(scope);
201
+ if (prefix.empty()) {
202
+ return getAllKeys(scope);
203
+ }
204
+
205
+ switch (s) {
206
+ case Scope::Memory: {
207
+ std::lock_guard<std::mutex> lock(memoryMutex_);
208
+ std::vector<std::string> keys;
209
+ keys.reserve(memoryStore_.size());
210
+ for (const auto& [key, _] : memoryStore_) {
211
+ if (key.rfind(prefix, 0) == 0) {
212
+ keys.push_back(key);
213
+ }
214
+ }
215
+ return keys;
216
+ }
217
+ case Scope::Disk:
218
+ case Scope::Secure: {
219
+ const int scopeValue = static_cast<int>(s);
220
+ ensureKeyIndexHydrated(scopeValue);
221
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
222
+ std::vector<std::string> keys;
223
+ auto indexIt = keyIndex_.find(scopeValue);
224
+ if (indexIt == keyIndex_.end()) {
225
+ return keys;
226
+ }
227
+ keys.reserve(indexIt->second.size());
228
+ for (const auto& key : indexIt->second) {
229
+ if (key.rfind(prefix, 0) == 0) {
230
+ keys.push_back(key);
231
+ }
232
+ }
233
+ return keys;
234
+ }
183
235
  }
184
236
  return {};
185
237
  }
@@ -193,11 +245,16 @@ double HybridStorage::size(double scope) {
193
245
  return static_cast<double>(memoryStore_.size());
194
246
  }
195
247
  case Scope::Disk:
196
- ensureAdapter();
197
- return static_cast<double>(nativeAdapter_->sizeDisk());
198
- case Scope::Secure:
199
- ensureAdapter();
200
- return static_cast<double>(nativeAdapter_->sizeSecure());
248
+ case Scope::Secure: {
249
+ const int scopeValue = static_cast<int>(s);
250
+ ensureKeyIndexHydrated(scopeValue);
251
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
252
+ auto indexIt = keyIndex_.find(scopeValue);
253
+ if (indexIt == keyIndex_.end()) {
254
+ return 0.0;
255
+ }
256
+ return static_cast<double>(indexIt->second.size());
257
+ }
201
258
  }
202
259
  return 0.0;
203
260
  }
@@ -257,7 +314,8 @@ void HybridStorage::clear(double scope) {
257
314
  }
258
315
  break;
259
316
  }
260
-
317
+
318
+ onScopeClear(static_cast<int>(s));
261
319
  notifyListeners(static_cast<int>(s), "", std::nullopt);
262
320
  }
263
321
 
@@ -298,8 +356,13 @@ void HybridStorage::setBatch(const std::vector<std::string>& keys, const std::ve
298
356
  break;
299
357
  }
300
358
 
359
+ const auto scopeValue = static_cast<int>(s);
360
+ for (const auto& key : keys) {
361
+ onKeySet(scopeValue, key);
362
+ }
363
+ const auto listeners = copyListenersForScope(scopeValue);
301
364
  for (size_t i = 0; i < keys.size(); ++i) {
302
- notifyListeners(static_cast<int>(s), keys[i], values[i]);
365
+ notifyListeners(listeners, keys[i], values[i]);
303
366
  }
304
367
  }
305
368
 
@@ -392,9 +455,28 @@ void HybridStorage::removeBatch(const std::vector<std::string>& keys, double sco
392
455
  break;
393
456
  }
394
457
 
458
+ const auto scopeValue = static_cast<int>(s);
395
459
  for (const auto& key : keys) {
396
- notifyListeners(static_cast<int>(s), key, std::nullopt);
460
+ onKeyRemove(scopeValue, key);
461
+ }
462
+ const auto listeners = copyListenersForScope(scopeValue);
463
+ for (const auto& key : keys) {
464
+ notifyListeners(listeners, key, std::nullopt);
465
+ }
466
+ }
467
+
468
+ void HybridStorage::removeByPrefix(const std::string& prefix, double scope) {
469
+ if (prefix.empty()) {
470
+ return;
471
+ }
472
+
473
+ const auto prefixedKeys = getKeysByPrefix(prefix, scope);
474
+
475
+ if (prefixedKeys.empty()) {
476
+ return;
397
477
  }
478
+
479
+ removeBatch(prefixedKeys, scope);
398
480
  }
399
481
 
400
482
  // --- Configuration ---
@@ -404,6 +486,11 @@ void HybridStorage::setSecureAccessControl(double level) {
404
486
  nativeAdapter_->setSecureAccessControl(static_cast<int>(level));
405
487
  }
406
488
 
489
+ void HybridStorage::setSecureWritesAsync(bool enabled) {
490
+ ensureAdapter();
491
+ nativeAdapter_->setSecureWritesAsync(enabled);
492
+ }
493
+
407
494
  void HybridStorage::setKeychainAccessGroup(const std::string& group) {
408
495
  ensureAdapter();
409
496
  nativeAdapter_->setKeychainAccessGroup(group);
@@ -412,9 +499,18 @@ void HybridStorage::setKeychainAccessGroup(const std::string& group) {
412
499
  // --- Biometric ---
413
500
 
414
501
  void HybridStorage::setSecureBiometric(const std::string& key, const std::string& value) {
502
+ setSecureBiometricWithLevel(key, value, kDefaultBiometricLevel);
503
+ }
504
+
505
+ void HybridStorage::setSecureBiometricWithLevel(const std::string& key, const std::string& value, double level) {
415
506
  ensureAdapter();
416
507
  try {
417
- nativeAdapter_->setSecureBiometric(key, value);
508
+ nativeAdapter_->setSecureBiometricWithLevel(
509
+ key,
510
+ value,
511
+ static_cast<int>(level)
512
+ );
513
+ onKeySet(static_cast<int>(Scope::Secure), key);
418
514
  notifyListeners(static_cast<int>(Scope::Secure), key, value);
419
515
  } catch (const std::exception& e) {
420
516
  throw std::runtime_error(std::string("NitroStorage: Biometric set failed: ") + e.what());
@@ -434,6 +530,7 @@ void HybridStorage::deleteSecureBiometric(const std::string& key) {
434
530
  ensureAdapter();
435
531
  try {
436
532
  nativeAdapter_->deleteSecureBiometric(key);
533
+ onKeyRemove(static_cast<int>(Scope::Secure), key);
437
534
  notifyListeners(static_cast<int>(Scope::Secure), key, std::nullopt);
438
535
  } catch (const std::exception& e) {
439
536
  throw std::runtime_error(std::string("NitroStorage: Biometric delete failed: ") + e.what());
@@ -449,6 +546,7 @@ void HybridStorage::clearSecureBiometric() {
449
546
  ensureAdapter();
450
547
  try {
451
548
  nativeAdapter_->clearSecureBiometric();
549
+ onScopeClear(static_cast<int>(Scope::Secure));
452
550
  notifyListeners(static_cast<int>(Scope::Secure), "", std::nullopt);
453
551
  } catch (const std::exception& e) {
454
552
  throw std::runtime_error(std::string("NitroStorage: Biometric clear failed: ") + e.what());
@@ -457,11 +555,7 @@ void HybridStorage::clearSecureBiometric() {
457
555
 
458
556
  // --- Internal ---
459
557
 
460
- void HybridStorage::notifyListeners(
461
- int scope,
462
- const std::string& key,
463
- const std::optional<std::string>& value
464
- ) {
558
+ std::vector<HybridStorage::Listener> HybridStorage::copyListenersForScope(int scope) {
465
559
  std::vector<Listener> listenersCopy;
466
560
  {
467
561
  std::lock_guard<std::mutex> lock(listenersMutex_);
@@ -471,8 +565,15 @@ void HybridStorage::notifyListeners(
471
565
  listenersCopy = it->second;
472
566
  }
473
567
  }
474
-
475
- for (const auto& listener : listenersCopy) {
568
+ return listenersCopy;
569
+ }
570
+
571
+ void HybridStorage::notifyListeners(
572
+ const std::vector<Listener>& listeners,
573
+ const std::string& key,
574
+ const std::optional<std::string>& value
575
+ ) {
576
+ for (const auto& listener : listeners) {
476
577
  try {
477
578
  listener.callback(key, value);
478
579
  } catch (...) {
@@ -481,6 +582,96 @@ void HybridStorage::notifyListeners(
481
582
  }
482
583
  }
483
584
 
585
+ void HybridStorage::notifyListeners(
586
+ int scope,
587
+ const std::string& key,
588
+ const std::optional<std::string>& value
589
+ ) {
590
+ const auto listeners = copyListenersForScope(scope);
591
+ notifyListeners(listeners, key, value);
592
+ }
593
+
594
+ std::vector<std::string> HybridStorage::toVector(const std::unordered_set<std::string>& keys) {
595
+ std::vector<std::string> values;
596
+ values.reserve(keys.size());
597
+ for (const auto& key : keys) {
598
+ values.push_back(key);
599
+ }
600
+ return values;
601
+ }
602
+
603
+ void HybridStorage::ensureKeyIndexHydrated(int scope) {
604
+ if (scope != static_cast<int>(Scope::Disk) && scope != static_cast<int>(Scope::Secure)) {
605
+ return;
606
+ }
607
+
608
+ {
609
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
610
+ auto hydratedIt = keyIndexHydrated_.find(scope);
611
+ if (hydratedIt != keyIndexHydrated_.end() && hydratedIt->second) {
612
+ return;
613
+ }
614
+ }
615
+
616
+ ensureAdapter();
617
+ std::vector<std::string> keys;
618
+ try {
619
+ if (scope == static_cast<int>(Scope::Disk)) {
620
+ keys = nativeAdapter_->getAllKeysDisk();
621
+ } else {
622
+ keys = nativeAdapter_->getAllKeysSecure();
623
+ }
624
+ } catch (const std::exception& e) {
625
+ throw std::runtime_error(std::string("NitroStorage: Key index hydration failed: ") + e.what());
626
+ } catch (...) {
627
+ throw std::runtime_error("NitroStorage: Key index hydration failed");
628
+ }
629
+
630
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
631
+ auto& index = keyIndex_[scope];
632
+ index.clear();
633
+ for (const auto& key : keys) {
634
+ index.insert(key);
635
+ }
636
+ keyIndexHydrated_[scope] = true;
637
+ }
638
+
639
+ void HybridStorage::onKeySet(int scope, const std::string& key) {
640
+ if (scope != static_cast<int>(Scope::Disk) && scope != static_cast<int>(Scope::Secure)) {
641
+ return;
642
+ }
643
+
644
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
645
+ auto hydratedIt = keyIndexHydrated_.find(scope);
646
+ if (hydratedIt != keyIndexHydrated_.end() && hydratedIt->second) {
647
+ keyIndex_[scope].insert(key);
648
+ }
649
+ }
650
+
651
+ void HybridStorage::onKeyRemove(int scope, const std::string& key) {
652
+ if (scope != static_cast<int>(Scope::Disk) && scope != static_cast<int>(Scope::Secure)) {
653
+ return;
654
+ }
655
+
656
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
657
+ auto hydratedIt = keyIndexHydrated_.find(scope);
658
+ if (hydratedIt != keyIndexHydrated_.end() && hydratedIt->second) {
659
+ keyIndex_[scope].erase(key);
660
+ }
661
+ }
662
+
663
+ void HybridStorage::onScopeClear(int scope) {
664
+ if (scope != static_cast<int>(Scope::Disk) && scope != static_cast<int>(Scope::Secure)) {
665
+ return;
666
+ }
667
+
668
+ std::lock_guard<std::mutex> lock(keyIndexMutex_);
669
+ auto hydratedIt = keyIndexHydrated_.find(scope);
670
+ if (hydratedIt != keyIndexHydrated_.end() && hydratedIt->second) {
671
+ keyIndex_[scope].clear();
672
+ }
673
+ }
674
+
484
675
  void HybridStorage::ensureAdapter() const {
485
676
  if (!nativeAdapter_) {
486
677
  throw std::runtime_error("NitroStorage: Native adapter not initialized");
@@ -3,13 +3,23 @@
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>
9
10
  #include <vector>
11
+ #include <unordered_set>
10
12
 
11
13
  namespace margelo::nitro::NitroStorage {
12
14
 
15
+ #ifdef NITRO_STORAGE_USE_ORDERED_MAP_FOR_TESTS
16
+ template <typename Key, typename Value>
17
+ using HybridStorageMap = std::map<Key, Value>;
18
+ #else
19
+ template <typename Key, typename Value>
20
+ using HybridStorageMap = std::unordered_map<Key, Value>;
21
+ #endif
22
+
13
23
  class HybridStorage : public HybridStorageSpec {
14
24
  public:
15
25
  HybridStorage();
@@ -22,17 +32,21 @@ public:
22
32
  void clear(double scope) override;
23
33
  bool has(const std::string& key, double scope) override;
24
34
  std::vector<std::string> getAllKeys(double scope) override;
35
+ std::vector<std::string> getKeysByPrefix(const std::string& prefix, double scope) override;
25
36
  double size(double scope) override;
26
37
  void setBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values, double scope) override;
27
38
  std::vector<std::string> getBatch(const std::vector<std::string>& keys, double scope) override;
28
39
  void removeBatch(const std::vector<std::string>& keys, double scope) override;
40
+ void removeByPrefix(const std::string& prefix, double scope) override;
29
41
  std::function<void()> addOnChange(
30
42
  double scope,
31
43
  const std::function<void(const std::string&, const std::optional<std::string>&)>& callback
32
44
  ) override;
33
45
  void setSecureAccessControl(double level) override;
46
+ void setSecureWritesAsync(bool enabled) override;
34
47
  void setKeychainAccessGroup(const std::string& group) override;
35
48
  void setSecureBiometric(const std::string& key, const std::string& value) override;
49
+ void setSecureBiometricWithLevel(const std::string& key, const std::string& value, double level) override;
36
50
  std::optional<std::string> getSecureBiometric(const std::string& key) override;
37
51
  void deleteSecureBiometric(const std::string& key) override;
38
52
  bool hasSecureBiometric(const std::string& key) override;
@@ -50,16 +64,30 @@ private:
50
64
  std::function<void(const std::string&, const std::optional<std::string>&)> callback;
51
65
  };
52
66
 
53
- std::unordered_map<std::string, std::string> memoryStore_;
67
+ HybridStorageMap<std::string, std::string> memoryStore_;
54
68
  std::mutex memoryMutex_;
55
69
 
56
70
  std::shared_ptr<::NitroStorage::NativeStorageAdapter> nativeAdapter_;
57
-
58
- std::unordered_map<int, std::vector<Listener>> listeners_;
71
+
72
+ HybridStorageMap<int, std::vector<Listener>> listeners_;
59
73
  std::mutex listenersMutex_;
60
74
  size_t nextListenerId_ = 0;
75
+ HybridStorageMap<int, std::unordered_set<std::string>> keyIndex_;
76
+ HybridStorageMap<int, bool> keyIndexHydrated_;
77
+ std::mutex keyIndexMutex_;
61
78
 
79
+ std::vector<Listener> copyListenersForScope(int scope);
80
+ void notifyListeners(
81
+ const std::vector<Listener>& listeners,
82
+ const std::string& key,
83
+ const std::optional<std::string>& value
84
+ );
62
85
  void notifyListeners(int scope, const std::string& key, const std::optional<std::string>& value);
86
+ std::vector<std::string> toVector(const std::unordered_set<std::string>& keys);
87
+ void ensureKeyIndexHydrated(int scope);
88
+ void onKeySet(int scope, const std::string& key);
89
+ void onKeyRemove(int scope, const std::string& key);
90
+ void onScopeClear(int scope);
63
91
  void ensureAdapter() const;
64
92
  Scope toScope(double scopeValue);
65
93
  };
@@ -15,6 +15,7 @@ public:
15
15
  virtual void deleteDisk(const std::string& key) = 0;
16
16
  virtual bool hasDisk(const std::string& key) = 0;
17
17
  virtual std::vector<std::string> getAllKeysDisk() = 0;
18
+ virtual std::vector<std::string> getKeysByPrefixDisk(const std::string& prefix) = 0;
18
19
  virtual size_t sizeDisk() = 0;
19
20
  virtual void setDiskBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) = 0;
20
21
  virtual std::vector<std::optional<std::string>> getDiskBatch(const std::vector<std::string>& keys) = 0;
@@ -25,6 +26,7 @@ public:
25
26
  virtual void deleteSecure(const std::string& key) = 0;
26
27
  virtual bool hasSecure(const std::string& key) = 0;
27
28
  virtual std::vector<std::string> getAllKeysSecure() = 0;
29
+ virtual std::vector<std::string> getKeysByPrefixSecure(const std::string& prefix) = 0;
28
30
  virtual size_t sizeSecure() = 0;
29
31
  virtual void setSecureBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) = 0;
30
32
  virtual std::vector<std::optional<std::string>> getSecureBatch(const std::vector<std::string>& keys) = 0;
@@ -34,9 +36,11 @@ public:
34
36
  virtual void clearSecure() = 0;
35
37
 
36
38
  virtual void setSecureAccessControl(int level) = 0;
39
+ virtual void setSecureWritesAsync(bool enabled) = 0;
37
40
  virtual void setKeychainAccessGroup(const std::string& group) = 0;
38
41
 
39
42
  virtual void setSecureBiometric(const std::string& key, const std::string& value) = 0;
43
+ virtual void setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) = 0;
40
44
  virtual std::optional<std::string> getSecureBiometric(const std::string& key) = 0;
41
45
  virtual void deleteSecureBiometric(const std::string& key) = 0;
42
46
  virtual bool hasSecureBiometric(const std::string& key) = 0;