react-native-nitro-storage 0.4.0 → 0.4.3

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 (41) hide show
  1. package/README.md +90 -0
  2. package/android/build.gradle +0 -12
  3. package/android/consumer-rules.pro +26 -4
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +7 -10
  5. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +0 -4
  6. package/android/src/main/cpp/cpp-adapter.cpp +3 -1
  7. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +172 -77
  8. package/cpp/bindings/HybridStorage.cpp +120 -69
  9. package/cpp/bindings/HybridStorage.hpp +4 -0
  10. package/ios/IOSStorageAdapterCpp.hpp +2 -1
  11. package/ios/IOSStorageAdapterCpp.mm +264 -49
  12. package/lib/commonjs/index.js +128 -20
  13. package/lib/commonjs/index.js.map +1 -1
  14. package/lib/commonjs/index.web.js +169 -41
  15. package/lib/commonjs/index.web.js.map +1 -1
  16. package/lib/commonjs/indexeddb-backend.js +130 -0
  17. package/lib/commonjs/indexeddb-backend.js.map +1 -0
  18. package/lib/commonjs/internal.js +51 -23
  19. package/lib/commonjs/internal.js.map +1 -1
  20. package/lib/module/index.js +121 -20
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/index.web.js +162 -41
  23. package/lib/module/index.web.js.map +1 -1
  24. package/lib/module/indexeddb-backend.js +126 -0
  25. package/lib/module/indexeddb-backend.js.map +1 -0
  26. package/lib/module/internal.js +51 -23
  27. package/lib/module/internal.js.map +1 -1
  28. package/lib/typescript/index.d.ts +6 -0
  29. package/lib/typescript/index.d.ts.map +1 -1
  30. package/lib/typescript/index.web.d.ts +7 -1
  31. package/lib/typescript/index.web.d.ts.map +1 -1
  32. package/lib/typescript/indexeddb-backend.d.ts +29 -0
  33. package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
  34. package/lib/typescript/internal.d.ts.map +1 -1
  35. package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
  36. package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
  37. package/package.json +7 -3
  38. package/src/index.ts +137 -27
  39. package/src/index.web.ts +182 -49
  40. package/src/indexeddb-backend.ts +143 -0
  41. package/src/internal.ts +51 -23
package/src/index.web.ts CHANGED
@@ -163,10 +163,12 @@ const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
163
163
  const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
164
164
  const pendingSecureWrites = new Map<string, PendingSecureWrite>();
165
165
  let secureFlushScheduled = false;
166
+ let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
166
167
  const SECURE_WEB_PREFIX = "__secure_";
167
168
  const BIOMETRIC_WEB_PREFIX = "__bio_";
168
169
  let hasWarnedAboutWebBiometricFallback = false;
169
170
  let hasWebStorageEventSubscription = false;
171
+ let webStorageSubscriberCount = 0;
170
172
  let metricsObserver: StorageMetricsObserver | undefined;
171
173
  const metricsCounters = new Map<
172
174
  string,
@@ -200,6 +202,9 @@ function measureOperation<T>(
200
202
  fn: () => T,
201
203
  keysCount = 1,
202
204
  ): T {
205
+ if (!metricsObserver) {
206
+ return fn();
207
+ }
203
208
  const start = Date.now();
204
209
  try {
205
210
  return fn();
@@ -232,6 +237,9 @@ function createLocalStorageWebSecureBackend(): WebSecureStorageBackend {
232
237
  let webSecureStorageBackend: WebSecureStorageBackend | undefined =
233
238
  createLocalStorageWebSecureBackend();
234
239
 
240
+ let cachedSecureBrowserStorage: BrowserStorageLike | undefined;
241
+ let cachedSecureBackendRef: WebSecureStorageBackend | undefined;
242
+
235
243
  function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
236
244
  if (scope === StorageScope.Disk) {
237
245
  return globalThis.localStorage;
@@ -240,16 +248,24 @@ function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
240
248
  if (!webSecureStorageBackend) {
241
249
  return undefined;
242
250
  }
243
- return {
244
- setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
245
- getItem: (key) => webSecureStorageBackend?.getItem(key) ?? null,
246
- removeItem: (key) => webSecureStorageBackend?.removeItem(key),
247
- clear: () => webSecureStorageBackend?.clear(),
248
- key: (index) => webSecureStorageBackend?.getAllKeys()[index] ?? null,
251
+ if (
252
+ cachedSecureBackendRef === webSecureStorageBackend &&
253
+ cachedSecureBrowserStorage
254
+ ) {
255
+ return cachedSecureBrowserStorage;
256
+ }
257
+ cachedSecureBackendRef = webSecureStorageBackend;
258
+ cachedSecureBrowserStorage = {
259
+ setItem: (key, value) => webSecureStorageBackend!.setItem(key, value),
260
+ getItem: (key) => webSecureStorageBackend!.getItem(key) ?? null,
261
+ removeItem: (key) => webSecureStorageBackend!.removeItem(key),
262
+ clear: () => webSecureStorageBackend!.clear(),
263
+ key: (index) => webSecureStorageBackend!.getAllKeys()[index] ?? null,
249
264
  get length() {
250
- return webSecureStorageBackend?.getAllKeys().length ?? 0;
265
+ return webSecureStorageBackend!.getAllKeys().length;
251
266
  },
252
267
  };
268
+ return cachedSecureBrowserStorage;
253
269
  }
254
270
  return undefined;
255
271
  }
@@ -370,17 +386,27 @@ function handleWebStorageEvent(event: StorageEvent): void {
370
386
  }
371
387
 
372
388
  function ensureWebStorageEventSubscription(): void {
373
- if (hasWebStorageEventSubscription) {
374
- return;
389
+ webStorageSubscriberCount += 1;
390
+ if (
391
+ webStorageSubscriberCount === 1 &&
392
+ typeof window !== "undefined" &&
393
+ typeof window.addEventListener === "function"
394
+ ) {
395
+ window.addEventListener("storage", handleWebStorageEvent);
396
+ hasWebStorageEventSubscription = true;
375
397
  }
398
+ }
399
+
400
+ function maybeCleanupWebStorageSubscription(): void {
401
+ webStorageSubscriberCount = Math.max(0, webStorageSubscriberCount - 1);
376
402
  if (
377
- typeof window === "undefined" ||
378
- typeof window.addEventListener !== "function"
403
+ webStorageSubscriberCount === 0 &&
404
+ hasWebStorageEventSubscription &&
405
+ typeof window !== "undefined"
379
406
  ) {
380
- return;
407
+ window.removeEventListener("storage", handleWebStorageEvent);
408
+ hasWebStorageEventSubscription = false;
381
409
  }
382
- window.addEventListener("storage", handleWebStorageEvent);
383
- hasWebStorageEventSubscription = true;
384
410
  }
385
411
 
386
412
  function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
@@ -417,13 +443,20 @@ function clearScopeRawCache(scope: NonMemoryScope): void {
417
443
  }
418
444
 
419
445
  function notifyKeyListeners(registry: KeyListenerRegistry, key: string): void {
420
- registry.get(key)?.forEach((listener) => listener());
446
+ const listeners = registry.get(key);
447
+ if (listeners) {
448
+ for (const listener of listeners) {
449
+ listener();
450
+ }
451
+ }
421
452
  }
422
453
 
423
454
  function notifyAllListeners(registry: KeyListenerRegistry): void {
424
- registry.forEach((listeners) => {
425
- listeners.forEach((listener) => listener());
426
- });
455
+ for (const listeners of registry.values()) {
456
+ for (const listener of listeners) {
457
+ listener();
458
+ }
459
+ }
427
460
  }
428
461
 
429
462
  function addKeyListener(
@@ -482,7 +515,7 @@ function flushSecureWrites(): void {
482
515
  if (value === undefined) {
483
516
  keysToRemove.push(key);
484
517
  } else {
485
- const resolvedAccessControl = accessControl ?? AccessControl.WhenUnlocked;
518
+ const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
486
519
  const existingGroup = groupedSetWrites.get(resolvedAccessControl);
487
520
  const group = existingGroup ?? { keys: [], values: [] };
488
521
  group.keys.push(key);
@@ -882,7 +915,12 @@ export const storage = {
882
915
  if (scope === StorageScope.Secure) {
883
916
  flushSecureWrites();
884
917
  }
885
- clearScopeRawCache(scope);
918
+ const scopeCache = getScopeRawCache(scope);
919
+ for (const key of scopeCache.keys()) {
920
+ if (isNamespaced(key, namespace)) {
921
+ scopeCache.delete(key);
922
+ }
923
+ }
886
924
  WebStorage.removeByPrefix(keyPrefix, scope);
887
925
  });
888
926
  },
@@ -958,9 +996,13 @@ export const storage = {
958
996
  return result;
959
997
  }
960
998
  const keys = WebStorage.getAllKeys(scope);
961
- keys.forEach((key) => {
962
- const val = WebStorage.get(key, scope);
963
- if (val !== undefined) result[key] = val;
999
+ if (keys.length === 0) return {};
1000
+ const values = WebStorage.getBatch(keys, scope);
1001
+ keys.forEach((key, index) => {
1002
+ const val = values[index];
1003
+ if (val !== undefined && val !== null) {
1004
+ result[key] = val;
1005
+ }
964
1006
  });
965
1007
  return result;
966
1008
  });
@@ -972,7 +1014,8 @@ export const storage = {
972
1014
  return WebStorage.size(scope);
973
1015
  });
974
1016
  },
975
- setAccessControl: (_level: AccessControl) => {
1017
+ setAccessControl: (level: AccessControl) => {
1018
+ secureDefaultAccessControl = level;
976
1019
  recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
977
1020
  },
978
1021
  setSecureWritesAsync: (_enabled: boolean) => {
@@ -1005,12 +1048,58 @@ export const storage = {
1005
1048
  resetMetrics: () => {
1006
1049
  metricsCounters.clear();
1007
1050
  },
1051
+ getString: (key: string, scope: StorageScope): string | undefined => {
1052
+ return measureOperation("storage:getString", scope, () => {
1053
+ return getRawValue(key, scope);
1054
+ });
1055
+ },
1056
+ setString: (key: string, value: string, scope: StorageScope): void => {
1057
+ measureOperation("storage:setString", scope, () => {
1058
+ setRawValue(key, value, scope);
1059
+ });
1060
+ },
1061
+ deleteString: (key: string, scope: StorageScope): void => {
1062
+ measureOperation("storage:deleteString", scope, () => {
1063
+ removeRawValue(key, scope);
1064
+ });
1065
+ },
1066
+ import: (data: Record<string, string>, scope: StorageScope): void => {
1067
+ const keys = Object.keys(data);
1068
+ measureOperation(
1069
+ "storage:import",
1070
+ scope,
1071
+ () => {
1072
+ assertValidScope(scope);
1073
+ if (keys.length === 0) return;
1074
+ const values = keys.map((k) => data[k]!);
1075
+
1076
+ if (scope === StorageScope.Memory) {
1077
+ keys.forEach((key, index) => {
1078
+ memoryStore.set(key, values[index]);
1079
+ });
1080
+ keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1081
+ return;
1082
+ }
1083
+
1084
+ if (scope === StorageScope.Secure) {
1085
+ flushSecureWrites();
1086
+ WebStorage.setSecureAccessControl(secureDefaultAccessControl);
1087
+ }
1088
+
1089
+ WebStorage.setBatch(keys, values, scope);
1090
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1091
+ },
1092
+ keys.length,
1093
+ );
1094
+ },
1008
1095
  };
1009
1096
 
1010
1097
  export function setWebSecureStorageBackend(
1011
1098
  backend?: WebSecureStorageBackend,
1012
1099
  ): void {
1013
1100
  webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
1101
+ cachedSecureBrowserStorage = undefined;
1102
+ cachedSecureBackendRef = undefined;
1014
1103
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
1015
1104
  clearScopeRawCache(StorageScope.Secure);
1016
1105
  }
@@ -1058,6 +1147,7 @@ export interface StorageItem<T> {
1058
1147
 
1059
1148
  type StorageItemInternal<T> = StorageItem<T> & {
1060
1149
  _triggerListeners: () => void;
1150
+ _invalidateParsedCacheOnly: () => void;
1061
1151
  _hasValidation: boolean;
1062
1152
  _hasExpiration: boolean;
1063
1153
  _readCacheEnabled: boolean;
@@ -1183,17 +1273,18 @@ export function createStorageItem<T = undefined>(
1183
1273
  return memoryStore.get(storageKey);
1184
1274
  }
1185
1275
 
1186
- if (
1187
- nonMemoryScope === StorageScope.Secure &&
1188
- !isBiometric &&
1189
- hasPendingSecureWrite(storageKey)
1190
- ) {
1191
- return readPendingSecureWrite(storageKey);
1276
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1277
+ const pending = pendingSecureWrites.get(storageKey);
1278
+ if (pending !== undefined) {
1279
+ return pending.value;
1280
+ }
1192
1281
  }
1193
1282
 
1194
1283
  if (readCache) {
1195
- if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
1196
- return readCachedRawValue(nonMemoryScope!, storageKey);
1284
+ const cache = getScopeRawCache(nonMemoryScope!);
1285
+ const cached = cache.get(storageKey);
1286
+ if (cached !== undefined || cache.has(storageKey)) {
1287
+ return cached;
1197
1288
  }
1198
1289
  }
1199
1290
 
@@ -1222,7 +1313,7 @@ export function createStorageItem<T = undefined>(
1222
1313
  scheduleSecureWrite(
1223
1314
  storageKey,
1224
1315
  rawValue,
1225
- secureAccessControl ?? AccessControl.WhenUnlocked,
1316
+ secureAccessControl ?? secureDefaultAccessControl,
1226
1317
  );
1227
1318
  return;
1228
1319
  }
@@ -1246,7 +1337,7 @@ export function createStorageItem<T = undefined>(
1246
1337
  scheduleSecureWrite(
1247
1338
  storageKey,
1248
1339
  undefined,
1249
- secureAccessControl ?? AccessControl.WhenUnlocked,
1340
+ secureAccessControl ?? secureDefaultAccessControl,
1250
1341
  );
1251
1342
  return;
1252
1343
  }
@@ -1326,6 +1417,7 @@ export function createStorageItem<T = undefined>(
1326
1417
  onExpired?.(storageKey);
1327
1418
  lastValue = ensureValidatedValue(defaultValue, false);
1328
1419
  hasLastValue = true;
1420
+ listeners.forEach((cb) => cb());
1329
1421
  return lastValue;
1330
1422
  }
1331
1423
  }
@@ -1367,6 +1459,7 @@ export function createStorageItem<T = undefined>(
1367
1459
  onExpired?.(storageKey);
1368
1460
  lastValue = ensureValidatedValue(defaultValue, false);
1369
1461
  hasLastValue = true;
1462
+ listeners.forEach((cb) => cb());
1370
1463
  return lastValue;
1371
1464
  }
1372
1465
 
@@ -1405,14 +1498,13 @@ export function createStorageItem<T = undefined>(
1405
1498
  ? valueOrFn(getInternal())
1406
1499
  : valueOrFn;
1407
1500
 
1408
- invalidateParsedCache();
1409
-
1410
1501
  if (validate && !validate(newValue)) {
1411
1502
  throw new Error(
1412
1503
  `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
1413
1504
  );
1414
1505
  }
1415
1506
 
1507
+ invalidateParsedCache();
1416
1508
  writeValueWithoutValidation(newValue);
1417
1509
  });
1418
1510
  };
@@ -1462,6 +1554,9 @@ export function createStorageItem<T = undefined>(
1462
1554
  if (listeners.size === 0 && unsubscribe) {
1463
1555
  unsubscribe();
1464
1556
  unsubscribe = null;
1557
+ if (!isMemory) {
1558
+ maybeCleanupWebStorageSubscription();
1559
+ }
1465
1560
  }
1466
1561
  };
1467
1562
  };
@@ -1480,6 +1575,9 @@ export function createStorageItem<T = undefined>(
1480
1575
  invalidateParsedCache();
1481
1576
  listeners.forEach((listener) => listener());
1482
1577
  },
1578
+ _invalidateParsedCacheOnly: () => {
1579
+ invalidateParsedCache();
1580
+ },
1483
1581
  _hasValidation: validate !== undefined,
1484
1582
  _hasExpiration: expiration !== undefined,
1485
1583
  _readCacheEnabled: readCache,
@@ -1496,6 +1594,7 @@ export function createStorageItem<T = undefined>(
1496
1594
  }
1497
1595
 
1498
1596
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
1597
+ export { createIndexedDBBackend } from "./indexeddb-backend";
1499
1598
 
1500
1599
  type BatchReadItem<T> = Pick<
1501
1600
  StorageItem<T>,
@@ -1544,15 +1643,18 @@ export function getBatch(
1544
1643
 
1545
1644
  items.forEach((item, index) => {
1546
1645
  if (scope === StorageScope.Secure) {
1547
- if (hasPendingSecureWrite(item.key)) {
1548
- rawValues[index] = readPendingSecureWrite(item.key);
1646
+ const pending = pendingSecureWrites.get(item.key);
1647
+ if (pending !== undefined) {
1648
+ rawValues[index] = pending.value;
1549
1649
  return;
1550
1650
  }
1551
1651
  }
1552
1652
 
1553
1653
  if (item._readCacheEnabled === true) {
1554
- if (hasCachedRawValue(scope, item.key)) {
1555
- rawValues[index] = readCachedRawValue(scope, item.key);
1654
+ const cache = getScopeRawCache(scope);
1655
+ const cached = cache.get(item.key);
1656
+ if (cached !== undefined || cache.has(item.key)) {
1657
+ rawValues[index] = cached;
1556
1658
  return;
1557
1659
  }
1558
1660
  }
@@ -1600,7 +1702,25 @@ export function setBatch<T>(
1600
1702
  );
1601
1703
 
1602
1704
  if (scope === StorageScope.Memory) {
1603
- items.forEach(({ item, value }) => item.set(value));
1705
+ // Determine if any item needs per-item handling (validation or TTL)
1706
+ const needsIndividualSets = items.some(({ item }) => {
1707
+ const internal = asInternal(item as StorageItem<unknown>);
1708
+ return internal._hasValidation || internal._hasExpiration;
1709
+ });
1710
+
1711
+ if (needsIndividualSets) {
1712
+ items.forEach(({ item, value }) => item.set(value));
1713
+ return;
1714
+ }
1715
+
1716
+ // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1717
+ items.forEach(({ item, value }) => {
1718
+ memoryStore.set(item.key, value);
1719
+ asInternal(item as StorageItem<unknown>)._invalidateParsedCacheOnly();
1720
+ });
1721
+ items.forEach(({ item }) =>
1722
+ notifyKeyListeners(memoryListeners, item.key),
1723
+ );
1604
1724
  return;
1605
1725
  }
1606
1726
 
@@ -1626,7 +1746,7 @@ export function setBatch<T>(
1626
1746
 
1627
1747
  secureEntries.forEach(({ item, value, internal }) => {
1628
1748
  const accessControl =
1629
- internal._secureAccessControl ?? AccessControl.WhenUnlocked;
1749
+ internal._secureAccessControl ?? secureDefaultAccessControl;
1630
1750
  const existingGroup = groupedByAccessControl.get(accessControl);
1631
1751
  const group = existingGroup ?? { keys: [], values: [] };
1632
1752
  group.keys.push(item.key);
@@ -1725,10 +1845,13 @@ export function migrateToLatest(
1725
1845
  return;
1726
1846
  }
1727
1847
  migration(context);
1728
- writeMigrationVersion(scope, version);
1729
1848
  appliedVersion = version;
1730
1849
  });
1731
1850
 
1851
+ if (appliedVersion !== currentVersion) {
1852
+ writeMigrationVersion(scope, appliedVersion);
1853
+ }
1854
+
1732
1855
  return appliedVersion;
1733
1856
  });
1734
1857
  }
@@ -1743,13 +1866,18 @@ export function runTransaction<T>(
1743
1866
  flushSecureWrites();
1744
1867
  }
1745
1868
 
1746
- const rollback = new Map<string, string | undefined>();
1869
+ const NOT_SET = Symbol();
1870
+ const rollback = new Map<string, unknown>();
1747
1871
 
1748
1872
  const rememberRollback = (key: string) => {
1749
1873
  if (rollback.has(key)) {
1750
1874
  return;
1751
1875
  }
1752
- rollback.set(key, getRawValue(key, scope));
1876
+ if (scope === StorageScope.Memory) {
1877
+ rollback.set(key, memoryStore.has(key) ? memoryStore.get(key) : NOT_SET);
1878
+ } else {
1879
+ rollback.set(key, getRawValue(key, scope));
1880
+ }
1753
1881
  };
1754
1882
 
1755
1883
  const tx: TransactionContext = {
@@ -1785,11 +1913,12 @@ export function runTransaction<T>(
1785
1913
  const rollbackEntries = Array.from(rollback.entries()).reverse();
1786
1914
  if (scope === StorageScope.Memory) {
1787
1915
  rollbackEntries.forEach(([key, previousValue]) => {
1788
- if (previousValue === undefined) {
1789
- removeRawValue(key, scope);
1916
+ if (previousValue === NOT_SET) {
1917
+ memoryStore.delete(key);
1790
1918
  } else {
1791
- setRawValue(key, previousValue, scope);
1919
+ memoryStore.set(key, previousValue);
1792
1920
  }
1921
+ notifyKeyListeners(memoryListeners, key);
1793
1922
  });
1794
1923
  } else {
1795
1924
  const keysToSet: string[] = [];
@@ -1801,7 +1930,7 @@ export function runTransaction<T>(
1801
1930
  keysToRemove.push(key);
1802
1931
  } else {
1803
1932
  keysToSet.push(key);
1804
- valuesToSet.push(previousValue);
1933
+ valuesToSet.push(previousValue as string);
1805
1934
  }
1806
1935
  });
1807
1936
 
@@ -1867,3 +1996,7 @@ export function createSecureAuthStorage<K extends string>(
1867
1996
 
1868
1997
  return result as Record<K, StorageItem<string>>;
1869
1998
  }
1999
+
2000
+ export function isKeychainLockedError(_err: unknown): boolean {
2001
+ return false;
2002
+ }
@@ -0,0 +1,143 @@
1
+ import type { WebSecureStorageBackend } from "./index.web";
2
+
3
+ const DEFAULT_DB_NAME = "nitro-storage-secure";
4
+ const DEFAULT_STORE_NAME = "keyvalue";
5
+ const DB_VERSION = 1;
6
+
7
+ /**
8
+ * Opens (or creates) an IndexedDB database and returns the underlying IDBDatabase.
9
+ * Rejects if IndexedDB is unavailable in the current environment.
10
+ */
11
+ function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
12
+ return new Promise((resolve, reject) => {
13
+ if (typeof indexedDB === "undefined") {
14
+ reject(new Error("IndexedDB is not available in this environment."));
15
+ return;
16
+ }
17
+
18
+ const request = indexedDB.open(dbName, DB_VERSION);
19
+
20
+ request.onupgradeneeded = () => {
21
+ const db = request.result;
22
+ if (!db.objectStoreNames.contains(storeName)) {
23
+ db.createObjectStore(storeName);
24
+ }
25
+ };
26
+
27
+ request.onsuccess = () => resolve(request.result);
28
+ request.onerror = () =>
29
+ reject(request.error ?? new Error("Failed to open IndexedDB database."));
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Creates a `WebSecureStorageBackend` backed by IndexedDB.
35
+ *
36
+ * IndexedDB is async, but `WebSecureStorageBackend` requires a synchronous
37
+ * interface. This implementation bridges the gap with a write-through in-memory
38
+ * cache:
39
+ *
40
+ * - **Reads** are always served from the in-memory cache (synchronous, O(1)).
41
+ * - **Writes** update the cache synchronously, then persist to IndexedDB
42
+ * asynchronously in the background.
43
+ * - **Initialisation**: the returned backend pre-loads all persisted entries
44
+ * from IndexedDB into memory before resolving, so the first synchronous read
45
+ * after `await createIndexedDBBackend()` already returns the correct value.
46
+ *
47
+ * @param dbName Name of the IndexedDB database. Defaults to `"nitro-storage-secure"`.
48
+ * @param storeName Name of the object store inside the database. Defaults to `"keyvalue"`.
49
+ *
50
+ * @example
51
+ * ```ts
52
+ * import { setWebSecureStorageBackend } from "react-native-nitro-storage";
53
+ * import { createIndexedDBBackend } from "react-native-nitro-storage/indexeddb-backend";
54
+ *
55
+ * const backend = await createIndexedDBBackend();
56
+ * setWebSecureStorageBackend(backend);
57
+ * ```
58
+ */
59
+ export async function createIndexedDBBackend(
60
+ dbName = DEFAULT_DB_NAME,
61
+ storeName = DEFAULT_STORE_NAME,
62
+ ): Promise<WebSecureStorageBackend> {
63
+ const db = await openDB(dbName, storeName);
64
+ const cache = new Map<string, string>();
65
+
66
+ // Hydrate the in-memory cache from IndexedDB.
67
+ await new Promise<void>((resolve, reject) => {
68
+ const tx = db.transaction(storeName, "readonly");
69
+ const store = tx.objectStore(storeName);
70
+ const request = store.openCursor();
71
+
72
+ request.onsuccess = () => {
73
+ const cursor = request.result;
74
+ if (cursor) {
75
+ const value = cursor.value as unknown;
76
+ if (typeof value === "string") {
77
+ cache.set(String(cursor.key), value);
78
+ }
79
+ cursor.continue();
80
+ }
81
+ };
82
+
83
+ tx.oncomplete = () => resolve();
84
+ tx.onerror = () =>
85
+ reject(tx.error ?? new Error("Failed to load IndexedDB entries."));
86
+ });
87
+
88
+ /** Fire-and-forget IndexedDB write. Errors are silently ignored to avoid
89
+ * breaking the synchronous caller — the in-memory cache is always authoritative. */
90
+ function persistSet(key: string, value: string): void {
91
+ try {
92
+ const tx = db.transaction(storeName, "readwrite");
93
+ tx.objectStore(storeName).put(value, key);
94
+ } catch {
95
+ // Best-effort; cache is the source of truth.
96
+ }
97
+ }
98
+
99
+ function persistDelete(key: string): void {
100
+ try {
101
+ const tx = db.transaction(storeName, "readwrite");
102
+ tx.objectStore(storeName).delete(key);
103
+ } catch {
104
+ // Best-effort.
105
+ }
106
+ }
107
+
108
+ function persistClear(): void {
109
+ try {
110
+ const tx = db.transaction(storeName, "readwrite");
111
+ tx.objectStore(storeName).clear();
112
+ } catch {
113
+ // Best-effort.
114
+ }
115
+ }
116
+
117
+ const backend: WebSecureStorageBackend = {
118
+ getItem(key: string): string | null {
119
+ return cache.get(key) ?? null;
120
+ },
121
+
122
+ setItem(key: string, value: string): void {
123
+ cache.set(key, value);
124
+ persistSet(key, value);
125
+ },
126
+
127
+ removeItem(key: string): void {
128
+ cache.delete(key);
129
+ persistDelete(key);
130
+ },
131
+
132
+ clear(): void {
133
+ cache.clear();
134
+ persistClear();
135
+ },
136
+
137
+ getAllKeys(): string[] {
138
+ return Array.from(cache.keys());
139
+ },
140
+ };
141
+
142
+ return backend;
143
+ }