react-native-nitro-storage 0.4.4 → 0.4.5
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 +107 -7
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
- package/ios/IOSStorageAdapterCpp.mm +44 -14
- package/lib/commonjs/index.js +221 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +444 -202
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +129 -7
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +41 -0
- package/lib/commonjs/storage-runtime.js.map +1 -0
- package/lib/commonjs/web-storage-backend.js +90 -0
- package/lib/commonjs/web-storage-backend.js.map +1 -0
- package/lib/module/index.js +213 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +436 -202
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +129 -7
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-runtime.js +36 -0
- package/lib/module/storage-runtime.js.map +1 -0
- package/lib/module/web-storage-backend.js +86 -0
- package/lib/module/web-storage-backend.js.map +1 -0
- package/lib/typescript/index.d.ts +11 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +12 -8
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +6 -2
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +16 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +30 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +264 -20
- package/src/index.web.ts +597 -245
- package/src/indexeddb-backend.ts +147 -6
- package/src/storage-runtime.ts +94 -0
- package/src/web-storage-backend.ts +129 -0
package/src/index.web.ts
CHANGED
|
@@ -11,9 +11,32 @@ import {
|
|
|
11
11
|
prefixKey,
|
|
12
12
|
isNamespaced,
|
|
13
13
|
} from "./internal";
|
|
14
|
+
import {
|
|
15
|
+
createLocalStorageWebBackend,
|
|
16
|
+
type WebDiskStorageBackend,
|
|
17
|
+
type WebSecureStorageBackend,
|
|
18
|
+
type WebStorageBackend,
|
|
19
|
+
type WebStorageChangeEvent,
|
|
20
|
+
} from "./web-storage-backend";
|
|
21
|
+
import {
|
|
22
|
+
getStorageErrorCode,
|
|
23
|
+
isLockedStorageErrorCode,
|
|
24
|
+
type StorageCapabilities,
|
|
25
|
+
type StorageErrorCode,
|
|
26
|
+
} from "./storage-runtime";
|
|
14
27
|
|
|
15
28
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
16
29
|
export { migrateFromMMKV } from "./migration";
|
|
30
|
+
export {
|
|
31
|
+
getStorageErrorCode,
|
|
32
|
+
type StorageCapabilities,
|
|
33
|
+
type StorageErrorCode,
|
|
34
|
+
} from "./storage-runtime";
|
|
35
|
+
export type {
|
|
36
|
+
WebStorageBackend,
|
|
37
|
+
WebStorageChangeEvent,
|
|
38
|
+
WebStorageScope,
|
|
39
|
+
} from "./web-storage-backend";
|
|
17
40
|
|
|
18
41
|
export type Validator<T> = (value: unknown) => value is T;
|
|
19
42
|
export type ExpirationConfig = {
|
|
@@ -37,14 +60,6 @@ export type StorageMetricSummary = {
|
|
|
37
60
|
avgDurationMs: number;
|
|
38
61
|
maxDurationMs: number;
|
|
39
62
|
};
|
|
40
|
-
export type WebSecureStorageBackend = {
|
|
41
|
-
getItem: (key: string) => string | null;
|
|
42
|
-
setItem: (key: string, value: string) => void;
|
|
43
|
-
removeItem: (key: string) => void;
|
|
44
|
-
clear: () => void;
|
|
45
|
-
getAllKeys: () => string[];
|
|
46
|
-
};
|
|
47
|
-
|
|
48
63
|
export type MigrationContext = {
|
|
49
64
|
scope: StorageScope;
|
|
50
65
|
getRaw: (key: string) => string | undefined;
|
|
@@ -91,19 +106,15 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
|
91
106
|
return Object.keys(record) as K[];
|
|
92
107
|
}
|
|
93
108
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
109
|
+
type PendingDiskWrite = {
|
|
110
|
+
key: string;
|
|
111
|
+
value: string | undefined;
|
|
112
|
+
};
|
|
94
113
|
type PendingSecureWrite = {
|
|
95
114
|
key: string;
|
|
96
115
|
value: string | undefined;
|
|
97
116
|
accessControl?: AccessControl;
|
|
98
117
|
};
|
|
99
|
-
type BrowserStorageLike = {
|
|
100
|
-
setItem: (key: string, value: string) => void;
|
|
101
|
-
getItem: (key: string) => string | null;
|
|
102
|
-
removeItem: (key: string) => void;
|
|
103
|
-
clear: () => void;
|
|
104
|
-
key: (index: number) => string | null;
|
|
105
|
-
readonly length: number;
|
|
106
|
-
};
|
|
107
118
|
|
|
108
119
|
const registeredMigrations = new Map<number, Migration>();
|
|
109
120
|
const runMicrotask =
|
|
@@ -112,6 +123,10 @@ const runMicrotask =
|
|
|
112
123
|
: (task: () => void) => {
|
|
113
124
|
Promise.resolve().then(task);
|
|
114
125
|
};
|
|
126
|
+
const now =
|
|
127
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
128
|
+
? () => performance.now()
|
|
129
|
+
: () => Date.now();
|
|
115
130
|
|
|
116
131
|
export interface Storage {
|
|
117
132
|
name: string;
|
|
@@ -161,14 +176,16 @@ const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
|
|
|
161
176
|
[StorageScope.Secure, new Set()],
|
|
162
177
|
]);
|
|
163
178
|
const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
|
|
179
|
+
const pendingDiskWrites = new Map<string, PendingDiskWrite>();
|
|
180
|
+
let diskFlushScheduled = false;
|
|
181
|
+
let diskWritesAsync = false;
|
|
164
182
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
165
183
|
let secureFlushScheduled = false;
|
|
166
184
|
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
167
185
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
168
186
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
169
187
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
170
|
-
let
|
|
171
|
-
let webStorageSubscriberCount = 0;
|
|
188
|
+
let hasWindowStorageEventSubscription = false;
|
|
172
189
|
let metricsObserver: StorageMetricsObserver | undefined;
|
|
173
190
|
const metricsCounters = new Map<
|
|
174
191
|
string,
|
|
@@ -205,69 +222,86 @@ function measureOperation<T>(
|
|
|
205
222
|
if (!metricsObserver) {
|
|
206
223
|
return fn();
|
|
207
224
|
}
|
|
208
|
-
const start =
|
|
225
|
+
const start = now();
|
|
209
226
|
try {
|
|
210
227
|
return fn();
|
|
211
228
|
} finally {
|
|
212
|
-
recordMetric(operation, scope,
|
|
229
|
+
recordMetric(operation, scope, now() - start, keysCount);
|
|
213
230
|
}
|
|
214
231
|
}
|
|
215
232
|
|
|
216
|
-
function
|
|
217
|
-
return {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
const storage = globalThis.localStorage;
|
|
224
|
-
if (!storage) return [];
|
|
225
|
-
const keys: string[] = [];
|
|
226
|
-
for (let index = 0; index < storage.length; index += 1) {
|
|
227
|
-
const key = storage.key(index);
|
|
228
|
-
if (key) {
|
|
229
|
-
keys.push(key);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
return keys;
|
|
233
|
-
},
|
|
234
|
-
};
|
|
233
|
+
function createDefaultDiskBackend(): WebDiskStorageBackend {
|
|
234
|
+
return createLocalStorageWebBackend({
|
|
235
|
+
name: "localStorage:disk",
|
|
236
|
+
includeKey: (key) =>
|
|
237
|
+
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
238
|
+
!key.startsWith(BIOMETRIC_WEB_PREFIX),
|
|
239
|
+
});
|
|
235
240
|
}
|
|
236
241
|
|
|
242
|
+
function createDefaultSecureBackend(): WebSecureStorageBackend {
|
|
243
|
+
return createLocalStorageWebBackend({
|
|
244
|
+
name: "localStorage:secure",
|
|
245
|
+
includeKey: (key) =>
|
|
246
|
+
key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX),
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
let webDiskStorageBackend: WebDiskStorageBackend | undefined =
|
|
251
|
+
createDefaultDiskBackend();
|
|
237
252
|
let webSecureStorageBackend: WebSecureStorageBackend | undefined =
|
|
238
|
-
|
|
253
|
+
createDefaultSecureBackend();
|
|
254
|
+
const externalSyncUnsubscribers = new Map<NonMemoryScope, () => void>();
|
|
255
|
+
|
|
256
|
+
function getBackendName(
|
|
257
|
+
scope: NonMemoryScope,
|
|
258
|
+
backend: WebStorageBackend | undefined,
|
|
259
|
+
): string {
|
|
260
|
+
const scopeName = scope === StorageScope.Disk ? "disk" : "secure";
|
|
261
|
+
return backend?.name ?? `web:${scopeName}`;
|
|
262
|
+
}
|
|
239
263
|
|
|
240
|
-
|
|
241
|
-
|
|
264
|
+
function createWebStorageError(
|
|
265
|
+
scope: NonMemoryScope,
|
|
266
|
+
operation: string,
|
|
267
|
+
error: unknown,
|
|
268
|
+
backend: WebStorageBackend | undefined,
|
|
269
|
+
): Error {
|
|
270
|
+
const backendName = getBackendName(scope, backend);
|
|
271
|
+
const message =
|
|
272
|
+
error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
273
|
+
return new Error(
|
|
274
|
+
`NitroStorage(web): ${operation} failed for ${backendName}: ${message}`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
242
277
|
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
278
|
+
function withWebBackendOperation<T>(
|
|
279
|
+
scope: NonMemoryScope,
|
|
280
|
+
operation: string,
|
|
281
|
+
fn: (backend: WebStorageBackend) => T,
|
|
282
|
+
): T {
|
|
283
|
+
const backend =
|
|
284
|
+
scope === StorageScope.Disk
|
|
285
|
+
? webDiskStorageBackend
|
|
286
|
+
: webSecureStorageBackend;
|
|
287
|
+
if (!backend) {
|
|
288
|
+
throw new Error(
|
|
289
|
+
`NitroStorage(web): ${operation} failed because no ${scope === StorageScope.Disk ? "disk" : "secure"} backend is configured.`,
|
|
290
|
+
);
|
|
246
291
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
cachedSecureBrowserStorage
|
|
254
|
-
) {
|
|
255
|
-
return cachedSecureBrowserStorage;
|
|
256
|
-
}
|
|
257
|
-
cachedSecureBackendRef = webSecureStorageBackend;
|
|
258
|
-
cachedSecureBrowserStorage = {
|
|
259
|
-
setItem: (key, value) => webSecureStorageBackend!.setItem(key, value),
|
|
260
|
-
getItem: (key) => webSecureStorageBackend!.getItem(key) ?? null,
|
|
261
|
-
removeItem: (key) => webSecureStorageBackend!.removeItem(key),
|
|
262
|
-
clear: () => webSecureStorageBackend!.clear(),
|
|
263
|
-
key: (index) => webSecureStorageBackend!.getAllKeys()[index] ?? null,
|
|
264
|
-
get length() {
|
|
265
|
-
return webSecureStorageBackend!.getAllKeys().length;
|
|
266
|
-
},
|
|
267
|
-
};
|
|
268
|
-
return cachedSecureBrowserStorage;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
ensureExternalSyncSubscriptions();
|
|
295
|
+
return fn(backend);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
throw createWebStorageError(scope, operation, error, backend);
|
|
269
298
|
}
|
|
270
|
-
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getWebBackend(scope: NonMemoryScope): WebStorageBackend | undefined {
|
|
302
|
+
return scope === StorageScope.Disk
|
|
303
|
+
? webDiskStorageBackend
|
|
304
|
+
: webSecureStorageBackend;
|
|
271
305
|
}
|
|
272
306
|
|
|
273
307
|
function toSecureStorageKey(key: string): string {
|
|
@@ -295,32 +329,22 @@ function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
|
|
|
295
329
|
return;
|
|
296
330
|
}
|
|
297
331
|
|
|
298
|
-
const
|
|
332
|
+
const backend = getWebBackend(scope);
|
|
299
333
|
const keyIndex = getWebScopeKeyIndex(scope);
|
|
300
334
|
keyIndex.clear();
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (scope === StorageScope.Disk) {
|
|
308
|
-
if (
|
|
309
|
-
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
310
|
-
!key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
311
|
-
) {
|
|
312
|
-
keyIndex.add(key);
|
|
313
|
-
}
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
335
|
+
const keys = backend?.getAllKeys() ?? [];
|
|
336
|
+
for (const key of keys) {
|
|
337
|
+
if (scope === StorageScope.Disk) {
|
|
338
|
+
keyIndex.add(key);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
316
341
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
342
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
343
|
+
keyIndex.add(fromSecureStorageKey(key));
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
347
|
+
keyIndex.add(fromBiometricStorageKey(key));
|
|
324
348
|
}
|
|
325
349
|
}
|
|
326
350
|
hydratedWebScopeKeyIndex.add(scope);
|
|
@@ -331,37 +355,39 @@ function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
|
331
355
|
return getWebScopeKeyIndex(scope);
|
|
332
356
|
}
|
|
333
357
|
|
|
334
|
-
function
|
|
335
|
-
|
|
358
|
+
function applyExternalChangeEvent(
|
|
359
|
+
scope: NonMemoryScope,
|
|
360
|
+
key: string | null,
|
|
361
|
+
newValue: string | null,
|
|
362
|
+
): void {
|
|
336
363
|
if (key === null) {
|
|
337
|
-
clearScopeRawCache(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
ensureWebScopeKeyIndex(StorageScope.Secure).clear();
|
|
341
|
-
notifyAllListeners(getScopedListeners(StorageScope.Disk));
|
|
342
|
-
notifyAllListeners(getScopedListeners(StorageScope.Secure));
|
|
364
|
+
clearScopeRawCache(scope);
|
|
365
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
366
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
343
367
|
return;
|
|
344
368
|
}
|
|
345
369
|
|
|
346
|
-
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
370
|
+
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
347
371
|
const plainKey = fromSecureStorageKey(key);
|
|
348
|
-
if (
|
|
372
|
+
if (newValue === null) {
|
|
349
373
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
350
374
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
351
375
|
} else {
|
|
352
376
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
353
|
-
cacheRawValue(StorageScope.Secure, plainKey,
|
|
377
|
+
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
354
378
|
}
|
|
355
379
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
356
380
|
return;
|
|
357
381
|
}
|
|
358
382
|
|
|
359
|
-
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
383
|
+
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
360
384
|
const plainKey = fromBiometricStorageKey(key);
|
|
361
|
-
if (
|
|
385
|
+
if (newValue === null) {
|
|
362
386
|
if (
|
|
363
|
-
|
|
364
|
-
|
|
387
|
+
withWebBackendOperation(
|
|
388
|
+
StorageScope.Secure,
|
|
389
|
+
"external-sync:getItem",
|
|
390
|
+
(backend) => backend.getItem(toSecureStorageKey(plainKey)),
|
|
365
391
|
) === null
|
|
366
392
|
) {
|
|
367
393
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
@@ -369,44 +395,74 @@ function handleWebStorageEvent(event: StorageEvent): void {
|
|
|
369
395
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
370
396
|
} else {
|
|
371
397
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
372
|
-
cacheRawValue(StorageScope.Secure, plainKey,
|
|
398
|
+
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
373
399
|
}
|
|
374
400
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
375
401
|
return;
|
|
376
402
|
}
|
|
377
403
|
|
|
378
|
-
if (
|
|
379
|
-
ensureWebScopeKeyIndex(
|
|
380
|
-
cacheRawValue(
|
|
404
|
+
if (newValue === null) {
|
|
405
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
406
|
+
cacheRawValue(scope, key, undefined);
|
|
381
407
|
} else {
|
|
382
|
-
ensureWebScopeKeyIndex(
|
|
383
|
-
cacheRawValue(
|
|
408
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
409
|
+
cacheRawValue(scope, key, newValue);
|
|
384
410
|
}
|
|
385
|
-
notifyKeyListeners(getScopedListeners(
|
|
411
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
386
412
|
}
|
|
387
413
|
|
|
388
|
-
function
|
|
389
|
-
|
|
414
|
+
function handleWebStorageEvent(event: StorageEvent): void {
|
|
415
|
+
const key = event.key;
|
|
416
|
+
if (key === null) {
|
|
417
|
+
applyExternalChangeEvent(StorageScope.Disk, null, null);
|
|
418
|
+
applyExternalChangeEvent(StorageScope.Secure, null, null);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
390
422
|
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
typeof window.addEventListener === "function"
|
|
423
|
+
key.startsWith(SECURE_WEB_PREFIX) ||
|
|
424
|
+
key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
394
425
|
) {
|
|
395
|
-
|
|
396
|
-
|
|
426
|
+
applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
|
|
427
|
+
return;
|
|
397
428
|
}
|
|
429
|
+
|
|
430
|
+
applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
|
|
398
431
|
}
|
|
399
432
|
|
|
400
|
-
function
|
|
401
|
-
|
|
433
|
+
function subscribeToBackendChanges(scope: NonMemoryScope): void {
|
|
434
|
+
if (externalSyncUnsubscribers.has(scope)) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
const backend = getWebBackend(scope);
|
|
439
|
+
if (!backend?.subscribe) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const unsubscribe = backend.subscribe((event: WebStorageChangeEvent) => {
|
|
444
|
+
applyExternalChangeEvent(scope, event.key, event.newValue);
|
|
445
|
+
});
|
|
446
|
+
externalSyncUnsubscribers.set(scope, unsubscribe);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function resetBackendChangeSubscription(scope: NonMemoryScope): void {
|
|
450
|
+
externalSyncUnsubscribers.get(scope)?.();
|
|
451
|
+
externalSyncUnsubscribers.delete(scope);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function ensureExternalSyncSubscriptions(): void {
|
|
402
455
|
if (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
typeof window
|
|
456
|
+
!hasWindowStorageEventSubscription &&
|
|
457
|
+
typeof window !== "undefined" &&
|
|
458
|
+
typeof window.addEventListener === "function"
|
|
406
459
|
) {
|
|
407
|
-
window.
|
|
408
|
-
|
|
460
|
+
window.addEventListener("storage", handleWebStorageEvent);
|
|
461
|
+
hasWindowStorageEventSubscription = true;
|
|
409
462
|
}
|
|
463
|
+
|
|
464
|
+
subscribeToBackendChanges(StorageScope.Disk);
|
|
465
|
+
subscribeToBackendChanges(StorageScope.Secure);
|
|
410
466
|
}
|
|
411
467
|
|
|
412
468
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
@@ -487,14 +543,58 @@ function readPendingSecureWrite(key: string): string | undefined {
|
|
|
487
543
|
return pendingSecureWrites.get(key)?.value;
|
|
488
544
|
}
|
|
489
545
|
|
|
546
|
+
function readPendingDiskWrite(key: string): string | undefined {
|
|
547
|
+
return pendingDiskWrites.get(key)?.value;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function hasPendingDiskWrite(key: string): boolean {
|
|
551
|
+
return pendingDiskWrites.has(key);
|
|
552
|
+
}
|
|
553
|
+
|
|
490
554
|
function hasPendingSecureWrite(key: string): boolean {
|
|
491
555
|
return pendingSecureWrites.has(key);
|
|
492
556
|
}
|
|
493
557
|
|
|
558
|
+
function clearPendingDiskWrite(key: string): void {
|
|
559
|
+
pendingDiskWrites.delete(key);
|
|
560
|
+
}
|
|
561
|
+
|
|
494
562
|
function clearPendingSecureWrite(key: string): void {
|
|
495
563
|
pendingSecureWrites.delete(key);
|
|
496
564
|
}
|
|
497
565
|
|
|
566
|
+
function flushDiskWrites(): void {
|
|
567
|
+
diskFlushScheduled = false;
|
|
568
|
+
|
|
569
|
+
if (pendingDiskWrites.size === 0) {
|
|
570
|
+
return;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const writes = Array.from(pendingDiskWrites.values());
|
|
574
|
+
pendingDiskWrites.clear();
|
|
575
|
+
|
|
576
|
+
const keysToSet: string[] = [];
|
|
577
|
+
const valuesToSet: string[] = [];
|
|
578
|
+
const keysToRemove: string[] = [];
|
|
579
|
+
|
|
580
|
+
writes.forEach(({ key, value }) => {
|
|
581
|
+
if (value === undefined) {
|
|
582
|
+
keysToRemove.push(key);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
keysToSet.push(key);
|
|
587
|
+
valuesToSet.push(value);
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
if (keysToSet.length > 0) {
|
|
591
|
+
WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
592
|
+
}
|
|
593
|
+
if (keysToRemove.length > 0) {
|
|
594
|
+
WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
498
598
|
function flushSecureWrites(): void {
|
|
499
599
|
secureFlushScheduled = false;
|
|
500
600
|
|
|
@@ -535,6 +635,15 @@ function flushSecureWrites(): void {
|
|
|
535
635
|
}
|
|
536
636
|
}
|
|
537
637
|
|
|
638
|
+
function scheduleDiskWrite(key: string, value: string | undefined): void {
|
|
639
|
+
pendingDiskWrites.set(key, { key, value });
|
|
640
|
+
if (diskFlushScheduled) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
diskFlushScheduled = true;
|
|
644
|
+
runMicrotask(flushDiskWrites);
|
|
645
|
+
}
|
|
646
|
+
|
|
538
647
|
function scheduleSecureWrite(
|
|
539
648
|
key: string,
|
|
540
649
|
value: string | undefined,
|
|
@@ -557,131 +666,142 @@ const WebStorage: Storage = {
|
|
|
557
666
|
equals: (other) => other === WebStorage,
|
|
558
667
|
dispose: () => {},
|
|
559
668
|
set: (key: string, value: string, scope: number) => {
|
|
560
|
-
|
|
561
|
-
if (!storage) {
|
|
669
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
562
670
|
return;
|
|
563
671
|
}
|
|
564
672
|
const storageKey =
|
|
565
673
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
674
|
+
withWebBackendOperation(scope, "set", (backend) => {
|
|
675
|
+
backend.setItem(storageKey, value);
|
|
676
|
+
});
|
|
677
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
678
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
571
679
|
},
|
|
572
680
|
get: (key: string, scope: number) => {
|
|
573
|
-
|
|
681
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
682
|
+
return undefined;
|
|
683
|
+
}
|
|
574
684
|
const storageKey =
|
|
575
685
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
576
|
-
|
|
686
|
+
const value = withWebBackendOperation(scope, "get", (backend) =>
|
|
687
|
+
backend.getItem(storageKey),
|
|
688
|
+
);
|
|
689
|
+
return value ?? undefined;
|
|
577
690
|
},
|
|
578
691
|
remove: (key: string, scope: number) => {
|
|
579
|
-
|
|
580
|
-
if (!storage) {
|
|
692
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
581
693
|
return;
|
|
582
694
|
}
|
|
583
695
|
if (scope === StorageScope.Secure) {
|
|
584
|
-
|
|
585
|
-
|
|
696
|
+
withWebBackendOperation(scope, "remove", (backend) => {
|
|
697
|
+
if (backend.removeMany) {
|
|
698
|
+
backend.removeMany([
|
|
699
|
+
toSecureStorageKey(key),
|
|
700
|
+
toBiometricStorageKey(key),
|
|
701
|
+
]);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
backend.removeItem(toSecureStorageKey(key));
|
|
705
|
+
backend.removeItem(toBiometricStorageKey(key));
|
|
706
|
+
});
|
|
586
707
|
} else {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
ensureWebScopeKeyIndex(scope).delete(key);
|
|
591
|
-
notifyKeyListeners(getScopedListeners(scope), key);
|
|
708
|
+
withWebBackendOperation(scope, "remove", (backend) => {
|
|
709
|
+
backend.removeItem(key);
|
|
710
|
+
});
|
|
592
711
|
}
|
|
712
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
713
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
593
714
|
},
|
|
594
715
|
clear: (scope: number) => {
|
|
595
|
-
|
|
596
|
-
if (!storage) {
|
|
716
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
597
717
|
return;
|
|
598
718
|
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
key?.startsWith(SECURE_WEB_PREFIX) ||
|
|
605
|
-
key?.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
606
|
-
) {
|
|
607
|
-
keysToRemove.push(key);
|
|
608
|
-
}
|
|
609
|
-
}
|
|
610
|
-
keysToRemove.forEach((key) => storage.removeItem(key));
|
|
611
|
-
} else if (scope === StorageScope.Disk) {
|
|
612
|
-
const keysToRemove: string[] = [];
|
|
613
|
-
for (let i = 0; i < storage.length; i++) {
|
|
614
|
-
const key = storage.key(i);
|
|
615
|
-
if (
|
|
616
|
-
key &&
|
|
617
|
-
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
618
|
-
!key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
619
|
-
) {
|
|
620
|
-
keysToRemove.push(key);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
keysToRemove.forEach((key) => storage.removeItem(key));
|
|
624
|
-
} else {
|
|
625
|
-
storage.clear();
|
|
626
|
-
}
|
|
627
|
-
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
628
|
-
ensureWebScopeKeyIndex(scope).clear();
|
|
629
|
-
notifyAllListeners(getScopedListeners(scope));
|
|
630
|
-
}
|
|
719
|
+
withWebBackendOperation(scope, "clear", (backend) => {
|
|
720
|
+
backend.clear();
|
|
721
|
+
});
|
|
722
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
723
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
631
724
|
},
|
|
632
725
|
setBatch: (keys: string[], values: string[], scope: number) => {
|
|
633
|
-
|
|
634
|
-
if (!storage) {
|
|
726
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
635
727
|
return;
|
|
636
728
|
}
|
|
637
729
|
|
|
730
|
+
const entries: (readonly [string, string])[] = [];
|
|
638
731
|
keys.forEach((key, index) => {
|
|
639
732
|
const value = values[index];
|
|
640
733
|
if (value === undefined) {
|
|
641
734
|
return;
|
|
642
735
|
}
|
|
643
|
-
|
|
644
|
-
scope === StorageScope.Secure ? toSecureStorageKey(key) : key
|
|
645
|
-
|
|
736
|
+
entries.push([
|
|
737
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key,
|
|
738
|
+
value,
|
|
739
|
+
]);
|
|
646
740
|
});
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
741
|
+
withWebBackendOperation(scope, "setBatch", (backend) => {
|
|
742
|
+
if (backend.setMany) {
|
|
743
|
+
backend.setMany(entries);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
entries.forEach(([storageKey, value]) => {
|
|
747
|
+
backend.setItem(storageKey, value);
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
751
|
+
keys.forEach((key) => keyIndex.add(key));
|
|
752
|
+
const listeners = getScopedListeners(scope);
|
|
753
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
653
754
|
},
|
|
654
755
|
getBatch: (keys: string[], scope: number) => {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
756
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
757
|
+
return keys.map(() => undefined);
|
|
758
|
+
}
|
|
759
|
+
const storageKeys = keys.map((key) =>
|
|
760
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key,
|
|
761
|
+
);
|
|
762
|
+
const values = withWebBackendOperation(scope, "getBatch", (backend) => {
|
|
763
|
+
if (backend.getMany) {
|
|
764
|
+
return backend.getMany(storageKeys);
|
|
765
|
+
}
|
|
766
|
+
return storageKeys.map((storageKey) => backend.getItem(storageKey));
|
|
660
767
|
});
|
|
768
|
+
return values.map((value) => value ?? undefined);
|
|
661
769
|
},
|
|
662
770
|
removeBatch: (keys: string[], scope: number) => {
|
|
663
|
-
|
|
664
|
-
if (!storage) {
|
|
771
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
665
772
|
return;
|
|
666
773
|
}
|
|
667
774
|
|
|
668
775
|
if (scope === StorageScope.Secure) {
|
|
669
|
-
keys.
|
|
670
|
-
|
|
671
|
-
|
|
776
|
+
const storageKeys = keys.flatMap((key) => [
|
|
777
|
+
toSecureStorageKey(key),
|
|
778
|
+
toBiometricStorageKey(key),
|
|
779
|
+
]);
|
|
780
|
+
withWebBackendOperation(scope, "removeBatch", (backend) => {
|
|
781
|
+
if (backend.removeMany) {
|
|
782
|
+
backend.removeMany(storageKeys);
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
storageKeys.forEach((storageKey) => {
|
|
786
|
+
backend.removeItem(storageKey);
|
|
787
|
+
});
|
|
672
788
|
});
|
|
673
789
|
} else {
|
|
674
|
-
|
|
675
|
-
|
|
790
|
+
withWebBackendOperation(scope, "removeBatch", (backend) => {
|
|
791
|
+
if (backend.removeMany) {
|
|
792
|
+
backend.removeMany(keys);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
keys.forEach((key) => {
|
|
796
|
+
backend.removeItem(key);
|
|
797
|
+
});
|
|
676
798
|
});
|
|
677
799
|
}
|
|
678
800
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
684
|
-
}
|
|
801
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
802
|
+
keys.forEach((key) => keyIndex.delete(key));
|
|
803
|
+
const listeners = getScopedListeners(scope);
|
|
804
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
685
805
|
},
|
|
686
806
|
removeByPrefix: (prefix: string, scope: number) => {
|
|
687
807
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -703,14 +823,24 @@ const WebStorage: Storage = {
|
|
|
703
823
|
return () => {};
|
|
704
824
|
},
|
|
705
825
|
has: (key: string, scope: number) => {
|
|
706
|
-
const storage = getBrowserStorage(scope);
|
|
707
826
|
if (scope === StorageScope.Secure) {
|
|
708
827
|
return (
|
|
709
|
-
|
|
710
|
-
|
|
828
|
+
withWebBackendOperation(scope, "has", (backend) =>
|
|
829
|
+
backend.getItem(toSecureStorageKey(key)),
|
|
830
|
+
) !== null ||
|
|
831
|
+
withWebBackendOperation(scope, "has", (backend) =>
|
|
832
|
+
backend.getItem(toBiometricStorageKey(key)),
|
|
833
|
+
) !== null
|
|
711
834
|
);
|
|
712
835
|
}
|
|
713
|
-
|
|
836
|
+
if (scope !== StorageScope.Disk) {
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
return (
|
|
840
|
+
withWebBackendOperation(scope, "has", (backend) =>
|
|
841
|
+
backend.getItem(key),
|
|
842
|
+
) !== null
|
|
843
|
+
);
|
|
714
844
|
},
|
|
715
845
|
getAllKeys: (scope: number) => {
|
|
716
846
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -753,51 +883,85 @@ const WebStorage: Storage = {
|
|
|
753
883
|
"[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
|
|
754
884
|
);
|
|
755
885
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
886
|
+
withWebBackendOperation(
|
|
887
|
+
StorageScope.Secure,
|
|
888
|
+
"setSecureBiometric",
|
|
889
|
+
(backend) => backend.setItem(toBiometricStorageKey(key), value),
|
|
759
890
|
);
|
|
760
891
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
761
892
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
762
893
|
},
|
|
763
894
|
getSecureBiometric: (key: string) => {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
)
|
|
895
|
+
const value = withWebBackendOperation(
|
|
896
|
+
StorageScope.Secure,
|
|
897
|
+
"getSecureBiometric",
|
|
898
|
+
(backend) => backend.getItem(toBiometricStorageKey(key)),
|
|
768
899
|
);
|
|
900
|
+
return value ?? undefined;
|
|
769
901
|
},
|
|
770
902
|
deleteSecureBiometric: (key: string) => {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
903
|
+
withWebBackendOperation(
|
|
904
|
+
StorageScope.Secure,
|
|
905
|
+
"deleteSecureBiometric",
|
|
906
|
+
(backend) => backend.removeItem(toBiometricStorageKey(key)),
|
|
907
|
+
);
|
|
908
|
+
if (
|
|
909
|
+
withWebBackendOperation(
|
|
910
|
+
StorageScope.Secure,
|
|
911
|
+
"deleteSecureBiometric:getItem",
|
|
912
|
+
(backend) => backend.getItem(toSecureStorageKey(key)),
|
|
913
|
+
) === null
|
|
914
|
+
) {
|
|
774
915
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
775
916
|
}
|
|
776
917
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
777
918
|
},
|
|
778
919
|
hasSecureBiometric: (key: string) => {
|
|
779
920
|
return (
|
|
780
|
-
|
|
781
|
-
|
|
921
|
+
withWebBackendOperation(
|
|
922
|
+
StorageScope.Secure,
|
|
923
|
+
"hasSecureBiometric",
|
|
924
|
+
(backend) => backend.getItem(toBiometricStorageKey(key)),
|
|
782
925
|
) !== null
|
|
783
926
|
);
|
|
784
927
|
},
|
|
785
928
|
clearSecureBiometric: () => {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
929
|
+
const storageKeys = withWebBackendOperation(
|
|
930
|
+
StorageScope.Secure,
|
|
931
|
+
"clearSecureBiometric:getAllKeys",
|
|
932
|
+
(backend) => backend.getAllKeys(),
|
|
933
|
+
);
|
|
934
|
+
const keysToNotify = storageKeys
|
|
935
|
+
.filter((key) => key.startsWith(BIOMETRIC_WEB_PREFIX))
|
|
936
|
+
.map((key) => fromBiometricStorageKey(key));
|
|
937
|
+
if (keysToNotify.length === 0) {
|
|
938
|
+
return;
|
|
796
939
|
}
|
|
797
|
-
|
|
940
|
+
withWebBackendOperation(
|
|
941
|
+
StorageScope.Secure,
|
|
942
|
+
"clearSecureBiometric",
|
|
943
|
+
(backend) => {
|
|
944
|
+
const biometricKeys = keysToNotify.map((key) =>
|
|
945
|
+
toBiometricStorageKey(key),
|
|
946
|
+
);
|
|
947
|
+
if (backend.removeMany) {
|
|
948
|
+
backend.removeMany(biometricKeys);
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
biometricKeys.forEach((storageKey) => {
|
|
952
|
+
backend.removeItem(storageKey);
|
|
953
|
+
});
|
|
954
|
+
},
|
|
955
|
+
);
|
|
798
956
|
const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
|
|
799
957
|
keysToNotify.forEach((key) => {
|
|
800
|
-
if (
|
|
958
|
+
if (
|
|
959
|
+
withWebBackendOperation(
|
|
960
|
+
StorageScope.Secure,
|
|
961
|
+
"clearSecureBiometric:getItem",
|
|
962
|
+
(backend) => backend.getItem(toSecureStorageKey(key)),
|
|
963
|
+
) === null
|
|
964
|
+
) {
|
|
801
965
|
keyIndex.delete(key);
|
|
802
966
|
}
|
|
803
967
|
});
|
|
@@ -813,6 +977,10 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
|
813
977
|
return typeof value === "string" ? value : undefined;
|
|
814
978
|
}
|
|
815
979
|
|
|
980
|
+
if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
|
|
981
|
+
return readPendingDiskWrite(key);
|
|
982
|
+
}
|
|
983
|
+
|
|
816
984
|
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
817
985
|
return readPendingSecureWrite(key);
|
|
818
986
|
}
|
|
@@ -828,6 +996,17 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
828
996
|
return;
|
|
829
997
|
}
|
|
830
998
|
|
|
999
|
+
if (scope === StorageScope.Disk) {
|
|
1000
|
+
cacheRawValue(scope, key, value);
|
|
1001
|
+
if (diskWritesAsync) {
|
|
1002
|
+
scheduleDiskWrite(key, value);
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
flushDiskWrites();
|
|
1007
|
+
clearPendingDiskWrite(key);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
831
1010
|
if (scope === StorageScope.Secure) {
|
|
832
1011
|
flushSecureWrites();
|
|
833
1012
|
clearPendingSecureWrite(key);
|
|
@@ -845,6 +1024,17 @@ function removeRawValue(key: string, scope: StorageScope): void {
|
|
|
845
1024
|
return;
|
|
846
1025
|
}
|
|
847
1026
|
|
|
1027
|
+
if (scope === StorageScope.Disk) {
|
|
1028
|
+
cacheRawValue(scope, key, undefined);
|
|
1029
|
+
if (diskWritesAsync) {
|
|
1030
|
+
scheduleDiskWrite(key, undefined);
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
flushDiskWrites();
|
|
1035
|
+
clearPendingDiskWrite(key);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
848
1038
|
if (scope === StorageScope.Secure) {
|
|
849
1039
|
flushSecureWrites();
|
|
850
1040
|
clearPendingSecureWrite(key);
|
|
@@ -877,6 +1067,11 @@ export const storage = {
|
|
|
877
1067
|
return;
|
|
878
1068
|
}
|
|
879
1069
|
|
|
1070
|
+
if (scope === StorageScope.Disk) {
|
|
1071
|
+
flushDiskWrites();
|
|
1072
|
+
pendingDiskWrites.clear();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
880
1075
|
if (scope === StorageScope.Secure) {
|
|
881
1076
|
flushSecureWrites();
|
|
882
1077
|
pendingSecureWrites.clear();
|
|
@@ -912,6 +1107,9 @@ export const storage = {
|
|
|
912
1107
|
}
|
|
913
1108
|
|
|
914
1109
|
const keyPrefix = prefixKey(namespace, "");
|
|
1110
|
+
if (scope === StorageScope.Disk) {
|
|
1111
|
+
flushDiskWrites();
|
|
1112
|
+
}
|
|
915
1113
|
if (scope === StorageScope.Secure) {
|
|
916
1114
|
flushSecureWrites();
|
|
917
1115
|
}
|
|
@@ -933,6 +1131,12 @@ export const storage = {
|
|
|
933
1131
|
return measureOperation("storage:has", scope, () => {
|
|
934
1132
|
assertValidScope(scope);
|
|
935
1133
|
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
1134
|
+
if (scope === StorageScope.Disk) {
|
|
1135
|
+
flushDiskWrites();
|
|
1136
|
+
}
|
|
1137
|
+
if (scope === StorageScope.Secure) {
|
|
1138
|
+
flushSecureWrites();
|
|
1139
|
+
}
|
|
936
1140
|
return WebStorage.has(key, scope);
|
|
937
1141
|
});
|
|
938
1142
|
},
|
|
@@ -940,6 +1144,12 @@ export const storage = {
|
|
|
940
1144
|
return measureOperation("storage:getAllKeys", scope, () => {
|
|
941
1145
|
assertValidScope(scope);
|
|
942
1146
|
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
1147
|
+
if (scope === StorageScope.Disk) {
|
|
1148
|
+
flushDiskWrites();
|
|
1149
|
+
}
|
|
1150
|
+
if (scope === StorageScope.Secure) {
|
|
1151
|
+
flushSecureWrites();
|
|
1152
|
+
}
|
|
943
1153
|
return WebStorage.getAllKeys(scope);
|
|
944
1154
|
});
|
|
945
1155
|
},
|
|
@@ -951,6 +1161,12 @@ export const storage = {
|
|
|
951
1161
|
key.startsWith(prefix),
|
|
952
1162
|
);
|
|
953
1163
|
}
|
|
1164
|
+
if (scope === StorageScope.Disk) {
|
|
1165
|
+
flushDiskWrites();
|
|
1166
|
+
}
|
|
1167
|
+
if (scope === StorageScope.Secure) {
|
|
1168
|
+
flushSecureWrites();
|
|
1169
|
+
}
|
|
954
1170
|
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
955
1171
|
});
|
|
956
1172
|
},
|
|
@@ -975,6 +1191,12 @@ export const storage = {
|
|
|
975
1191
|
return result;
|
|
976
1192
|
}
|
|
977
1193
|
|
|
1194
|
+
if (scope === StorageScope.Disk) {
|
|
1195
|
+
flushDiskWrites();
|
|
1196
|
+
}
|
|
1197
|
+
if (scope === StorageScope.Secure) {
|
|
1198
|
+
flushSecureWrites();
|
|
1199
|
+
}
|
|
978
1200
|
const values = WebStorage.getBatch(keys, scope);
|
|
979
1201
|
keys.forEach((key, index) => {
|
|
980
1202
|
const value = values[index];
|
|
@@ -995,6 +1217,12 @@ export const storage = {
|
|
|
995
1217
|
});
|
|
996
1218
|
return result;
|
|
997
1219
|
}
|
|
1220
|
+
if (scope === StorageScope.Disk) {
|
|
1221
|
+
flushDiskWrites();
|
|
1222
|
+
}
|
|
1223
|
+
if (scope === StorageScope.Secure) {
|
|
1224
|
+
flushSecureWrites();
|
|
1225
|
+
}
|
|
998
1226
|
const keys = WebStorage.getAllKeys(scope);
|
|
999
1227
|
if (keys.length === 0) return {};
|
|
1000
1228
|
const values = WebStorage.getBatch(keys, scope);
|
|
@@ -1011,6 +1239,12 @@ export const storage = {
|
|
|
1011
1239
|
return measureOperation("storage:size", scope, () => {
|
|
1012
1240
|
assertValidScope(scope);
|
|
1013
1241
|
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
1242
|
+
if (scope === StorageScope.Disk) {
|
|
1243
|
+
flushDiskWrites();
|
|
1244
|
+
}
|
|
1245
|
+
if (scope === StorageScope.Secure) {
|
|
1246
|
+
flushSecureWrites();
|
|
1247
|
+
}
|
|
1014
1248
|
return WebStorage.size(scope);
|
|
1015
1249
|
});
|
|
1016
1250
|
},
|
|
@@ -1021,6 +1255,19 @@ export const storage = {
|
|
|
1021
1255
|
setSecureWritesAsync: (_enabled: boolean) => {
|
|
1022
1256
|
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
1023
1257
|
},
|
|
1258
|
+
setDiskWritesAsync: (enabled: boolean) => {
|
|
1259
|
+
measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
|
|
1260
|
+
diskWritesAsync = enabled;
|
|
1261
|
+
if (!enabled) {
|
|
1262
|
+
flushDiskWrites();
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
1265
|
+
},
|
|
1266
|
+
flushDiskWrites: () => {
|
|
1267
|
+
measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
|
|
1268
|
+
flushDiskWrites();
|
|
1269
|
+
});
|
|
1270
|
+
},
|
|
1024
1271
|
flushSecureWrites: () => {
|
|
1025
1272
|
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
1026
1273
|
flushSecureWrites();
|
|
@@ -1048,6 +1295,18 @@ export const storage = {
|
|
|
1048
1295
|
resetMetrics: () => {
|
|
1049
1296
|
metricsCounters.clear();
|
|
1050
1297
|
},
|
|
1298
|
+
getCapabilities: (): StorageCapabilities => ({
|
|
1299
|
+
platform: "web",
|
|
1300
|
+
backend: {
|
|
1301
|
+
disk: getBackendName(StorageScope.Disk, webDiskStorageBackend),
|
|
1302
|
+
secure: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
1303
|
+
},
|
|
1304
|
+
writeBuffering: {
|
|
1305
|
+
disk: true,
|
|
1306
|
+
secure: true,
|
|
1307
|
+
},
|
|
1308
|
+
errorClassification: true,
|
|
1309
|
+
}),
|
|
1051
1310
|
getString: (key: string, scope: StorageScope): string | undefined => {
|
|
1052
1311
|
return measureOperation("storage:getString", scope, () => {
|
|
1053
1312
|
return getRawValue(key, scope);
|
|
@@ -1085,6 +1344,9 @@ export const storage = {
|
|
|
1085
1344
|
flushSecureWrites();
|
|
1086
1345
|
WebStorage.setSecureAccessControl(secureDefaultAccessControl);
|
|
1087
1346
|
}
|
|
1347
|
+
if (scope === StorageScope.Disk) {
|
|
1348
|
+
flushDiskWrites();
|
|
1349
|
+
}
|
|
1088
1350
|
|
|
1089
1351
|
WebStorage.setBatch(keys, values, scope);
|
|
1090
1352
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
@@ -1097,11 +1359,12 @@ export const storage = {
|
|
|
1097
1359
|
export function setWebSecureStorageBackend(
|
|
1098
1360
|
backend?: WebSecureStorageBackend,
|
|
1099
1361
|
): void {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1362
|
+
pendingSecureWrites.clear();
|
|
1363
|
+
webSecureStorageBackend = backend ?? createDefaultSecureBackend();
|
|
1364
|
+
resetBackendChangeSubscription(StorageScope.Secure);
|
|
1103
1365
|
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
1104
1366
|
clearScopeRawCache(StorageScope.Secure);
|
|
1367
|
+
ensureExternalSyncSubscriptions();
|
|
1105
1368
|
}
|
|
1106
1369
|
|
|
1107
1370
|
export function getWebSecureStorageBackend():
|
|
@@ -1110,6 +1373,39 @@ export function getWebSecureStorageBackend():
|
|
|
1110
1373
|
return webSecureStorageBackend;
|
|
1111
1374
|
}
|
|
1112
1375
|
|
|
1376
|
+
export function setWebDiskStorageBackend(
|
|
1377
|
+
backend?: WebDiskStorageBackend,
|
|
1378
|
+
): void {
|
|
1379
|
+
pendingDiskWrites.clear();
|
|
1380
|
+
webDiskStorageBackend = backend ?? createDefaultDiskBackend();
|
|
1381
|
+
resetBackendChangeSubscription(StorageScope.Disk);
|
|
1382
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
|
|
1383
|
+
clearScopeRawCache(StorageScope.Disk);
|
|
1384
|
+
ensureExternalSyncSubscriptions();
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
|
|
1388
|
+
return webDiskStorageBackend;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
export async function flushWebStorageBackends(): Promise<void> {
|
|
1392
|
+
flushDiskWrites();
|
|
1393
|
+
flushSecureWrites();
|
|
1394
|
+
|
|
1395
|
+
const flushes: Promise<void>[] = [];
|
|
1396
|
+
const diskFlush = webDiskStorageBackend?.flush;
|
|
1397
|
+
const secureFlush = webSecureStorageBackend?.flush;
|
|
1398
|
+
|
|
1399
|
+
if (diskFlush) {
|
|
1400
|
+
flushes.push(diskFlush());
|
|
1401
|
+
}
|
|
1402
|
+
if (secureFlush) {
|
|
1403
|
+
flushes.push(secureFlush());
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
await Promise.all(flushes);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1113
1409
|
export interface StorageItemConfig<T> {
|
|
1114
1410
|
key: string;
|
|
1115
1411
|
scope: StorageScope;
|
|
@@ -1121,6 +1417,7 @@ export interface StorageItemConfig<T> {
|
|
|
1121
1417
|
expiration?: ExpirationConfig;
|
|
1122
1418
|
onExpired?: (key: string) => void;
|
|
1123
1419
|
readCache?: boolean;
|
|
1420
|
+
coalesceDiskWrites?: boolean;
|
|
1124
1421
|
coalesceSecureWrites?: boolean;
|
|
1125
1422
|
namespace?: string;
|
|
1126
1423
|
biometric?: boolean;
|
|
@@ -1205,6 +1502,8 @@ export function createStorageItem<T = undefined>(
|
|
|
1205
1502
|
const memoryExpiration =
|
|
1206
1503
|
expiration && isMemory ? new Map<string, number>() : null;
|
|
1207
1504
|
const readCache = !isMemory && config.readCache === true;
|
|
1505
|
+
const coalesceDiskWrites =
|
|
1506
|
+
config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
1208
1507
|
const coalesceSecureWrites =
|
|
1209
1508
|
config.scope === StorageScope.Secure &&
|
|
1210
1509
|
config.coalesceSecureWrites === true &&
|
|
@@ -1250,7 +1549,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1250
1549
|
return;
|
|
1251
1550
|
}
|
|
1252
1551
|
|
|
1253
|
-
|
|
1552
|
+
ensureExternalSyncSubscriptions();
|
|
1254
1553
|
unsubscribe = addKeyListener(
|
|
1255
1554
|
getScopedListeners(nonMemoryScope!),
|
|
1256
1555
|
storageKey,
|
|
@@ -1273,6 +1572,13 @@ export function createStorageItem<T = undefined>(
|
|
|
1273
1572
|
return memoryStore.get(storageKey);
|
|
1274
1573
|
}
|
|
1275
1574
|
|
|
1575
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1576
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1577
|
+
if (pending !== undefined) {
|
|
1578
|
+
return pending.value;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1276
1582
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1277
1583
|
const pending = pendingSecureWrites.get(storageKey);
|
|
1278
1584
|
if (pending !== undefined) {
|
|
@@ -1309,6 +1615,15 @@ export function createStorageItem<T = undefined>(
|
|
|
1309
1615
|
|
|
1310
1616
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
1311
1617
|
|
|
1618
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1619
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1620
|
+
scheduleDiskWrite(storageKey, rawValue);
|
|
1621
|
+
return;
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
clearPendingDiskWrite(storageKey);
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1312
1627
|
if (coalesceSecureWrites) {
|
|
1313
1628
|
scheduleSecureWrite(
|
|
1314
1629
|
storageKey,
|
|
@@ -1333,6 +1648,15 @@ export function createStorageItem<T = undefined>(
|
|
|
1333
1648
|
|
|
1334
1649
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
1335
1650
|
|
|
1651
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1652
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1653
|
+
scheduleDiskWrite(storageKey, undefined);
|
|
1654
|
+
return;
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
clearPendingDiskWrite(storageKey);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1336
1660
|
if (coalesceSecureWrites) {
|
|
1337
1661
|
scheduleSecureWrite(
|
|
1338
1662
|
storageKey,
|
|
@@ -1543,6 +1867,18 @@ export function createStorageItem<T = undefined>(
|
|
|
1543
1867
|
measureOperation("item:has", config.scope, () => {
|
|
1544
1868
|
if (isMemory) return memoryStore.has(storageKey);
|
|
1545
1869
|
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
1870
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1871
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1872
|
+
if (pending !== undefined) {
|
|
1873
|
+
return pending.value !== undefined;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1877
|
+
const pending = pendingSecureWrites.get(storageKey);
|
|
1878
|
+
if (pending !== undefined) {
|
|
1879
|
+
return pending.value !== undefined;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1546
1882
|
return WebStorage.has(storageKey, config.scope);
|
|
1547
1883
|
});
|
|
1548
1884
|
|
|
@@ -1554,9 +1890,6 @@ export function createStorageItem<T = undefined>(
|
|
|
1554
1890
|
if (listeners.size === 0 && unsubscribe) {
|
|
1555
1891
|
unsubscribe();
|
|
1556
1892
|
unsubscribe = null;
|
|
1557
|
-
if (!isMemory) {
|
|
1558
|
-
maybeCleanupWebStorageSubscription();
|
|
1559
|
-
}
|
|
1560
1893
|
}
|
|
1561
1894
|
};
|
|
1562
1895
|
};
|
|
@@ -1642,6 +1975,14 @@ export function getBatch(
|
|
|
1642
1975
|
const keyIndexes: number[] = [];
|
|
1643
1976
|
|
|
1644
1977
|
items.forEach((item, index) => {
|
|
1978
|
+
if (scope === StorageScope.Disk) {
|
|
1979
|
+
const pending = pendingDiskWrites.get(item.key);
|
|
1980
|
+
if (pending !== undefined) {
|
|
1981
|
+
rawValues[index] = pending.value;
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
|
|
1645
1986
|
if (scope === StorageScope.Secure) {
|
|
1646
1987
|
const pending = pendingSecureWrites.get(item.key);
|
|
1647
1988
|
if (pending !== undefined) {
|
|
@@ -1766,6 +2107,8 @@ export function setBatch<T>(
|
|
|
1766
2107
|
return;
|
|
1767
2108
|
}
|
|
1768
2109
|
|
|
2110
|
+
flushDiskWrites();
|
|
2111
|
+
|
|
1769
2112
|
const useRawBatchPath = items.every(({ item }) =>
|
|
1770
2113
|
canUseRawBatchPath(asInternal(item)),
|
|
1771
2114
|
);
|
|
@@ -1799,6 +2142,9 @@ export function removeBatch(
|
|
|
1799
2142
|
}
|
|
1800
2143
|
|
|
1801
2144
|
const keys = items.map((item) => item.key);
|
|
2145
|
+
if (scope === StorageScope.Disk) {
|
|
2146
|
+
flushDiskWrites();
|
|
2147
|
+
}
|
|
1802
2148
|
if (scope === StorageScope.Secure) {
|
|
1803
2149
|
flushSecureWrites();
|
|
1804
2150
|
}
|
|
@@ -1862,6 +2208,9 @@ export function runTransaction<T>(
|
|
|
1862
2208
|
): T {
|
|
1863
2209
|
return measureOperation("transaction:run", scope, () => {
|
|
1864
2210
|
assertValidScope(scope);
|
|
2211
|
+
if (scope === StorageScope.Disk) {
|
|
2212
|
+
flushDiskWrites();
|
|
2213
|
+
}
|
|
1865
2214
|
if (scope === StorageScope.Secure) {
|
|
1866
2215
|
flushSecureWrites();
|
|
1867
2216
|
}
|
|
@@ -1937,6 +2286,9 @@ export function runTransaction<T>(
|
|
|
1937
2286
|
}
|
|
1938
2287
|
});
|
|
1939
2288
|
|
|
2289
|
+
if (scope === StorageScope.Disk) {
|
|
2290
|
+
flushDiskWrites();
|
|
2291
|
+
}
|
|
1940
2292
|
if (scope === StorageScope.Secure) {
|
|
1941
2293
|
flushSecureWrites();
|
|
1942
2294
|
}
|
|
@@ -2000,6 +2352,6 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
2000
2352
|
return result as Record<K, StorageItem<string>>;
|
|
2001
2353
|
}
|
|
2002
2354
|
|
|
2003
|
-
export function isKeychainLockedError(
|
|
2004
|
-
return
|
|
2355
|
+
export function isKeychainLockedError(err: unknown): boolean {
|
|
2356
|
+
return isLockedStorageErrorCode(getStorageErrorCode(err));
|
|
2005
2357
|
}
|