react-native-nitro-storage 0.4.1 → 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.
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,13 +1048,28 @@ 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
+ },
1008
1066
  import: (data: Record<string, string>, scope: StorageScope): void => {
1067
+ const keys = Object.keys(data);
1009
1068
  measureOperation(
1010
1069
  "storage:import",
1011
1070
  scope,
1012
1071
  () => {
1013
1072
  assertValidScope(scope);
1014
- const keys = Object.keys(data);
1015
1073
  if (keys.length === 0) return;
1016
1074
  const values = keys.map((k) => data[k]!);
1017
1075
 
@@ -1023,9 +1081,15 @@ export const storage = {
1023
1081
  return;
1024
1082
  }
1025
1083
 
1084
+ if (scope === StorageScope.Secure) {
1085
+ flushSecureWrites();
1086
+ WebStorage.setSecureAccessControl(secureDefaultAccessControl);
1087
+ }
1088
+
1026
1089
  WebStorage.setBatch(keys, values, scope);
1090
+ keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1027
1091
  },
1028
- Object.keys(data).length,
1092
+ keys.length,
1029
1093
  );
1030
1094
  },
1031
1095
  };
@@ -1034,6 +1098,8 @@ export function setWebSecureStorageBackend(
1034
1098
  backend?: WebSecureStorageBackend,
1035
1099
  ): void {
1036
1100
  webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
1101
+ cachedSecureBrowserStorage = undefined;
1102
+ cachedSecureBackendRef = undefined;
1037
1103
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
1038
1104
  clearScopeRawCache(StorageScope.Secure);
1039
1105
  }
@@ -1207,17 +1273,18 @@ export function createStorageItem<T = undefined>(
1207
1273
  return memoryStore.get(storageKey);
1208
1274
  }
1209
1275
 
1210
- if (
1211
- nonMemoryScope === StorageScope.Secure &&
1212
- !isBiometric &&
1213
- hasPendingSecureWrite(storageKey)
1214
- ) {
1215
- return readPendingSecureWrite(storageKey);
1276
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
1277
+ const pending = pendingSecureWrites.get(storageKey);
1278
+ if (pending !== undefined) {
1279
+ return pending.value;
1280
+ }
1216
1281
  }
1217
1282
 
1218
1283
  if (readCache) {
1219
- if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
1220
- 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;
1221
1288
  }
1222
1289
  }
1223
1290
 
@@ -1246,7 +1313,7 @@ export function createStorageItem<T = undefined>(
1246
1313
  scheduleSecureWrite(
1247
1314
  storageKey,
1248
1315
  rawValue,
1249
- secureAccessControl ?? AccessControl.WhenUnlocked,
1316
+ secureAccessControl ?? secureDefaultAccessControl,
1250
1317
  );
1251
1318
  return;
1252
1319
  }
@@ -1270,7 +1337,7 @@ export function createStorageItem<T = undefined>(
1270
1337
  scheduleSecureWrite(
1271
1338
  storageKey,
1272
1339
  undefined,
1273
- secureAccessControl ?? AccessControl.WhenUnlocked,
1340
+ secureAccessControl ?? secureDefaultAccessControl,
1274
1341
  );
1275
1342
  return;
1276
1343
  }
@@ -1431,14 +1498,13 @@ export function createStorageItem<T = undefined>(
1431
1498
  ? valueOrFn(getInternal())
1432
1499
  : valueOrFn;
1433
1500
 
1434
- invalidateParsedCache();
1435
-
1436
1501
  if (validate && !validate(newValue)) {
1437
1502
  throw new Error(
1438
1503
  `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
1439
1504
  );
1440
1505
  }
1441
1506
 
1507
+ invalidateParsedCache();
1442
1508
  writeValueWithoutValidation(newValue);
1443
1509
  });
1444
1510
  };
@@ -1488,6 +1554,9 @@ export function createStorageItem<T = undefined>(
1488
1554
  if (listeners.size === 0 && unsubscribe) {
1489
1555
  unsubscribe();
1490
1556
  unsubscribe = null;
1557
+ if (!isMemory) {
1558
+ maybeCleanupWebStorageSubscription();
1559
+ }
1491
1560
  }
1492
1561
  };
1493
1562
  };
@@ -1574,15 +1643,18 @@ export function getBatch(
1574
1643
 
1575
1644
  items.forEach((item, index) => {
1576
1645
  if (scope === StorageScope.Secure) {
1577
- if (hasPendingSecureWrite(item.key)) {
1578
- rawValues[index] = readPendingSecureWrite(item.key);
1646
+ const pending = pendingSecureWrites.get(item.key);
1647
+ if (pending !== undefined) {
1648
+ rawValues[index] = pending.value;
1579
1649
  return;
1580
1650
  }
1581
1651
  }
1582
1652
 
1583
1653
  if (item._readCacheEnabled === true) {
1584
- if (hasCachedRawValue(scope, item.key)) {
1585
- 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;
1586
1658
  return;
1587
1659
  }
1588
1660
  }
@@ -1674,7 +1746,7 @@ export function setBatch<T>(
1674
1746
 
1675
1747
  secureEntries.forEach(({ item, value, internal }) => {
1676
1748
  const accessControl =
1677
- internal._secureAccessControl ?? AccessControl.WhenUnlocked;
1749
+ internal._secureAccessControl ?? secureDefaultAccessControl;
1678
1750
  const existingGroup = groupedByAccessControl.get(accessControl);
1679
1751
  const group = existingGroup ?? { keys: [], values: [] };
1680
1752
  group.keys.push(item.key);
@@ -1773,10 +1845,13 @@ export function migrateToLatest(
1773
1845
  return;
1774
1846
  }
1775
1847
  migration(context);
1776
- writeMigrationVersion(scope, version);
1777
1848
  appliedVersion = version;
1778
1849
  });
1779
1850
 
1851
+ if (appliedVersion !== currentVersion) {
1852
+ writeMigrationVersion(scope, appliedVersion);
1853
+ }
1854
+
1780
1855
  return appliedVersion;
1781
1856
  });
1782
1857
  }
@@ -1791,13 +1866,18 @@ export function runTransaction<T>(
1791
1866
  flushSecureWrites();
1792
1867
  }
1793
1868
 
1794
- const rollback = new Map<string, string | undefined>();
1869
+ const NOT_SET = Symbol();
1870
+ const rollback = new Map<string, unknown>();
1795
1871
 
1796
1872
  const rememberRollback = (key: string) => {
1797
1873
  if (rollback.has(key)) {
1798
1874
  return;
1799
1875
  }
1800
- 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
+ }
1801
1881
  };
1802
1882
 
1803
1883
  const tx: TransactionContext = {
@@ -1833,11 +1913,12 @@ export function runTransaction<T>(
1833
1913
  const rollbackEntries = Array.from(rollback.entries()).reverse();
1834
1914
  if (scope === StorageScope.Memory) {
1835
1915
  rollbackEntries.forEach(([key, previousValue]) => {
1836
- if (previousValue === undefined) {
1837
- removeRawValue(key, scope);
1916
+ if (previousValue === NOT_SET) {
1917
+ memoryStore.delete(key);
1838
1918
  } else {
1839
- setRawValue(key, previousValue, scope);
1919
+ memoryStore.set(key, previousValue);
1840
1920
  }
1921
+ notifyKeyListeners(memoryListeners, key);
1841
1922
  });
1842
1923
  } else {
1843
1924
  const keysToSet: string[] = [];
@@ -1849,7 +1930,7 @@ export function runTransaction<T>(
1849
1930
  keysToRemove.push(key);
1850
1931
  } else {
1851
1932
  keysToSet.push(key);
1852
- valuesToSet.push(previousValue);
1933
+ valuesToSet.push(previousValue as string);
1853
1934
  }
1854
1935
  });
1855
1936
 
@@ -1915,3 +1996,7 @@ export function createSecureAuthStorage<K extends string>(
1915
1996
 
1916
1997
  return result as Record<K, StorageItem<string>>;
1917
1998
  }
1999
+
2000
+ export function isKeychainLockedError(_err: unknown): boolean {
2001
+ return false;
2002
+ }
package/src/internal.ts CHANGED
@@ -4,6 +4,15 @@ export const MIGRATION_VERSION_KEY = "__nitro_storage_migration_version__";
4
4
  export const NATIVE_BATCH_MISSING_SENTINEL =
5
5
  "__nitro_storage_batch_missing__::v1";
6
6
  const PRIMITIVE_FAST_PATH_PREFIX = "__nitro_storage_primitive__:";
7
+ const PRIM_NULL = "__nitro_storage_primitive__:l";
8
+ const PRIM_UNDEFINED = "__nitro_storage_primitive__:u";
9
+ const PRIM_TRUE = "__nitro_storage_primitive__:b:1";
10
+ const PRIM_FALSE = "__nitro_storage_primitive__:b:0";
11
+ const PRIM_STRING_PREFIX = "__nitro_storage_primitive__:s:";
12
+ const PRIM_NUMBER_PREFIX = "__nitro_storage_primitive__:n:";
13
+ const PRIM_INFINITY = "__nitro_storage_primitive__:n:Infinity";
14
+ const PRIM_NEG_INFINITY = "__nitro_storage_primitive__:n:-Infinity";
15
+ const PRIM_NAN = "__nitro_storage_primitive__:n:NaN";
7
16
  const NAMESPACE_SEPARATOR = ":";
8
17
  const VERSION_TOKEN_PREFIX = "__nitro_storage_version__:";
9
18
 
@@ -80,21 +89,30 @@ export function isNamespaced(key: string, namespace: string): boolean {
80
89
 
81
90
  export function serializeWithPrimitiveFastPath<T>(value: T): string {
82
91
  if (value === null) {
83
- return `${PRIMITIVE_FAST_PATH_PREFIX}l`;
92
+ return PRIM_NULL;
84
93
  }
85
94
 
86
95
  switch (typeof value) {
87
96
  case "string":
88
- return `${PRIMITIVE_FAST_PATH_PREFIX}s:${value}`;
97
+ return PRIM_STRING_PREFIX + (value as string);
89
98
  case "number":
90
99
  if (Number.isFinite(value)) {
91
- return `${PRIMITIVE_FAST_PATH_PREFIX}n:${value}`;
100
+ return PRIM_NUMBER_PREFIX + String(value);
101
+ }
102
+ if (Number.isNaN(value as number)) {
103
+ return PRIM_NAN;
104
+ }
105
+ if (value === Infinity) {
106
+ return PRIM_INFINITY;
107
+ }
108
+ if (value === -Infinity) {
109
+ return PRIM_NEG_INFINITY;
92
110
  }
93
111
  break;
94
112
  case "boolean":
95
- return `${PRIMITIVE_FAST_PATH_PREFIX}b:${value ? "1" : "0"}`;
113
+ return value ? PRIM_TRUE : PRIM_FALSE;
96
114
  case "undefined":
97
- return `${PRIMITIVE_FAST_PATH_PREFIX}u`;
115
+ return PRIM_UNDEFINED;
98
116
  default:
99
117
  break;
100
118
  }
@@ -108,31 +126,41 @@ export function serializeWithPrimitiveFastPath<T>(value: T): string {
108
126
  return serialized;
109
127
  }
110
128
 
129
+ // charCode constants for fast tag dispatch
130
+ const CHAR_U = 117; // 'u'
131
+ const CHAR_L = 108; // 'l'
132
+ const CHAR_S = 115; // 's'
133
+ const CHAR_B = 98; // 'b'
134
+ const CHAR_N = 110; // 'n'
135
+
111
136
  export function deserializeWithPrimitiveFastPath<T>(value: string): T {
112
137
  if (value.startsWith(PRIMITIVE_FAST_PATH_PREFIX)) {
113
- const encodedValue = value.slice(PRIMITIVE_FAST_PATH_PREFIX.length);
114
- if (encodedValue === "u") {
138
+ const prefixLen = PRIMITIVE_FAST_PATH_PREFIX.length;
139
+ const tagChar = value.charCodeAt(prefixLen);
140
+
141
+ if (tagChar === CHAR_U) {
115
142
  return undefined as T;
116
143
  }
117
- if (encodedValue === "l") {
144
+ if (tagChar === CHAR_L) {
118
145
  return null as T;
119
146
  }
120
147
 
121
- const separatorIndex = encodedValue.indexOf(":");
122
- if (separatorIndex > 0) {
123
- const tag = encodedValue.slice(0, separatorIndex);
124
- const payload = encodedValue.slice(separatorIndex + 1);
125
- if (tag === "s") {
126
- return payload as T;
127
- }
128
- if (tag === "b") {
129
- return (payload === "1") as T;
130
- }
131
- if (tag === "n") {
132
- const parsed = Number(payload);
133
- if (Number.isFinite(parsed)) {
134
- return parsed as T;
135
- }
148
+ // Tagged values have format: prefix + tag + ':' + payload
149
+ const payload = value.slice(prefixLen + 2);
150
+
151
+ if (tagChar === CHAR_S) {
152
+ return payload as T;
153
+ }
154
+ if (tagChar === CHAR_B) {
155
+ return (payload === "1") as T;
156
+ }
157
+ if (tagChar === CHAR_N) {
158
+ if (payload === "NaN") return NaN as T;
159
+ if (payload === "Infinity") return Infinity as T;
160
+ if (payload === "-Infinity") return -Infinity as T;
161
+ const parsed = Number(payload);
162
+ if (Number.isFinite(parsed)) {
163
+ return parsed as T;
136
164
  }
137
165
  }
138
166
  }