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/lib/module/index.js
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { useRef, useSyncExternalStore } from "react";
|
|
4
4
|
import { NitroModules } from "react-native-nitro-modules";
|
|
5
5
|
import { StorageScope } from "./Storage.types";
|
|
6
|
+
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath } from "./internal";
|
|
6
7
|
export { StorageScope } from "./Storage.types";
|
|
8
|
+
export { migrateFromMMKV } from "./migration";
|
|
9
|
+
const registeredMigrations = new Map();
|
|
10
|
+
const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
|
|
11
|
+
Promise.resolve().then(task);
|
|
12
|
+
};
|
|
7
13
|
let _storageModule = null;
|
|
8
14
|
function getStorageModule() {
|
|
9
15
|
if (!_storageModule) {
|
|
@@ -12,18 +18,202 @@ function getStorageModule() {
|
|
|
12
18
|
return _storageModule;
|
|
13
19
|
}
|
|
14
20
|
const memoryStore = new Map();
|
|
15
|
-
const memoryListeners = new
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const memoryListeners = new Map();
|
|
22
|
+
const scopedListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
23
|
+
const scopedUnsubscribers = new Map();
|
|
24
|
+
const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
25
|
+
const pendingSecureWrites = new Map();
|
|
26
|
+
let secureFlushScheduled = false;
|
|
27
|
+
function getScopedListeners(scope) {
|
|
28
|
+
return scopedListeners.get(scope);
|
|
29
|
+
}
|
|
30
|
+
function getScopeRawCache(scope) {
|
|
31
|
+
return scopedRawCache.get(scope);
|
|
32
|
+
}
|
|
33
|
+
function cacheRawValue(scope, key, value) {
|
|
34
|
+
getScopeRawCache(scope).set(key, value);
|
|
35
|
+
}
|
|
36
|
+
function readCachedRawValue(scope, key) {
|
|
37
|
+
return getScopeRawCache(scope).get(key);
|
|
38
|
+
}
|
|
39
|
+
function hasCachedRawValue(scope, key) {
|
|
40
|
+
return getScopeRawCache(scope).has(key);
|
|
41
|
+
}
|
|
42
|
+
function clearScopeRawCache(scope) {
|
|
43
|
+
getScopeRawCache(scope).clear();
|
|
44
|
+
}
|
|
45
|
+
function notifyKeyListeners(registry, key) {
|
|
46
|
+
registry.get(key)?.forEach(listener => listener());
|
|
47
|
+
}
|
|
48
|
+
function notifyAllListeners(registry) {
|
|
49
|
+
registry.forEach(listeners => {
|
|
50
|
+
listeners.forEach(listener => listener());
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function addKeyListener(registry, key, listener) {
|
|
54
|
+
let listeners = registry.get(key);
|
|
55
|
+
if (!listeners) {
|
|
56
|
+
listeners = new Set();
|
|
57
|
+
registry.set(key, listeners);
|
|
58
|
+
}
|
|
59
|
+
listeners.add(listener);
|
|
60
|
+
return () => {
|
|
61
|
+
const scopedListeners = registry.get(key);
|
|
62
|
+
if (!scopedListeners) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
scopedListeners.delete(listener);
|
|
66
|
+
if (scopedListeners.size === 0) {
|
|
67
|
+
registry.delete(key);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function readPendingSecureWrite(key) {
|
|
72
|
+
return pendingSecureWrites.get(key)?.value;
|
|
73
|
+
}
|
|
74
|
+
function hasPendingSecureWrite(key) {
|
|
75
|
+
return pendingSecureWrites.has(key);
|
|
76
|
+
}
|
|
77
|
+
function clearPendingSecureWrite(key) {
|
|
78
|
+
pendingSecureWrites.delete(key);
|
|
79
|
+
}
|
|
80
|
+
function flushSecureWrites() {
|
|
81
|
+
secureFlushScheduled = false;
|
|
82
|
+
if (pendingSecureWrites.size === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const writes = Array.from(pendingSecureWrites.values());
|
|
86
|
+
pendingSecureWrites.clear();
|
|
87
|
+
const keysToSet = [];
|
|
88
|
+
const valuesToSet = [];
|
|
89
|
+
const keysToRemove = [];
|
|
90
|
+
writes.forEach(({
|
|
91
|
+
key,
|
|
92
|
+
value
|
|
93
|
+
}) => {
|
|
94
|
+
if (value === undefined) {
|
|
95
|
+
keysToRemove.push(key);
|
|
96
|
+
} else {
|
|
97
|
+
keysToSet.push(key);
|
|
98
|
+
valuesToSet.push(value);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
const storageModule = getStorageModule();
|
|
102
|
+
if (keysToSet.length > 0) {
|
|
103
|
+
storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
|
|
104
|
+
}
|
|
105
|
+
if (keysToRemove.length > 0) {
|
|
106
|
+
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function scheduleSecureWrite(key, value) {
|
|
110
|
+
pendingSecureWrites.set(key, {
|
|
111
|
+
key,
|
|
112
|
+
value
|
|
113
|
+
});
|
|
114
|
+
if (secureFlushScheduled) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
secureFlushScheduled = true;
|
|
118
|
+
runMicrotask(flushSecureWrites);
|
|
119
|
+
}
|
|
120
|
+
function ensureNativeScopeSubscription(scope) {
|
|
121
|
+
if (scopedUnsubscribers.has(scope)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
|
|
125
|
+
if (scope === StorageScope.Secure) {
|
|
126
|
+
if (key === "") {
|
|
127
|
+
pendingSecureWrites.clear();
|
|
128
|
+
} else {
|
|
129
|
+
clearPendingSecureWrite(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (key === "") {
|
|
133
|
+
clearScopeRawCache(scope);
|
|
134
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
cacheRawValue(scope, key, value);
|
|
138
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
139
|
+
});
|
|
140
|
+
scopedUnsubscribers.set(scope, unsubscribe);
|
|
141
|
+
}
|
|
142
|
+
function maybeCleanupNativeScopeSubscription(scope) {
|
|
143
|
+
const listeners = getScopedListeners(scope);
|
|
144
|
+
if (listeners.size > 0) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const unsubscribe = scopedUnsubscribers.get(scope);
|
|
148
|
+
if (!unsubscribe) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
unsubscribe();
|
|
152
|
+
scopedUnsubscribers.delete(scope);
|
|
153
|
+
}
|
|
154
|
+
function getRawValue(key, scope) {
|
|
155
|
+
assertValidScope(scope);
|
|
156
|
+
if (scope === StorageScope.Memory) {
|
|
157
|
+
const value = memoryStore.get(key);
|
|
158
|
+
return typeof value === "string" ? value : undefined;
|
|
159
|
+
}
|
|
160
|
+
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
161
|
+
return readPendingSecureWrite(key);
|
|
162
|
+
}
|
|
163
|
+
return getStorageModule().get(key, scope);
|
|
164
|
+
}
|
|
165
|
+
function setRawValue(key, value, scope) {
|
|
166
|
+
assertValidScope(scope);
|
|
167
|
+
if (scope === StorageScope.Memory) {
|
|
168
|
+
memoryStore.set(key, value);
|
|
169
|
+
notifyKeyListeners(memoryListeners, key);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (scope === StorageScope.Secure) {
|
|
173
|
+
flushSecureWrites();
|
|
174
|
+
clearPendingSecureWrite(key);
|
|
175
|
+
}
|
|
176
|
+
getStorageModule().set(key, value, scope);
|
|
177
|
+
cacheRawValue(scope, key, value);
|
|
178
|
+
}
|
|
179
|
+
function removeRawValue(key, scope) {
|
|
180
|
+
assertValidScope(scope);
|
|
181
|
+
if (scope === StorageScope.Memory) {
|
|
182
|
+
memoryStore.delete(key);
|
|
183
|
+
notifyKeyListeners(memoryListeners, key);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (scope === StorageScope.Secure) {
|
|
187
|
+
flushSecureWrites();
|
|
188
|
+
clearPendingSecureWrite(key);
|
|
189
|
+
}
|
|
190
|
+
getStorageModule().remove(key, scope);
|
|
191
|
+
cacheRawValue(scope, key, undefined);
|
|
192
|
+
}
|
|
193
|
+
function readMigrationVersion(scope) {
|
|
194
|
+
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
195
|
+
if (raw === undefined) {
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
const parsed = Number.parseInt(raw, 10);
|
|
199
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
200
|
+
}
|
|
201
|
+
function writeMigrationVersion(scope, version) {
|
|
202
|
+
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
18
203
|
}
|
|
19
204
|
export const storage = {
|
|
20
205
|
clear: scope => {
|
|
21
206
|
if (scope === StorageScope.Memory) {
|
|
22
207
|
memoryStore.clear();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
208
|
+
notifyAllListeners(memoryListeners);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (scope === StorageScope.Secure) {
|
|
212
|
+
flushSecureWrites();
|
|
213
|
+
pendingSecureWrites.clear();
|
|
26
214
|
}
|
|
215
|
+
clearScopeRawCache(scope);
|
|
216
|
+
getStorageModule().clear(scope);
|
|
27
217
|
},
|
|
28
218
|
clearAll: () => {
|
|
29
219
|
storage.clear(StorageScope.Memory);
|
|
@@ -31,83 +221,201 @@ export const storage = {
|
|
|
31
221
|
storage.clear(StorageScope.Secure);
|
|
32
222
|
}
|
|
33
223
|
};
|
|
224
|
+
function canUseRawBatchPath(item) {
|
|
225
|
+
return item._hasExpiration === false && item._hasValidation === false;
|
|
226
|
+
}
|
|
34
227
|
function defaultSerialize(value) {
|
|
35
|
-
return
|
|
228
|
+
return serializeWithPrimitiveFastPath(value);
|
|
36
229
|
}
|
|
37
230
|
function defaultDeserialize(value) {
|
|
38
|
-
return
|
|
231
|
+
return deserializeWithPrimitiveFastPath(value);
|
|
39
232
|
}
|
|
40
233
|
export function createStorageItem(config) {
|
|
41
234
|
const serialize = config.serialize ?? defaultSerialize;
|
|
42
235
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
43
236
|
const isMemory = config.scope === StorageScope.Memory;
|
|
237
|
+
const validate = config.validate;
|
|
238
|
+
const onValidationError = config.onValidationError;
|
|
239
|
+
const expiration = config.expiration;
|
|
240
|
+
const expirationTtlMs = expiration?.ttlMs;
|
|
241
|
+
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
242
|
+
const readCache = !isMemory && config.readCache === true;
|
|
243
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true;
|
|
244
|
+
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
245
|
+
if (expiration && expiration.ttlMs <= 0) {
|
|
246
|
+
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
247
|
+
}
|
|
44
248
|
const listeners = new Set();
|
|
45
249
|
let unsubscribe = null;
|
|
250
|
+
let lastRaw = undefined;
|
|
251
|
+
let lastValue;
|
|
252
|
+
let hasLastValue = false;
|
|
253
|
+
const invalidateParsedCache = () => {
|
|
254
|
+
lastRaw = undefined;
|
|
255
|
+
lastValue = undefined;
|
|
256
|
+
hasLastValue = false;
|
|
257
|
+
};
|
|
46
258
|
const ensureSubscription = () => {
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
259
|
+
if (unsubscribe) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const listener = () => {
|
|
263
|
+
invalidateParsedCache();
|
|
264
|
+
listeners.forEach(callback => callback());
|
|
265
|
+
};
|
|
266
|
+
if (isMemory) {
|
|
267
|
+
unsubscribe = addKeyListener(memoryListeners, config.key, listener);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
ensureNativeScopeSubscription(nonMemoryScope);
|
|
271
|
+
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), config.key, listener);
|
|
272
|
+
};
|
|
273
|
+
const readStoredRaw = () => {
|
|
274
|
+
if (isMemory) {
|
|
275
|
+
if (memoryExpiration) {
|
|
276
|
+
const expiresAt = memoryExpiration.get(config.key);
|
|
277
|
+
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
278
|
+
memoryExpiration.delete(config.key);
|
|
279
|
+
memoryStore.delete(config.key);
|
|
280
|
+
notifyKeyListeners(memoryListeners, config.key);
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return memoryStore.get(config.key);
|
|
285
|
+
}
|
|
286
|
+
if (nonMemoryScope === StorageScope.Secure && hasPendingSecureWrite(config.key)) {
|
|
287
|
+
return readPendingSecureWrite(config.key);
|
|
288
|
+
}
|
|
289
|
+
if (readCache) {
|
|
290
|
+
if (hasCachedRawValue(nonMemoryScope, config.key)) {
|
|
291
|
+
return readCachedRawValue(nonMemoryScope, config.key);
|
|
66
292
|
}
|
|
67
293
|
}
|
|
294
|
+
const raw = getStorageModule().get(config.key, config.scope);
|
|
295
|
+
cacheRawValue(nonMemoryScope, config.key, raw);
|
|
296
|
+
return raw;
|
|
68
297
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
298
|
+
const writeStoredRaw = rawValue => {
|
|
299
|
+
cacheRawValue(nonMemoryScope, config.key, rawValue);
|
|
300
|
+
if (coalesceSecureWrites) {
|
|
301
|
+
scheduleSecureWrite(config.key, rawValue);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
305
|
+
clearPendingSecureWrite(config.key);
|
|
306
|
+
}
|
|
307
|
+
getStorageModule().set(config.key, rawValue, config.scope);
|
|
308
|
+
};
|
|
309
|
+
const removeStoredRaw = () => {
|
|
310
|
+
cacheRawValue(nonMemoryScope, config.key, undefined);
|
|
311
|
+
if (coalesceSecureWrites) {
|
|
312
|
+
scheduleSecureWrite(config.key, undefined);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
316
|
+
clearPendingSecureWrite(config.key);
|
|
317
|
+
}
|
|
318
|
+
getStorageModule().remove(config.key, config.scope);
|
|
319
|
+
};
|
|
320
|
+
const writeValueWithoutValidation = value => {
|
|
73
321
|
if (isMemory) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
322
|
+
if (memoryExpiration) {
|
|
323
|
+
memoryExpiration.set(config.key, Date.now() + (expirationTtlMs ?? 0));
|
|
324
|
+
}
|
|
325
|
+
memoryStore.set(config.key, value);
|
|
326
|
+
notifyKeyListeners(memoryListeners, config.key);
|
|
327
|
+
return;
|
|
77
328
|
}
|
|
78
|
-
|
|
329
|
+
const serialized = serialize(value);
|
|
330
|
+
if (expiration) {
|
|
331
|
+
const envelope = {
|
|
332
|
+
__nitroStorageEnvelope: true,
|
|
333
|
+
expiresAt: Date.now() + expiration.ttlMs,
|
|
334
|
+
payload: serialized
|
|
335
|
+
};
|
|
336
|
+
writeStoredRaw(JSON.stringify(envelope));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
writeStoredRaw(serialized);
|
|
340
|
+
};
|
|
341
|
+
const resolveInvalidValue = invalidValue => {
|
|
342
|
+
if (onValidationError) {
|
|
343
|
+
return onValidationError(invalidValue);
|
|
344
|
+
}
|
|
345
|
+
return config.defaultValue;
|
|
346
|
+
};
|
|
347
|
+
const ensureValidatedValue = (candidate, hadStoredValue) => {
|
|
348
|
+
if (!validate || validate(candidate)) {
|
|
349
|
+
return candidate;
|
|
350
|
+
}
|
|
351
|
+
const resolved = resolveInvalidValue(candidate);
|
|
352
|
+
if (validate && !validate(resolved)) {
|
|
353
|
+
return config.defaultValue;
|
|
354
|
+
}
|
|
355
|
+
if (hadStoredValue) {
|
|
356
|
+
writeValueWithoutValidation(resolved);
|
|
357
|
+
}
|
|
358
|
+
return resolved;
|
|
359
|
+
};
|
|
360
|
+
const get = () => {
|
|
361
|
+
const raw = readStoredRaw();
|
|
362
|
+
const canUseCachedValue = !expiration && !memoryExpiration;
|
|
363
|
+
if (canUseCachedValue && raw === lastRaw && hasLastValue) {
|
|
79
364
|
return lastValue;
|
|
80
365
|
}
|
|
81
366
|
lastRaw = raw;
|
|
82
367
|
if (raw === undefined) {
|
|
83
|
-
lastValue = config.defaultValue;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
368
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
369
|
+
hasLastValue = true;
|
|
370
|
+
return lastValue;
|
|
371
|
+
}
|
|
372
|
+
if (isMemory) {
|
|
373
|
+
lastValue = ensureValidatedValue(raw, true);
|
|
374
|
+
hasLastValue = true;
|
|
375
|
+
return lastValue;
|
|
376
|
+
}
|
|
377
|
+
let deserializableRaw = raw;
|
|
378
|
+
if (expiration) {
|
|
379
|
+
try {
|
|
380
|
+
const parsed = JSON.parse(raw);
|
|
381
|
+
if (isStoredEnvelope(parsed)) {
|
|
382
|
+
if (parsed.expiresAt <= Date.now()) {
|
|
383
|
+
removeStoredRaw();
|
|
384
|
+
invalidateParsedCache();
|
|
385
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
386
|
+
hasLastValue = true;
|
|
387
|
+
return lastValue;
|
|
388
|
+
}
|
|
389
|
+
deserializableRaw = parsed.payload;
|
|
390
|
+
}
|
|
391
|
+
} catch {
|
|
392
|
+
// Keep backward compatibility with legacy raw values.
|
|
89
393
|
}
|
|
90
394
|
}
|
|
395
|
+
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
396
|
+
hasLastValue = true;
|
|
91
397
|
return lastValue;
|
|
92
398
|
};
|
|
93
399
|
const set = valueOrFn => {
|
|
94
400
|
const currentValue = get();
|
|
95
401
|
const newValue = typeof valueOrFn === "function" ? valueOrFn(currentValue) : valueOrFn;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
} else {
|
|
100
|
-
const serialized = serialize(newValue);
|
|
101
|
-
getStorageModule().set(config.key, serialized, config.scope);
|
|
402
|
+
invalidateParsedCache();
|
|
403
|
+
if (validate && !validate(newValue)) {
|
|
404
|
+
throw new Error(`Validation failed for key "${config.key}" in scope "${StorageScope[config.scope]}".`);
|
|
102
405
|
}
|
|
406
|
+
writeValueWithoutValidation(newValue);
|
|
103
407
|
};
|
|
104
408
|
const deleteItem = () => {
|
|
409
|
+
invalidateParsedCache();
|
|
105
410
|
if (isMemory) {
|
|
411
|
+
if (memoryExpiration) {
|
|
412
|
+
memoryExpiration.delete(config.key);
|
|
413
|
+
}
|
|
106
414
|
memoryStore.delete(config.key);
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
getStorageModule().remove(config.key, config.scope);
|
|
415
|
+
notifyKeyListeners(memoryListeners, config.key);
|
|
416
|
+
return;
|
|
110
417
|
}
|
|
418
|
+
removeStoredRaw();
|
|
111
419
|
};
|
|
112
420
|
const subscribe = callback => {
|
|
113
421
|
ensureSubscription();
|
|
@@ -116,11 +424,14 @@ export function createStorageItem(config) {
|
|
|
116
424
|
listeners.delete(callback);
|
|
117
425
|
if (listeners.size === 0 && unsubscribe) {
|
|
118
426
|
unsubscribe();
|
|
427
|
+
if (!isMemory) {
|
|
428
|
+
maybeCleanupNativeScopeSubscription(nonMemoryScope);
|
|
429
|
+
}
|
|
119
430
|
unsubscribe = null;
|
|
120
431
|
}
|
|
121
432
|
};
|
|
122
433
|
};
|
|
123
|
-
|
|
434
|
+
const storageItem = {
|
|
124
435
|
get,
|
|
125
436
|
set,
|
|
126
437
|
delete: deleteItem,
|
|
@@ -128,29 +439,83 @@ export function createStorageItem(config) {
|
|
|
128
439
|
serialize,
|
|
129
440
|
deserialize,
|
|
130
441
|
_triggerListeners: () => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
listeners.forEach(l => l());
|
|
442
|
+
invalidateParsedCache();
|
|
443
|
+
listeners.forEach(listener => listener());
|
|
134
444
|
},
|
|
445
|
+
_hasValidation: validate !== undefined,
|
|
446
|
+
_hasExpiration: expiration !== undefined,
|
|
447
|
+
_readCacheEnabled: readCache,
|
|
135
448
|
scope: config.scope,
|
|
136
449
|
key: config.key
|
|
137
450
|
};
|
|
451
|
+
return storageItem;
|
|
138
452
|
}
|
|
139
453
|
export function useStorage(item) {
|
|
140
454
|
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
141
455
|
return [value, item.set];
|
|
142
456
|
}
|
|
457
|
+
export function useStorageSelector(item, selector, isEqual = Object.is) {
|
|
458
|
+
const selectedRef = useRef({
|
|
459
|
+
hasValue: false
|
|
460
|
+
});
|
|
461
|
+
const getSelectedSnapshot = () => {
|
|
462
|
+
const nextSelected = selector(item.get());
|
|
463
|
+
const current = selectedRef.current;
|
|
464
|
+
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
465
|
+
return current.value;
|
|
466
|
+
}
|
|
467
|
+
selectedRef.current = {
|
|
468
|
+
hasValue: true,
|
|
469
|
+
value: nextSelected
|
|
470
|
+
};
|
|
471
|
+
return nextSelected;
|
|
472
|
+
};
|
|
473
|
+
const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
|
|
474
|
+
return [selectedValue, item.set];
|
|
475
|
+
}
|
|
143
476
|
export function useSetStorage(item) {
|
|
144
477
|
return item.set;
|
|
145
478
|
}
|
|
146
479
|
export function getBatch(items, scope) {
|
|
480
|
+
assertBatchScope(items, scope);
|
|
147
481
|
if (scope === StorageScope.Memory) {
|
|
148
482
|
return items.map(item => item.get());
|
|
149
483
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
484
|
+
const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
|
|
485
|
+
if (!useRawBatchPath) {
|
|
486
|
+
return items.map(item => item.get());
|
|
487
|
+
}
|
|
488
|
+
const useBatchCache = items.every(item => item._readCacheEnabled === true);
|
|
489
|
+
const rawValues = new Array(items.length);
|
|
490
|
+
const keysToFetch = [];
|
|
491
|
+
const keyIndexes = [];
|
|
492
|
+
items.forEach((item, index) => {
|
|
493
|
+
if (scope === StorageScope.Secure) {
|
|
494
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
495
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (useBatchCache) {
|
|
500
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
501
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
keysToFetch.push(item.key);
|
|
506
|
+
keyIndexes.push(index);
|
|
507
|
+
});
|
|
508
|
+
if (keysToFetch.length > 0) {
|
|
509
|
+
const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
|
|
510
|
+
fetchedValues.forEach((value, index) => {
|
|
511
|
+
const key = keysToFetch[index];
|
|
512
|
+
const targetIndex = keyIndexes[index];
|
|
513
|
+
rawValues[targetIndex] = value;
|
|
514
|
+
cacheRawValue(scope, key, value);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return items.map((item, index) => {
|
|
518
|
+
const raw = rawValues[index];
|
|
154
519
|
if (raw === undefined) {
|
|
155
520
|
return item.get();
|
|
156
521
|
}
|
|
@@ -158,6 +523,7 @@ export function getBatch(items, scope) {
|
|
|
158
523
|
});
|
|
159
524
|
}
|
|
160
525
|
export function setBatch(items, scope) {
|
|
526
|
+
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
161
527
|
if (scope === StorageScope.Memory) {
|
|
162
528
|
items.forEach(({
|
|
163
529
|
item,
|
|
@@ -165,22 +531,117 @@ export function setBatch(items, scope) {
|
|
|
165
531
|
}) => item.set(value));
|
|
166
532
|
return;
|
|
167
533
|
}
|
|
168
|
-
const
|
|
169
|
-
const values = items.map(i => i.item.serialize(i.value));
|
|
170
|
-
getStorageModule().setBatch(keys, values, scope);
|
|
171
|
-
items.forEach(({
|
|
534
|
+
const useRawBatchPath = items.every(({
|
|
172
535
|
item
|
|
173
|
-
}) =>
|
|
174
|
-
|
|
175
|
-
|
|
536
|
+
}) => canUseRawBatchPath(item));
|
|
537
|
+
if (!useRawBatchPath) {
|
|
538
|
+
items.forEach(({
|
|
539
|
+
item,
|
|
540
|
+
value
|
|
541
|
+
}) => item.set(value));
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const keys = items.map(entry => entry.item.key);
|
|
545
|
+
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
546
|
+
if (scope === StorageScope.Secure) {
|
|
547
|
+
flushSecureWrites();
|
|
548
|
+
}
|
|
549
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
550
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
176
551
|
}
|
|
177
552
|
export function removeBatch(items, scope) {
|
|
553
|
+
assertBatchScope(items, scope);
|
|
178
554
|
if (scope === StorageScope.Memory) {
|
|
179
555
|
items.forEach(item => item.delete());
|
|
180
556
|
return;
|
|
181
557
|
}
|
|
182
558
|
const keys = items.map(item => item.key);
|
|
559
|
+
if (scope === StorageScope.Secure) {
|
|
560
|
+
flushSecureWrites();
|
|
561
|
+
}
|
|
183
562
|
getStorageModule().removeBatch(keys, scope);
|
|
184
|
-
|
|
563
|
+
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
564
|
+
}
|
|
565
|
+
export function registerMigration(version, migration) {
|
|
566
|
+
if (!Number.isInteger(version) || version <= 0) {
|
|
567
|
+
throw new Error("Migration version must be a positive integer.");
|
|
568
|
+
}
|
|
569
|
+
if (registeredMigrations.has(version)) {
|
|
570
|
+
throw new Error(`Migration version ${version} is already registered.`);
|
|
571
|
+
}
|
|
572
|
+
registeredMigrations.set(version, migration);
|
|
573
|
+
}
|
|
574
|
+
export function migrateToLatest(scope = StorageScope.Disk) {
|
|
575
|
+
assertValidScope(scope);
|
|
576
|
+
const currentVersion = readMigrationVersion(scope);
|
|
577
|
+
const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
|
|
578
|
+
let appliedVersion = currentVersion;
|
|
579
|
+
const context = {
|
|
580
|
+
scope,
|
|
581
|
+
getRaw: key => getRawValue(key, scope),
|
|
582
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
583
|
+
removeRaw: key => removeRawValue(key, scope)
|
|
584
|
+
};
|
|
585
|
+
versions.forEach(version => {
|
|
586
|
+
const migration = registeredMigrations.get(version);
|
|
587
|
+
if (!migration) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
migration(context);
|
|
591
|
+
writeMigrationVersion(scope, version);
|
|
592
|
+
appliedVersion = version;
|
|
593
|
+
});
|
|
594
|
+
return appliedVersion;
|
|
595
|
+
}
|
|
596
|
+
export function runTransaction(scope, transaction) {
|
|
597
|
+
assertValidScope(scope);
|
|
598
|
+
if (scope === StorageScope.Secure) {
|
|
599
|
+
flushSecureWrites();
|
|
600
|
+
}
|
|
601
|
+
const rollback = new Map();
|
|
602
|
+
const rememberRollback = key => {
|
|
603
|
+
if (rollback.has(key)) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
rollback.set(key, getRawValue(key, scope));
|
|
607
|
+
};
|
|
608
|
+
const tx = {
|
|
609
|
+
scope,
|
|
610
|
+
getRaw: key => getRawValue(key, scope),
|
|
611
|
+
setRaw: (key, value) => {
|
|
612
|
+
rememberRollback(key);
|
|
613
|
+
setRawValue(key, value, scope);
|
|
614
|
+
},
|
|
615
|
+
removeRaw: key => {
|
|
616
|
+
rememberRollback(key);
|
|
617
|
+
removeRawValue(key, scope);
|
|
618
|
+
},
|
|
619
|
+
getItem: item => {
|
|
620
|
+
assertBatchScope([item], scope);
|
|
621
|
+
return item.get();
|
|
622
|
+
},
|
|
623
|
+
setItem: (item, value) => {
|
|
624
|
+
assertBatchScope([item], scope);
|
|
625
|
+
rememberRollback(item.key);
|
|
626
|
+
item.set(value);
|
|
627
|
+
},
|
|
628
|
+
removeItem: item => {
|
|
629
|
+
assertBatchScope([item], scope);
|
|
630
|
+
rememberRollback(item.key);
|
|
631
|
+
item.delete();
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
try {
|
|
635
|
+
return transaction(tx);
|
|
636
|
+
} catch (error) {
|
|
637
|
+
Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
|
|
638
|
+
if (previousValue === undefined) {
|
|
639
|
+
removeRawValue(key, scope);
|
|
640
|
+
} else {
|
|
641
|
+
setRawValue(key, previousValue, scope);
|
|
642
|
+
}
|
|
643
|
+
});
|
|
644
|
+
throw error;
|
|
645
|
+
}
|
|
185
646
|
}
|
|
186
647
|
//# sourceMappingURL=index.js.map
|