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