react-native-nitro-storage 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +237 -862
- package/SECURITY.md +26 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
- package/docs/api-reference.md +217 -0
- package/docs/batch-transactions-migrations.md +186 -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 +281 -0
- package/docs/secure-storage.md +171 -0
- package/docs/web-backends.md +141 -0
- package/ios/IOSStorageAdapterCpp.mm +44 -14
- package/lib/commonjs/index.js +271 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +498 -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 +263 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +490 -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 +14 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +15 -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 +48 -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 +21 -8
- package/src/index.ts +330 -20
- package/src/index.web.ts +673 -245
- package/src/indexeddb-backend.ts +147 -6
- package/src/storage-runtime.ts +129 -0
- package/src/web-storage-backend.ts +129 -0
package/src/index.web.ts
CHANGED
|
@@ -11,9 +11,36 @@ 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 SecureStorageMetadata,
|
|
25
|
+
type SecurityCapabilities,
|
|
26
|
+
type StorageCapabilities,
|
|
27
|
+
type StorageErrorCode,
|
|
28
|
+
} from "./storage-runtime";
|
|
14
29
|
|
|
15
30
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
16
31
|
export { migrateFromMMKV } from "./migration";
|
|
32
|
+
export {
|
|
33
|
+
getStorageErrorCode,
|
|
34
|
+
type SecureStorageMetadata,
|
|
35
|
+
type SecurityCapabilities,
|
|
36
|
+
type StorageCapabilities,
|
|
37
|
+
type StorageErrorCode,
|
|
38
|
+
} from "./storage-runtime";
|
|
39
|
+
export type {
|
|
40
|
+
WebStorageBackend,
|
|
41
|
+
WebStorageChangeEvent,
|
|
42
|
+
WebStorageScope,
|
|
43
|
+
} from "./web-storage-backend";
|
|
17
44
|
|
|
18
45
|
export type Validator<T> = (value: unknown) => value is T;
|
|
19
46
|
export type ExpirationConfig = {
|
|
@@ -37,14 +64,6 @@ export type StorageMetricSummary = {
|
|
|
37
64
|
avgDurationMs: number;
|
|
38
65
|
maxDurationMs: number;
|
|
39
66
|
};
|
|
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
67
|
export type MigrationContext = {
|
|
49
68
|
scope: StorageScope;
|
|
50
69
|
getRaw: (key: string) => string | undefined;
|
|
@@ -91,19 +110,15 @@ function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
|
91
110
|
return Object.keys(record) as K[];
|
|
92
111
|
}
|
|
93
112
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
113
|
+
type PendingDiskWrite = {
|
|
114
|
+
key: string;
|
|
115
|
+
value: string | undefined;
|
|
116
|
+
};
|
|
94
117
|
type PendingSecureWrite = {
|
|
95
118
|
key: string;
|
|
96
119
|
value: string | undefined;
|
|
97
120
|
accessControl?: AccessControl;
|
|
98
121
|
};
|
|
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
122
|
|
|
108
123
|
const registeredMigrations = new Map<number, Migration>();
|
|
109
124
|
const runMicrotask =
|
|
@@ -112,6 +127,10 @@ const runMicrotask =
|
|
|
112
127
|
: (task: () => void) => {
|
|
113
128
|
Promise.resolve().then(task);
|
|
114
129
|
};
|
|
130
|
+
const now =
|
|
131
|
+
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
132
|
+
? () => performance.now()
|
|
133
|
+
: () => Date.now();
|
|
115
134
|
|
|
116
135
|
export interface Storage {
|
|
117
136
|
name: string;
|
|
@@ -161,14 +180,16 @@ const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
|
|
|
161
180
|
[StorageScope.Secure, new Set()],
|
|
162
181
|
]);
|
|
163
182
|
const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
|
|
183
|
+
const pendingDiskWrites = new Map<string, PendingDiskWrite>();
|
|
184
|
+
let diskFlushScheduled = false;
|
|
185
|
+
let diskWritesAsync = false;
|
|
164
186
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
165
187
|
let secureFlushScheduled = false;
|
|
166
188
|
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
167
189
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
168
190
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
169
191
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
170
|
-
let
|
|
171
|
-
let webStorageSubscriberCount = 0;
|
|
192
|
+
let hasWindowStorageEventSubscription = false;
|
|
172
193
|
let metricsObserver: StorageMetricsObserver | undefined;
|
|
173
194
|
const metricsCounters = new Map<
|
|
174
195
|
string,
|
|
@@ -205,69 +226,92 @@ function measureOperation<T>(
|
|
|
205
226
|
if (!metricsObserver) {
|
|
206
227
|
return fn();
|
|
207
228
|
}
|
|
208
|
-
const start =
|
|
229
|
+
const start = now();
|
|
209
230
|
try {
|
|
210
231
|
return fn();
|
|
211
232
|
} finally {
|
|
212
|
-
recordMetric(operation, scope,
|
|
233
|
+
recordMetric(operation, scope, now() - start, keysCount);
|
|
213
234
|
}
|
|
214
235
|
}
|
|
215
236
|
|
|
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
|
-
};
|
|
237
|
+
function createDefaultDiskBackend(): WebDiskStorageBackend {
|
|
238
|
+
return createLocalStorageWebBackend({
|
|
239
|
+
name: "localStorage:disk",
|
|
240
|
+
includeKey: (key) =>
|
|
241
|
+
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
242
|
+
!key.startsWith(BIOMETRIC_WEB_PREFIX),
|
|
243
|
+
});
|
|
235
244
|
}
|
|
236
245
|
|
|
246
|
+
function createDefaultSecureBackend(): WebSecureStorageBackend {
|
|
247
|
+
return createLocalStorageWebBackend({
|
|
248
|
+
name: "localStorage:secure",
|
|
249
|
+
includeKey: (key) =>
|
|
250
|
+
key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
let webDiskStorageBackend: WebDiskStorageBackend | undefined =
|
|
255
|
+
createDefaultDiskBackend();
|
|
237
256
|
let webSecureStorageBackend: WebSecureStorageBackend | undefined =
|
|
238
|
-
|
|
257
|
+
createDefaultSecureBackend();
|
|
258
|
+
const externalSyncUnsubscribers = new Map<NonMemoryScope, () => void>();
|
|
239
259
|
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
function getBackendName(
|
|
261
|
+
scope: NonMemoryScope,
|
|
262
|
+
backend: WebStorageBackend | undefined,
|
|
263
|
+
): string {
|
|
264
|
+
const scopeName = scope === StorageScope.Disk ? "disk" : "secure";
|
|
265
|
+
return backend?.name ?? `web:${scopeName}`;
|
|
266
|
+
}
|
|
242
267
|
|
|
243
|
-
function
|
|
244
|
-
|
|
245
|
-
|
|
268
|
+
function getWebSecureEncryptionStatus(
|
|
269
|
+
backend: WebSecureStorageBackend | undefined,
|
|
270
|
+
): "unavailable" | "unknown" {
|
|
271
|
+
return backend?.name === "localStorage:secure" ? "unavailable" : "unknown";
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function createWebStorageError(
|
|
275
|
+
scope: NonMemoryScope,
|
|
276
|
+
operation: string,
|
|
277
|
+
error: unknown,
|
|
278
|
+
backend: WebStorageBackend | undefined,
|
|
279
|
+
): Error {
|
|
280
|
+
const backendName = getBackendName(scope, backend);
|
|
281
|
+
const message =
|
|
282
|
+
error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
283
|
+
return new Error(
|
|
284
|
+
`NitroStorage(web): ${operation} failed for ${backendName}: ${message}`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function withWebBackendOperation<T>(
|
|
289
|
+
scope: NonMemoryScope,
|
|
290
|
+
operation: string,
|
|
291
|
+
fn: (backend: WebStorageBackend) => T,
|
|
292
|
+
): T {
|
|
293
|
+
const backend =
|
|
294
|
+
scope === StorageScope.Disk
|
|
295
|
+
? webDiskStorageBackend
|
|
296
|
+
: webSecureStorageBackend;
|
|
297
|
+
if (!backend) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
`NitroStorage(web): ${operation} failed because no ${scope === StorageScope.Disk ? "disk" : "secure"} backend is configured.`,
|
|
300
|
+
);
|
|
246
301
|
}
|
|
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;
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
ensureExternalSyncSubscriptions();
|
|
305
|
+
return fn(backend);
|
|
306
|
+
} catch (error) {
|
|
307
|
+
throw createWebStorageError(scope, operation, error, backend);
|
|
269
308
|
}
|
|
270
|
-
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getWebBackend(scope: NonMemoryScope): WebStorageBackend | undefined {
|
|
312
|
+
return scope === StorageScope.Disk
|
|
313
|
+
? webDiskStorageBackend
|
|
314
|
+
: webSecureStorageBackend;
|
|
271
315
|
}
|
|
272
316
|
|
|
273
317
|
function toSecureStorageKey(key: string): string {
|
|
@@ -295,32 +339,22 @@ function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
|
|
|
295
339
|
return;
|
|
296
340
|
}
|
|
297
341
|
|
|
298
|
-
const
|
|
342
|
+
const backend = getWebBackend(scope);
|
|
299
343
|
const keyIndex = getWebScopeKeyIndex(scope);
|
|
300
344
|
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
|
-
}
|
|
345
|
+
const keys = backend?.getAllKeys() ?? [];
|
|
346
|
+
for (const key of keys) {
|
|
347
|
+
if (scope === StorageScope.Disk) {
|
|
348
|
+
keyIndex.add(key);
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
316
351
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
352
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
353
|
+
keyIndex.add(fromSecureStorageKey(key));
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
357
|
+
keyIndex.add(fromBiometricStorageKey(key));
|
|
324
358
|
}
|
|
325
359
|
}
|
|
326
360
|
hydratedWebScopeKeyIndex.add(scope);
|
|
@@ -331,37 +365,39 @@ function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
|
|
|
331
365
|
return getWebScopeKeyIndex(scope);
|
|
332
366
|
}
|
|
333
367
|
|
|
334
|
-
function
|
|
335
|
-
|
|
368
|
+
function applyExternalChangeEvent(
|
|
369
|
+
scope: NonMemoryScope,
|
|
370
|
+
key: string | null,
|
|
371
|
+
newValue: string | null,
|
|
372
|
+
): void {
|
|
336
373
|
if (key === null) {
|
|
337
|
-
clearScopeRawCache(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
ensureWebScopeKeyIndex(StorageScope.Secure).clear();
|
|
341
|
-
notifyAllListeners(getScopedListeners(StorageScope.Disk));
|
|
342
|
-
notifyAllListeners(getScopedListeners(StorageScope.Secure));
|
|
374
|
+
clearScopeRawCache(scope);
|
|
375
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
376
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
343
377
|
return;
|
|
344
378
|
}
|
|
345
379
|
|
|
346
|
-
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
380
|
+
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
347
381
|
const plainKey = fromSecureStorageKey(key);
|
|
348
|
-
if (
|
|
382
|
+
if (newValue === null) {
|
|
349
383
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
350
384
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
351
385
|
} else {
|
|
352
386
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
353
|
-
cacheRawValue(StorageScope.Secure, plainKey,
|
|
387
|
+
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
354
388
|
}
|
|
355
389
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
356
390
|
return;
|
|
357
391
|
}
|
|
358
392
|
|
|
359
|
-
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
393
|
+
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
360
394
|
const plainKey = fromBiometricStorageKey(key);
|
|
361
|
-
if (
|
|
395
|
+
if (newValue === null) {
|
|
362
396
|
if (
|
|
363
|
-
|
|
364
|
-
|
|
397
|
+
withWebBackendOperation(
|
|
398
|
+
StorageScope.Secure,
|
|
399
|
+
"external-sync:getItem",
|
|
400
|
+
(backend) => backend.getItem(toSecureStorageKey(plainKey)),
|
|
365
401
|
) === null
|
|
366
402
|
) {
|
|
367
403
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
@@ -369,44 +405,74 @@ function handleWebStorageEvent(event: StorageEvent): void {
|
|
|
369
405
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
370
406
|
} else {
|
|
371
407
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
372
|
-
cacheRawValue(StorageScope.Secure, plainKey,
|
|
408
|
+
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
373
409
|
}
|
|
374
410
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
375
411
|
return;
|
|
376
412
|
}
|
|
377
413
|
|
|
378
|
-
if (
|
|
379
|
-
ensureWebScopeKeyIndex(
|
|
380
|
-
cacheRawValue(
|
|
414
|
+
if (newValue === null) {
|
|
415
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
416
|
+
cacheRawValue(scope, key, undefined);
|
|
381
417
|
} else {
|
|
382
|
-
ensureWebScopeKeyIndex(
|
|
383
|
-
cacheRawValue(
|
|
418
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
419
|
+
cacheRawValue(scope, key, newValue);
|
|
384
420
|
}
|
|
385
|
-
notifyKeyListeners(getScopedListeners(
|
|
421
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
386
422
|
}
|
|
387
423
|
|
|
388
|
-
function
|
|
389
|
-
|
|
424
|
+
function handleWebStorageEvent(event: StorageEvent): void {
|
|
425
|
+
const key = event.key;
|
|
426
|
+
if (key === null) {
|
|
427
|
+
applyExternalChangeEvent(StorageScope.Disk, null, null);
|
|
428
|
+
applyExternalChangeEvent(StorageScope.Secure, null, null);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
390
432
|
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
typeof window.addEventListener === "function"
|
|
433
|
+
key.startsWith(SECURE_WEB_PREFIX) ||
|
|
434
|
+
key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
394
435
|
) {
|
|
395
|
-
|
|
396
|
-
|
|
436
|
+
applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
|
|
437
|
+
return;
|
|
397
438
|
}
|
|
439
|
+
|
|
440
|
+
applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function subscribeToBackendChanges(scope: NonMemoryScope): void {
|
|
444
|
+
if (externalSyncUnsubscribers.has(scope)) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const backend = getWebBackend(scope);
|
|
449
|
+
if (!backend?.subscribe) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const unsubscribe = backend.subscribe((event: WebStorageChangeEvent) => {
|
|
454
|
+
applyExternalChangeEvent(scope, event.key, event.newValue);
|
|
455
|
+
});
|
|
456
|
+
externalSyncUnsubscribers.set(scope, unsubscribe);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function resetBackendChangeSubscription(scope: NonMemoryScope): void {
|
|
460
|
+
externalSyncUnsubscribers.get(scope)?.();
|
|
461
|
+
externalSyncUnsubscribers.delete(scope);
|
|
398
462
|
}
|
|
399
463
|
|
|
400
|
-
function
|
|
401
|
-
webStorageSubscriberCount = Math.max(0, webStorageSubscriberCount - 1);
|
|
464
|
+
function ensureExternalSyncSubscriptions(): void {
|
|
402
465
|
if (
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
typeof window
|
|
466
|
+
!hasWindowStorageEventSubscription &&
|
|
467
|
+
typeof window !== "undefined" &&
|
|
468
|
+
typeof window.addEventListener === "function"
|
|
406
469
|
) {
|
|
407
|
-
window.
|
|
408
|
-
|
|
470
|
+
window.addEventListener("storage", handleWebStorageEvent);
|
|
471
|
+
hasWindowStorageEventSubscription = true;
|
|
409
472
|
}
|
|
473
|
+
|
|
474
|
+
subscribeToBackendChanges(StorageScope.Disk);
|
|
475
|
+
subscribeToBackendChanges(StorageScope.Secure);
|
|
410
476
|
}
|
|
411
477
|
|
|
412
478
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
@@ -487,14 +553,58 @@ function readPendingSecureWrite(key: string): string | undefined {
|
|
|
487
553
|
return pendingSecureWrites.get(key)?.value;
|
|
488
554
|
}
|
|
489
555
|
|
|
556
|
+
function readPendingDiskWrite(key: string): string | undefined {
|
|
557
|
+
return pendingDiskWrites.get(key)?.value;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function hasPendingDiskWrite(key: string): boolean {
|
|
561
|
+
return pendingDiskWrites.has(key);
|
|
562
|
+
}
|
|
563
|
+
|
|
490
564
|
function hasPendingSecureWrite(key: string): boolean {
|
|
491
565
|
return pendingSecureWrites.has(key);
|
|
492
566
|
}
|
|
493
567
|
|
|
568
|
+
function clearPendingDiskWrite(key: string): void {
|
|
569
|
+
pendingDiskWrites.delete(key);
|
|
570
|
+
}
|
|
571
|
+
|
|
494
572
|
function clearPendingSecureWrite(key: string): void {
|
|
495
573
|
pendingSecureWrites.delete(key);
|
|
496
574
|
}
|
|
497
575
|
|
|
576
|
+
function flushDiskWrites(): void {
|
|
577
|
+
diskFlushScheduled = false;
|
|
578
|
+
|
|
579
|
+
if (pendingDiskWrites.size === 0) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const writes = Array.from(pendingDiskWrites.values());
|
|
584
|
+
pendingDiskWrites.clear();
|
|
585
|
+
|
|
586
|
+
const keysToSet: string[] = [];
|
|
587
|
+
const valuesToSet: string[] = [];
|
|
588
|
+
const keysToRemove: string[] = [];
|
|
589
|
+
|
|
590
|
+
writes.forEach(({ key, value }) => {
|
|
591
|
+
if (value === undefined) {
|
|
592
|
+
keysToRemove.push(key);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
keysToSet.push(key);
|
|
597
|
+
valuesToSet.push(value);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
if (keysToSet.length > 0) {
|
|
601
|
+
WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
602
|
+
}
|
|
603
|
+
if (keysToRemove.length > 0) {
|
|
604
|
+
WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
498
608
|
function flushSecureWrites(): void {
|
|
499
609
|
secureFlushScheduled = false;
|
|
500
610
|
|
|
@@ -535,6 +645,15 @@ function flushSecureWrites(): void {
|
|
|
535
645
|
}
|
|
536
646
|
}
|
|
537
647
|
|
|
648
|
+
function scheduleDiskWrite(key: string, value: string | undefined): void {
|
|
649
|
+
pendingDiskWrites.set(key, { key, value });
|
|
650
|
+
if (diskFlushScheduled) {
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
diskFlushScheduled = true;
|
|
654
|
+
runMicrotask(flushDiskWrites);
|
|
655
|
+
}
|
|
656
|
+
|
|
538
657
|
function scheduleSecureWrite(
|
|
539
658
|
key: string,
|
|
540
659
|
value: string | undefined,
|
|
@@ -557,131 +676,142 @@ const WebStorage: Storage = {
|
|
|
557
676
|
equals: (other) => other === WebStorage,
|
|
558
677
|
dispose: () => {},
|
|
559
678
|
set: (key: string, value: string, scope: number) => {
|
|
560
|
-
|
|
561
|
-
if (!storage) {
|
|
679
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
562
680
|
return;
|
|
563
681
|
}
|
|
564
682
|
const storageKey =
|
|
565
683
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
684
|
+
withWebBackendOperation(scope, "set", (backend) => {
|
|
685
|
+
backend.setItem(storageKey, value);
|
|
686
|
+
});
|
|
687
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
688
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
571
689
|
},
|
|
572
690
|
get: (key: string, scope: number) => {
|
|
573
|
-
|
|
691
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
574
694
|
const storageKey =
|
|
575
695
|
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
576
|
-
|
|
696
|
+
const value = withWebBackendOperation(scope, "get", (backend) =>
|
|
697
|
+
backend.getItem(storageKey),
|
|
698
|
+
);
|
|
699
|
+
return value ?? undefined;
|
|
577
700
|
},
|
|
578
701
|
remove: (key: string, scope: number) => {
|
|
579
|
-
|
|
580
|
-
if (!storage) {
|
|
702
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
581
703
|
return;
|
|
582
704
|
}
|
|
583
705
|
if (scope === StorageScope.Secure) {
|
|
584
|
-
|
|
585
|
-
|
|
706
|
+
withWebBackendOperation(scope, "remove", (backend) => {
|
|
707
|
+
if (backend.removeMany) {
|
|
708
|
+
backend.removeMany([
|
|
709
|
+
toSecureStorageKey(key),
|
|
710
|
+
toBiometricStorageKey(key),
|
|
711
|
+
]);
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
backend.removeItem(toSecureStorageKey(key));
|
|
715
|
+
backend.removeItem(toBiometricStorageKey(key));
|
|
716
|
+
});
|
|
586
717
|
} else {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
ensureWebScopeKeyIndex(scope).delete(key);
|
|
591
|
-
notifyKeyListeners(getScopedListeners(scope), key);
|
|
718
|
+
withWebBackendOperation(scope, "remove", (backend) => {
|
|
719
|
+
backend.removeItem(key);
|
|
720
|
+
});
|
|
592
721
|
}
|
|
722
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
723
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
593
724
|
},
|
|
594
725
|
clear: (scope: number) => {
|
|
595
|
-
|
|
596
|
-
if (!storage) {
|
|
726
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
597
727
|
return;
|
|
598
728
|
}
|
|
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
|
-
}
|
|
729
|
+
withWebBackendOperation(scope, "clear", (backend) => {
|
|
730
|
+
backend.clear();
|
|
731
|
+
});
|
|
732
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
733
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
631
734
|
},
|
|
632
735
|
setBatch: (keys: string[], values: string[], scope: number) => {
|
|
633
|
-
|
|
634
|
-
if (!storage) {
|
|
736
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
635
737
|
return;
|
|
636
738
|
}
|
|
637
739
|
|
|
740
|
+
const entries: (readonly [string, string])[] = [];
|
|
638
741
|
keys.forEach((key, index) => {
|
|
639
742
|
const value = values[index];
|
|
640
743
|
if (value === undefined) {
|
|
641
744
|
return;
|
|
642
745
|
}
|
|
643
|
-
|
|
644
|
-
scope === StorageScope.Secure ? toSecureStorageKey(key) : key
|
|
645
|
-
|
|
746
|
+
entries.push([
|
|
747
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key,
|
|
748
|
+
value,
|
|
749
|
+
]);
|
|
646
750
|
});
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
751
|
+
withWebBackendOperation(scope, "setBatch", (backend) => {
|
|
752
|
+
if (backend.setMany) {
|
|
753
|
+
backend.setMany(entries);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
entries.forEach(([storageKey, value]) => {
|
|
757
|
+
backend.setItem(storageKey, value);
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
761
|
+
keys.forEach((key) => keyIndex.add(key));
|
|
762
|
+
const listeners = getScopedListeners(scope);
|
|
763
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
653
764
|
},
|
|
654
765
|
getBatch: (keys: string[], scope: number) => {
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
766
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
767
|
+
return keys.map(() => undefined);
|
|
768
|
+
}
|
|
769
|
+
const storageKeys = keys.map((key) =>
|
|
770
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key,
|
|
771
|
+
);
|
|
772
|
+
const values = withWebBackendOperation(scope, "getBatch", (backend) => {
|
|
773
|
+
if (backend.getMany) {
|
|
774
|
+
return backend.getMany(storageKeys);
|
|
775
|
+
}
|
|
776
|
+
return storageKeys.map((storageKey) => backend.getItem(storageKey));
|
|
660
777
|
});
|
|
778
|
+
return values.map((value) => value ?? undefined);
|
|
661
779
|
},
|
|
662
780
|
removeBatch: (keys: string[], scope: number) => {
|
|
663
|
-
|
|
664
|
-
if (!storage) {
|
|
781
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
665
782
|
return;
|
|
666
783
|
}
|
|
667
784
|
|
|
668
785
|
if (scope === StorageScope.Secure) {
|
|
669
|
-
keys.
|
|
670
|
-
|
|
671
|
-
|
|
786
|
+
const storageKeys = keys.flatMap((key) => [
|
|
787
|
+
toSecureStorageKey(key),
|
|
788
|
+
toBiometricStorageKey(key),
|
|
789
|
+
]);
|
|
790
|
+
withWebBackendOperation(scope, "removeBatch", (backend) => {
|
|
791
|
+
if (backend.removeMany) {
|
|
792
|
+
backend.removeMany(storageKeys);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
storageKeys.forEach((storageKey) => {
|
|
796
|
+
backend.removeItem(storageKey);
|
|
797
|
+
});
|
|
672
798
|
});
|
|
673
799
|
} else {
|
|
674
|
-
|
|
675
|
-
|
|
800
|
+
withWebBackendOperation(scope, "removeBatch", (backend) => {
|
|
801
|
+
if (backend.removeMany) {
|
|
802
|
+
backend.removeMany(keys);
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
keys.forEach((key) => {
|
|
806
|
+
backend.removeItem(key);
|
|
807
|
+
});
|
|
676
808
|
});
|
|
677
809
|
}
|
|
678
810
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
684
|
-
}
|
|
811
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
812
|
+
keys.forEach((key) => keyIndex.delete(key));
|
|
813
|
+
const listeners = getScopedListeners(scope);
|
|
814
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
685
815
|
},
|
|
686
816
|
removeByPrefix: (prefix: string, scope: number) => {
|
|
687
817
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -703,14 +833,24 @@ const WebStorage: Storage = {
|
|
|
703
833
|
return () => {};
|
|
704
834
|
},
|
|
705
835
|
has: (key: string, scope: number) => {
|
|
706
|
-
const storage = getBrowserStorage(scope);
|
|
707
836
|
if (scope === StorageScope.Secure) {
|
|
708
837
|
return (
|
|
709
|
-
|
|
710
|
-
|
|
838
|
+
withWebBackendOperation(scope, "has", (backend) =>
|
|
839
|
+
backend.getItem(toSecureStorageKey(key)),
|
|
840
|
+
) !== null ||
|
|
841
|
+
withWebBackendOperation(scope, "has", (backend) =>
|
|
842
|
+
backend.getItem(toBiometricStorageKey(key)),
|
|
843
|
+
) !== null
|
|
711
844
|
);
|
|
712
845
|
}
|
|
713
|
-
|
|
846
|
+
if (scope !== StorageScope.Disk) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
return (
|
|
850
|
+
withWebBackendOperation(scope, "has", (backend) =>
|
|
851
|
+
backend.getItem(key),
|
|
852
|
+
) !== null
|
|
853
|
+
);
|
|
714
854
|
},
|
|
715
855
|
getAllKeys: (scope: number) => {
|
|
716
856
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -753,51 +893,85 @@ const WebStorage: Storage = {
|
|
|
753
893
|
"[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
|
|
754
894
|
);
|
|
755
895
|
}
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
896
|
+
withWebBackendOperation(
|
|
897
|
+
StorageScope.Secure,
|
|
898
|
+
"setSecureBiometric",
|
|
899
|
+
(backend) => backend.setItem(toBiometricStorageKey(key), value),
|
|
759
900
|
);
|
|
760
901
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
761
902
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
762
903
|
},
|
|
763
904
|
getSecureBiometric: (key: string) => {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
)
|
|
905
|
+
const value = withWebBackendOperation(
|
|
906
|
+
StorageScope.Secure,
|
|
907
|
+
"getSecureBiometric",
|
|
908
|
+
(backend) => backend.getItem(toBiometricStorageKey(key)),
|
|
768
909
|
);
|
|
910
|
+
return value ?? undefined;
|
|
769
911
|
},
|
|
770
912
|
deleteSecureBiometric: (key: string) => {
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
913
|
+
withWebBackendOperation(
|
|
914
|
+
StorageScope.Secure,
|
|
915
|
+
"deleteSecureBiometric",
|
|
916
|
+
(backend) => backend.removeItem(toBiometricStorageKey(key)),
|
|
917
|
+
);
|
|
918
|
+
if (
|
|
919
|
+
withWebBackendOperation(
|
|
920
|
+
StorageScope.Secure,
|
|
921
|
+
"deleteSecureBiometric:getItem",
|
|
922
|
+
(backend) => backend.getItem(toSecureStorageKey(key)),
|
|
923
|
+
) === null
|
|
924
|
+
) {
|
|
774
925
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
775
926
|
}
|
|
776
927
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
777
928
|
},
|
|
778
929
|
hasSecureBiometric: (key: string) => {
|
|
779
930
|
return (
|
|
780
|
-
|
|
781
|
-
|
|
931
|
+
withWebBackendOperation(
|
|
932
|
+
StorageScope.Secure,
|
|
933
|
+
"hasSecureBiometric",
|
|
934
|
+
(backend) => backend.getItem(toBiometricStorageKey(key)),
|
|
782
935
|
) !== null
|
|
783
936
|
);
|
|
784
937
|
},
|
|
785
938
|
clearSecureBiometric: () => {
|
|
786
|
-
const
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
939
|
+
const storageKeys = withWebBackendOperation(
|
|
940
|
+
StorageScope.Secure,
|
|
941
|
+
"clearSecureBiometric:getAllKeys",
|
|
942
|
+
(backend) => backend.getAllKeys(),
|
|
943
|
+
);
|
|
944
|
+
const keysToNotify = storageKeys
|
|
945
|
+
.filter((key) => key.startsWith(BIOMETRIC_WEB_PREFIX))
|
|
946
|
+
.map((key) => fromBiometricStorageKey(key));
|
|
947
|
+
if (keysToNotify.length === 0) {
|
|
948
|
+
return;
|
|
796
949
|
}
|
|
797
|
-
|
|
950
|
+
withWebBackendOperation(
|
|
951
|
+
StorageScope.Secure,
|
|
952
|
+
"clearSecureBiometric",
|
|
953
|
+
(backend) => {
|
|
954
|
+
const biometricKeys = keysToNotify.map((key) =>
|
|
955
|
+
toBiometricStorageKey(key),
|
|
956
|
+
);
|
|
957
|
+
if (backend.removeMany) {
|
|
958
|
+
backend.removeMany(biometricKeys);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
biometricKeys.forEach((storageKey) => {
|
|
962
|
+
backend.removeItem(storageKey);
|
|
963
|
+
});
|
|
964
|
+
},
|
|
965
|
+
);
|
|
798
966
|
const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
|
|
799
967
|
keysToNotify.forEach((key) => {
|
|
800
|
-
if (
|
|
968
|
+
if (
|
|
969
|
+
withWebBackendOperation(
|
|
970
|
+
StorageScope.Secure,
|
|
971
|
+
"clearSecureBiometric:getItem",
|
|
972
|
+
(backend) => backend.getItem(toSecureStorageKey(key)),
|
|
973
|
+
) === null
|
|
974
|
+
) {
|
|
801
975
|
keyIndex.delete(key);
|
|
802
976
|
}
|
|
803
977
|
});
|
|
@@ -813,6 +987,10 @@ function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
|
813
987
|
return typeof value === "string" ? value : undefined;
|
|
814
988
|
}
|
|
815
989
|
|
|
990
|
+
if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
|
|
991
|
+
return readPendingDiskWrite(key);
|
|
992
|
+
}
|
|
993
|
+
|
|
816
994
|
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
817
995
|
return readPendingSecureWrite(key);
|
|
818
996
|
}
|
|
@@ -828,6 +1006,17 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
828
1006
|
return;
|
|
829
1007
|
}
|
|
830
1008
|
|
|
1009
|
+
if (scope === StorageScope.Disk) {
|
|
1010
|
+
cacheRawValue(scope, key, value);
|
|
1011
|
+
if (diskWritesAsync) {
|
|
1012
|
+
scheduleDiskWrite(key, value);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
flushDiskWrites();
|
|
1017
|
+
clearPendingDiskWrite(key);
|
|
1018
|
+
}
|
|
1019
|
+
|
|
831
1020
|
if (scope === StorageScope.Secure) {
|
|
832
1021
|
flushSecureWrites();
|
|
833
1022
|
clearPendingSecureWrite(key);
|
|
@@ -845,6 +1034,17 @@ function removeRawValue(key: string, scope: StorageScope): void {
|
|
|
845
1034
|
return;
|
|
846
1035
|
}
|
|
847
1036
|
|
|
1037
|
+
if (scope === StorageScope.Disk) {
|
|
1038
|
+
cacheRawValue(scope, key, undefined);
|
|
1039
|
+
if (diskWritesAsync) {
|
|
1040
|
+
scheduleDiskWrite(key, undefined);
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
flushDiskWrites();
|
|
1045
|
+
clearPendingDiskWrite(key);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
848
1048
|
if (scope === StorageScope.Secure) {
|
|
849
1049
|
flushSecureWrites();
|
|
850
1050
|
clearPendingSecureWrite(key);
|
|
@@ -877,6 +1077,11 @@ export const storage = {
|
|
|
877
1077
|
return;
|
|
878
1078
|
}
|
|
879
1079
|
|
|
1080
|
+
if (scope === StorageScope.Disk) {
|
|
1081
|
+
flushDiskWrites();
|
|
1082
|
+
pendingDiskWrites.clear();
|
|
1083
|
+
}
|
|
1084
|
+
|
|
880
1085
|
if (scope === StorageScope.Secure) {
|
|
881
1086
|
flushSecureWrites();
|
|
882
1087
|
pendingSecureWrites.clear();
|
|
@@ -912,6 +1117,9 @@ export const storage = {
|
|
|
912
1117
|
}
|
|
913
1118
|
|
|
914
1119
|
const keyPrefix = prefixKey(namespace, "");
|
|
1120
|
+
if (scope === StorageScope.Disk) {
|
|
1121
|
+
flushDiskWrites();
|
|
1122
|
+
}
|
|
915
1123
|
if (scope === StorageScope.Secure) {
|
|
916
1124
|
flushSecureWrites();
|
|
917
1125
|
}
|
|
@@ -933,6 +1141,12 @@ export const storage = {
|
|
|
933
1141
|
return measureOperation("storage:has", scope, () => {
|
|
934
1142
|
assertValidScope(scope);
|
|
935
1143
|
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
1144
|
+
if (scope === StorageScope.Disk) {
|
|
1145
|
+
flushDiskWrites();
|
|
1146
|
+
}
|
|
1147
|
+
if (scope === StorageScope.Secure) {
|
|
1148
|
+
flushSecureWrites();
|
|
1149
|
+
}
|
|
936
1150
|
return WebStorage.has(key, scope);
|
|
937
1151
|
});
|
|
938
1152
|
},
|
|
@@ -940,6 +1154,12 @@ export const storage = {
|
|
|
940
1154
|
return measureOperation("storage:getAllKeys", scope, () => {
|
|
941
1155
|
assertValidScope(scope);
|
|
942
1156
|
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
1157
|
+
if (scope === StorageScope.Disk) {
|
|
1158
|
+
flushDiskWrites();
|
|
1159
|
+
}
|
|
1160
|
+
if (scope === StorageScope.Secure) {
|
|
1161
|
+
flushSecureWrites();
|
|
1162
|
+
}
|
|
943
1163
|
return WebStorage.getAllKeys(scope);
|
|
944
1164
|
});
|
|
945
1165
|
},
|
|
@@ -951,6 +1171,12 @@ export const storage = {
|
|
|
951
1171
|
key.startsWith(prefix),
|
|
952
1172
|
);
|
|
953
1173
|
}
|
|
1174
|
+
if (scope === StorageScope.Disk) {
|
|
1175
|
+
flushDiskWrites();
|
|
1176
|
+
}
|
|
1177
|
+
if (scope === StorageScope.Secure) {
|
|
1178
|
+
flushSecureWrites();
|
|
1179
|
+
}
|
|
954
1180
|
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
955
1181
|
});
|
|
956
1182
|
},
|
|
@@ -975,6 +1201,12 @@ export const storage = {
|
|
|
975
1201
|
return result;
|
|
976
1202
|
}
|
|
977
1203
|
|
|
1204
|
+
if (scope === StorageScope.Disk) {
|
|
1205
|
+
flushDiskWrites();
|
|
1206
|
+
}
|
|
1207
|
+
if (scope === StorageScope.Secure) {
|
|
1208
|
+
flushSecureWrites();
|
|
1209
|
+
}
|
|
978
1210
|
const values = WebStorage.getBatch(keys, scope);
|
|
979
1211
|
keys.forEach((key, index) => {
|
|
980
1212
|
const value = values[index];
|
|
@@ -995,6 +1227,12 @@ export const storage = {
|
|
|
995
1227
|
});
|
|
996
1228
|
return result;
|
|
997
1229
|
}
|
|
1230
|
+
if (scope === StorageScope.Disk) {
|
|
1231
|
+
flushDiskWrites();
|
|
1232
|
+
}
|
|
1233
|
+
if (scope === StorageScope.Secure) {
|
|
1234
|
+
flushSecureWrites();
|
|
1235
|
+
}
|
|
998
1236
|
const keys = WebStorage.getAllKeys(scope);
|
|
999
1237
|
if (keys.length === 0) return {};
|
|
1000
1238
|
const values = WebStorage.getBatch(keys, scope);
|
|
@@ -1011,6 +1249,12 @@ export const storage = {
|
|
|
1011
1249
|
return measureOperation("storage:size", scope, () => {
|
|
1012
1250
|
assertValidScope(scope);
|
|
1013
1251
|
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
1252
|
+
if (scope === StorageScope.Disk) {
|
|
1253
|
+
flushDiskWrites();
|
|
1254
|
+
}
|
|
1255
|
+
if (scope === StorageScope.Secure) {
|
|
1256
|
+
flushSecureWrites();
|
|
1257
|
+
}
|
|
1014
1258
|
return WebStorage.size(scope);
|
|
1015
1259
|
});
|
|
1016
1260
|
},
|
|
@@ -1021,6 +1265,19 @@ export const storage = {
|
|
|
1021
1265
|
setSecureWritesAsync: (_enabled: boolean) => {
|
|
1022
1266
|
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
1023
1267
|
},
|
|
1268
|
+
setDiskWritesAsync: (enabled: boolean) => {
|
|
1269
|
+
measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
|
|
1270
|
+
diskWritesAsync = enabled;
|
|
1271
|
+
if (!enabled) {
|
|
1272
|
+
flushDiskWrites();
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
},
|
|
1276
|
+
flushDiskWrites: () => {
|
|
1277
|
+
measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
|
|
1278
|
+
flushDiskWrites();
|
|
1279
|
+
});
|
|
1280
|
+
},
|
|
1024
1281
|
flushSecureWrites: () => {
|
|
1025
1282
|
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
1026
1283
|
flushSecureWrites();
|
|
@@ -1048,6 +1305,84 @@ export const storage = {
|
|
|
1048
1305
|
resetMetrics: () => {
|
|
1049
1306
|
metricsCounters.clear();
|
|
1050
1307
|
},
|
|
1308
|
+
getCapabilities: (): StorageCapabilities => ({
|
|
1309
|
+
platform: "web",
|
|
1310
|
+
backend: {
|
|
1311
|
+
disk: getBackendName(StorageScope.Disk, webDiskStorageBackend),
|
|
1312
|
+
secure: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
1313
|
+
},
|
|
1314
|
+
writeBuffering: {
|
|
1315
|
+
disk: true,
|
|
1316
|
+
secure: true,
|
|
1317
|
+
},
|
|
1318
|
+
errorClassification: true,
|
|
1319
|
+
}),
|
|
1320
|
+
getSecurityCapabilities: (): SecurityCapabilities => {
|
|
1321
|
+
const secureBackend = getBackendName(
|
|
1322
|
+
StorageScope.Secure,
|
|
1323
|
+
webSecureStorageBackend,
|
|
1324
|
+
);
|
|
1325
|
+
return {
|
|
1326
|
+
platform: "web",
|
|
1327
|
+
secureStorage: {
|
|
1328
|
+
backend: secureBackend,
|
|
1329
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
1330
|
+
accessControl: "unavailable",
|
|
1331
|
+
keychainAccessGroup: "unavailable",
|
|
1332
|
+
hardwareBacked: "unavailable",
|
|
1333
|
+
},
|
|
1334
|
+
biometric: {
|
|
1335
|
+
storage: "unavailable",
|
|
1336
|
+
prompt: "unavailable",
|
|
1337
|
+
biometryOnly: "unavailable",
|
|
1338
|
+
biometryOrPasscode: "unavailable",
|
|
1339
|
+
},
|
|
1340
|
+
metadata: {
|
|
1341
|
+
perKey: true,
|
|
1342
|
+
listsWithoutValues: true,
|
|
1343
|
+
persistsTimestamps: false,
|
|
1344
|
+
},
|
|
1345
|
+
};
|
|
1346
|
+
},
|
|
1347
|
+
getSecureMetadata: (key: string): SecureStorageMetadata => {
|
|
1348
|
+
return measureOperation(
|
|
1349
|
+
"storage:getSecureMetadata",
|
|
1350
|
+
StorageScope.Secure,
|
|
1351
|
+
() => {
|
|
1352
|
+
flushSecureWrites();
|
|
1353
|
+
const biometricProtected = WebStorage.hasSecureBiometric(key);
|
|
1354
|
+
const exists =
|
|
1355
|
+
biometricProtected || WebStorage.has(key, StorageScope.Secure);
|
|
1356
|
+
let kind: SecureStorageMetadata["kind"] = "missing";
|
|
1357
|
+
if (exists) {
|
|
1358
|
+
kind = biometricProtected ? "biometric" : "secure";
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
return {
|
|
1362
|
+
key,
|
|
1363
|
+
exists,
|
|
1364
|
+
kind,
|
|
1365
|
+
backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
1366
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
1367
|
+
hardwareBacked: "unavailable",
|
|
1368
|
+
biometricProtected,
|
|
1369
|
+
valueExposed: false,
|
|
1370
|
+
};
|
|
1371
|
+
},
|
|
1372
|
+
);
|
|
1373
|
+
},
|
|
1374
|
+
getAllSecureMetadata: (): SecureStorageMetadata[] => {
|
|
1375
|
+
return measureOperation(
|
|
1376
|
+
"storage:getAllSecureMetadata",
|
|
1377
|
+
StorageScope.Secure,
|
|
1378
|
+
() => {
|
|
1379
|
+
flushSecureWrites();
|
|
1380
|
+
return WebStorage.getAllKeys(StorageScope.Secure).map((key) =>
|
|
1381
|
+
storage.getSecureMetadata(key),
|
|
1382
|
+
);
|
|
1383
|
+
},
|
|
1384
|
+
);
|
|
1385
|
+
},
|
|
1051
1386
|
getString: (key: string, scope: StorageScope): string | undefined => {
|
|
1052
1387
|
return measureOperation("storage:getString", scope, () => {
|
|
1053
1388
|
return getRawValue(key, scope);
|
|
@@ -1085,6 +1420,9 @@ export const storage = {
|
|
|
1085
1420
|
flushSecureWrites();
|
|
1086
1421
|
WebStorage.setSecureAccessControl(secureDefaultAccessControl);
|
|
1087
1422
|
}
|
|
1423
|
+
if (scope === StorageScope.Disk) {
|
|
1424
|
+
flushDiskWrites();
|
|
1425
|
+
}
|
|
1088
1426
|
|
|
1089
1427
|
WebStorage.setBatch(keys, values, scope);
|
|
1090
1428
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
@@ -1097,11 +1435,12 @@ export const storage = {
|
|
|
1097
1435
|
export function setWebSecureStorageBackend(
|
|
1098
1436
|
backend?: WebSecureStorageBackend,
|
|
1099
1437
|
): void {
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1438
|
+
pendingSecureWrites.clear();
|
|
1439
|
+
webSecureStorageBackend = backend ?? createDefaultSecureBackend();
|
|
1440
|
+
resetBackendChangeSubscription(StorageScope.Secure);
|
|
1103
1441
|
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
1104
1442
|
clearScopeRawCache(StorageScope.Secure);
|
|
1443
|
+
ensureExternalSyncSubscriptions();
|
|
1105
1444
|
}
|
|
1106
1445
|
|
|
1107
1446
|
export function getWebSecureStorageBackend():
|
|
@@ -1110,6 +1449,39 @@ export function getWebSecureStorageBackend():
|
|
|
1110
1449
|
return webSecureStorageBackend;
|
|
1111
1450
|
}
|
|
1112
1451
|
|
|
1452
|
+
export function setWebDiskStorageBackend(
|
|
1453
|
+
backend?: WebDiskStorageBackend,
|
|
1454
|
+
): void {
|
|
1455
|
+
pendingDiskWrites.clear();
|
|
1456
|
+
webDiskStorageBackend = backend ?? createDefaultDiskBackend();
|
|
1457
|
+
resetBackendChangeSubscription(StorageScope.Disk);
|
|
1458
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
|
|
1459
|
+
clearScopeRawCache(StorageScope.Disk);
|
|
1460
|
+
ensureExternalSyncSubscriptions();
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
|
|
1464
|
+
return webDiskStorageBackend;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
export async function flushWebStorageBackends(): Promise<void> {
|
|
1468
|
+
flushDiskWrites();
|
|
1469
|
+
flushSecureWrites();
|
|
1470
|
+
|
|
1471
|
+
const flushes: Promise<void>[] = [];
|
|
1472
|
+
const diskFlush = webDiskStorageBackend?.flush;
|
|
1473
|
+
const secureFlush = webSecureStorageBackend?.flush;
|
|
1474
|
+
|
|
1475
|
+
if (diskFlush) {
|
|
1476
|
+
flushes.push(diskFlush());
|
|
1477
|
+
}
|
|
1478
|
+
if (secureFlush) {
|
|
1479
|
+
flushes.push(secureFlush());
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
await Promise.all(flushes);
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1113
1485
|
export interface StorageItemConfig<T> {
|
|
1114
1486
|
key: string;
|
|
1115
1487
|
scope: StorageScope;
|
|
@@ -1121,6 +1493,7 @@ export interface StorageItemConfig<T> {
|
|
|
1121
1493
|
expiration?: ExpirationConfig;
|
|
1122
1494
|
onExpired?: (key: string) => void;
|
|
1123
1495
|
readCache?: boolean;
|
|
1496
|
+
coalesceDiskWrites?: boolean;
|
|
1124
1497
|
coalesceSecureWrites?: boolean;
|
|
1125
1498
|
namespace?: string;
|
|
1126
1499
|
biometric?: boolean;
|
|
@@ -1205,6 +1578,8 @@ export function createStorageItem<T = undefined>(
|
|
|
1205
1578
|
const memoryExpiration =
|
|
1206
1579
|
expiration && isMemory ? new Map<string, number>() : null;
|
|
1207
1580
|
const readCache = !isMemory && config.readCache === true;
|
|
1581
|
+
const coalesceDiskWrites =
|
|
1582
|
+
config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
1208
1583
|
const coalesceSecureWrites =
|
|
1209
1584
|
config.scope === StorageScope.Secure &&
|
|
1210
1585
|
config.coalesceSecureWrites === true &&
|
|
@@ -1250,7 +1625,7 @@ export function createStorageItem<T = undefined>(
|
|
|
1250
1625
|
return;
|
|
1251
1626
|
}
|
|
1252
1627
|
|
|
1253
|
-
|
|
1628
|
+
ensureExternalSyncSubscriptions();
|
|
1254
1629
|
unsubscribe = addKeyListener(
|
|
1255
1630
|
getScopedListeners(nonMemoryScope!),
|
|
1256
1631
|
storageKey,
|
|
@@ -1273,6 +1648,13 @@ export function createStorageItem<T = undefined>(
|
|
|
1273
1648
|
return memoryStore.get(storageKey);
|
|
1274
1649
|
}
|
|
1275
1650
|
|
|
1651
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1652
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1653
|
+
if (pending !== undefined) {
|
|
1654
|
+
return pending.value;
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1276
1658
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1277
1659
|
const pending = pendingSecureWrites.get(storageKey);
|
|
1278
1660
|
if (pending !== undefined) {
|
|
@@ -1309,6 +1691,15 @@ export function createStorageItem<T = undefined>(
|
|
|
1309
1691
|
|
|
1310
1692
|
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
1311
1693
|
|
|
1694
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1695
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1696
|
+
scheduleDiskWrite(storageKey, rawValue);
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
clearPendingDiskWrite(storageKey);
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1312
1703
|
if (coalesceSecureWrites) {
|
|
1313
1704
|
scheduleSecureWrite(
|
|
1314
1705
|
storageKey,
|
|
@@ -1333,6 +1724,15 @@ export function createStorageItem<T = undefined>(
|
|
|
1333
1724
|
|
|
1334
1725
|
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
1335
1726
|
|
|
1727
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1728
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1729
|
+
scheduleDiskWrite(storageKey, undefined);
|
|
1730
|
+
return;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
clearPendingDiskWrite(storageKey);
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1336
1736
|
if (coalesceSecureWrites) {
|
|
1337
1737
|
scheduleSecureWrite(
|
|
1338
1738
|
storageKey,
|
|
@@ -1543,6 +1943,18 @@ export function createStorageItem<T = undefined>(
|
|
|
1543
1943
|
measureOperation("item:has", config.scope, () => {
|
|
1544
1944
|
if (isMemory) return memoryStore.has(storageKey);
|
|
1545
1945
|
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
1946
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1947
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1948
|
+
if (pending !== undefined) {
|
|
1949
|
+
return pending.value !== undefined;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1953
|
+
const pending = pendingSecureWrites.get(storageKey);
|
|
1954
|
+
if (pending !== undefined) {
|
|
1955
|
+
return pending.value !== undefined;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1546
1958
|
return WebStorage.has(storageKey, config.scope);
|
|
1547
1959
|
});
|
|
1548
1960
|
|
|
@@ -1554,9 +1966,6 @@ export function createStorageItem<T = undefined>(
|
|
|
1554
1966
|
if (listeners.size === 0 && unsubscribe) {
|
|
1555
1967
|
unsubscribe();
|
|
1556
1968
|
unsubscribe = null;
|
|
1557
|
-
if (!isMemory) {
|
|
1558
|
-
maybeCleanupWebStorageSubscription();
|
|
1559
|
-
}
|
|
1560
1969
|
}
|
|
1561
1970
|
};
|
|
1562
1971
|
};
|
|
@@ -1642,6 +2051,14 @@ export function getBatch(
|
|
|
1642
2051
|
const keyIndexes: number[] = [];
|
|
1643
2052
|
|
|
1644
2053
|
items.forEach((item, index) => {
|
|
2054
|
+
if (scope === StorageScope.Disk) {
|
|
2055
|
+
const pending = pendingDiskWrites.get(item.key);
|
|
2056
|
+
if (pending !== undefined) {
|
|
2057
|
+
rawValues[index] = pending.value;
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
|
|
1645
2062
|
if (scope === StorageScope.Secure) {
|
|
1646
2063
|
const pending = pendingSecureWrites.get(item.key);
|
|
1647
2064
|
if (pending !== undefined) {
|
|
@@ -1766,6 +2183,8 @@ export function setBatch<T>(
|
|
|
1766
2183
|
return;
|
|
1767
2184
|
}
|
|
1768
2185
|
|
|
2186
|
+
flushDiskWrites();
|
|
2187
|
+
|
|
1769
2188
|
const useRawBatchPath = items.every(({ item }) =>
|
|
1770
2189
|
canUseRawBatchPath(asInternal(item)),
|
|
1771
2190
|
);
|
|
@@ -1799,6 +2218,9 @@ export function removeBatch(
|
|
|
1799
2218
|
}
|
|
1800
2219
|
|
|
1801
2220
|
const keys = items.map((item) => item.key);
|
|
2221
|
+
if (scope === StorageScope.Disk) {
|
|
2222
|
+
flushDiskWrites();
|
|
2223
|
+
}
|
|
1802
2224
|
if (scope === StorageScope.Secure) {
|
|
1803
2225
|
flushSecureWrites();
|
|
1804
2226
|
}
|
|
@@ -1862,6 +2284,9 @@ export function runTransaction<T>(
|
|
|
1862
2284
|
): T {
|
|
1863
2285
|
return measureOperation("transaction:run", scope, () => {
|
|
1864
2286
|
assertValidScope(scope);
|
|
2287
|
+
if (scope === StorageScope.Disk) {
|
|
2288
|
+
flushDiskWrites();
|
|
2289
|
+
}
|
|
1865
2290
|
if (scope === StorageScope.Secure) {
|
|
1866
2291
|
flushSecureWrites();
|
|
1867
2292
|
}
|
|
@@ -1937,6 +2362,9 @@ export function runTransaction<T>(
|
|
|
1937
2362
|
}
|
|
1938
2363
|
});
|
|
1939
2364
|
|
|
2365
|
+
if (scope === StorageScope.Disk) {
|
|
2366
|
+
flushDiskWrites();
|
|
2367
|
+
}
|
|
1940
2368
|
if (scope === StorageScope.Secure) {
|
|
1941
2369
|
flushSecureWrites();
|
|
1942
2370
|
}
|
|
@@ -2000,6 +2428,6 @@ export function createSecureAuthStorage<K extends string>(
|
|
|
2000
2428
|
return result as Record<K, StorageItem<string>>;
|
|
2001
2429
|
}
|
|
2002
2430
|
|
|
2003
|
-
export function isKeychainLockedError(
|
|
2004
|
-
return
|
|
2431
|
+
export function isKeychainLockedError(err: unknown): boolean {
|
|
2432
|
+
return isLockedStorageErrorCode(getStorageErrorCode(err));
|
|
2005
2433
|
}
|