react-native-nitro-storage 0.6.0 → 0.8.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 (108) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/README.md +36 -12
  3. package/SECURITY.md +2 -2
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +225 -76
  5. package/app.plugin.js +2 -0
  6. package/cpp/bindings/HybridStorage.cpp +4 -12
  7. package/docs/api-reference.md +11 -1
  8. package/docs/batch-transactions-migrations.md +8 -2
  9. package/docs/benchmarks.md +19 -15
  10. package/docs/secure-storage.md +4 -0
  11. package/docs/web-backends.md +18 -1
  12. package/ios/IOSStorageAdapterCpp.mm +162 -17
  13. package/lib/commonjs/capabilities.js +26 -0
  14. package/lib/commonjs/capabilities.js.map +1 -0
  15. package/lib/commonjs/core/durability.js +137 -0
  16. package/lib/commonjs/core/durability.js.map +1 -0
  17. package/lib/commonjs/core/metrics.js +91 -0
  18. package/lib/commonjs/core/metrics.js.map +1 -0
  19. package/lib/commonjs/index.js +47 -22
  20. package/lib/commonjs/index.js.map +1 -1
  21. package/lib/commonjs/index.web.js +118 -66
  22. package/lib/commonjs/index.web.js.map +1 -1
  23. package/lib/commonjs/indexeddb-backend.js +73 -23
  24. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  25. package/lib/commonjs/internal.js +27 -4
  26. package/lib/commonjs/internal.js.map +1 -1
  27. package/lib/commonjs/shared.js +2 -2
  28. package/lib/commonjs/shared.js.map +1 -1
  29. package/lib/commonjs/storage-core.js +217 -233
  30. package/lib/commonjs/storage-core.js.map +1 -1
  31. package/lib/commonjs/storage-platform.js +6 -0
  32. package/lib/commonjs/storage-platform.js.map +1 -0
  33. package/lib/commonjs/storage-runtime.js +3 -21
  34. package/lib/commonjs/storage-runtime.js.map +1 -1
  35. package/lib/commonjs/testing.js +19 -4
  36. package/lib/commonjs/testing.js.map +1 -1
  37. package/lib/commonjs/web-backend-contract.js +27 -0
  38. package/lib/commonjs/web-backend-contract.js.map +1 -0
  39. package/lib/module/capabilities.js +21 -0
  40. package/lib/module/capabilities.js.map +1 -0
  41. package/lib/module/core/durability.js +133 -0
  42. package/lib/module/core/durability.js.map +1 -0
  43. package/lib/module/core/metrics.js +87 -0
  44. package/lib/module/core/metrics.js.map +1 -0
  45. package/lib/module/index.js +48 -23
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/index.web.js +103 -64
  48. package/lib/module/index.web.js.map +1 -1
  49. package/lib/module/indexeddb-backend.js +73 -23
  50. package/lib/module/indexeddb-backend.js.map +1 -1
  51. package/lib/module/internal.js +24 -3
  52. package/lib/module/internal.js.map +1 -1
  53. package/lib/module/shared.js +2 -2
  54. package/lib/module/shared.js.map +1 -1
  55. package/lib/module/storage-core.js +218 -234
  56. package/lib/module/storage-core.js.map +1 -1
  57. package/lib/module/storage-platform.js +4 -0
  58. package/lib/module/storage-platform.js.map +1 -0
  59. package/lib/module/storage-runtime.js +3 -21
  60. package/lib/module/storage-runtime.js.map +1 -1
  61. package/lib/module/testing.js +5 -3
  62. package/lib/module/testing.js.map +1 -1
  63. package/lib/module/web-backend-contract.js +22 -0
  64. package/lib/module/web-backend-contract.js.map +1 -0
  65. package/lib/typescript/capabilities.d.ts +11 -0
  66. package/lib/typescript/capabilities.d.ts.map +1 -0
  67. package/lib/typescript/core/durability.d.ts +27 -0
  68. package/lib/typescript/core/durability.d.ts.map +1 -0
  69. package/lib/typescript/core/metrics.d.ts +12 -0
  70. package/lib/typescript/core/metrics.d.ts.map +1 -0
  71. package/lib/typescript/index.d.ts +11 -3
  72. package/lib/typescript/index.d.ts.map +1 -1
  73. package/lib/typescript/index.web.d.ts +14 -5
  74. package/lib/typescript/index.web.d.ts.map +1 -1
  75. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  76. package/lib/typescript/internal.d.ts +3 -0
  77. package/lib/typescript/internal.d.ts.map +1 -1
  78. package/lib/typescript/shared.d.ts +2 -1
  79. package/lib/typescript/shared.d.ts.map +1 -1
  80. package/lib/typescript/storage-core.d.ts +14 -4
  81. package/lib/typescript/storage-core.d.ts.map +1 -1
  82. package/lib/typescript/storage-events.d.ts +2 -2
  83. package/lib/typescript/storage-events.d.ts.map +1 -1
  84. package/lib/typescript/storage-platform.d.ts +12 -0
  85. package/lib/typescript/storage-platform.d.ts.map +1 -0
  86. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  87. package/lib/typescript/testing.d.ts +17 -3
  88. package/lib/typescript/testing.d.ts.map +1 -1
  89. package/lib/typescript/web-backend-contract.d.ts +10 -0
  90. package/lib/typescript/web-backend-contract.d.ts.map +1 -0
  91. package/lib/typescript/web-storage-backend.d.ts +1 -1
  92. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  93. package/package.json +6 -6
  94. package/src/capabilities.ts +36 -0
  95. package/src/core/durability.ts +170 -0
  96. package/src/core/metrics.ts +133 -0
  97. package/src/index.ts +67 -42
  98. package/src/index.web.ts +159 -88
  99. package/src/indexeddb-backend.ts +92 -22
  100. package/src/internal.ts +51 -5
  101. package/src/shared.ts +7 -6
  102. package/src/storage-core.ts +336 -287
  103. package/src/storage-events.ts +3 -3
  104. package/src/storage-platform.ts +16 -0
  105. package/src/storage-runtime.ts +13 -47
  106. package/src/testing.ts +14 -7
  107. package/src/web-backend-contract.ts +34 -0
  108. package/src/web-storage-backend.ts +1 -1
@@ -19,7 +19,11 @@ export type IndexedDBBackendOptions = {
19
19
  function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
20
20
  return new Promise((resolve, reject) => {
21
21
  if (typeof indexedDB === "undefined") {
22
- reject(new Error("IndexedDB is not available in this environment."));
22
+ reject(
23
+ new Error(
24
+ "[nitro-error:unsupported] IndexedDB is not available in this environment.",
25
+ ),
26
+ );
23
27
  return;
24
28
  }
25
29
 
@@ -32,9 +36,12 @@ function openDB(dbName: string, storeName: string): Promise<IDBDatabase> {
32
36
  }
33
37
  };
34
38
 
35
- request.onsuccess = () => resolve(request.result);
36
- request.onerror = () =>
39
+ request.onsuccess = () => {
40
+ resolve(request.result);
41
+ };
42
+ request.onerror = () => {
37
43
  reject(request.error ?? new Error("Failed to open IndexedDB database."));
44
+ };
38
45
  });
39
46
  }
40
47
 
@@ -72,7 +79,7 @@ export async function createIndexedDBBackend(
72
79
  const db = await openDB(dbName, storeName);
73
80
  const cache = new Map<string, string>();
74
81
  const pendingWrites = new Set<Promise<void>>();
75
- const pendingErrors: Error[] = [];
82
+ const pendingErrors: { keys: string[] | null; error: Error }[] = [];
76
83
  const subscribers = new Set<(event: WebStorageChangeEvent) => void>();
77
84
  let closed = false;
78
85
  const sourceId = `nitro-storage-${Math.random().toString(36).slice(2)}`;
@@ -95,23 +102,58 @@ export async function createIndexedDBBackend(
95
102
  });
96
103
  }
97
104
 
98
- function handleAsyncError(error: unknown): void {
105
+ function handleAsyncError(keys: string[] | null, error: unknown): void {
99
106
  const normalized =
100
107
  error instanceof Error
101
108
  ? error
102
109
  : new Error(String(error ?? "Unknown IndexedDB error"));
103
- pendingErrors.push(normalized);
110
+ pendingErrors.push({ keys, error: normalized });
104
111
  options.onError?.(normalized);
105
112
  }
106
113
 
114
+ function handleLifecycleFlush(): void {
115
+ if (closed || pendingWrites.size === 0) {
116
+ return;
117
+ }
118
+ void Promise.all(Array.from(pendingWrites)).catch(() => {
119
+ // Errors are surfaced through flush() and onError; the page may be
120
+ // hidden, so the flush result is intentionally not awaited here.
121
+ });
122
+ }
123
+
124
+ function installLifecycleFlush(): () => void {
125
+ if (typeof globalThis === "undefined" || !globalThis.addEventListener) {
126
+ return () => {};
127
+ }
128
+
129
+ const onPageHide = () => {
130
+ handleLifecycleFlush();
131
+ };
132
+ const onVisibilityChange = () => {
133
+ if (
134
+ typeof document !== "undefined" &&
135
+ document.visibilityState === "hidden"
136
+ ) {
137
+ handleLifecycleFlush();
138
+ }
139
+ };
140
+ globalThis.addEventListener("pagehide", onPageHide);
141
+ globalThis.addEventListener("visibilitychange", onVisibilityChange);
142
+ return () => {
143
+ globalThis.removeEventListener("pagehide", onPageHide);
144
+ globalThis.removeEventListener("visibilitychange", onVisibilityChange);
145
+ };
146
+ }
147
+
148
+ const removeLifecycleFlush = installLifecycleFlush();
149
+
107
150
  channel?.addEventListener("message", (event: MessageEvent) => {
108
151
  if (closed) {
109
152
  return;
110
153
  }
111
154
 
112
155
  const data = event.data as
113
- | (WebStorageChangeEvent & { sourceId?: string })
114
- | undefined;
156
+ (WebStorageChangeEvent & { sourceId?: string }) | undefined;
115
157
  if (!data || data.sourceId === sourceId) {
116
158
  return;
117
159
  }
@@ -154,22 +196,29 @@ export async function createIndexedDBBackend(
154
196
  }
155
197
  };
156
198
 
157
- tx.oncomplete = () => resolve();
158
- tx.onerror = () =>
199
+ tx.oncomplete = () => {
200
+ resolve();
201
+ };
202
+ tx.onerror = () => {
159
203
  reject(tx.error ?? new Error("Failed to load IndexedDB entries."));
204
+ };
160
205
  });
161
206
 
162
- function trackWrite(tx: IDBTransaction): void {
207
+ function trackWrite(tx: IDBTransaction, keys: string[] | null): void {
163
208
  const pending = new Promise<void>((resolve) => {
164
- tx.oncomplete = () => resolve();
209
+ tx.oncomplete = () => {
210
+ resolve();
211
+ };
165
212
  tx.onerror = () => {
166
213
  handleAsyncError(
214
+ keys,
167
215
  tx.error ?? new Error("Failed to persist IndexedDB transaction."),
168
216
  );
169
217
  resolve();
170
218
  };
171
219
  tx.onabort = () => {
172
220
  handleAsyncError(
221
+ keys,
173
222
  tx.error ?? new Error("IndexedDB transaction was aborted."),
174
223
  );
175
224
  resolve();
@@ -187,9 +236,10 @@ export async function createIndexedDBBackend(
187
236
  try {
188
237
  const tx = db.transaction(storeName, "readwrite");
189
238
  tx.objectStore(storeName).put(value, key);
190
- trackWrite(tx);
239
+ trackWrite(tx, [key]);
191
240
  } catch {
192
241
  handleAsyncError(
242
+ [key],
193
243
  new Error(`Failed to queue IndexedDB write for "${key}".`),
194
244
  );
195
245
  }
@@ -199,9 +249,10 @@ export async function createIndexedDBBackend(
199
249
  try {
200
250
  const tx = db.transaction(storeName, "readwrite");
201
251
  tx.objectStore(storeName).delete(key);
202
- trackWrite(tx);
252
+ trackWrite(tx, [key]);
203
253
  } catch {
204
254
  handleAsyncError(
255
+ [key],
205
256
  new Error(`Failed to queue IndexedDB delete for "${key}".`),
206
257
  );
207
258
  }
@@ -211,9 +262,9 @@ export async function createIndexedDBBackend(
211
262
  try {
212
263
  const tx = db.transaction(storeName, "readwrite");
213
264
  tx.objectStore(storeName).clear();
214
- trackWrite(tx);
265
+ trackWrite(tx, null);
215
266
  } catch {
216
- handleAsyncError(new Error("Failed to queue IndexedDB clear."));
267
+ handleAsyncError(null, new Error("Failed to queue IndexedDB clear."));
217
268
  }
218
269
  }
219
270
 
@@ -255,8 +306,10 @@ export async function createIndexedDBBackend(
255
306
  },
256
307
  setMany(entries): void {
257
308
  assertOpen();
309
+ const keys: string[] = [];
258
310
  entries.forEach(([key, value]) => {
259
311
  cache.set(key, value);
312
+ keys.push(key);
260
313
  });
261
314
  try {
262
315
  const tx = db.transaction(storeName, "readwrite");
@@ -265,9 +318,12 @@ export async function createIndexedDBBackend(
265
318
  store.put(value, key);
266
319
  publish({ key, newValue: value });
267
320
  });
268
- trackWrite(tx);
321
+ trackWrite(tx, keys);
269
322
  } catch {
270
- handleAsyncError(new Error("Failed to queue IndexedDB batch write."));
323
+ handleAsyncError(
324
+ null,
325
+ new Error("Failed to queue IndexedDB batch write."),
326
+ );
271
327
  }
272
328
  },
273
329
  removeMany(keys: string[]): void {
@@ -282,9 +338,12 @@ export async function createIndexedDBBackend(
282
338
  store.delete(key);
283
339
  publish({ key, newValue: null });
284
340
  });
285
- trackWrite(tx);
341
+ trackWrite(tx, keys);
286
342
  } catch {
287
- handleAsyncError(new Error("Failed to queue IndexedDB batch delete."));
343
+ handleAsyncError(
344
+ null,
345
+ new Error("Failed to queue IndexedDB batch delete."),
346
+ );
288
347
  }
289
348
  },
290
349
  size(): number {
@@ -304,14 +363,25 @@ export async function createIndexedDBBackend(
304
363
  return;
305
364
  }
306
365
 
307
- const [error] = pendingErrors.splice(0);
308
- throw error;
366
+ const failures = pendingErrors.splice(0);
367
+ const failedKeys = failures
368
+ .flatMap(({ keys }) => keys ?? [])
369
+ .filter((key, index, all) => all.indexOf(key) === index);
370
+ const keyDetail =
371
+ failedKeys.length > 0
372
+ ? ` Affected keys: ${failedKeys.join(", ")}.`
373
+ : "";
374
+ const [first] = failures;
375
+ throw new Error(
376
+ `NitroStorage: IndexedDB persistence failed for ${failures.length} write(s).${keyDetail} ${first?.error.message ?? ""}`,
377
+ );
309
378
  },
310
379
  close(): void {
311
380
  if (closed) {
312
381
  return;
313
382
  }
314
383
  closed = true;
384
+ removeLifecycleFlush();
315
385
  subscribers.clear();
316
386
  channel?.close();
317
387
  db.close();
package/src/internal.ts CHANGED
@@ -16,6 +16,36 @@ const PRIM_NAN = "__nitro_storage_primitive__:n:NaN";
16
16
  const NAMESPACE_SEPARATOR = ":";
17
17
  const VERSION_TOKEN_PREFIX = "__nitro_storage_version__:";
18
18
 
19
+ export const ESCAPE_PREFIX = "__nitro_storage_escaped__:";
20
+
21
+ const RESERVED_RAW_TOKENS = new Set<string>([
22
+ PRIM_NULL,
23
+ PRIM_UNDEFINED,
24
+ PRIM_TRUE,
25
+ PRIM_FALSE,
26
+ PRIM_INFINITY,
27
+ PRIM_NEG_INFINITY,
28
+ PRIM_NAN,
29
+ NATIVE_BATCH_MISSING_SENTINEL,
30
+ ]);
31
+
32
+ export function escapeCollidingRawValue(value: string): string {
33
+ if (
34
+ RESERVED_RAW_TOKENS.has(value) ||
35
+ value.startsWith(PRIMITIVE_FAST_PATH_PREFIX) ||
36
+ value.startsWith(ESCAPE_PREFIX)
37
+ ) {
38
+ return `${ESCAPE_PREFIX}${value}`;
39
+ }
40
+ return value;
41
+ }
42
+
43
+ export function unescapeCollidingRawValue(value: string): string {
44
+ return value.startsWith(ESCAPE_PREFIX)
45
+ ? value.slice(ESCAPE_PREFIX.length)
46
+ : value;
47
+ }
48
+
19
49
  export type StoredEnvelope = {
20
50
  __nitroStorageEnvelope: true;
21
51
  expiresAt: number;
@@ -71,7 +101,7 @@ export function assertBatchScope(
71
101
  export function decodeNativeBatchValue(
72
102
  value: string | undefined,
73
103
  ): string | undefined {
74
- if (value === NATIVE_BATCH_MISSING_SENTINEL) {
104
+ if (value === undefined || value === NATIVE_BATCH_MISSING_SENTINEL) {
75
105
  return undefined;
76
106
  }
77
107
 
@@ -93,13 +123,22 @@ export function serializeWithPrimitiveFastPath<T>(value: T): string {
93
123
  }
94
124
 
95
125
  switch (typeof value) {
96
- case "string":
97
- return PRIM_STRING_PREFIX + (value as string);
126
+ case "string": {
127
+ const stringValue = value as string;
128
+ if (
129
+ stringValue === NATIVE_BATCH_MISSING_SENTINEL ||
130
+ stringValue.startsWith(PRIMITIVE_FAST_PATH_PREFIX) ||
131
+ stringValue.startsWith(ESCAPE_PREFIX)
132
+ ) {
133
+ return `${PRIM_STRING_PREFIX}${ESCAPE_PREFIX}${stringValue}`;
134
+ }
135
+ return PRIM_STRING_PREFIX + stringValue;
136
+ }
98
137
  case "number":
99
138
  if (Number.isFinite(value)) {
100
139
  return PRIM_NUMBER_PREFIX + String(value);
101
140
  }
102
- if (Number.isNaN(value as number)) {
141
+ if (Number.isNaN(value)) {
103
142
  return PRIM_NAN;
104
143
  }
105
144
  if (value === Infinity) {
@@ -134,6 +173,10 @@ const CHAR_B = 98; // 'b'
134
173
  const CHAR_N = 110; // 'n'
135
174
 
136
175
  export function deserializeWithPrimitiveFastPath<T>(value: string): T {
176
+ if (value.startsWith(ESCAPE_PREFIX)) {
177
+ return value.slice(ESCAPE_PREFIX.length) as T;
178
+ }
179
+
137
180
  if (value.startsWith(PRIMITIVE_FAST_PATH_PREFIX)) {
138
181
  const prefixLen = PRIMITIVE_FAST_PATH_PREFIX.length;
139
182
  const tagChar = value.charCodeAt(prefixLen);
@@ -149,7 +192,10 @@ export function deserializeWithPrimitiveFastPath<T>(value: string): T {
149
192
  const payload = value.slice(prefixLen + 2);
150
193
 
151
194
  if (tagChar === CHAR_S) {
152
- return payload as T;
195
+ const unescapedPayload = payload.startsWith(ESCAPE_PREFIX)
196
+ ? payload.slice(ESCAPE_PREFIX.length)
197
+ : payload;
198
+ return unescapedPayload as T;
153
199
  }
154
200
  if (tagChar === CHAR_B) {
155
201
  return (payload === "1") as T;
package/src/shared.ts CHANGED
@@ -1,17 +1,18 @@
1
- import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
2
1
  import {
3
2
  serializeWithPrimitiveFastPath,
4
3
  deserializeWithPrimitiveFastPath,
5
4
  } from "./internal";
6
- import {
7
- getStorageErrorCode,
8
- isLockedStorageErrorCode,
9
- } from "./storage-runtime";
10
5
  import type {
11
6
  StorageChangeOperation,
12
7
  StorageChangeSource,
13
8
  StorageKeyChangeEvent,
14
9
  } from "./storage-events";
10
+ import {
11
+ getStorageErrorCode,
12
+ isLockedStorageErrorCode,
13
+ } from "./storage-runtime";
14
+ import { StorageScope } from "./Storage.types";
15
+ import type { AccessControl, BiometricLevel } from "./Storage.types";
15
16
 
16
17
  export type Validator<T> = (value: unknown) => value is T;
17
18
 
@@ -144,7 +145,7 @@ export const runMicrotask =
144
145
  typeof queueMicrotask === "function"
145
146
  ? queueMicrotask
146
147
  : (task: () => void) => {
147
- Promise.resolve().then(task);
148
+ void Promise.resolve().then(task);
148
149
  };
149
150
 
150
151
  export const now =