react-native-nitro-storage 0.4.5 → 0.5.1
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 +254 -945
- package/SECURITY.md +26 -0
- package/docs/api-reference.md +281 -0
- package/docs/batch-transactions-migrations.md +200 -0
- package/docs/benchmarks.md +37 -0
- package/docs/mmkv-migration.md +80 -0
- package/docs/react-hooks.md +113 -0
- package/docs/recipes.md +302 -0
- package/docs/secure-storage.md +190 -0
- package/docs/web-backends.md +141 -0
- package/lib/commonjs/index.js +265 -14
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +220 -11
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-events.js +117 -0
- package/lib/commonjs/storage-events.js.map +1 -0
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/module/index.js +265 -14
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +220 -11
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-events.js +112 -0
- package/lib/module/storage-events.js.map +1 -0
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/typescript/index.d.ts +19 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +19 -2
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-events.d.ts +37 -0
- package/lib/typescript/storage-events.d.ts.map +1 -0
- package/lib/typescript/storage-runtime.d.ts +32 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/package.json +25 -11
- package/src/index.ts +601 -14
- package/src/index.web.ts +535 -22
- package/src/storage-events.ts +184 -0
- package/src/storage-runtime.ts +35 -0
package/lib/module/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { NitroModules } from "react-native-nitro-modules";
|
|
|
4
4
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
5
5
|
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
|
|
6
6
|
import { getStorageErrorCode, isLockedStorageErrorCode } from "./storage-runtime";
|
|
7
|
+
import { StorageEventRegistry } from "./storage-events";
|
|
7
8
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
8
9
|
export { migrateFromMMKV } from "./migration";
|
|
9
10
|
export { getStorageErrorCode } from "./storage-runtime";
|
|
@@ -39,8 +40,12 @@ let diskWritesAsync = false;
|
|
|
39
40
|
const pendingSecureWrites = new Map();
|
|
40
41
|
let secureFlushScheduled = false;
|
|
41
42
|
let secureDefaultAccessControl = AccessControl.WhenUnlocked;
|
|
43
|
+
const suppressedNativeEvents = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
42
44
|
let metricsObserver;
|
|
45
|
+
let eventObserver;
|
|
43
46
|
const metricsCounters = new Map();
|
|
47
|
+
const storageEvents = new StorageEventRegistry();
|
|
48
|
+
const nativeSecureBackend = "platform-secure-storage";
|
|
44
49
|
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
45
50
|
const existing = metricsCounters.get(operation);
|
|
46
51
|
if (!existing) {
|
|
@@ -90,6 +95,23 @@ function hasCachedRawValue(scope, key) {
|
|
|
90
95
|
function clearScopeRawCache(scope) {
|
|
91
96
|
getScopeRawCache(scope).clear();
|
|
92
97
|
}
|
|
98
|
+
function suppressNativeEvent(scope, key) {
|
|
99
|
+
const suppressedEvents = suppressedNativeEvents.get(scope);
|
|
100
|
+
suppressedEvents.set(key, (suppressedEvents.get(key) ?? 0) + 1);
|
|
101
|
+
}
|
|
102
|
+
function consumeSuppressedNativeEvent(scope, key) {
|
|
103
|
+
const suppressedEvents = suppressedNativeEvents.get(scope);
|
|
104
|
+
const count = suppressedEvents.get(key);
|
|
105
|
+
if (count === undefined) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
if (count <= 1) {
|
|
109
|
+
suppressedEvents.delete(key);
|
|
110
|
+
} else {
|
|
111
|
+
suppressedEvents.set(key, count - 1);
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
93
115
|
function notifyKeyListeners(registry, key) {
|
|
94
116
|
const listeners = registry.get(key);
|
|
95
117
|
if (listeners) {
|
|
@@ -123,6 +145,52 @@ function addKeyListener(registry, key, listener) {
|
|
|
123
145
|
}
|
|
124
146
|
};
|
|
125
147
|
}
|
|
148
|
+
function getEventRawValue(scope, key) {
|
|
149
|
+
if (scope === StorageScope.Memory) {
|
|
150
|
+
const value = memoryStore.get(key);
|
|
151
|
+
return typeof value === "string" ? value : undefined;
|
|
152
|
+
}
|
|
153
|
+
return getRawValue(key, scope);
|
|
154
|
+
}
|
|
155
|
+
function createKeyChange(scope, key, oldValue, newValue, operation, source) {
|
|
156
|
+
return {
|
|
157
|
+
type: "key",
|
|
158
|
+
scope,
|
|
159
|
+
key,
|
|
160
|
+
oldValue,
|
|
161
|
+
newValue,
|
|
162
|
+
operation,
|
|
163
|
+
source
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function hasStorageChangeObservers(scope) {
|
|
167
|
+
return storageEvents.hasListeners(scope) || eventObserver !== undefined;
|
|
168
|
+
}
|
|
169
|
+
function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
|
|
170
|
+
if (source === "native" && operation !== "external" && scope !== StorageScope.Memory && scopedUnsubscribers.has(scope)) {
|
|
171
|
+
suppressNativeEvent(scope, key);
|
|
172
|
+
}
|
|
173
|
+
const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
|
|
174
|
+
storageEvents.emitKey(event);
|
|
175
|
+
eventObserver?.(event);
|
|
176
|
+
}
|
|
177
|
+
function emitBatchChange(scope, operation, source, changes) {
|
|
178
|
+
if (changes.length === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (source === "native" && operation !== "external" && scope !== StorageScope.Memory && scopedUnsubscribers.has(scope)) {
|
|
182
|
+
changes.forEach(change => suppressNativeEvent(scope, change.key));
|
|
183
|
+
}
|
|
184
|
+
const event = {
|
|
185
|
+
type: "batch",
|
|
186
|
+
scope,
|
|
187
|
+
operation,
|
|
188
|
+
source,
|
|
189
|
+
changes
|
|
190
|
+
};
|
|
191
|
+
storageEvents.emitBatch(event);
|
|
192
|
+
eventObserver?.(event);
|
|
193
|
+
}
|
|
126
194
|
function readPendingSecureWrite(key) {
|
|
127
195
|
return pendingSecureWrites.get(key)?.value;
|
|
128
196
|
}
|
|
@@ -259,14 +327,19 @@ function ensureNativeScopeSubscription(scope) {
|
|
|
259
327
|
notifyAllListeners(getScopedListeners(scope));
|
|
260
328
|
return;
|
|
261
329
|
}
|
|
330
|
+
const oldValue = readCachedRawValue(scope, key);
|
|
262
331
|
cacheRawValue(scope, key, value);
|
|
263
332
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
333
|
+
if (consumeSuppressedNativeEvent(scope, key)) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
emitKeyChange(scope, key, oldValue, value, "external", "native");
|
|
264
337
|
});
|
|
265
|
-
scopedUnsubscribers.set(scope, unsubscribe);
|
|
338
|
+
scopedUnsubscribers.set(scope, typeof unsubscribe === "function" ? unsubscribe : () => {});
|
|
266
339
|
}
|
|
267
340
|
function maybeCleanupNativeScopeSubscription(scope) {
|
|
268
341
|
const listeners = getScopedListeners(scope);
|
|
269
|
-
if (listeners.size > 0) {
|
|
342
|
+
if (listeners.size > 0 || storageEvents.hasListeners(scope) || eventObserver !== undefined) {
|
|
270
343
|
return;
|
|
271
344
|
}
|
|
272
345
|
const unsubscribe = scopedUnsubscribers.get(scope);
|
|
@@ -292,15 +365,18 @@ function getRawValue(key, scope) {
|
|
|
292
365
|
}
|
|
293
366
|
function setRawValue(key, value, scope) {
|
|
294
367
|
assertValidScope(scope);
|
|
368
|
+
const oldValue = scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
295
369
|
if (scope === StorageScope.Memory) {
|
|
296
370
|
memoryStore.set(key, value);
|
|
297
371
|
notifyKeyListeners(memoryListeners, key);
|
|
372
|
+
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
298
373
|
return;
|
|
299
374
|
}
|
|
300
375
|
if (scope === StorageScope.Disk) {
|
|
301
376
|
cacheRawValue(scope, key, value);
|
|
302
377
|
if (diskWritesAsync) {
|
|
303
378
|
scheduleDiskWrite(key, value);
|
|
379
|
+
emitKeyChange(scope, key, oldValue, value, "set", "native");
|
|
304
380
|
return;
|
|
305
381
|
}
|
|
306
382
|
flushDiskWrites();
|
|
@@ -313,18 +389,22 @@ function setRawValue(key, value, scope) {
|
|
|
313
389
|
}
|
|
314
390
|
getStorageModule().set(key, value, scope);
|
|
315
391
|
cacheRawValue(scope, key, value);
|
|
392
|
+
emitKeyChange(scope, key, oldValue, value, "set", "native");
|
|
316
393
|
}
|
|
317
394
|
function removeRawValue(key, scope) {
|
|
318
395
|
assertValidScope(scope);
|
|
396
|
+
const oldValue = getEventRawValue(scope, key);
|
|
319
397
|
if (scope === StorageScope.Memory) {
|
|
320
398
|
memoryStore.delete(key);
|
|
321
399
|
notifyKeyListeners(memoryListeners, key);
|
|
400
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
322
401
|
return;
|
|
323
402
|
}
|
|
324
403
|
if (scope === StorageScope.Disk) {
|
|
325
404
|
cacheRawValue(scope, key, undefined);
|
|
326
405
|
if (diskWritesAsync) {
|
|
327
406
|
scheduleDiskWrite(key, undefined);
|
|
407
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
|
|
328
408
|
return;
|
|
329
409
|
}
|
|
330
410
|
flushDiskWrites();
|
|
@@ -336,6 +416,7 @@ function removeRawValue(key, scope) {
|
|
|
336
416
|
}
|
|
337
417
|
getStorageModule().remove(key, scope);
|
|
338
418
|
cacheRawValue(scope, key, undefined);
|
|
419
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
|
|
339
420
|
}
|
|
340
421
|
function readMigrationVersion(scope) {
|
|
341
422
|
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
@@ -349,11 +430,62 @@ function writeMigrationVersion(scope, version) {
|
|
|
349
430
|
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
350
431
|
}
|
|
351
432
|
export const storage = {
|
|
433
|
+
subscribe: (scope, listener) => {
|
|
434
|
+
assertValidScope(scope);
|
|
435
|
+
if (scope !== StorageScope.Memory) {
|
|
436
|
+
ensureNativeScopeSubscription(scope);
|
|
437
|
+
const unsubscribe = storageEvents.subscribe(scope, listener);
|
|
438
|
+
return () => {
|
|
439
|
+
unsubscribe();
|
|
440
|
+
maybeCleanupNativeScopeSubscription(scope);
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
return storageEvents.subscribe(scope, listener);
|
|
444
|
+
},
|
|
445
|
+
subscribeKey: (scope, key, listener) => {
|
|
446
|
+
assertValidScope(scope);
|
|
447
|
+
if (scope !== StorageScope.Memory) {
|
|
448
|
+
ensureNativeScopeSubscription(scope);
|
|
449
|
+
const unsubscribe = storageEvents.subscribeKey(scope, key, listener);
|
|
450
|
+
return () => {
|
|
451
|
+
unsubscribe();
|
|
452
|
+
maybeCleanupNativeScopeSubscription(scope);
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
return storageEvents.subscribeKey(scope, key, listener);
|
|
456
|
+
},
|
|
457
|
+
subscribePrefix: (scope, prefix, listener) => {
|
|
458
|
+
assertValidScope(scope);
|
|
459
|
+
if (scope !== StorageScope.Memory) {
|
|
460
|
+
ensureNativeScopeSubscription(scope);
|
|
461
|
+
const unsubscribe = storageEvents.subscribePrefix(scope, prefix, listener);
|
|
462
|
+
return () => {
|
|
463
|
+
unsubscribe();
|
|
464
|
+
maybeCleanupNativeScopeSubscription(scope);
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
return storageEvents.subscribePrefix(scope, prefix, listener);
|
|
468
|
+
},
|
|
469
|
+
subscribeNamespace: (namespace, scope, listener) => {
|
|
470
|
+
return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
|
|
471
|
+
},
|
|
472
|
+
setEventObserver: observer => {
|
|
473
|
+
eventObserver = observer;
|
|
474
|
+
if (observer) {
|
|
475
|
+
ensureNativeScopeSubscription(StorageScope.Disk);
|
|
476
|
+
ensureNativeScopeSubscription(StorageScope.Secure);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
maybeCleanupNativeScopeSubscription(StorageScope.Disk);
|
|
480
|
+
maybeCleanupNativeScopeSubscription(StorageScope.Secure);
|
|
481
|
+
},
|
|
352
482
|
clear: scope => {
|
|
353
483
|
measureOperation("storage:clear", scope, () => {
|
|
484
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
|
|
354
485
|
if (scope === StorageScope.Memory) {
|
|
355
486
|
memoryStore.clear();
|
|
356
487
|
notifyAllListeners(memoryListeners);
|
|
488
|
+
emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
|
|
357
489
|
return;
|
|
358
490
|
}
|
|
359
491
|
if (scope === StorageScope.Disk) {
|
|
@@ -366,6 +498,7 @@ export const storage = {
|
|
|
366
498
|
}
|
|
367
499
|
clearScopeRawCache(scope);
|
|
368
500
|
getStorageModule().clear(scope);
|
|
501
|
+
emitBatchChange(scope, "clear", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "native")));
|
|
369
502
|
});
|
|
370
503
|
},
|
|
371
504
|
clearAll: () => {
|
|
@@ -379,15 +512,26 @@ export const storage = {
|
|
|
379
512
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
380
513
|
assertValidScope(scope);
|
|
381
514
|
if (scope === StorageScope.Memory) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
515
|
+
const affectedKeys = Array.from(memoryStore.keys()).filter(key => isNamespaced(key, namespace));
|
|
516
|
+
const previousValues = affectedKeys.map(key => ({
|
|
517
|
+
key,
|
|
518
|
+
value: getEventRawValue(scope, key)
|
|
519
|
+
}));
|
|
520
|
+
if (affectedKeys.length === 0) {
|
|
521
|
+
return;
|
|
386
522
|
}
|
|
387
|
-
|
|
523
|
+
affectedKeys.forEach(key => {
|
|
524
|
+
memoryStore.delete(key);
|
|
525
|
+
});
|
|
526
|
+
affectedKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
|
|
527
|
+
emitBatchChange(scope, "clearNamespace", "memory", previousValues.map(({
|
|
528
|
+
key,
|
|
529
|
+
value
|
|
530
|
+
}) => createKeyChange(scope, key, value, undefined, "clearNamespace", "memory")));
|
|
388
531
|
return;
|
|
389
532
|
}
|
|
390
533
|
const keyPrefix = prefixKey(namespace, "");
|
|
534
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
|
|
391
535
|
if (scope === StorageScope.Disk) {
|
|
392
536
|
flushDiskWrites();
|
|
393
537
|
}
|
|
@@ -401,6 +545,7 @@ export const storage = {
|
|
|
401
545
|
}
|
|
402
546
|
}
|
|
403
547
|
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
548
|
+
emitBatchChange(scope, "clearNamespace", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "native")));
|
|
404
549
|
});
|
|
405
550
|
},
|
|
406
551
|
clearBiometric: () => {
|
|
@@ -450,7 +595,7 @@ export const storage = {
|
|
|
450
595
|
if (scope === StorageScope.Secure) {
|
|
451
596
|
flushSecureWrites();
|
|
452
597
|
}
|
|
453
|
-
return getStorageModule().getKeysByPrefix(prefix, scope);
|
|
598
|
+
return getStorageModule().getKeysByPrefix(prefix, scope) ?? [];
|
|
454
599
|
});
|
|
455
600
|
},
|
|
456
601
|
getByPrefix: (prefix, scope) => {
|
|
@@ -475,7 +620,7 @@ export const storage = {
|
|
|
475
620
|
if (scope === StorageScope.Secure) {
|
|
476
621
|
flushSecureWrites();
|
|
477
622
|
}
|
|
478
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
623
|
+
const values = getStorageModule().getBatch(keys, scope) ?? [];
|
|
479
624
|
keys.forEach((key, idx) => {
|
|
480
625
|
const value = decodeNativeBatchValue(values[idx]);
|
|
481
626
|
if (value !== undefined) {
|
|
@@ -490,9 +635,10 @@ export const storage = {
|
|
|
490
635
|
assertValidScope(scope);
|
|
491
636
|
const result = {};
|
|
492
637
|
if (scope === StorageScope.Memory) {
|
|
493
|
-
memoryStore.
|
|
638
|
+
for (const key of memoryStore.keys()) {
|
|
639
|
+
const value = memoryStore.get(key);
|
|
494
640
|
if (typeof value === "string") result[key] = value;
|
|
495
|
-
}
|
|
641
|
+
}
|
|
496
642
|
return result;
|
|
497
643
|
}
|
|
498
644
|
if (scope === StorageScope.Disk) {
|
|
@@ -501,9 +647,9 @@ export const storage = {
|
|
|
501
647
|
if (scope === StorageScope.Secure) {
|
|
502
648
|
flushSecureWrites();
|
|
503
649
|
}
|
|
504
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
650
|
+
const keys = getStorageModule().getAllKeys(scope) ?? [];
|
|
505
651
|
if (keys.length === 0) return result;
|
|
506
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
652
|
+
const values = getStorageModule().getBatch(keys, scope) ?? [];
|
|
507
653
|
keys.forEach((key, idx) => {
|
|
508
654
|
const val = decodeNativeBatchValue(values[idx]);
|
|
509
655
|
if (val !== undefined) result[key] = val;
|
|
@@ -511,6 +657,9 @@ export const storage = {
|
|
|
511
657
|
return result;
|
|
512
658
|
});
|
|
513
659
|
},
|
|
660
|
+
export: scope => {
|
|
661
|
+
return measureOperation("storage:export", scope, () => storage.getAll(scope));
|
|
662
|
+
},
|
|
514
663
|
size: scope => {
|
|
515
664
|
return measureOperation("storage:size", scope, () => {
|
|
516
665
|
assertValidScope(scope);
|
|
@@ -582,7 +731,7 @@ export const storage = {
|
|
|
582
731
|
platform: "native",
|
|
583
732
|
backend: {
|
|
584
733
|
disk: "platform-preferences",
|
|
585
|
-
secure:
|
|
734
|
+
secure: nativeSecureBackend
|
|
586
735
|
},
|
|
587
736
|
writeBuffering: {
|
|
588
737
|
disk: true,
|
|
@@ -590,6 +739,55 @@ export const storage = {
|
|
|
590
739
|
},
|
|
591
740
|
errorClassification: true
|
|
592
741
|
}),
|
|
742
|
+
getSecurityCapabilities: () => ({
|
|
743
|
+
platform: "native",
|
|
744
|
+
secureStorage: {
|
|
745
|
+
backend: nativeSecureBackend,
|
|
746
|
+
encrypted: "available",
|
|
747
|
+
accessControl: "unknown",
|
|
748
|
+
keychainAccessGroup: "unknown",
|
|
749
|
+
hardwareBacked: "unknown"
|
|
750
|
+
},
|
|
751
|
+
biometric: {
|
|
752
|
+
storage: "unknown",
|
|
753
|
+
prompt: "unknown",
|
|
754
|
+
biometryOnly: "unknown",
|
|
755
|
+
biometryOrPasscode: "unknown"
|
|
756
|
+
},
|
|
757
|
+
metadata: {
|
|
758
|
+
perKey: true,
|
|
759
|
+
listsWithoutValues: true,
|
|
760
|
+
persistsTimestamps: false
|
|
761
|
+
}
|
|
762
|
+
}),
|
|
763
|
+
getSecureMetadata: key => {
|
|
764
|
+
return measureOperation("storage:getSecureMetadata", StorageScope.Secure, () => {
|
|
765
|
+
flushSecureWrites();
|
|
766
|
+
const storageModule = getStorageModule();
|
|
767
|
+
const biometricProtected = storageModule.hasSecureBiometric(key);
|
|
768
|
+
const exists = biometricProtected || storageModule.has(key, StorageScope.Secure);
|
|
769
|
+
let kind = "missing";
|
|
770
|
+
if (exists) {
|
|
771
|
+
kind = biometricProtected ? "biometric" : "secure";
|
|
772
|
+
}
|
|
773
|
+
return {
|
|
774
|
+
key,
|
|
775
|
+
exists,
|
|
776
|
+
kind,
|
|
777
|
+
backend: nativeSecureBackend,
|
|
778
|
+
encrypted: "available",
|
|
779
|
+
hardwareBacked: "unknown",
|
|
780
|
+
biometricProtected,
|
|
781
|
+
valueExposed: false
|
|
782
|
+
};
|
|
783
|
+
});
|
|
784
|
+
},
|
|
785
|
+
getAllSecureMetadata: () => {
|
|
786
|
+
return measureOperation("storage:getAllSecureMetadata", StorageScope.Secure, () => {
|
|
787
|
+
flushSecureWrites();
|
|
788
|
+
return getStorageModule().getAllKeys(StorageScope.Secure).map(key => storage.getSecureMetadata(key));
|
|
789
|
+
});
|
|
790
|
+
},
|
|
593
791
|
getString: (key, scope) => {
|
|
594
792
|
return measureOperation("storage:getString", scope, () => {
|
|
595
793
|
return getRawValue(key, scope);
|
|
@@ -611,11 +809,13 @@ export const storage = {
|
|
|
611
809
|
assertValidScope(scope);
|
|
612
810
|
if (keys.length === 0) return;
|
|
613
811
|
const values = keys.map(k => data[k]);
|
|
812
|
+
const changes = keys.map((key, index) => createKeyChange(scope, key, getEventRawValue(scope, key), values[index], "import", scope === StorageScope.Memory ? "memory" : "native"));
|
|
614
813
|
if (scope === StorageScope.Memory) {
|
|
615
814
|
keys.forEach((key, index) => {
|
|
616
815
|
memoryStore.set(key, values[index]);
|
|
617
816
|
});
|
|
618
817
|
keys.forEach(key => notifyKeyListeners(memoryListeners, key));
|
|
818
|
+
emitBatchChange(scope, "import", "memory", changes);
|
|
619
819
|
return;
|
|
620
820
|
}
|
|
621
821
|
if (scope === StorageScope.Secure) {
|
|
@@ -624,6 +824,7 @@ export const storage = {
|
|
|
624
824
|
}
|
|
625
825
|
getStorageModule().setBatch(keys, values, scope);
|
|
626
826
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
827
|
+
emitBatchChange(scope, "import", "native", changes);
|
|
627
828
|
}, keys.length);
|
|
628
829
|
}
|
|
629
830
|
};
|
|
@@ -744,20 +945,24 @@ export function createStorageItem(config) {
|
|
|
744
945
|
return raw;
|
|
745
946
|
};
|
|
746
947
|
const writeStoredRaw = rawValue => {
|
|
948
|
+
const oldValue = undefined;
|
|
747
949
|
if (isBiometric) {
|
|
748
950
|
getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
951
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
749
952
|
return;
|
|
750
953
|
}
|
|
751
954
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
752
955
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
753
956
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
754
957
|
scheduleDiskWrite(storageKey, rawValue);
|
|
958
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
755
959
|
return;
|
|
756
960
|
}
|
|
757
961
|
clearPendingDiskWrite(storageKey);
|
|
758
962
|
}
|
|
759
963
|
if (coalesceSecureWrites) {
|
|
760
964
|
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
965
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
761
966
|
return;
|
|
762
967
|
}
|
|
763
968
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -765,36 +970,44 @@ export function createStorageItem(config) {
|
|
|
765
970
|
getStorageModule().setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
|
|
766
971
|
}
|
|
767
972
|
getStorageModule().set(storageKey, rawValue, config.scope);
|
|
973
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
768
974
|
};
|
|
769
975
|
const removeStoredRaw = () => {
|
|
976
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
770
977
|
if (isBiometric) {
|
|
771
978
|
getStorageModule().deleteSecureBiometric(storageKey);
|
|
979
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
772
980
|
return;
|
|
773
981
|
}
|
|
774
982
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
775
983
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
776
984
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
777
985
|
scheduleDiskWrite(storageKey, undefined);
|
|
986
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
778
987
|
return;
|
|
779
988
|
}
|
|
780
989
|
clearPendingDiskWrite(storageKey);
|
|
781
990
|
}
|
|
782
991
|
if (coalesceSecureWrites) {
|
|
783
992
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
993
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
784
994
|
return;
|
|
785
995
|
}
|
|
786
996
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
787
997
|
clearPendingSecureWrite(storageKey);
|
|
788
998
|
}
|
|
789
999
|
getStorageModule().remove(storageKey, config.scope);
|
|
1000
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
790
1001
|
};
|
|
791
1002
|
const writeValueWithoutValidation = value => {
|
|
792
1003
|
if (isMemory) {
|
|
1004
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
793
1005
|
if (memoryExpiration) {
|
|
794
1006
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
795
1007
|
}
|
|
796
1008
|
memoryStore.set(storageKey, value);
|
|
797
1009
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1010
|
+
emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
|
|
798
1011
|
return;
|
|
799
1012
|
}
|
|
800
1013
|
const serialized = serialize(value);
|
|
@@ -926,11 +1139,13 @@ export function createStorageItem(config) {
|
|
|
926
1139
|
measureOperation("item:delete", config.scope, () => {
|
|
927
1140
|
invalidateParsedCache();
|
|
928
1141
|
if (isMemory) {
|
|
1142
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
929
1143
|
if (memoryExpiration) {
|
|
930
1144
|
memoryExpiration.delete(storageKey);
|
|
931
1145
|
}
|
|
932
1146
|
memoryStore.delete(storageKey);
|
|
933
1147
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1148
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
934
1149
|
return;
|
|
935
1150
|
}
|
|
936
1151
|
removeStoredRaw();
|
|
@@ -967,6 +1182,22 @@ export function createStorageItem(config) {
|
|
|
967
1182
|
}
|
|
968
1183
|
};
|
|
969
1184
|
};
|
|
1185
|
+
const subscribeSelector = (selector, listener, options = {}) => {
|
|
1186
|
+
const isEqual = options.isEqual ?? Object.is;
|
|
1187
|
+
let currentValue = selector(getInternal());
|
|
1188
|
+
if (options.fireImmediately === true) {
|
|
1189
|
+
listener(currentValue, currentValue);
|
|
1190
|
+
}
|
|
1191
|
+
return subscribe(() => {
|
|
1192
|
+
const nextValue = selector(getInternal());
|
|
1193
|
+
if (isEqual(currentValue, nextValue)) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const previousValue = currentValue;
|
|
1197
|
+
currentValue = nextValue;
|
|
1198
|
+
listener(nextValue, previousValue);
|
|
1199
|
+
});
|
|
1200
|
+
};
|
|
970
1201
|
const storageItem = {
|
|
971
1202
|
get,
|
|
972
1203
|
getWithVersion,
|
|
@@ -975,6 +1206,7 @@ export function createStorageItem(config) {
|
|
|
975
1206
|
delete: deleteItem,
|
|
976
1207
|
has: hasItem,
|
|
977
1208
|
subscribe,
|
|
1209
|
+
subscribeSelector,
|
|
978
1210
|
serialize,
|
|
979
1211
|
deserialize,
|
|
980
1212
|
_triggerListeners: () => {
|
|
@@ -1078,6 +1310,10 @@ export function setBatch(items, scope) {
|
|
|
1078
1310
|
}) => item.set(value));
|
|
1079
1311
|
return;
|
|
1080
1312
|
}
|
|
1313
|
+
const changes = items.map(({
|
|
1314
|
+
item,
|
|
1315
|
+
value
|
|
1316
|
+
}) => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), typeof value === "string" ? value : undefined, "setBatch", "memory"));
|
|
1081
1317
|
|
|
1082
1318
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
1083
1319
|
items.forEach(({
|
|
@@ -1090,6 +1326,7 @@ export function setBatch(items, scope) {
|
|
|
1090
1326
|
items.forEach(({
|
|
1091
1327
|
item
|
|
1092
1328
|
}) => notifyKeyListeners(memoryListeners, item.key));
|
|
1329
|
+
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
1093
1330
|
return;
|
|
1094
1331
|
}
|
|
1095
1332
|
if (scope === StorageScope.Secure) {
|
|
@@ -1113,6 +1350,10 @@ export function setBatch(items, scope) {
|
|
|
1113
1350
|
}
|
|
1114
1351
|
flushSecureWrites();
|
|
1115
1352
|
const storageModule = getStorageModule();
|
|
1353
|
+
const keys = secureEntries.map(({
|
|
1354
|
+
item
|
|
1355
|
+
}) => item.key);
|
|
1356
|
+
const oldValues = hasStorageChangeObservers(scope) ? storageModule.getBatch(keys, scope) ?? [] : [];
|
|
1116
1357
|
const groupedByAccessControl = new Map();
|
|
1117
1358
|
secureEntries.forEach(({
|
|
1118
1359
|
item,
|
|
@@ -1136,6 +1377,10 @@ export function setBatch(items, scope) {
|
|
|
1136
1377
|
storageModule.setBatch(group.keys, group.values, scope);
|
|
1137
1378
|
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
1138
1379
|
});
|
|
1380
|
+
emitBatchChange(scope, "setBatch", "native", secureEntries.map(({
|
|
1381
|
+
item,
|
|
1382
|
+
value
|
|
1383
|
+
}, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", "native")));
|
|
1139
1384
|
return;
|
|
1140
1385
|
}
|
|
1141
1386
|
flushDiskWrites();
|
|
@@ -1151,15 +1396,19 @@ export function setBatch(items, scope) {
|
|
|
1151
1396
|
}
|
|
1152
1397
|
const keys = items.map(entry => entry.item.key);
|
|
1153
1398
|
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
1399
|
+
const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
|
|
1154
1400
|
getStorageModule().setBatch(keys, values, scope);
|
|
1155
1401
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1402
|
+
emitBatchChange(scope, "setBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "native")));
|
|
1156
1403
|
}, items.length);
|
|
1157
1404
|
}
|
|
1158
1405
|
export function removeBatch(items, scope) {
|
|
1159
1406
|
measureOperation("batch:remove", scope, () => {
|
|
1160
1407
|
assertBatchScope(items, scope);
|
|
1161
1408
|
if (scope === StorageScope.Memory) {
|
|
1409
|
+
const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
|
|
1162
1410
|
items.forEach(item => item.delete());
|
|
1411
|
+
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
1163
1412
|
return;
|
|
1164
1413
|
}
|
|
1165
1414
|
const keys = items.map(item => item.key);
|
|
@@ -1169,8 +1418,10 @@ export function removeBatch(items, scope) {
|
|
|
1169
1418
|
if (scope === StorageScope.Secure) {
|
|
1170
1419
|
flushSecureWrites();
|
|
1171
1420
|
}
|
|
1421
|
+
const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
|
|
1172
1422
|
getStorageModule().removeBatch(keys, scope);
|
|
1173
1423
|
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1424
|
+
emitBatchChange(scope, "removeBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "native")));
|
|
1174
1425
|
}, items.length);
|
|
1175
1426
|
}
|
|
1176
1427
|
export function registerMigration(version, migration) {
|