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.
Files changed (77) hide show
  1. package/CHANGELOG.md +101 -29
  2. package/README.md +83 -26
  3. package/android/build.gradle +3 -1
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
  6. package/cpp/bindings/HybridStorage.cpp +106 -283
  7. package/cpp/bindings/HybridStorage.hpp +16 -9
  8. package/docs/api-reference.md +75 -19
  9. package/docs/benchmarks.md +6 -1
  10. package/docs/keychain-lifecycle-testing.md +36 -0
  11. package/docs/recipes.md +1 -2
  12. package/docs/secure-storage.md +45 -9
  13. package/ios/IOSStorageAdapterCpp.mm +391 -175
  14. package/lib/commonjs/capabilities.js +3 -1
  15. package/lib/commonjs/capabilities.js.map +1 -1
  16. package/lib/commonjs/core/durability.js +111 -45
  17. package/lib/commonjs/core/durability.js.map +1 -1
  18. package/lib/commonjs/index.js +15 -5
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.web.js +219 -97
  21. package/lib/commonjs/index.web.js.map +1 -1
  22. package/lib/commonjs/internal.js +3 -11
  23. package/lib/commonjs/internal.js.map +1 -1
  24. package/lib/commonjs/shared.js +62 -2
  25. package/lib/commonjs/shared.js.map +1 -1
  26. package/lib/commonjs/storage-core.js +883 -163
  27. package/lib/commonjs/storage-core.js.map +1 -1
  28. package/lib/commonjs/storage-runtime.js +5 -1
  29. package/lib/commonjs/storage-runtime.js.map +1 -1
  30. package/lib/commonjs/testing.js +13 -0
  31. package/lib/commonjs/testing.js.map +1 -1
  32. package/lib/module/capabilities.js +2 -1
  33. package/lib/module/capabilities.js.map +1 -1
  34. package/lib/module/core/durability.js +111 -45
  35. package/lib/module/core/durability.js.map +1 -1
  36. package/lib/module/index.js +12 -8
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/index.web.js +215 -99
  39. package/lib/module/index.web.js.map +1 -1
  40. package/lib/module/internal.js +2 -9
  41. package/lib/module/internal.js.map +1 -1
  42. package/lib/module/shared.js +60 -2
  43. package/lib/module/shared.js.map +1 -1
  44. package/lib/module/storage-core.js +884 -164
  45. package/lib/module/storage-core.js.map +1 -1
  46. package/lib/module/storage-runtime.js +4 -1
  47. package/lib/module/storage-runtime.js.map +1 -1
  48. package/lib/module/testing.js +8 -1
  49. package/lib/module/testing.js.map +1 -1
  50. package/lib/typescript/capabilities.d.ts +2 -1
  51. package/lib/typescript/capabilities.d.ts.map +1 -1
  52. package/lib/typescript/core/durability.d.ts +8 -4
  53. package/lib/typescript/core/durability.d.ts.map +1 -1
  54. package/lib/typescript/index.d.ts +2 -2
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/index.web.d.ts +3 -29
  57. package/lib/typescript/index.web.d.ts.map +1 -1
  58. package/lib/typescript/internal.d.ts +0 -2
  59. package/lib/typescript/internal.d.ts.map +1 -1
  60. package/lib/typescript/shared.d.ts +19 -0
  61. package/lib/typescript/shared.d.ts.map +1 -1
  62. package/lib/typescript/storage-core.d.ts +15 -6
  63. package/lib/typescript/storage-core.d.ts.map +1 -1
  64. package/lib/typescript/storage-runtime.d.ts +2 -1
  65. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  66. package/lib/typescript/testing.d.ts +1 -1
  67. package/lib/typescript/testing.d.ts.map +1 -1
  68. package/package.json +2 -2
  69. package/src/capabilities.ts +3 -1
  70. package/src/core/durability.ts +140 -43
  71. package/src/index.ts +20 -10
  72. package/src/index.web.ts +328 -166
  73. package/src/internal.ts +0 -14
  74. package/src/shared.ts +85 -0
  75. package/src/storage-core.ts +1198 -232
  76. package/src/storage-runtime.ts +6 -0
  77. package/src/testing.ts +8 -1
@@ -13,31 +13,60 @@ export type DurabilityCoordinator = {
13
13
  isDiskWritesAsync(): boolean;
14
14
  hasPendingDiskWrite(key: string): boolean;
15
15
  hasPendingSecureWrite(key: string): boolean;
16
- readPendingDiskWrite(key: string): string | undefined;
17
- readPendingSecureWrite(key: string): string | undefined;
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): void;
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
- ): void;
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 keysToSet: string[] = [];
53
- const valuesToSet: string[] = [];
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
- writes.forEach(({ key, value }) => {
57
- if (value === undefined) {
58
- keysToRemove.push(key);
59
- return;
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
- if (keysToRemove.length > 0) {
70
- options.backend.removeBatch(keysToRemove, StorageScope.Disk);
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
- { keys: string[]; values: string[] }
122
+ { writes: PendingSecureWrite[] }
87
123
  >();
88
- const keysToRemove: string[] = [];
124
+ const removeWrites: PendingSecureWrite[] = [];
89
125
 
90
- writes.forEach(({ key, value, accessControl }) => {
126
+ writes.forEach((write) => {
127
+ const { value, accessControl } = write;
91
128
  if (value === undefined) {
92
- keysToRemove.push(key);
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 ?? { keys: [], values: [] };
98
- group.keys.push(key);
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
- groupedSetWrites.forEach((group, accessControl) => {
107
- options.backend.setSecureAccessControl(accessControl);
108
- options.backend.setBatch(group.keys, group.values, StorageScope.Secure);
109
- });
110
- if (keysToRemove.length > 0) {
111
- options.backend.removeBatch(keysToRemove, StorageScope.Secure);
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(key: string, value: string | undefined): void {
116
- pendingDiskWrites.set(key, { key, value });
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
- ): void {
129
- const pendingWrite: PendingSecureWrite = { key, value };
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
- readPendingDiskWrite: (key) => pendingDiskWrites.get(key)?.value,
152
- readPendingSecureWrite: (key) => pendingSecureWrites.get(key)?.value,
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 { resolveNativeWriteBuffering } from "./capabilities";
4
- import { decodeNativeBatchValue, unescapeCollidingRawValue } from "./internal";
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 = internals.readCachedRawValue(scope, key);
200
- internals.cacheRawValue(scope, key, value);
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 = false;
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
  },