react-native-nitro-storage 0.1.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -391
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +101 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +6 -41
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +125 -37
- package/app.plugin.js +9 -7
- package/cpp/bindings/HybridStorage.cpp +214 -19
- package/cpp/bindings/HybridStorage.hpp +1 -0
- package/cpp/core/NativeStorageAdapter.hpp +7 -0
- package/ios/IOSStorageAdapterCpp.hpp +6 -0
- package/ios/IOSStorageAdapterCpp.mm +90 -7
- package/lib/commonjs/index.js +537 -66
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +558 -130
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +102 -0
- package/lib/commonjs/internal.js.map +1 -0
- package/lib/module/index.js +528 -67
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +536 -122
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +92 -0
- package/lib/module/internal.js.map +1 -0
- package/lib/typescript/index.d.ts +42 -6
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +45 -12
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +19 -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/android/NitroStorage+autolinking.cmake +1 -1
- package/nitrogen/generated/android/NitroStorage+autolinking.gradle +1 -1
- package/nitrogen/generated/android/NitroStorageOnLoad.cpp +1 -1
- package/nitrogen/generated/android/NitroStorageOnLoad.hpp +1 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitrostorage/NitroStorageOnLoad.kt +1 -1
- package/nitrogen/generated/ios/NitroStorage+autolinking.rb +1 -1
- package/nitrogen/generated/ios/NitroStorage-Swift-Cxx-Bridge.cpp +1 -1
- package/nitrogen/generated/ios/NitroStorage-Swift-Cxx-Bridge.hpp +1 -1
- package/nitrogen/generated/ios/NitroStorage-Swift-Cxx-Umbrella.hpp +1 -1
- package/nitrogen/generated/ios/NitroStorageAutolinking.mm +1 -1
- package/nitrogen/generated/ios/NitroStorageAutolinking.swift +5 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
- package/package.json +19 -8
- package/src/index.ts +734 -74
- package/src/index.web.ts +732 -128
- package/src/internal.ts +134 -0
- package/src/migration.ts +2 -2
package/src/index.web.ts
CHANGED
|
@@ -1,14 +1,67 @@
|
|
|
1
|
-
import { useSyncExternalStore } from "react";
|
|
1
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
+
import { StorageScope } from "./Storage.types";
|
|
3
|
+
import {
|
|
4
|
+
MIGRATION_VERSION_KEY,
|
|
5
|
+
type StoredEnvelope,
|
|
6
|
+
isStoredEnvelope,
|
|
7
|
+
assertBatchScope,
|
|
8
|
+
assertValidScope,
|
|
9
|
+
serializeWithPrimitiveFastPath,
|
|
10
|
+
deserializeWithPrimitiveFastPath,
|
|
11
|
+
} from "./internal";
|
|
12
|
+
|
|
13
|
+
export { StorageScope } from "./Storage.types";
|
|
14
|
+
export { migrateFromMMKV } from "./migration";
|
|
15
|
+
|
|
16
|
+
export type Validator<T> = (value: unknown) => value is T;
|
|
17
|
+
export type ExpirationConfig = {
|
|
18
|
+
ttlMs: number;
|
|
19
|
+
};
|
|
2
20
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
21
|
+
export type MigrationContext = {
|
|
22
|
+
scope: StorageScope;
|
|
23
|
+
getRaw: (key: string) => string | undefined;
|
|
24
|
+
setRaw: (key: string, value: string) => void;
|
|
25
|
+
removeRaw: (key: string) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type Migration = (context: MigrationContext) => void;
|
|
29
|
+
|
|
30
|
+
export type TransactionContext = {
|
|
31
|
+
scope: StorageScope;
|
|
32
|
+
getRaw: (key: string) => string | undefined;
|
|
33
|
+
setRaw: (key: string, value: string) => void;
|
|
34
|
+
removeRaw: (key: string) => void;
|
|
35
|
+
getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
|
|
36
|
+
setItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "set">, value: T) => void;
|
|
37
|
+
removeItem: (item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">) => void;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
41
|
+
type RawBatchPathItem = {
|
|
42
|
+
_hasValidation?: boolean;
|
|
43
|
+
_hasExpiration?: boolean;
|
|
44
|
+
};
|
|
45
|
+
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
46
|
+
type PendingSecureWrite = { key: string; value: string | undefined };
|
|
47
|
+
type BrowserStorageLike = {
|
|
48
|
+
setItem: (key: string, value: string) => void;
|
|
49
|
+
getItem: (key: string) => string | null;
|
|
50
|
+
removeItem: (key: string) => void;
|
|
51
|
+
clear: () => void;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const registeredMigrations = new Map<number, Migration>();
|
|
55
|
+
const runMicrotask =
|
|
56
|
+
typeof queueMicrotask === "function"
|
|
57
|
+
? queueMicrotask
|
|
58
|
+
: (task: () => void) => {
|
|
59
|
+
Promise.resolve().then(task);
|
|
60
|
+
};
|
|
8
61
|
|
|
9
62
|
export interface Storage {
|
|
10
63
|
name: string;
|
|
11
|
-
equals: (other:
|
|
64
|
+
equals: (other: unknown) => boolean;
|
|
12
65
|
dispose: () => void;
|
|
13
66
|
set(key: string, value: string, scope: number): void;
|
|
14
67
|
get(key: string, scope: number): string | undefined;
|
|
@@ -23,15 +76,140 @@ export interface Storage {
|
|
|
23
76
|
): () => void;
|
|
24
77
|
}
|
|
25
78
|
|
|
26
|
-
const
|
|
27
|
-
const
|
|
79
|
+
const memoryStore = new Map<string, unknown>();
|
|
80
|
+
const memoryListeners: KeyListenerRegistry = new Map();
|
|
81
|
+
const webScopeListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
82
|
+
[StorageScope.Disk, new Map()],
|
|
83
|
+
[StorageScope.Secure, new Map()],
|
|
84
|
+
]);
|
|
85
|
+
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>([
|
|
86
|
+
[StorageScope.Disk, new Map()],
|
|
87
|
+
[StorageScope.Secure, new Map()],
|
|
88
|
+
]);
|
|
89
|
+
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
90
|
+
let secureFlushScheduled = false;
|
|
91
|
+
|
|
92
|
+
function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
|
|
93
|
+
if (scope === StorageScope.Disk) {
|
|
94
|
+
return globalThis.localStorage;
|
|
95
|
+
}
|
|
96
|
+
if (scope === StorageScope.Secure) {
|
|
97
|
+
return globalThis.sessionStorage;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
103
|
+
return webScopeListeners.get(scope)!;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function getScopeRawCache(scope: NonMemoryScope): Map<string, string | undefined> {
|
|
107
|
+
return scopedRawCache.get(scope)!;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function cacheRawValue(scope: NonMemoryScope, key: string, value: string | undefined): void {
|
|
111
|
+
getScopeRawCache(scope).set(key, value);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function readCachedRawValue(
|
|
115
|
+
scope: NonMemoryScope,
|
|
116
|
+
key: string
|
|
117
|
+
): string | undefined {
|
|
118
|
+
return getScopeRawCache(scope).get(key);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function hasCachedRawValue(scope: NonMemoryScope, key: string): boolean {
|
|
122
|
+
return getScopeRawCache(scope).has(key);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function clearScopeRawCache(scope: NonMemoryScope): void {
|
|
126
|
+
getScopeRawCache(scope).clear();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function notifyKeyListeners(registry: KeyListenerRegistry, key: string): void {
|
|
130
|
+
registry.get(key)?.forEach((listener) => listener());
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function notifyAllListeners(registry: KeyListenerRegistry): void {
|
|
134
|
+
registry.forEach((listeners) => {
|
|
135
|
+
listeners.forEach((listener) => listener());
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function addKeyListener(
|
|
140
|
+
registry: KeyListenerRegistry,
|
|
141
|
+
key: string,
|
|
142
|
+
listener: () => void
|
|
143
|
+
): () => void {
|
|
144
|
+
let listeners = registry.get(key);
|
|
145
|
+
if (!listeners) {
|
|
146
|
+
listeners = new Set();
|
|
147
|
+
registry.set(key, listeners);
|
|
148
|
+
}
|
|
149
|
+
listeners.add(listener);
|
|
150
|
+
|
|
151
|
+
return () => {
|
|
152
|
+
const scopedListeners = registry.get(key);
|
|
153
|
+
if (!scopedListeners) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
scopedListeners.delete(listener);
|
|
157
|
+
if (scopedListeners.size === 0) {
|
|
158
|
+
registry.delete(key);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readPendingSecureWrite(key: string): string | undefined {
|
|
164
|
+
return pendingSecureWrites.get(key)?.value;
|
|
165
|
+
}
|
|
28
166
|
|
|
29
|
-
function
|
|
30
|
-
|
|
167
|
+
function hasPendingSecureWrite(key: string): boolean {
|
|
168
|
+
return pendingSecureWrites.has(key);
|
|
31
169
|
}
|
|
32
170
|
|
|
33
|
-
function
|
|
34
|
-
|
|
171
|
+
function clearPendingSecureWrite(key: string): void {
|
|
172
|
+
pendingSecureWrites.delete(key);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function flushSecureWrites(): void {
|
|
176
|
+
secureFlushScheduled = false;
|
|
177
|
+
|
|
178
|
+
if (pendingSecureWrites.size === 0) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const writes = Array.from(pendingSecureWrites.values());
|
|
183
|
+
pendingSecureWrites.clear();
|
|
184
|
+
|
|
185
|
+
const keysToSet: string[] = [];
|
|
186
|
+
const valuesToSet: string[] = [];
|
|
187
|
+
const keysToRemove: string[] = [];
|
|
188
|
+
|
|
189
|
+
writes.forEach(({ key, value }) => {
|
|
190
|
+
if (value === undefined) {
|
|
191
|
+
keysToRemove.push(key);
|
|
192
|
+
} else {
|
|
193
|
+
keysToSet.push(key);
|
|
194
|
+
valuesToSet.push(value);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (keysToSet.length > 0) {
|
|
199
|
+
WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
|
|
200
|
+
}
|
|
201
|
+
if (keysToRemove.length > 0) {
|
|
202
|
+
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function scheduleSecureWrite(key: string, value: string | undefined): void {
|
|
207
|
+
pendingSecureWrites.set(key, { key, value });
|
|
208
|
+
if (secureFlushScheduled) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
secureFlushScheduled = true;
|
|
212
|
+
runMicrotask(flushSecureWrites);
|
|
35
213
|
}
|
|
36
214
|
|
|
37
215
|
const WebStorage: Storage = {
|
|
@@ -39,54 +217,61 @@ const WebStorage: Storage = {
|
|
|
39
217
|
equals: (other) => other === WebStorage,
|
|
40
218
|
dispose: () => {},
|
|
41
219
|
set: (key: string, value: string, scope: number) => {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
sessionStorage?.setItem(key, value);
|
|
47
|
-
notifySecureListeners(key);
|
|
220
|
+
const storage = getBrowserStorage(scope);
|
|
221
|
+
storage?.setItem(key, value);
|
|
222
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
223
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
48
224
|
}
|
|
49
225
|
},
|
|
50
|
-
|
|
51
226
|
get: (key: string, scope: number) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
} else if (scope === StorageScope.Secure) {
|
|
55
|
-
return sessionStorage?.getItem(key) ?? undefined;
|
|
56
|
-
}
|
|
57
|
-
return undefined;
|
|
227
|
+
const storage = getBrowserStorage(scope);
|
|
228
|
+
return storage?.getItem(key) ?? undefined;
|
|
58
229
|
},
|
|
59
230
|
remove: (key: string, scope: number) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
sessionStorage?.removeItem(key);
|
|
65
|
-
notifySecureListeners(key);
|
|
231
|
+
const storage = getBrowserStorage(scope);
|
|
232
|
+
storage?.removeItem(key);
|
|
233
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
234
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
66
235
|
}
|
|
67
236
|
},
|
|
68
|
-
|
|
69
237
|
clear: (scope: number) => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
});
|
|
75
|
-
} else if (scope === StorageScope.Secure) {
|
|
76
|
-
sessionStorage?.clear();
|
|
77
|
-
secureListeners.forEach((listeners) => {
|
|
78
|
-
listeners.forEach((cb) => cb());
|
|
79
|
-
});
|
|
238
|
+
const storage = getBrowserStorage(scope);
|
|
239
|
+
storage?.clear();
|
|
240
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
241
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
80
242
|
}
|
|
81
243
|
},
|
|
82
244
|
setBatch: (keys: string[], values: string[], scope: number) => {
|
|
83
|
-
|
|
245
|
+
const storage = getBrowserStorage(scope);
|
|
246
|
+
if (!storage) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
keys.forEach((key, index) => {
|
|
251
|
+
storage.setItem(key, values[index]);
|
|
252
|
+
});
|
|
253
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
254
|
+
const listeners = getScopedListeners(scope);
|
|
255
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
256
|
+
}
|
|
84
257
|
},
|
|
85
258
|
getBatch: (keys: string[], scope: number) => {
|
|
86
|
-
|
|
259
|
+
const storage = getBrowserStorage(scope);
|
|
260
|
+
return keys.map((key) => storage?.getItem(key) ?? undefined);
|
|
87
261
|
},
|
|
88
262
|
removeBatch: (keys: string[], scope: number) => {
|
|
89
|
-
|
|
263
|
+
const storage = getBrowserStorage(scope);
|
|
264
|
+
if (!storage) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
keys.forEach((key) => {
|
|
269
|
+
storage.removeItem(key);
|
|
270
|
+
});
|
|
271
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
272
|
+
const listeners = getScopedListeners(scope);
|
|
273
|
+
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
274
|
+
}
|
|
90
275
|
},
|
|
91
276
|
addOnChange: (
|
|
92
277
|
_scope: number,
|
|
@@ -96,21 +281,83 @@ const WebStorage: Storage = {
|
|
|
96
281
|
},
|
|
97
282
|
};
|
|
98
283
|
|
|
99
|
-
|
|
100
|
-
|
|
284
|
+
function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
285
|
+
assertValidScope(scope);
|
|
286
|
+
if (scope === StorageScope.Memory) {
|
|
287
|
+
const value = memoryStore.get(key);
|
|
288
|
+
return typeof value === "string" ? value : undefined;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
292
|
+
return readPendingSecureWrite(key);
|
|
293
|
+
}
|
|
101
294
|
|
|
102
|
-
|
|
103
|
-
|
|
295
|
+
return WebStorage.get(key, scope);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
299
|
+
assertValidScope(scope);
|
|
300
|
+
if (scope === StorageScope.Memory) {
|
|
301
|
+
memoryStore.set(key, value);
|
|
302
|
+
notifyKeyListeners(memoryListeners, key);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (scope === StorageScope.Secure) {
|
|
307
|
+
flushSecureWrites();
|
|
308
|
+
clearPendingSecureWrite(key);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
WebStorage.set(key, value, scope);
|
|
312
|
+
cacheRawValue(scope, key, value);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function removeRawValue(key: string, scope: StorageScope): void {
|
|
316
|
+
assertValidScope(scope);
|
|
317
|
+
if (scope === StorageScope.Memory) {
|
|
318
|
+
memoryStore.delete(key);
|
|
319
|
+
notifyKeyListeners(memoryListeners, key);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (scope === StorageScope.Secure) {
|
|
324
|
+
flushSecureWrites();
|
|
325
|
+
clearPendingSecureWrite(key);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
WebStorage.remove(key, scope);
|
|
329
|
+
cacheRawValue(scope, key, undefined);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function readMigrationVersion(scope: StorageScope): number {
|
|
333
|
+
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
334
|
+
if (raw === undefined) {
|
|
335
|
+
return 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const parsed = Number.parseInt(raw, 10);
|
|
339
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function writeMigrationVersion(scope: StorageScope, version: number): void {
|
|
343
|
+
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
104
344
|
}
|
|
105
345
|
|
|
106
346
|
export const storage = {
|
|
107
347
|
clear: (scope: StorageScope) => {
|
|
108
348
|
if (scope === StorageScope.Memory) {
|
|
109
349
|
memoryStore.clear();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
WebStorage.clear(scope);
|
|
350
|
+
notifyAllListeners(memoryListeners);
|
|
351
|
+
return;
|
|
113
352
|
}
|
|
353
|
+
|
|
354
|
+
if (scope === StorageScope.Secure) {
|
|
355
|
+
flushSecureWrites();
|
|
356
|
+
pendingSecureWrites.clear();
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
clearScopeRawCache(scope);
|
|
360
|
+
WebStorage.clear(scope);
|
|
114
361
|
},
|
|
115
362
|
clearAll: () => {
|
|
116
363
|
storage.clear(StorageScope.Memory);
|
|
@@ -125,6 +372,11 @@ export interface StorageItemConfig<T> {
|
|
|
125
372
|
defaultValue?: T;
|
|
126
373
|
serialize?: (value: T) => string;
|
|
127
374
|
deserialize?: (value: string) => T;
|
|
375
|
+
validate?: Validator<T>;
|
|
376
|
+
onValidationError?: (invalidValue: unknown) => T;
|
|
377
|
+
expiration?: ExpirationConfig;
|
|
378
|
+
readCache?: boolean;
|
|
379
|
+
coalesceSecureWrites?: boolean;
|
|
128
380
|
}
|
|
129
381
|
|
|
130
382
|
export interface StorageItem<T> {
|
|
@@ -135,16 +387,23 @@ export interface StorageItem<T> {
|
|
|
135
387
|
serialize: (value: T) => string;
|
|
136
388
|
deserialize: (value: string) => T;
|
|
137
389
|
_triggerListeners: () => void;
|
|
390
|
+
_hasValidation?: boolean;
|
|
391
|
+
_hasExpiration?: boolean;
|
|
392
|
+
_readCacheEnabled?: boolean;
|
|
138
393
|
scope: StorageScope;
|
|
139
394
|
key: string;
|
|
140
395
|
}
|
|
141
396
|
|
|
397
|
+
function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
398
|
+
return item._hasExpiration === false && item._hasValidation === false;
|
|
399
|
+
}
|
|
400
|
+
|
|
142
401
|
function defaultSerialize<T>(value: T): string {
|
|
143
|
-
return
|
|
402
|
+
return serializeWithPrimitiveFastPath(value);
|
|
144
403
|
}
|
|
145
404
|
|
|
146
405
|
function defaultDeserialize<T>(value: string): T {
|
|
147
|
-
return
|
|
406
|
+
return deserializeWithPrimitiveFastPath(value);
|
|
148
407
|
}
|
|
149
408
|
|
|
150
409
|
export function createStorageItem<T = undefined>(
|
|
@@ -153,70 +412,206 @@ export function createStorageItem<T = undefined>(
|
|
|
153
412
|
const serialize = config.serialize ?? defaultSerialize;
|
|
154
413
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
155
414
|
const isMemory = config.scope === StorageScope.Memory;
|
|
415
|
+
const validate = config.validate;
|
|
416
|
+
const onValidationError = config.onValidationError;
|
|
417
|
+
const expiration = config.expiration;
|
|
418
|
+
const expirationTtlMs = expiration?.ttlMs;
|
|
419
|
+
const memoryExpiration = expiration && isMemory ? new Map<string, number>() : null;
|
|
420
|
+
const readCache = !isMemory && config.readCache === true;
|
|
421
|
+
const coalesceSecureWrites =
|
|
422
|
+
config.scope === StorageScope.Secure && config.coalesceSecureWrites === true;
|
|
423
|
+
const nonMemoryScope: NonMemoryScope | null =
|
|
424
|
+
config.scope === StorageScope.Disk
|
|
425
|
+
? StorageScope.Disk
|
|
426
|
+
: config.scope === StorageScope.Secure
|
|
427
|
+
? StorageScope.Secure
|
|
428
|
+
: null;
|
|
429
|
+
|
|
430
|
+
if (expiration && expiration.ttlMs <= 0) {
|
|
431
|
+
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
432
|
+
}
|
|
156
433
|
|
|
157
434
|
const listeners = new Set<() => void>();
|
|
158
435
|
let unsubscribe: (() => void) | null = null;
|
|
159
|
-
let lastRaw:
|
|
436
|
+
let lastRaw: unknown = undefined;
|
|
160
437
|
let lastValue: T | undefined;
|
|
438
|
+
let hasLastValue = false;
|
|
439
|
+
|
|
440
|
+
const invalidateParsedCache = () => {
|
|
441
|
+
lastRaw = undefined;
|
|
442
|
+
lastValue = undefined;
|
|
443
|
+
hasLastValue = false;
|
|
444
|
+
};
|
|
161
445
|
|
|
162
446
|
const ensureSubscription = () => {
|
|
163
|
-
if (
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
listeners.forEach((l) => l());
|
|
190
|
-
};
|
|
191
|
-
if (!secureListeners.has(config.key)) {
|
|
192
|
-
secureListeners.set(config.key, new Set());
|
|
447
|
+
if (unsubscribe) {
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const listener = () => {
|
|
452
|
+
invalidateParsedCache();
|
|
453
|
+
listeners.forEach((callback) => callback());
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
if (isMemory) {
|
|
457
|
+
unsubscribe = addKeyListener(memoryListeners, config.key, listener);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope!), config.key, listener);
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const readStoredRaw = (): unknown => {
|
|
465
|
+
if (isMemory) {
|
|
466
|
+
if (memoryExpiration) {
|
|
467
|
+
const expiresAt = memoryExpiration.get(config.key);
|
|
468
|
+
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
469
|
+
memoryExpiration.delete(config.key);
|
|
470
|
+
memoryStore.delete(config.key);
|
|
471
|
+
notifyKeyListeners(memoryListeners, config.key);
|
|
472
|
+
return undefined;
|
|
193
473
|
}
|
|
194
|
-
|
|
195
|
-
|
|
474
|
+
}
|
|
475
|
+
return memoryStore.get(config.key) as T | undefined;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (nonMemoryScope === StorageScope.Secure && hasPendingSecureWrite(config.key)) {
|
|
479
|
+
return readPendingSecureWrite(config.key);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (readCache) {
|
|
483
|
+
if (hasCachedRawValue(nonMemoryScope!, config.key)) {
|
|
484
|
+
return readCachedRawValue(nonMemoryScope!, config.key);
|
|
196
485
|
}
|
|
197
486
|
}
|
|
487
|
+
|
|
488
|
+
const raw = WebStorage.get(config.key, config.scope);
|
|
489
|
+
cacheRawValue(nonMemoryScope!, config.key, raw);
|
|
490
|
+
return raw;
|
|
198
491
|
};
|
|
199
492
|
|
|
200
|
-
const
|
|
201
|
-
|
|
493
|
+
const writeStoredRaw = (rawValue: string): void => {
|
|
494
|
+
cacheRawValue(nonMemoryScope!, config.key, rawValue);
|
|
495
|
+
|
|
496
|
+
if (coalesceSecureWrites) {
|
|
497
|
+
scheduleSecureWrite(config.key, rawValue);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
502
|
+
clearPendingSecureWrite(config.key);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
WebStorage.set(config.key, rawValue, config.scope);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
const removeStoredRaw = (): void => {
|
|
509
|
+
cacheRawValue(nonMemoryScope!, config.key, undefined);
|
|
510
|
+
|
|
511
|
+
if (coalesceSecureWrites) {
|
|
512
|
+
scheduleSecureWrite(config.key, undefined);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
517
|
+
clearPendingSecureWrite(config.key);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
WebStorage.remove(config.key, config.scope);
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const writeValueWithoutValidation = (value: T): void => {
|
|
202
524
|
if (isMemory) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
525
|
+
if (memoryExpiration) {
|
|
526
|
+
memoryExpiration.set(config.key, Date.now() + (expirationTtlMs ?? 0));
|
|
527
|
+
}
|
|
528
|
+
memoryStore.set(config.key, value);
|
|
529
|
+
notifyKeyListeners(memoryListeners, config.key);
|
|
530
|
+
return;
|
|
206
531
|
}
|
|
207
532
|
|
|
208
|
-
|
|
209
|
-
|
|
533
|
+
const serialized = serialize(value);
|
|
534
|
+
if (expiration) {
|
|
535
|
+
const envelope: StoredEnvelope = {
|
|
536
|
+
__nitroStorageEnvelope: true,
|
|
537
|
+
expiresAt: Date.now() + expiration.ttlMs,
|
|
538
|
+
payload: serialized,
|
|
539
|
+
};
|
|
540
|
+
writeStoredRaw(JSON.stringify(envelope));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
writeStoredRaw(serialized);
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
const resolveInvalidValue = (invalidValue: unknown): T => {
|
|
548
|
+
if (onValidationError) {
|
|
549
|
+
return onValidationError(invalidValue);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return config.defaultValue as T;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
const ensureValidatedValue = (candidate: unknown, hadStoredValue: boolean): T => {
|
|
556
|
+
if (!validate || validate(candidate)) {
|
|
557
|
+
return candidate as T;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const resolved = resolveInvalidValue(candidate);
|
|
561
|
+
if (validate && !validate(resolved)) {
|
|
562
|
+
return config.defaultValue as T;
|
|
563
|
+
}
|
|
564
|
+
if (hadStoredValue) {
|
|
565
|
+
writeValueWithoutValidation(resolved);
|
|
566
|
+
}
|
|
567
|
+
return resolved;
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const get = (): T => {
|
|
571
|
+
const raw = readStoredRaw();
|
|
572
|
+
|
|
573
|
+
const canUseCachedValue = !expiration && !memoryExpiration;
|
|
574
|
+
if (canUseCachedValue && raw === lastRaw && hasLastValue) {
|
|
575
|
+
return lastValue as T;
|
|
210
576
|
}
|
|
211
577
|
|
|
212
578
|
lastRaw = raw;
|
|
213
579
|
|
|
214
580
|
if (raw === undefined) {
|
|
215
|
-
lastValue = config.defaultValue
|
|
216
|
-
|
|
217
|
-
lastValue
|
|
581
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
582
|
+
hasLastValue = true;
|
|
583
|
+
return lastValue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (isMemory) {
|
|
587
|
+
lastValue = ensureValidatedValue(raw, true);
|
|
588
|
+
hasLastValue = true;
|
|
589
|
+
return lastValue;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
let deserializableRaw = raw as string;
|
|
593
|
+
|
|
594
|
+
if (expiration) {
|
|
595
|
+
try {
|
|
596
|
+
const parsed = JSON.parse(raw as string) as unknown;
|
|
597
|
+
if (isStoredEnvelope(parsed)) {
|
|
598
|
+
if (parsed.expiresAt <= Date.now()) {
|
|
599
|
+
removeStoredRaw();
|
|
600
|
+
invalidateParsedCache();
|
|
601
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
602
|
+
hasLastValue = true;
|
|
603
|
+
return lastValue;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
deserializableRaw = parsed.payload;
|
|
607
|
+
}
|
|
608
|
+
} catch {
|
|
609
|
+
// Keep backward compatibility with legacy raw values.
|
|
610
|
+
}
|
|
218
611
|
}
|
|
219
612
|
|
|
613
|
+
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
614
|
+
hasLastValue = true;
|
|
220
615
|
return lastValue;
|
|
221
616
|
};
|
|
222
617
|
|
|
@@ -227,25 +622,30 @@ export function createStorageItem<T = undefined>(
|
|
|
227
622
|
? (valueOrFn as (prev: T) => T)(currentValue)
|
|
228
623
|
: valueOrFn;
|
|
229
624
|
|
|
230
|
-
|
|
625
|
+
invalidateParsedCache();
|
|
231
626
|
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
WebStorage.set(config.key, serialize(newValue), config.scope);
|
|
627
|
+
if (validate && !validate(newValue)) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
`Validation failed for key "${config.key}" in scope "${StorageScope[config.scope]}".`
|
|
630
|
+
);
|
|
237
631
|
}
|
|
632
|
+
|
|
633
|
+
writeValueWithoutValidation(newValue);
|
|
238
634
|
};
|
|
239
635
|
|
|
240
636
|
const deleteItem = (): void => {
|
|
241
|
-
|
|
637
|
+
invalidateParsedCache();
|
|
242
638
|
|
|
243
639
|
if (isMemory) {
|
|
640
|
+
if (memoryExpiration) {
|
|
641
|
+
memoryExpiration.delete(config.key);
|
|
642
|
+
}
|
|
244
643
|
memoryStore.delete(config.key);
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
WebStorage.remove(config.key, config.scope);
|
|
644
|
+
notifyKeyListeners(memoryListeners, config.key);
|
|
645
|
+
return;
|
|
248
646
|
}
|
|
647
|
+
|
|
648
|
+
removeStoredRaw();
|
|
249
649
|
};
|
|
250
650
|
|
|
251
651
|
const subscribe = (callback: () => void): (() => void) => {
|
|
@@ -260,7 +660,7 @@ export function createStorageItem<T = undefined>(
|
|
|
260
660
|
};
|
|
261
661
|
};
|
|
262
662
|
|
|
263
|
-
|
|
663
|
+
const storageItem: StorageItem<T> = {
|
|
264
664
|
get,
|
|
265
665
|
set,
|
|
266
666
|
delete: deleteItem,
|
|
@@ -268,13 +668,17 @@ export function createStorageItem<T = undefined>(
|
|
|
268
668
|
serialize,
|
|
269
669
|
deserialize,
|
|
270
670
|
_triggerListeners: () => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
listeners.forEach((l) => l());
|
|
671
|
+
invalidateParsedCache();
|
|
672
|
+
listeners.forEach((listener) => listener());
|
|
274
673
|
},
|
|
674
|
+
_hasValidation: validate !== undefined,
|
|
675
|
+
_hasExpiration: expiration !== undefined,
|
|
676
|
+
_readCacheEnabled: readCache,
|
|
275
677
|
scope: config.scope,
|
|
276
678
|
key: config.key,
|
|
277
679
|
};
|
|
680
|
+
|
|
681
|
+
return storageItem;
|
|
278
682
|
}
|
|
279
683
|
|
|
280
684
|
export function useStorage<T>(
|
|
@@ -284,23 +688,106 @@ export function useStorage<T>(
|
|
|
284
688
|
return [value, item.set];
|
|
285
689
|
}
|
|
286
690
|
|
|
691
|
+
export function useStorageSelector<T, TSelected>(
|
|
692
|
+
item: StorageItem<T>,
|
|
693
|
+
selector: (value: T) => TSelected,
|
|
694
|
+
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is
|
|
695
|
+
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
696
|
+
const selectedRef = useRef<{ hasValue: false } | { hasValue: true; value: TSelected }>({
|
|
697
|
+
hasValue: false,
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
const getSelectedSnapshot = () => {
|
|
701
|
+
const nextSelected = selector(item.get());
|
|
702
|
+
const current = selectedRef.current;
|
|
703
|
+
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
704
|
+
return current.value;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
selectedRef.current = { hasValue: true, value: nextSelected };
|
|
708
|
+
return nextSelected;
|
|
709
|
+
};
|
|
710
|
+
|
|
711
|
+
const selectedValue = useSyncExternalStore(
|
|
712
|
+
item.subscribe,
|
|
713
|
+
getSelectedSnapshot,
|
|
714
|
+
getSelectedSnapshot
|
|
715
|
+
);
|
|
716
|
+
return [selectedValue, item.set];
|
|
717
|
+
}
|
|
718
|
+
|
|
287
719
|
export function useSetStorage<T>(item: StorageItem<T>) {
|
|
288
720
|
return item.set;
|
|
289
721
|
}
|
|
290
722
|
|
|
723
|
+
type BatchReadItem<T> = Pick<
|
|
724
|
+
StorageItem<T>,
|
|
725
|
+
| "key"
|
|
726
|
+
| "scope"
|
|
727
|
+
| "get"
|
|
728
|
+
| "deserialize"
|
|
729
|
+
| "_hasValidation"
|
|
730
|
+
| "_hasExpiration"
|
|
731
|
+
| "_readCacheEnabled"
|
|
732
|
+
>;
|
|
733
|
+
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
734
|
+
|
|
735
|
+
export type StorageBatchSetItem<T> = {
|
|
736
|
+
item: StorageItem<T>;
|
|
737
|
+
value: T;
|
|
738
|
+
};
|
|
739
|
+
|
|
291
740
|
export function getBatch(
|
|
292
|
-
items:
|
|
741
|
+
items: readonly BatchReadItem<unknown>[],
|
|
293
742
|
scope: StorageScope
|
|
294
|
-
):
|
|
743
|
+
): unknown[] {
|
|
744
|
+
assertBatchScope(items, scope);
|
|
745
|
+
|
|
295
746
|
if (scope === StorageScope.Memory) {
|
|
296
747
|
return items.map((item) => item.get());
|
|
297
748
|
}
|
|
298
749
|
|
|
299
|
-
const
|
|
300
|
-
|
|
750
|
+
const useRawBatchPath = items.every((item) => canUseRawBatchPath(item));
|
|
751
|
+
if (!useRawBatchPath) {
|
|
752
|
+
return items.map((item) => item.get());
|
|
753
|
+
}
|
|
754
|
+
const useBatchCache = items.every((item) => item._readCacheEnabled === true);
|
|
301
755
|
|
|
302
|
-
|
|
303
|
-
|
|
756
|
+
const rawValues = new Array<string | undefined>(items.length);
|
|
757
|
+
const keysToFetch: string[] = [];
|
|
758
|
+
const keyIndexes: number[] = [];
|
|
759
|
+
|
|
760
|
+
items.forEach((item, index) => {
|
|
761
|
+
if (scope === StorageScope.Secure) {
|
|
762
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
763
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (useBatchCache) {
|
|
769
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
770
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
keysToFetch.push(item.key);
|
|
776
|
+
keyIndexes.push(index);
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
if (keysToFetch.length > 0) {
|
|
780
|
+
const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
|
|
781
|
+
fetchedValues.forEach((value, index) => {
|
|
782
|
+
const key = keysToFetch[index];
|
|
783
|
+
const targetIndex = keyIndexes[index];
|
|
784
|
+
rawValues[targetIndex] = value;
|
|
785
|
+
cacheRawValue(scope, key, value);
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return items.map((item, index) => {
|
|
790
|
+
const raw = rawValues[index];
|
|
304
791
|
if (raw === undefined) {
|
|
305
792
|
return item.get();
|
|
306
793
|
}
|
|
@@ -308,34 +795,151 @@ export function getBatch(
|
|
|
308
795
|
});
|
|
309
796
|
}
|
|
310
797
|
|
|
311
|
-
export function setBatch(
|
|
312
|
-
items:
|
|
798
|
+
export function setBatch<T>(
|
|
799
|
+
items: readonly StorageBatchSetItem<T>[],
|
|
313
800
|
scope: StorageScope
|
|
314
801
|
): void {
|
|
802
|
+
assertBatchScope(
|
|
803
|
+
items.map((batchEntry) => batchEntry.item),
|
|
804
|
+
scope
|
|
805
|
+
);
|
|
806
|
+
|
|
315
807
|
if (scope === StorageScope.Memory) {
|
|
316
808
|
items.forEach(({ item, value }) => item.set(value));
|
|
317
809
|
return;
|
|
318
810
|
}
|
|
319
811
|
|
|
320
|
-
const
|
|
321
|
-
|
|
322
|
-
|
|
812
|
+
const useRawBatchPath = items.every(({ item }) => canUseRawBatchPath(item));
|
|
813
|
+
if (!useRawBatchPath) {
|
|
814
|
+
items.forEach(({ item, value }) => item.set(value));
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
323
817
|
|
|
324
|
-
items.
|
|
325
|
-
|
|
326
|
-
|
|
818
|
+
const keys = items.map((entry) => entry.item.key);
|
|
819
|
+
const values = items.map((entry) => entry.item.serialize(entry.value));
|
|
820
|
+
if (scope === StorageScope.Secure) {
|
|
821
|
+
flushSecureWrites();
|
|
822
|
+
}
|
|
823
|
+
WebStorage.setBatch(keys, values, scope);
|
|
824
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
327
825
|
}
|
|
328
826
|
|
|
329
827
|
export function removeBatch(
|
|
330
|
-
items:
|
|
828
|
+
items: readonly BatchRemoveItem[],
|
|
331
829
|
scope: StorageScope
|
|
332
830
|
): void {
|
|
831
|
+
assertBatchScope(items, scope);
|
|
832
|
+
|
|
333
833
|
if (scope === StorageScope.Memory) {
|
|
334
834
|
items.forEach((item) => item.delete());
|
|
335
835
|
return;
|
|
336
836
|
}
|
|
337
837
|
|
|
338
838
|
const keys = items.map((item) => item.key);
|
|
839
|
+
if (scope === StorageScope.Secure) {
|
|
840
|
+
flushSecureWrites();
|
|
841
|
+
}
|
|
339
842
|
WebStorage.removeBatch(keys, scope);
|
|
340
|
-
|
|
843
|
+
keys.forEach((key) => cacheRawValue(scope, key, undefined));
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function registerMigration(version: number, migration: Migration): void {
|
|
847
|
+
if (!Number.isInteger(version) || version <= 0) {
|
|
848
|
+
throw new Error("Migration version must be a positive integer.");
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (registeredMigrations.has(version)) {
|
|
852
|
+
throw new Error(`Migration version ${version} is already registered.`);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
registeredMigrations.set(version, migration);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number {
|
|
859
|
+
assertValidScope(scope);
|
|
860
|
+
const currentVersion = readMigrationVersion(scope);
|
|
861
|
+
const versions = Array.from(registeredMigrations.keys())
|
|
862
|
+
.filter((version) => version > currentVersion)
|
|
863
|
+
.sort((a, b) => a - b);
|
|
864
|
+
|
|
865
|
+
let appliedVersion = currentVersion;
|
|
866
|
+
const context: MigrationContext = {
|
|
867
|
+
scope,
|
|
868
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
869
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
870
|
+
removeRaw: (key) => removeRawValue(key, scope),
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
versions.forEach((version) => {
|
|
874
|
+
const migration = registeredMigrations.get(version);
|
|
875
|
+
if (!migration) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
migration(context);
|
|
879
|
+
writeMigrationVersion(scope, version);
|
|
880
|
+
appliedVersion = version;
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
return appliedVersion;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
export function runTransaction<T>(
|
|
887
|
+
scope: StorageScope,
|
|
888
|
+
transaction: (context: TransactionContext) => T
|
|
889
|
+
): T {
|
|
890
|
+
assertValidScope(scope);
|
|
891
|
+
if (scope === StorageScope.Secure) {
|
|
892
|
+
flushSecureWrites();
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
const rollback = new Map<string, string | undefined>();
|
|
896
|
+
|
|
897
|
+
const rememberRollback = (key: string) => {
|
|
898
|
+
if (rollback.has(key)) {
|
|
899
|
+
return;
|
|
900
|
+
}
|
|
901
|
+
rollback.set(key, getRawValue(key, scope));
|
|
902
|
+
};
|
|
903
|
+
|
|
904
|
+
const tx: TransactionContext = {
|
|
905
|
+
scope,
|
|
906
|
+
getRaw: (key) => getRawValue(key, scope),
|
|
907
|
+
setRaw: (key, value) => {
|
|
908
|
+
rememberRollback(key);
|
|
909
|
+
setRawValue(key, value, scope);
|
|
910
|
+
},
|
|
911
|
+
removeRaw: (key) => {
|
|
912
|
+
rememberRollback(key);
|
|
913
|
+
removeRawValue(key, scope);
|
|
914
|
+
},
|
|
915
|
+
getItem: (item) => {
|
|
916
|
+
assertBatchScope([item], scope);
|
|
917
|
+
return item.get();
|
|
918
|
+
},
|
|
919
|
+
setItem: (item, value) => {
|
|
920
|
+
assertBatchScope([item], scope);
|
|
921
|
+
rememberRollback(item.key);
|
|
922
|
+
item.set(value);
|
|
923
|
+
},
|
|
924
|
+
removeItem: (item) => {
|
|
925
|
+
assertBatchScope([item], scope);
|
|
926
|
+
rememberRollback(item.key);
|
|
927
|
+
item.delete();
|
|
928
|
+
},
|
|
929
|
+
};
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
return transaction(tx);
|
|
933
|
+
} catch (error) {
|
|
934
|
+
Array.from(rollback.entries())
|
|
935
|
+
.reverse()
|
|
936
|
+
.forEach(([key, previousValue]) => {
|
|
937
|
+
if (previousValue === undefined) {
|
|
938
|
+
removeRawValue(key, scope);
|
|
939
|
+
} else {
|
|
940
|
+
setRawValue(key, previousValue, scope);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
throw error;
|
|
944
|
+
}
|
|
341
945
|
}
|