react-native-nitro-storage 0.8.0 → 0.10.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/CHANGELOG.md +107 -25
- package/README.md +140 -27
- 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 +109 -293
- package/cpp/bindings/HybridStorage.hpp +17 -10
- 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 +110 -43
- 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 +859 -145
- 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 +110 -43
- 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 +860 -146
- 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 +7 -2
- 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 +12 -3
- 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/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
- package/package.json +3 -3
- package/react-native-nitro-storage.podspec +3 -1
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +139 -39
- 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 +1165 -199
- package/src/storage-runtime.ts +6 -0
- package/src/testing.ts +8 -1
package/src/core/durability.ts
CHANGED
|
@@ -15,29 +15,59 @@ export type DurabilityCoordinator = {
|
|
|
15
15
|
hasPendingSecureWrite(key: string): boolean;
|
|
16
16
|
readPendingDiskWrite(key: string): string | undefined;
|
|
17
17
|
readPendingSecureWrite(key: string): string | undefined;
|
|
18
|
+
readPendingSecureAccessControl(key: string): AccessControl | undefined;
|
|
18
19
|
clearPendingDiskWrite(key: string): void;
|
|
19
20
|
clearPendingSecureWrite(key: string): void;
|
|
21
|
+
clearPendingDiskWriteIf(write: PendingDiskWrite): void;
|
|
22
|
+
clearPendingSecureWriteIf(write: PendingSecureWrite): void;
|
|
20
23
|
clearAllPendingDiskWrites(): void;
|
|
21
24
|
clearAllPendingSecureWrites(): void;
|
|
22
|
-
scheduleDiskWrite(key: string, value: string | undefined):
|
|
25
|
+
scheduleDiskWrite(key: string, value: string | undefined): PendingDiskWrite;
|
|
23
26
|
scheduleSecureWrite(
|
|
24
27
|
key: string,
|
|
25
28
|
value: string | undefined,
|
|
26
29
|
accessControl?: AccessControl,
|
|
27
|
-
):
|
|
30
|
+
): PendingSecureWrite;
|
|
28
31
|
flushDiskWrites(): void;
|
|
29
32
|
flushSecureWrites(): void;
|
|
33
|
+
runSecurePromotion<T>(key: string, promotion: () => T): T;
|
|
30
34
|
};
|
|
31
35
|
|
|
32
36
|
export function createDurabilityCoordinator(options: {
|
|
33
37
|
backend: DurabilityBackend;
|
|
34
38
|
resolveSecureDefaultAccessControl(): AccessControl;
|
|
35
39
|
}): DurabilityCoordinator {
|
|
40
|
+
type PendingWrite = PendingDiskWrite | PendingSecureWrite;
|
|
41
|
+
|
|
36
42
|
const pendingDiskWrites = new Map<string, PendingDiskWrite>();
|
|
43
|
+
let nextGeneration = 0;
|
|
37
44
|
let diskFlushScheduled = false;
|
|
38
45
|
let diskWritesAsync = false;
|
|
39
46
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
40
47
|
let secureFlushScheduled = false;
|
|
48
|
+
const securePromotionsInProgress = new Set<string>();
|
|
49
|
+
|
|
50
|
+
function clearPendingWrites<T extends PendingWrite>(
|
|
51
|
+
pendingWrites: Map<string, T>,
|
|
52
|
+
writes: readonly T[],
|
|
53
|
+
): void {
|
|
54
|
+
writes.forEach((write) => {
|
|
55
|
+
if (pendingWrites.get(write.key) === write) {
|
|
56
|
+
pendingWrites.delete(write.key);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function restorePendingWrites<T extends PendingWrite>(
|
|
62
|
+
pendingWrites: Map<string, T>,
|
|
63
|
+
writes: readonly T[],
|
|
64
|
+
): void {
|
|
65
|
+
writes.forEach((write) => {
|
|
66
|
+
if (!pendingWrites.has(write.key)) {
|
|
67
|
+
pendingWrites.set(write.key, write);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
41
71
|
|
|
42
72
|
function flushDiskWrites(): void {
|
|
43
73
|
diskFlushScheduled = false;
|
|
@@ -47,27 +77,35 @@ export function createDurabilityCoordinator(options: {
|
|
|
47
77
|
}
|
|
48
78
|
|
|
49
79
|
const writes = Array.from(pendingDiskWrites.values());
|
|
50
|
-
pendingDiskWrites.clear();
|
|
51
80
|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const keysToRemove: string[] = [];
|
|
81
|
+
const setWrites = writes.filter((write) => write.value !== undefined);
|
|
82
|
+
const removeWrites = writes.filter((write) => write.value === undefined);
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
84
|
+
if (setWrites.length > 0) {
|
|
85
|
+
try {
|
|
86
|
+
options.backend.setBatch(
|
|
87
|
+
setWrites.map(({ key }) => key),
|
|
88
|
+
setWrites.map(({ value }) => value as string),
|
|
89
|
+
StorageScope.Disk,
|
|
90
|
+
);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
restorePendingWrites(pendingDiskWrites, setWrites);
|
|
93
|
+
throw error;
|
|
60
94
|
}
|
|
61
|
-
|
|
62
|
-
keysToSet.push(key);
|
|
63
|
-
valuesToSet.push(value);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
if (keysToSet.length > 0) {
|
|
67
|
-
options.backend.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
|
|
95
|
+
clearPendingWrites(pendingDiskWrites, setWrites);
|
|
68
96
|
}
|
|
69
|
-
|
|
70
|
-
|
|
97
|
+
|
|
98
|
+
if (removeWrites.length > 0) {
|
|
99
|
+
try {
|
|
100
|
+
options.backend.removeBatch(
|
|
101
|
+
removeWrites.map(({ key }) => key),
|
|
102
|
+
StorageScope.Disk,
|
|
103
|
+
);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
restorePendingWrites(pendingDiskWrites, removeWrites);
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
clearPendingWrites(pendingDiskWrites, removeWrites);
|
|
71
109
|
}
|
|
72
110
|
}
|
|
73
111
|
|
|
@@ -79,63 +117,112 @@ export function createDurabilityCoordinator(options: {
|
|
|
79
117
|
}
|
|
80
118
|
|
|
81
119
|
const writes = Array.from(pendingSecureWrites.values());
|
|
82
|
-
pendingSecureWrites.clear();
|
|
83
120
|
|
|
84
121
|
const groupedSetWrites = new Map<
|
|
85
122
|
AccessControl,
|
|
86
|
-
{
|
|
123
|
+
{ writes: PendingSecureWrite[] }
|
|
87
124
|
>();
|
|
88
|
-
const
|
|
125
|
+
const removeWrites: PendingSecureWrite[] = [];
|
|
89
126
|
|
|
90
|
-
writes.forEach((
|
|
127
|
+
writes.forEach((write) => {
|
|
128
|
+
const { value, accessControl } = write;
|
|
91
129
|
if (value === undefined) {
|
|
92
|
-
|
|
130
|
+
removeWrites.push(write);
|
|
93
131
|
} else {
|
|
94
132
|
const resolvedAccessControl =
|
|
95
133
|
accessControl ?? options.resolveSecureDefaultAccessControl();
|
|
96
134
|
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
97
|
-
const group = existingGroup ?? {
|
|
98
|
-
group.
|
|
99
|
-
group.values.push(value);
|
|
135
|
+
const group = existingGroup ?? { writes: [] };
|
|
136
|
+
group.writes.push(write);
|
|
100
137
|
if (!existingGroup) {
|
|
101
138
|
groupedSetWrites.set(resolvedAccessControl, group);
|
|
102
139
|
}
|
|
103
140
|
}
|
|
104
141
|
});
|
|
105
142
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
143
|
+
for (const [accessControl, group] of groupedSetWrites) {
|
|
144
|
+
try {
|
|
145
|
+
options.backend.setSecureAccessControl(accessControl);
|
|
146
|
+
options.backend.setBatch(
|
|
147
|
+
group.writes.map(({ key }) => key),
|
|
148
|
+
group.writes.map(({ value }) => value as string),
|
|
149
|
+
StorageScope.Secure,
|
|
150
|
+
);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
restorePendingWrites(pendingSecureWrites, group.writes);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
clearPendingWrites(pendingSecureWrites, group.writes);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (removeWrites.length > 0) {
|
|
159
|
+
try {
|
|
160
|
+
options.backend.removeBatch(
|
|
161
|
+
removeWrites.map(({ key }) => key),
|
|
162
|
+
StorageScope.Secure,
|
|
163
|
+
);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
restorePendingWrites(pendingSecureWrites, removeWrites);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
clearPendingWrites(pendingSecureWrites, removeWrites);
|
|
112
169
|
}
|
|
113
170
|
}
|
|
114
171
|
|
|
115
|
-
function scheduleDiskWrite(
|
|
116
|
-
|
|
172
|
+
function scheduleDiskWrite(
|
|
173
|
+
key: string,
|
|
174
|
+
value: string | undefined,
|
|
175
|
+
): PendingDiskWrite {
|
|
176
|
+
const pendingWrite: PendingDiskWrite = {
|
|
177
|
+
key,
|
|
178
|
+
value,
|
|
179
|
+
generation: ++nextGeneration,
|
|
180
|
+
};
|
|
181
|
+
pendingDiskWrites.set(key, pendingWrite);
|
|
117
182
|
if (diskFlushScheduled) {
|
|
118
|
-
return;
|
|
183
|
+
return pendingWrite;
|
|
119
184
|
}
|
|
120
185
|
diskFlushScheduled = true;
|
|
121
186
|
runMicrotask(flushDiskWrites);
|
|
187
|
+
return pendingWrite;
|
|
122
188
|
}
|
|
123
189
|
|
|
124
190
|
function scheduleSecureWrite(
|
|
125
191
|
key: string,
|
|
126
192
|
value: string | undefined,
|
|
127
193
|
accessControl?: AccessControl,
|
|
128
|
-
):
|
|
129
|
-
const pendingWrite: PendingSecureWrite = {
|
|
194
|
+
): PendingSecureWrite {
|
|
195
|
+
const pendingWrite: PendingSecureWrite = {
|
|
196
|
+
key,
|
|
197
|
+
value,
|
|
198
|
+
generation: ++nextGeneration,
|
|
199
|
+
};
|
|
130
200
|
if (accessControl !== undefined) {
|
|
131
201
|
pendingWrite.accessControl = accessControl;
|
|
132
202
|
}
|
|
133
203
|
pendingSecureWrites.set(key, pendingWrite);
|
|
134
204
|
if (secureFlushScheduled) {
|
|
135
|
-
return;
|
|
205
|
+
return pendingWrite;
|
|
136
206
|
}
|
|
137
207
|
secureFlushScheduled = true;
|
|
138
208
|
runMicrotask(flushSecureWrites);
|
|
209
|
+
return pendingWrite;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function runSecurePromotion<T>(key: string, promotion: () => T): T {
|
|
213
|
+
if (securePromotionsInProgress.has(key)) {
|
|
214
|
+
throw new Error("NitroStorage: Reentrant secure promotion");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
securePromotionsInProgress.add(key);
|
|
218
|
+
try {
|
|
219
|
+
if (pendingSecureWrites.has(key)) {
|
|
220
|
+
flushSecureWrites();
|
|
221
|
+
}
|
|
222
|
+
return promotion();
|
|
223
|
+
} finally {
|
|
224
|
+
securePromotionsInProgress.delete(key);
|
|
225
|
+
}
|
|
139
226
|
}
|
|
140
227
|
|
|
141
228
|
return {
|
|
@@ -150,12 +237,24 @@ export function createDurabilityCoordinator(options: {
|
|
|
150
237
|
hasPendingSecureWrite: (key) => pendingSecureWrites.has(key),
|
|
151
238
|
readPendingDiskWrite: (key) => pendingDiskWrites.get(key)?.value,
|
|
152
239
|
readPendingSecureWrite: (key) => pendingSecureWrites.get(key)?.value,
|
|
240
|
+
readPendingSecureAccessControl: (key) =>
|
|
241
|
+
pendingSecureWrites.get(key)?.accessControl,
|
|
153
242
|
clearPendingDiskWrite: (key) => {
|
|
154
243
|
pendingDiskWrites.delete(key);
|
|
155
244
|
},
|
|
156
245
|
clearPendingSecureWrite: (key) => {
|
|
157
246
|
pendingSecureWrites.delete(key);
|
|
158
247
|
},
|
|
248
|
+
clearPendingDiskWriteIf: (write) => {
|
|
249
|
+
if (pendingDiskWrites.get(write.key) === write) {
|
|
250
|
+
pendingDiskWrites.delete(write.key);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
clearPendingSecureWriteIf: (write) => {
|
|
254
|
+
if (pendingSecureWrites.get(write.key) === write) {
|
|
255
|
+
pendingSecureWrites.delete(write.key);
|
|
256
|
+
}
|
|
257
|
+
},
|
|
159
258
|
clearAllPendingDiskWrites: () => {
|
|
160
259
|
pendingDiskWrites.clear();
|
|
161
260
|
},
|
|
@@ -166,5 +265,6 @@ export function createDurabilityCoordinator(options: {
|
|
|
166
265
|
scheduleSecureWrite,
|
|
167
266
|
flushDiskWrites,
|
|
168
267
|
flushSecureWrites,
|
|
268
|
+
runSecurePromotion,
|
|
169
269
|
};
|
|
170
270
|
}
|
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
|
},
|