react-native-nitro-storage 0.5.7 → 0.5.8
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 +1 -0
- package/lib/commonjs/index.js +119 -1599
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +83 -1550
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/shared.js +102 -0
- package/lib/commonjs/shared.js.map +1 -0
- package/lib/commonjs/storage-core.js +1505 -0
- package/lib/commonjs/storage-core.js.map +1 -0
- package/lib/module/index.js +111 -1591
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +69 -1536
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/shared.js +82 -0
- package/lib/module/shared.js.map +1 -0
- package/lib/module/storage-core.js +1501 -0
- package/lib/module/storage-core.js.map +1 -0
- package/lib/typescript/index.d.ts +30 -133
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +30 -133
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +96 -0
- package/lib/typescript/shared.d.ts.map +1 -0
- package/lib/typescript/storage-core.d.ts +157 -0
- package/lib/typescript/storage-core.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/index.ts +260 -2519
- package/src/index.web.ts +132 -2331
- package/src/shared.ts +249 -0
- package/src/storage-core.ts +2349 -0
package/src/index.ts
CHANGED
|
@@ -1,41 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
assertAccessControlLevel,
|
|
3
|
+
notifyAllListeners,
|
|
4
|
+
notifyKeyListeners,
|
|
5
|
+
type NonMemoryScope,
|
|
6
|
+
} from "./shared";
|
|
1
7
|
import { NitroModules } from "react-native-nitro-modules";
|
|
2
8
|
import type { Storage } from "./Storage.nitro";
|
|
3
|
-
import { StorageScope, AccessControl
|
|
4
|
-
import {
|
|
5
|
-
MIGRATION_VERSION_KEY,
|
|
6
|
-
type StoredEnvelope,
|
|
7
|
-
isStoredEnvelope,
|
|
8
|
-
assertBatchScope,
|
|
9
|
-
assertValidScope,
|
|
10
|
-
decodeNativeBatchValue,
|
|
11
|
-
serializeWithPrimitiveFastPath,
|
|
12
|
-
deserializeWithPrimitiveFastPath,
|
|
13
|
-
toVersionToken,
|
|
14
|
-
prefixKey,
|
|
15
|
-
isNamespaced,
|
|
16
|
-
} from "./internal";
|
|
9
|
+
import { StorageScope, AccessControl } from "./Storage.types";
|
|
10
|
+
import { decodeNativeBatchValue } from "./internal";
|
|
17
11
|
import type {
|
|
18
12
|
WebDiskStorageBackend,
|
|
19
13
|
WebSecureStorageBackend,
|
|
20
14
|
} from "./web-storage-backend";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
type SecureStorageMetadata,
|
|
25
|
-
type SecurityCapabilities,
|
|
26
|
-
type StorageCapabilities,
|
|
27
|
-
type StorageErrorCode,
|
|
15
|
+
import type {
|
|
16
|
+
SecurityCapabilities,
|
|
17
|
+
StorageCapabilities,
|
|
28
18
|
} from "./storage-runtime";
|
|
29
19
|
import {
|
|
30
|
-
|
|
31
|
-
type
|
|
32
|
-
type
|
|
33
|
-
type
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
20
|
+
createStorageCore,
|
|
21
|
+
type StorageCoreAdapter,
|
|
22
|
+
type StorageCoreBackend,
|
|
23
|
+
type StorageCoreInternals,
|
|
24
|
+
} from "./storage-core";
|
|
25
|
+
export type {
|
|
26
|
+
ExpirationConfig,
|
|
27
|
+
Migration,
|
|
28
|
+
MigrationContext,
|
|
29
|
+
SecureAuthStorageConfig,
|
|
30
|
+
StorageEventObserverOptions,
|
|
31
|
+
StorageExportOptions,
|
|
32
|
+
StorageMetricSummary,
|
|
33
|
+
StorageMetricsEvent,
|
|
34
|
+
StorageMetricsObserver,
|
|
35
|
+
StorageSelectorListener,
|
|
36
|
+
StorageSelectorSubscribeOptions,
|
|
37
|
+
StorageVersion,
|
|
38
|
+
Validator,
|
|
39
|
+
VersionedValue,
|
|
40
|
+
} from "./shared";
|
|
41
|
+
export { isKeychainLockedError } from "./shared";
|
|
39
42
|
|
|
40
43
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
41
44
|
export type { Storage } from "./Storage.nitro";
|
|
@@ -63,147 +66,12 @@ export type {
|
|
|
63
66
|
WebStorageChangeEvent,
|
|
64
67
|
WebStorageScope,
|
|
65
68
|
} from "./web-storage-backend";
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
export type VersionedValue<T> = {
|
|
73
|
-
value: T;
|
|
74
|
-
version: StorageVersion;
|
|
75
|
-
};
|
|
76
|
-
export type StorageMetricsEvent = {
|
|
77
|
-
operation: string;
|
|
78
|
-
scope: StorageScope;
|
|
79
|
-
durationMs: number;
|
|
80
|
-
keysCount: number;
|
|
81
|
-
};
|
|
82
|
-
export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
|
|
83
|
-
export type StorageEventObserverOptions = {
|
|
84
|
-
redactSecureValues?: boolean;
|
|
85
|
-
};
|
|
86
|
-
export type StorageExportOptions = {
|
|
87
|
-
includeSecureValues?: boolean;
|
|
88
|
-
};
|
|
89
|
-
export type StorageMetricSummary = {
|
|
90
|
-
count: number;
|
|
91
|
-
totalDurationMs: number;
|
|
92
|
-
avgDurationMs: number;
|
|
93
|
-
maxDurationMs: number;
|
|
94
|
-
};
|
|
95
|
-
export type StorageSelectorListener<TSelected> = (
|
|
96
|
-
value: TSelected,
|
|
97
|
-
previousValue: TSelected,
|
|
98
|
-
) => void;
|
|
99
|
-
export type StorageSelectorSubscribeOptions<TSelected> = {
|
|
100
|
-
isEqual?: (previousValue: TSelected, nextValue: TSelected) => boolean;
|
|
101
|
-
fireImmediately?: boolean;
|
|
102
|
-
};
|
|
103
|
-
export type MigrationContext = {
|
|
104
|
-
scope: StorageScope;
|
|
105
|
-
getRaw: (key: string) => string | undefined;
|
|
106
|
-
setRaw: (key: string, value: string) => void;
|
|
107
|
-
removeRaw: (key: string) => void;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
export type Migration = (context: MigrationContext) => void;
|
|
111
|
-
|
|
112
|
-
export type TransactionContext = {
|
|
113
|
-
scope: StorageScope;
|
|
114
|
-
getRaw: (key: string) => string | undefined;
|
|
115
|
-
setRaw: (key: string, value: string) => void;
|
|
116
|
-
removeRaw: (key: string) => void;
|
|
117
|
-
getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
|
|
118
|
-
setItem: <T>(
|
|
119
|
-
item: Pick<StorageItem<T>, "scope" | "key" | "set">,
|
|
120
|
-
value: T,
|
|
121
|
-
) => void;
|
|
122
|
-
removeItem: (
|
|
123
|
-
item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
|
|
124
|
-
) => void;
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
128
|
-
type RawBatchPathItem = {
|
|
129
|
-
_hasValidation?: boolean;
|
|
130
|
-
_hasExpiration?: boolean;
|
|
131
|
-
_isBiometric?: boolean;
|
|
132
|
-
_biometricLevel?: BiometricLevel;
|
|
133
|
-
_secureAccessControl?: AccessControl;
|
|
134
|
-
};
|
|
135
|
-
type RollbackRecord =
|
|
136
|
-
| {
|
|
137
|
-
kind: "memory";
|
|
138
|
-
value: unknown;
|
|
139
|
-
}
|
|
140
|
-
| {
|
|
141
|
-
kind: "raw";
|
|
142
|
-
value: string | undefined;
|
|
143
|
-
accessControl?: AccessControl;
|
|
144
|
-
}
|
|
145
|
-
| {
|
|
146
|
-
kind: "biometric";
|
|
147
|
-
value: string | undefined;
|
|
148
|
-
level: BiometricLevel;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
|
|
152
|
-
return item as StorageItemInternal<T>;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function isUpdater<T>(
|
|
156
|
-
valueOrFn: T | ((prev: T) => T),
|
|
157
|
-
): valueOrFn is (prev: T) => T {
|
|
158
|
-
return typeof valueOrFn === "function";
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
|
|
162
|
-
return Object.keys(record) as K[];
|
|
163
|
-
}
|
|
164
|
-
function assertEnumInteger(
|
|
165
|
-
value: number,
|
|
166
|
-
min: number,
|
|
167
|
-
max: number,
|
|
168
|
-
label: string,
|
|
169
|
-
): void {
|
|
170
|
-
if (!Number.isFinite(value) || value < min || value > max) {
|
|
171
|
-
throw new Error(`NitroStorage: Invalid ${label}`);
|
|
172
|
-
}
|
|
173
|
-
if (value !== Math.trunc(value)) {
|
|
174
|
-
throw new Error(`NitroStorage: Invalid ${label}`);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function assertAccessControlLevel(level: number): void {
|
|
179
|
-
assertEnumInteger(level, 0, 4, "access control level");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function assertBiometricLevel(level: number): void {
|
|
183
|
-
assertEnumInteger(level, 0, 2, "biometric level");
|
|
184
|
-
}
|
|
185
|
-
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
186
|
-
type PendingDiskWrite = {
|
|
187
|
-
key: string;
|
|
188
|
-
value: string | undefined;
|
|
189
|
-
};
|
|
190
|
-
type PendingSecureWrite = {
|
|
191
|
-
key: string;
|
|
192
|
-
value: string | undefined;
|
|
193
|
-
accessControl?: AccessControl;
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
const registeredMigrations = new Map<number, Migration>();
|
|
197
|
-
const runMicrotask =
|
|
198
|
-
typeof queueMicrotask === "function"
|
|
199
|
-
? queueMicrotask
|
|
200
|
-
: (task: () => void) => {
|
|
201
|
-
Promise.resolve().then(task);
|
|
202
|
-
};
|
|
203
|
-
const now =
|
|
204
|
-
typeof performance !== "undefined" && typeof performance.now === "function"
|
|
205
|
-
? () => performance.now()
|
|
206
|
-
: () => Date.now();
|
|
69
|
+
export type {
|
|
70
|
+
StorageBatchSetItem,
|
|
71
|
+
StorageItem,
|
|
72
|
+
StorageItemConfig,
|
|
73
|
+
TransactionContext,
|
|
74
|
+
} from "./storage-core";
|
|
207
75
|
|
|
208
76
|
let _storageModule: Storage | null = null;
|
|
209
77
|
|
|
@@ -214,2391 +82,264 @@ function getStorageModule(): Storage {
|
|
|
214
82
|
return _storageModule;
|
|
215
83
|
}
|
|
216
84
|
|
|
217
|
-
const memoryStore = new Map<string, unknown>();
|
|
218
|
-
const memoryListeners: KeyListenerRegistry = new Map();
|
|
219
|
-
const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
220
|
-
[StorageScope.Disk, new Map()],
|
|
221
|
-
[StorageScope.Secure, new Map()],
|
|
222
|
-
]);
|
|
223
|
-
const scopedUnsubscribers = new Map<NonMemoryScope, () => void>();
|
|
224
|
-
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
225
|
-
[
|
|
226
|
-
[StorageScope.Disk, new Map()],
|
|
227
|
-
[StorageScope.Secure, new Map()],
|
|
228
|
-
],
|
|
229
|
-
);
|
|
230
|
-
const pendingDiskWrites = new Map<string, PendingDiskWrite>();
|
|
231
|
-
let diskFlushScheduled = false;
|
|
232
|
-
let diskWritesAsync = false;
|
|
233
|
-
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
234
|
-
let secureFlushScheduled = false;
|
|
235
|
-
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
236
|
-
const suppressedNativeEvents = new Map<NonMemoryScope, Map<string, number>>([
|
|
237
|
-
[StorageScope.Disk, new Map()],
|
|
238
|
-
[StorageScope.Secure, new Map()],
|
|
239
|
-
]);
|
|
240
|
-
let metricsObserver: StorageMetricsObserver | undefined;
|
|
241
|
-
let eventObserver: StorageEventListener | undefined;
|
|
242
|
-
let eventObserverRedactSecureValues = true;
|
|
243
|
-
const metricsCounters = new Map<
|
|
244
|
-
string,
|
|
245
|
-
{ count: number; totalDurationMs: number; maxDurationMs: number }
|
|
246
|
-
>();
|
|
247
|
-
const storageEvents = new StorageEventRegistry();
|
|
248
85
|
const nativeSecureBackend = "platform-secure-storage";
|
|
249
86
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
operation: string,
|
|
279
|
-
scope: StorageScope,
|
|
280
|
-
fn: () => T,
|
|
281
|
-
keysCount = 1,
|
|
282
|
-
): T {
|
|
283
|
-
if (!metricsObserver) {
|
|
284
|
-
return fn();
|
|
285
|
-
}
|
|
286
|
-
const start = now();
|
|
287
|
-
try {
|
|
288
|
-
return fn();
|
|
289
|
-
} finally {
|
|
290
|
-
recordMetric(operation, scope, now() - start, keysCount);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
295
|
-
return scopedListeners.get(scope)!;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
function getScopeRawCache(
|
|
299
|
-
scope: NonMemoryScope,
|
|
300
|
-
): Map<string, string | undefined> {
|
|
301
|
-
return scopedRawCache.get(scope)!;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function cacheRawValue(
|
|
305
|
-
scope: NonMemoryScope,
|
|
306
|
-
key: string,
|
|
307
|
-
value: string | undefined,
|
|
308
|
-
): void {
|
|
309
|
-
getScopeRawCache(scope).set(key, value);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function readCachedRawValue(
|
|
313
|
-
scope: NonMemoryScope,
|
|
314
|
-
key: string,
|
|
315
|
-
): string | undefined {
|
|
316
|
-
return getScopeRawCache(scope).get(key);
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function hasCachedRawValue(scope: NonMemoryScope, key: string): boolean {
|
|
320
|
-
return getScopeRawCache(scope).has(key);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function clearScopeRawCache(scope: NonMemoryScope): void {
|
|
324
|
-
getScopeRawCache(scope).clear();
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
function suppressNativeEvent(scope: NonMemoryScope, key: string): void {
|
|
328
|
-
const suppressedEvents = suppressedNativeEvents.get(scope)!;
|
|
329
|
-
suppressedEvents.set(key, (suppressedEvents.get(key) ?? 0) + 1);
|
|
330
|
-
}
|
|
87
|
+
const nativeBackend: StorageCoreBackend = {
|
|
88
|
+
get: (key, scope) => getStorageModule().get(key, scope),
|
|
89
|
+
set: (key, value, scope) => getStorageModule().set(key, value, scope),
|
|
90
|
+
remove: (key, scope) => getStorageModule().remove(key, scope),
|
|
91
|
+
clear: (scope) => getStorageModule().clear(scope),
|
|
92
|
+
has: (key, scope) => getStorageModule().has(key, scope),
|
|
93
|
+
getAllKeys: (scope) => getStorageModule().getAllKeys(scope) ?? [],
|
|
94
|
+
getKeysByPrefix: (prefix, scope) =>
|
|
95
|
+
getStorageModule().getKeysByPrefix(prefix, scope) ?? [],
|
|
96
|
+
size: (scope) => getStorageModule().size(scope),
|
|
97
|
+
setBatch: (keys, values, scope) =>
|
|
98
|
+
getStorageModule().setBatch(keys, values, scope),
|
|
99
|
+
getBatch: (keys, scope) =>
|
|
100
|
+
(getStorageModule().getBatch(keys, scope) ?? []).map((value) =>
|
|
101
|
+
decodeNativeBatchValue(value),
|
|
102
|
+
),
|
|
103
|
+
removeBatch: (keys, scope) => getStorageModule().removeBatch(keys, scope),
|
|
104
|
+
removeByPrefix: (prefix, scope) =>
|
|
105
|
+
getStorageModule().removeByPrefix(prefix, scope),
|
|
106
|
+
setSecureAccessControl: (level) =>
|
|
107
|
+
getStorageModule().setSecureAccessControl(level),
|
|
108
|
+
getSecureBiometric: (key) => getStorageModule().getSecureBiometric(key),
|
|
109
|
+
setSecureBiometricWithLevel: (key, value, level) =>
|
|
110
|
+
getStorageModule().setSecureBiometricWithLevel(key, value, level),
|
|
111
|
+
deleteSecureBiometric: (key) => getStorageModule().deleteSecureBiometric(key),
|
|
112
|
+
hasSecureBiometric: (key) => getStorageModule().hasSecureBiometric(key),
|
|
113
|
+
clearSecureBiometric: () => getStorageModule().clearSecureBiometric(),
|
|
114
|
+
};
|
|
331
115
|
|
|
332
|
-
function
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
)
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
341
|
-
if (count <= 1) {
|
|
342
|
-
suppressedEvents.delete(key);
|
|
343
|
-
} else {
|
|
344
|
-
suppressedEvents.set(key, count - 1);
|
|
345
|
-
}
|
|
346
|
-
return true;
|
|
347
|
-
}
|
|
116
|
+
function buildNativeAdapter(
|
|
117
|
+
internals: StorageCoreInternals,
|
|
118
|
+
): StorageCoreAdapter {
|
|
119
|
+
const scopedUnsubscribers = new Map<NonMemoryScope, () => void>();
|
|
120
|
+
const suppressedNativeEvents = new Map<NonMemoryScope, Map<string, number>>([
|
|
121
|
+
[StorageScope.Disk, new Map()],
|
|
122
|
+
[StorageScope.Secure, new Map()],
|
|
123
|
+
]);
|
|
348
124
|
|
|
349
|
-
function
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
for (const listener of listeners) {
|
|
353
|
-
listener();
|
|
354
|
-
}
|
|
125
|
+
function suppressNativeEvent(scope: NonMemoryScope, key: string): void {
|
|
126
|
+
const suppressedEvents = suppressedNativeEvents.get(scope)!;
|
|
127
|
+
suppressedEvents.set(key, (suppressedEvents.get(key) ?? 0) + 1);
|
|
355
128
|
}
|
|
356
|
-
}
|
|
357
129
|
|
|
358
|
-
function
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
130
|
+
function consumeSuppressedNativeEvent(
|
|
131
|
+
scope: NonMemoryScope,
|
|
132
|
+
key: string,
|
|
133
|
+
): boolean {
|
|
134
|
+
const suppressedEvents = suppressedNativeEvents.get(scope)!;
|
|
135
|
+
const count = suppressedEvents.get(key);
|
|
136
|
+
if (count === undefined) {
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
if (count <= 1) {
|
|
140
|
+
suppressedEvents.delete(key);
|
|
141
|
+
} else {
|
|
142
|
+
suppressedEvents.set(key, count - 1);
|
|
362
143
|
}
|
|
144
|
+
return true;
|
|
363
145
|
}
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
function addKeyListener(
|
|
367
|
-
registry: KeyListenerRegistry,
|
|
368
|
-
key: string,
|
|
369
|
-
listener: () => void,
|
|
370
|
-
): () => void {
|
|
371
|
-
let listeners = registry.get(key);
|
|
372
|
-
if (!listeners) {
|
|
373
|
-
listeners = new Set();
|
|
374
|
-
registry.set(key, listeners);
|
|
375
|
-
}
|
|
376
|
-
listeners.add(listener);
|
|
377
146
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
if (!scopedListeners) {
|
|
147
|
+
function ensureNativeScopeSubscription(scope: NonMemoryScope): void {
|
|
148
|
+
if (scopedUnsubscribers.has(scope)) {
|
|
381
149
|
return;
|
|
382
150
|
}
|
|
383
|
-
scopedListeners.delete(listener);
|
|
384
|
-
if (scopedListeners.size === 0) {
|
|
385
|
-
registry.delete(key);
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function getEventRawValue(
|
|
391
|
-
scope: StorageScope,
|
|
392
|
-
key: string,
|
|
393
|
-
): string | undefined {
|
|
394
|
-
if (scope === StorageScope.Memory) {
|
|
395
|
-
const value = memoryStore.get(key);
|
|
396
|
-
return typeof value === "string" ? value : undefined;
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
return getRawValue(key, scope);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function createKeyChange(
|
|
403
|
-
scope: StorageScope,
|
|
404
|
-
key: string,
|
|
405
|
-
oldValue: string | undefined,
|
|
406
|
-
newValue: string | undefined,
|
|
407
|
-
operation: StorageChangeOperation,
|
|
408
|
-
source: StorageChangeSource,
|
|
409
|
-
): StorageKeyChangeEvent {
|
|
410
|
-
return {
|
|
411
|
-
type: "key",
|
|
412
|
-
scope,
|
|
413
|
-
key,
|
|
414
|
-
oldValue,
|
|
415
|
-
newValue,
|
|
416
|
-
operation,
|
|
417
|
-
source,
|
|
418
|
-
};
|
|
419
|
-
}
|
|
420
151
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
152
|
+
const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
|
|
153
|
+
if (scope === StorageScope.Disk) {
|
|
154
|
+
if (key === "") {
|
|
155
|
+
internals.clearAllPendingDiskWrites();
|
|
156
|
+
} else {
|
|
157
|
+
internals.clearPendingDiskWrite(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
424
160
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
|
|
433
|
-
}
|
|
161
|
+
if (scope === StorageScope.Secure) {
|
|
162
|
+
if (key === "") {
|
|
163
|
+
internals.clearAllPendingSecureWrites();
|
|
164
|
+
} else {
|
|
165
|
+
internals.clearPendingSecureWrite(key);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
434
168
|
|
|
435
|
-
|
|
169
|
+
if (key === "") {
|
|
170
|
+
internals.clearScopeRawCache(scope);
|
|
171
|
+
notifyAllListeners(internals.getScopedListeners(scope));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
436
174
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
)
|
|
440
|
-
|
|
441
|
-
|
|
175
|
+
const oldValue = internals.readCachedRawValue(scope, key);
|
|
176
|
+
internals.cacheRawValue(scope, key, value);
|
|
177
|
+
notifyKeyListeners(internals.getScopedListeners(scope), key);
|
|
178
|
+
if (consumeSuppressedNativeEvent(scope, key)) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
internals.emitKeyChange(
|
|
182
|
+
scope,
|
|
183
|
+
key,
|
|
184
|
+
oldValue,
|
|
185
|
+
value,
|
|
186
|
+
"external",
|
|
187
|
+
"native",
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
scopedUnsubscribers.set(
|
|
191
|
+
scope,
|
|
192
|
+
typeof unsubscribe === "function" ? unsubscribe : () => {},
|
|
193
|
+
);
|
|
442
194
|
}
|
|
443
195
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
196
|
+
function maybeCleanupNativeScopeSubscription(scope: NonMemoryScope): void {
|
|
197
|
+
const listeners = internals.getScopedListeners(scope);
|
|
198
|
+
if (
|
|
199
|
+
listeners.size > 0 ||
|
|
200
|
+
internals.hasScopeEventListeners(scope) ||
|
|
201
|
+
internals.hasEventObserver()
|
|
202
|
+
) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
452
205
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
206
|
+
const unsubscribe = scopedUnsubscribers.get(scope);
|
|
207
|
+
if (!unsubscribe) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
457
210
|
|
|
458
|
-
|
|
459
|
-
|
|
211
|
+
unsubscribe();
|
|
212
|
+
scopedUnsubscribers.delete(scope);
|
|
460
213
|
}
|
|
461
214
|
|
|
462
215
|
return {
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
)
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const event = createKeyChange(
|
|
485
|
-
scope,
|
|
486
|
-
key,
|
|
487
|
-
oldValue,
|
|
488
|
-
newValue,
|
|
489
|
-
operation,
|
|
490
|
-
source,
|
|
491
|
-
);
|
|
492
|
-
storageEvents.emitKey(event);
|
|
493
|
-
eventObserver?.(eventForGlobalObserver(event));
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function emitBatchChange(
|
|
497
|
-
scope: StorageScope,
|
|
498
|
-
operation: StorageChangeOperation,
|
|
499
|
-
source: StorageChangeSource,
|
|
500
|
-
changes: StorageKeyChangeEvent[],
|
|
501
|
-
): void {
|
|
502
|
-
if (changes.length === 0) {
|
|
503
|
-
return;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (
|
|
507
|
-
source === "native" &&
|
|
508
|
-
operation !== "external" &&
|
|
509
|
-
scope !== StorageScope.Memory &&
|
|
510
|
-
scopedUnsubscribers.has(scope)
|
|
511
|
-
) {
|
|
512
|
-
changes.forEach((change) => suppressNativeEvent(scope, change.key));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const event: StorageBatchChangeEvent = {
|
|
516
|
-
type: "batch",
|
|
517
|
-
scope,
|
|
518
|
-
operation,
|
|
519
|
-
source,
|
|
520
|
-
changes,
|
|
216
|
+
backend: nativeBackend,
|
|
217
|
+
changeSource: "native",
|
|
218
|
+
applyAccessControlOnSecureRawWrite: true,
|
|
219
|
+
flushDiskWritesOnImport: false,
|
|
220
|
+
ensureScopeSubscription: ensureNativeScopeSubscription,
|
|
221
|
+
maybeCleanupScopeSubscription: maybeCleanupNativeScopeSubscription,
|
|
222
|
+
onWillEmitChanges: (scope, keys, operation, source) => {
|
|
223
|
+
if (
|
|
224
|
+
source === "native" &&
|
|
225
|
+
operation !== "external" &&
|
|
226
|
+
scope !== StorageScope.Memory &&
|
|
227
|
+
scopedUnsubscribers.has(scope)
|
|
228
|
+
) {
|
|
229
|
+
keys.forEach((key) => suppressNativeEvent(scope, key));
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
getSecureMetadataProfile: () => ({
|
|
233
|
+
backend: nativeSecureBackend,
|
|
234
|
+
encrypted: "available",
|
|
235
|
+
hardwareBacked: "unknown",
|
|
236
|
+
}),
|
|
521
237
|
};
|
|
522
|
-
storageEvents.emitBatch(event);
|
|
523
|
-
eventObserver?.(eventForGlobalObserver(event));
|
|
524
238
|
}
|
|
525
239
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
function readPendingDiskWrite(key: string): string | undefined {
|
|
531
|
-
return pendingDiskWrites.get(key)?.value;
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function hasPendingDiskWrite(key: string): boolean {
|
|
535
|
-
return pendingDiskWrites.has(key);
|
|
536
|
-
}
|
|
240
|
+
const core = createStorageCore(buildNativeAdapter);
|
|
241
|
+
const { internals } = core;
|
|
537
242
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
243
|
+
export const storage = {
|
|
244
|
+
...core.storage,
|
|
245
|
+
setAccessControl: (level: AccessControl) => {
|
|
246
|
+
internals.measureOperation(
|
|
247
|
+
"storage:setAccessControl",
|
|
248
|
+
StorageScope.Secure,
|
|
249
|
+
() => {
|
|
250
|
+
assertAccessControlLevel(level);
|
|
251
|
+
internals.setSecureDefaultAccessControl(level);
|
|
252
|
+
getStorageModule().setSecureAccessControl(level);
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
},
|
|
256
|
+
setSecureWritesAsync: (enabled: boolean) => {
|
|
257
|
+
internals.measureOperation(
|
|
258
|
+
"storage:setSecureWritesAsync",
|
|
259
|
+
StorageScope.Secure,
|
|
260
|
+
() => {
|
|
261
|
+
getStorageModule().setSecureWritesAsync(enabled);
|
|
262
|
+
},
|
|
263
|
+
);
|
|
264
|
+
},
|
|
265
|
+
setKeychainAccessGroup: (group: string) => {
|
|
266
|
+
internals.measureOperation(
|
|
267
|
+
"storage:setKeychainAccessGroup",
|
|
268
|
+
StorageScope.Secure,
|
|
269
|
+
() => {
|
|
270
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
271
|
+
},
|
|
272
|
+
);
|
|
273
|
+
},
|
|
274
|
+
getCapabilities: (): StorageCapabilities => ({
|
|
275
|
+
platform: "native",
|
|
276
|
+
backend: {
|
|
277
|
+
disk: "platform-preferences",
|
|
278
|
+
secure: nativeSecureBackend,
|
|
279
|
+
},
|
|
280
|
+
writeBuffering: {
|
|
281
|
+
disk: true,
|
|
282
|
+
secure: true,
|
|
283
|
+
},
|
|
284
|
+
errorClassification: true,
|
|
285
|
+
}),
|
|
286
|
+
getSecurityCapabilities: (): SecurityCapabilities => ({
|
|
287
|
+
platform: "native",
|
|
288
|
+
secureStorage: {
|
|
289
|
+
backend: nativeSecureBackend,
|
|
290
|
+
encrypted: "available",
|
|
291
|
+
accessControl: "unknown",
|
|
292
|
+
keychainAccessGroup: "unknown",
|
|
293
|
+
hardwareBacked: "unknown",
|
|
294
|
+
},
|
|
295
|
+
biometric: {
|
|
296
|
+
storage: "unknown",
|
|
297
|
+
prompt: "unknown",
|
|
298
|
+
biometryOnly: "unknown",
|
|
299
|
+
biometryOrPasscode: "unknown",
|
|
300
|
+
},
|
|
301
|
+
metadata: {
|
|
302
|
+
perKey: true,
|
|
303
|
+
listsWithoutValues: true,
|
|
304
|
+
persistsTimestamps: false,
|
|
305
|
+
},
|
|
306
|
+
}),
|
|
307
|
+
};
|
|
541
308
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
309
|
+
export const createStorageItem = core.createStorageItem;
|
|
310
|
+
export const getBatch = core.getBatch;
|
|
311
|
+
export const setBatch = core.setBatch;
|
|
312
|
+
export const removeBatch = core.removeBatch;
|
|
313
|
+
export const registerMigration = core.registerMigration;
|
|
314
|
+
export const migrateToLatest = core.migrateToLatest;
|
|
315
|
+
export const runTransaction = core.runTransaction;
|
|
316
|
+
export const createSecureAuthStorage = core.createSecureAuthStorage;
|
|
545
317
|
|
|
546
|
-
function
|
|
547
|
-
|
|
318
|
+
export function setWebSecureStorageBackend(
|
|
319
|
+
_backend?: WebSecureStorageBackend,
|
|
320
|
+
): void {
|
|
321
|
+
// Native platforms do not use web secure backends.
|
|
548
322
|
}
|
|
549
323
|
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
return;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
const writes = Array.from(pendingDiskWrites.values());
|
|
558
|
-
pendingDiskWrites.clear();
|
|
559
|
-
|
|
560
|
-
const keysToSet: string[] = [];
|
|
561
|
-
const valuesToSet: string[] = [];
|
|
562
|
-
const keysToRemove: string[] = [];
|
|
563
|
-
|
|
564
|
-
writes.forEach(({ key, value }) => {
|
|
565
|
-
if (value === undefined) {
|
|
566
|
-
keysToRemove.push(key);
|
|
567
|
-
return;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
keysToSet.push(key);
|
|
571
|
-
valuesToSet.push(value);
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
const storageModule = getStorageModule();
|
|
575
|
-
if (keysToSet.length > 0) {
|
|
576
|
-
storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
577
|
-
}
|
|
578
|
-
if (keysToRemove.length > 0) {
|
|
579
|
-
storageModule.removeBatch(keysToRemove, StorageScope.Disk);
|
|
580
|
-
}
|
|
324
|
+
export function getWebSecureStorageBackend():
|
|
325
|
+
| WebSecureStorageBackend
|
|
326
|
+
| undefined {
|
|
327
|
+
return undefined;
|
|
581
328
|
}
|
|
582
329
|
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
const writes = Array.from(pendingSecureWrites.values());
|
|
591
|
-
pendingSecureWrites.clear();
|
|
592
|
-
|
|
593
|
-
const groupedSetWrites = new Map<
|
|
594
|
-
AccessControl,
|
|
595
|
-
{ keys: string[]; values: string[] }
|
|
596
|
-
>();
|
|
597
|
-
const keysToRemove: string[] = [];
|
|
598
|
-
|
|
599
|
-
writes.forEach(({ key, value, accessControl }) => {
|
|
600
|
-
if (value === undefined) {
|
|
601
|
-
keysToRemove.push(key);
|
|
602
|
-
} else {
|
|
603
|
-
const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
|
|
604
|
-
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
605
|
-
const group = existingGroup ?? { keys: [], values: [] };
|
|
606
|
-
group.keys.push(key);
|
|
607
|
-
group.values.push(value);
|
|
608
|
-
if (!existingGroup) {
|
|
609
|
-
groupedSetWrites.set(resolvedAccessControl, group);
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
const storageModule = getStorageModule();
|
|
615
|
-
groupedSetWrites.forEach((group, accessControl) => {
|
|
616
|
-
storageModule.setSecureAccessControl(accessControl);
|
|
617
|
-
storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
618
|
-
});
|
|
619
|
-
if (keysToRemove.length > 0) {
|
|
620
|
-
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
621
|
-
}
|
|
330
|
+
export function setWebDiskStorageBackend(
|
|
331
|
+
_backend?: WebDiskStorageBackend,
|
|
332
|
+
): void {
|
|
333
|
+
// Native platforms do not use web disk backends.
|
|
622
334
|
}
|
|
623
335
|
|
|
624
|
-
function
|
|
625
|
-
|
|
626
|
-
if (diskFlushScheduled) {
|
|
627
|
-
return;
|
|
628
|
-
}
|
|
629
|
-
diskFlushScheduled = true;
|
|
630
|
-
runMicrotask(flushDiskWrites);
|
|
336
|
+
export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
|
|
337
|
+
return undefined;
|
|
631
338
|
}
|
|
632
339
|
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
value: string | undefined,
|
|
636
|
-
accessControl?: AccessControl,
|
|
637
|
-
): void {
|
|
638
|
-
const pendingWrite: PendingSecureWrite = { key, value };
|
|
639
|
-
if (accessControl !== undefined) {
|
|
640
|
-
pendingWrite.accessControl = accessControl;
|
|
641
|
-
}
|
|
642
|
-
pendingSecureWrites.set(key, pendingWrite);
|
|
643
|
-
if (secureFlushScheduled) {
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
secureFlushScheduled = true;
|
|
647
|
-
runMicrotask(flushSecureWrites);
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function ensureNativeScopeSubscription(scope: NonMemoryScope): void {
|
|
651
|
-
if (scopedUnsubscribers.has(scope)) {
|
|
652
|
-
return;
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
|
|
656
|
-
if (scope === StorageScope.Disk) {
|
|
657
|
-
if (key === "") {
|
|
658
|
-
pendingDiskWrites.clear();
|
|
659
|
-
} else {
|
|
660
|
-
clearPendingDiskWrite(key);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
if (scope === StorageScope.Secure) {
|
|
665
|
-
if (key === "") {
|
|
666
|
-
pendingSecureWrites.clear();
|
|
667
|
-
} else {
|
|
668
|
-
clearPendingSecureWrite(key);
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
if (key === "") {
|
|
673
|
-
clearScopeRawCache(scope);
|
|
674
|
-
notifyAllListeners(getScopedListeners(scope));
|
|
675
|
-
return;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
const oldValue = readCachedRawValue(scope, key);
|
|
679
|
-
cacheRawValue(scope, key, value);
|
|
680
|
-
notifyKeyListeners(getScopedListeners(scope), key);
|
|
681
|
-
if (consumeSuppressedNativeEvent(scope, key)) {
|
|
682
|
-
return;
|
|
683
|
-
}
|
|
684
|
-
emitKeyChange(scope, key, oldValue, value, "external", "native");
|
|
685
|
-
});
|
|
686
|
-
scopedUnsubscribers.set(
|
|
687
|
-
scope,
|
|
688
|
-
typeof unsubscribe === "function" ? unsubscribe : () => {},
|
|
689
|
-
);
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
function maybeCleanupNativeScopeSubscription(scope: NonMemoryScope): void {
|
|
693
|
-
const listeners = getScopedListeners(scope);
|
|
694
|
-
if (
|
|
695
|
-
listeners.size > 0 ||
|
|
696
|
-
storageEvents.hasListeners(scope) ||
|
|
697
|
-
eventObserver !== undefined
|
|
698
|
-
) {
|
|
699
|
-
return;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
const unsubscribe = scopedUnsubscribers.get(scope);
|
|
703
|
-
if (!unsubscribe) {
|
|
704
|
-
return;
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
unsubscribe();
|
|
708
|
-
scopedUnsubscribers.delete(scope);
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
712
|
-
assertValidScope(scope);
|
|
713
|
-
if (scope === StorageScope.Memory) {
|
|
714
|
-
const value = memoryStore.get(key);
|
|
715
|
-
return typeof value === "string" ? value : undefined;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
|
|
719
|
-
return readPendingDiskWrite(key);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
723
|
-
return readPendingSecureWrite(key);
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
return getStorageModule().get(key, scope);
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
730
|
-
assertValidScope(scope);
|
|
731
|
-
const oldValue =
|
|
732
|
-
scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
|
|
733
|
-
if (scope === StorageScope.Memory) {
|
|
734
|
-
memoryStore.set(key, value);
|
|
735
|
-
notifyKeyListeners(memoryListeners, key);
|
|
736
|
-
emitKeyChange(scope, key, oldValue, value, "set", "memory");
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (scope === StorageScope.Disk) {
|
|
741
|
-
cacheRawValue(scope, key, value);
|
|
742
|
-
if (diskWritesAsync) {
|
|
743
|
-
scheduleDiskWrite(key, value);
|
|
744
|
-
emitKeyChange(scope, key, oldValue, value, "set", "native");
|
|
745
|
-
return;
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
flushDiskWrites();
|
|
749
|
-
clearPendingDiskWrite(key);
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (scope === StorageScope.Secure) {
|
|
753
|
-
flushSecureWrites();
|
|
754
|
-
clearPendingSecureWrite(key);
|
|
755
|
-
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
getStorageModule().set(key, value, scope);
|
|
759
|
-
cacheRawValue(scope, key, value);
|
|
760
|
-
emitKeyChange(scope, key, oldValue, value, "set", "native");
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function removeRawValue(key: string, scope: StorageScope): void {
|
|
764
|
-
assertValidScope(scope);
|
|
765
|
-
const oldValue = getEventRawValue(scope, key);
|
|
766
|
-
if (scope === StorageScope.Memory) {
|
|
767
|
-
memoryStore.delete(key);
|
|
768
|
-
notifyKeyListeners(memoryListeners, key);
|
|
769
|
-
emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
|
|
770
|
-
return;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
if (scope === StorageScope.Disk) {
|
|
774
|
-
cacheRawValue(scope, key, undefined);
|
|
775
|
-
if (diskWritesAsync) {
|
|
776
|
-
scheduleDiskWrite(key, undefined);
|
|
777
|
-
emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
flushDiskWrites();
|
|
782
|
-
clearPendingDiskWrite(key);
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
if (scope === StorageScope.Secure) {
|
|
786
|
-
flushSecureWrites();
|
|
787
|
-
clearPendingSecureWrite(key);
|
|
788
|
-
}
|
|
789
|
-
|
|
790
|
-
getStorageModule().remove(key, scope);
|
|
791
|
-
cacheRawValue(scope, key, undefined);
|
|
792
|
-
emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
function readMigrationVersion(scope: StorageScope): number {
|
|
796
|
-
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
797
|
-
if (raw === undefined) {
|
|
798
|
-
return 0;
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
const parsed = Number.parseInt(raw, 10);
|
|
802
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
806
|
-
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
807
|
-
}
|
|
808
|
-
|
|
809
|
-
export const storage = {
|
|
810
|
-
subscribe: (
|
|
811
|
-
scope: StorageScope,
|
|
812
|
-
listener: StorageEventListener,
|
|
813
|
-
): (() => void) => {
|
|
814
|
-
assertValidScope(scope);
|
|
815
|
-
if (scope !== StorageScope.Memory) {
|
|
816
|
-
ensureNativeScopeSubscription(scope);
|
|
817
|
-
const unsubscribe = storageEvents.subscribe(scope, listener);
|
|
818
|
-
return () => {
|
|
819
|
-
unsubscribe();
|
|
820
|
-
maybeCleanupNativeScopeSubscription(scope);
|
|
821
|
-
};
|
|
822
|
-
}
|
|
823
|
-
return storageEvents.subscribe(scope, listener);
|
|
824
|
-
},
|
|
825
|
-
subscribeKey: (
|
|
826
|
-
scope: StorageScope,
|
|
827
|
-
key: string,
|
|
828
|
-
listener: StorageEventListener,
|
|
829
|
-
): (() => void) => {
|
|
830
|
-
assertValidScope(scope);
|
|
831
|
-
if (scope !== StorageScope.Memory) {
|
|
832
|
-
ensureNativeScopeSubscription(scope);
|
|
833
|
-
const unsubscribe = storageEvents.subscribeKey(scope, key, listener);
|
|
834
|
-
return () => {
|
|
835
|
-
unsubscribe();
|
|
836
|
-
maybeCleanupNativeScopeSubscription(scope);
|
|
837
|
-
};
|
|
838
|
-
}
|
|
839
|
-
return storageEvents.subscribeKey(scope, key, listener);
|
|
840
|
-
},
|
|
841
|
-
subscribePrefix: (
|
|
842
|
-
scope: StorageScope,
|
|
843
|
-
prefix: string,
|
|
844
|
-
listener: StorageEventListener,
|
|
845
|
-
): (() => void) => {
|
|
846
|
-
assertValidScope(scope);
|
|
847
|
-
if (scope !== StorageScope.Memory) {
|
|
848
|
-
ensureNativeScopeSubscription(scope);
|
|
849
|
-
const unsubscribe = storageEvents.subscribePrefix(
|
|
850
|
-
scope,
|
|
851
|
-
prefix,
|
|
852
|
-
listener,
|
|
853
|
-
);
|
|
854
|
-
return () => {
|
|
855
|
-
unsubscribe();
|
|
856
|
-
maybeCleanupNativeScopeSubscription(scope);
|
|
857
|
-
};
|
|
858
|
-
}
|
|
859
|
-
return storageEvents.subscribePrefix(scope, prefix, listener);
|
|
860
|
-
},
|
|
861
|
-
subscribeNamespace: (
|
|
862
|
-
namespace: string,
|
|
863
|
-
scope: StorageScope,
|
|
864
|
-
listener: StorageEventListener,
|
|
865
|
-
): (() => void) => {
|
|
866
|
-
return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
|
|
867
|
-
},
|
|
868
|
-
setEventObserver: (
|
|
869
|
-
observer?: StorageEventListener,
|
|
870
|
-
options: StorageEventObserverOptions = {},
|
|
871
|
-
) => {
|
|
872
|
-
eventObserver = observer;
|
|
873
|
-
eventObserverRedactSecureValues = options.redactSecureValues !== false;
|
|
874
|
-
if (observer) {
|
|
875
|
-
ensureNativeScopeSubscription(StorageScope.Disk);
|
|
876
|
-
ensureNativeScopeSubscription(StorageScope.Secure);
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
maybeCleanupNativeScopeSubscription(StorageScope.Disk);
|
|
880
|
-
maybeCleanupNativeScopeSubscription(StorageScope.Secure);
|
|
881
|
-
},
|
|
882
|
-
clear: (scope: StorageScope) => {
|
|
883
|
-
measureOperation("storage:clear", scope, () => {
|
|
884
|
-
const previousValues = shouldReadPreviousEventValues(scope)
|
|
885
|
-
? storage.getAll(scope)
|
|
886
|
-
: {};
|
|
887
|
-
if (scope === StorageScope.Memory) {
|
|
888
|
-
memoryStore.clear();
|
|
889
|
-
notifyAllListeners(memoryListeners);
|
|
890
|
-
emitBatchChange(
|
|
891
|
-
scope,
|
|
892
|
-
"clear",
|
|
893
|
-
"memory",
|
|
894
|
-
Object.keys(previousValues).map((key) =>
|
|
895
|
-
createKeyChange(
|
|
896
|
-
scope,
|
|
897
|
-
key,
|
|
898
|
-
previousValues[key],
|
|
899
|
-
undefined,
|
|
900
|
-
"clear",
|
|
901
|
-
"memory",
|
|
902
|
-
),
|
|
903
|
-
),
|
|
904
|
-
);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
|
|
908
|
-
if (scope === StorageScope.Disk) {
|
|
909
|
-
flushDiskWrites();
|
|
910
|
-
pendingDiskWrites.clear();
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
if (scope === StorageScope.Secure) {
|
|
914
|
-
flushSecureWrites();
|
|
915
|
-
pendingSecureWrites.clear();
|
|
916
|
-
}
|
|
917
|
-
|
|
918
|
-
clearScopeRawCache(scope);
|
|
919
|
-
getStorageModule().clear(scope);
|
|
920
|
-
emitBatchChange(
|
|
921
|
-
scope,
|
|
922
|
-
"clear",
|
|
923
|
-
"native",
|
|
924
|
-
Object.keys(previousValues).map((key) =>
|
|
925
|
-
createKeyChange(
|
|
926
|
-
scope,
|
|
927
|
-
key,
|
|
928
|
-
previousValues[key],
|
|
929
|
-
undefined,
|
|
930
|
-
"clear",
|
|
931
|
-
"native",
|
|
932
|
-
),
|
|
933
|
-
),
|
|
934
|
-
);
|
|
935
|
-
});
|
|
936
|
-
},
|
|
937
|
-
clearAll: () => {
|
|
938
|
-
measureOperation(
|
|
939
|
-
"storage:clearAll",
|
|
940
|
-
StorageScope.Memory,
|
|
941
|
-
() => {
|
|
942
|
-
storage.clear(StorageScope.Memory);
|
|
943
|
-
storage.clear(StorageScope.Disk);
|
|
944
|
-
storage.clear(StorageScope.Secure);
|
|
945
|
-
},
|
|
946
|
-
3,
|
|
947
|
-
);
|
|
948
|
-
},
|
|
949
|
-
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
950
|
-
measureOperation("storage:clearNamespace", scope, () => {
|
|
951
|
-
assertValidScope(scope);
|
|
952
|
-
if (scope === StorageScope.Memory) {
|
|
953
|
-
const affectedKeys = Array.from(memoryStore.keys()).filter((key) =>
|
|
954
|
-
isNamespaced(key, namespace),
|
|
955
|
-
);
|
|
956
|
-
const previousValues = affectedKeys.map((key) => ({
|
|
957
|
-
key,
|
|
958
|
-
value: getEventRawValue(scope, key),
|
|
959
|
-
}));
|
|
960
|
-
|
|
961
|
-
if (affectedKeys.length === 0) {
|
|
962
|
-
return;
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
affectedKeys.forEach((key) => {
|
|
966
|
-
memoryStore.delete(key);
|
|
967
|
-
});
|
|
968
|
-
affectedKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
969
|
-
emitBatchChange(
|
|
970
|
-
scope,
|
|
971
|
-
"clearNamespace",
|
|
972
|
-
"memory",
|
|
973
|
-
previousValues.map(({ key, value }) =>
|
|
974
|
-
createKeyChange(
|
|
975
|
-
scope,
|
|
976
|
-
key,
|
|
977
|
-
value,
|
|
978
|
-
undefined,
|
|
979
|
-
"clearNamespace",
|
|
980
|
-
"memory",
|
|
981
|
-
),
|
|
982
|
-
),
|
|
983
|
-
);
|
|
984
|
-
return;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
const keyPrefix = prefixKey(namespace, "");
|
|
988
|
-
const previousValues = shouldReadPreviousEventValues(scope)
|
|
989
|
-
? storage.getByPrefix(keyPrefix, scope)
|
|
990
|
-
: {};
|
|
991
|
-
if (scope === StorageScope.Disk) {
|
|
992
|
-
flushDiskWrites();
|
|
993
|
-
}
|
|
994
|
-
if (scope === StorageScope.Secure) {
|
|
995
|
-
flushSecureWrites();
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
const scopeCache = getScopeRawCache(scope);
|
|
999
|
-
for (const key of scopeCache.keys()) {
|
|
1000
|
-
if (isNamespaced(key, namespace)) {
|
|
1001
|
-
scopeCache.delete(key);
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
getStorageModule().removeByPrefix(keyPrefix, scope);
|
|
1005
|
-
emitBatchChange(
|
|
1006
|
-
scope,
|
|
1007
|
-
"clearNamespace",
|
|
1008
|
-
"native",
|
|
1009
|
-
Object.keys(previousValues).map((key) =>
|
|
1010
|
-
createKeyChange(
|
|
1011
|
-
scope,
|
|
1012
|
-
key,
|
|
1013
|
-
previousValues[key],
|
|
1014
|
-
undefined,
|
|
1015
|
-
"clearNamespace",
|
|
1016
|
-
"native",
|
|
1017
|
-
),
|
|
1018
|
-
),
|
|
1019
|
-
);
|
|
1020
|
-
});
|
|
1021
|
-
},
|
|
1022
|
-
clearBiometric: () => {
|
|
1023
|
-
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
1024
|
-
getStorageModule().clearSecureBiometric();
|
|
1025
|
-
});
|
|
1026
|
-
},
|
|
1027
|
-
has: (key: string, scope: StorageScope): boolean => {
|
|
1028
|
-
return measureOperation("storage:has", scope, () => {
|
|
1029
|
-
assertValidScope(scope);
|
|
1030
|
-
if (scope === StorageScope.Memory) {
|
|
1031
|
-
return memoryStore.has(key);
|
|
1032
|
-
}
|
|
1033
|
-
if (scope === StorageScope.Disk) {
|
|
1034
|
-
flushDiskWrites();
|
|
1035
|
-
}
|
|
1036
|
-
if (scope === StorageScope.Secure) {
|
|
1037
|
-
flushSecureWrites();
|
|
1038
|
-
}
|
|
1039
|
-
return getStorageModule().has(key, scope);
|
|
1040
|
-
});
|
|
1041
|
-
},
|
|
1042
|
-
getAllKeys: (scope: StorageScope): string[] => {
|
|
1043
|
-
return measureOperation("storage:getAllKeys", scope, () => {
|
|
1044
|
-
assertValidScope(scope);
|
|
1045
|
-
if (scope === StorageScope.Memory) {
|
|
1046
|
-
return Array.from(memoryStore.keys());
|
|
1047
|
-
}
|
|
1048
|
-
if (scope === StorageScope.Disk) {
|
|
1049
|
-
flushDiskWrites();
|
|
1050
|
-
}
|
|
1051
|
-
if (scope === StorageScope.Secure) {
|
|
1052
|
-
flushSecureWrites();
|
|
1053
|
-
}
|
|
1054
|
-
return getStorageModule().getAllKeys(scope);
|
|
1055
|
-
});
|
|
1056
|
-
},
|
|
1057
|
-
getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
|
|
1058
|
-
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
1059
|
-
assertValidScope(scope);
|
|
1060
|
-
if (scope === StorageScope.Memory) {
|
|
1061
|
-
return Array.from(memoryStore.keys()).filter((key) =>
|
|
1062
|
-
key.startsWith(prefix),
|
|
1063
|
-
);
|
|
1064
|
-
}
|
|
1065
|
-
if (scope === StorageScope.Disk) {
|
|
1066
|
-
flushDiskWrites();
|
|
1067
|
-
}
|
|
1068
|
-
if (scope === StorageScope.Secure) {
|
|
1069
|
-
flushSecureWrites();
|
|
1070
|
-
}
|
|
1071
|
-
return getStorageModule().getKeysByPrefix(prefix, scope) ?? [];
|
|
1072
|
-
});
|
|
1073
|
-
},
|
|
1074
|
-
getByPrefix: (
|
|
1075
|
-
prefix: string,
|
|
1076
|
-
scope: StorageScope,
|
|
1077
|
-
): Record<string, string> => {
|
|
1078
|
-
return measureOperation("storage:getByPrefix", scope, () => {
|
|
1079
|
-
const result: Record<string, string> = {};
|
|
1080
|
-
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
1081
|
-
if (keys.length === 0) {
|
|
1082
|
-
return result;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
if (scope === StorageScope.Memory) {
|
|
1086
|
-
keys.forEach((key) => {
|
|
1087
|
-
const value = memoryStore.get(key);
|
|
1088
|
-
if (typeof value === "string") {
|
|
1089
|
-
result[key] = value;
|
|
1090
|
-
}
|
|
1091
|
-
});
|
|
1092
|
-
return result;
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
if (scope === StorageScope.Disk) {
|
|
1096
|
-
flushDiskWrites();
|
|
1097
|
-
}
|
|
1098
|
-
if (scope === StorageScope.Secure) {
|
|
1099
|
-
flushSecureWrites();
|
|
1100
|
-
}
|
|
1101
|
-
const values = getStorageModule().getBatch(keys, scope) ?? [];
|
|
1102
|
-
keys.forEach((key, idx) => {
|
|
1103
|
-
const value = decodeNativeBatchValue(values[idx]);
|
|
1104
|
-
if (value !== undefined) {
|
|
1105
|
-
result[key] = value;
|
|
1106
|
-
}
|
|
1107
|
-
});
|
|
1108
|
-
return result;
|
|
1109
|
-
});
|
|
1110
|
-
},
|
|
1111
|
-
getAll: (scope: StorageScope): Record<string, string> => {
|
|
1112
|
-
return measureOperation("storage:getAll", scope, () => {
|
|
1113
|
-
assertValidScope(scope);
|
|
1114
|
-
const result: Record<string, string> = {};
|
|
1115
|
-
if (scope === StorageScope.Memory) {
|
|
1116
|
-
for (const key of memoryStore.keys()) {
|
|
1117
|
-
const value = memoryStore.get(key);
|
|
1118
|
-
if (typeof value === "string") result[key] = value;
|
|
1119
|
-
}
|
|
1120
|
-
return result;
|
|
1121
|
-
}
|
|
1122
|
-
if (scope === StorageScope.Disk) {
|
|
1123
|
-
flushDiskWrites();
|
|
1124
|
-
}
|
|
1125
|
-
if (scope === StorageScope.Secure) {
|
|
1126
|
-
flushSecureWrites();
|
|
1127
|
-
}
|
|
1128
|
-
const keys = getStorageModule().getAllKeys(scope) ?? [];
|
|
1129
|
-
if (keys.length === 0) return result;
|
|
1130
|
-
const values = getStorageModule().getBatch(keys, scope) ?? [];
|
|
1131
|
-
keys.forEach((key, idx) => {
|
|
1132
|
-
const val = decodeNativeBatchValue(values[idx]);
|
|
1133
|
-
if (val !== undefined) result[key] = val;
|
|
1134
|
-
});
|
|
1135
|
-
return result;
|
|
1136
|
-
});
|
|
1137
|
-
},
|
|
1138
|
-
export: (
|
|
1139
|
-
scope: StorageScope,
|
|
1140
|
-
options: StorageExportOptions = {},
|
|
1141
|
-
): Record<string, string> => {
|
|
1142
|
-
if (scope === StorageScope.Secure && options.includeSecureValues !== true) {
|
|
1143
|
-
throw new Error(
|
|
1144
|
-
"NitroStorage: exporting Secure scope exposes raw secret values. Pass { includeSecureValues: true } or use exportSecureUnsafe().",
|
|
1145
|
-
);
|
|
1146
|
-
}
|
|
1147
|
-
return measureOperation("storage:export", scope, () =>
|
|
1148
|
-
storage.getAll(scope),
|
|
1149
|
-
);
|
|
1150
|
-
},
|
|
1151
|
-
exportSecureUnsafe: (): Record<string, string> => {
|
|
1152
|
-
return measureOperation(
|
|
1153
|
-
"storage:exportSecureUnsafe",
|
|
1154
|
-
StorageScope.Secure,
|
|
1155
|
-
() => storage.getAll(StorageScope.Secure),
|
|
1156
|
-
);
|
|
1157
|
-
},
|
|
1158
|
-
size: (scope: StorageScope): number => {
|
|
1159
|
-
return measureOperation("storage:size", scope, () => {
|
|
1160
|
-
assertValidScope(scope);
|
|
1161
|
-
if (scope === StorageScope.Memory) {
|
|
1162
|
-
return memoryStore.size;
|
|
1163
|
-
}
|
|
1164
|
-
if (scope === StorageScope.Disk) {
|
|
1165
|
-
flushDiskWrites();
|
|
1166
|
-
}
|
|
1167
|
-
if (scope === StorageScope.Secure) {
|
|
1168
|
-
flushSecureWrites();
|
|
1169
|
-
}
|
|
1170
|
-
return getStorageModule().size(scope);
|
|
1171
|
-
});
|
|
1172
|
-
},
|
|
1173
|
-
setAccessControl: (level: AccessControl) => {
|
|
1174
|
-
measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
|
|
1175
|
-
assertAccessControlLevel(level);
|
|
1176
|
-
secureDefaultAccessControl = level;
|
|
1177
|
-
getStorageModule().setSecureAccessControl(level);
|
|
1178
|
-
});
|
|
1179
|
-
},
|
|
1180
|
-
setSecureWritesAsync: (enabled: boolean) => {
|
|
1181
|
-
measureOperation(
|
|
1182
|
-
"storage:setSecureWritesAsync",
|
|
1183
|
-
StorageScope.Secure,
|
|
1184
|
-
() => {
|
|
1185
|
-
getStorageModule().setSecureWritesAsync(enabled);
|
|
1186
|
-
},
|
|
1187
|
-
);
|
|
1188
|
-
},
|
|
1189
|
-
setDiskWritesAsync: (enabled: boolean) => {
|
|
1190
|
-
measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
|
|
1191
|
-
diskWritesAsync = enabled;
|
|
1192
|
-
if (!enabled) {
|
|
1193
|
-
flushDiskWrites();
|
|
1194
|
-
}
|
|
1195
|
-
});
|
|
1196
|
-
},
|
|
1197
|
-
flushDiskWrites: () => {
|
|
1198
|
-
measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
|
|
1199
|
-
flushDiskWrites();
|
|
1200
|
-
});
|
|
1201
|
-
},
|
|
1202
|
-
flushSecureWrites: () => {
|
|
1203
|
-
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
1204
|
-
flushSecureWrites();
|
|
1205
|
-
});
|
|
1206
|
-
},
|
|
1207
|
-
setKeychainAccessGroup: (group: string) => {
|
|
1208
|
-
measureOperation(
|
|
1209
|
-
"storage:setKeychainAccessGroup",
|
|
1210
|
-
StorageScope.Secure,
|
|
1211
|
-
() => {
|
|
1212
|
-
getStorageModule().setKeychainAccessGroup(group);
|
|
1213
|
-
},
|
|
1214
|
-
);
|
|
1215
|
-
},
|
|
1216
|
-
setMetricsObserver: (observer?: StorageMetricsObserver) => {
|
|
1217
|
-
metricsObserver = observer;
|
|
1218
|
-
},
|
|
1219
|
-
getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
|
|
1220
|
-
const snapshot: Record<string, StorageMetricSummary> = {};
|
|
1221
|
-
metricsCounters.forEach((value, key) => {
|
|
1222
|
-
snapshot[key] = {
|
|
1223
|
-
count: value.count,
|
|
1224
|
-
totalDurationMs: value.totalDurationMs,
|
|
1225
|
-
avgDurationMs:
|
|
1226
|
-
value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
1227
|
-
maxDurationMs: value.maxDurationMs,
|
|
1228
|
-
};
|
|
1229
|
-
});
|
|
1230
|
-
return snapshot;
|
|
1231
|
-
},
|
|
1232
|
-
resetMetrics: () => {
|
|
1233
|
-
metricsCounters.clear();
|
|
1234
|
-
},
|
|
1235
|
-
getCapabilities: (): StorageCapabilities => ({
|
|
1236
|
-
platform: "native",
|
|
1237
|
-
backend: {
|
|
1238
|
-
disk: "platform-preferences",
|
|
1239
|
-
secure: nativeSecureBackend,
|
|
1240
|
-
},
|
|
1241
|
-
writeBuffering: {
|
|
1242
|
-
disk: true,
|
|
1243
|
-
secure: true,
|
|
1244
|
-
},
|
|
1245
|
-
errorClassification: true,
|
|
1246
|
-
}),
|
|
1247
|
-
getSecurityCapabilities: (): SecurityCapabilities => ({
|
|
1248
|
-
platform: "native",
|
|
1249
|
-
secureStorage: {
|
|
1250
|
-
backend: nativeSecureBackend,
|
|
1251
|
-
encrypted: "available",
|
|
1252
|
-
accessControl: "unknown",
|
|
1253
|
-
keychainAccessGroup: "unknown",
|
|
1254
|
-
hardwareBacked: "unknown",
|
|
1255
|
-
},
|
|
1256
|
-
biometric: {
|
|
1257
|
-
storage: "unknown",
|
|
1258
|
-
prompt: "unknown",
|
|
1259
|
-
biometryOnly: "unknown",
|
|
1260
|
-
biometryOrPasscode: "unknown",
|
|
1261
|
-
},
|
|
1262
|
-
metadata: {
|
|
1263
|
-
perKey: true,
|
|
1264
|
-
listsWithoutValues: true,
|
|
1265
|
-
persistsTimestamps: false,
|
|
1266
|
-
},
|
|
1267
|
-
}),
|
|
1268
|
-
getSecureMetadata: (key: string): SecureStorageMetadata => {
|
|
1269
|
-
return measureOperation(
|
|
1270
|
-
"storage:getSecureMetadata",
|
|
1271
|
-
StorageScope.Secure,
|
|
1272
|
-
() => {
|
|
1273
|
-
flushSecureWrites();
|
|
1274
|
-
const storageModule = getStorageModule();
|
|
1275
|
-
const biometricProtected = storageModule.hasSecureBiometric(key);
|
|
1276
|
-
const exists =
|
|
1277
|
-
biometricProtected || storageModule.has(key, StorageScope.Secure);
|
|
1278
|
-
let kind: SecureStorageMetadata["kind"] = "missing";
|
|
1279
|
-
if (exists) {
|
|
1280
|
-
kind = biometricProtected ? "biometric" : "secure";
|
|
1281
|
-
}
|
|
1282
|
-
|
|
1283
|
-
return {
|
|
1284
|
-
key,
|
|
1285
|
-
exists,
|
|
1286
|
-
kind,
|
|
1287
|
-
backend: nativeSecureBackend,
|
|
1288
|
-
encrypted: "available",
|
|
1289
|
-
hardwareBacked: "unknown",
|
|
1290
|
-
biometricProtected,
|
|
1291
|
-
valueExposed: false,
|
|
1292
|
-
};
|
|
1293
|
-
},
|
|
1294
|
-
);
|
|
1295
|
-
},
|
|
1296
|
-
getAllSecureMetadata: (): SecureStorageMetadata[] => {
|
|
1297
|
-
return measureOperation(
|
|
1298
|
-
"storage:getAllSecureMetadata",
|
|
1299
|
-
StorageScope.Secure,
|
|
1300
|
-
() => {
|
|
1301
|
-
flushSecureWrites();
|
|
1302
|
-
return getStorageModule()
|
|
1303
|
-
.getAllKeys(StorageScope.Secure)
|
|
1304
|
-
.map((key) => storage.getSecureMetadata(key));
|
|
1305
|
-
},
|
|
1306
|
-
);
|
|
1307
|
-
},
|
|
1308
|
-
getString: (key: string, scope: StorageScope): string | undefined => {
|
|
1309
|
-
return measureOperation("storage:getString", scope, () => {
|
|
1310
|
-
return getRawValue(key, scope);
|
|
1311
|
-
});
|
|
1312
|
-
},
|
|
1313
|
-
setString: (key: string, value: string, scope: StorageScope): void => {
|
|
1314
|
-
measureOperation("storage:setString", scope, () => {
|
|
1315
|
-
setRawValue(key, value, scope);
|
|
1316
|
-
});
|
|
1317
|
-
},
|
|
1318
|
-
deleteString: (key: string, scope: StorageScope): void => {
|
|
1319
|
-
measureOperation("storage:deleteString", scope, () => {
|
|
1320
|
-
removeRawValue(key, scope);
|
|
1321
|
-
});
|
|
1322
|
-
},
|
|
1323
|
-
import: (data: Record<string, string>, scope: StorageScope): void => {
|
|
1324
|
-
const keys = Object.keys(data);
|
|
1325
|
-
measureOperation(
|
|
1326
|
-
"storage:import",
|
|
1327
|
-
scope,
|
|
1328
|
-
() => {
|
|
1329
|
-
assertValidScope(scope);
|
|
1330
|
-
if (keys.length === 0) return;
|
|
1331
|
-
const values = keys.map((k) => data[k]!);
|
|
1332
|
-
const changes = keys.map((key, index) =>
|
|
1333
|
-
createKeyChange(
|
|
1334
|
-
scope,
|
|
1335
|
-
key,
|
|
1336
|
-
getEventRawValue(scope, key),
|
|
1337
|
-
values[index],
|
|
1338
|
-
"import",
|
|
1339
|
-
scope === StorageScope.Memory ? "memory" : "native",
|
|
1340
|
-
),
|
|
1341
|
-
);
|
|
1342
|
-
|
|
1343
|
-
if (scope === StorageScope.Memory) {
|
|
1344
|
-
keys.forEach((key, index) => {
|
|
1345
|
-
memoryStore.set(key, values[index]);
|
|
1346
|
-
});
|
|
1347
|
-
keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
|
|
1348
|
-
emitBatchChange(scope, "import", "memory", changes);
|
|
1349
|
-
return;
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
|
-
if (scope === StorageScope.Secure) {
|
|
1353
|
-
flushSecureWrites();
|
|
1354
|
-
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
getStorageModule().setBatch(keys, values, scope);
|
|
1358
|
-
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1359
|
-
emitBatchChange(scope, "import", "native", changes);
|
|
1360
|
-
},
|
|
1361
|
-
keys.length,
|
|
1362
|
-
);
|
|
1363
|
-
},
|
|
1364
|
-
};
|
|
1365
|
-
|
|
1366
|
-
export function setWebSecureStorageBackend(
|
|
1367
|
-
_backend?: WebSecureStorageBackend,
|
|
1368
|
-
): void {
|
|
1369
|
-
// Native platforms do not use web secure backends.
|
|
1370
|
-
}
|
|
1371
|
-
|
|
1372
|
-
export function getWebSecureStorageBackend():
|
|
1373
|
-
| WebSecureStorageBackend
|
|
1374
|
-
| undefined {
|
|
1375
|
-
return undefined;
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
export function setWebDiskStorageBackend(
|
|
1379
|
-
_backend?: WebDiskStorageBackend,
|
|
1380
|
-
): void {
|
|
1381
|
-
// Native platforms do not use web disk backends.
|
|
1382
|
-
}
|
|
1383
|
-
|
|
1384
|
-
export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
|
|
1385
|
-
return undefined;
|
|
1386
|
-
}
|
|
1387
|
-
|
|
1388
|
-
export async function flushWebStorageBackends(): Promise<void> {
|
|
1389
|
-
// Native platforms do not use web storage backends.
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
export type StorageItemConfig<T> = {
|
|
1393
|
-
key: string;
|
|
1394
|
-
scope: StorageScope;
|
|
1395
|
-
defaultValue?: T;
|
|
1396
|
-
serialize?: (value: T) => string;
|
|
1397
|
-
deserialize?: (value: string) => T;
|
|
1398
|
-
validate?: Validator<T>;
|
|
1399
|
-
onValidationError?: (invalidValue: unknown) => T;
|
|
1400
|
-
expiration?: ExpirationConfig;
|
|
1401
|
-
onExpired?: (key: string) => void;
|
|
1402
|
-
readCache?: boolean;
|
|
1403
|
-
coalesceDiskWrites?: boolean;
|
|
1404
|
-
coalesceSecureWrites?: boolean;
|
|
1405
|
-
namespace?: string;
|
|
1406
|
-
biometric?: boolean;
|
|
1407
|
-
biometricLevel?: BiometricLevel;
|
|
1408
|
-
accessControl?: AccessControl;
|
|
1409
|
-
};
|
|
1410
|
-
|
|
1411
|
-
export type StorageItem<T> = {
|
|
1412
|
-
get: () => T;
|
|
1413
|
-
getWithVersion: () => VersionedValue<T>;
|
|
1414
|
-
set: StorageSetter<T>;
|
|
1415
|
-
setIfVersion: (
|
|
1416
|
-
version: StorageVersion,
|
|
1417
|
-
value: T | ((prev: T) => T),
|
|
1418
|
-
) => boolean;
|
|
1419
|
-
delete: () => void;
|
|
1420
|
-
has: () => boolean;
|
|
1421
|
-
subscribe: (callback: () => void) => () => void;
|
|
1422
|
-
subscribeSelector: <TSelected>(
|
|
1423
|
-
selector: (value: T) => TSelected,
|
|
1424
|
-
listener: StorageSelectorListener<TSelected>,
|
|
1425
|
-
options?: StorageSelectorSubscribeOptions<TSelected>,
|
|
1426
|
-
) => () => void;
|
|
1427
|
-
serialize: (value: T) => string;
|
|
1428
|
-
deserialize: (value: string) => T;
|
|
1429
|
-
scope: StorageScope;
|
|
1430
|
-
key: string;
|
|
1431
|
-
};
|
|
1432
|
-
|
|
1433
|
-
type StorageItemInternal<T> = StorageItem<T> & {
|
|
1434
|
-
_triggerListeners: () => void;
|
|
1435
|
-
_invalidateParsedCacheOnly: () => void;
|
|
1436
|
-
_hasValidation: boolean;
|
|
1437
|
-
_hasExpiration: boolean;
|
|
1438
|
-
_readCacheEnabled: boolean;
|
|
1439
|
-
_isBiometric: boolean;
|
|
1440
|
-
_biometricLevel: BiometricLevel;
|
|
1441
|
-
_defaultValue: T;
|
|
1442
|
-
_secureAccessControl?: AccessControl;
|
|
1443
|
-
};
|
|
1444
|
-
|
|
1445
|
-
function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
1446
|
-
return (
|
|
1447
|
-
item._hasExpiration === false &&
|
|
1448
|
-
item._hasValidation === false &&
|
|
1449
|
-
item._isBiometric !== true &&
|
|
1450
|
-
item._secureAccessControl === undefined
|
|
1451
|
-
);
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
|
|
1455
|
-
return (
|
|
1456
|
-
item._hasExpiration === false &&
|
|
1457
|
-
item._hasValidation === false &&
|
|
1458
|
-
item._isBiometric !== true
|
|
1459
|
-
);
|
|
1460
|
-
}
|
|
1461
|
-
|
|
1462
|
-
function defaultSerialize<T>(value: T): string {
|
|
1463
|
-
return serializeWithPrimitiveFastPath(value);
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
function defaultDeserialize<T>(value: string): T {
|
|
1467
|
-
return deserializeWithPrimitiveFastPath(value);
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
export function createStorageItem<T = undefined>(
|
|
1471
|
-
config: StorageItemConfig<T>,
|
|
1472
|
-
): StorageItem<T> {
|
|
1473
|
-
const storageKey = prefixKey(config.namespace, config.key);
|
|
1474
|
-
const serialize = config.serialize ?? defaultSerialize;
|
|
1475
|
-
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
1476
|
-
const isMemory = config.scope === StorageScope.Memory;
|
|
1477
|
-
const resolvedBiometricLevel =
|
|
1478
|
-
config.scope === StorageScope.Secure
|
|
1479
|
-
? (config.biometricLevel ??
|
|
1480
|
-
(config.biometric === true
|
|
1481
|
-
? BiometricLevel.BiometryOnly
|
|
1482
|
-
: BiometricLevel.None))
|
|
1483
|
-
: BiometricLevel.None;
|
|
1484
|
-
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
1485
|
-
const secureAccessControl = config.accessControl;
|
|
1486
|
-
const validate = config.validate;
|
|
1487
|
-
const onValidationError = config.onValidationError;
|
|
1488
|
-
const expiration = config.expiration;
|
|
1489
|
-
const onExpired = config.onExpired;
|
|
1490
|
-
const expirationTtlMs = expiration?.ttlMs;
|
|
1491
|
-
const memoryExpiration =
|
|
1492
|
-
expiration && isMemory ? new Map<string, number>() : null;
|
|
1493
|
-
const readCache = !isMemory && config.readCache === true;
|
|
1494
|
-
const coalesceDiskWrites =
|
|
1495
|
-
config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
1496
|
-
const coalesceSecureWrites =
|
|
1497
|
-
config.scope === StorageScope.Secure &&
|
|
1498
|
-
config.coalesceSecureWrites === true &&
|
|
1499
|
-
!isBiometric;
|
|
1500
|
-
const defaultValue = config.defaultValue as T;
|
|
1501
|
-
const nonMemoryScope: NonMemoryScope | null =
|
|
1502
|
-
config.scope === StorageScope.Disk
|
|
1503
|
-
? StorageScope.Disk
|
|
1504
|
-
: config.scope === StorageScope.Secure
|
|
1505
|
-
? StorageScope.Secure
|
|
1506
|
-
: null;
|
|
1507
|
-
|
|
1508
|
-
if (expiration && expiration.ttlMs <= 0) {
|
|
1509
|
-
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
1510
|
-
}
|
|
1511
|
-
if (config.scope === StorageScope.Secure) {
|
|
1512
|
-
assertBiometricLevel(resolvedBiometricLevel);
|
|
1513
|
-
if (secureAccessControl !== undefined) {
|
|
1514
|
-
assertAccessControlLevel(secureAccessControl);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
const listeners = new Set<() => void>();
|
|
1519
|
-
let unsubscribe: (() => void) | null = null;
|
|
1520
|
-
let lastRaw: unknown = undefined;
|
|
1521
|
-
let lastValue: T | undefined;
|
|
1522
|
-
let hasLastValue = false;
|
|
1523
|
-
let lastExpiresAt: number | null | undefined = undefined;
|
|
1524
|
-
|
|
1525
|
-
const invalidateParsedCache = () => {
|
|
1526
|
-
lastRaw = undefined;
|
|
1527
|
-
lastValue = undefined;
|
|
1528
|
-
hasLastValue = false;
|
|
1529
|
-
lastExpiresAt = undefined;
|
|
1530
|
-
};
|
|
1531
|
-
|
|
1532
|
-
const ensureSubscription = () => {
|
|
1533
|
-
if (unsubscribe) {
|
|
1534
|
-
return;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
const listener = () => {
|
|
1538
|
-
invalidateParsedCache();
|
|
1539
|
-
listeners.forEach((callback) => callback());
|
|
1540
|
-
};
|
|
1541
|
-
|
|
1542
|
-
if (isMemory) {
|
|
1543
|
-
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
|
|
1547
|
-
ensureNativeScopeSubscription(nonMemoryScope!);
|
|
1548
|
-
unsubscribe = addKeyListener(
|
|
1549
|
-
getScopedListeners(nonMemoryScope!),
|
|
1550
|
-
storageKey,
|
|
1551
|
-
listener,
|
|
1552
|
-
);
|
|
1553
|
-
};
|
|
1554
|
-
|
|
1555
|
-
const readStoredRaw = (): unknown => {
|
|
1556
|
-
if (isMemory) {
|
|
1557
|
-
if (memoryExpiration) {
|
|
1558
|
-
const expiresAt = memoryExpiration.get(storageKey);
|
|
1559
|
-
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
1560
|
-
memoryExpiration.delete(storageKey);
|
|
1561
|
-
memoryStore.delete(storageKey);
|
|
1562
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
1563
|
-
onExpired?.(storageKey);
|
|
1564
|
-
return undefined;
|
|
1565
|
-
}
|
|
1566
|
-
}
|
|
1567
|
-
return memoryStore.get(storageKey);
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
1571
|
-
const pending = pendingDiskWrites.get(storageKey);
|
|
1572
|
-
if (pending !== undefined) {
|
|
1573
|
-
return pending.value;
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
|
|
1577
|
-
if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
|
|
1578
|
-
const pending = pendingSecureWrites.get(storageKey);
|
|
1579
|
-
if (pending !== undefined) {
|
|
1580
|
-
return pending.value;
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
|
|
1584
|
-
if (readCache) {
|
|
1585
|
-
const cache = getScopeRawCache(nonMemoryScope!);
|
|
1586
|
-
const cached = cache.get(storageKey);
|
|
1587
|
-
if (cached !== undefined || cache.has(storageKey)) {
|
|
1588
|
-
return cached;
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
|
|
1592
|
-
if (isBiometric) {
|
|
1593
|
-
return getStorageModule().getSecureBiometric(storageKey);
|
|
1594
|
-
}
|
|
1595
|
-
|
|
1596
|
-
const raw = getStorageModule().get(storageKey, config.scope);
|
|
1597
|
-
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
1598
|
-
return raw;
|
|
1599
|
-
};
|
|
1600
|
-
|
|
1601
|
-
const writeStoredRaw = (rawValue: string): void => {
|
|
1602
|
-
const oldValue = undefined;
|
|
1603
|
-
if (isBiometric) {
|
|
1604
|
-
getStorageModule().setSecureBiometricWithLevel(
|
|
1605
|
-
storageKey,
|
|
1606
|
-
rawValue,
|
|
1607
|
-
resolvedBiometricLevel,
|
|
1608
|
-
);
|
|
1609
|
-
emitKeyChange(
|
|
1610
|
-
config.scope,
|
|
1611
|
-
storageKey,
|
|
1612
|
-
oldValue,
|
|
1613
|
-
rawValue,
|
|
1614
|
-
"set",
|
|
1615
|
-
"native",
|
|
1616
|
-
);
|
|
1617
|
-
return;
|
|
1618
|
-
}
|
|
1619
|
-
|
|
1620
|
-
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
1621
|
-
|
|
1622
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
1623
|
-
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1624
|
-
scheduleDiskWrite(storageKey, rawValue);
|
|
1625
|
-
emitKeyChange(
|
|
1626
|
-
config.scope,
|
|
1627
|
-
storageKey,
|
|
1628
|
-
oldValue,
|
|
1629
|
-
rawValue,
|
|
1630
|
-
"set",
|
|
1631
|
-
"native",
|
|
1632
|
-
);
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
|
|
1636
|
-
clearPendingDiskWrite(storageKey);
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
if (coalesceSecureWrites) {
|
|
1640
|
-
scheduleSecureWrite(
|
|
1641
|
-
storageKey,
|
|
1642
|
-
rawValue,
|
|
1643
|
-
secureAccessControl ?? secureDefaultAccessControl,
|
|
1644
|
-
);
|
|
1645
|
-
emitKeyChange(
|
|
1646
|
-
config.scope,
|
|
1647
|
-
storageKey,
|
|
1648
|
-
oldValue,
|
|
1649
|
-
rawValue,
|
|
1650
|
-
"set",
|
|
1651
|
-
"native",
|
|
1652
|
-
);
|
|
1653
|
-
return;
|
|
1654
|
-
}
|
|
1655
|
-
|
|
1656
|
-
if (nonMemoryScope === StorageScope.Secure) {
|
|
1657
|
-
clearPendingSecureWrite(storageKey);
|
|
1658
|
-
getStorageModule().setSecureAccessControl(
|
|
1659
|
-
secureAccessControl ?? secureDefaultAccessControl,
|
|
1660
|
-
);
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
getStorageModule().set(storageKey, rawValue, config.scope);
|
|
1664
|
-
emitKeyChange(
|
|
1665
|
-
config.scope,
|
|
1666
|
-
storageKey,
|
|
1667
|
-
oldValue,
|
|
1668
|
-
rawValue,
|
|
1669
|
-
"set",
|
|
1670
|
-
"native",
|
|
1671
|
-
);
|
|
1672
|
-
};
|
|
1673
|
-
|
|
1674
|
-
const removeStoredRaw = (): void => {
|
|
1675
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1676
|
-
if (isBiometric) {
|
|
1677
|
-
getStorageModule().deleteSecureBiometric(storageKey);
|
|
1678
|
-
emitKeyChange(
|
|
1679
|
-
config.scope,
|
|
1680
|
-
storageKey,
|
|
1681
|
-
oldValue,
|
|
1682
|
-
undefined,
|
|
1683
|
-
"remove",
|
|
1684
|
-
"native",
|
|
1685
|
-
);
|
|
1686
|
-
return;
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
1690
|
-
|
|
1691
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
1692
|
-
if (coalesceDiskWrites || diskWritesAsync) {
|
|
1693
|
-
scheduleDiskWrite(storageKey, undefined);
|
|
1694
|
-
emitKeyChange(
|
|
1695
|
-
config.scope,
|
|
1696
|
-
storageKey,
|
|
1697
|
-
oldValue,
|
|
1698
|
-
undefined,
|
|
1699
|
-
"remove",
|
|
1700
|
-
"native",
|
|
1701
|
-
);
|
|
1702
|
-
return;
|
|
1703
|
-
}
|
|
1704
|
-
|
|
1705
|
-
clearPendingDiskWrite(storageKey);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
if (coalesceSecureWrites) {
|
|
1709
|
-
scheduleSecureWrite(
|
|
1710
|
-
storageKey,
|
|
1711
|
-
undefined,
|
|
1712
|
-
secureAccessControl ?? secureDefaultAccessControl,
|
|
1713
|
-
);
|
|
1714
|
-
emitKeyChange(
|
|
1715
|
-
config.scope,
|
|
1716
|
-
storageKey,
|
|
1717
|
-
oldValue,
|
|
1718
|
-
undefined,
|
|
1719
|
-
"remove",
|
|
1720
|
-
"native",
|
|
1721
|
-
);
|
|
1722
|
-
return;
|
|
1723
|
-
}
|
|
1724
|
-
|
|
1725
|
-
if (nonMemoryScope === StorageScope.Secure) {
|
|
1726
|
-
clearPendingSecureWrite(storageKey);
|
|
1727
|
-
}
|
|
1728
|
-
|
|
1729
|
-
getStorageModule().remove(storageKey, config.scope);
|
|
1730
|
-
emitKeyChange(
|
|
1731
|
-
config.scope,
|
|
1732
|
-
storageKey,
|
|
1733
|
-
oldValue,
|
|
1734
|
-
undefined,
|
|
1735
|
-
"remove",
|
|
1736
|
-
"native",
|
|
1737
|
-
);
|
|
1738
|
-
};
|
|
1739
|
-
|
|
1740
|
-
const writeValueWithoutValidation = (value: T): void => {
|
|
1741
|
-
if (isMemory) {
|
|
1742
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1743
|
-
if (memoryExpiration) {
|
|
1744
|
-
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
1745
|
-
}
|
|
1746
|
-
memoryStore.set(storageKey, value);
|
|
1747
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
1748
|
-
emitKeyChange(
|
|
1749
|
-
config.scope,
|
|
1750
|
-
storageKey,
|
|
1751
|
-
oldValue,
|
|
1752
|
-
typeof value === "string" ? value : undefined,
|
|
1753
|
-
"set",
|
|
1754
|
-
"memory",
|
|
1755
|
-
);
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
|
|
1759
|
-
const serialized = serialize(value);
|
|
1760
|
-
if (expiration) {
|
|
1761
|
-
const envelope: StoredEnvelope = {
|
|
1762
|
-
__nitroStorageEnvelope: true,
|
|
1763
|
-
expiresAt: Date.now() + expiration.ttlMs,
|
|
1764
|
-
payload: serialized,
|
|
1765
|
-
};
|
|
1766
|
-
writeStoredRaw(JSON.stringify(envelope));
|
|
1767
|
-
return;
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
|
-
writeStoredRaw(serialized);
|
|
1771
|
-
};
|
|
1772
|
-
|
|
1773
|
-
const resolveInvalidValue = (invalidValue: unknown): T => {
|
|
1774
|
-
if (onValidationError) {
|
|
1775
|
-
return onValidationError(invalidValue);
|
|
1776
|
-
}
|
|
1777
|
-
|
|
1778
|
-
return defaultValue;
|
|
1779
|
-
};
|
|
1780
|
-
|
|
1781
|
-
const ensureValidatedValue = (
|
|
1782
|
-
candidate: unknown,
|
|
1783
|
-
hadStoredValue: boolean,
|
|
1784
|
-
): T => {
|
|
1785
|
-
if (!validate || validate(candidate)) {
|
|
1786
|
-
return candidate as T;
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
const resolved = resolveInvalidValue(candidate);
|
|
1790
|
-
if (validate && !validate(resolved)) {
|
|
1791
|
-
return defaultValue;
|
|
1792
|
-
}
|
|
1793
|
-
if (hadStoredValue) {
|
|
1794
|
-
writeValueWithoutValidation(resolved);
|
|
1795
|
-
}
|
|
1796
|
-
return resolved;
|
|
1797
|
-
};
|
|
1798
|
-
|
|
1799
|
-
const getInternal = (): T => {
|
|
1800
|
-
const raw = readStoredRaw();
|
|
1801
|
-
|
|
1802
|
-
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
1803
|
-
if (!expiration || lastExpiresAt === null) {
|
|
1804
|
-
return lastValue as T;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
if (typeof lastExpiresAt === "number") {
|
|
1808
|
-
if (lastExpiresAt > Date.now()) {
|
|
1809
|
-
return lastValue as T;
|
|
1810
|
-
}
|
|
1811
|
-
|
|
1812
|
-
removeStoredRaw();
|
|
1813
|
-
invalidateParsedCache();
|
|
1814
|
-
onExpired?.(storageKey);
|
|
1815
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1816
|
-
hasLastValue = true;
|
|
1817
|
-
listeners.forEach((cb) => cb());
|
|
1818
|
-
return lastValue;
|
|
1819
|
-
}
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
lastRaw = raw;
|
|
1823
|
-
|
|
1824
|
-
if (raw === undefined) {
|
|
1825
|
-
lastExpiresAt = undefined;
|
|
1826
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1827
|
-
hasLastValue = true;
|
|
1828
|
-
return lastValue;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
|
-
if (isMemory) {
|
|
1832
|
-
lastExpiresAt = undefined;
|
|
1833
|
-
lastValue = ensureValidatedValue(raw, true);
|
|
1834
|
-
hasLastValue = true;
|
|
1835
|
-
return lastValue;
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
if (typeof raw !== "string") {
|
|
1839
|
-
lastExpiresAt = undefined;
|
|
1840
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1841
|
-
hasLastValue = true;
|
|
1842
|
-
return lastValue;
|
|
1843
|
-
}
|
|
1844
|
-
|
|
1845
|
-
let deserializableRaw = raw;
|
|
1846
|
-
|
|
1847
|
-
if (expiration) {
|
|
1848
|
-
let envelopeExpiresAt: number | null = null;
|
|
1849
|
-
try {
|
|
1850
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
1851
|
-
if (isStoredEnvelope(parsed)) {
|
|
1852
|
-
envelopeExpiresAt = parsed.expiresAt;
|
|
1853
|
-
if (parsed.expiresAt <= Date.now()) {
|
|
1854
|
-
removeStoredRaw();
|
|
1855
|
-
invalidateParsedCache();
|
|
1856
|
-
onExpired?.(storageKey);
|
|
1857
|
-
lastValue = ensureValidatedValue(defaultValue, false);
|
|
1858
|
-
hasLastValue = true;
|
|
1859
|
-
listeners.forEach((cb) => cb());
|
|
1860
|
-
return lastValue;
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
deserializableRaw = parsed.payload;
|
|
1864
|
-
}
|
|
1865
|
-
} catch {
|
|
1866
|
-
// Keep backward compatibility with legacy raw values.
|
|
1867
|
-
}
|
|
1868
|
-
lastExpiresAt = envelopeExpiresAt;
|
|
1869
|
-
} else {
|
|
1870
|
-
lastExpiresAt = undefined;
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
1874
|
-
hasLastValue = true;
|
|
1875
|
-
return lastValue;
|
|
1876
|
-
};
|
|
1877
|
-
|
|
1878
|
-
const getCurrentVersion = (): StorageVersion => {
|
|
1879
|
-
const raw = readStoredRaw();
|
|
1880
|
-
return toVersionToken(raw);
|
|
1881
|
-
};
|
|
1882
|
-
|
|
1883
|
-
const get = (): T =>
|
|
1884
|
-
measureOperation("item:get", config.scope, () => getInternal());
|
|
1885
|
-
|
|
1886
|
-
const getWithVersion = (): VersionedValue<T> =>
|
|
1887
|
-
measureOperation("item:getWithVersion", config.scope, () => ({
|
|
1888
|
-
value: getInternal(),
|
|
1889
|
-
version: getCurrentVersion(),
|
|
1890
|
-
}));
|
|
1891
|
-
|
|
1892
|
-
const set = (valueOrFn: T | ((prev: T) => T)): void => {
|
|
1893
|
-
measureOperation("item:set", config.scope, () => {
|
|
1894
|
-
const newValue = isUpdater(valueOrFn)
|
|
1895
|
-
? valueOrFn(getInternal())
|
|
1896
|
-
: valueOrFn;
|
|
1897
|
-
|
|
1898
|
-
if (validate && !validate(newValue)) {
|
|
1899
|
-
throw new Error(
|
|
1900
|
-
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
1901
|
-
);
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
invalidateParsedCache();
|
|
1905
|
-
writeValueWithoutValidation(newValue);
|
|
1906
|
-
});
|
|
1907
|
-
};
|
|
1908
|
-
|
|
1909
|
-
const setIfVersion = (
|
|
1910
|
-
version: StorageVersion,
|
|
1911
|
-
valueOrFn: T | ((prev: T) => T),
|
|
1912
|
-
): boolean =>
|
|
1913
|
-
measureOperation("item:setIfVersion", config.scope, () => {
|
|
1914
|
-
const currentVersion = getCurrentVersion();
|
|
1915
|
-
if (currentVersion !== version) {
|
|
1916
|
-
return false;
|
|
1917
|
-
}
|
|
1918
|
-
set(valueOrFn);
|
|
1919
|
-
return true;
|
|
1920
|
-
});
|
|
1921
|
-
|
|
1922
|
-
const deleteItem = (): void => {
|
|
1923
|
-
measureOperation("item:delete", config.scope, () => {
|
|
1924
|
-
invalidateParsedCache();
|
|
1925
|
-
|
|
1926
|
-
if (isMemory) {
|
|
1927
|
-
const oldValue = getEventRawValue(config.scope, storageKey);
|
|
1928
|
-
if (memoryExpiration) {
|
|
1929
|
-
memoryExpiration.delete(storageKey);
|
|
1930
|
-
}
|
|
1931
|
-
memoryStore.delete(storageKey);
|
|
1932
|
-
notifyKeyListeners(memoryListeners, storageKey);
|
|
1933
|
-
emitKeyChange(
|
|
1934
|
-
config.scope,
|
|
1935
|
-
storageKey,
|
|
1936
|
-
oldValue,
|
|
1937
|
-
undefined,
|
|
1938
|
-
"remove",
|
|
1939
|
-
"memory",
|
|
1940
|
-
);
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
removeStoredRaw();
|
|
1945
|
-
});
|
|
1946
|
-
};
|
|
1947
|
-
|
|
1948
|
-
const hasItem = (): boolean =>
|
|
1949
|
-
measureOperation("item:has", config.scope, () => {
|
|
1950
|
-
if (isMemory) return memoryStore.has(storageKey);
|
|
1951
|
-
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
1952
|
-
if (nonMemoryScope === StorageScope.Disk) {
|
|
1953
|
-
const pending = pendingDiskWrites.get(storageKey);
|
|
1954
|
-
if (pending !== undefined) {
|
|
1955
|
-
return pending.value !== undefined;
|
|
1956
|
-
}
|
|
1957
|
-
}
|
|
1958
|
-
if (nonMemoryScope === StorageScope.Secure) {
|
|
1959
|
-
const pending = pendingSecureWrites.get(storageKey);
|
|
1960
|
-
if (pending !== undefined) {
|
|
1961
|
-
return pending.value !== undefined;
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
return getStorageModule().has(storageKey, config.scope);
|
|
1965
|
-
});
|
|
1966
|
-
|
|
1967
|
-
const subscribe = (callback: () => void): (() => void) => {
|
|
1968
|
-
ensureSubscription();
|
|
1969
|
-
listeners.add(callback);
|
|
1970
|
-
return () => {
|
|
1971
|
-
listeners.delete(callback);
|
|
1972
|
-
if (listeners.size === 0 && unsubscribe) {
|
|
1973
|
-
unsubscribe();
|
|
1974
|
-
if (!isMemory) {
|
|
1975
|
-
maybeCleanupNativeScopeSubscription(nonMemoryScope!);
|
|
1976
|
-
}
|
|
1977
|
-
unsubscribe = null;
|
|
1978
|
-
}
|
|
1979
|
-
};
|
|
1980
|
-
};
|
|
1981
|
-
|
|
1982
|
-
const subscribeSelector = <TSelected>(
|
|
1983
|
-
selector: (value: T) => TSelected,
|
|
1984
|
-
listener: StorageSelectorListener<TSelected>,
|
|
1985
|
-
options: StorageSelectorSubscribeOptions<TSelected> = {},
|
|
1986
|
-
): (() => void) => {
|
|
1987
|
-
const isEqual = options.isEqual ?? Object.is;
|
|
1988
|
-
let currentValue = selector(getInternal());
|
|
1989
|
-
|
|
1990
|
-
if (options.fireImmediately === true) {
|
|
1991
|
-
listener(currentValue, currentValue);
|
|
1992
|
-
}
|
|
1993
|
-
|
|
1994
|
-
return subscribe(() => {
|
|
1995
|
-
const nextValue = selector(getInternal());
|
|
1996
|
-
if (isEqual(currentValue, nextValue)) {
|
|
1997
|
-
return;
|
|
1998
|
-
}
|
|
1999
|
-
|
|
2000
|
-
const previousValue = currentValue;
|
|
2001
|
-
currentValue = nextValue;
|
|
2002
|
-
listener(nextValue, previousValue);
|
|
2003
|
-
});
|
|
2004
|
-
};
|
|
2005
|
-
|
|
2006
|
-
const storageItem: StorageItemInternal<T> = {
|
|
2007
|
-
get,
|
|
2008
|
-
getWithVersion,
|
|
2009
|
-
set,
|
|
2010
|
-
setIfVersion,
|
|
2011
|
-
delete: deleteItem,
|
|
2012
|
-
has: hasItem,
|
|
2013
|
-
subscribe,
|
|
2014
|
-
subscribeSelector,
|
|
2015
|
-
serialize,
|
|
2016
|
-
deserialize,
|
|
2017
|
-
_triggerListeners: () => {
|
|
2018
|
-
invalidateParsedCache();
|
|
2019
|
-
listeners.forEach((listener) => listener());
|
|
2020
|
-
},
|
|
2021
|
-
_invalidateParsedCacheOnly: () => {
|
|
2022
|
-
invalidateParsedCache();
|
|
2023
|
-
},
|
|
2024
|
-
_hasValidation: validate !== undefined,
|
|
2025
|
-
_hasExpiration: expiration !== undefined,
|
|
2026
|
-
_readCacheEnabled: readCache,
|
|
2027
|
-
_isBiometric: isBiometric,
|
|
2028
|
-
_biometricLevel: resolvedBiometricLevel,
|
|
2029
|
-
_defaultValue: defaultValue,
|
|
2030
|
-
...(secureAccessControl !== undefined
|
|
2031
|
-
? { _secureAccessControl: secureAccessControl }
|
|
2032
|
-
: {}),
|
|
2033
|
-
scope: config.scope,
|
|
2034
|
-
key: storageKey,
|
|
2035
|
-
};
|
|
2036
|
-
|
|
2037
|
-
return storageItem;
|
|
340
|
+
export async function flushWebStorageBackends(): Promise<void> {
|
|
341
|
+
// Native platforms do not use web storage backends.
|
|
2038
342
|
}
|
|
2039
343
|
|
|
2040
344
|
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
2041
345
|
export { createIndexedDBBackend } from "./indexeddb-backend";
|
|
2042
|
-
|
|
2043
|
-
type BatchReadItem<T> = Pick<
|
|
2044
|
-
StorageItem<T>,
|
|
2045
|
-
"key" | "scope" | "get" | "deserialize"
|
|
2046
|
-
> & {
|
|
2047
|
-
_hasValidation?: boolean;
|
|
2048
|
-
_hasExpiration?: boolean;
|
|
2049
|
-
_readCacheEnabled?: boolean;
|
|
2050
|
-
_isBiometric?: boolean;
|
|
2051
|
-
_defaultValue?: unknown;
|
|
2052
|
-
_secureAccessControl?: AccessControl;
|
|
2053
|
-
};
|
|
2054
|
-
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
2055
|
-
type BatchValues<TItems extends readonly BatchReadItem<unknown>[]> = {
|
|
2056
|
-
[Index in keyof TItems]: TItems[Index] extends BatchReadItem<infer Value>
|
|
2057
|
-
? Value
|
|
2058
|
-
: never;
|
|
2059
|
-
};
|
|
2060
|
-
|
|
2061
|
-
export type StorageBatchSetItem<T> = {
|
|
2062
|
-
item: StorageItem<T>;
|
|
2063
|
-
value: T;
|
|
2064
|
-
};
|
|
2065
|
-
|
|
2066
|
-
export function getBatch<
|
|
2067
|
-
const TItems extends readonly BatchReadItem<unknown>[],
|
|
2068
|
-
>(items: TItems, scope: StorageScope): BatchValues<TItems> {
|
|
2069
|
-
return measureOperation(
|
|
2070
|
-
"batch:get",
|
|
2071
|
-
scope,
|
|
2072
|
-
() => {
|
|
2073
|
-
assertBatchScope(items, scope);
|
|
2074
|
-
|
|
2075
|
-
if (scope === StorageScope.Memory) {
|
|
2076
|
-
return items.map((item) => item.get());
|
|
2077
|
-
}
|
|
2078
|
-
|
|
2079
|
-
const useRawBatchPath = items.every((item) =>
|
|
2080
|
-
scope === StorageScope.Secure
|
|
2081
|
-
? canUseSecureRawBatchPath(item)
|
|
2082
|
-
: canUseRawBatchPath(item),
|
|
2083
|
-
);
|
|
2084
|
-
if (!useRawBatchPath) {
|
|
2085
|
-
return items.map((item) => item.get());
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
const rawValues = new Array<string | undefined>(items.length);
|
|
2089
|
-
const keysToFetch: string[] = [];
|
|
2090
|
-
const keyIndexes: number[] = [];
|
|
2091
|
-
|
|
2092
|
-
items.forEach((item, index) => {
|
|
2093
|
-
if (scope === StorageScope.Disk) {
|
|
2094
|
-
const pending = pendingDiskWrites.get(item.key);
|
|
2095
|
-
if (pending !== undefined) {
|
|
2096
|
-
rawValues[index] = pending.value;
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
|
-
}
|
|
2100
|
-
|
|
2101
|
-
if (scope === StorageScope.Secure) {
|
|
2102
|
-
const pending = pendingSecureWrites.get(item.key);
|
|
2103
|
-
if (pending !== undefined) {
|
|
2104
|
-
rawValues[index] = pending.value;
|
|
2105
|
-
return;
|
|
2106
|
-
}
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
if (item._readCacheEnabled === true) {
|
|
2110
|
-
const cache = getScopeRawCache(scope);
|
|
2111
|
-
const cached = cache.get(item.key);
|
|
2112
|
-
if (cached !== undefined || cache.has(item.key)) {
|
|
2113
|
-
rawValues[index] = cached;
|
|
2114
|
-
return;
|
|
2115
|
-
}
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
keysToFetch.push(item.key);
|
|
2119
|
-
keyIndexes.push(index);
|
|
2120
|
-
});
|
|
2121
|
-
|
|
2122
|
-
if (keysToFetch.length > 0) {
|
|
2123
|
-
const fetchedValues = getStorageModule()
|
|
2124
|
-
.getBatch(keysToFetch, scope)
|
|
2125
|
-
.map((value) => decodeNativeBatchValue(value));
|
|
2126
|
-
|
|
2127
|
-
fetchedValues.forEach((value, index) => {
|
|
2128
|
-
const key = keysToFetch[index];
|
|
2129
|
-
const targetIndex = keyIndexes[index];
|
|
2130
|
-
if (key === undefined || targetIndex === undefined) {
|
|
2131
|
-
return;
|
|
2132
|
-
}
|
|
2133
|
-
rawValues[targetIndex] = value;
|
|
2134
|
-
cacheRawValue(scope, key, value);
|
|
2135
|
-
});
|
|
2136
|
-
}
|
|
2137
|
-
|
|
2138
|
-
return items.map((item, index) => {
|
|
2139
|
-
const raw = rawValues[index];
|
|
2140
|
-
if (raw === undefined) {
|
|
2141
|
-
return asInternal(item as StorageItem<unknown>)._defaultValue;
|
|
2142
|
-
}
|
|
2143
|
-
return item.deserialize(raw);
|
|
2144
|
-
});
|
|
2145
|
-
},
|
|
2146
|
-
items.length,
|
|
2147
|
-
) as BatchValues<TItems>;
|
|
2148
|
-
}
|
|
2149
|
-
|
|
2150
|
-
export function setBatch<T>(
|
|
2151
|
-
items: readonly StorageBatchSetItem<T>[],
|
|
2152
|
-
scope: StorageScope,
|
|
2153
|
-
): void {
|
|
2154
|
-
measureOperation(
|
|
2155
|
-
"batch:set",
|
|
2156
|
-
scope,
|
|
2157
|
-
() => {
|
|
2158
|
-
assertBatchScope(
|
|
2159
|
-
items.map((batchEntry) => batchEntry.item),
|
|
2160
|
-
scope,
|
|
2161
|
-
);
|
|
2162
|
-
|
|
2163
|
-
if (scope === StorageScope.Memory) {
|
|
2164
|
-
// Determine if any item needs per-item handling (validation or TTL)
|
|
2165
|
-
const needsIndividualSets = items.some(({ item }) => {
|
|
2166
|
-
const internal = asInternal(item as StorageItem<unknown>);
|
|
2167
|
-
return internal._hasValidation || internal._hasExpiration;
|
|
2168
|
-
});
|
|
2169
|
-
|
|
2170
|
-
if (needsIndividualSets) {
|
|
2171
|
-
// Fall back to individual sets to preserve validation and TTL semantics
|
|
2172
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
2173
|
-
return;
|
|
2174
|
-
}
|
|
2175
|
-
|
|
2176
|
-
const changes = items.map(({ item, value }) =>
|
|
2177
|
-
createKeyChange(
|
|
2178
|
-
scope,
|
|
2179
|
-
item.key,
|
|
2180
|
-
getEventRawValue(scope, item.key),
|
|
2181
|
-
typeof value === "string" ? value : undefined,
|
|
2182
|
-
"setBatch",
|
|
2183
|
-
"memory",
|
|
2184
|
-
),
|
|
2185
|
-
);
|
|
2186
|
-
|
|
2187
|
-
// Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
|
|
2188
|
-
items.forEach(({ item, value }) => {
|
|
2189
|
-
memoryStore.set(item.key, value);
|
|
2190
|
-
asInternal(item as StorageItem<unknown>)._invalidateParsedCacheOnly();
|
|
2191
|
-
});
|
|
2192
|
-
items.forEach(({ item }) =>
|
|
2193
|
-
notifyKeyListeners(memoryListeners, item.key),
|
|
2194
|
-
);
|
|
2195
|
-
emitBatchChange(scope, "setBatch", "memory", changes);
|
|
2196
|
-
return;
|
|
2197
|
-
}
|
|
2198
|
-
|
|
2199
|
-
if (scope === StorageScope.Secure) {
|
|
2200
|
-
const secureEntries = items.map(({ item, value }) => ({
|
|
2201
|
-
item,
|
|
2202
|
-
value,
|
|
2203
|
-
internal: asInternal(item),
|
|
2204
|
-
}));
|
|
2205
|
-
const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
|
|
2206
|
-
canUseSecureRawBatchPath(internal),
|
|
2207
|
-
);
|
|
2208
|
-
if (!canUseSecureBatchPath) {
|
|
2209
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
2210
|
-
return;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
flushSecureWrites();
|
|
2214
|
-
const storageModule = getStorageModule();
|
|
2215
|
-
const keys = secureEntries.map(({ item }) => item.key);
|
|
2216
|
-
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2217
|
-
? (storageModule.getBatch(keys, scope) ?? [])
|
|
2218
|
-
: [];
|
|
2219
|
-
const groupedByAccessControl = new Map<
|
|
2220
|
-
number,
|
|
2221
|
-
{ keys: string[]; values: string[] }
|
|
2222
|
-
>();
|
|
2223
|
-
|
|
2224
|
-
secureEntries.forEach(({ item, value, internal }) => {
|
|
2225
|
-
const accessControl =
|
|
2226
|
-
internal._secureAccessControl ?? secureDefaultAccessControl;
|
|
2227
|
-
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
2228
|
-
const group = existingGroup ?? { keys: [], values: [] };
|
|
2229
|
-
group.keys.push(item.key);
|
|
2230
|
-
group.values.push(item.serialize(value));
|
|
2231
|
-
if (!existingGroup) {
|
|
2232
|
-
groupedByAccessControl.set(accessControl, group);
|
|
2233
|
-
}
|
|
2234
|
-
});
|
|
2235
|
-
|
|
2236
|
-
groupedByAccessControl.forEach((group, accessControl) => {
|
|
2237
|
-
storageModule.setSecureAccessControl(accessControl);
|
|
2238
|
-
storageModule.setBatch(group.keys, group.values, scope);
|
|
2239
|
-
group.keys.forEach((key, index) =>
|
|
2240
|
-
cacheRawValue(scope, key, group.values[index]),
|
|
2241
|
-
);
|
|
2242
|
-
});
|
|
2243
|
-
emitBatchChange(
|
|
2244
|
-
scope,
|
|
2245
|
-
"setBatch",
|
|
2246
|
-
"native",
|
|
2247
|
-
secureEntries.map(({ item, value }, index) =>
|
|
2248
|
-
createKeyChange(
|
|
2249
|
-
scope,
|
|
2250
|
-
item.key,
|
|
2251
|
-
oldValues[index],
|
|
2252
|
-
item.serialize(value),
|
|
2253
|
-
"setBatch",
|
|
2254
|
-
"native",
|
|
2255
|
-
),
|
|
2256
|
-
),
|
|
2257
|
-
);
|
|
2258
|
-
return;
|
|
2259
|
-
}
|
|
2260
|
-
|
|
2261
|
-
flushDiskWrites();
|
|
2262
|
-
|
|
2263
|
-
const useRawBatchPath = items.every(({ item }) =>
|
|
2264
|
-
canUseRawBatchPath(asInternal(item)),
|
|
2265
|
-
);
|
|
2266
|
-
if (!useRawBatchPath) {
|
|
2267
|
-
items.forEach(({ item, value }) => item.set(value));
|
|
2268
|
-
return;
|
|
2269
|
-
}
|
|
2270
|
-
|
|
2271
|
-
const keys = items.map((entry) => entry.item.key);
|
|
2272
|
-
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
2273
|
-
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2274
|
-
? (getStorageModule().getBatch(keys, scope) ?? [])
|
|
2275
|
-
: [];
|
|
2276
|
-
|
|
2277
|
-
getStorageModule().setBatch(keys, values, scope);
|
|
2278
|
-
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
2279
|
-
emitBatchChange(
|
|
2280
|
-
scope,
|
|
2281
|
-
"setBatch",
|
|
2282
|
-
"native",
|
|
2283
|
-
keys.map((key, index) =>
|
|
2284
|
-
createKeyChange(
|
|
2285
|
-
scope,
|
|
2286
|
-
key,
|
|
2287
|
-
oldValues[index],
|
|
2288
|
-
values[index],
|
|
2289
|
-
"setBatch",
|
|
2290
|
-
"native",
|
|
2291
|
-
),
|
|
2292
|
-
),
|
|
2293
|
-
);
|
|
2294
|
-
},
|
|
2295
|
-
items.length,
|
|
2296
|
-
);
|
|
2297
|
-
}
|
|
2298
|
-
|
|
2299
|
-
export function removeBatch(
|
|
2300
|
-
items: readonly BatchRemoveItem[],
|
|
2301
|
-
scope: StorageScope,
|
|
2302
|
-
): void {
|
|
2303
|
-
measureOperation(
|
|
2304
|
-
"batch:remove",
|
|
2305
|
-
scope,
|
|
2306
|
-
() => {
|
|
2307
|
-
assertBatchScope(items, scope);
|
|
2308
|
-
|
|
2309
|
-
if (scope === StorageScope.Memory) {
|
|
2310
|
-
const changes = items.map((item) =>
|
|
2311
|
-
createKeyChange(
|
|
2312
|
-
scope,
|
|
2313
|
-
item.key,
|
|
2314
|
-
getEventRawValue(scope, item.key),
|
|
2315
|
-
undefined,
|
|
2316
|
-
"removeBatch",
|
|
2317
|
-
"memory",
|
|
2318
|
-
),
|
|
2319
|
-
);
|
|
2320
|
-
items.forEach((item) => item.delete());
|
|
2321
|
-
emitBatchChange(scope, "removeBatch", "memory", changes);
|
|
2322
|
-
return;
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
const keys = items.map((item) => item.key);
|
|
2326
|
-
if (scope === StorageScope.Disk) {
|
|
2327
|
-
flushDiskWrites();
|
|
2328
|
-
}
|
|
2329
|
-
if (scope === StorageScope.Secure) {
|
|
2330
|
-
flushSecureWrites();
|
|
2331
|
-
}
|
|
2332
|
-
const oldValues = shouldReadPreviousEventValues(scope)
|
|
2333
|
-
? (getStorageModule().getBatch(keys, scope) ?? [])
|
|
2334
|
-
: [];
|
|
2335
|
-
getStorageModule().removeBatch(keys, scope);
|
|
2336
|
-
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
2337
|
-
emitBatchChange(
|
|
2338
|
-
scope,
|
|
2339
|
-
"removeBatch",
|
|
2340
|
-
"native",
|
|
2341
|
-
keys.map((key, index) =>
|
|
2342
|
-
createKeyChange(
|
|
2343
|
-
scope,
|
|
2344
|
-
key,
|
|
2345
|
-
oldValues[index],
|
|
2346
|
-
undefined,
|
|
2347
|
-
"removeBatch",
|
|
2348
|
-
"native",
|
|
2349
|
-
),
|
|
2350
|
-
),
|
|
2351
|
-
);
|
|
2352
|
-
},
|
|
2353
|
-
items.length,
|
|
2354
|
-
);
|
|
2355
|
-
}
|
|
2356
|
-
|
|
2357
|
-
export function registerMigration(version: number, migration: Migration): void {
|
|
2358
|
-
if (!Number.isInteger(version) || version <= 0) {
|
|
2359
|
-
throw new Error("Migration version must be a positive integer.");
|
|
2360
|
-
}
|
|
2361
|
-
|
|
2362
|
-
if (registeredMigrations.has(version)) {
|
|
2363
|
-
throw new Error(`Migration version ${version} is already registered.`);
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
registeredMigrations.set(version, migration);
|
|
2367
|
-
}
|
|
2368
|
-
|
|
2369
|
-
export function migrateToLatest(
|
|
2370
|
-
scope: StorageScope = StorageScope.Disk,
|
|
2371
|
-
): number {
|
|
2372
|
-
return measureOperation("migration:run", scope, () => {
|
|
2373
|
-
assertValidScope(scope);
|
|
2374
|
-
const currentVersion = readMigrationVersion(scope);
|
|
2375
|
-
const versions = Array.from(registeredMigrations.keys())
|
|
2376
|
-
.filter((version) => version > currentVersion)
|
|
2377
|
-
.sort((a, b) => a - b);
|
|
2378
|
-
|
|
2379
|
-
let appliedVersion = currentVersion;
|
|
2380
|
-
const context: MigrationContext = {
|
|
2381
|
-
scope,
|
|
2382
|
-
getRaw: (key) => getRawValue(key, scope),
|
|
2383
|
-
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
2384
|
-
removeRaw: (key) => removeRawValue(key, scope),
|
|
2385
|
-
};
|
|
2386
|
-
|
|
2387
|
-
versions.forEach((version) => {
|
|
2388
|
-
const migration = registeredMigrations.get(version);
|
|
2389
|
-
if (!migration) {
|
|
2390
|
-
return;
|
|
2391
|
-
}
|
|
2392
|
-
migration(context);
|
|
2393
|
-
appliedVersion = version;
|
|
2394
|
-
});
|
|
2395
|
-
|
|
2396
|
-
if (appliedVersion !== currentVersion) {
|
|
2397
|
-
writeMigrationVersion(scope, appliedVersion);
|
|
2398
|
-
}
|
|
2399
|
-
|
|
2400
|
-
return appliedVersion;
|
|
2401
|
-
});
|
|
2402
|
-
}
|
|
2403
|
-
|
|
2404
|
-
export function runTransaction<T>(
|
|
2405
|
-
scope: StorageScope,
|
|
2406
|
-
transaction: (context: TransactionContext) => T,
|
|
2407
|
-
): T {
|
|
2408
|
-
return measureOperation("transaction:run", scope, () => {
|
|
2409
|
-
assertValidScope(scope);
|
|
2410
|
-
if (scope === StorageScope.Disk) {
|
|
2411
|
-
flushDiskWrites();
|
|
2412
|
-
}
|
|
2413
|
-
if (scope === StorageScope.Secure) {
|
|
2414
|
-
flushSecureWrites();
|
|
2415
|
-
}
|
|
2416
|
-
|
|
2417
|
-
const NOT_SET = Symbol();
|
|
2418
|
-
const rollback = new Map<string, RollbackRecord>();
|
|
2419
|
-
|
|
2420
|
-
const rememberRollback = (
|
|
2421
|
-
key: string,
|
|
2422
|
-
item?: Pick<StorageItem<unknown>, "key" | "scope">,
|
|
2423
|
-
) => {
|
|
2424
|
-
if (rollback.has(key)) {
|
|
2425
|
-
return;
|
|
2426
|
-
}
|
|
2427
|
-
if (scope === StorageScope.Memory) {
|
|
2428
|
-
rollback.set(key, {
|
|
2429
|
-
kind: "memory",
|
|
2430
|
-
value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
|
|
2431
|
-
});
|
|
2432
|
-
} else {
|
|
2433
|
-
const internal = item
|
|
2434
|
-
? (item as StorageItemInternal<unknown>)
|
|
2435
|
-
: undefined;
|
|
2436
|
-
if (scope === StorageScope.Secure && internal?._isBiometric === true) {
|
|
2437
|
-
rollback.set(key, {
|
|
2438
|
-
kind: "biometric",
|
|
2439
|
-
value: getStorageModule().getSecureBiometric(key),
|
|
2440
|
-
level: internal._biometricLevel,
|
|
2441
|
-
});
|
|
2442
|
-
return;
|
|
2443
|
-
}
|
|
2444
|
-
rollback.set(key, {
|
|
2445
|
-
kind: "raw",
|
|
2446
|
-
value: getRawValue(key, scope),
|
|
2447
|
-
...(scope === StorageScope.Secure &&
|
|
2448
|
-
internal?._secureAccessControl !== undefined
|
|
2449
|
-
? { accessControl: internal._secureAccessControl }
|
|
2450
|
-
: {}),
|
|
2451
|
-
});
|
|
2452
|
-
}
|
|
2453
|
-
};
|
|
2454
|
-
|
|
2455
|
-
const tx: TransactionContext = {
|
|
2456
|
-
scope,
|
|
2457
|
-
getRaw: (key) => getRawValue(key, scope),
|
|
2458
|
-
setRaw: (key, value) => {
|
|
2459
|
-
rememberRollback(key);
|
|
2460
|
-
setRawValue(key, value, scope);
|
|
2461
|
-
},
|
|
2462
|
-
removeRaw: (key) => {
|
|
2463
|
-
rememberRollback(key);
|
|
2464
|
-
removeRawValue(key, scope);
|
|
2465
|
-
},
|
|
2466
|
-
getItem: (item) => {
|
|
2467
|
-
assertBatchScope([item], scope);
|
|
2468
|
-
return item.get();
|
|
2469
|
-
},
|
|
2470
|
-
setItem: (item, value) => {
|
|
2471
|
-
assertBatchScope([item], scope);
|
|
2472
|
-
rememberRollback(item.key, item);
|
|
2473
|
-
item.set(value);
|
|
2474
|
-
},
|
|
2475
|
-
removeItem: (item) => {
|
|
2476
|
-
assertBatchScope([item], scope);
|
|
2477
|
-
rememberRollback(item.key, item);
|
|
2478
|
-
item.delete();
|
|
2479
|
-
},
|
|
2480
|
-
};
|
|
2481
|
-
|
|
2482
|
-
try {
|
|
2483
|
-
return transaction(tx);
|
|
2484
|
-
} catch (error) {
|
|
2485
|
-
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
2486
|
-
if (scope === StorageScope.Memory) {
|
|
2487
|
-
rollbackEntries.forEach(([key, record]) => {
|
|
2488
|
-
if (record.value === NOT_SET) {
|
|
2489
|
-
memoryStore.delete(key);
|
|
2490
|
-
} else {
|
|
2491
|
-
memoryStore.set(key, record.value);
|
|
2492
|
-
}
|
|
2493
|
-
notifyKeyListeners(memoryListeners, key);
|
|
2494
|
-
});
|
|
2495
|
-
} else {
|
|
2496
|
-
const groupedKeysToSet = new Map<
|
|
2497
|
-
AccessControl,
|
|
2498
|
-
{ keys: string[]; values: string[] }
|
|
2499
|
-
>();
|
|
2500
|
-
const keysToRemove: string[] = [];
|
|
2501
|
-
|
|
2502
|
-
rollbackEntries.forEach(([key, record]) => {
|
|
2503
|
-
if (record.kind === "biometric") {
|
|
2504
|
-
if (record.value === undefined) {
|
|
2505
|
-
getStorageModule().deleteSecureBiometric(key);
|
|
2506
|
-
} else {
|
|
2507
|
-
getStorageModule().setSecureBiometricWithLevel(
|
|
2508
|
-
key,
|
|
2509
|
-
record.value,
|
|
2510
|
-
record.level,
|
|
2511
|
-
);
|
|
2512
|
-
}
|
|
2513
|
-
return;
|
|
2514
|
-
}
|
|
2515
|
-
if (record.kind !== "raw") {
|
|
2516
|
-
return;
|
|
2517
|
-
}
|
|
2518
|
-
if (record.value === undefined) {
|
|
2519
|
-
keysToRemove.push(key);
|
|
2520
|
-
} else {
|
|
2521
|
-
const accessControl =
|
|
2522
|
-
record.accessControl ?? secureDefaultAccessControl;
|
|
2523
|
-
const existingGroup = groupedKeysToSet.get(accessControl);
|
|
2524
|
-
const group = existingGroup ?? { keys: [], values: [] };
|
|
2525
|
-
group.keys.push(key);
|
|
2526
|
-
group.values.push(record.value);
|
|
2527
|
-
if (!existingGroup) {
|
|
2528
|
-
groupedKeysToSet.set(accessControl, group);
|
|
2529
|
-
}
|
|
2530
|
-
}
|
|
2531
|
-
});
|
|
2532
|
-
|
|
2533
|
-
if (scope === StorageScope.Disk) {
|
|
2534
|
-
flushDiskWrites();
|
|
2535
|
-
}
|
|
2536
|
-
if (scope === StorageScope.Secure) {
|
|
2537
|
-
flushSecureWrites();
|
|
2538
|
-
}
|
|
2539
|
-
groupedKeysToSet.forEach((group, accessControl) => {
|
|
2540
|
-
if (scope === StorageScope.Secure) {
|
|
2541
|
-
getStorageModule().setSecureAccessControl(accessControl);
|
|
2542
|
-
}
|
|
2543
|
-
getStorageModule().setBatch(group.keys, group.values, scope);
|
|
2544
|
-
group.keys.forEach((key, index) =>
|
|
2545
|
-
cacheRawValue(scope, key, group.values[index]),
|
|
2546
|
-
);
|
|
2547
|
-
});
|
|
2548
|
-
if (keysToRemove.length > 0) {
|
|
2549
|
-
getStorageModule().removeBatch(keysToRemove, scope);
|
|
2550
|
-
keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
2551
|
-
}
|
|
2552
|
-
}
|
|
2553
|
-
throw error;
|
|
2554
|
-
}
|
|
2555
|
-
});
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
2559
|
-
K,
|
|
2560
|
-
{
|
|
2561
|
-
ttlMs?: number;
|
|
2562
|
-
biometric?: boolean;
|
|
2563
|
-
biometricLevel?: BiometricLevel;
|
|
2564
|
-
accessControl?: AccessControl;
|
|
2565
|
-
}
|
|
2566
|
-
>;
|
|
2567
|
-
|
|
2568
|
-
export function isKeychainLockedError(err: unknown): boolean {
|
|
2569
|
-
return isLockedStorageErrorCode(getStorageErrorCode(err));
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
export function createSecureAuthStorage<K extends string>(
|
|
2573
|
-
config: SecureAuthStorageConfig<K>,
|
|
2574
|
-
options?: { namespace?: string },
|
|
2575
|
-
): Record<K, StorageItem<string>> {
|
|
2576
|
-
const ns = options?.namespace ?? "auth";
|
|
2577
|
-
const result: Partial<Record<K, StorageItem<string>>> = {};
|
|
2578
|
-
|
|
2579
|
-
for (const key of typedKeys(config)) {
|
|
2580
|
-
const itemConfig = config[key];
|
|
2581
|
-
const expirationConfig =
|
|
2582
|
-
itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
|
|
2583
|
-
result[key] = createStorageItem<string>({
|
|
2584
|
-
key,
|
|
2585
|
-
scope: StorageScope.Secure,
|
|
2586
|
-
defaultValue: "",
|
|
2587
|
-
namespace: ns,
|
|
2588
|
-
...(itemConfig.biometric !== undefined
|
|
2589
|
-
? { biometric: itemConfig.biometric }
|
|
2590
|
-
: {}),
|
|
2591
|
-
...(itemConfig.biometricLevel !== undefined
|
|
2592
|
-
? { biometricLevel: itemConfig.biometricLevel }
|
|
2593
|
-
: {}),
|
|
2594
|
-
...(itemConfig.accessControl !== undefined
|
|
2595
|
-
? { accessControl: itemConfig.accessControl }
|
|
2596
|
-
: {}),
|
|
2597
|
-
...(expirationConfig !== undefined
|
|
2598
|
-
? { expiration: expirationConfig }
|
|
2599
|
-
: {}),
|
|
2600
|
-
});
|
|
2601
|
-
}
|
|
2602
|
-
|
|
2603
|
-
return result as Record<K, StorageItem<string>>;
|
|
2604
|
-
}
|