react-native-nitro-storage 0.5.0 → 0.5.2
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 +42 -8
- package/docs/api-reference.md +95 -31
- package/docs/batch-transactions-migrations.md +23 -9
- package/docs/benchmarks.md +1 -1
- package/docs/recipes.md +30 -9
- package/docs/secure-storage.md +20 -1
- package/lib/commonjs/index.js +214 -13
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +166 -11
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +7 -0
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-events.js +117 -0
- package/lib/commonjs/storage-events.js.map +1 -0
- package/lib/module/index.js +214 -13
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +166 -11
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +7 -0
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-events.js +112 -0
- package/lib/module/storage-events.js.map +1 -0
- package/lib/typescript/index.d.ts +14 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +14 -0
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.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/package.json +5 -4
- package/src/index.ts +534 -13
- package/src/index.web.ts +459 -22
- package/src/indexeddb-backend.ts +8 -0
- package/src/storage-events.ts +184 -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,11 @@ 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();
|
|
44
48
|
const nativeSecureBackend = "platform-secure-storage";
|
|
45
49
|
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
46
50
|
const existing = metricsCounters.get(operation);
|
|
@@ -91,6 +95,23 @@ function hasCachedRawValue(scope, key) {
|
|
|
91
95
|
function clearScopeRawCache(scope) {
|
|
92
96
|
getScopeRawCache(scope).clear();
|
|
93
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
|
+
}
|
|
94
115
|
function notifyKeyListeners(registry, key) {
|
|
95
116
|
const listeners = registry.get(key);
|
|
96
117
|
if (listeners) {
|
|
@@ -124,6 +145,52 @@ function addKeyListener(registry, key, listener) {
|
|
|
124
145
|
}
|
|
125
146
|
};
|
|
126
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
|
+
}
|
|
127
194
|
function readPendingSecureWrite(key) {
|
|
128
195
|
return pendingSecureWrites.get(key)?.value;
|
|
129
196
|
}
|
|
@@ -260,14 +327,19 @@ function ensureNativeScopeSubscription(scope) {
|
|
|
260
327
|
notifyAllListeners(getScopedListeners(scope));
|
|
261
328
|
return;
|
|
262
329
|
}
|
|
330
|
+
const oldValue = readCachedRawValue(scope, key);
|
|
263
331
|
cacheRawValue(scope, key, value);
|
|
264
332
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
333
|
+
if (consumeSuppressedNativeEvent(scope, key)) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
emitKeyChange(scope, key, oldValue, value, "external", "native");
|
|
265
337
|
});
|
|
266
|
-
scopedUnsubscribers.set(scope, unsubscribe);
|
|
338
|
+
scopedUnsubscribers.set(scope, typeof unsubscribe === "function" ? unsubscribe : () => {});
|
|
267
339
|
}
|
|
268
340
|
function maybeCleanupNativeScopeSubscription(scope) {
|
|
269
341
|
const listeners = getScopedListeners(scope);
|
|
270
|
-
if (listeners.size > 0) {
|
|
342
|
+
if (listeners.size > 0 || storageEvents.hasListeners(scope) || eventObserver !== undefined) {
|
|
271
343
|
return;
|
|
272
344
|
}
|
|
273
345
|
const unsubscribe = scopedUnsubscribers.get(scope);
|
|
@@ -293,15 +365,18 @@ function getRawValue(key, scope) {
|
|
|
293
365
|
}
|
|
294
366
|
function setRawValue(key, value, scope) {
|
|
295
367
|
assertValidScope(scope);
|
|
368
|
+
const oldValue = scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
296
369
|
if (scope === StorageScope.Memory) {
|
|
297
370
|
memoryStore.set(key, value);
|
|
298
371
|
notifyKeyListeners(memoryListeners, key);
|
|
372
|
+
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
299
373
|
return;
|
|
300
374
|
}
|
|
301
375
|
if (scope === StorageScope.Disk) {
|
|
302
376
|
cacheRawValue(scope, key, value);
|
|
303
377
|
if (diskWritesAsync) {
|
|
304
378
|
scheduleDiskWrite(key, value);
|
|
379
|
+
emitKeyChange(scope, key, oldValue, value, "set", "native");
|
|
305
380
|
return;
|
|
306
381
|
}
|
|
307
382
|
flushDiskWrites();
|
|
@@ -314,18 +389,22 @@ function setRawValue(key, value, scope) {
|
|
|
314
389
|
}
|
|
315
390
|
getStorageModule().set(key, value, scope);
|
|
316
391
|
cacheRawValue(scope, key, value);
|
|
392
|
+
emitKeyChange(scope, key, oldValue, value, "set", "native");
|
|
317
393
|
}
|
|
318
394
|
function removeRawValue(key, scope) {
|
|
319
395
|
assertValidScope(scope);
|
|
396
|
+
const oldValue = getEventRawValue(scope, key);
|
|
320
397
|
if (scope === StorageScope.Memory) {
|
|
321
398
|
memoryStore.delete(key);
|
|
322
399
|
notifyKeyListeners(memoryListeners, key);
|
|
400
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
323
401
|
return;
|
|
324
402
|
}
|
|
325
403
|
if (scope === StorageScope.Disk) {
|
|
326
404
|
cacheRawValue(scope, key, undefined);
|
|
327
405
|
if (diskWritesAsync) {
|
|
328
406
|
scheduleDiskWrite(key, undefined);
|
|
407
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
|
|
329
408
|
return;
|
|
330
409
|
}
|
|
331
410
|
flushDiskWrites();
|
|
@@ -337,6 +416,7 @@ function removeRawValue(key, scope) {
|
|
|
337
416
|
}
|
|
338
417
|
getStorageModule().remove(key, scope);
|
|
339
418
|
cacheRawValue(scope, key, undefined);
|
|
419
|
+
emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
|
|
340
420
|
}
|
|
341
421
|
function readMigrationVersion(scope) {
|
|
342
422
|
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
@@ -350,11 +430,62 @@ function writeMigrationVersion(scope, version) {
|
|
|
350
430
|
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
351
431
|
}
|
|
352
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
|
+
},
|
|
353
482
|
clear: scope => {
|
|
354
483
|
measureOperation("storage:clear", scope, () => {
|
|
484
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
|
|
355
485
|
if (scope === StorageScope.Memory) {
|
|
356
486
|
memoryStore.clear();
|
|
357
487
|
notifyAllListeners(memoryListeners);
|
|
488
|
+
emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
|
|
358
489
|
return;
|
|
359
490
|
}
|
|
360
491
|
if (scope === StorageScope.Disk) {
|
|
@@ -367,6 +498,7 @@ export const storage = {
|
|
|
367
498
|
}
|
|
368
499
|
clearScopeRawCache(scope);
|
|
369
500
|
getStorageModule().clear(scope);
|
|
501
|
+
emitBatchChange(scope, "clear", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "native")));
|
|
370
502
|
});
|
|
371
503
|
},
|
|
372
504
|
clearAll: () => {
|
|
@@ -380,15 +512,26 @@ export const storage = {
|
|
|
380
512
|
measureOperation("storage:clearNamespace", scope, () => {
|
|
381
513
|
assertValidScope(scope);
|
|
382
514
|
if (scope === StorageScope.Memory) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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;
|
|
387
522
|
}
|
|
388
|
-
|
|
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")));
|
|
389
531
|
return;
|
|
390
532
|
}
|
|
391
533
|
const keyPrefix = prefixKey(namespace, "");
|
|
534
|
+
const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
|
|
392
535
|
if (scope === StorageScope.Disk) {
|
|
393
536
|
flushDiskWrites();
|
|
394
537
|
}
|
|
@@ -402,6 +545,7 @@ export const storage = {
|
|
|
402
545
|
}
|
|
403
546
|
}
|
|
404
547
|
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
548
|
+
emitBatchChange(scope, "clearNamespace", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "native")));
|
|
405
549
|
});
|
|
406
550
|
},
|
|
407
551
|
clearBiometric: () => {
|
|
@@ -451,7 +595,7 @@ export const storage = {
|
|
|
451
595
|
if (scope === StorageScope.Secure) {
|
|
452
596
|
flushSecureWrites();
|
|
453
597
|
}
|
|
454
|
-
return getStorageModule().getKeysByPrefix(prefix, scope);
|
|
598
|
+
return getStorageModule().getKeysByPrefix(prefix, scope) ?? [];
|
|
455
599
|
});
|
|
456
600
|
},
|
|
457
601
|
getByPrefix: (prefix, scope) => {
|
|
@@ -476,7 +620,7 @@ export const storage = {
|
|
|
476
620
|
if (scope === StorageScope.Secure) {
|
|
477
621
|
flushSecureWrites();
|
|
478
622
|
}
|
|
479
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
623
|
+
const values = getStorageModule().getBatch(keys, scope) ?? [];
|
|
480
624
|
keys.forEach((key, idx) => {
|
|
481
625
|
const value = decodeNativeBatchValue(values[idx]);
|
|
482
626
|
if (value !== undefined) {
|
|
@@ -491,9 +635,10 @@ export const storage = {
|
|
|
491
635
|
assertValidScope(scope);
|
|
492
636
|
const result = {};
|
|
493
637
|
if (scope === StorageScope.Memory) {
|
|
494
|
-
memoryStore.
|
|
638
|
+
for (const key of memoryStore.keys()) {
|
|
639
|
+
const value = memoryStore.get(key);
|
|
495
640
|
if (typeof value === "string") result[key] = value;
|
|
496
|
-
}
|
|
641
|
+
}
|
|
497
642
|
return result;
|
|
498
643
|
}
|
|
499
644
|
if (scope === StorageScope.Disk) {
|
|
@@ -502,9 +647,9 @@ export const storage = {
|
|
|
502
647
|
if (scope === StorageScope.Secure) {
|
|
503
648
|
flushSecureWrites();
|
|
504
649
|
}
|
|
505
|
-
const keys = getStorageModule().getAllKeys(scope);
|
|
650
|
+
const keys = getStorageModule().getAllKeys(scope) ?? [];
|
|
506
651
|
if (keys.length === 0) return result;
|
|
507
|
-
const values = getStorageModule().getBatch(keys, scope);
|
|
652
|
+
const values = getStorageModule().getBatch(keys, scope) ?? [];
|
|
508
653
|
keys.forEach((key, idx) => {
|
|
509
654
|
const val = decodeNativeBatchValue(values[idx]);
|
|
510
655
|
if (val !== undefined) result[key] = val;
|
|
@@ -512,6 +657,9 @@ export const storage = {
|
|
|
512
657
|
return result;
|
|
513
658
|
});
|
|
514
659
|
},
|
|
660
|
+
export: scope => {
|
|
661
|
+
return measureOperation("storage:export", scope, () => storage.getAll(scope));
|
|
662
|
+
},
|
|
515
663
|
size: scope => {
|
|
516
664
|
return measureOperation("storage:size", scope, () => {
|
|
517
665
|
assertValidScope(scope);
|
|
@@ -661,11 +809,13 @@ export const storage = {
|
|
|
661
809
|
assertValidScope(scope);
|
|
662
810
|
if (keys.length === 0) return;
|
|
663
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"));
|
|
664
813
|
if (scope === StorageScope.Memory) {
|
|
665
814
|
keys.forEach((key, index) => {
|
|
666
815
|
memoryStore.set(key, values[index]);
|
|
667
816
|
});
|
|
668
817
|
keys.forEach(key => notifyKeyListeners(memoryListeners, key));
|
|
818
|
+
emitBatchChange(scope, "import", "memory", changes);
|
|
669
819
|
return;
|
|
670
820
|
}
|
|
671
821
|
if (scope === StorageScope.Secure) {
|
|
@@ -674,6 +824,7 @@ export const storage = {
|
|
|
674
824
|
}
|
|
675
825
|
getStorageModule().setBatch(keys, values, scope);
|
|
676
826
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
827
|
+
emitBatchChange(scope, "import", "native", changes);
|
|
677
828
|
}, keys.length);
|
|
678
829
|
}
|
|
679
830
|
};
|
|
@@ -794,20 +945,24 @@ export function createStorageItem(config) {
|
|
|
794
945
|
return raw;
|
|
795
946
|
};
|
|
796
947
|
const writeStoredRaw = rawValue => {
|
|
948
|
+
const oldValue = undefined;
|
|
797
949
|
if (isBiometric) {
|
|
798
950
|
getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
951
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
799
952
|
return;
|
|
800
953
|
}
|
|
801
954
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
802
955
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
803
956
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
804
957
|
scheduleDiskWrite(storageKey, rawValue);
|
|
958
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
805
959
|
return;
|
|
806
960
|
}
|
|
807
961
|
clearPendingDiskWrite(storageKey);
|
|
808
962
|
}
|
|
809
963
|
if (coalesceSecureWrites) {
|
|
810
964
|
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
965
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
811
966
|
return;
|
|
812
967
|
}
|
|
813
968
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -815,36 +970,44 @@ export function createStorageItem(config) {
|
|
|
815
970
|
getStorageModule().setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
|
|
816
971
|
}
|
|
817
972
|
getStorageModule().set(storageKey, rawValue, config.scope);
|
|
973
|
+
emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
|
|
818
974
|
};
|
|
819
975
|
const removeStoredRaw = () => {
|
|
976
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
820
977
|
if (isBiometric) {
|
|
821
978
|
getStorageModule().deleteSecureBiometric(storageKey);
|
|
979
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
822
980
|
return;
|
|
823
981
|
}
|
|
824
982
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
825
983
|
if (nonMemoryScope === StorageScope.Disk) {
|
|
826
984
|
if (coalesceDiskWrites || diskWritesAsync) {
|
|
827
985
|
scheduleDiskWrite(storageKey, undefined);
|
|
986
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
828
987
|
return;
|
|
829
988
|
}
|
|
830
989
|
clearPendingDiskWrite(storageKey);
|
|
831
990
|
}
|
|
832
991
|
if (coalesceSecureWrites) {
|
|
833
992
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
993
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
834
994
|
return;
|
|
835
995
|
}
|
|
836
996
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
837
997
|
clearPendingSecureWrite(storageKey);
|
|
838
998
|
}
|
|
839
999
|
getStorageModule().remove(storageKey, config.scope);
|
|
1000
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
|
|
840
1001
|
};
|
|
841
1002
|
const writeValueWithoutValidation = value => {
|
|
842
1003
|
if (isMemory) {
|
|
1004
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
843
1005
|
if (memoryExpiration) {
|
|
844
1006
|
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
845
1007
|
}
|
|
846
1008
|
memoryStore.set(storageKey, value);
|
|
847
1009
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1010
|
+
emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
|
|
848
1011
|
return;
|
|
849
1012
|
}
|
|
850
1013
|
const serialized = serialize(value);
|
|
@@ -976,11 +1139,13 @@ export function createStorageItem(config) {
|
|
|
976
1139
|
measureOperation("item:delete", config.scope, () => {
|
|
977
1140
|
invalidateParsedCache();
|
|
978
1141
|
if (isMemory) {
|
|
1142
|
+
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
979
1143
|
if (memoryExpiration) {
|
|
980
1144
|
memoryExpiration.delete(storageKey);
|
|
981
1145
|
}
|
|
982
1146
|
memoryStore.delete(storageKey);
|
|
983
1147
|
notifyKeyListeners(memoryListeners, storageKey);
|
|
1148
|
+
emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
|
|
984
1149
|
return;
|
|
985
1150
|
}
|
|
986
1151
|
removeStoredRaw();
|
|
@@ -1017,6 +1182,22 @@ export function createStorageItem(config) {
|
|
|
1017
1182
|
}
|
|
1018
1183
|
};
|
|
1019
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
|
+
};
|
|
1020
1201
|
const storageItem = {
|
|
1021
1202
|
get,
|
|
1022
1203
|
getWithVersion,
|
|
@@ -1025,6 +1206,7 @@ export function createStorageItem(config) {
|
|
|
1025
1206
|
delete: deleteItem,
|
|
1026
1207
|
has: hasItem,
|
|
1027
1208
|
subscribe,
|
|
1209
|
+
subscribeSelector,
|
|
1028
1210
|
serialize,
|
|
1029
1211
|
deserialize,
|
|
1030
1212
|
_triggerListeners: () => {
|
|
@@ -1128,6 +1310,10 @@ export function setBatch(items, scope) {
|
|
|
1128
1310
|
}) => item.set(value));
|
|
1129
1311
|
return;
|
|
1130
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"));
|
|
1131
1317
|
|
|
1132
1318
|
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
1133
1319
|
items.forEach(({
|
|
@@ -1140,6 +1326,7 @@ export function setBatch(items, scope) {
|
|
|
1140
1326
|
items.forEach(({
|
|
1141
1327
|
item
|
|
1142
1328
|
}) => notifyKeyListeners(memoryListeners, item.key));
|
|
1329
|
+
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
1143
1330
|
return;
|
|
1144
1331
|
}
|
|
1145
1332
|
if (scope === StorageScope.Secure) {
|
|
@@ -1163,6 +1350,10 @@ export function setBatch(items, scope) {
|
|
|
1163
1350
|
}
|
|
1164
1351
|
flushSecureWrites();
|
|
1165
1352
|
const storageModule = getStorageModule();
|
|
1353
|
+
const keys = secureEntries.map(({
|
|
1354
|
+
item
|
|
1355
|
+
}) => item.key);
|
|
1356
|
+
const oldValues = hasStorageChangeObservers(scope) ? storageModule.getBatch(keys, scope) ?? [] : [];
|
|
1166
1357
|
const groupedByAccessControl = new Map();
|
|
1167
1358
|
secureEntries.forEach(({
|
|
1168
1359
|
item,
|
|
@@ -1186,6 +1377,10 @@ export function setBatch(items, scope) {
|
|
|
1186
1377
|
storageModule.setBatch(group.keys, group.values, scope);
|
|
1187
1378
|
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
1188
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")));
|
|
1189
1384
|
return;
|
|
1190
1385
|
}
|
|
1191
1386
|
flushDiskWrites();
|
|
@@ -1201,15 +1396,19 @@ export function setBatch(items, scope) {
|
|
|
1201
1396
|
}
|
|
1202
1397
|
const keys = items.map(entry => entry.item.key);
|
|
1203
1398
|
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
1399
|
+
const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
|
|
1204
1400
|
getStorageModule().setBatch(keys, values, scope);
|
|
1205
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")));
|
|
1206
1403
|
}, items.length);
|
|
1207
1404
|
}
|
|
1208
1405
|
export function removeBatch(items, scope) {
|
|
1209
1406
|
measureOperation("batch:remove", scope, () => {
|
|
1210
1407
|
assertBatchScope(items, scope);
|
|
1211
1408
|
if (scope === StorageScope.Memory) {
|
|
1409
|
+
const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
|
|
1212
1410
|
items.forEach(item => item.delete());
|
|
1411
|
+
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
1213
1412
|
return;
|
|
1214
1413
|
}
|
|
1215
1414
|
const keys = items.map(item => item.key);
|
|
@@ -1219,8 +1418,10 @@ export function removeBatch(items, scope) {
|
|
|
1219
1418
|
if (scope === StorageScope.Secure) {
|
|
1220
1419
|
flushSecureWrites();
|
|
1221
1420
|
}
|
|
1421
|
+
const oldValues = hasStorageChangeObservers(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
|
|
1222
1422
|
getStorageModule().removeBatch(keys, scope);
|
|
1223
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")));
|
|
1224
1425
|
}, items.length);
|
|
1225
1426
|
}
|
|
1226
1427
|
export function registerMigration(version, migration) {
|