react-native-nitro-storage 0.1.4 → 0.3.1
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 +432 -345
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +191 -3
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +21 -41
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +181 -29
- package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
- package/app.plugin.js +9 -7
- package/cpp/bindings/HybridStorage.cpp +239 -10
- package/cpp/bindings/HybridStorage.hpp +10 -0
- package/cpp/core/NativeStorageAdapter.hpp +22 -0
- package/ios/IOSStorageAdapterCpp.hpp +25 -0
- package/ios/IOSStorageAdapterCpp.mm +315 -33
- package/lib/commonjs/Storage.types.js +23 -1
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/index.js +680 -68
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +801 -133
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +112 -0
- package/lib/commonjs/internal.js.map +1 -0
- package/lib/module/Storage.types.js +22 -0
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/index.js +660 -71
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +766 -125
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +100 -0
- package/lib/module/internal.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +10 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/Storage.types.d.ts +20 -0
- package/lib/typescript/Storage.types.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +68 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +79 -13
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +21 -0
- package/lib/typescript/internal.d.ts.map +1 -0
- package/lib/typescript/migration.d.ts +2 -3
- package/lib/typescript/migration.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +10 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +10 -0
- package/package.json +22 -8
- package/src/Storage.nitro.ts +11 -2
- package/src/Storage.types.ts +22 -0
- package/src/index.ts +943 -84
- package/src/index.web.ts +1082 -137
- package/src/internal.ts +144 -0
- package/src/migration.ts +3 -3
package/src/index.ts
CHANGED
|
@@ -1,10 +1,74 @@
|
|
|
1
|
-
import { useSyncExternalStore } from "react";
|
|
1
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { NitroModules } from "react-native-nitro-modules";
|
|
3
3
|
import type { Storage } from "./Storage.nitro";
|
|
4
|
-
import { StorageScope } from "./Storage.types";
|
|
4
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
5
|
+
import {
|
|
6
|
+
MIGRATION_VERSION_KEY,
|
|
7
|
+
type StoredEnvelope,
|
|
8
|
+
isStoredEnvelope,
|
|
9
|
+
assertBatchScope,
|
|
10
|
+
assertValidScope,
|
|
11
|
+
decodeNativeBatchValue,
|
|
12
|
+
serializeWithPrimitiveFastPath,
|
|
13
|
+
deserializeWithPrimitiveFastPath,
|
|
14
|
+
prefixKey,
|
|
15
|
+
isNamespaced,
|
|
16
|
+
} from "./internal";
|
|
5
17
|
|
|
6
|
-
export { StorageScope } from "./Storage.types";
|
|
18
|
+
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
7
19
|
export type { Storage } from "./Storage.nitro";
|
|
20
|
+
export { migrateFromMMKV } from "./migration";
|
|
21
|
+
|
|
22
|
+
export type Validator<T> = (value: unknown) => value is T;
|
|
23
|
+
export type ExpirationConfig = {
|
|
24
|
+
ttlMs: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MigrationContext = {
|
|
28
|
+
scope: StorageScope;
|
|
29
|
+
getRaw: (key: string) => string | undefined;
|
|
30
|
+
setRaw: (key: string, value: string) => void;
|
|
31
|
+
removeRaw: (key: string) => void;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type Migration = (context: MigrationContext) => void;
|
|
35
|
+
|
|
36
|
+
export type TransactionContext = {
|
|
37
|
+
scope: StorageScope;
|
|
38
|
+
getRaw: (key: string) => string | undefined;
|
|
39
|
+
setRaw: (key: string, value: string) => void;
|
|
40
|
+
removeRaw: (key: string) => void;
|
|
41
|
+
getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
|
|
42
|
+
setItem: <T>(
|
|
43
|
+
item: Pick<StorageItem<T>, "scope" | "key" | "set">,
|
|
44
|
+
value: T,
|
|
45
|
+
) => void;
|
|
46
|
+
removeItem: (
|
|
47
|
+
item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
|
|
48
|
+
) => void;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
52
|
+
type RawBatchPathItem = {
|
|
53
|
+
_hasValidation?: boolean;
|
|
54
|
+
_hasExpiration?: boolean;
|
|
55
|
+
_isBiometric?: boolean;
|
|
56
|
+
_secureAccessControl?: AccessControl;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function asInternal(item: StorageItem<any>): StorageItemInternal<any> {
|
|
60
|
+
return item as unknown as StorageItemInternal<any>;
|
|
61
|
+
}
|
|
62
|
+
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
63
|
+
type PendingSecureWrite = { key: string; value: string | undefined };
|
|
64
|
+
|
|
65
|
+
const registeredMigrations = new Map<number, Migration>();
|
|
66
|
+
const runMicrotask =
|
|
67
|
+
typeof queueMicrotask === "function"
|
|
68
|
+
? queueMicrotask
|
|
69
|
+
: (task: () => void) => {
|
|
70
|
+
Promise.resolve().then(task);
|
|
71
|
+
};
|
|
8
72
|
|
|
9
73
|
let _storageModule: Storage | null = null;
|
|
10
74
|
|
|
@@ -12,23 +76,268 @@ function getStorageModule(): Storage {
|
|
|
12
76
|
if (!_storageModule) {
|
|
13
77
|
_storageModule = NitroModules.createHybridObject<Storage>("Storage");
|
|
14
78
|
}
|
|
15
|
-
return _storageModule
|
|
79
|
+
return _storageModule;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const memoryStore = new Map<string, unknown>();
|
|
83
|
+
const memoryListeners: KeyListenerRegistry = new Map();
|
|
84
|
+
const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
85
|
+
[StorageScope.Disk, new Map()],
|
|
86
|
+
[StorageScope.Secure, new Map()],
|
|
87
|
+
]);
|
|
88
|
+
const scopedUnsubscribers = new Map<NonMemoryScope, () => void>();
|
|
89
|
+
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
90
|
+
[
|
|
91
|
+
[StorageScope.Disk, new Map()],
|
|
92
|
+
[StorageScope.Secure, new Map()],
|
|
93
|
+
],
|
|
94
|
+
);
|
|
95
|
+
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
96
|
+
let secureFlushScheduled = false;
|
|
97
|
+
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
98
|
+
|
|
99
|
+
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
100
|
+
return scopedListeners.get(scope)!;
|
|
16
101
|
}
|
|
17
102
|
|
|
18
|
-
|
|
19
|
-
|
|
103
|
+
function getScopeRawCache(
|
|
104
|
+
scope: NonMemoryScope,
|
|
105
|
+
): Map<string, string | undefined> {
|
|
106
|
+
return scopedRawCache.get(scope)!;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function cacheRawValue(
|
|
110
|
+
scope: NonMemoryScope,
|
|
111
|
+
key: string,
|
|
112
|
+
value: string | undefined,
|
|
113
|
+
): void {
|
|
114
|
+
getScopeRawCache(scope).set(key, value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function readCachedRawValue(
|
|
118
|
+
scope: NonMemoryScope,
|
|
119
|
+
key: string,
|
|
120
|
+
): string | undefined {
|
|
121
|
+
return getScopeRawCache(scope).get(key);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function hasCachedRawValue(scope: NonMemoryScope, key: string): boolean {
|
|
125
|
+
return getScopeRawCache(scope).has(key);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function clearScopeRawCache(scope: NonMemoryScope): void {
|
|
129
|
+
getScopeRawCache(scope).clear();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function notifyKeyListeners(registry: KeyListenerRegistry, key: string): void {
|
|
133
|
+
registry.get(key)?.forEach((listener) => listener());
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function notifyAllListeners(registry: KeyListenerRegistry): void {
|
|
137
|
+
registry.forEach((listeners) => {
|
|
138
|
+
listeners.forEach((listener) => listener());
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function addKeyListener(
|
|
143
|
+
registry: KeyListenerRegistry,
|
|
144
|
+
key: string,
|
|
145
|
+
listener: () => void,
|
|
146
|
+
): () => void {
|
|
147
|
+
let listeners = registry.get(key);
|
|
148
|
+
if (!listeners) {
|
|
149
|
+
listeners = new Set();
|
|
150
|
+
registry.set(key, listeners);
|
|
151
|
+
}
|
|
152
|
+
listeners.add(listener);
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
const scopedListeners = registry.get(key);
|
|
156
|
+
if (!scopedListeners) {
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
scopedListeners.delete(listener);
|
|
160
|
+
if (scopedListeners.size === 0) {
|
|
161
|
+
registry.delete(key);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
20
165
|
|
|
21
|
-
function
|
|
22
|
-
|
|
166
|
+
function readPendingSecureWrite(key: string): string | undefined {
|
|
167
|
+
return pendingSecureWrites.get(key)?.value;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function hasPendingSecureWrite(key: string): boolean {
|
|
171
|
+
return pendingSecureWrites.has(key);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function clearPendingSecureWrite(key: string): void {
|
|
175
|
+
pendingSecureWrites.delete(key);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function flushSecureWrites(): void {
|
|
179
|
+
secureFlushScheduled = false;
|
|
180
|
+
|
|
181
|
+
if (pendingSecureWrites.size === 0) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const writes = Array.from(pendingSecureWrites.values());
|
|
186
|
+
pendingSecureWrites.clear();
|
|
187
|
+
|
|
188
|
+
const keysToSet: string[] = [];
|
|
189
|
+
const valuesToSet: string[] = [];
|
|
190
|
+
const keysToRemove: string[] = [];
|
|
191
|
+
|
|
192
|
+
writes.forEach(({ key, value }) => {
|
|
193
|
+
if (value === undefined) {
|
|
194
|
+
keysToRemove.push(key);
|
|
195
|
+
} else {
|
|
196
|
+
keysToSet.push(key);
|
|
197
|
+
valuesToSet.push(value);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const storageModule = getStorageModule();
|
|
202
|
+
storageModule.setSecureAccessControl(secureDefaultAccessControl);
|
|
203
|
+
if (keysToSet.length > 0) {
|
|
204
|
+
storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
|
|
205
|
+
}
|
|
206
|
+
if (keysToRemove.length > 0) {
|
|
207
|
+
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function scheduleSecureWrite(key: string, value: string | undefined): void {
|
|
212
|
+
pendingSecureWrites.set(key, { key, value });
|
|
213
|
+
if (secureFlushScheduled) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
secureFlushScheduled = true;
|
|
217
|
+
runMicrotask(flushSecureWrites);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function ensureNativeScopeSubscription(scope: NonMemoryScope): void {
|
|
221
|
+
if (scopedUnsubscribers.has(scope)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
|
|
226
|
+
if (scope === StorageScope.Secure) {
|
|
227
|
+
if (key === "") {
|
|
228
|
+
pendingSecureWrites.clear();
|
|
229
|
+
} else {
|
|
230
|
+
clearPendingSecureWrite(key);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (key === "") {
|
|
235
|
+
clearScopeRawCache(scope);
|
|
236
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
cacheRawValue(scope, key, value);
|
|
241
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
242
|
+
});
|
|
243
|
+
scopedUnsubscribers.set(scope, unsubscribe);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function maybeCleanupNativeScopeSubscription(scope: NonMemoryScope): void {
|
|
247
|
+
const listeners = getScopedListeners(scope);
|
|
248
|
+
if (listeners.size > 0) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const unsubscribe = scopedUnsubscribers.get(scope);
|
|
253
|
+
if (!unsubscribe) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
unsubscribe();
|
|
258
|
+
scopedUnsubscribers.delete(scope);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
262
|
+
assertValidScope(scope);
|
|
263
|
+
if (scope === StorageScope.Memory) {
|
|
264
|
+
const value = memoryStore.get(key);
|
|
265
|
+
return typeof value === "string" ? value : undefined;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
269
|
+
return readPendingSecureWrite(key);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return getStorageModule().get(key, scope);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
276
|
+
assertValidScope(scope);
|
|
277
|
+
if (scope === StorageScope.Memory) {
|
|
278
|
+
memoryStore.set(key, value);
|
|
279
|
+
notifyKeyListeners(memoryListeners, key);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (scope === StorageScope.Secure) {
|
|
284
|
+
flushSecureWrites();
|
|
285
|
+
clearPendingSecureWrite(key);
|
|
286
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
getStorageModule().set(key, value, scope);
|
|
290
|
+
cacheRawValue(scope, key, value);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function removeRawValue(key: string, scope: StorageScope): void {
|
|
294
|
+
assertValidScope(scope);
|
|
295
|
+
if (scope === StorageScope.Memory) {
|
|
296
|
+
memoryStore.delete(key);
|
|
297
|
+
notifyKeyListeners(memoryListeners, key);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (scope === StorageScope.Secure) {
|
|
302
|
+
flushSecureWrites();
|
|
303
|
+
clearPendingSecureWrite(key);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
getStorageModule().remove(key, scope);
|
|
307
|
+
cacheRawValue(scope, key, undefined);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readMigrationVersion(scope: StorageScope): number {
|
|
311
|
+
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
312
|
+
if (raw === undefined) {
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const parsed = Number.parseInt(raw, 10);
|
|
317
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
321
|
+
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
23
322
|
}
|
|
24
323
|
|
|
25
324
|
export const storage = {
|
|
26
325
|
clear: (scope: StorageScope) => {
|
|
27
326
|
if (scope === StorageScope.Memory) {
|
|
28
327
|
memoryStore.clear();
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
328
|
+
notifyAllListeners(memoryListeners);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (scope === StorageScope.Secure) {
|
|
333
|
+
flushSecureWrites();
|
|
334
|
+
pendingSecureWrites.clear();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
clearScopeRawCache(scope);
|
|
338
|
+
getStorageModule().clear(scope);
|
|
339
|
+
if (scope === StorageScope.Secure) {
|
|
340
|
+
getStorageModule().clearSecureBiometric();
|
|
32
341
|
}
|
|
33
342
|
},
|
|
34
343
|
clearAll: () => {
|
|
@@ -36,6 +345,79 @@ export const storage = {
|
|
|
36
345
|
storage.clear(StorageScope.Disk);
|
|
37
346
|
storage.clear(StorageScope.Secure);
|
|
38
347
|
},
|
|
348
|
+
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
349
|
+
assertValidScope(scope);
|
|
350
|
+
if (scope === StorageScope.Memory) {
|
|
351
|
+
for (const key of memoryStore.keys()) {
|
|
352
|
+
if (isNamespaced(key, namespace)) {
|
|
353
|
+
memoryStore.delete(key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
notifyAllListeners(memoryListeners);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (scope === StorageScope.Secure) {
|
|
360
|
+
flushSecureWrites();
|
|
361
|
+
}
|
|
362
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
363
|
+
const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
|
|
364
|
+
if (namespacedKeys.length > 0) {
|
|
365
|
+
getStorageModule().removeBatch(namespacedKeys, scope);
|
|
366
|
+
namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
|
|
367
|
+
if (scope === StorageScope.Secure) {
|
|
368
|
+
namespacedKeys.forEach((k) => clearPendingSecureWrite(k));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
clearBiometric: () => {
|
|
373
|
+
getStorageModule().clearSecureBiometric();
|
|
374
|
+
},
|
|
375
|
+
has: (key: string, scope: StorageScope): boolean => {
|
|
376
|
+
assertValidScope(scope);
|
|
377
|
+
if (scope === StorageScope.Memory) {
|
|
378
|
+
return memoryStore.has(key);
|
|
379
|
+
}
|
|
380
|
+
return getStorageModule().has(key, scope);
|
|
381
|
+
},
|
|
382
|
+
getAllKeys: (scope: StorageScope): string[] => {
|
|
383
|
+
assertValidScope(scope);
|
|
384
|
+
if (scope === StorageScope.Memory) {
|
|
385
|
+
return Array.from(memoryStore.keys());
|
|
386
|
+
}
|
|
387
|
+
return getStorageModule().getAllKeys(scope);
|
|
388
|
+
},
|
|
389
|
+
getAll: (scope: StorageScope): Record<string, string> => {
|
|
390
|
+
assertValidScope(scope);
|
|
391
|
+
const result: Record<string, string> = {};
|
|
392
|
+
if (scope === StorageScope.Memory) {
|
|
393
|
+
memoryStore.forEach((value, key) => {
|
|
394
|
+
if (typeof value === "string") result[key] = value;
|
|
395
|
+
});
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
399
|
+
if (keys.length === 0) return result;
|
|
400
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
401
|
+
keys.forEach((key, idx) => {
|
|
402
|
+
const val = decodeNativeBatchValue(values[idx]);
|
|
403
|
+
if (val !== undefined) result[key] = val;
|
|
404
|
+
});
|
|
405
|
+
return result;
|
|
406
|
+
},
|
|
407
|
+
size: (scope: StorageScope): number => {
|
|
408
|
+
assertValidScope(scope);
|
|
409
|
+
if (scope === StorageScope.Memory) {
|
|
410
|
+
return memoryStore.size;
|
|
411
|
+
}
|
|
412
|
+
return getStorageModule().size(scope);
|
|
413
|
+
},
|
|
414
|
+
setAccessControl: (level: AccessControl) => {
|
|
415
|
+
secureDefaultAccessControl = level;
|
|
416
|
+
getStorageModule().setSecureAccessControl(level);
|
|
417
|
+
},
|
|
418
|
+
setKeychainAccessGroup: (group: string) => {
|
|
419
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
420
|
+
},
|
|
39
421
|
};
|
|
40
422
|
|
|
41
423
|
export interface StorageItemConfig<T> {
|
|
@@ -44,90 +426,301 @@ export interface StorageItemConfig<T> {
|
|
|
44
426
|
defaultValue?: T;
|
|
45
427
|
serialize?: (value: T) => string;
|
|
46
428
|
deserialize?: (value: string) => T;
|
|
429
|
+
validate?: Validator<T>;
|
|
430
|
+
onValidationError?: (invalidValue: unknown) => T;
|
|
431
|
+
expiration?: ExpirationConfig;
|
|
432
|
+
onExpired?: (key: string) => void;
|
|
433
|
+
readCache?: boolean;
|
|
434
|
+
coalesceSecureWrites?: boolean;
|
|
435
|
+
namespace?: string;
|
|
436
|
+
biometric?: boolean;
|
|
437
|
+
accessControl?: AccessControl;
|
|
47
438
|
}
|
|
48
439
|
|
|
49
440
|
export interface StorageItem<T> {
|
|
50
441
|
get: () => T;
|
|
51
442
|
set: (value: T | ((prev: T) => T)) => void;
|
|
52
443
|
delete: () => void;
|
|
444
|
+
has: () => boolean;
|
|
53
445
|
subscribe: (callback: () => void) => () => void;
|
|
54
446
|
serialize: (value: T) => string;
|
|
55
447
|
deserialize: (value: string) => T;
|
|
56
|
-
_triggerListeners: () => void;
|
|
57
448
|
scope: StorageScope;
|
|
58
449
|
key: string;
|
|
59
450
|
}
|
|
60
451
|
|
|
452
|
+
type StorageItemInternal<T> = StorageItem<T> & {
|
|
453
|
+
_triggerListeners: () => void;
|
|
454
|
+
_hasValidation: boolean;
|
|
455
|
+
_hasExpiration: boolean;
|
|
456
|
+
_readCacheEnabled: boolean;
|
|
457
|
+
_isBiometric: boolean;
|
|
458
|
+
_secureAccessControl?: AccessControl;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
462
|
+
return (
|
|
463
|
+
item._hasExpiration === false &&
|
|
464
|
+
item._hasValidation === false &&
|
|
465
|
+
item._isBiometric !== true &&
|
|
466
|
+
item._secureAccessControl === undefined
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
|
|
61
470
|
function defaultSerialize<T>(value: T): string {
|
|
62
|
-
return
|
|
471
|
+
return serializeWithPrimitiveFastPath(value);
|
|
63
472
|
}
|
|
64
473
|
|
|
65
474
|
function defaultDeserialize<T>(value: string): T {
|
|
66
|
-
return
|
|
475
|
+
return deserializeWithPrimitiveFastPath(value);
|
|
67
476
|
}
|
|
68
477
|
|
|
69
478
|
export function createStorageItem<T = undefined>(
|
|
70
|
-
config: StorageItemConfig<T
|
|
479
|
+
config: StorageItemConfig<T>,
|
|
71
480
|
): StorageItem<T> {
|
|
481
|
+
const storageKey = prefixKey(config.namespace, config.key);
|
|
72
482
|
const serialize = config.serialize ?? defaultSerialize;
|
|
73
483
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
74
484
|
const isMemory = config.scope === StorageScope.Memory;
|
|
485
|
+
const isBiometric =
|
|
486
|
+
config.biometric === true && config.scope === StorageScope.Secure;
|
|
487
|
+
const secureAccessControl = config.accessControl;
|
|
488
|
+
const validate = config.validate;
|
|
489
|
+
const onValidationError = config.onValidationError;
|
|
490
|
+
const expiration = config.expiration;
|
|
491
|
+
const onExpired = config.onExpired;
|
|
492
|
+
const expirationTtlMs = expiration?.ttlMs;
|
|
493
|
+
const memoryExpiration =
|
|
494
|
+
expiration && isMemory ? new Map<string, number>() : null;
|
|
495
|
+
const readCache = !isMemory && config.readCache === true;
|
|
496
|
+
const coalesceSecureWrites =
|
|
497
|
+
config.scope === StorageScope.Secure &&
|
|
498
|
+
config.coalesceSecureWrites === true &&
|
|
499
|
+
!isBiometric &&
|
|
500
|
+
secureAccessControl === undefined;
|
|
501
|
+
const nonMemoryScope: NonMemoryScope | null =
|
|
502
|
+
config.scope === StorageScope.Disk
|
|
503
|
+
? StorageScope.Disk
|
|
504
|
+
: config.scope === StorageScope.Secure
|
|
505
|
+
? StorageScope.Secure
|
|
506
|
+
: null;
|
|
507
|
+
|
|
508
|
+
if (expiration && expiration.ttlMs <= 0) {
|
|
509
|
+
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
510
|
+
}
|
|
75
511
|
|
|
76
512
|
const listeners = new Set<() => void>();
|
|
77
513
|
let unsubscribe: (() => void) | null = null;
|
|
514
|
+
let lastRaw: unknown = undefined;
|
|
515
|
+
let lastValue: T | undefined;
|
|
516
|
+
let hasLastValue = false;
|
|
517
|
+
|
|
518
|
+
const invalidateParsedCache = () => {
|
|
519
|
+
lastRaw = undefined;
|
|
520
|
+
lastValue = undefined;
|
|
521
|
+
hasLastValue = false;
|
|
522
|
+
};
|
|
78
523
|
|
|
79
524
|
const ensureSubscription = () => {
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
525
|
+
if (unsubscribe) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const listener = () => {
|
|
530
|
+
invalidateParsedCache();
|
|
531
|
+
listeners.forEach((callback) => callback());
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
if (isMemory) {
|
|
535
|
+
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
ensureNativeScopeSubscription(nonMemoryScope!);
|
|
540
|
+
unsubscribe = addKeyListener(
|
|
541
|
+
getScopedListeners(nonMemoryScope!),
|
|
542
|
+
storageKey,
|
|
543
|
+
listener,
|
|
544
|
+
);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const readStoredRaw = (): unknown => {
|
|
548
|
+
if (isMemory) {
|
|
549
|
+
if (memoryExpiration) {
|
|
550
|
+
const expiresAt = memoryExpiration.get(storageKey);
|
|
551
|
+
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
552
|
+
memoryExpiration.delete(storageKey);
|
|
553
|
+
memoryStore.delete(storageKey);
|
|
554
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
555
|
+
onExpired?.(storageKey);
|
|
556
|
+
return undefined;
|
|
557
|
+
}
|
|
99
558
|
}
|
|
559
|
+
return memoryStore.get(storageKey) as T | undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (
|
|
563
|
+
nonMemoryScope === StorageScope.Secure &&
|
|
564
|
+
!isBiometric &&
|
|
565
|
+
hasPendingSecureWrite(storageKey)
|
|
566
|
+
) {
|
|
567
|
+
return readPendingSecureWrite(storageKey);
|
|
100
568
|
}
|
|
569
|
+
|
|
570
|
+
if (readCache) {
|
|
571
|
+
if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
|
|
572
|
+
return readCachedRawValue(nonMemoryScope!, storageKey);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (isBiometric) {
|
|
577
|
+
return getStorageModule().getSecureBiometric(storageKey);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const raw = getStorageModule().get(storageKey, config.scope);
|
|
581
|
+
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
582
|
+
return raw;
|
|
101
583
|
};
|
|
102
584
|
|
|
103
|
-
|
|
104
|
-
|
|
585
|
+
const writeStoredRaw = (rawValue: string): void => {
|
|
586
|
+
if (isBiometric) {
|
|
587
|
+
getStorageModule().setSecureBiometric(storageKey, rawValue);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
105
590
|
|
|
106
|
-
|
|
107
|
-
let raw: string | undefined;
|
|
591
|
+
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
108
592
|
|
|
593
|
+
if (coalesceSecureWrites) {
|
|
594
|
+
scheduleSecureWrite(storageKey, rawValue);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
599
|
+
clearPendingSecureWrite(storageKey);
|
|
600
|
+
getStorageModule().setSecureAccessControl(
|
|
601
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
602
|
+
);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
getStorageModule().set(storageKey, rawValue, config.scope);
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const removeStoredRaw = (): void => {
|
|
609
|
+
if (isBiometric) {
|
|
610
|
+
getStorageModule().deleteSecureBiometric(storageKey);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
615
|
+
|
|
616
|
+
if (coalesceSecureWrites) {
|
|
617
|
+
scheduleSecureWrite(storageKey, undefined);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
622
|
+
clearPendingSecureWrite(storageKey);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
getStorageModule().remove(storageKey, config.scope);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
const writeValueWithoutValidation = (value: T): void => {
|
|
109
629
|
if (isMemory) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
630
|
+
if (memoryExpiration) {
|
|
631
|
+
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
632
|
+
}
|
|
633
|
+
memoryStore.set(storageKey, value);
|
|
634
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
635
|
+
return;
|
|
113
636
|
}
|
|
114
637
|
|
|
115
|
-
|
|
116
|
-
|
|
638
|
+
const serialized = serialize(value);
|
|
639
|
+
if (expiration) {
|
|
640
|
+
const envelope: StoredEnvelope = {
|
|
641
|
+
__nitroStorageEnvelope: true,
|
|
642
|
+
expiresAt: Date.now() + expiration.ttlMs,
|
|
643
|
+
payload: serialized,
|
|
644
|
+
};
|
|
645
|
+
writeStoredRaw(JSON.stringify(envelope));
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
writeStoredRaw(serialized);
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const resolveInvalidValue = (invalidValue: unknown): T => {
|
|
653
|
+
if (onValidationError) {
|
|
654
|
+
return onValidationError(invalidValue);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
return config.defaultValue as T;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
const ensureValidatedValue = (
|
|
661
|
+
candidate: unknown,
|
|
662
|
+
hadStoredValue: boolean,
|
|
663
|
+
): T => {
|
|
664
|
+
if (!validate || validate(candidate)) {
|
|
665
|
+
return candidate as T;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const resolved = resolveInvalidValue(candidate);
|
|
669
|
+
if (validate && !validate(resolved)) {
|
|
670
|
+
return config.defaultValue as T;
|
|
671
|
+
}
|
|
672
|
+
if (hadStoredValue) {
|
|
673
|
+
writeValueWithoutValidation(resolved);
|
|
674
|
+
}
|
|
675
|
+
return resolved;
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const get = (): T => {
|
|
679
|
+
const raw = readStoredRaw();
|
|
680
|
+
|
|
681
|
+
const canUseCachedValue = !expiration && !memoryExpiration;
|
|
682
|
+
if (canUseCachedValue && raw === lastRaw && hasLastValue) {
|
|
683
|
+
return lastValue as T;
|
|
117
684
|
}
|
|
118
685
|
|
|
119
686
|
lastRaw = raw;
|
|
120
687
|
|
|
121
688
|
if (raw === undefined) {
|
|
122
|
-
lastValue = config.defaultValue
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
689
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
690
|
+
hasLastValue = true;
|
|
691
|
+
return lastValue;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
if (isMemory) {
|
|
695
|
+
lastValue = ensureValidatedValue(raw, true);
|
|
696
|
+
hasLastValue = true;
|
|
697
|
+
return lastValue;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
let deserializableRaw = raw as string;
|
|
701
|
+
|
|
702
|
+
if (expiration) {
|
|
703
|
+
try {
|
|
704
|
+
const parsed = JSON.parse(raw as string) as unknown;
|
|
705
|
+
if (isStoredEnvelope(parsed)) {
|
|
706
|
+
if (parsed.expiresAt <= Date.now()) {
|
|
707
|
+
removeStoredRaw();
|
|
708
|
+
invalidateParsedCache();
|
|
709
|
+
onExpired?.(storageKey);
|
|
710
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
711
|
+
hasLastValue = true;
|
|
712
|
+
return lastValue;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
deserializableRaw = parsed.payload;
|
|
716
|
+
}
|
|
717
|
+
} catch {
|
|
718
|
+
// Keep backward compatibility with legacy raw values.
|
|
128
719
|
}
|
|
129
720
|
}
|
|
130
721
|
|
|
722
|
+
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
723
|
+
hasLastValue = true;
|
|
131
724
|
return lastValue;
|
|
132
725
|
};
|
|
133
726
|
|
|
@@ -138,22 +731,36 @@ export function createStorageItem<T = undefined>(
|
|
|
138
731
|
? (valueOrFn as (prev: T) => T)(currentValue)
|
|
139
732
|
: valueOrFn;
|
|
140
733
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
734
|
+
invalidateParsedCache();
|
|
735
|
+
|
|
736
|
+
if (validate && !validate(newValue)) {
|
|
737
|
+
throw new Error(
|
|
738
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
739
|
+
);
|
|
147
740
|
}
|
|
741
|
+
|
|
742
|
+
writeValueWithoutValidation(newValue);
|
|
148
743
|
};
|
|
149
744
|
|
|
150
745
|
const deleteItem = (): void => {
|
|
746
|
+
invalidateParsedCache();
|
|
747
|
+
|
|
151
748
|
if (isMemory) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
749
|
+
if (memoryExpiration) {
|
|
750
|
+
memoryExpiration.delete(storageKey);
|
|
751
|
+
}
|
|
752
|
+
memoryStore.delete(storageKey);
|
|
753
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
754
|
+
return;
|
|
156
755
|
}
|
|
756
|
+
|
|
757
|
+
removeStoredRaw();
|
|
758
|
+
};
|
|
759
|
+
|
|
760
|
+
const hasItem = (): boolean => {
|
|
761
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
762
|
+
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
763
|
+
return getStorageModule().has(storageKey, config.scope);
|
|
157
764
|
};
|
|
158
765
|
|
|
159
766
|
const subscribe = (callback: () => void): (() => void) => {
|
|
@@ -163,52 +770,150 @@ export function createStorageItem<T = undefined>(
|
|
|
163
770
|
listeners.delete(callback);
|
|
164
771
|
if (listeners.size === 0 && unsubscribe) {
|
|
165
772
|
unsubscribe();
|
|
773
|
+
if (!isMemory) {
|
|
774
|
+
maybeCleanupNativeScopeSubscription(nonMemoryScope!);
|
|
775
|
+
}
|
|
166
776
|
unsubscribe = null;
|
|
167
777
|
}
|
|
168
778
|
};
|
|
169
779
|
};
|
|
170
780
|
|
|
171
|
-
|
|
781
|
+
const storageItem: StorageItemInternal<T> = {
|
|
172
782
|
get,
|
|
173
783
|
set,
|
|
174
784
|
delete: deleteItem,
|
|
785
|
+
has: hasItem,
|
|
175
786
|
subscribe,
|
|
176
787
|
serialize,
|
|
177
788
|
deserialize,
|
|
178
789
|
_triggerListeners: () => {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
listeners.forEach((l) => l());
|
|
790
|
+
invalidateParsedCache();
|
|
791
|
+
listeners.forEach((listener) => listener());
|
|
182
792
|
},
|
|
793
|
+
_hasValidation: validate !== undefined,
|
|
794
|
+
_hasExpiration: expiration !== undefined,
|
|
795
|
+
_readCacheEnabled: readCache,
|
|
796
|
+
_isBiometric: isBiometric,
|
|
797
|
+
_secureAccessControl: secureAccessControl,
|
|
183
798
|
scope: config.scope,
|
|
184
|
-
key:
|
|
799
|
+
key: storageKey,
|
|
185
800
|
};
|
|
801
|
+
|
|
802
|
+
return storageItem as StorageItem<T>;
|
|
186
803
|
}
|
|
187
804
|
|
|
188
805
|
export function useStorage<T>(
|
|
189
|
-
item: StorageItem<T
|
|
806
|
+
item: StorageItem<T>,
|
|
190
807
|
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
191
808
|
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
192
809
|
return [value, item.set];
|
|
193
810
|
}
|
|
194
811
|
|
|
812
|
+
export function useStorageSelector<T, TSelected>(
|
|
813
|
+
item: StorageItem<T>,
|
|
814
|
+
selector: (value: T) => TSelected,
|
|
815
|
+
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
|
|
816
|
+
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
817
|
+
const selectedRef = useRef<
|
|
818
|
+
{ hasValue: false } | { hasValue: true; value: TSelected }
|
|
819
|
+
>({
|
|
820
|
+
hasValue: false,
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
const getSelectedSnapshot = () => {
|
|
824
|
+
const nextSelected = selector(item.get());
|
|
825
|
+
const current = selectedRef.current;
|
|
826
|
+
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
827
|
+
return current.value;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
selectedRef.current = { hasValue: true, value: nextSelected };
|
|
831
|
+
return nextSelected;
|
|
832
|
+
};
|
|
833
|
+
|
|
834
|
+
const selectedValue = useSyncExternalStore(
|
|
835
|
+
item.subscribe,
|
|
836
|
+
getSelectedSnapshot,
|
|
837
|
+
getSelectedSnapshot,
|
|
838
|
+
);
|
|
839
|
+
return [selectedValue, item.set];
|
|
840
|
+
}
|
|
841
|
+
|
|
195
842
|
export function useSetStorage<T>(item: StorageItem<T>) {
|
|
196
843
|
return item.set;
|
|
197
844
|
}
|
|
198
845
|
|
|
846
|
+
type BatchReadItem<T> = Pick<
|
|
847
|
+
StorageItem<T>,
|
|
848
|
+
"key" | "scope" | "get" | "deserialize"
|
|
849
|
+
> & {
|
|
850
|
+
_hasValidation?: boolean;
|
|
851
|
+
_hasExpiration?: boolean;
|
|
852
|
+
_readCacheEnabled?: boolean;
|
|
853
|
+
_isBiometric?: boolean;
|
|
854
|
+
_secureAccessControl?: AccessControl;
|
|
855
|
+
};
|
|
856
|
+
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
857
|
+
|
|
858
|
+
export type StorageBatchSetItem<T> = {
|
|
859
|
+
item: StorageItem<T>;
|
|
860
|
+
value: T;
|
|
861
|
+
};
|
|
862
|
+
|
|
199
863
|
export function getBatch(
|
|
200
|
-
items:
|
|
201
|
-
scope: StorageScope
|
|
202
|
-
):
|
|
864
|
+
items: readonly BatchReadItem<unknown>[],
|
|
865
|
+
scope: StorageScope,
|
|
866
|
+
): unknown[] {
|
|
867
|
+
assertBatchScope(items, scope);
|
|
868
|
+
|
|
203
869
|
if (scope === StorageScope.Memory) {
|
|
204
870
|
return items.map((item) => item.get());
|
|
205
871
|
}
|
|
206
872
|
|
|
207
|
-
const
|
|
208
|
-
|
|
873
|
+
const useRawBatchPath = items.every((item) => canUseRawBatchPath(item));
|
|
874
|
+
if (!useRawBatchPath) {
|
|
875
|
+
return items.map((item) => item.get());
|
|
876
|
+
}
|
|
877
|
+
const useBatchCache = items.every((item) => item._readCacheEnabled === true);
|
|
878
|
+
|
|
879
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
880
|
+
const keysToFetch: string[] = [];
|
|
881
|
+
const keyIndexes: number[] = [];
|
|
882
|
+
|
|
883
|
+
items.forEach((item, index) => {
|
|
884
|
+
if (scope === StorageScope.Secure) {
|
|
885
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
886
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (useBatchCache) {
|
|
892
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
893
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
keysToFetch.push(item.key);
|
|
899
|
+
keyIndexes.push(index);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
if (keysToFetch.length > 0) {
|
|
903
|
+
const fetchedValues = getStorageModule()
|
|
904
|
+
.getBatch(keysToFetch, scope)
|
|
905
|
+
.map((value) => decodeNativeBatchValue(value));
|
|
209
906
|
|
|
210
|
-
|
|
211
|
-
|
|
907
|
+
fetchedValues.forEach((value, index) => {
|
|
908
|
+
const key = keysToFetch[index];
|
|
909
|
+
const targetIndex = keyIndexes[index];
|
|
910
|
+
rawValues[targetIndex] = value;
|
|
911
|
+
cacheRawValue(scope, key, value);
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return items.map((item, index) => {
|
|
916
|
+
const raw = rawValues[index];
|
|
212
917
|
if (raw === undefined) {
|
|
213
918
|
return item.get();
|
|
214
919
|
}
|
|
@@ -216,35 +921,189 @@ export function getBatch(
|
|
|
216
921
|
});
|
|
217
922
|
}
|
|
218
923
|
|
|
219
|
-
export function setBatch(
|
|
220
|
-
items:
|
|
221
|
-
scope: StorageScope
|
|
924
|
+
export function setBatch<T>(
|
|
925
|
+
items: readonly StorageBatchSetItem<T>[],
|
|
926
|
+
scope: StorageScope,
|
|
222
927
|
): void {
|
|
928
|
+
assertBatchScope(
|
|
929
|
+
items.map((batchEntry) => batchEntry.item),
|
|
930
|
+
scope,
|
|
931
|
+
);
|
|
932
|
+
|
|
223
933
|
if (scope === StorageScope.Memory) {
|
|
224
934
|
items.forEach(({ item, value }) => item.set(value));
|
|
225
935
|
return;
|
|
226
936
|
}
|
|
227
937
|
|
|
228
|
-
const
|
|
229
|
-
|
|
938
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
939
|
+
canUseRawBatchPath(asInternal(item)),
|
|
940
|
+
);
|
|
941
|
+
if (!useRawBatchPath) {
|
|
942
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
230
945
|
|
|
231
|
-
|
|
946
|
+
const keys = items.map((entry) => entry.item.key);
|
|
947
|
+
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
232
948
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
949
|
+
if (scope === StorageScope.Secure) {
|
|
950
|
+
flushSecureWrites();
|
|
951
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
952
|
+
}
|
|
953
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
954
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
236
955
|
}
|
|
237
956
|
|
|
238
957
|
export function removeBatch(
|
|
239
|
-
items:
|
|
240
|
-
scope: StorageScope
|
|
958
|
+
items: readonly BatchRemoveItem[],
|
|
959
|
+
scope: StorageScope,
|
|
241
960
|
): void {
|
|
961
|
+
assertBatchScope(items, scope);
|
|
962
|
+
|
|
242
963
|
if (scope === StorageScope.Memory) {
|
|
243
964
|
items.forEach((item) => item.delete());
|
|
244
965
|
return;
|
|
245
966
|
}
|
|
246
967
|
|
|
247
968
|
const keys = items.map((item) => item.key);
|
|
969
|
+
if (scope === StorageScope.Secure) {
|
|
970
|
+
flushSecureWrites();
|
|
971
|
+
}
|
|
248
972
|
getStorageModule().removeBatch(keys, scope);
|
|
249
|
-
|
|
973
|
+
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
export function registerMigration(version: number, migration: Migration): void {
|
|
977
|
+
if (!Number.isInteger(version) || version <= 0) {
|
|
978
|
+
throw new Error("Migration version must be a positive integer.");
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
if (registeredMigrations.has(version)) {
|
|
982
|
+
throw new Error(`Migration version ${version} is already registered.`);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
registeredMigrations.set(version, migration);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
export function migrateToLatest(
|
|
989
|
+
scope: StorageScope = StorageScope.Disk,
|
|
990
|
+
): number {
|
|
991
|
+
assertValidScope(scope);
|
|
992
|
+
const currentVersion = readMigrationVersion(scope);
|
|
993
|
+
const versions = Array.from(registeredMigrations.keys())
|
|
994
|
+
.filter((version) => version > currentVersion)
|
|
995
|
+
.sort((a, b) => a - b);
|
|
996
|
+
|
|
997
|
+
let appliedVersion = currentVersion;
|
|
998
|
+
const context: MigrationContext = {
|
|
999
|
+
scope,
|
|
1000
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1001
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1002
|
+
removeRaw: (key) => removeRawValue(key, scope),
|
|
1003
|
+
};
|
|
1004
|
+
|
|
1005
|
+
versions.forEach((version) => {
|
|
1006
|
+
const migration = registeredMigrations.get(version);
|
|
1007
|
+
if (!migration) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
migration(context);
|
|
1011
|
+
writeMigrationVersion(scope, version);
|
|
1012
|
+
appliedVersion = version;
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
return appliedVersion;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function runTransaction<T>(
|
|
1019
|
+
scope: StorageScope,
|
|
1020
|
+
transaction: (context: TransactionContext) => T,
|
|
1021
|
+
): T {
|
|
1022
|
+
assertValidScope(scope);
|
|
1023
|
+
if (scope === StorageScope.Secure) {
|
|
1024
|
+
flushSecureWrites();
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const rollback = new Map<string, string | undefined>();
|
|
1028
|
+
|
|
1029
|
+
const rememberRollback = (key: string) => {
|
|
1030
|
+
if (rollback.has(key)) {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
const tx: TransactionContext = {
|
|
1037
|
+
scope,
|
|
1038
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
1039
|
+
setRaw: (key, value) => {
|
|
1040
|
+
rememberRollback(key);
|
|
1041
|
+
setRawValue(key, value, scope);
|
|
1042
|
+
},
|
|
1043
|
+
removeRaw: (key) => {
|
|
1044
|
+
rememberRollback(key);
|
|
1045
|
+
removeRawValue(key, scope);
|
|
1046
|
+
},
|
|
1047
|
+
getItem: (item) => {
|
|
1048
|
+
assertBatchScope([item], scope);
|
|
1049
|
+
return item.get();
|
|
1050
|
+
},
|
|
1051
|
+
setItem: (item, value) => {
|
|
1052
|
+
assertBatchScope([item], scope);
|
|
1053
|
+
rememberRollback(item.key);
|
|
1054
|
+
item.set(value);
|
|
1055
|
+
},
|
|
1056
|
+
removeItem: (item) => {
|
|
1057
|
+
assertBatchScope([item], scope);
|
|
1058
|
+
rememberRollback(item.key);
|
|
1059
|
+
item.delete();
|
|
1060
|
+
},
|
|
1061
|
+
};
|
|
1062
|
+
|
|
1063
|
+
try {
|
|
1064
|
+
return transaction(tx);
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
Array.from(rollback.entries())
|
|
1067
|
+
.reverse()
|
|
1068
|
+
.forEach(([key, previousValue]) => {
|
|
1069
|
+
if (previousValue === undefined) {
|
|
1070
|
+
removeRawValue(key, scope);
|
|
1071
|
+
} else {
|
|
1072
|
+
setRawValue(key, previousValue, scope);
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
throw error;
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
1080
|
+
K,
|
|
1081
|
+
{
|
|
1082
|
+
ttlMs?: number;
|
|
1083
|
+
biometric?: boolean;
|
|
1084
|
+
accessControl?: AccessControl;
|
|
1085
|
+
}
|
|
1086
|
+
>;
|
|
1087
|
+
|
|
1088
|
+
export function createSecureAuthStorage<K extends string>(
|
|
1089
|
+
config: SecureAuthStorageConfig<K>,
|
|
1090
|
+
options?: { namespace?: string },
|
|
1091
|
+
): Record<K, StorageItem<string>> {
|
|
1092
|
+
const ns = options?.namespace ?? "auth";
|
|
1093
|
+
const result = {} as Record<K, StorageItem<string>>;
|
|
1094
|
+
|
|
1095
|
+
for (const key of Object.keys(config) as K[]) {
|
|
1096
|
+
const itemConfig = config[key];
|
|
1097
|
+
result[key] = createStorageItem<string>({
|
|
1098
|
+
key,
|
|
1099
|
+
scope: StorageScope.Secure,
|
|
1100
|
+
defaultValue: "",
|
|
1101
|
+
namespace: ns,
|
|
1102
|
+
biometric: itemConfig.biometric,
|
|
1103
|
+
accessControl: itemConfig.accessControl,
|
|
1104
|
+
expiration: itemConfig.ttlMs ? { ttlMs: itemConfig.ttlMs } : undefined,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return result;
|
|
250
1109
|
}
|