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.
- package/README.md +90 -0
- package/android/build.gradle +0 -12
- package/android/consumer-rules.pro +26 -4
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +7 -10
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +0 -4
- package/android/src/main/cpp/cpp-adapter.cpp +3 -1
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +172 -77
- package/cpp/bindings/HybridStorage.cpp +120 -69
- package/cpp/bindings/HybridStorage.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.hpp +2 -1
- package/ios/IOSStorageAdapterCpp.mm +264 -49
- package/lib/commonjs/index.js +128 -20
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +169 -41
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +130 -0
- package/lib/commonjs/indexeddb-backend.js.map +1 -0
- package/lib/commonjs/internal.js +51 -23
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/module/index.js +121 -20
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +162 -41
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +126 -0
- package/lib/module/indexeddb-backend.js.map +1 -0
- package/lib/module/internal.js +51 -23
- package/lib/module/internal.js.map +1 -1
- package/lib/typescript/index.d.ts +6 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +7 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +29 -0
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
- package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
- package/package.json +7 -3
- package/src/index.ts +137 -27
- package/src/index.web.ts +182 -49
- package/src/indexeddb-backend.ts +143 -0
- 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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
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
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
403
|
+
webStorageSubscriberCount === 0 &&
|
|
404
|
+
hasWebStorageEventSubscription &&
|
|
405
|
+
typeof window !== "undefined"
|
|
379
406
|
) {
|
|
380
|
-
|
|
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)
|
|
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.
|
|
425
|
-
|
|
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 ??
|
|
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
|
-
|
|
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.
|
|
962
|
-
|
|
963
|
-
|
|
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: (
|
|
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
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
1196
|
-
|
|
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 ??
|
|
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 ??
|
|
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
|
-
|
|
1548
|
-
|
|
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
|
-
|
|
1555
|
-
|
|
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
|
-
|
|
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 ??
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
1789
|
-
|
|
1916
|
+
if (previousValue === NOT_SET) {
|
|
1917
|
+
memoryStore.delete(key);
|
|
1790
1918
|
} else {
|
|
1791
|
-
|
|
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
|
+
}
|