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