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
@@ -25,10 +25,12 @@ const webScopeKeyIndex = new Map([[StorageScope.Disk, new Set()], [StorageScope.
25
25
  const hydratedWebScopeKeyIndex = new Set();
26
26
  const pendingSecureWrites = new Map();
27
27
  let secureFlushScheduled = false;
28
+ let secureDefaultAccessControl = AccessControl.WhenUnlocked;
28
29
  const SECURE_WEB_PREFIX = "__secure_";
29
30
  const BIOMETRIC_WEB_PREFIX = "__bio_";
30
31
  let hasWarnedAboutWebBiometricFallback = false;
31
32
  let hasWebStorageEventSubscription = false;
33
+ let webStorageSubscriberCount = 0;
32
34
  let metricsObserver;
33
35
  const metricsCounters = new Map();
34
36
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
@@ -52,6 +54,9 @@ function recordMetric(operation, scope, durationMs, keysCount = 1) {
52
54
  });
53
55
  }
54
56
  function measureOperation(operation, scope, fn, keysCount = 1) {
57
+ if (!metricsObserver) {
58
+ return fn();
59
+ }
55
60
  const start = Date.now();
56
61
  try {
57
62
  return fn();
@@ -80,6 +85,8 @@ function createLocalStorageWebSecureBackend() {
80
85
  };
81
86
  }
82
87
  let webSecureStorageBackend = createLocalStorageWebSecureBackend();
88
+ let cachedSecureBrowserStorage;
89
+ let cachedSecureBackendRef;
83
90
  function getBrowserStorage(scope) {
84
91
  if (scope === StorageScope.Disk) {
85
92
  return globalThis.localStorage;
@@ -88,16 +95,21 @@ function getBrowserStorage(scope) {
88
95
  if (!webSecureStorageBackend) {
89
96
  return undefined;
90
97
  }
91
- return {
92
- setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
93
- getItem: key => webSecureStorageBackend?.getItem(key) ?? null,
94
- removeItem: key => webSecureStorageBackend?.removeItem(key),
95
- clear: () => webSecureStorageBackend?.clear(),
96
- key: index => webSecureStorageBackend?.getAllKeys()[index] ?? null,
98
+ if (cachedSecureBackendRef === webSecureStorageBackend && cachedSecureBrowserStorage) {
99
+ return cachedSecureBrowserStorage;
100
+ }
101
+ cachedSecureBackendRef = webSecureStorageBackend;
102
+ cachedSecureBrowserStorage = {
103
+ setItem: (key, value) => webSecureStorageBackend.setItem(key, value),
104
+ getItem: key => webSecureStorageBackend.getItem(key) ?? null,
105
+ removeItem: key => webSecureStorageBackend.removeItem(key),
106
+ clear: () => webSecureStorageBackend.clear(),
107
+ key: index => webSecureStorageBackend.getAllKeys()[index] ?? null,
97
108
  get length() {
98
- return webSecureStorageBackend?.getAllKeys().length ?? 0;
109
+ return webSecureStorageBackend.getAllKeys().length;
99
110
  }
100
111
  };
112
+ return cachedSecureBrowserStorage;
101
113
  }
102
114
  return undefined;
103
115
  }
@@ -197,14 +209,18 @@ function handleWebStorageEvent(event) {
197
209
  notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
198
210
  }
199
211
  function ensureWebStorageEventSubscription() {
200
- if (hasWebStorageEventSubscription) {
201
- return;
212
+ webStorageSubscriberCount += 1;
213
+ if (webStorageSubscriberCount === 1 && typeof window !== "undefined" && typeof window.addEventListener === "function") {
214
+ window.addEventListener("storage", handleWebStorageEvent);
215
+ hasWebStorageEventSubscription = true;
202
216
  }
203
- if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
204
- return;
217
+ }
218
+ function maybeCleanupWebStorageSubscription() {
219
+ webStorageSubscriberCount = Math.max(0, webStorageSubscriberCount - 1);
220
+ if (webStorageSubscriberCount === 0 && hasWebStorageEventSubscription && typeof window !== "undefined") {
221
+ window.removeEventListener("storage", handleWebStorageEvent);
222
+ hasWebStorageEventSubscription = false;
205
223
  }
206
- window.addEventListener("storage", handleWebStorageEvent);
207
- hasWebStorageEventSubscription = true;
208
224
  }
209
225
  function getScopedListeners(scope) {
210
226
  return webScopeListeners.get(scope);
@@ -225,12 +241,19 @@ function clearScopeRawCache(scope) {
225
241
  getScopeRawCache(scope).clear();
226
242
  }
227
243
  function notifyKeyListeners(registry, key) {
228
- registry.get(key)?.forEach(listener => listener());
244
+ const listeners = registry.get(key);
245
+ if (listeners) {
246
+ for (const listener of listeners) {
247
+ listener();
248
+ }
249
+ }
229
250
  }
230
251
  function notifyAllListeners(registry) {
231
- registry.forEach(listeners => {
232
- listeners.forEach(listener => listener());
233
- });
252
+ for (const listeners of registry.values()) {
253
+ for (const listener of listeners) {
254
+ listener();
255
+ }
256
+ }
234
257
  }
235
258
  function addKeyListener(registry, key, listener) {
236
259
  let listeners = registry.get(key);
@@ -276,7 +299,7 @@ function flushSecureWrites() {
276
299
  if (value === undefined) {
277
300
  keysToRemove.push(key);
278
301
  } else {
279
- const resolvedAccessControl = accessControl ?? AccessControl.WhenUnlocked;
302
+ const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
280
303
  const existingGroup = groupedSetWrites.get(resolvedAccessControl);
281
304
  const group = existingGroup ?? {
282
305
  keys: [],
@@ -609,7 +632,12 @@ export const storage = {
609
632
  if (scope === StorageScope.Secure) {
610
633
  flushSecureWrites();
611
634
  }
612
- clearScopeRawCache(scope);
635
+ const scopeCache = getScopeRawCache(scope);
636
+ for (const key of scopeCache.keys()) {
637
+ if (isNamespaced(key, namespace)) {
638
+ scopeCache.delete(key);
639
+ }
640
+ }
613
641
  WebStorage.removeByPrefix(keyPrefix, scope);
614
642
  });
615
643
  },
@@ -678,9 +706,13 @@ export const storage = {
678
706
  return result;
679
707
  }
680
708
  const keys = WebStorage.getAllKeys(scope);
681
- keys.forEach(key => {
682
- const val = WebStorage.get(key, scope);
683
- if (val !== undefined) result[key] = val;
709
+ if (keys.length === 0) return {};
710
+ const values = WebStorage.getBatch(keys, scope);
711
+ keys.forEach((key, index) => {
712
+ const val = values[index];
713
+ if (val !== undefined && val !== null) {
714
+ result[key] = val;
715
+ }
684
716
  });
685
717
  return result;
686
718
  });
@@ -692,7 +724,8 @@ export const storage = {
692
724
  return WebStorage.size(scope);
693
725
  });
694
726
  },
695
- setAccessControl: _level => {
727
+ setAccessControl: level => {
728
+ secureDefaultAccessControl = level;
696
729
  recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
697
730
  },
698
731
  setSecureWritesAsync: _enabled => {
@@ -723,10 +756,48 @@ export const storage = {
723
756
  },
724
757
  resetMetrics: () => {
725
758
  metricsCounters.clear();
759
+ },
760
+ getString: (key, scope) => {
761
+ return measureOperation("storage:getString", scope, () => {
762
+ return getRawValue(key, scope);
763
+ });
764
+ },
765
+ setString: (key, value, scope) => {
766
+ measureOperation("storage:setString", scope, () => {
767
+ setRawValue(key, value, scope);
768
+ });
769
+ },
770
+ deleteString: (key, scope) => {
771
+ measureOperation("storage:deleteString", scope, () => {
772
+ removeRawValue(key, scope);
773
+ });
774
+ },
775
+ import: (data, scope) => {
776
+ const keys = Object.keys(data);
777
+ measureOperation("storage:import", scope, () => {
778
+ assertValidScope(scope);
779
+ if (keys.length === 0) return;
780
+ const values = keys.map(k => data[k]);
781
+ if (scope === StorageScope.Memory) {
782
+ keys.forEach((key, index) => {
783
+ memoryStore.set(key, values[index]);
784
+ });
785
+ keys.forEach(key => notifyKeyListeners(memoryListeners, key));
786
+ return;
787
+ }
788
+ if (scope === StorageScope.Secure) {
789
+ flushSecureWrites();
790
+ WebStorage.setSecureAccessControl(secureDefaultAccessControl);
791
+ }
792
+ WebStorage.setBatch(keys, values, scope);
793
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
794
+ }, keys.length);
726
795
  }
727
796
  };
728
797
  export function setWebSecureStorageBackend(backend) {
729
798
  webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
799
+ cachedSecureBrowserStorage = undefined;
800
+ cachedSecureBackendRef = undefined;
730
801
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
731
802
  clearScopeRawCache(StorageScope.Secure);
732
803
  }
@@ -807,12 +878,17 @@ export function createStorageItem(config) {
807
878
  }
808
879
  return memoryStore.get(storageKey);
809
880
  }
810
- if (nonMemoryScope === StorageScope.Secure && !isBiometric && hasPendingSecureWrite(storageKey)) {
811
- return readPendingSecureWrite(storageKey);
881
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
882
+ const pending = pendingSecureWrites.get(storageKey);
883
+ if (pending !== undefined) {
884
+ return pending.value;
885
+ }
812
886
  }
813
887
  if (readCache) {
814
- if (hasCachedRawValue(nonMemoryScope, storageKey)) {
815
- return readCachedRawValue(nonMemoryScope, storageKey);
888
+ const cache = getScopeRawCache(nonMemoryScope);
889
+ const cached = cache.get(storageKey);
890
+ if (cached !== undefined || cache.has(storageKey)) {
891
+ return cached;
816
892
  }
817
893
  }
818
894
  if (isBiometric) {
@@ -829,7 +905,7 @@ export function createStorageItem(config) {
829
905
  }
830
906
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
831
907
  if (coalesceSecureWrites) {
832
- scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? AccessControl.WhenUnlocked);
908
+ scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
833
909
  return;
834
910
  }
835
911
  if (nonMemoryScope === StorageScope.Secure) {
@@ -844,7 +920,7 @@ export function createStorageItem(config) {
844
920
  }
845
921
  cacheRawValue(nonMemoryScope, storageKey, undefined);
846
922
  if (coalesceSecureWrites) {
847
- scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? AccessControl.WhenUnlocked);
923
+ scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
848
924
  return;
849
925
  }
850
926
  if (nonMemoryScope === StorageScope.Secure) {
@@ -907,6 +983,7 @@ export function createStorageItem(config) {
907
983
  onExpired?.(storageKey);
908
984
  lastValue = ensureValidatedValue(defaultValue, false);
909
985
  hasLastValue = true;
986
+ listeners.forEach(cb => cb());
910
987
  return lastValue;
911
988
  }
912
989
  }
@@ -942,6 +1019,7 @@ export function createStorageItem(config) {
942
1019
  onExpired?.(storageKey);
943
1020
  lastValue = ensureValidatedValue(defaultValue, false);
944
1021
  hasLastValue = true;
1022
+ listeners.forEach(cb => cb());
945
1023
  return lastValue;
946
1024
  }
947
1025
  deserializableRaw = parsed.payload;
@@ -969,10 +1047,10 @@ export function createStorageItem(config) {
969
1047
  const set = valueOrFn => {
970
1048
  measureOperation("item:set", config.scope, () => {
971
1049
  const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
972
- invalidateParsedCache();
973
1050
  if (validate && !validate(newValue)) {
974
1051
  throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
975
1052
  }
1053
+ invalidateParsedCache();
976
1054
  writeValueWithoutValidation(newValue);
977
1055
  });
978
1056
  };
@@ -1011,6 +1089,9 @@ export function createStorageItem(config) {
1011
1089
  if (listeners.size === 0 && unsubscribe) {
1012
1090
  unsubscribe();
1013
1091
  unsubscribe = null;
1092
+ if (!isMemory) {
1093
+ maybeCleanupWebStorageSubscription();
1094
+ }
1014
1095
  }
1015
1096
  };
1016
1097
  };
@@ -1028,6 +1109,9 @@ export function createStorageItem(config) {
1028
1109
  invalidateParsedCache();
1029
1110
  listeners.forEach(listener => listener());
1030
1111
  },
1112
+ _invalidateParsedCacheOnly: () => {
1113
+ invalidateParsedCache();
1114
+ },
1031
1115
  _hasValidation: validate !== undefined,
1032
1116
  _hasExpiration: expiration !== undefined,
1033
1117
  _readCacheEnabled: readCache,
@@ -1042,6 +1126,7 @@ export function createStorageItem(config) {
1042
1126
  return storageItem;
1043
1127
  }
1044
1128
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
1129
+ export { createIndexedDBBackend } from "./indexeddb-backend";
1045
1130
  export function getBatch(items, scope) {
1046
1131
  return measureOperation("batch:get", scope, () => {
1047
1132
  assertBatchScope(items, scope);
@@ -1057,14 +1142,17 @@ export function getBatch(items, scope) {
1057
1142
  const keyIndexes = [];
1058
1143
  items.forEach((item, index) => {
1059
1144
  if (scope === StorageScope.Secure) {
1060
- if (hasPendingSecureWrite(item.key)) {
1061
- rawValues[index] = readPendingSecureWrite(item.key);
1145
+ const pending = pendingSecureWrites.get(item.key);
1146
+ if (pending !== undefined) {
1147
+ rawValues[index] = pending.value;
1062
1148
  return;
1063
1149
  }
1064
1150
  }
1065
1151
  if (item._readCacheEnabled === true) {
1066
- if (hasCachedRawValue(scope, item.key)) {
1067
- rawValues[index] = readCachedRawValue(scope, item.key);
1152
+ const cache = getScopeRawCache(scope);
1153
+ const cached = cache.get(item.key);
1154
+ if (cached !== undefined || cache.has(item.key)) {
1155
+ rawValues[index] = cached;
1068
1156
  return;
1069
1157
  }
1070
1158
  }
@@ -1096,10 +1184,32 @@ export function setBatch(items, scope) {
1096
1184
  measureOperation("batch:set", scope, () => {
1097
1185
  assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
1098
1186
  if (scope === StorageScope.Memory) {
1187
+ // Determine if any item needs per-item handling (validation or TTL)
1188
+ const needsIndividualSets = items.some(({
1189
+ item
1190
+ }) => {
1191
+ const internal = asInternal(item);
1192
+ return internal._hasValidation || internal._hasExpiration;
1193
+ });
1194
+ if (needsIndividualSets) {
1195
+ items.forEach(({
1196
+ item,
1197
+ value
1198
+ }) => item.set(value));
1199
+ return;
1200
+ }
1201
+
1202
+ // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1099
1203
  items.forEach(({
1100
1204
  item,
1101
1205
  value
1102
- }) => item.set(value));
1206
+ }) => {
1207
+ memoryStore.set(item.key, value);
1208
+ asInternal(item)._invalidateParsedCacheOnly();
1209
+ });
1210
+ items.forEach(({
1211
+ item
1212
+ }) => notifyKeyListeners(memoryListeners, item.key));
1103
1213
  return;
1104
1214
  }
1105
1215
  if (scope === StorageScope.Secure) {
@@ -1128,7 +1238,7 @@ export function setBatch(items, scope) {
1128
1238
  value,
1129
1239
  internal
1130
1240
  }) => {
1131
- const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
1241
+ const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
1132
1242
  const existingGroup = groupedByAccessControl.get(accessControl);
1133
1243
  const group = existingGroup ?? {
1134
1244
  keys: [],
@@ -1205,9 +1315,11 @@ export function migrateToLatest(scope = StorageScope.Disk) {
1205
1315
  return;
1206
1316
  }
1207
1317
  migration(context);
1208
- writeMigrationVersion(scope, version);
1209
1318
  appliedVersion = version;
1210
1319
  });
1320
+ if (appliedVersion !== currentVersion) {
1321
+ writeMigrationVersion(scope, appliedVersion);
1322
+ }
1211
1323
  return appliedVersion;
1212
1324
  });
1213
1325
  }
@@ -1217,12 +1329,17 @@ export function runTransaction(scope, transaction) {
1217
1329
  if (scope === StorageScope.Secure) {
1218
1330
  flushSecureWrites();
1219
1331
  }
1332
+ const NOT_SET = Symbol();
1220
1333
  const rollback = new Map();
1221
1334
  const rememberRollback = key => {
1222
1335
  if (rollback.has(key)) {
1223
1336
  return;
1224
1337
  }
1225
- rollback.set(key, getRawValue(key, scope));
1338
+ if (scope === StorageScope.Memory) {
1339
+ rollback.set(key, memoryStore.has(key) ? memoryStore.get(key) : NOT_SET);
1340
+ } else {
1341
+ rollback.set(key, getRawValue(key, scope));
1342
+ }
1226
1343
  };
1227
1344
  const tx = {
1228
1345
  scope,
@@ -1256,11 +1373,12 @@ export function runTransaction(scope, transaction) {
1256
1373
  const rollbackEntries = Array.from(rollback.entries()).reverse();
1257
1374
  if (scope === StorageScope.Memory) {
1258
1375
  rollbackEntries.forEach(([key, previousValue]) => {
1259
- if (previousValue === undefined) {
1260
- removeRawValue(key, scope);
1376
+ if (previousValue === NOT_SET) {
1377
+ memoryStore.delete(key);
1261
1378
  } else {
1262
- setRawValue(key, previousValue, scope);
1379
+ memoryStore.set(key, previousValue);
1263
1380
  }
1381
+ notifyKeyListeners(memoryListeners, key);
1264
1382
  });
1265
1383
  } else {
1266
1384
  const keysToSet = [];
@@ -1319,4 +1437,7 @@ export function createSecureAuthStorage(config, options) {
1319
1437
  }
1320
1438
  return result;
1321
1439
  }
1440
+ export function isKeychainLockedError(_err) {
1441
+ return false;
1442
+ }
1322
1443
  //# sourceMappingURL=index.web.js.map