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.
- package/README.md +334 -34
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +26 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +4 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +90 -18
- package/cpp/bindings/HybridStorage.cpp +214 -23
- package/cpp/bindings/HybridStorage.hpp +31 -3
- package/cpp/core/NativeStorageAdapter.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.hpp +17 -0
- package/ios/IOSStorageAdapterCpp.mm +140 -10
- package/lib/commonjs/index.js +555 -288
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +750 -309
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +25 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/index.js +537 -287
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +732 -308
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +24 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +4 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +41 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +45 -4
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +10 -0
- package/lib/typescript/storage-hooks.d.ts.map +1 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +4 -0
- package/package.json +5 -3
- package/src/Storage.nitro.ts +4 -0
- package/src/index.ts +704 -324
- package/src/index.web.ts +929 -346
- package/src/internal.ts +28 -0
- package/src/storage-hooks.ts +48 -0
package/lib/module/index.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useRef, useSyncExternalStore } from "react";
|
|
4
3
|
import { NitroModules } from "react-native-nitro-modules";
|
|
5
|
-
import { StorageScope, AccessControl } from "./Storage.types";
|
|
6
|
-
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
|
|
4
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
5
|
+
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
|
|
7
6
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
8
7
|
export { migrateFromMMKV } from "./migration";
|
|
9
8
|
function asInternal(item) {
|
|
10
9
|
return item;
|
|
11
10
|
}
|
|
11
|
+
function isUpdater(valueOrFn) {
|
|
12
|
+
return typeof valueOrFn === "function";
|
|
13
|
+
}
|
|
14
|
+
function typedKeys(record) {
|
|
15
|
+
return Object.keys(record);
|
|
16
|
+
}
|
|
12
17
|
const registeredMigrations = new Map();
|
|
13
18
|
const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
|
|
14
19
|
Promise.resolve().then(task);
|
|
@@ -28,6 +33,36 @@ const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Se
|
|
|
28
33
|
const pendingSecureWrites = new Map();
|
|
29
34
|
let secureFlushScheduled = false;
|
|
30
35
|
let secureDefaultAccessControl = AccessControl.WhenUnlocked;
|
|
36
|
+
let metricsObserver;
|
|
37
|
+
const metricsCounters = new Map();
|
|
38
|
+
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
39
|
+
const existing = metricsCounters.get(operation);
|
|
40
|
+
if (!existing) {
|
|
41
|
+
metricsCounters.set(operation, {
|
|
42
|
+
count: 1,
|
|
43
|
+
totalDurationMs: durationMs,
|
|
44
|
+
maxDurationMs: durationMs
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
existing.count += 1;
|
|
48
|
+
existing.totalDurationMs += durationMs;
|
|
49
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
50
|
+
}
|
|
51
|
+
metricsObserver?.({
|
|
52
|
+
operation,
|
|
53
|
+
scope,
|
|
54
|
+
durationMs,
|
|
55
|
+
keysCount
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function measureOperation(operation, scope, fn, keysCount = 1) {
|
|
59
|
+
const start = Date.now();
|
|
60
|
+
try {
|
|
61
|
+
return fn();
|
|
62
|
+
} finally {
|
|
63
|
+
recordMetric(operation, scope, Date.now() - start, keysCount);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
31
66
|
function getScopedListeners(scope) {
|
|
32
67
|
return scopedListeners.get(scope);
|
|
33
68
|
}
|
|
@@ -88,34 +123,47 @@ function flushSecureWrites() {
|
|
|
88
123
|
}
|
|
89
124
|
const writes = Array.from(pendingSecureWrites.values());
|
|
90
125
|
pendingSecureWrites.clear();
|
|
91
|
-
const
|
|
92
|
-
const valuesToSet = [];
|
|
126
|
+
const groupedSetWrites = new Map();
|
|
93
127
|
const keysToRemove = [];
|
|
94
128
|
writes.forEach(({
|
|
95
129
|
key,
|
|
96
|
-
value
|
|
130
|
+
value,
|
|
131
|
+
accessControl
|
|
97
132
|
}) => {
|
|
98
133
|
if (value === undefined) {
|
|
99
134
|
keysToRemove.push(key);
|
|
100
135
|
} else {
|
|
101
|
-
|
|
102
|
-
|
|
136
|
+
const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
|
|
137
|
+
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
138
|
+
const group = existingGroup ?? {
|
|
139
|
+
keys: [],
|
|
140
|
+
values: []
|
|
141
|
+
};
|
|
142
|
+
group.keys.push(key);
|
|
143
|
+
group.values.push(value);
|
|
144
|
+
if (!existingGroup) {
|
|
145
|
+
groupedSetWrites.set(resolvedAccessControl, group);
|
|
146
|
+
}
|
|
103
147
|
}
|
|
104
148
|
});
|
|
105
149
|
const storageModule = getStorageModule();
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
storageModule.setBatch(
|
|
109
|
-
}
|
|
150
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
151
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
152
|
+
storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
153
|
+
});
|
|
110
154
|
if (keysToRemove.length > 0) {
|
|
111
155
|
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
112
156
|
}
|
|
113
157
|
}
|
|
114
|
-
function scheduleSecureWrite(key, value) {
|
|
115
|
-
|
|
158
|
+
function scheduleSecureWrite(key, value, accessControl) {
|
|
159
|
+
const pendingWrite = {
|
|
116
160
|
key,
|
|
117
161
|
value
|
|
118
|
-
}
|
|
162
|
+
};
|
|
163
|
+
if (accessControl !== undefined) {
|
|
164
|
+
pendingWrite.accessControl = accessControl;
|
|
165
|
+
}
|
|
166
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
119
167
|
if (secureFlushScheduled) {
|
|
120
168
|
return;
|
|
121
169
|
}
|
|
@@ -209,103 +257,186 @@ function writeMigrationVersion(scope, version) {
|
|
|
209
257
|
}
|
|
210
258
|
export const storage = {
|
|
211
259
|
clear: scope => {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
}
|
|
260
|
+
measureOperation("storage:clear", scope, () => {
|
|
261
|
+
if (scope === StorageScope.Memory) {
|
|
262
|
+
memoryStore.clear();
|
|
263
|
+
notifyAllListeners(memoryListeners);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (scope === StorageScope.Secure) {
|
|
267
|
+
flushSecureWrites();
|
|
268
|
+
pendingSecureWrites.clear();
|
|
269
|
+
}
|
|
270
|
+
clearScopeRawCache(scope);
|
|
271
|
+
getStorageModule().clear(scope);
|
|
272
|
+
});
|
|
226
273
|
},
|
|
227
274
|
clearAll: () => {
|
|
228
|
-
storage
|
|
229
|
-
|
|
230
|
-
|
|
275
|
+
measureOperation("storage:clearAll", StorageScope.Memory, () => {
|
|
276
|
+
storage.clear(StorageScope.Memory);
|
|
277
|
+
storage.clear(StorageScope.Disk);
|
|
278
|
+
storage.clear(StorageScope.Secure);
|
|
279
|
+
}, 3);
|
|
231
280
|
},
|
|
232
281
|
clearNamespace: (namespace, scope) => {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
282
|
+
measureOperation("storage:clearNamespace", scope, () => {
|
|
283
|
+
assertValidScope(scope);
|
|
284
|
+
if (scope === StorageScope.Memory) {
|
|
285
|
+
for (const key of memoryStore.keys()) {
|
|
286
|
+
if (isNamespaced(key, namespace)) {
|
|
287
|
+
memoryStore.delete(key);
|
|
288
|
+
}
|
|
238
289
|
}
|
|
290
|
+
notifyAllListeners(memoryListeners);
|
|
291
|
+
return;
|
|
239
292
|
}
|
|
240
|
-
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (scope === StorageScope.Secure) {
|
|
244
|
-
flushSecureWrites();
|
|
245
|
-
}
|
|
246
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
247
|
-
const namespacedKeys = keys.filter(k => isNamespaced(k, namespace));
|
|
248
|
-
if (namespacedKeys.length > 0) {
|
|
249
|
-
getStorageModule().removeBatch(namespacedKeys, scope);
|
|
250
|
-
namespacedKeys.forEach(k => cacheRawValue(scope, k, undefined));
|
|
293
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
251
294
|
if (scope === StorageScope.Secure) {
|
|
252
|
-
|
|
295
|
+
flushSecureWrites();
|
|
253
296
|
}
|
|
254
|
-
|
|
297
|
+
clearScopeRawCache(scope);
|
|
298
|
+
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
299
|
+
});
|
|
255
300
|
},
|
|
256
301
|
clearBiometric: () => {
|
|
257
|
-
|
|
302
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
303
|
+
getStorageModule().clearSecureBiometric();
|
|
304
|
+
});
|
|
258
305
|
},
|
|
259
306
|
has: (key, scope) => {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
307
|
+
return measureOperation("storage:has", scope, () => {
|
|
308
|
+
assertValidScope(scope);
|
|
309
|
+
if (scope === StorageScope.Memory) {
|
|
310
|
+
return memoryStore.has(key);
|
|
311
|
+
}
|
|
312
|
+
return getStorageModule().has(key, scope);
|
|
313
|
+
});
|
|
265
314
|
},
|
|
266
315
|
getAllKeys: scope => {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
316
|
+
return measureOperation("storage:getAllKeys", scope, () => {
|
|
317
|
+
assertValidScope(scope);
|
|
318
|
+
if (scope === StorageScope.Memory) {
|
|
319
|
+
return Array.from(memoryStore.keys());
|
|
320
|
+
}
|
|
321
|
+
return getStorageModule().getAllKeys(scope);
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
getKeysByPrefix: (prefix, scope) => {
|
|
325
|
+
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
326
|
+
assertValidScope(scope);
|
|
327
|
+
if (scope === StorageScope.Memory) {
|
|
328
|
+
return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
|
|
329
|
+
}
|
|
330
|
+
return getStorageModule().getKeysByPrefix(prefix, scope);
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
getByPrefix: (prefix, scope) => {
|
|
334
|
+
return measureOperation("storage:getByPrefix", scope, () => {
|
|
335
|
+
const result = {};
|
|
336
|
+
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
337
|
+
if (keys.length === 0) {
|
|
338
|
+
return result;
|
|
339
|
+
}
|
|
340
|
+
if (scope === StorageScope.Memory) {
|
|
341
|
+
keys.forEach(key => {
|
|
342
|
+
const value = memoryStore.get(key);
|
|
343
|
+
if (typeof value === "string") {
|
|
344
|
+
result[key] = value;
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
350
|
+
keys.forEach((key, idx) => {
|
|
351
|
+
const value = decodeNativeBatchValue(values[idx]);
|
|
352
|
+
if (value !== undefined) {
|
|
353
|
+
result[key] = value;
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
return result;
|
|
357
|
+
});
|
|
272
358
|
},
|
|
273
359
|
getAll: scope => {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
360
|
+
return measureOperation("storage:getAll", scope, () => {
|
|
361
|
+
assertValidScope(scope);
|
|
362
|
+
const result = {};
|
|
363
|
+
if (scope === StorageScope.Memory) {
|
|
364
|
+
memoryStore.forEach((value, key) => {
|
|
365
|
+
if (typeof value === "string") result[key] = value;
|
|
366
|
+
});
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
370
|
+
if (keys.length === 0) return result;
|
|
371
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
372
|
+
keys.forEach((key, idx) => {
|
|
373
|
+
const val = decodeNativeBatchValue(values[idx]);
|
|
374
|
+
if (val !== undefined) result[key] = val;
|
|
279
375
|
});
|
|
280
376
|
return result;
|
|
281
|
-
}
|
|
282
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
283
|
-
if (keys.length === 0) return result;
|
|
284
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
285
|
-
keys.forEach((key, idx) => {
|
|
286
|
-
const val = decodeNativeBatchValue(values[idx]);
|
|
287
|
-
if (val !== undefined) result[key] = val;
|
|
288
377
|
});
|
|
289
|
-
return result;
|
|
290
378
|
},
|
|
291
379
|
size: scope => {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
380
|
+
return measureOperation("storage:size", scope, () => {
|
|
381
|
+
assertValidScope(scope);
|
|
382
|
+
if (scope === StorageScope.Memory) {
|
|
383
|
+
return memoryStore.size;
|
|
384
|
+
}
|
|
385
|
+
return getStorageModule().size(scope);
|
|
386
|
+
});
|
|
297
387
|
},
|
|
298
388
|
setAccessControl: level => {
|
|
299
|
-
|
|
300
|
-
|
|
389
|
+
measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
|
|
390
|
+
secureDefaultAccessControl = level;
|
|
391
|
+
getStorageModule().setSecureAccessControl(level);
|
|
392
|
+
});
|
|
393
|
+
},
|
|
394
|
+
setSecureWritesAsync: enabled => {
|
|
395
|
+
measureOperation("storage:setSecureWritesAsync", StorageScope.Secure, () => {
|
|
396
|
+
getStorageModule().setSecureWritesAsync(enabled);
|
|
397
|
+
});
|
|
398
|
+
},
|
|
399
|
+
flushSecureWrites: () => {
|
|
400
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
401
|
+
flushSecureWrites();
|
|
402
|
+
});
|
|
301
403
|
},
|
|
302
404
|
setKeychainAccessGroup: group => {
|
|
303
|
-
|
|
405
|
+
measureOperation("storage:setKeychainAccessGroup", StorageScope.Secure, () => {
|
|
406
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
407
|
+
});
|
|
408
|
+
},
|
|
409
|
+
setMetricsObserver: observer => {
|
|
410
|
+
metricsObserver = observer;
|
|
411
|
+
},
|
|
412
|
+
getMetricsSnapshot: () => {
|
|
413
|
+
const snapshot = {};
|
|
414
|
+
metricsCounters.forEach((value, key) => {
|
|
415
|
+
snapshot[key] = {
|
|
416
|
+
count: value.count,
|
|
417
|
+
totalDurationMs: value.totalDurationMs,
|
|
418
|
+
avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
419
|
+
maxDurationMs: value.maxDurationMs
|
|
420
|
+
};
|
|
421
|
+
});
|
|
422
|
+
return snapshot;
|
|
423
|
+
},
|
|
424
|
+
resetMetrics: () => {
|
|
425
|
+
metricsCounters.clear();
|
|
304
426
|
}
|
|
305
427
|
};
|
|
428
|
+
export function setWebSecureStorageBackend(_backend) {
|
|
429
|
+
// Native platforms do not use web secure backends.
|
|
430
|
+
}
|
|
431
|
+
export function getWebSecureStorageBackend() {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
306
434
|
function canUseRawBatchPath(item) {
|
|
307
435
|
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
308
436
|
}
|
|
437
|
+
function canUseSecureRawBatchPath(item) {
|
|
438
|
+
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
|
|
439
|
+
}
|
|
309
440
|
function defaultSerialize(value) {
|
|
310
441
|
return serializeWithPrimitiveFastPath(value);
|
|
311
442
|
}
|
|
@@ -317,7 +448,8 @@ export function createStorageItem(config) {
|
|
|
317
448
|
const serialize = config.serialize ?? defaultSerialize;
|
|
318
449
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
319
450
|
const isMemory = config.scope === StorageScope.Memory;
|
|
320
|
-
const
|
|
451
|
+
const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
|
|
452
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
321
453
|
const secureAccessControl = config.accessControl;
|
|
322
454
|
const validate = config.validate;
|
|
323
455
|
const onValidationError = config.onValidationError;
|
|
@@ -326,7 +458,8 @@ export function createStorageItem(config) {
|
|
|
326
458
|
const expirationTtlMs = expiration?.ttlMs;
|
|
327
459
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
328
460
|
const readCache = !isMemory && config.readCache === true;
|
|
329
|
-
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric
|
|
461
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
462
|
+
const defaultValue = config.defaultValue;
|
|
330
463
|
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
331
464
|
if (expiration && expiration.ttlMs <= 0) {
|
|
332
465
|
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
@@ -336,10 +469,12 @@ export function createStorageItem(config) {
|
|
|
336
469
|
let lastRaw = undefined;
|
|
337
470
|
let lastValue;
|
|
338
471
|
let hasLastValue = false;
|
|
472
|
+
let lastExpiresAt = undefined;
|
|
339
473
|
const invalidateParsedCache = () => {
|
|
340
474
|
lastRaw = undefined;
|
|
341
475
|
lastValue = undefined;
|
|
342
476
|
hasLastValue = false;
|
|
477
|
+
lastExpiresAt = undefined;
|
|
343
478
|
};
|
|
344
479
|
const ensureSubscription = () => {
|
|
345
480
|
if (unsubscribe) {
|
|
@@ -387,12 +522,12 @@ export function createStorageItem(config) {
|
|
|
387
522
|
};
|
|
388
523
|
const writeStoredRaw = rawValue => {
|
|
389
524
|
if (isBiometric) {
|
|
390
|
-
getStorageModule().
|
|
525
|
+
getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
391
526
|
return;
|
|
392
527
|
}
|
|
393
528
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
394
529
|
if (coalesceSecureWrites) {
|
|
395
|
-
scheduleSecureWrite(storageKey, rawValue);
|
|
530
|
+
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
396
531
|
return;
|
|
397
532
|
}
|
|
398
533
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -408,7 +543,7 @@ export function createStorageItem(config) {
|
|
|
408
543
|
}
|
|
409
544
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
410
545
|
if (coalesceSecureWrites) {
|
|
411
|
-
scheduleSecureWrite(storageKey, undefined);
|
|
546
|
+
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
412
547
|
return;
|
|
413
548
|
}
|
|
414
549
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -441,7 +576,7 @@ export function createStorageItem(config) {
|
|
|
441
576
|
if (onValidationError) {
|
|
442
577
|
return onValidationError(invalidValue);
|
|
443
578
|
}
|
|
444
|
-
return
|
|
579
|
+
return defaultValue;
|
|
445
580
|
};
|
|
446
581
|
const ensureValidatedValue = (candidate, hadStoredValue) => {
|
|
447
582
|
if (!validate || validate(candidate)) {
|
|
@@ -449,40 +584,62 @@ export function createStorageItem(config) {
|
|
|
449
584
|
}
|
|
450
585
|
const resolved = resolveInvalidValue(candidate);
|
|
451
586
|
if (validate && !validate(resolved)) {
|
|
452
|
-
return
|
|
587
|
+
return defaultValue;
|
|
453
588
|
}
|
|
454
589
|
if (hadStoredValue) {
|
|
455
590
|
writeValueWithoutValidation(resolved);
|
|
456
591
|
}
|
|
457
592
|
return resolved;
|
|
458
593
|
};
|
|
459
|
-
const
|
|
594
|
+
const getInternal = () => {
|
|
460
595
|
const raw = readStoredRaw();
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
596
|
+
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
597
|
+
if (!expiration || lastExpiresAt === null) {
|
|
598
|
+
return lastValue;
|
|
599
|
+
}
|
|
600
|
+
if (typeof lastExpiresAt === "number") {
|
|
601
|
+
if (lastExpiresAt > Date.now()) {
|
|
602
|
+
return lastValue;
|
|
603
|
+
}
|
|
604
|
+
removeStoredRaw();
|
|
605
|
+
invalidateParsedCache();
|
|
606
|
+
onExpired?.(storageKey);
|
|
607
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
608
|
+
hasLastValue = true;
|
|
609
|
+
return lastValue;
|
|
610
|
+
}
|
|
464
611
|
}
|
|
465
612
|
lastRaw = raw;
|
|
466
613
|
if (raw === undefined) {
|
|
467
|
-
|
|
614
|
+
lastExpiresAt = undefined;
|
|
615
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
468
616
|
hasLastValue = true;
|
|
469
617
|
return lastValue;
|
|
470
618
|
}
|
|
471
619
|
if (isMemory) {
|
|
620
|
+
lastExpiresAt = undefined;
|
|
472
621
|
lastValue = ensureValidatedValue(raw, true);
|
|
473
622
|
hasLastValue = true;
|
|
474
623
|
return lastValue;
|
|
475
624
|
}
|
|
625
|
+
if (typeof raw !== "string") {
|
|
626
|
+
lastExpiresAt = undefined;
|
|
627
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
628
|
+
hasLastValue = true;
|
|
629
|
+
return lastValue;
|
|
630
|
+
}
|
|
476
631
|
let deserializableRaw = raw;
|
|
477
632
|
if (expiration) {
|
|
633
|
+
let envelopeExpiresAt = null;
|
|
478
634
|
try {
|
|
479
635
|
const parsed = JSON.parse(raw);
|
|
480
636
|
if (isStoredEnvelope(parsed)) {
|
|
637
|
+
envelopeExpiresAt = parsed.expiresAt;
|
|
481
638
|
if (parsed.expiresAt <= Date.now()) {
|
|
482
639
|
removeStoredRaw();
|
|
483
640
|
invalidateParsedCache();
|
|
484
641
|
onExpired?.(storageKey);
|
|
485
|
-
lastValue = ensureValidatedValue(
|
|
642
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
486
643
|
hasLastValue = true;
|
|
487
644
|
return lastValue;
|
|
488
645
|
}
|
|
@@ -491,37 +648,60 @@ export function createStorageItem(config) {
|
|
|
491
648
|
} catch {
|
|
492
649
|
// Keep backward compatibility with legacy raw values.
|
|
493
650
|
}
|
|
651
|
+
lastExpiresAt = envelopeExpiresAt;
|
|
652
|
+
} else {
|
|
653
|
+
lastExpiresAt = undefined;
|
|
494
654
|
}
|
|
495
655
|
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
496
656
|
hasLastValue = true;
|
|
497
657
|
return lastValue;
|
|
498
658
|
};
|
|
659
|
+
const getCurrentVersion = () => {
|
|
660
|
+
const raw = readStoredRaw();
|
|
661
|
+
return toVersionToken(raw);
|
|
662
|
+
};
|
|
663
|
+
const get = () => measureOperation("item:get", config.scope, () => getInternal());
|
|
664
|
+
const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
|
|
665
|
+
value: getInternal(),
|
|
666
|
+
version: getCurrentVersion()
|
|
667
|
+
}));
|
|
499
668
|
const set = valueOrFn => {
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
669
|
+
measureOperation("item:set", config.scope, () => {
|
|
670
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
|
|
671
|
+
invalidateParsedCache();
|
|
672
|
+
if (validate && !validate(newValue)) {
|
|
673
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
674
|
+
}
|
|
675
|
+
writeValueWithoutValidation(newValue);
|
|
676
|
+
});
|
|
507
677
|
};
|
|
678
|
+
const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
|
|
679
|
+
const currentVersion = getCurrentVersion();
|
|
680
|
+
if (currentVersion !== version) {
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
set(valueOrFn);
|
|
684
|
+
return true;
|
|
685
|
+
});
|
|
508
686
|
const deleteItem = () => {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
if (
|
|
512
|
-
memoryExpiration
|
|
687
|
+
measureOperation("item:delete", config.scope, () => {
|
|
688
|
+
invalidateParsedCache();
|
|
689
|
+
if (isMemory) {
|
|
690
|
+
if (memoryExpiration) {
|
|
691
|
+
memoryExpiration.delete(storageKey);
|
|
692
|
+
}
|
|
693
|
+
memoryStore.delete(storageKey);
|
|
694
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
695
|
+
return;
|
|
513
696
|
}
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
return;
|
|
517
|
-
}
|
|
518
|
-
removeStoredRaw();
|
|
697
|
+
removeStoredRaw();
|
|
698
|
+
});
|
|
519
699
|
};
|
|
520
|
-
const hasItem = () => {
|
|
700
|
+
const hasItem = () => measureOperation("item:has", config.scope, () => {
|
|
521
701
|
if (isMemory) return memoryStore.has(storageKey);
|
|
522
702
|
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
523
703
|
return getStorageModule().has(storageKey, config.scope);
|
|
524
|
-
};
|
|
704
|
+
});
|
|
525
705
|
const subscribe = callback => {
|
|
526
706
|
ensureSubscription();
|
|
527
707
|
listeners.add(callback);
|
|
@@ -538,7 +718,9 @@ export function createStorageItem(config) {
|
|
|
538
718
|
};
|
|
539
719
|
const storageItem = {
|
|
540
720
|
get,
|
|
721
|
+
getWithVersion,
|
|
541
722
|
set,
|
|
723
|
+
setIfVersion,
|
|
542
724
|
delete: deleteItem,
|
|
543
725
|
has: hasItem,
|
|
544
726
|
subscribe,
|
|
@@ -552,124 +734,152 @@ export function createStorageItem(config) {
|
|
|
552
734
|
_hasExpiration: expiration !== undefined,
|
|
553
735
|
_readCacheEnabled: readCache,
|
|
554
736
|
_isBiometric: isBiometric,
|
|
555
|
-
|
|
737
|
+
_defaultValue: defaultValue,
|
|
738
|
+
...(secureAccessControl !== undefined ? {
|
|
739
|
+
_secureAccessControl: secureAccessControl
|
|
740
|
+
} : {}),
|
|
556
741
|
scope: config.scope,
|
|
557
742
|
key: storageKey
|
|
558
743
|
};
|
|
559
744
|
return storageItem;
|
|
560
745
|
}
|
|
561
|
-
export
|
|
562
|
-
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
563
|
-
return [value, item.set];
|
|
564
|
-
}
|
|
565
|
-
export function useStorageSelector(item, selector, isEqual = Object.is) {
|
|
566
|
-
const selectedRef = useRef({
|
|
567
|
-
hasValue: false
|
|
568
|
-
});
|
|
569
|
-
const getSelectedSnapshot = () => {
|
|
570
|
-
const nextSelected = selector(item.get());
|
|
571
|
-
const current = selectedRef.current;
|
|
572
|
-
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
573
|
-
return current.value;
|
|
574
|
-
}
|
|
575
|
-
selectedRef.current = {
|
|
576
|
-
hasValue: true,
|
|
577
|
-
value: nextSelected
|
|
578
|
-
};
|
|
579
|
-
return nextSelected;
|
|
580
|
-
};
|
|
581
|
-
const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
|
|
582
|
-
return [selectedValue, item.set];
|
|
583
|
-
}
|
|
584
|
-
export function useSetStorage(item) {
|
|
585
|
-
return item.set;
|
|
586
|
-
}
|
|
746
|
+
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
587
747
|
export function getBatch(items, scope) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
|
|
593
|
-
if (!useRawBatchPath) {
|
|
594
|
-
return items.map(item => item.get());
|
|
595
|
-
}
|
|
596
|
-
const useBatchCache = items.every(item => item._readCacheEnabled === true);
|
|
597
|
-
const rawValues = new Array(items.length);
|
|
598
|
-
const keysToFetch = [];
|
|
599
|
-
const keyIndexes = [];
|
|
600
|
-
items.forEach((item, index) => {
|
|
601
|
-
if (scope === StorageScope.Secure) {
|
|
602
|
-
if (hasPendingSecureWrite(item.key)) {
|
|
603
|
-
rawValues[index] = readPendingSecureWrite(item.key);
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
748
|
+
return measureOperation("batch:get", scope, () => {
|
|
749
|
+
assertBatchScope(items, scope);
|
|
750
|
+
if (scope === StorageScope.Memory) {
|
|
751
|
+
return items.map(item => item.get());
|
|
606
752
|
}
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return;
|
|
611
|
-
}
|
|
753
|
+
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
754
|
+
if (!useRawBatchPath) {
|
|
755
|
+
return items.map(item => item.get());
|
|
612
756
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
757
|
+
const rawValues = new Array(items.length);
|
|
758
|
+
const keysToFetch = [];
|
|
759
|
+
const keyIndexes = [];
|
|
760
|
+
items.forEach((item, index) => {
|
|
761
|
+
if (scope === StorageScope.Secure) {
|
|
762
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
763
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (item._readCacheEnabled === true) {
|
|
768
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
769
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
keysToFetch.push(item.key);
|
|
774
|
+
keyIndexes.push(index);
|
|
623
775
|
});
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
776
|
+
if (keysToFetch.length > 0) {
|
|
777
|
+
const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
|
|
778
|
+
fetchedValues.forEach((value, index) => {
|
|
779
|
+
const key = keysToFetch[index];
|
|
780
|
+
const targetIndex = keyIndexes[index];
|
|
781
|
+
if (key === undefined || targetIndex === undefined) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
rawValues[targetIndex] = value;
|
|
785
|
+
cacheRawValue(scope, key, value);
|
|
786
|
+
});
|
|
629
787
|
}
|
|
630
|
-
return
|
|
631
|
-
|
|
788
|
+
return items.map((item, index) => {
|
|
789
|
+
const raw = rawValues[index];
|
|
790
|
+
if (raw === undefined) {
|
|
791
|
+
return asInternal(item)._defaultValue;
|
|
792
|
+
}
|
|
793
|
+
return item.deserialize(raw);
|
|
794
|
+
});
|
|
795
|
+
}, items.length);
|
|
632
796
|
}
|
|
633
797
|
export function setBatch(items, scope) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
798
|
+
measureOperation("batch:set", scope, () => {
|
|
799
|
+
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
800
|
+
if (scope === StorageScope.Memory) {
|
|
801
|
+
items.forEach(({
|
|
802
|
+
item,
|
|
803
|
+
value
|
|
804
|
+
}) => item.set(value));
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
if (scope === StorageScope.Secure) {
|
|
808
|
+
const secureEntries = items.map(({
|
|
809
|
+
item,
|
|
810
|
+
value
|
|
811
|
+
}) => ({
|
|
812
|
+
item,
|
|
813
|
+
value,
|
|
814
|
+
internal: asInternal(item)
|
|
815
|
+
}));
|
|
816
|
+
const canUseSecureBatchPath = secureEntries.every(({
|
|
817
|
+
internal
|
|
818
|
+
}) => canUseSecureRawBatchPath(internal));
|
|
819
|
+
if (!canUseSecureBatchPath) {
|
|
820
|
+
items.forEach(({
|
|
821
|
+
item,
|
|
822
|
+
value
|
|
823
|
+
}) => item.set(value));
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
flushSecureWrites();
|
|
827
|
+
const storageModule = getStorageModule();
|
|
828
|
+
const groupedByAccessControl = new Map();
|
|
829
|
+
secureEntries.forEach(({
|
|
830
|
+
item,
|
|
831
|
+
value,
|
|
832
|
+
internal
|
|
833
|
+
}) => {
|
|
834
|
+
const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
835
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
836
|
+
const group = existingGroup ?? {
|
|
837
|
+
keys: [],
|
|
838
|
+
values: []
|
|
839
|
+
};
|
|
840
|
+
group.keys.push(item.key);
|
|
841
|
+
group.values.push(item.serialize(value));
|
|
842
|
+
if (!existingGroup) {
|
|
843
|
+
groupedByAccessControl.set(accessControl, group);
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
847
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
848
|
+
storageModule.setBatch(group.keys, group.values, scope);
|
|
849
|
+
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
850
|
+
});
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const useRawBatchPath = items.every(({
|
|
854
|
+
item
|
|
855
|
+
}) => canUseRawBatchPath(asInternal(item)));
|
|
856
|
+
if (!useRawBatchPath) {
|
|
857
|
+
items.forEach(({
|
|
858
|
+
item,
|
|
859
|
+
value
|
|
860
|
+
}) => item.set(value));
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
const keys = items.map(entry => entry.item.key);
|
|
864
|
+
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
865
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
866
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
867
|
+
}, items.length);
|
|
660
868
|
}
|
|
661
869
|
export function removeBatch(items, scope) {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
870
|
+
measureOperation("batch:remove", scope, () => {
|
|
871
|
+
assertBatchScope(items, scope);
|
|
872
|
+
if (scope === StorageScope.Memory) {
|
|
873
|
+
items.forEach(item => item.delete());
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
const keys = items.map(item => item.key);
|
|
877
|
+
if (scope === StorageScope.Secure) {
|
|
878
|
+
flushSecureWrites();
|
|
879
|
+
}
|
|
880
|
+
getStorageModule().removeBatch(keys, scope);
|
|
881
|
+
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
882
|
+
}, items.length);
|
|
673
883
|
}
|
|
674
884
|
export function registerMigration(version, migration) {
|
|
675
885
|
if (!Number.isInteger(version) || version <= 0) {
|
|
@@ -681,93 +891,133 @@ export function registerMigration(version, migration) {
|
|
|
681
891
|
registeredMigrations.set(version, migration);
|
|
682
892
|
}
|
|
683
893
|
export function migrateToLatest(scope = StorageScope.Disk) {
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
894
|
+
return measureOperation("migration:run", scope, () => {
|
|
895
|
+
assertValidScope(scope);
|
|
896
|
+
const currentVersion = readMigrationVersion(scope);
|
|
897
|
+
const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
|
|
898
|
+
let appliedVersion = currentVersion;
|
|
899
|
+
const context = {
|
|
900
|
+
scope,
|
|
901
|
+
getRaw: key => getRawValue(key, scope),
|
|
902
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
903
|
+
removeRaw: key => removeRawValue(key, scope)
|
|
904
|
+
};
|
|
905
|
+
versions.forEach(version => {
|
|
906
|
+
const migration = registeredMigrations.get(version);
|
|
907
|
+
if (!migration) {
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
migration(context);
|
|
911
|
+
writeMigrationVersion(scope, version);
|
|
912
|
+
appliedVersion = version;
|
|
913
|
+
});
|
|
914
|
+
return appliedVersion;
|
|
702
915
|
});
|
|
703
|
-
return appliedVersion;
|
|
704
916
|
}
|
|
705
917
|
export function runTransaction(scope, transaction) {
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
const rollback = new Map();
|
|
711
|
-
const rememberRollback = key => {
|
|
712
|
-
if (rollback.has(key)) {
|
|
713
|
-
return;
|
|
714
|
-
}
|
|
715
|
-
rollback.set(key, getRawValue(key, scope));
|
|
716
|
-
};
|
|
717
|
-
const tx = {
|
|
718
|
-
scope,
|
|
719
|
-
getRaw: key => getRawValue(key, scope),
|
|
720
|
-
setRaw: (key, value) => {
|
|
721
|
-
rememberRollback(key);
|
|
722
|
-
setRawValue(key, value, scope);
|
|
723
|
-
},
|
|
724
|
-
removeRaw: key => {
|
|
725
|
-
rememberRollback(key);
|
|
726
|
-
removeRawValue(key, scope);
|
|
727
|
-
},
|
|
728
|
-
getItem: item => {
|
|
729
|
-
assertBatchScope([item], scope);
|
|
730
|
-
return item.get();
|
|
731
|
-
},
|
|
732
|
-
setItem: (item, value) => {
|
|
733
|
-
assertBatchScope([item], scope);
|
|
734
|
-
rememberRollback(item.key);
|
|
735
|
-
item.set(value);
|
|
736
|
-
},
|
|
737
|
-
removeItem: item => {
|
|
738
|
-
assertBatchScope([item], scope);
|
|
739
|
-
rememberRollback(item.key);
|
|
740
|
-
item.delete();
|
|
918
|
+
return measureOperation("transaction:run", scope, () => {
|
|
919
|
+
assertValidScope(scope);
|
|
920
|
+
if (scope === StorageScope.Secure) {
|
|
921
|
+
flushSecureWrites();
|
|
741
922
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
923
|
+
const rollback = new Map();
|
|
924
|
+
const rememberRollback = key => {
|
|
925
|
+
if (rollback.has(key)) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
rollback.set(key, getRawValue(key, scope));
|
|
929
|
+
};
|
|
930
|
+
const tx = {
|
|
931
|
+
scope,
|
|
932
|
+
getRaw: key => getRawValue(key, scope),
|
|
933
|
+
setRaw: (key, value) => {
|
|
934
|
+
rememberRollback(key);
|
|
935
|
+
setRawValue(key, value, scope);
|
|
936
|
+
},
|
|
937
|
+
removeRaw: key => {
|
|
938
|
+
rememberRollback(key);
|
|
748
939
|
removeRawValue(key, scope);
|
|
940
|
+
},
|
|
941
|
+
getItem: item => {
|
|
942
|
+
assertBatchScope([item], scope);
|
|
943
|
+
return item.get();
|
|
944
|
+
},
|
|
945
|
+
setItem: (item, value) => {
|
|
946
|
+
assertBatchScope([item], scope);
|
|
947
|
+
rememberRollback(item.key);
|
|
948
|
+
item.set(value);
|
|
949
|
+
},
|
|
950
|
+
removeItem: item => {
|
|
951
|
+
assertBatchScope([item], scope);
|
|
952
|
+
rememberRollback(item.key);
|
|
953
|
+
item.delete();
|
|
954
|
+
}
|
|
955
|
+
};
|
|
956
|
+
try {
|
|
957
|
+
return transaction(tx);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
960
|
+
if (scope === StorageScope.Memory) {
|
|
961
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
962
|
+
if (previousValue === undefined) {
|
|
963
|
+
removeRawValue(key, scope);
|
|
964
|
+
} else {
|
|
965
|
+
setRawValue(key, previousValue, scope);
|
|
966
|
+
}
|
|
967
|
+
});
|
|
749
968
|
} else {
|
|
750
|
-
|
|
969
|
+
const keysToSet = [];
|
|
970
|
+
const valuesToSet = [];
|
|
971
|
+
const keysToRemove = [];
|
|
972
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
973
|
+
if (previousValue === undefined) {
|
|
974
|
+
keysToRemove.push(key);
|
|
975
|
+
} else {
|
|
976
|
+
keysToSet.push(key);
|
|
977
|
+
valuesToSet.push(previousValue);
|
|
978
|
+
}
|
|
979
|
+
});
|
|
980
|
+
if (scope === StorageScope.Secure) {
|
|
981
|
+
flushSecureWrites();
|
|
982
|
+
}
|
|
983
|
+
if (keysToSet.length > 0) {
|
|
984
|
+
getStorageModule().setBatch(keysToSet, valuesToSet, scope);
|
|
985
|
+
keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
|
|
986
|
+
}
|
|
987
|
+
if (keysToRemove.length > 0) {
|
|
988
|
+
getStorageModule().removeBatch(keysToRemove, scope);
|
|
989
|
+
keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
|
|
990
|
+
}
|
|
751
991
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
}
|
|
992
|
+
throw error;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
755
995
|
}
|
|
756
996
|
export function createSecureAuthStorage(config, options) {
|
|
757
997
|
const ns = options?.namespace ?? "auth";
|
|
758
998
|
const result = {};
|
|
759
|
-
for (const key of
|
|
999
|
+
for (const key of typedKeys(config)) {
|
|
760
1000
|
const itemConfig = config[key];
|
|
1001
|
+
const expirationConfig = itemConfig.ttlMs !== undefined ? {
|
|
1002
|
+
ttlMs: itemConfig.ttlMs
|
|
1003
|
+
} : undefined;
|
|
761
1004
|
result[key] = createStorageItem({
|
|
762
1005
|
key,
|
|
763
1006
|
scope: StorageScope.Secure,
|
|
764
1007
|
defaultValue: "",
|
|
765
1008
|
namespace: ns,
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
1009
|
+
...(itemConfig.biometric !== undefined ? {
|
|
1010
|
+
biometric: itemConfig.biometric
|
|
1011
|
+
} : {}),
|
|
1012
|
+
...(itemConfig.biometricLevel !== undefined ? {
|
|
1013
|
+
biometricLevel: itemConfig.biometricLevel
|
|
1014
|
+
} : {}),
|
|
1015
|
+
...(itemConfig.accessControl !== undefined ? {
|
|
1016
|
+
accessControl: itemConfig.accessControl
|
|
1017
|
+
} : {}),
|
|
1018
|
+
...(expirationConfig !== undefined ? {
|
|
1019
|
+
expiration: expirationConfig
|
|
1020
|
+
} : {})
|
|
771
1021
|
});
|
|
772
1022
|
}
|
|
773
1023
|
return result;
|