react-native-nitro-storage 0.1.4 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +432 -345
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +191 -3
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +21 -41
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +181 -29
- package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
- package/app.plugin.js +9 -7
- package/cpp/bindings/HybridStorage.cpp +239 -10
- package/cpp/bindings/HybridStorage.hpp +10 -0
- package/cpp/core/NativeStorageAdapter.hpp +22 -0
- package/ios/IOSStorageAdapterCpp.hpp +25 -0
- package/ios/IOSStorageAdapterCpp.mm +315 -33
- package/lib/commonjs/Storage.types.js +23 -1
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/index.js +680 -68
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +801 -133
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +112 -0
- package/lib/commonjs/internal.js.map +1 -0
- package/lib/module/Storage.types.js +22 -0
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/index.js +660 -71
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +766 -125
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +100 -0
- package/lib/module/internal.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +10 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/Storage.types.d.ts +20 -0
- package/lib/typescript/Storage.types.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +68 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +79 -13
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +21 -0
- package/lib/typescript/internal.d.ts.map +1 -0
- package/lib/typescript/migration.d.ts +2 -3
- package/lib/typescript/migration.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +10 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +10 -0
- package/package.json +22 -8
- package/src/Storage.nitro.ts +11 -2
- package/src/Storage.types.ts +22 -0
- package/src/index.ts +943 -84
- package/src/index.web.ts +1082 -137
- package/src/internal.ts +144 -0
- package/src/migration.ts +3 -3
package/lib/module/index.js
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
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
|
-
import { StorageScope } from "./Storage.types";
|
|
6
|
-
|
|
5
|
+
import { StorageScope, AccessControl } from "./Storage.types";
|
|
6
|
+
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
|
|
7
|
+
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
8
|
+
export { migrateFromMMKV } from "./migration";
|
|
9
|
+
function asInternal(item) {
|
|
10
|
+
return item;
|
|
11
|
+
}
|
|
12
|
+
const registeredMigrations = new Map();
|
|
13
|
+
const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
|
|
14
|
+
Promise.resolve().then(task);
|
|
15
|
+
};
|
|
7
16
|
let _storageModule = null;
|
|
8
17
|
function getStorageModule() {
|
|
9
18
|
if (!_storageModule) {
|
|
@@ -12,102 +21,506 @@ function getStorageModule() {
|
|
|
12
21
|
return _storageModule;
|
|
13
22
|
}
|
|
14
23
|
const memoryStore = new Map();
|
|
15
|
-
const memoryListeners = new
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
const memoryListeners = new Map();
|
|
25
|
+
const scopedListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
26
|
+
const scopedUnsubscribers = new Map();
|
|
27
|
+
const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
28
|
+
const pendingSecureWrites = new Map();
|
|
29
|
+
let secureFlushScheduled = false;
|
|
30
|
+
let secureDefaultAccessControl = AccessControl.WhenUnlocked;
|
|
31
|
+
function getScopedListeners(scope) {
|
|
32
|
+
return scopedListeners.get(scope);
|
|
33
|
+
}
|
|
34
|
+
function getScopeRawCache(scope) {
|
|
35
|
+
return scopedRawCache.get(scope);
|
|
36
|
+
}
|
|
37
|
+
function cacheRawValue(scope, key, value) {
|
|
38
|
+
getScopeRawCache(scope).set(key, value);
|
|
39
|
+
}
|
|
40
|
+
function readCachedRawValue(scope, key) {
|
|
41
|
+
return getScopeRawCache(scope).get(key);
|
|
42
|
+
}
|
|
43
|
+
function hasCachedRawValue(scope, key) {
|
|
44
|
+
return getScopeRawCache(scope).has(key);
|
|
45
|
+
}
|
|
46
|
+
function clearScopeRawCache(scope) {
|
|
47
|
+
getScopeRawCache(scope).clear();
|
|
48
|
+
}
|
|
49
|
+
function notifyKeyListeners(registry, key) {
|
|
50
|
+
registry.get(key)?.forEach(listener => listener());
|
|
51
|
+
}
|
|
52
|
+
function notifyAllListeners(registry) {
|
|
53
|
+
registry.forEach(listeners => {
|
|
54
|
+
listeners.forEach(listener => listener());
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function addKeyListener(registry, key, listener) {
|
|
58
|
+
let listeners = registry.get(key);
|
|
59
|
+
if (!listeners) {
|
|
60
|
+
listeners = new Set();
|
|
61
|
+
registry.set(key, listeners);
|
|
62
|
+
}
|
|
63
|
+
listeners.add(listener);
|
|
64
|
+
return () => {
|
|
65
|
+
const scopedListeners = registry.get(key);
|
|
66
|
+
if (!scopedListeners) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
scopedListeners.delete(listener);
|
|
70
|
+
if (scopedListeners.size === 0) {
|
|
71
|
+
registry.delete(key);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function readPendingSecureWrite(key) {
|
|
76
|
+
return pendingSecureWrites.get(key)?.value;
|
|
77
|
+
}
|
|
78
|
+
function hasPendingSecureWrite(key) {
|
|
79
|
+
return pendingSecureWrites.has(key);
|
|
80
|
+
}
|
|
81
|
+
function clearPendingSecureWrite(key) {
|
|
82
|
+
pendingSecureWrites.delete(key);
|
|
83
|
+
}
|
|
84
|
+
function flushSecureWrites() {
|
|
85
|
+
secureFlushScheduled = false;
|
|
86
|
+
if (pendingSecureWrites.size === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const writes = Array.from(pendingSecureWrites.values());
|
|
90
|
+
pendingSecureWrites.clear();
|
|
91
|
+
const keysToSet = [];
|
|
92
|
+
const valuesToSet = [];
|
|
93
|
+
const keysToRemove = [];
|
|
94
|
+
writes.forEach(({
|
|
95
|
+
key,
|
|
96
|
+
value
|
|
97
|
+
}) => {
|
|
98
|
+
if (value === undefined) {
|
|
99
|
+
keysToRemove.push(key);
|
|
100
|
+
} else {
|
|
101
|
+
keysToSet.push(key);
|
|
102
|
+
valuesToSet.push(value);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
const storageModule = getStorageModule();
|
|
106
|
+
storageModule.setSecureAccessControl(secureDefaultAccessControl);
|
|
107
|
+
if (keysToSet.length > 0) {
|
|
108
|
+
storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
|
|
109
|
+
}
|
|
110
|
+
if (keysToRemove.length > 0) {
|
|
111
|
+
storageModule.removeBatch(keysToRemove, StorageScope.Secure);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function scheduleSecureWrite(key, value) {
|
|
115
|
+
pendingSecureWrites.set(key, {
|
|
116
|
+
key,
|
|
117
|
+
value
|
|
118
|
+
});
|
|
119
|
+
if (secureFlushScheduled) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
secureFlushScheduled = true;
|
|
123
|
+
runMicrotask(flushSecureWrites);
|
|
124
|
+
}
|
|
125
|
+
function ensureNativeScopeSubscription(scope) {
|
|
126
|
+
if (scopedUnsubscribers.has(scope)) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
|
|
130
|
+
if (scope === StorageScope.Secure) {
|
|
131
|
+
if (key === "") {
|
|
132
|
+
pendingSecureWrites.clear();
|
|
133
|
+
} else {
|
|
134
|
+
clearPendingSecureWrite(key);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (key === "") {
|
|
138
|
+
clearScopeRawCache(scope);
|
|
139
|
+
notifyAllListeners(getScopedListeners(scope));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
cacheRawValue(scope, key, value);
|
|
143
|
+
notifyKeyListeners(getScopedListeners(scope), key);
|
|
144
|
+
});
|
|
145
|
+
scopedUnsubscribers.set(scope, unsubscribe);
|
|
146
|
+
}
|
|
147
|
+
function maybeCleanupNativeScopeSubscription(scope) {
|
|
148
|
+
const listeners = getScopedListeners(scope);
|
|
149
|
+
if (listeners.size > 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const unsubscribe = scopedUnsubscribers.get(scope);
|
|
153
|
+
if (!unsubscribe) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
unsubscribe();
|
|
157
|
+
scopedUnsubscribers.delete(scope);
|
|
158
|
+
}
|
|
159
|
+
function getRawValue(key, scope) {
|
|
160
|
+
assertValidScope(scope);
|
|
161
|
+
if (scope === StorageScope.Memory) {
|
|
162
|
+
const value = memoryStore.get(key);
|
|
163
|
+
return typeof value === "string" ? value : undefined;
|
|
164
|
+
}
|
|
165
|
+
if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
166
|
+
return readPendingSecureWrite(key);
|
|
167
|
+
}
|
|
168
|
+
return getStorageModule().get(key, scope);
|
|
169
|
+
}
|
|
170
|
+
function setRawValue(key, value, scope) {
|
|
171
|
+
assertValidScope(scope);
|
|
172
|
+
if (scope === StorageScope.Memory) {
|
|
173
|
+
memoryStore.set(key, value);
|
|
174
|
+
notifyKeyListeners(memoryListeners, key);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (scope === StorageScope.Secure) {
|
|
178
|
+
flushSecureWrites();
|
|
179
|
+
clearPendingSecureWrite(key);
|
|
180
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
181
|
+
}
|
|
182
|
+
getStorageModule().set(key, value, scope);
|
|
183
|
+
cacheRawValue(scope, key, value);
|
|
184
|
+
}
|
|
185
|
+
function removeRawValue(key, scope) {
|
|
186
|
+
assertValidScope(scope);
|
|
187
|
+
if (scope === StorageScope.Memory) {
|
|
188
|
+
memoryStore.delete(key);
|
|
189
|
+
notifyKeyListeners(memoryListeners, key);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
if (scope === StorageScope.Secure) {
|
|
193
|
+
flushSecureWrites();
|
|
194
|
+
clearPendingSecureWrite(key);
|
|
195
|
+
}
|
|
196
|
+
getStorageModule().remove(key, scope);
|
|
197
|
+
cacheRawValue(scope, key, undefined);
|
|
198
|
+
}
|
|
199
|
+
function readMigrationVersion(scope) {
|
|
200
|
+
const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
|
|
201
|
+
if (raw === undefined) {
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
const parsed = Number.parseInt(raw, 10);
|
|
205
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
206
|
+
}
|
|
207
|
+
function writeMigrationVersion(scope, version) {
|
|
208
|
+
setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
|
|
18
209
|
}
|
|
19
210
|
export const storage = {
|
|
20
211
|
clear: scope => {
|
|
21
212
|
if (scope === StorageScope.Memory) {
|
|
22
213
|
memoryStore.clear();
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
214
|
+
notifyAllListeners(memoryListeners);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (scope === StorageScope.Secure) {
|
|
218
|
+
flushSecureWrites();
|
|
219
|
+
pendingSecureWrites.clear();
|
|
220
|
+
}
|
|
221
|
+
clearScopeRawCache(scope);
|
|
222
|
+
getStorageModule().clear(scope);
|
|
223
|
+
if (scope === StorageScope.Secure) {
|
|
224
|
+
getStorageModule().clearSecureBiometric();
|
|
26
225
|
}
|
|
27
226
|
},
|
|
28
227
|
clearAll: () => {
|
|
29
228
|
storage.clear(StorageScope.Memory);
|
|
30
229
|
storage.clear(StorageScope.Disk);
|
|
31
230
|
storage.clear(StorageScope.Secure);
|
|
231
|
+
},
|
|
232
|
+
clearNamespace: (namespace, scope) => {
|
|
233
|
+
assertValidScope(scope);
|
|
234
|
+
if (scope === StorageScope.Memory) {
|
|
235
|
+
for (const key of memoryStore.keys()) {
|
|
236
|
+
if (isNamespaced(key, namespace)) {
|
|
237
|
+
memoryStore.delete(key);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
notifyAllListeners(memoryListeners);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
if (scope === StorageScope.Secure) {
|
|
244
|
+
flushSecureWrites();
|
|
245
|
+
}
|
|
246
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
247
|
+
const namespacedKeys = keys.filter(k => isNamespaced(k, namespace));
|
|
248
|
+
if (namespacedKeys.length > 0) {
|
|
249
|
+
getStorageModule().removeBatch(namespacedKeys, scope);
|
|
250
|
+
namespacedKeys.forEach(k => cacheRawValue(scope, k, undefined));
|
|
251
|
+
if (scope === StorageScope.Secure) {
|
|
252
|
+
namespacedKeys.forEach(k => clearPendingSecureWrite(k));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
clearBiometric: () => {
|
|
257
|
+
getStorageModule().clearSecureBiometric();
|
|
258
|
+
},
|
|
259
|
+
has: (key, scope) => {
|
|
260
|
+
assertValidScope(scope);
|
|
261
|
+
if (scope === StorageScope.Memory) {
|
|
262
|
+
return memoryStore.has(key);
|
|
263
|
+
}
|
|
264
|
+
return getStorageModule().has(key, scope);
|
|
265
|
+
},
|
|
266
|
+
getAllKeys: scope => {
|
|
267
|
+
assertValidScope(scope);
|
|
268
|
+
if (scope === StorageScope.Memory) {
|
|
269
|
+
return Array.from(memoryStore.keys());
|
|
270
|
+
}
|
|
271
|
+
return getStorageModule().getAllKeys(scope);
|
|
272
|
+
},
|
|
273
|
+
getAll: scope => {
|
|
274
|
+
assertValidScope(scope);
|
|
275
|
+
const result = {};
|
|
276
|
+
if (scope === StorageScope.Memory) {
|
|
277
|
+
memoryStore.forEach((value, key) => {
|
|
278
|
+
if (typeof value === "string") result[key] = value;
|
|
279
|
+
});
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
283
|
+
if (keys.length === 0) return result;
|
|
284
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
285
|
+
keys.forEach((key, idx) => {
|
|
286
|
+
const val = decodeNativeBatchValue(values[idx]);
|
|
287
|
+
if (val !== undefined) result[key] = val;
|
|
288
|
+
});
|
|
289
|
+
return result;
|
|
290
|
+
},
|
|
291
|
+
size: scope => {
|
|
292
|
+
assertValidScope(scope);
|
|
293
|
+
if (scope === StorageScope.Memory) {
|
|
294
|
+
return memoryStore.size;
|
|
295
|
+
}
|
|
296
|
+
return getStorageModule().size(scope);
|
|
297
|
+
},
|
|
298
|
+
setAccessControl: level => {
|
|
299
|
+
secureDefaultAccessControl = level;
|
|
300
|
+
getStorageModule().setSecureAccessControl(level);
|
|
301
|
+
},
|
|
302
|
+
setKeychainAccessGroup: group => {
|
|
303
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
32
304
|
}
|
|
33
305
|
};
|
|
306
|
+
function canUseRawBatchPath(item) {
|
|
307
|
+
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
308
|
+
}
|
|
34
309
|
function defaultSerialize(value) {
|
|
35
|
-
return
|
|
310
|
+
return serializeWithPrimitiveFastPath(value);
|
|
36
311
|
}
|
|
37
312
|
function defaultDeserialize(value) {
|
|
38
|
-
return
|
|
313
|
+
return deserializeWithPrimitiveFastPath(value);
|
|
39
314
|
}
|
|
40
315
|
export function createStorageItem(config) {
|
|
316
|
+
const storageKey = prefixKey(config.namespace, config.key);
|
|
41
317
|
const serialize = config.serialize ?? defaultSerialize;
|
|
42
318
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
43
319
|
const isMemory = config.scope === StorageScope.Memory;
|
|
320
|
+
const isBiometric = config.biometric === true && config.scope === StorageScope.Secure;
|
|
321
|
+
const secureAccessControl = config.accessControl;
|
|
322
|
+
const validate = config.validate;
|
|
323
|
+
const onValidationError = config.onValidationError;
|
|
324
|
+
const expiration = config.expiration;
|
|
325
|
+
const onExpired = config.onExpired;
|
|
326
|
+
const expirationTtlMs = expiration?.ttlMs;
|
|
327
|
+
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
328
|
+
const readCache = !isMemory && config.readCache === true;
|
|
329
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
|
|
330
|
+
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
331
|
+
if (expiration && expiration.ttlMs <= 0) {
|
|
332
|
+
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
333
|
+
}
|
|
44
334
|
const listeners = new Set();
|
|
45
335
|
let unsubscribe = null;
|
|
336
|
+
let lastRaw = undefined;
|
|
337
|
+
let lastValue;
|
|
338
|
+
let hasLastValue = false;
|
|
339
|
+
const invalidateParsedCache = () => {
|
|
340
|
+
lastRaw = undefined;
|
|
341
|
+
lastValue = undefined;
|
|
342
|
+
hasLastValue = false;
|
|
343
|
+
};
|
|
46
344
|
const ensureSubscription = () => {
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
345
|
+
if (unsubscribe) {
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const listener = () => {
|
|
349
|
+
invalidateParsedCache();
|
|
350
|
+
listeners.forEach(callback => callback());
|
|
351
|
+
};
|
|
352
|
+
if (isMemory) {
|
|
353
|
+
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
ensureNativeScopeSubscription(nonMemoryScope);
|
|
357
|
+
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
|
|
358
|
+
};
|
|
359
|
+
const readStoredRaw = () => {
|
|
360
|
+
if (isMemory) {
|
|
361
|
+
if (memoryExpiration) {
|
|
362
|
+
const expiresAt = memoryExpiration.get(storageKey);
|
|
363
|
+
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
364
|
+
memoryExpiration.delete(storageKey);
|
|
365
|
+
memoryStore.delete(storageKey);
|
|
366
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
367
|
+
onExpired?.(storageKey);
|
|
368
|
+
return undefined;
|
|
369
|
+
}
|
|
66
370
|
}
|
|
371
|
+
return memoryStore.get(storageKey);
|
|
67
372
|
}
|
|
373
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric && hasPendingSecureWrite(storageKey)) {
|
|
374
|
+
return readPendingSecureWrite(storageKey);
|
|
375
|
+
}
|
|
376
|
+
if (readCache) {
|
|
377
|
+
if (hasCachedRawValue(nonMemoryScope, storageKey)) {
|
|
378
|
+
return readCachedRawValue(nonMemoryScope, storageKey);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (isBiometric) {
|
|
382
|
+
return getStorageModule().getSecureBiometric(storageKey);
|
|
383
|
+
}
|
|
384
|
+
const raw = getStorageModule().get(storageKey, config.scope);
|
|
385
|
+
cacheRawValue(nonMemoryScope, storageKey, raw);
|
|
386
|
+
return raw;
|
|
68
387
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
388
|
+
const writeStoredRaw = rawValue => {
|
|
389
|
+
if (isBiometric) {
|
|
390
|
+
getStorageModule().setSecureBiometric(storageKey, rawValue);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
394
|
+
if (coalesceSecureWrites) {
|
|
395
|
+
scheduleSecureWrite(storageKey, rawValue);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
399
|
+
clearPendingSecureWrite(storageKey);
|
|
400
|
+
getStorageModule().setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
|
|
401
|
+
}
|
|
402
|
+
getStorageModule().set(storageKey, rawValue, config.scope);
|
|
403
|
+
};
|
|
404
|
+
const removeStoredRaw = () => {
|
|
405
|
+
if (isBiometric) {
|
|
406
|
+
getStorageModule().deleteSecureBiometric(storageKey);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
410
|
+
if (coalesceSecureWrites) {
|
|
411
|
+
scheduleSecureWrite(storageKey, undefined);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (nonMemoryScope === StorageScope.Secure) {
|
|
415
|
+
clearPendingSecureWrite(storageKey);
|
|
416
|
+
}
|
|
417
|
+
getStorageModule().remove(storageKey, config.scope);
|
|
418
|
+
};
|
|
419
|
+
const writeValueWithoutValidation = value => {
|
|
73
420
|
if (isMemory) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
421
|
+
if (memoryExpiration) {
|
|
422
|
+
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
423
|
+
}
|
|
424
|
+
memoryStore.set(storageKey, value);
|
|
425
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const serialized = serialize(value);
|
|
429
|
+
if (expiration) {
|
|
430
|
+
const envelope = {
|
|
431
|
+
__nitroStorageEnvelope: true,
|
|
432
|
+
expiresAt: Date.now() + expiration.ttlMs,
|
|
433
|
+
payload: serialized
|
|
434
|
+
};
|
|
435
|
+
writeStoredRaw(JSON.stringify(envelope));
|
|
436
|
+
return;
|
|
77
437
|
}
|
|
78
|
-
|
|
438
|
+
writeStoredRaw(serialized);
|
|
439
|
+
};
|
|
440
|
+
const resolveInvalidValue = invalidValue => {
|
|
441
|
+
if (onValidationError) {
|
|
442
|
+
return onValidationError(invalidValue);
|
|
443
|
+
}
|
|
444
|
+
return config.defaultValue;
|
|
445
|
+
};
|
|
446
|
+
const ensureValidatedValue = (candidate, hadStoredValue) => {
|
|
447
|
+
if (!validate || validate(candidate)) {
|
|
448
|
+
return candidate;
|
|
449
|
+
}
|
|
450
|
+
const resolved = resolveInvalidValue(candidate);
|
|
451
|
+
if (validate && !validate(resolved)) {
|
|
452
|
+
return config.defaultValue;
|
|
453
|
+
}
|
|
454
|
+
if (hadStoredValue) {
|
|
455
|
+
writeValueWithoutValidation(resolved);
|
|
456
|
+
}
|
|
457
|
+
return resolved;
|
|
458
|
+
};
|
|
459
|
+
const get = () => {
|
|
460
|
+
const raw = readStoredRaw();
|
|
461
|
+
const canUseCachedValue = !expiration && !memoryExpiration;
|
|
462
|
+
if (canUseCachedValue && raw === lastRaw && hasLastValue) {
|
|
79
463
|
return lastValue;
|
|
80
464
|
}
|
|
81
465
|
lastRaw = raw;
|
|
82
466
|
if (raw === undefined) {
|
|
83
|
-
lastValue = config.defaultValue;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
467
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
468
|
+
hasLastValue = true;
|
|
469
|
+
return lastValue;
|
|
470
|
+
}
|
|
471
|
+
if (isMemory) {
|
|
472
|
+
lastValue = ensureValidatedValue(raw, true);
|
|
473
|
+
hasLastValue = true;
|
|
474
|
+
return lastValue;
|
|
475
|
+
}
|
|
476
|
+
let deserializableRaw = raw;
|
|
477
|
+
if (expiration) {
|
|
478
|
+
try {
|
|
479
|
+
const parsed = JSON.parse(raw);
|
|
480
|
+
if (isStoredEnvelope(parsed)) {
|
|
481
|
+
if (parsed.expiresAt <= Date.now()) {
|
|
482
|
+
removeStoredRaw();
|
|
483
|
+
invalidateParsedCache();
|
|
484
|
+
onExpired?.(storageKey);
|
|
485
|
+
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
486
|
+
hasLastValue = true;
|
|
487
|
+
return lastValue;
|
|
488
|
+
}
|
|
489
|
+
deserializableRaw = parsed.payload;
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
// Keep backward compatibility with legacy raw values.
|
|
89
493
|
}
|
|
90
494
|
}
|
|
495
|
+
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
496
|
+
hasLastValue = true;
|
|
91
497
|
return lastValue;
|
|
92
498
|
};
|
|
93
499
|
const set = valueOrFn => {
|
|
94
500
|
const currentValue = get();
|
|
95
501
|
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);
|
|
502
|
+
invalidateParsedCache();
|
|
503
|
+
if (validate && !validate(newValue)) {
|
|
504
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
102
505
|
}
|
|
506
|
+
writeValueWithoutValidation(newValue);
|
|
103
507
|
};
|
|
104
508
|
const deleteItem = () => {
|
|
509
|
+
invalidateParsedCache();
|
|
105
510
|
if (isMemory) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
511
|
+
if (memoryExpiration) {
|
|
512
|
+
memoryExpiration.delete(storageKey);
|
|
513
|
+
}
|
|
514
|
+
memoryStore.delete(storageKey);
|
|
515
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
516
|
+
return;
|
|
110
517
|
}
|
|
518
|
+
removeStoredRaw();
|
|
519
|
+
};
|
|
520
|
+
const hasItem = () => {
|
|
521
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
522
|
+
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
523
|
+
return getStorageModule().has(storageKey, config.scope);
|
|
111
524
|
};
|
|
112
525
|
const subscribe = callback => {
|
|
113
526
|
ensureSubscription();
|
|
@@ -116,41 +529,101 @@ export function createStorageItem(config) {
|
|
|
116
529
|
listeners.delete(callback);
|
|
117
530
|
if (listeners.size === 0 && unsubscribe) {
|
|
118
531
|
unsubscribe();
|
|
532
|
+
if (!isMemory) {
|
|
533
|
+
maybeCleanupNativeScopeSubscription(nonMemoryScope);
|
|
534
|
+
}
|
|
119
535
|
unsubscribe = null;
|
|
120
536
|
}
|
|
121
537
|
};
|
|
122
538
|
};
|
|
123
|
-
|
|
539
|
+
const storageItem = {
|
|
124
540
|
get,
|
|
125
541
|
set,
|
|
126
542
|
delete: deleteItem,
|
|
543
|
+
has: hasItem,
|
|
127
544
|
subscribe,
|
|
128
545
|
serialize,
|
|
129
546
|
deserialize,
|
|
130
547
|
_triggerListeners: () => {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
listeners.forEach(l => l());
|
|
548
|
+
invalidateParsedCache();
|
|
549
|
+
listeners.forEach(listener => listener());
|
|
134
550
|
},
|
|
551
|
+
_hasValidation: validate !== undefined,
|
|
552
|
+
_hasExpiration: expiration !== undefined,
|
|
553
|
+
_readCacheEnabled: readCache,
|
|
554
|
+
_isBiometric: isBiometric,
|
|
555
|
+
_secureAccessControl: secureAccessControl,
|
|
135
556
|
scope: config.scope,
|
|
136
|
-
key:
|
|
557
|
+
key: storageKey
|
|
137
558
|
};
|
|
559
|
+
return storageItem;
|
|
138
560
|
}
|
|
139
561
|
export function useStorage(item) {
|
|
140
562
|
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
141
563
|
return [value, item.set];
|
|
142
564
|
}
|
|
565
|
+
export function useStorageSelector(item, selector, isEqual = Object.is) {
|
|
566
|
+
const selectedRef = useRef({
|
|
567
|
+
hasValue: false
|
|
568
|
+
});
|
|
569
|
+
const getSelectedSnapshot = () => {
|
|
570
|
+
const nextSelected = selector(item.get());
|
|
571
|
+
const current = selectedRef.current;
|
|
572
|
+
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
573
|
+
return current.value;
|
|
574
|
+
}
|
|
575
|
+
selectedRef.current = {
|
|
576
|
+
hasValue: true,
|
|
577
|
+
value: nextSelected
|
|
578
|
+
};
|
|
579
|
+
return nextSelected;
|
|
580
|
+
};
|
|
581
|
+
const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
|
|
582
|
+
return [selectedValue, item.set];
|
|
583
|
+
}
|
|
143
584
|
export function useSetStorage(item) {
|
|
144
585
|
return item.set;
|
|
145
586
|
}
|
|
146
587
|
export function getBatch(items, scope) {
|
|
588
|
+
assertBatchScope(items, scope);
|
|
147
589
|
if (scope === StorageScope.Memory) {
|
|
148
590
|
return items.map(item => item.get());
|
|
149
591
|
}
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
592
|
+
const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
|
|
593
|
+
if (!useRawBatchPath) {
|
|
594
|
+
return items.map(item => item.get());
|
|
595
|
+
}
|
|
596
|
+
const useBatchCache = items.every(item => item._readCacheEnabled === true);
|
|
597
|
+
const rawValues = new Array(items.length);
|
|
598
|
+
const keysToFetch = [];
|
|
599
|
+
const keyIndexes = [];
|
|
600
|
+
items.forEach((item, index) => {
|
|
601
|
+
if (scope === StorageScope.Secure) {
|
|
602
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
603
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (useBatchCache) {
|
|
608
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
609
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
keysToFetch.push(item.key);
|
|
614
|
+
keyIndexes.push(index);
|
|
615
|
+
});
|
|
616
|
+
if (keysToFetch.length > 0) {
|
|
617
|
+
const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
|
|
618
|
+
fetchedValues.forEach((value, index) => {
|
|
619
|
+
const key = keysToFetch[index];
|
|
620
|
+
const targetIndex = keyIndexes[index];
|
|
621
|
+
rawValues[targetIndex] = value;
|
|
622
|
+
cacheRawValue(scope, key, value);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
return items.map((item, index) => {
|
|
626
|
+
const raw = rawValues[index];
|
|
154
627
|
if (raw === undefined) {
|
|
155
628
|
return item.get();
|
|
156
629
|
}
|
|
@@ -158,6 +631,7 @@ export function getBatch(items, scope) {
|
|
|
158
631
|
});
|
|
159
632
|
}
|
|
160
633
|
export function setBatch(items, scope) {
|
|
634
|
+
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
161
635
|
if (scope === StorageScope.Memory) {
|
|
162
636
|
items.forEach(({
|
|
163
637
|
item,
|
|
@@ -165,22 +639,137 @@ export function setBatch(items, scope) {
|
|
|
165
639
|
}) => item.set(value));
|
|
166
640
|
return;
|
|
167
641
|
}
|
|
168
|
-
const
|
|
169
|
-
const values = items.map(i => i.item.serialize(i.value));
|
|
170
|
-
getStorageModule().setBatch(keys, values, scope);
|
|
171
|
-
items.forEach(({
|
|
642
|
+
const useRawBatchPath = items.every(({
|
|
172
643
|
item
|
|
173
|
-
}) =>
|
|
174
|
-
|
|
175
|
-
|
|
644
|
+
}) => canUseRawBatchPath(asInternal(item)));
|
|
645
|
+
if (!useRawBatchPath) {
|
|
646
|
+
items.forEach(({
|
|
647
|
+
item,
|
|
648
|
+
value
|
|
649
|
+
}) => item.set(value));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const keys = items.map(entry => entry.item.key);
|
|
653
|
+
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
654
|
+
if (scope === StorageScope.Secure) {
|
|
655
|
+
flushSecureWrites();
|
|
656
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
657
|
+
}
|
|
658
|
+
getStorageModule().setBatch(keys, values, scope);
|
|
659
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
176
660
|
}
|
|
177
661
|
export function removeBatch(items, scope) {
|
|
662
|
+
assertBatchScope(items, scope);
|
|
178
663
|
if (scope === StorageScope.Memory) {
|
|
179
664
|
items.forEach(item => item.delete());
|
|
180
665
|
return;
|
|
181
666
|
}
|
|
182
667
|
const keys = items.map(item => item.key);
|
|
668
|
+
if (scope === StorageScope.Secure) {
|
|
669
|
+
flushSecureWrites();
|
|
670
|
+
}
|
|
183
671
|
getStorageModule().removeBatch(keys, scope);
|
|
184
|
-
|
|
672
|
+
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
673
|
+
}
|
|
674
|
+
export function registerMigration(version, migration) {
|
|
675
|
+
if (!Number.isInteger(version) || version <= 0) {
|
|
676
|
+
throw new Error("Migration version must be a positive integer.");
|
|
677
|
+
}
|
|
678
|
+
if (registeredMigrations.has(version)) {
|
|
679
|
+
throw new Error(`Migration version ${version} is already registered.`);
|
|
680
|
+
}
|
|
681
|
+
registeredMigrations.set(version, migration);
|
|
682
|
+
}
|
|
683
|
+
export function migrateToLatest(scope = StorageScope.Disk) {
|
|
684
|
+
assertValidScope(scope);
|
|
685
|
+
const currentVersion = readMigrationVersion(scope);
|
|
686
|
+
const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
|
|
687
|
+
let appliedVersion = currentVersion;
|
|
688
|
+
const context = {
|
|
689
|
+
scope,
|
|
690
|
+
getRaw: key => getRawValue(key, scope),
|
|
691
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
692
|
+
removeRaw: key => removeRawValue(key, scope)
|
|
693
|
+
};
|
|
694
|
+
versions.forEach(version => {
|
|
695
|
+
const migration = registeredMigrations.get(version);
|
|
696
|
+
if (!migration) {
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
migration(context);
|
|
700
|
+
writeMigrationVersion(scope, version);
|
|
701
|
+
appliedVersion = version;
|
|
702
|
+
});
|
|
703
|
+
return appliedVersion;
|
|
704
|
+
}
|
|
705
|
+
export function runTransaction(scope, transaction) {
|
|
706
|
+
assertValidScope(scope);
|
|
707
|
+
if (scope === StorageScope.Secure) {
|
|
708
|
+
flushSecureWrites();
|
|
709
|
+
}
|
|
710
|
+
const rollback = new Map();
|
|
711
|
+
const rememberRollback = key => {
|
|
712
|
+
if (rollback.has(key)) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
rollback.set(key, getRawValue(key, scope));
|
|
716
|
+
};
|
|
717
|
+
const tx = {
|
|
718
|
+
scope,
|
|
719
|
+
getRaw: key => getRawValue(key, scope),
|
|
720
|
+
setRaw: (key, value) => {
|
|
721
|
+
rememberRollback(key);
|
|
722
|
+
setRawValue(key, value, scope);
|
|
723
|
+
},
|
|
724
|
+
removeRaw: key => {
|
|
725
|
+
rememberRollback(key);
|
|
726
|
+
removeRawValue(key, scope);
|
|
727
|
+
},
|
|
728
|
+
getItem: item => {
|
|
729
|
+
assertBatchScope([item], scope);
|
|
730
|
+
return item.get();
|
|
731
|
+
},
|
|
732
|
+
setItem: (item, value) => {
|
|
733
|
+
assertBatchScope([item], scope);
|
|
734
|
+
rememberRollback(item.key);
|
|
735
|
+
item.set(value);
|
|
736
|
+
},
|
|
737
|
+
removeItem: item => {
|
|
738
|
+
assertBatchScope([item], scope);
|
|
739
|
+
rememberRollback(item.key);
|
|
740
|
+
item.delete();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
try {
|
|
744
|
+
return transaction(tx);
|
|
745
|
+
} catch (error) {
|
|
746
|
+
Array.from(rollback.entries()).reverse().forEach(([key, previousValue]) => {
|
|
747
|
+
if (previousValue === undefined) {
|
|
748
|
+
removeRawValue(key, scope);
|
|
749
|
+
} else {
|
|
750
|
+
setRawValue(key, previousValue, scope);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
export function createSecureAuthStorage(config, options) {
|
|
757
|
+
const ns = options?.namespace ?? "auth";
|
|
758
|
+
const result = {};
|
|
759
|
+
for (const key of Object.keys(config)) {
|
|
760
|
+
const itemConfig = config[key];
|
|
761
|
+
result[key] = createStorageItem({
|
|
762
|
+
key,
|
|
763
|
+
scope: StorageScope.Secure,
|
|
764
|
+
defaultValue: "",
|
|
765
|
+
namespace: ns,
|
|
766
|
+
biometric: itemConfig.biometric,
|
|
767
|
+
accessControl: itemConfig.accessControl,
|
|
768
|
+
expiration: itemConfig.ttlMs ? {
|
|
769
|
+
ttlMs: itemConfig.ttlMs
|
|
770
|
+
} : undefined
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
return result;
|
|
185
774
|
}
|
|
186
775
|
//# sourceMappingURL=index.js.map
|