react-native-nitro-storage 0.3.2 → 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 +141 -30
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
- package/cpp/bindings/HybridStorage.cpp +167 -22
- package/cpp/bindings/HybridStorage.hpp +12 -1
- package/cpp/core/NativeStorageAdapter.hpp +3 -0
- package/ios/IOSStorageAdapterCpp.hpp +16 -0
- package/ios/IOSStorageAdapterCpp.mm +135 -11
- package/lib/commonjs/index.js +466 -275
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +564 -270
- 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/module/index.js +466 -277
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +564 -272
- 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/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +38 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +40 -1
- 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/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +1 -1
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +616 -296
- package/src/index.web.ts +728 -288
- package/src/internal.ts +28 -0
package/lib/module/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
import { NitroModules } from "react-native-nitro-modules";
|
|
4
|
-
import { StorageScope, AccessControl } from "./Storage.types";
|
|
5
|
-
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";
|
|
6
6
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
7
7
|
export { migrateFromMMKV } from "./migration";
|
|
8
8
|
function asInternal(item) {
|
|
@@ -33,6 +33,36 @@ const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Se
|
|
|
33
33
|
const pendingSecureWrites = new Map();
|
|
34
34
|
let secureFlushScheduled = false;
|
|
35
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
|
+
}
|
|
36
66
|
function getScopedListeners(scope) {
|
|
37
67
|
return scopedListeners.get(scope);
|
|
38
68
|
}
|
|
@@ -93,34 +123,47 @@ function flushSecureWrites() {
|
|
|
93
123
|
}
|
|
94
124
|
const writes = Array.from(pendingSecureWrites.values());
|
|
95
125
|
pendingSecureWrites.clear();
|
|
96
|
-
const
|
|
97
|
-
const valuesToSet = [];
|
|
126
|
+
const groupedSetWrites = new Map();
|
|
98
127
|
const keysToRemove = [];
|
|
99
128
|
writes.forEach(({
|
|
100
129
|
key,
|
|
101
|
-
value
|
|
130
|
+
value,
|
|
131
|
+
accessControl
|
|
102
132
|
}) => {
|
|
103
133
|
if (value === undefined) {
|
|
104
134
|
keysToRemove.push(key);
|
|
105
135
|
} else {
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
147
|
}
|
|
109
148
|
});
|
|
110
149
|
const storageModule = getStorageModule();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
storageModule.setBatch(
|
|
114
|
-
}
|
|
150
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
151
|
+
storageModule.setSecureAccessControl(accessControl);
|
|
152
|
+
storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
153
|
+
});
|
|
115
154
|
if (keysToRemove.length > 0) {
|
|
116
155
|
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
117
156
|
}
|
|
118
157
|
}
|
|
119
|
-
function scheduleSecureWrite(key, value) {
|
|
120
|
-
|
|
158
|
+
function scheduleSecureWrite(key, value, accessControl) {
|
|
159
|
+
const pendingWrite = {
|
|
121
160
|
key,
|
|
122
161
|
value
|
|
123
|
-
}
|
|
162
|
+
};
|
|
163
|
+
if (accessControl !== undefined) {
|
|
164
|
+
pendingWrite.accessControl = accessControl;
|
|
165
|
+
}
|
|
166
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
124
167
|
if (secureFlushScheduled) {
|
|
125
168
|
return;
|
|
126
169
|
}
|
|
@@ -214,97 +257,180 @@ function writeMigrationVersion(scope, version) {
|
|
|
214
257
|
}
|
|
215
258
|
export const storage = {
|
|
216
259
|
clear: scope => {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
+
});
|
|
228
273
|
},
|
|
229
274
|
clearAll: () => {
|
|
230
|
-
storage
|
|
231
|
-
|
|
232
|
-
|
|
275
|
+
measureOperation("storage:clearAll", StorageScope.Memory, () => {
|
|
276
|
+
storage.clear(StorageScope.Memory);
|
|
277
|
+
storage.clear(StorageScope.Disk);
|
|
278
|
+
storage.clear(StorageScope.Secure);
|
|
279
|
+
}, 3);
|
|
233
280
|
},
|
|
234
281
|
clearNamespace: (namespace, scope) => {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
}
|
|
240
289
|
}
|
|
290
|
+
notifyAllListeners(memoryListeners);
|
|
291
|
+
return;
|
|
241
292
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
}
|
|
249
|
-
clearScopeRawCache(scope);
|
|
250
|
-
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
293
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
294
|
+
if (scope === StorageScope.Secure) {
|
|
295
|
+
flushSecureWrites();
|
|
296
|
+
}
|
|
297
|
+
clearScopeRawCache(scope);
|
|
298
|
+
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
299
|
+
});
|
|
251
300
|
},
|
|
252
301
|
clearBiometric: () => {
|
|
253
|
-
|
|
302
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
303
|
+
getStorageModule().clearSecureBiometric();
|
|
304
|
+
});
|
|
254
305
|
},
|
|
255
306
|
has: (key, scope) => {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
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
|
+
});
|
|
261
314
|
},
|
|
262
315
|
getAllKeys: scope => {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
});
|
|
268
358
|
},
|
|
269
359
|
getAll: scope => {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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;
|
|
275
375
|
});
|
|
276
376
|
return result;
|
|
277
|
-
}
|
|
278
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
279
|
-
if (keys.length === 0) return result;
|
|
280
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
281
|
-
keys.forEach((key, idx) => {
|
|
282
|
-
const val = decodeNativeBatchValue(values[idx]);
|
|
283
|
-
if (val !== undefined) result[key] = val;
|
|
284
377
|
});
|
|
285
|
-
return result;
|
|
286
378
|
},
|
|
287
379
|
size: scope => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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
|
+
});
|
|
293
387
|
},
|
|
294
388
|
setAccessControl: level => {
|
|
295
|
-
|
|
296
|
-
|
|
389
|
+
measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
|
|
390
|
+
secureDefaultAccessControl = level;
|
|
391
|
+
getStorageModule().setSecureAccessControl(level);
|
|
392
|
+
});
|
|
297
393
|
},
|
|
298
394
|
setSecureWritesAsync: enabled => {
|
|
299
|
-
|
|
395
|
+
measureOperation("storage:setSecureWritesAsync", StorageScope.Secure, () => {
|
|
396
|
+
getStorageModule().setSecureWritesAsync(enabled);
|
|
397
|
+
});
|
|
300
398
|
},
|
|
301
399
|
flushSecureWrites: () => {
|
|
302
|
-
flushSecureWrites()
|
|
400
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
401
|
+
flushSecureWrites();
|
|
402
|
+
});
|
|
303
403
|
},
|
|
304
404
|
setKeychainAccessGroup: group => {
|
|
305
|
-
|
|
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();
|
|
306
426
|
}
|
|
307
427
|
};
|
|
428
|
+
export function setWebSecureStorageBackend(_backend) {
|
|
429
|
+
// Native platforms do not use web secure backends.
|
|
430
|
+
}
|
|
431
|
+
export function getWebSecureStorageBackend() {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
308
434
|
function canUseRawBatchPath(item) {
|
|
309
435
|
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
310
436
|
}
|
|
@@ -322,7 +448,8 @@ export function createStorageItem(config) {
|
|
|
322
448
|
const serialize = config.serialize ?? defaultSerialize;
|
|
323
449
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
324
450
|
const isMemory = config.scope === StorageScope.Memory;
|
|
325
|
-
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;
|
|
326
453
|
const secureAccessControl = config.accessControl;
|
|
327
454
|
const validate = config.validate;
|
|
328
455
|
const onValidationError = config.onValidationError;
|
|
@@ -331,7 +458,7 @@ export function createStorageItem(config) {
|
|
|
331
458
|
const expirationTtlMs = expiration?.ttlMs;
|
|
332
459
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
333
460
|
const readCache = !isMemory && config.readCache === true;
|
|
334
|
-
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric
|
|
461
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
335
462
|
const defaultValue = config.defaultValue;
|
|
336
463
|
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
337
464
|
if (expiration && expiration.ttlMs <= 0) {
|
|
@@ -395,12 +522,12 @@ export function createStorageItem(config) {
|
|
|
395
522
|
};
|
|
396
523
|
const writeStoredRaw = rawValue => {
|
|
397
524
|
if (isBiometric) {
|
|
398
|
-
getStorageModule().
|
|
525
|
+
getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
399
526
|
return;
|
|
400
527
|
}
|
|
401
528
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
402
529
|
if (coalesceSecureWrites) {
|
|
403
|
-
scheduleSecureWrite(storageKey, rawValue);
|
|
530
|
+
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
404
531
|
return;
|
|
405
532
|
}
|
|
406
533
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -416,7 +543,7 @@ export function createStorageItem(config) {
|
|
|
416
543
|
}
|
|
417
544
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
418
545
|
if (coalesceSecureWrites) {
|
|
419
|
-
scheduleSecureWrite(storageKey, undefined);
|
|
546
|
+
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
420
547
|
return;
|
|
421
548
|
}
|
|
422
549
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -464,7 +591,7 @@ export function createStorageItem(config) {
|
|
|
464
591
|
}
|
|
465
592
|
return resolved;
|
|
466
593
|
};
|
|
467
|
-
const
|
|
594
|
+
const getInternal = () => {
|
|
468
595
|
const raw = readStoredRaw();
|
|
469
596
|
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
470
597
|
if (!expiration || lastExpiresAt === null) {
|
|
@@ -529,31 +656,52 @@ export function createStorageItem(config) {
|
|
|
529
656
|
hasLastValue = true;
|
|
530
657
|
return lastValue;
|
|
531
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
|
+
}));
|
|
532
668
|
const set = valueOrFn => {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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
|
+
});
|
|
539
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
|
+
});
|
|
540
686
|
const deleteItem = () => {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
if (
|
|
544
|
-
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;
|
|
545
696
|
}
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
removeStoredRaw();
|
|
697
|
+
removeStoredRaw();
|
|
698
|
+
});
|
|
551
699
|
};
|
|
552
|
-
const hasItem = () => {
|
|
700
|
+
const hasItem = () => measureOperation("item:has", config.scope, () => {
|
|
553
701
|
if (isMemory) return memoryStore.has(storageKey);
|
|
554
702
|
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
555
703
|
return getStorageModule().has(storageKey, config.scope);
|
|
556
|
-
};
|
|
704
|
+
});
|
|
557
705
|
const subscribe = callback => {
|
|
558
706
|
ensureSubscription();
|
|
559
707
|
listeners.add(callback);
|
|
@@ -570,7 +718,9 @@ export function createStorageItem(config) {
|
|
|
570
718
|
};
|
|
571
719
|
const storageItem = {
|
|
572
720
|
get,
|
|
721
|
+
getWithVersion,
|
|
573
722
|
set,
|
|
723
|
+
setIfVersion,
|
|
574
724
|
delete: deleteItem,
|
|
575
725
|
has: hasItem,
|
|
576
726
|
subscribe,
|
|
@@ -584,6 +734,7 @@ export function createStorageItem(config) {
|
|
|
584
734
|
_hasExpiration: expiration !== undefined,
|
|
585
735
|
_readCacheEnabled: readCache,
|
|
586
736
|
_isBiometric: isBiometric,
|
|
737
|
+
_defaultValue: defaultValue,
|
|
587
738
|
...(secureAccessControl !== undefined ? {
|
|
588
739
|
_secureAccessControl: secureAccessControl
|
|
589
740
|
} : {}),
|
|
@@ -594,136 +745,141 @@ export function createStorageItem(config) {
|
|
|
594
745
|
}
|
|
595
746
|
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
596
747
|
export function getBatch(items, scope) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
602
|
-
if (!useRawBatchPath) {
|
|
603
|
-
return items.map(item => item.get());
|
|
604
|
-
}
|
|
605
|
-
const useBatchCache = items.every(item => item._readCacheEnabled === true);
|
|
606
|
-
const rawValues = new Array(items.length);
|
|
607
|
-
const keysToFetch = [];
|
|
608
|
-
const keyIndexes = [];
|
|
609
|
-
items.forEach((item, index) => {
|
|
610
|
-
if (scope === StorageScope.Secure) {
|
|
611
|
-
if (hasPendingSecureWrite(item.key)) {
|
|
612
|
-
rawValues[index] = readPendingSecureWrite(item.key);
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
748
|
+
return measureOperation("batch:get", scope, () => {
|
|
749
|
+
assertBatchScope(items, scope);
|
|
750
|
+
if (scope === StorageScope.Memory) {
|
|
751
|
+
return items.map(item => item.get());
|
|
615
752
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
753
|
+
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
754
|
+
if (!useRawBatchPath) {
|
|
755
|
+
return items.map(item => item.get());
|
|
621
756
|
}
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
return;
|
|
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
|
+
}
|
|
632
766
|
}
|
|
633
|
-
|
|
634
|
-
|
|
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);
|
|
635
775
|
});
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
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
|
+
});
|
|
641
787
|
}
|
|
642
|
-
return
|
|
643
|
-
|
|
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);
|
|
644
796
|
}
|
|
645
797
|
export function setBatch(items, scope) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
item,
|
|
650
|
-
value
|
|
651
|
-
}) => item.set(value));
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
if (scope === StorageScope.Secure) {
|
|
655
|
-
const secureEntries = items.map(({
|
|
656
|
-
item,
|
|
657
|
-
value
|
|
658
|
-
}) => ({
|
|
659
|
-
item,
|
|
660
|
-
value,
|
|
661
|
-
internal: asInternal(item)
|
|
662
|
-
}));
|
|
663
|
-
const canUseSecureBatchPath = secureEntries.every(({
|
|
664
|
-
internal
|
|
665
|
-
}) => canUseSecureRawBatchPath(internal));
|
|
666
|
-
if (!canUseSecureBatchPath) {
|
|
798
|
+
measureOperation("batch:set", scope, () => {
|
|
799
|
+
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
800
|
+
if (scope === StorageScope.Memory) {
|
|
667
801
|
items.forEach(({
|
|
668
802
|
item,
|
|
669
803
|
value
|
|
670
804
|
}) => item.set(value));
|
|
671
805
|
return;
|
|
672
806
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
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;
|
|
691
825
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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);
|
|
714
868
|
}
|
|
715
869
|
export function removeBatch(items, scope) {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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);
|
|
727
883
|
}
|
|
728
884
|
export function registerMigration(version, migration) {
|
|
729
885
|
if (!Number.isInteger(version) || version <= 0) {
|
|
@@ -735,77 +891,107 @@ export function registerMigration(version, migration) {
|
|
|
735
891
|
registeredMigrations.set(version, migration);
|
|
736
892
|
}
|
|
737
893
|
export function migrateToLatest(scope = StorageScope.Disk) {
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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;
|
|
756
915
|
});
|
|
757
|
-
return appliedVersion;
|
|
758
916
|
}
|
|
759
917
|
export function runTransaction(scope, transaction) {
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
const rollback = new Map();
|
|
765
|
-
const rememberRollback = key => {
|
|
766
|
-
if (rollback.has(key)) {
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
rollback.set(key, getRawValue(key, scope));
|
|
770
|
-
};
|
|
771
|
-
const tx = {
|
|
772
|
-
scope,
|
|
773
|
-
getRaw: key => getRawValue(key, scope),
|
|
774
|
-
setRaw: (key, value) => {
|
|
775
|
-
rememberRollback(key);
|
|
776
|
-
setRawValue(key, value, scope);
|
|
777
|
-
},
|
|
778
|
-
removeRaw: key => {
|
|
779
|
-
rememberRollback(key);
|
|
780
|
-
removeRawValue(key, scope);
|
|
781
|
-
},
|
|
782
|
-
getItem: item => {
|
|
783
|
-
assertBatchScope([item], scope);
|
|
784
|
-
return item.get();
|
|
785
|
-
},
|
|
786
|
-
setItem: (item, value) => {
|
|
787
|
-
assertBatchScope([item], scope);
|
|
788
|
-
rememberRollback(item.key);
|
|
789
|
-
item.set(value);
|
|
790
|
-
},
|
|
791
|
-
removeItem: item => {
|
|
792
|
-
assertBatchScope([item], scope);
|
|
793
|
-
rememberRollback(item.key);
|
|
794
|
-
item.delete();
|
|
918
|
+
return measureOperation("transaction:run", scope, () => {
|
|
919
|
+
assertValidScope(scope);
|
|
920
|
+
if (scope === StorageScope.Secure) {
|
|
921
|
+
flushSecureWrites();
|
|
795
922
|
}
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
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);
|
|
802
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
|
+
});
|
|
803
968
|
} else {
|
|
804
|
-
|
|
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
|
+
}
|
|
805
991
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
}
|
|
992
|
+
throw error;
|
|
993
|
+
}
|
|
994
|
+
});
|
|
809
995
|
}
|
|
810
996
|
export function createSecureAuthStorage(config, options) {
|
|
811
997
|
const ns = options?.namespace ?? "auth";
|
|
@@ -823,6 +1009,9 @@ export function createSecureAuthStorage(config, options) {
|
|
|
823
1009
|
...(itemConfig.biometric !== undefined ? {
|
|
824
1010
|
biometric: itemConfig.biometric
|
|
825
1011
|
} : {}),
|
|
1012
|
+
...(itemConfig.biometricLevel !== undefined ? {
|
|
1013
|
+
biometricLevel: itemConfig.biometricLevel
|
|
1014
|
+
} : {}),
|
|
826
1015
|
...(itemConfig.accessControl !== undefined ? {
|
|
827
1016
|
accessControl: itemConfig.accessControl
|
|
828
1017
|
} : {}),
|