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/lib/module/index.web.js
CHANGED
|
@@ -2,8 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
4
4
|
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
|
|
5
|
+
import { createLocalStorageWebBackend } from "./web-storage-backend";
|
|
6
|
+
import { getStorageErrorCode, isLockedStorageErrorCode } from "./storage-runtime";
|
|
5
7
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
6
8
|
export { migrateFromMMKV } from "./migration";
|
|
9
|
+
export { getStorageErrorCode } from "./storage-runtime";
|
|
7
10
|
function asInternal(item) {
|
|
8
11
|
return item;
|
|
9
12
|
}
|
|
@@ -17,20 +20,23 @@ const registeredMigrations = new Map();
|
|
|
17
20
|
const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
|
|
18
21
|
Promise.resolve().then(task);
|
|
19
22
|
};
|
|
23
|
+
const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
|
|
20
24
|
const memoryStore = new Map();
|
|
21
25
|
const memoryListeners = new Map();
|
|
22
26
|
const webScopeListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
23
27
|
const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
24
28
|
const webScopeKeyIndex = new Map([[StorageScope.Disk, new Set()], [StorageScope.Secure, new Set()]]);
|
|
25
29
|
const hydratedWebScopeKeyIndex = new Set();
|
|
30
|
+
const pendingDiskWrites = new Map();
|
|
31
|
+
let diskFlushScheduled = false;
|
|
32
|
+
let diskWritesAsync = false;
|
|
26
33
|
const pendingSecureWrites = new Map();
|
|
27
34
|
let secureFlushScheduled = false;
|
|
28
35
|
let secureDefaultAccessControl = AccessControl.WhenUnlocked;
|
|
29
36
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
30
37
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
31
38
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
32
|
-
let
|
|
33
|
-
let webStorageSubscriberCount = 0;
|
|
39
|
+
let hasWindowStorageEventSubscription = false;
|
|
34
40
|
let metricsObserver;
|
|
35
41
|
const metricsCounters = new Map();
|
|
36
42
|
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
@@ -57,61 +63,54 @@ function measureOperation(operation, scope, fn, keysCount = 1) {
|
|
|
57
63
|
if (!metricsObserver) {
|
|
58
64
|
return fn();
|
|
59
65
|
}
|
|
60
|
-
const start =
|
|
66
|
+
const start = now();
|
|
61
67
|
try {
|
|
62
68
|
return fn();
|
|
63
69
|
} finally {
|
|
64
|
-
recordMetric(operation, scope,
|
|
70
|
+
recordMetric(operation, scope, now() - start, keysCount);
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
|
-
function
|
|
68
|
-
return {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
clear: () => globalThis.localStorage?.clear(),
|
|
73
|
-
getAllKeys: () => {
|
|
74
|
-
const storage = globalThis.localStorage;
|
|
75
|
-
if (!storage) return [];
|
|
76
|
-
const keys = [];
|
|
77
|
-
for (let index = 0; index < storage.length; index += 1) {
|
|
78
|
-
const key = storage.key(index);
|
|
79
|
-
if (key) {
|
|
80
|
-
keys.push(key);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
return keys;
|
|
84
|
-
}
|
|
85
|
-
};
|
|
73
|
+
function createDefaultDiskBackend() {
|
|
74
|
+
return createLocalStorageWebBackend({
|
|
75
|
+
name: "localStorage:disk",
|
|
76
|
+
includeKey: key => !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
77
|
+
});
|
|
86
78
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
79
|
+
function createDefaultSecureBackend() {
|
|
80
|
+
return createLocalStorageWebBackend({
|
|
81
|
+
name: "localStorage:secure",
|
|
82
|
+
includeKey: key => key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
let webDiskStorageBackend = createDefaultDiskBackend();
|
|
86
|
+
let webSecureStorageBackend = createDefaultSecureBackend();
|
|
87
|
+
const externalSyncUnsubscribers = new Map();
|
|
88
|
+
function getBackendName(scope, backend) {
|
|
89
|
+
const scopeName = scope === StorageScope.Disk ? "disk" : "secure";
|
|
90
|
+
return backend?.name ?? `web:${scopeName}`;
|
|
91
|
+
}
|
|
92
|
+
function getWebSecureEncryptionStatus(backend) {
|
|
93
|
+
return backend?.name === "localStorage:secure" ? "unavailable" : "unknown";
|
|
94
|
+
}
|
|
95
|
+
function createWebStorageError(scope, operation, error, backend) {
|
|
96
|
+
const backendName = getBackendName(scope, backend);
|
|
97
|
+
const message = error instanceof Error ? error.message : String(error ?? "Unknown error");
|
|
98
|
+
return new Error(`NitroStorage(web): ${operation} failed for ${backendName}: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
function withWebBackendOperation(scope, operation, fn) {
|
|
101
|
+
const backend = scope === StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
|
|
102
|
+
if (!backend) {
|
|
103
|
+
throw new Error(`NitroStorage(web): ${operation} failed because no ${scope === StorageScope.Disk ? "disk" : "secure"} backend is configured.`);
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
return cachedSecureBrowserStorage;
|
|
100
|
-
}
|
|
101
|
-
cachedSecureBackendRef = webSecureStorageBackend;
|
|
102
|
-
cachedSecureBrowserStorage = {
|
|
103
|
-
setItem: (key, value) => webSecureStorageBackend.setItem(key, value),
|
|
104
|
-
getItem: key => webSecureStorageBackend.getItem(key) ?? null,
|
|
105
|
-
removeItem: key => webSecureStorageBackend.removeItem(key),
|
|
106
|
-
clear: () => webSecureStorageBackend.clear(),
|
|
107
|
-
key: index => webSecureStorageBackend.getAllKeys()[index] ?? null,
|
|
108
|
-
get length() {
|
|
109
|
-
return webSecureStorageBackend.getAllKeys().length;
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
return cachedSecureBrowserStorage;
|
|
105
|
+
try {
|
|
106
|
+
ensureExternalSyncSubscriptions();
|
|
107
|
+
return fn(backend);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
throw createWebStorageError(scope, operation, error, backend);
|
|
113
110
|
}
|
|
114
|
-
|
|
111
|
+
}
|
|
112
|
+
function getWebBackend(scope) {
|
|
113
|
+
return scope === StorageScope.Disk ? webDiskStorageBackend : webSecureStorageBackend;
|
|
115
114
|
}
|
|
116
115
|
function toSecureStorageKey(key) {
|
|
117
116
|
return `${SECURE_WEB_PREFIX}${key}`;
|
|
@@ -132,28 +131,21 @@ function hydrateWebScopeKeyIndex(scope) {
|
|
|
132
131
|
if (hydratedWebScopeKeyIndex.has(scope)) {
|
|
133
132
|
return;
|
|
134
133
|
}
|
|
135
|
-
const
|
|
134
|
+
const backend = getWebBackend(scope);
|
|
136
135
|
const keyIndex = getWebScopeKeyIndex(scope);
|
|
137
136
|
keyIndex.clear();
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
151
|
-
keyIndex.add(fromSecureStorageKey(key));
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
155
|
-
keyIndex.add(fromBiometricStorageKey(key));
|
|
156
|
-
}
|
|
137
|
+
const keys = backend?.getAllKeys() ?? [];
|
|
138
|
+
for (const key of keys) {
|
|
139
|
+
if (scope === StorageScope.Disk) {
|
|
140
|
+
keyIndex.add(key);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
144
|
+
keyIndex.add(fromSecureStorageKey(key));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
148
|
+
keyIndex.add(fromBiometricStorageKey(key));
|
|
157
149
|
}
|
|
158
150
|
}
|
|
159
151
|
hydratedWebScopeKeyIndex.add(scope);
|
|
@@ -162,65 +154,85 @@ function ensureWebScopeKeyIndex(scope) {
|
|
|
162
154
|
hydrateWebScopeKeyIndex(scope);
|
|
163
155
|
return getWebScopeKeyIndex(scope);
|
|
164
156
|
}
|
|
165
|
-
function
|
|
166
|
-
const key = event.key;
|
|
157
|
+
function applyExternalChangeEvent(scope, key, newValue) {
|
|
167
158
|
if (key === null) {
|
|
168
|
-
clearScopeRawCache(
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
ensureWebScopeKeyIndex(StorageScope.Secure).clear();
|
|
172
|
-
notifyAllListeners(getScopedListeners(StorageScope.Disk));
|
|
173
|
-
notifyAllListeners(getScopedListeners(StorageScope.Secure));
|
|
159
|
+
clearScopeRawCache(scope);
|
|
160
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
161
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
174
162
|
return;
|
|
175
163
|
}
|
|
176
|
-
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
164
|
+
if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
|
|
177
165
|
const plainKey = fromSecureStorageKey(key);
|
|
178
|
-
if (
|
|
166
|
+
if (newValue === null) {
|
|
179
167
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
180
168
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
181
169
|
} else {
|
|
182
170
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
183
|
-
cacheRawValue(StorageScope.Secure, plainKey,
|
|
171
|
+
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
184
172
|
}
|
|
185
173
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
186
174
|
return;
|
|
187
175
|
}
|
|
188
|
-
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
176
|
+
if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
189
177
|
const plainKey = fromBiometricStorageKey(key);
|
|
190
|
-
if (
|
|
191
|
-
if (
|
|
178
|
+
if (newValue === null) {
|
|
179
|
+
if (withWebBackendOperation(StorageScope.Secure, "external-sync:getItem", backend => backend.getItem(toSecureStorageKey(plainKey))) === null) {
|
|
192
180
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
193
181
|
}
|
|
194
182
|
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
195
183
|
} else {
|
|
196
184
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
197
|
-
cacheRawValue(StorageScope.Secure, plainKey,
|
|
185
|
+
cacheRawValue(StorageScope.Secure, plainKey, newValue);
|
|
198
186
|
}
|
|
199
187
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
200
188
|
return;
|
|
201
189
|
}
|
|
202
|
-
if (
|
|
203
|
-
ensureWebScopeKeyIndex(
|
|
204
|
-
cacheRawValue(
|
|
190
|
+
if (newValue === null) {
|
|
191
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
192
|
+
cacheRawValue(scope, key, undefined);
|
|
205
193
|
} else {
|
|
206
|
-
ensureWebScopeKeyIndex(
|
|
207
|
-
cacheRawValue(
|
|
194
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
195
|
+
cacheRawValue(scope, key, newValue);
|
|
208
196
|
}
|
|
209
|
-
notifyKeyListeners(getScopedListeners(
|
|
197
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
210
198
|
}
|
|
211
|
-
function
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
|
|
215
|
-
|
|
199
|
+
function handleWebStorageEvent(event) {
|
|
200
|
+
const key = event.key;
|
|
201
|
+
if (key === null) {
|
|
202
|
+
applyExternalChangeEvent(StorageScope.Disk, null, null);
|
|
203
|
+
applyExternalChangeEvent(StorageScope.Secure, null, null);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (key.startsWith(SECURE_WEB_PREFIX) || key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
207
|
+
applyExternalChangeEvent(StorageScope.Secure, key, event.newValue);
|
|
208
|
+
return;
|
|
216
209
|
}
|
|
210
|
+
applyExternalChangeEvent(StorageScope.Disk, key, event.newValue);
|
|
211
|
+
}
|
|
212
|
+
function subscribeToBackendChanges(scope) {
|
|
213
|
+
if (externalSyncUnsubscribers.has(scope)) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const backend = getWebBackend(scope);
|
|
217
|
+
if (!backend?.subscribe) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const unsubscribe = backend.subscribe(event => {
|
|
221
|
+
applyExternalChangeEvent(scope, event.key, event.newValue);
|
|
222
|
+
});
|
|
223
|
+
externalSyncUnsubscribers.set(scope, unsubscribe);
|
|
224
|
+
}
|
|
225
|
+
function resetBackendChangeSubscription(scope) {
|
|
226
|
+
externalSyncUnsubscribers.get(scope)?.();
|
|
227
|
+
externalSyncUnsubscribers.delete(scope);
|
|
217
228
|
}
|
|
218
|
-
function
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
hasWebStorageEventSubscription = false;
|
|
229
|
+
function ensureExternalSyncSubscriptions() {
|
|
230
|
+
if (!hasWindowStorageEventSubscription && typeof window !== "undefined" && typeof window.addEventListener === "function") {
|
|
231
|
+
window.addEventListener("storage", handleWebStorageEvent);
|
|
232
|
+
hasWindowStorageEventSubscription = true;
|
|
223
233
|
}
|
|
234
|
+
subscribeToBackendChanges(StorageScope.Disk);
|
|
235
|
+
subscribeToBackendChanges(StorageScope.Secure);
|
|
224
236
|
}
|
|
225
237
|
function getScopedListeners(scope) {
|
|
226
238
|
return webScopeListeners.get(scope);
|
|
@@ -276,12 +288,49 @@ function addKeyListener(registry, key, listener) {
|
|
|
276
288
|
function readPendingSecureWrite(key) {
|
|
277
289
|
return pendingSecureWrites.get(key)?.value;
|
|
278
290
|
}
|
|
291
|
+
function readPendingDiskWrite(key) {
|
|
292
|
+
return pendingDiskWrites.get(key)?.value;
|
|
293
|
+
}
|
|
294
|
+
function hasPendingDiskWrite(key) {
|
|
295
|
+
return pendingDiskWrites.has(key);
|
|
296
|
+
}
|
|
279
297
|
function hasPendingSecureWrite(key) {
|
|
280
298
|
return pendingSecureWrites.has(key);
|
|
281
299
|
}
|
|
300
|
+
function clearPendingDiskWrite(key) {
|
|
301
|
+
pendingDiskWrites.delete(key);
|
|
302
|
+
}
|
|
282
303
|
function clearPendingSecureWrite(key) {
|
|
283
304
|
pendingSecureWrites.delete(key);
|
|
284
305
|
}
|
|
306
|
+
function flushDiskWrites() {
|
|
307
|
+
diskFlushScheduled = false;
|
|
308
|
+
if (pendingDiskWrites.size === 0) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const writes = Array.from(pendingDiskWrites.values());
|
|
312
|
+
pendingDiskWrites.clear();
|
|
313
|
+
const keysToSet = [];
|
|
314
|
+
const valuesToSet = [];
|
|
315
|
+
const keysToRemove = [];
|
|
316
|
+
writes.forEach(({
|
|
317
|
+
key,
|
|
318
|
+
value
|
|
319
|
+
}) => {
|
|
320
|
+
if (value === undefined) {
|
|
321
|
+
keysToRemove.push(key);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
keysToSet.push(key);
|
|
325
|
+
valuesToSet.push(value);
|
|
326
|
+
});
|
|
327
|
+
if (keysToSet.length > 0) {
|
|
328
|
+
WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
329
|
+
}
|
|
330
|
+
if (keysToRemove.length > 0) {
|
|
331
|
+
WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
285
334
|
function flushSecureWrites() {
|
|
286
335
|
secureFlushScheduled = false;
|
|
287
336
|
if (pendingSecureWrites.size === 0) {
|
|
@@ -320,6 +369,17 @@ function flushSecureWrites() {
|
|
|
320
369
|
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
321
370
|
}
|
|
322
371
|
}
|
|
372
|
+
function scheduleDiskWrite(key, value) {
|
|
373
|
+
pendingDiskWrites.set(key, {
|
|
374
|
+
key,
|
|
375
|
+
value
|
|
376
|
+
});
|
|
377
|
+
if (diskFlushScheduled) {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
diskFlushScheduled = true;
|
|
381
|
+
runMicrotask(flushDiskWrites);
|
|
382
|
+
}
|
|
323
383
|
function scheduleSecureWrite(key, value, accessControl) {
|
|
324
384
|
const pendingWrite = {
|
|
325
385
|
key,
|
|
@@ -340,117 +400,124 @@ const WebStorage = {
|
|
|
340
400
|
equals: other => other === WebStorage,
|
|
341
401
|
dispose: () => {},
|
|
342
402
|
set: (key, value, scope) => {
|
|
343
|
-
|
|
344
|
-
if (!storage) {
|
|
403
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
345
404
|
return;
|
|
346
405
|
}
|
|
347
406
|
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
407
|
+
withWebBackendOperation(scope, "set", backend => {
|
|
408
|
+
backend.setItem(storageKey, value);
|
|
409
|
+
});
|
|
410
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
411
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
353
412
|
},
|
|
354
413
|
get: (key, scope) => {
|
|
355
|
-
|
|
414
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
415
|
+
return undefined;
|
|
416
|
+
}
|
|
356
417
|
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
357
|
-
|
|
418
|
+
const value = withWebBackendOperation(scope, "get", backend => backend.getItem(storageKey));
|
|
419
|
+
return value ?? undefined;
|
|
358
420
|
},
|
|
359
421
|
remove: (key, scope) => {
|
|
360
|
-
|
|
361
|
-
if (!storage) {
|
|
422
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
362
423
|
return;
|
|
363
424
|
}
|
|
364
425
|
if (scope === StorageScope.Secure) {
|
|
365
|
-
|
|
366
|
-
|
|
426
|
+
withWebBackendOperation(scope, "remove", backend => {
|
|
427
|
+
if (backend.removeMany) {
|
|
428
|
+
backend.removeMany([toSecureStorageKey(key), toBiometricStorageKey(key)]);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
backend.removeItem(toSecureStorageKey(key));
|
|
432
|
+
backend.removeItem(toBiometricStorageKey(key));
|
|
433
|
+
});
|
|
367
434
|
} else {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
ensureWebScopeKeyIndex(scope).delete(key);
|
|
372
|
-
notifyKeyListeners(getScopedListeners(scope), key);
|
|
435
|
+
withWebBackendOperation(scope, "remove", backend => {
|
|
436
|
+
backend.removeItem(key);
|
|
437
|
+
});
|
|
373
438
|
}
|
|
439
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
440
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
374
441
|
},
|
|
375
442
|
clear: scope => {
|
|
376
|
-
|
|
377
|
-
if (!storage) {
|
|
443
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
378
444
|
return;
|
|
379
445
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
keysToRemove.push(key);
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
keysToRemove.forEach(key => storage.removeItem(key));
|
|
389
|
-
} else if (scope === StorageScope.Disk) {
|
|
390
|
-
const keysToRemove = [];
|
|
391
|
-
for (let i = 0; i < storage.length; i++) {
|
|
392
|
-
const key = storage.key(i);
|
|
393
|
-
if (key && !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
394
|
-
keysToRemove.push(key);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
keysToRemove.forEach(key => storage.removeItem(key));
|
|
398
|
-
} else {
|
|
399
|
-
storage.clear();
|
|
400
|
-
}
|
|
401
|
-
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
402
|
-
ensureWebScopeKeyIndex(scope).clear();
|
|
403
|
-
notifyAllListeners(getScopedListeners(scope));
|
|
404
|
-
}
|
|
446
|
+
withWebBackendOperation(scope, "clear", backend => {
|
|
447
|
+
backend.clear();
|
|
448
|
+
});
|
|
449
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
450
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
405
451
|
},
|
|
406
452
|
setBatch: (keys, values, scope) => {
|
|
407
|
-
|
|
408
|
-
if (!storage) {
|
|
453
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
409
454
|
return;
|
|
410
455
|
}
|
|
456
|
+
const entries = [];
|
|
411
457
|
keys.forEach((key, index) => {
|
|
412
458
|
const value = values[index];
|
|
413
459
|
if (value === undefined) {
|
|
414
460
|
return;
|
|
415
461
|
}
|
|
416
|
-
|
|
417
|
-
storage.setItem(storageKey, value);
|
|
462
|
+
entries.push([scope === StorageScope.Secure ? toSecureStorageKey(key) : key, value]);
|
|
418
463
|
});
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
464
|
+
withWebBackendOperation(scope, "setBatch", backend => {
|
|
465
|
+
if (backend.setMany) {
|
|
466
|
+
backend.setMany(entries);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
entries.forEach(([storageKey, value]) => {
|
|
470
|
+
backend.setItem(storageKey, value);
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
474
|
+
keys.forEach(key => keyIndex.add(key));
|
|
475
|
+
const listeners = getScopedListeners(scope);
|
|
476
|
+
keys.forEach(key => notifyKeyListeners(listeners, key));
|
|
425
477
|
},
|
|
426
478
|
getBatch: (keys, scope) => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
479
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
480
|
+
return keys.map(() => undefined);
|
|
481
|
+
}
|
|
482
|
+
const storageKeys = keys.map(key => scope === StorageScope.Secure ? toSecureStorageKey(key) : key);
|
|
483
|
+
const values = withWebBackendOperation(scope, "getBatch", backend => {
|
|
484
|
+
if (backend.getMany) {
|
|
485
|
+
return backend.getMany(storageKeys);
|
|
486
|
+
}
|
|
487
|
+
return storageKeys.map(storageKey => backend.getItem(storageKey));
|
|
431
488
|
});
|
|
489
|
+
return values.map(value => value ?? undefined);
|
|
432
490
|
},
|
|
433
491
|
removeBatch: (keys, scope) => {
|
|
434
|
-
|
|
435
|
-
if (!storage) {
|
|
492
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
436
493
|
return;
|
|
437
494
|
}
|
|
438
495
|
if (scope === StorageScope.Secure) {
|
|
439
|
-
keys.
|
|
440
|
-
|
|
441
|
-
|
|
496
|
+
const storageKeys = keys.flatMap(key => [toSecureStorageKey(key), toBiometricStorageKey(key)]);
|
|
497
|
+
withWebBackendOperation(scope, "removeBatch", backend => {
|
|
498
|
+
if (backend.removeMany) {
|
|
499
|
+
backend.removeMany(storageKeys);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
storageKeys.forEach(storageKey => {
|
|
503
|
+
backend.removeItem(storageKey);
|
|
504
|
+
});
|
|
442
505
|
});
|
|
443
506
|
} else {
|
|
444
|
-
|
|
445
|
-
|
|
507
|
+
withWebBackendOperation(scope, "removeBatch", backend => {
|
|
508
|
+
if (backend.removeMany) {
|
|
509
|
+
backend.removeMany(keys);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
keys.forEach(key => {
|
|
513
|
+
backend.removeItem(key);
|
|
514
|
+
});
|
|
446
515
|
});
|
|
447
516
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
keys.forEach(key => notifyKeyListeners(listeners, key));
|
|
453
|
-
}
|
|
517
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
518
|
+
keys.forEach(key => keyIndex.delete(key));
|
|
519
|
+
const listeners = getScopedListeners(scope);
|
|
520
|
+
keys.forEach(key => notifyKeyListeners(listeners, key));
|
|
454
521
|
},
|
|
455
522
|
removeByPrefix: (prefix, scope) => {
|
|
456
523
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -467,11 +534,13 @@ const WebStorage = {
|
|
|
467
534
|
return () => {};
|
|
468
535
|
},
|
|
469
536
|
has: (key, scope) => {
|
|
470
|
-
const storage = getBrowserStorage(scope);
|
|
471
537
|
if (scope === StorageScope.Secure) {
|
|
472
|
-
return
|
|
538
|
+
return withWebBackendOperation(scope, "has", backend => backend.getItem(toSecureStorageKey(key))) !== null || withWebBackendOperation(scope, "has", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
|
|
473
539
|
}
|
|
474
|
-
|
|
540
|
+
if (scope !== StorageScope.Disk) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
543
|
+
return withWebBackendOperation(scope, "has", backend => backend.getItem(key)) !== null;
|
|
475
544
|
},
|
|
476
545
|
getAllKeys: scope => {
|
|
477
546
|
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
@@ -502,40 +571,43 @@ const WebStorage = {
|
|
|
502
571
|
hasWarnedAboutWebBiometricFallback = true;
|
|
503
572
|
console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
|
|
504
573
|
}
|
|
505
|
-
|
|
574
|
+
withWebBackendOperation(StorageScope.Secure, "setSecureBiometric", backend => backend.setItem(toBiometricStorageKey(key), value));
|
|
506
575
|
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
507
576
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
508
577
|
},
|
|
509
578
|
getSecureBiometric: key => {
|
|
510
|
-
|
|
579
|
+
const value = withWebBackendOperation(StorageScope.Secure, "getSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key)));
|
|
580
|
+
return value ?? undefined;
|
|
511
581
|
},
|
|
512
582
|
deleteSecureBiometric: key => {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
583
|
+
withWebBackendOperation(StorageScope.Secure, "deleteSecureBiometric", backend => backend.removeItem(toBiometricStorageKey(key)));
|
|
584
|
+
if (withWebBackendOperation(StorageScope.Secure, "deleteSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
|
|
516
585
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
517
586
|
}
|
|
518
587
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
519
588
|
},
|
|
520
589
|
hasSecureBiometric: key => {
|
|
521
|
-
return
|
|
590
|
+
return withWebBackendOperation(StorageScope.Secure, "hasSecureBiometric", backend => backend.getItem(toBiometricStorageKey(key))) !== null;
|
|
522
591
|
},
|
|
523
592
|
clearSecureBiometric: () => {
|
|
524
|
-
const
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
for (let i = 0; i < storage.length; i++) {
|
|
529
|
-
const k = storage.key(i);
|
|
530
|
-
if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
531
|
-
toRemove.push(k);
|
|
532
|
-
keysToNotify.push(fromBiometricStorageKey(k));
|
|
533
|
-
}
|
|
593
|
+
const storageKeys = withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric:getAllKeys", backend => backend.getAllKeys());
|
|
594
|
+
const keysToNotify = storageKeys.filter(key => key.startsWith(BIOMETRIC_WEB_PREFIX)).map(key => fromBiometricStorageKey(key));
|
|
595
|
+
if (keysToNotify.length === 0) {
|
|
596
|
+
return;
|
|
534
597
|
}
|
|
535
|
-
|
|
598
|
+
withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric", backend => {
|
|
599
|
+
const biometricKeys = keysToNotify.map(key => toBiometricStorageKey(key));
|
|
600
|
+
if (backend.removeMany) {
|
|
601
|
+
backend.removeMany(biometricKeys);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
biometricKeys.forEach(storageKey => {
|
|
605
|
+
backend.removeItem(storageKey);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
536
608
|
const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
|
|
537
609
|
keysToNotify.forEach(key => {
|
|
538
|
-
if (
|
|
610
|
+
if (withWebBackendOperation(StorageScope.Secure, "clearSecureBiometric:getItem", backend => backend.getItem(toSecureStorageKey(key))) === null) {
|
|
539
611
|
keyIndex.delete(key);
|
|
540
612
|
}
|
|
541
613
|
});
|
|
@@ -549,6 +621,9 @@ function getRawValue(key, scope) {
|
|
|
549
621
|
const value = memoryStore.get(key);
|
|
550
622
|
return typeof value === "string" ? value : undefined;
|
|
551
623
|
}
|
|
624
|
+
if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
|
|
625
|
+
return readPendingDiskWrite(key);
|
|
626
|
+
}
|
|
552
627
|
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
553
628
|
return readPendingSecureWrite(key);
|
|
554
629
|
}
|
|
@@ -561,6 +636,15 @@ function setRawValue(key, value, scope) {
|
|
|
561
636
|
notifyKeyListeners(memoryListeners, key);
|
|
562
637
|
return;
|
|
563
638
|
}
|
|
639
|
+
if (scope === StorageScope.Disk) {
|
|
640
|
+
cacheRawValue(scope, key, value);
|
|
641
|
+
if (diskWritesAsync) {
|
|
642
|
+
scheduleDiskWrite(key, value);
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
flushDiskWrites();
|
|
646
|
+
clearPendingDiskWrite(key);
|
|
647
|
+
}
|
|
564
648
|
if (scope === StorageScope.Secure) {
|
|
565
649
|
flushSecureWrites();
|
|
566
650
|
clearPendingSecureWrite(key);
|
|
@@ -575,6 +659,15 @@ function removeRawValue(key, scope) {
|
|
|
575
659
|
notifyKeyListeners(memoryListeners, key);
|
|
576
660
|
return;
|
|
577
661
|
}
|
|
662
|
+
if (scope === StorageScope.Disk) {
|
|
663
|
+
cacheRawValue(scope, key, undefined);
|
|
664
|
+
if (diskWritesAsync) {
|
|
665
|
+
scheduleDiskWrite(key, undefined);
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
flushDiskWrites();
|
|
669
|
+
clearPendingDiskWrite(key);
|
|
670
|
+
}
|
|
578
671
|
if (scope === StorageScope.Secure) {
|
|
579
672
|
flushSecureWrites();
|
|
580
673
|
clearPendingSecureWrite(key);
|
|
@@ -601,6 +694,10 @@ export const storage = {
|
|
|
601
694
|
notifyAllListeners(memoryListeners);
|
|
602
695
|
return;
|
|
603
696
|
}
|
|
697
|
+
if (scope === StorageScope.Disk) {
|
|
698
|
+
flushDiskWrites();
|
|
699
|
+
pendingDiskWrites.clear();
|
|
700
|
+
}
|
|
604
701
|
if (scope === StorageScope.Secure) {
|
|
605
702
|
flushSecureWrites();
|
|
606
703
|
pendingSecureWrites.clear();
|
|
@@ -629,6 +726,9 @@ export const storage = {
|
|
|
629
726
|
return;
|
|
630
727
|
}
|
|
631
728
|
const keyPrefix = prefixKey(namespace, "");
|
|
729
|
+
if (scope === StorageScope.Disk) {
|
|
730
|
+
flushDiskWrites();
|
|
731
|
+
}
|
|
632
732
|
if (scope === StorageScope.Secure) {
|
|
633
733
|
flushSecureWrites();
|
|
634
734
|
}
|
|
@@ -650,6 +750,12 @@ export const storage = {
|
|
|
650
750
|
return measureOperation("storage:has", scope, () => {
|
|
651
751
|
assertValidScope(scope);
|
|
652
752
|
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
753
|
+
if (scope === StorageScope.Disk) {
|
|
754
|
+
flushDiskWrites();
|
|
755
|
+
}
|
|
756
|
+
if (scope === StorageScope.Secure) {
|
|
757
|
+
flushSecureWrites();
|
|
758
|
+
}
|
|
653
759
|
return WebStorage.has(key, scope);
|
|
654
760
|
});
|
|
655
761
|
},
|
|
@@ -657,6 +763,12 @@ export const storage = {
|
|
|
657
763
|
return measureOperation("storage:getAllKeys", scope, () => {
|
|
658
764
|
assertValidScope(scope);
|
|
659
765
|
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
766
|
+
if (scope === StorageScope.Disk) {
|
|
767
|
+
flushDiskWrites();
|
|
768
|
+
}
|
|
769
|
+
if (scope === StorageScope.Secure) {
|
|
770
|
+
flushSecureWrites();
|
|
771
|
+
}
|
|
660
772
|
return WebStorage.getAllKeys(scope);
|
|
661
773
|
});
|
|
662
774
|
},
|
|
@@ -666,6 +778,12 @@ export const storage = {
|
|
|
666
778
|
if (scope === StorageScope.Memory) {
|
|
667
779
|
return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
|
|
668
780
|
}
|
|
781
|
+
if (scope === StorageScope.Disk) {
|
|
782
|
+
flushDiskWrites();
|
|
783
|
+
}
|
|
784
|
+
if (scope === StorageScope.Secure) {
|
|
785
|
+
flushSecureWrites();
|
|
786
|
+
}
|
|
669
787
|
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
670
788
|
});
|
|
671
789
|
},
|
|
@@ -685,6 +803,12 @@ export const storage = {
|
|
|
685
803
|
});
|
|
686
804
|
return result;
|
|
687
805
|
}
|
|
806
|
+
if (scope === StorageScope.Disk) {
|
|
807
|
+
flushDiskWrites();
|
|
808
|
+
}
|
|
809
|
+
if (scope === StorageScope.Secure) {
|
|
810
|
+
flushSecureWrites();
|
|
811
|
+
}
|
|
688
812
|
const values = WebStorage.getBatch(keys, scope);
|
|
689
813
|
keys.forEach((key, index) => {
|
|
690
814
|
const value = values[index];
|
|
@@ -705,6 +829,12 @@ export const storage = {
|
|
|
705
829
|
});
|
|
706
830
|
return result;
|
|
707
831
|
}
|
|
832
|
+
if (scope === StorageScope.Disk) {
|
|
833
|
+
flushDiskWrites();
|
|
834
|
+
}
|
|
835
|
+
if (scope === StorageScope.Secure) {
|
|
836
|
+
flushSecureWrites();
|
|
837
|
+
}
|
|
708
838
|
const keys = WebStorage.getAllKeys(scope);
|
|
709
839
|
if (keys.length === 0) return {};
|
|
710
840
|
const values = WebStorage.getBatch(keys, scope);
|
|
@@ -721,6 +851,12 @@ export const storage = {
|
|
|
721
851
|
return measureOperation("storage:size", scope, () => {
|
|
722
852
|
assertValidScope(scope);
|
|
723
853
|
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
854
|
+
if (scope === StorageScope.Disk) {
|
|
855
|
+
flushDiskWrites();
|
|
856
|
+
}
|
|
857
|
+
if (scope === StorageScope.Secure) {
|
|
858
|
+
flushSecureWrites();
|
|
859
|
+
}
|
|
724
860
|
return WebStorage.size(scope);
|
|
725
861
|
});
|
|
726
862
|
},
|
|
@@ -731,6 +867,19 @@ export const storage = {
|
|
|
731
867
|
setSecureWritesAsync: _enabled => {
|
|
732
868
|
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
733
869
|
},
|
|
870
|
+
setDiskWritesAsync: enabled => {
|
|
871
|
+
measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
|
|
872
|
+
diskWritesAsync = enabled;
|
|
873
|
+
if (!enabled) {
|
|
874
|
+
flushDiskWrites();
|
|
875
|
+
}
|
|
876
|
+
});
|
|
877
|
+
},
|
|
878
|
+
flushDiskWrites: () => {
|
|
879
|
+
measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
|
|
880
|
+
flushDiskWrites();
|
|
881
|
+
});
|
|
882
|
+
},
|
|
734
883
|
flushSecureWrites: () => {
|
|
735
884
|
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
736
885
|
flushSecureWrites();
|
|
@@ -757,6 +906,69 @@ export const storage = {
|
|
|
757
906
|
resetMetrics: () => {
|
|
758
907
|
metricsCounters.clear();
|
|
759
908
|
},
|
|
909
|
+
getCapabilities: () => ({
|
|
910
|
+
platform: "web",
|
|
911
|
+
backend: {
|
|
912
|
+
disk: getBackendName(StorageScope.Disk, webDiskStorageBackend),
|
|
913
|
+
secure: getBackendName(StorageScope.Secure, webSecureStorageBackend)
|
|
914
|
+
},
|
|
915
|
+
writeBuffering: {
|
|
916
|
+
disk: true,
|
|
917
|
+
secure: true
|
|
918
|
+
},
|
|
919
|
+
errorClassification: true
|
|
920
|
+
}),
|
|
921
|
+
getSecurityCapabilities: () => {
|
|
922
|
+
const secureBackend = getBackendName(StorageScope.Secure, webSecureStorageBackend);
|
|
923
|
+
return {
|
|
924
|
+
platform: "web",
|
|
925
|
+
secureStorage: {
|
|
926
|
+
backend: secureBackend,
|
|
927
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
928
|
+
accessControl: "unavailable",
|
|
929
|
+
keychainAccessGroup: "unavailable",
|
|
930
|
+
hardwareBacked: "unavailable"
|
|
931
|
+
},
|
|
932
|
+
biometric: {
|
|
933
|
+
storage: "unavailable",
|
|
934
|
+
prompt: "unavailable",
|
|
935
|
+
biometryOnly: "unavailable",
|
|
936
|
+
biometryOrPasscode: "unavailable"
|
|
937
|
+
},
|
|
938
|
+
metadata: {
|
|
939
|
+
perKey: true,
|
|
940
|
+
listsWithoutValues: true,
|
|
941
|
+
persistsTimestamps: false
|
|
942
|
+
}
|
|
943
|
+
};
|
|
944
|
+
},
|
|
945
|
+
getSecureMetadata: key => {
|
|
946
|
+
return measureOperation("storage:getSecureMetadata", StorageScope.Secure, () => {
|
|
947
|
+
flushSecureWrites();
|
|
948
|
+
const biometricProtected = WebStorage.hasSecureBiometric(key);
|
|
949
|
+
const exists = biometricProtected || WebStorage.has(key, StorageScope.Secure);
|
|
950
|
+
let kind = "missing";
|
|
951
|
+
if (exists) {
|
|
952
|
+
kind = biometricProtected ? "biometric" : "secure";
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
key,
|
|
956
|
+
exists,
|
|
957
|
+
kind,
|
|
958
|
+
backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
|
|
959
|
+
encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
|
|
960
|
+
hardwareBacked: "unavailable",
|
|
961
|
+
biometricProtected,
|
|
962
|
+
valueExposed: false
|
|
963
|
+
};
|
|
964
|
+
});
|
|
965
|
+
},
|
|
966
|
+
getAllSecureMetadata: () => {
|
|
967
|
+
return measureOperation("storage:getAllSecureMetadata", StorageScope.Secure, () => {
|
|
968
|
+
flushSecureWrites();
|
|
969
|
+
return WebStorage.getAllKeys(StorageScope.Secure).map(key => storage.getSecureMetadata(key));
|
|
970
|
+
});
|
|
971
|
+
},
|
|
760
972
|
getString: (key, scope) => {
|
|
761
973
|
return measureOperation("storage:getString", scope, () => {
|
|
762
974
|
return getRawValue(key, scope);
|
|
@@ -789,21 +1001,50 @@ export const storage = {
|
|
|
789
1001
|
flushSecureWrites();
|
|
790
1002
|
WebStorage.setSecureAccessControl(secureDefaultAccessControl);
|
|
791
1003
|
}
|
|
1004
|
+
if (scope === StorageScope.Disk) {
|
|
1005
|
+
flushDiskWrites();
|
|
1006
|
+
}
|
|
792
1007
|
WebStorage.setBatch(keys, values, scope);
|
|
793
1008
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
794
1009
|
}, keys.length);
|
|
795
1010
|
}
|
|
796
1011
|
};
|
|
797
1012
|
export function setWebSecureStorageBackend(backend) {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1013
|
+
pendingSecureWrites.clear();
|
|
1014
|
+
webSecureStorageBackend = backend ?? createDefaultSecureBackend();
|
|
1015
|
+
resetBackendChangeSubscription(StorageScope.Secure);
|
|
801
1016
|
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
802
1017
|
clearScopeRawCache(StorageScope.Secure);
|
|
1018
|
+
ensureExternalSyncSubscriptions();
|
|
803
1019
|
}
|
|
804
1020
|
export function getWebSecureStorageBackend() {
|
|
805
1021
|
return webSecureStorageBackend;
|
|
806
1022
|
}
|
|
1023
|
+
export function setWebDiskStorageBackend(backend) {
|
|
1024
|
+
pendingDiskWrites.clear();
|
|
1025
|
+
webDiskStorageBackend = backend ?? createDefaultDiskBackend();
|
|
1026
|
+
resetBackendChangeSubscription(StorageScope.Disk);
|
|
1027
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
|
|
1028
|
+
clearScopeRawCache(StorageScope.Disk);
|
|
1029
|
+
ensureExternalSyncSubscriptions();
|
|
1030
|
+
}
|
|
1031
|
+
export function getWebDiskStorageBackend() {
|
|
1032
|
+
return webDiskStorageBackend;
|
|
1033
|
+
}
|
|
1034
|
+
export async function flushWebStorageBackends() {
|
|
1035
|
+
flushDiskWrites();
|
|
1036
|
+
flushSecureWrites();
|
|
1037
|
+
const flushes = [];
|
|
1038
|
+
const diskFlush = webDiskStorageBackend?.flush;
|
|
1039
|
+
const secureFlush = webSecureStorageBackend?.flush;
|
|
1040
|
+
if (diskFlush) {
|
|
1041
|
+
flushes.push(diskFlush());
|
|
1042
|
+
}
|
|
1043
|
+
if (secureFlush) {
|
|
1044
|
+
flushes.push(secureFlush());
|
|
1045
|
+
}
|
|
1046
|
+
await Promise.all(flushes);
|
|
1047
|
+
}
|
|
807
1048
|
function canUseRawBatchPath(item) {
|
|
808
1049
|
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
809
1050
|
}
|
|
@@ -831,6 +1072,7 @@ export function createStorageItem(config) {
|
|
|
831
1072
|
const expirationTtlMs = expiration?.ttlMs;
|
|
832
1073
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
833
1074
|
const readCache = !isMemory && config.readCache === true;
|
|
1075
|
+
const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
834
1076
|
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
835
1077
|
const defaultValue = config.defaultValue;
|
|
836
1078
|
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
@@ -861,7 +1103,7 @@ export function createStorageItem(config) {
|
|
|
861
1103
|
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
862
1104
|
return;
|
|
863
1105
|
}
|
|
864
|
-
|
|
1106
|
+
ensureExternalSyncSubscriptions();
|
|
865
1107
|
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
|
|
866
1108
|
};
|
|
867
1109
|
const readStoredRaw = () => {
|
|
@@ -878,6 +1120,12 @@ export function createStorageItem(config) {
|
|
|
878
1120
|
}
|
|
879
1121
|
return memoryStore.get(storageKey);
|
|
880
1122
|
}
|
|
1123
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1124
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1125
|
+
if (pending !== undefined) {
|
|
1126
|
+
return pending.value;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
881
1129
|
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
882
1130
|
const pending = pendingSecureWrites.get(storageKey);
|
|
883
1131
|
if (pending !== undefined) {
|
|
@@ -904,6 +1152,13 @@ export function createStorageItem(config) {
|
|
|
904
1152
|
return;
|
|
905
1153
|
}
|
|
906
1154
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
1155
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1156
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1157
|
+
scheduleDiskWrite(storageKey, rawValue);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
clearPendingDiskWrite(storageKey);
|
|
1161
|
+
}
|
|
907
1162
|
if (coalesceSecureWrites) {
|
|
908
1163
|
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
909
1164
|
return;
|
|
@@ -919,6 +1174,13 @@ export function createStorageItem(config) {
|
|
|
919
1174
|
return;
|
|
920
1175
|
}
|
|
921
1176
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
1177
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1178
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1179
|
+
scheduleDiskWrite(storageKey, undefined);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
clearPendingDiskWrite(storageKey);
|
|
1183
|
+
}
|
|
922
1184
|
if (coalesceSecureWrites) {
|
|
923
1185
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
924
1186
|
return;
|
|
@@ -1079,6 +1341,18 @@ export function createStorageItem(config) {
|
|
|
1079
1341
|
const hasItem = () => measureOperation("item:has", config.scope, () => {
|
|
1080
1342
|
if (isMemory) return memoryStore.has(storageKey);
|
|
1081
1343
|
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
1344
|
+
if (nonMemoryScope === StorageScope.Disk) {
|
|
1345
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1346
|
+
if (pending !== undefined) {
|
|
1347
|
+
return pending.value !== undefined;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
1351
|
+
const pending = pendingSecureWrites.get(storageKey);
|
|
1352
|
+
if (pending !== undefined) {
|
|
1353
|
+
return pending.value !== undefined;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1082
1356
|
return WebStorage.has(storageKey, config.scope);
|
|
1083
1357
|
});
|
|
1084
1358
|
const subscribe = callback => {
|
|
@@ -1089,9 +1363,6 @@ export function createStorageItem(config) {
|
|
|
1089
1363
|
if (listeners.size === 0 && unsubscribe) {
|
|
1090
1364
|
unsubscribe();
|
|
1091
1365
|
unsubscribe = null;
|
|
1092
|
-
if (!isMemory) {
|
|
1093
|
-
maybeCleanupWebStorageSubscription();
|
|
1094
|
-
}
|
|
1095
1366
|
}
|
|
1096
1367
|
};
|
|
1097
1368
|
};
|
|
@@ -1141,6 +1412,13 @@ export function getBatch(items, scope) {
|
|
|
1141
1412
|
const keysToFetch = [];
|
|
1142
1413
|
const keyIndexes = [];
|
|
1143
1414
|
items.forEach((item, index) => {
|
|
1415
|
+
if (scope === StorageScope.Disk) {
|
|
1416
|
+
const pending = pendingDiskWrites.get(item.key);
|
|
1417
|
+
if (pending !== undefined) {
|
|
1418
|
+
rawValues[index] = pending.value;
|
|
1419
|
+
return;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1144
1422
|
if (scope === StorageScope.Secure) {
|
|
1145
1423
|
const pending = pendingSecureWrites.get(item.key);
|
|
1146
1424
|
if (pending !== undefined) {
|
|
@@ -1257,6 +1535,7 @@ export function setBatch(items, scope) {
|
|
|
1257
1535
|
});
|
|
1258
1536
|
return;
|
|
1259
1537
|
}
|
|
1538
|
+
flushDiskWrites();
|
|
1260
1539
|
const useRawBatchPath = items.every(({
|
|
1261
1540
|
item
|
|
1262
1541
|
}) => canUseRawBatchPath(asInternal(item)));
|
|
@@ -1281,6 +1560,9 @@ export function removeBatch(items, scope) {
|
|
|
1281
1560
|
return;
|
|
1282
1561
|
}
|
|
1283
1562
|
const keys = items.map(item => item.key);
|
|
1563
|
+
if (scope === StorageScope.Disk) {
|
|
1564
|
+
flushDiskWrites();
|
|
1565
|
+
}
|
|
1284
1566
|
if (scope === StorageScope.Secure) {
|
|
1285
1567
|
flushSecureWrites();
|
|
1286
1568
|
}
|
|
@@ -1326,6 +1608,9 @@ export function migrateToLatest(scope = StorageScope.Disk) {
|
|
|
1326
1608
|
export function runTransaction(scope, transaction) {
|
|
1327
1609
|
return measureOperation("transaction:run", scope, () => {
|
|
1328
1610
|
assertValidScope(scope);
|
|
1611
|
+
if (scope === StorageScope.Disk) {
|
|
1612
|
+
flushDiskWrites();
|
|
1613
|
+
}
|
|
1329
1614
|
if (scope === StorageScope.Secure) {
|
|
1330
1615
|
flushSecureWrites();
|
|
1331
1616
|
}
|
|
@@ -1392,6 +1677,9 @@ export function runTransaction(scope, transaction) {
|
|
|
1392
1677
|
valuesToSet.push(previousValue);
|
|
1393
1678
|
}
|
|
1394
1679
|
});
|
|
1680
|
+
if (scope === StorageScope.Disk) {
|
|
1681
|
+
flushDiskWrites();
|
|
1682
|
+
}
|
|
1395
1683
|
if (scope === StorageScope.Secure) {
|
|
1396
1684
|
flushSecureWrites();
|
|
1397
1685
|
}
|
|
@@ -1437,7 +1725,7 @@ export function createSecureAuthStorage(config, options) {
|
|
|
1437
1725
|
}
|
|
1438
1726
|
return result;
|
|
1439
1727
|
}
|
|
1440
|
-
export function isKeychainLockedError(
|
|
1441
|
-
return
|
|
1728
|
+
export function isKeychainLockedError(err) {
|
|
1729
|
+
return isLockedStorageErrorCode(getStorageErrorCode(err));
|
|
1442
1730
|
}
|
|
1443
1731
|
//# sourceMappingURL=index.web.js.map
|