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.
Files changed (78) hide show
  1. package/CHANGELOG.md +107 -25
  2. package/README.md +140 -27
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
  5. package/cpp/bindings/HybridStorage.cpp +109 -293
  6. package/cpp/bindings/HybridStorage.hpp +17 -10
  7. package/docs/api-reference.md +75 -19
  8. package/docs/benchmarks.md +6 -1
  9. package/docs/keychain-lifecycle-testing.md +36 -0
  10. package/docs/recipes.md +1 -2
  11. package/docs/secure-storage.md +45 -9
  12. package/ios/IOSStorageAdapterCpp.mm +391 -175
  13. package/lib/commonjs/capabilities.js +3 -1
  14. package/lib/commonjs/capabilities.js.map +1 -1
  15. package/lib/commonjs/core/durability.js +110 -43
  16. package/lib/commonjs/core/durability.js.map +1 -1
  17. package/lib/commonjs/index.js +15 -5
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/index.web.js +219 -97
  20. package/lib/commonjs/index.web.js.map +1 -1
  21. package/lib/commonjs/internal.js +3 -11
  22. package/lib/commonjs/internal.js.map +1 -1
  23. package/lib/commonjs/shared.js +62 -2
  24. package/lib/commonjs/shared.js.map +1 -1
  25. package/lib/commonjs/storage-core.js +859 -145
  26. package/lib/commonjs/storage-core.js.map +1 -1
  27. package/lib/commonjs/storage-runtime.js +5 -1
  28. package/lib/commonjs/storage-runtime.js.map +1 -1
  29. package/lib/commonjs/testing.js +13 -0
  30. package/lib/commonjs/testing.js.map +1 -1
  31. package/lib/module/capabilities.js +2 -1
  32. package/lib/module/capabilities.js.map +1 -1
  33. package/lib/module/core/durability.js +110 -43
  34. package/lib/module/core/durability.js.map +1 -1
  35. package/lib/module/index.js +12 -8
  36. package/lib/module/index.js.map +1 -1
  37. package/lib/module/index.web.js +215 -99
  38. package/lib/module/index.web.js.map +1 -1
  39. package/lib/module/internal.js +2 -9
  40. package/lib/module/internal.js.map +1 -1
  41. package/lib/module/shared.js +60 -2
  42. package/lib/module/shared.js.map +1 -1
  43. package/lib/module/storage-core.js +860 -146
  44. package/lib/module/storage-core.js.map +1 -1
  45. package/lib/module/storage-runtime.js +4 -1
  46. package/lib/module/storage-runtime.js.map +1 -1
  47. package/lib/module/testing.js +8 -1
  48. package/lib/module/testing.js.map +1 -1
  49. package/lib/typescript/capabilities.d.ts +2 -1
  50. package/lib/typescript/capabilities.d.ts.map +1 -1
  51. package/lib/typescript/core/durability.d.ts +7 -2
  52. package/lib/typescript/core/durability.d.ts.map +1 -1
  53. package/lib/typescript/index.d.ts +2 -2
  54. package/lib/typescript/index.d.ts.map +1 -1
  55. package/lib/typescript/index.web.d.ts +3 -29
  56. package/lib/typescript/index.web.d.ts.map +1 -1
  57. package/lib/typescript/internal.d.ts +0 -2
  58. package/lib/typescript/internal.d.ts.map +1 -1
  59. package/lib/typescript/shared.d.ts +19 -0
  60. package/lib/typescript/shared.d.ts.map +1 -1
  61. package/lib/typescript/storage-core.d.ts +12 -3
  62. package/lib/typescript/storage-core.d.ts.map +1 -1
  63. package/lib/typescript/storage-runtime.d.ts +2 -1
  64. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  65. package/lib/typescript/testing.d.ts +1 -1
  66. package/lib/typescript/testing.d.ts.map +1 -1
  67. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
  68. package/package.json +3 -3
  69. package/react-native-nitro-storage.podspec +3 -1
  70. package/src/capabilities.ts +3 -1
  71. package/src/core/durability.ts +139 -39
  72. package/src/index.ts +20 -10
  73. package/src/index.web.ts +328 -166
  74. package/src/internal.ts +0 -14
  75. package/src/shared.ts +85 -0
  76. package/src/storage-core.ts +1165 -199
  77. package/src/storage-runtime.ts +6 -0
  78. package/src/testing.ts +8 -1
@@ -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): void;
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
- ): void;
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 keysToSet: string[] = [];
53
- const valuesToSet: string[] = [];
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
- writes.forEach(({ key, value }) => {
57
- if (value === undefined) {
58
- keysToRemove.push(key);
59
- return;
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
- if (keysToRemove.length > 0) {
70
- options.backend.removeBatch(keysToRemove, StorageScope.Disk);
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
- { keys: string[]; values: string[] }
123
+ { writes: PendingSecureWrite[] }
87
124
  >();
88
- const keysToRemove: string[] = [];
125
+ const removeWrites: PendingSecureWrite[] = [];
89
126
 
90
- writes.forEach(({ key, value, accessControl }) => {
127
+ writes.forEach((write) => {
128
+ const { value, accessControl } = write;
91
129
  if (value === undefined) {
92
- keysToRemove.push(key);
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 ?? { keys: [], values: [] };
98
- group.keys.push(key);
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
- 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);
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(key: string, value: string | undefined): void {
116
- pendingDiskWrites.set(key, { key, value });
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
- ): void {
129
- const pendingWrite: PendingSecureWrite = { key, value };
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 { 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
  },