react-native-nitro-storage 0.9.0 → 0.10.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/CHANGELOG.md +101 -29
- package/README.md +83 -26
- package/android/build.gradle +3 -1
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
- package/cpp/bindings/HybridStorage.cpp +106 -283
- package/cpp/bindings/HybridStorage.hpp +16 -9
- package/docs/api-reference.md +75 -19
- package/docs/benchmarks.md +6 -1
- package/docs/keychain-lifecycle-testing.md +36 -0
- package/docs/recipes.md +1 -2
- package/docs/secure-storage.md +45 -9
- package/ios/IOSStorageAdapterCpp.mm +391 -175
- package/lib/commonjs/capabilities.js +3 -1
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/core/durability.js +111 -45
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +15 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +219 -97
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +3 -11
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +62 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +883 -163
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +5 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +13 -0
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/capabilities.js +2 -1
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/core/durability.js +111 -45
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +12 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +215 -99
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +2 -9
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +60 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +884 -164
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +4 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +8 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/capabilities.d.ts +2 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +8 -4
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +3 -29
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +0 -2
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +19 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +15 -6
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +2 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +140 -43
- package/src/index.ts +20 -10
- package/src/index.web.ts +328 -166
- package/src/internal.ts +0 -14
- package/src/shared.ts +85 -0
- package/src/storage-core.ts +1198 -232
- package/src/storage-runtime.ts +6 -0
- package/src/testing.ts +8 -1
package/src/core/durability.ts
CHANGED
|
@@ -13,31 +13,60 @@ export type DurabilityCoordinator = {
|
|
|
13
13
|
isDiskWritesAsync(): boolean;
|
|
14
14
|
hasPendingDiskWrite(key: string): boolean;
|
|
15
15
|
hasPendingSecureWrite(key: string): boolean;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
getPendingDiskWrite(key: string): PendingDiskWrite | undefined;
|
|
17
|
+
getPendingSecureWrite(key: string): PendingSecureWrite | undefined;
|
|
18
18
|
clearPendingDiskWrite(key: string): void;
|
|
19
19
|
clearPendingSecureWrite(key: string): void;
|
|
20
|
+
clearPendingDiskWriteIf(write: PendingDiskWrite): void;
|
|
21
|
+
clearPendingSecureWriteIf(write: PendingSecureWrite): void;
|
|
20
22
|
clearAllPendingDiskWrites(): void;
|
|
21
23
|
clearAllPendingSecureWrites(): void;
|
|
22
|
-
scheduleDiskWrite(key: string, value: string | undefined):
|
|
24
|
+
scheduleDiskWrite(key: string, value: string | undefined): PendingDiskWrite;
|
|
23
25
|
scheduleSecureWrite(
|
|
24
26
|
key: string,
|
|
25
27
|
value: string | undefined,
|
|
26
28
|
accessControl?: AccessControl,
|
|
27
|
-
):
|
|
29
|
+
): PendingSecureWrite;
|
|
28
30
|
flushDiskWrites(): void;
|
|
29
31
|
flushSecureWrites(): void;
|
|
32
|
+
runSecurePromotion<T>(key: string, promotion: () => T): T;
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
export function createDurabilityCoordinator(options: {
|
|
33
36
|
backend: DurabilityBackend;
|
|
34
37
|
resolveSecureDefaultAccessControl(): AccessControl;
|
|
35
38
|
}): DurabilityCoordinator {
|
|
39
|
+
type PendingWrite = PendingDiskWrite | PendingSecureWrite;
|
|
40
|
+
|
|
36
41
|
const pendingDiskWrites = new Map<string, PendingDiskWrite>();
|
|
42
|
+
let nextGeneration = 0;
|
|
37
43
|
let diskFlushScheduled = false;
|
|
38
44
|
let diskWritesAsync = false;
|
|
39
45
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
40
46
|
let secureFlushScheduled = false;
|
|
47
|
+
const securePromotionsInProgress = new Set<string>();
|
|
48
|
+
|
|
49
|
+
function clearPendingWrites<T extends PendingWrite>(
|
|
50
|
+
pendingWrites: Map<string, T>,
|
|
51
|
+
writes: readonly T[],
|
|
52
|
+
): void {
|
|
53
|
+
writes.forEach((write) => {
|
|
54
|
+
if (pendingWrites.get(write.key) === write) {
|
|
55
|
+
pendingWrites.delete(write.key);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function restorePendingWrites<T extends PendingWrite>(
|
|
61
|
+
pendingWrites: Map<string, T>,
|
|
62
|
+
writes: readonly T[],
|
|
63
|
+
): void {
|
|
64
|
+
writes.forEach((write) => {
|
|
65
|
+
if (!pendingWrites.has(write.key)) {
|
|
66
|
+
pendingWrites.set(write.key, write);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
41
70
|
|
|
42
71
|
function flushDiskWrites(): void {
|
|
43
72
|
diskFlushScheduled = false;
|
|
@@ -47,27 +76,35 @@ export function createDurabilityCoordinator(options: {
|
|
|
47
76
|
}
|
|
48
77
|
|
|
49
78
|
const writes = Array.from(pendingDiskWrites.values());
|
|
50
|
-
pendingDiskWrites.clear();
|
|
51
79
|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const keysToRemove: string[] = [];
|
|
80
|
+
const setWrites = writes.filter((write) => write.value !== undefined);
|
|
81
|
+
const removeWrites = writes.filter((write) => write.value === undefined);
|
|
55
82
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
83
|
+
if (setWrites.length > 0) {
|
|
84
|
+
try {
|
|
85
|
+
options.backend.setBatch(
|
|
86
|
+
setWrites.map(({ key }) => key),
|
|
87
|
+
setWrites.map(({ value }) => value as string),
|
|
88
|
+
StorageScope.Disk,
|
|
89
|
+
);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
restorePendingWrites(pendingDiskWrites, setWrites);
|
|
92
|
+
throw error;
|
|
60
93
|
}
|
|
61
|
-
|
|
62
|
-
keysToSet.push(key);
|
|
63
|
-
valuesToSet.push(value);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
if (keysToSet.length > 0) {
|
|
67
|
-
options.backend.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
94
|
+
clearPendingWrites(pendingDiskWrites, setWrites);
|
|
68
95
|
}
|
|
69
|
-
|
|
70
|
-
|
|
96
|
+
|
|
97
|
+
if (removeWrites.length > 0) {
|
|
98
|
+
try {
|
|
99
|
+
options.backend.removeBatch(
|
|
100
|
+
removeWrites.map(({ key }) => key),
|
|
101
|
+
StorageScope.Disk,
|
|
102
|
+
);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
restorePendingWrites(pendingDiskWrites, removeWrites);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
clearPendingWrites(pendingDiskWrites, removeWrites);
|
|
71
108
|
}
|
|
72
109
|
}
|
|
73
110
|
|
|
@@ -79,63 +116,112 @@ export function createDurabilityCoordinator(options: {
|
|
|
79
116
|
}
|
|
80
117
|
|
|
81
118
|
const writes = Array.from(pendingSecureWrites.values());
|
|
82
|
-
pendingSecureWrites.clear();
|
|
83
119
|
|
|
84
120
|
const groupedSetWrites = new Map<
|
|
85
121
|
AccessControl,
|
|
86
|
-
{
|
|
122
|
+
{ writes: PendingSecureWrite[] }
|
|
87
123
|
>();
|
|
88
|
-
const
|
|
124
|
+
const removeWrites: PendingSecureWrite[] = [];
|
|
89
125
|
|
|
90
|
-
writes.forEach((
|
|
126
|
+
writes.forEach((write) => {
|
|
127
|
+
const { value, accessControl } = write;
|
|
91
128
|
if (value === undefined) {
|
|
92
|
-
|
|
129
|
+
removeWrites.push(write);
|
|
93
130
|
} else {
|
|
94
131
|
const resolvedAccessControl =
|
|
95
132
|
accessControl ?? options.resolveSecureDefaultAccessControl();
|
|
96
133
|
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
97
|
-
const group = existingGroup ?? {
|
|
98
|
-
group.
|
|
99
|
-
group.values.push(value);
|
|
134
|
+
const group = existingGroup ?? { writes: [] };
|
|
135
|
+
group.writes.push(write);
|
|
100
136
|
if (!existingGroup) {
|
|
101
137
|
groupedSetWrites.set(resolvedAccessControl, group);
|
|
102
138
|
}
|
|
103
139
|
}
|
|
104
140
|
});
|
|
105
141
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
142
|
+
for (const [accessControl, group] of groupedSetWrites) {
|
|
143
|
+
try {
|
|
144
|
+
options.backend.setSecureAccessControl(accessControl);
|
|
145
|
+
options.backend.setBatch(
|
|
146
|
+
group.writes.map(({ key }) => key),
|
|
147
|
+
group.writes.map(({ value }) => value as string),
|
|
148
|
+
StorageScope.Secure,
|
|
149
|
+
);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
restorePendingWrites(pendingSecureWrites, group.writes);
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
clearPendingWrites(pendingSecureWrites, group.writes);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (removeWrites.length > 0) {
|
|
158
|
+
try {
|
|
159
|
+
options.backend.removeBatch(
|
|
160
|
+
removeWrites.map(({ key }) => key),
|
|
161
|
+
StorageScope.Secure,
|
|
162
|
+
);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
restorePendingWrites(pendingSecureWrites, removeWrites);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
clearPendingWrites(pendingSecureWrites, removeWrites);
|
|
112
168
|
}
|
|
113
169
|
}
|
|
114
170
|
|
|
115
|
-
function scheduleDiskWrite(
|
|
116
|
-
|
|
171
|
+
function scheduleDiskWrite(
|
|
172
|
+
key: string,
|
|
173
|
+
value: string | undefined,
|
|
174
|
+
): PendingDiskWrite {
|
|
175
|
+
const pendingWrite: PendingDiskWrite = {
|
|
176
|
+
key,
|
|
177
|
+
value,
|
|
178
|
+
generation: ++nextGeneration,
|
|
179
|
+
};
|
|
180
|
+
pendingDiskWrites.set(key, pendingWrite);
|
|
117
181
|
if (diskFlushScheduled) {
|
|
118
|
-
return;
|
|
182
|
+
return pendingWrite;
|
|
119
183
|
}
|
|
120
184
|
diskFlushScheduled = true;
|
|
121
185
|
runMicrotask(flushDiskWrites);
|
|
186
|
+
return pendingWrite;
|
|
122
187
|
}
|
|
123
188
|
|
|
124
189
|
function scheduleSecureWrite(
|
|
125
190
|
key: string,
|
|
126
191
|
value: string | undefined,
|
|
127
192
|
accessControl?: AccessControl,
|
|
128
|
-
):
|
|
129
|
-
const pendingWrite: PendingSecureWrite = {
|
|
193
|
+
): PendingSecureWrite {
|
|
194
|
+
const pendingWrite: PendingSecureWrite = {
|
|
195
|
+
key,
|
|
196
|
+
value,
|
|
197
|
+
generation: ++nextGeneration,
|
|
198
|
+
};
|
|
130
199
|
if (accessControl !== undefined) {
|
|
131
200
|
pendingWrite.accessControl = accessControl;
|
|
132
201
|
}
|
|
133
202
|
pendingSecureWrites.set(key, pendingWrite);
|
|
134
203
|
if (secureFlushScheduled) {
|
|
135
|
-
return;
|
|
204
|
+
return pendingWrite;
|
|
136
205
|
}
|
|
137
206
|
secureFlushScheduled = true;
|
|
138
207
|
runMicrotask(flushSecureWrites);
|
|
208
|
+
return pendingWrite;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function runSecurePromotion<T>(key: string, promotion: () => T): T {
|
|
212
|
+
if (securePromotionsInProgress.has(key)) {
|
|
213
|
+
throw new Error("NitroStorage: Reentrant secure promotion");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
securePromotionsInProgress.add(key);
|
|
217
|
+
try {
|
|
218
|
+
if (pendingSecureWrites.has(key)) {
|
|
219
|
+
flushSecureWrites();
|
|
220
|
+
}
|
|
221
|
+
return promotion();
|
|
222
|
+
} finally {
|
|
223
|
+
securePromotionsInProgress.delete(key);
|
|
224
|
+
}
|
|
139
225
|
}
|
|
140
226
|
|
|
141
227
|
return {
|
|
@@ -148,14 +234,24 @@ export function createDurabilityCoordinator(options: {
|
|
|
148
234
|
isDiskWritesAsync: () => diskWritesAsync,
|
|
149
235
|
hasPendingDiskWrite: (key) => pendingDiskWrites.has(key),
|
|
150
236
|
hasPendingSecureWrite: (key) => pendingSecureWrites.has(key),
|
|
151
|
-
|
|
152
|
-
|
|
237
|
+
getPendingDiskWrite: (key) => pendingDiskWrites.get(key),
|
|
238
|
+
getPendingSecureWrite: (key) => pendingSecureWrites.get(key),
|
|
153
239
|
clearPendingDiskWrite: (key) => {
|
|
154
240
|
pendingDiskWrites.delete(key);
|
|
155
241
|
},
|
|
156
242
|
clearPendingSecureWrite: (key) => {
|
|
157
243
|
pendingSecureWrites.delete(key);
|
|
158
244
|
},
|
|
245
|
+
clearPendingDiskWriteIf: (write) => {
|
|
246
|
+
if (pendingDiskWrites.get(write.key) === write) {
|
|
247
|
+
pendingDiskWrites.delete(write.key);
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
clearPendingSecureWriteIf: (write) => {
|
|
251
|
+
if (pendingSecureWrites.get(write.key) === write) {
|
|
252
|
+
pendingSecureWrites.delete(write.key);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
159
255
|
clearAllPendingDiskWrites: () => {
|
|
160
256
|
pendingDiskWrites.clear();
|
|
161
257
|
},
|
|
@@ -166,5 +262,6 @@ export function createDurabilityCoordinator(options: {
|
|
|
166
262
|
scheduleSecureWrite,
|
|
167
263
|
flushDiskWrites,
|
|
168
264
|
flushSecureWrites,
|
|
265
|
+
runSecurePromotion,
|
|
169
266
|
};
|
|
170
267
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { Platform } from "react-native";
|
|
2
2
|
import { NitroModules } from "react-native-nitro-modules";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
DEFAULT_SECURE_WRITES_ASYNC,
|
|
5
|
+
resolveNativeWriteBuffering,
|
|
6
|
+
} from "./capabilities";
|
|
7
|
+
import { unescapeCollidingRawValue } from "./internal";
|
|
5
8
|
import {
|
|
6
9
|
assertAccessControlLevel,
|
|
7
10
|
notifyAllListeners,
|
|
@@ -37,6 +40,8 @@ export type {
|
|
|
37
40
|
StorageMetricsObserver,
|
|
38
41
|
StorageSelectorListener,
|
|
39
42
|
StorageSelectorSubscribeOptions,
|
|
43
|
+
StorageCompositeError,
|
|
44
|
+
StorageCompensationError,
|
|
40
45
|
StorageVersion,
|
|
41
46
|
Validator,
|
|
42
47
|
VersionedValue,
|
|
@@ -48,6 +53,7 @@ export type { Storage } from "./Storage.nitro";
|
|
|
48
53
|
export { migrateFromMMKV } from "./migration";
|
|
49
54
|
export {
|
|
50
55
|
getStorageErrorCode,
|
|
56
|
+
isStorageError,
|
|
51
57
|
type SecureStorageMetadata,
|
|
52
58
|
type SecurityCapabilities,
|
|
53
59
|
type StorageCapabilities,
|
|
@@ -111,10 +117,7 @@ const nativeBackend: StorageCoreBackend = {
|
|
|
111
117
|
setBatch: (keys, values, scope) => {
|
|
112
118
|
getStorageModule().setBatch(keys, values, scope);
|
|
113
119
|
},
|
|
114
|
-
getBatch: (keys, scope) =>
|
|
115
|
-
(getStorageModule().getBatch(keys, scope) ?? []).map((value) =>
|
|
116
|
-
decodeNativeBatchValue(value),
|
|
117
|
-
),
|
|
120
|
+
getBatch: (keys, scope) => getStorageModule().getBatch(keys, scope) ?? [],
|
|
118
121
|
removeBatch: (keys, scope) => {
|
|
119
122
|
getStorageModule().removeBatch(keys, scope);
|
|
120
123
|
},
|
|
@@ -196,8 +199,15 @@ function buildNativeAdapter(
|
|
|
196
199
|
return;
|
|
197
200
|
}
|
|
198
201
|
|
|
199
|
-
const oldValue =
|
|
200
|
-
|
|
202
|
+
const oldValue =
|
|
203
|
+
scope === StorageScope.Secure
|
|
204
|
+
? undefined
|
|
205
|
+
: internals.readCachedRawValue(scope, key);
|
|
206
|
+
if (scope === StorageScope.Secure) {
|
|
207
|
+
internals.invalidateRawCache(scope, key);
|
|
208
|
+
} else {
|
|
209
|
+
internals.cacheRawValue(scope, key, value);
|
|
210
|
+
}
|
|
201
211
|
notifyKeyListeners(internals.getScopedListeners(scope), key);
|
|
202
212
|
if (consumeSuppressedNativeEvent(scope, key)) {
|
|
203
213
|
return;
|
|
@@ -267,7 +277,7 @@ function buildNativeAdapter(
|
|
|
267
277
|
const core = createStorageCore(buildNativeAdapter);
|
|
268
278
|
const { internals } = core;
|
|
269
279
|
|
|
270
|
-
let secureWritesBuffered =
|
|
280
|
+
let secureWritesBuffered = DEFAULT_SECURE_WRITES_ASYNC;
|
|
271
281
|
|
|
272
282
|
export const storage = {
|
|
273
283
|
...core.storage,
|
|
@@ -287,8 +297,8 @@ export const storage = {
|
|
|
287
297
|
"storage:setSecureWritesAsync",
|
|
288
298
|
StorageScope.Secure,
|
|
289
299
|
() => {
|
|
290
|
-
secureWritesBuffered = enabled;
|
|
291
300
|
getStorageModule().setSecureWritesAsync(enabled);
|
|
301
|
+
secureWritesBuffered = enabled;
|
|
292
302
|
},
|
|
293
303
|
);
|
|
294
304
|
},
|