react-native-nitro-storage 0.7.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 (106) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +20 -9
  3. package/SECURITY.md +2 -2
  4. package/docs/api-reference.md +11 -1
  5. package/docs/batch-transactions-migrations.md +8 -2
  6. package/docs/benchmarks.md +19 -15
  7. package/docs/web-backends.md +18 -1
  8. package/ios/IOSStorageAdapterCpp.mm +162 -17
  9. package/lib/commonjs/capabilities.js +26 -0
  10. package/lib/commonjs/capabilities.js.map +1 -0
  11. package/lib/commonjs/core/durability.js +137 -0
  12. package/lib/commonjs/core/durability.js.map +1 -0
  13. package/lib/commonjs/core/metrics.js +91 -0
  14. package/lib/commonjs/core/metrics.js.map +1 -0
  15. package/lib/commonjs/index.js +47 -22
  16. package/lib/commonjs/index.js.map +1 -1
  17. package/lib/commonjs/index.web.js +118 -66
  18. package/lib/commonjs/index.web.js.map +1 -1
  19. package/lib/commonjs/indexeddb-backend.js +73 -23
  20. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  21. package/lib/commonjs/internal.js +27 -4
  22. package/lib/commonjs/internal.js.map +1 -1
  23. package/lib/commonjs/shared.js +2 -2
  24. package/lib/commonjs/shared.js.map +1 -1
  25. package/lib/commonjs/storage-core.js +217 -233
  26. package/lib/commonjs/storage-core.js.map +1 -1
  27. package/lib/commonjs/storage-events.js.map +1 -1
  28. package/lib/commonjs/storage-platform.js +6 -0
  29. package/lib/commonjs/storage-platform.js.map +1 -0
  30. package/lib/commonjs/storage-runtime.js +3 -21
  31. package/lib/commonjs/storage-runtime.js.map +1 -1
  32. package/lib/commonjs/testing.js +19 -4
  33. package/lib/commonjs/testing.js.map +1 -1
  34. package/lib/commonjs/web-backend-contract.js +27 -0
  35. package/lib/commonjs/web-backend-contract.js.map +1 -0
  36. package/lib/module/capabilities.js +21 -0
  37. package/lib/module/capabilities.js.map +1 -0
  38. package/lib/module/core/durability.js +133 -0
  39. package/lib/module/core/durability.js.map +1 -0
  40. package/lib/module/core/metrics.js +87 -0
  41. package/lib/module/core/metrics.js.map +1 -0
  42. package/lib/module/index.js +48 -23
  43. package/lib/module/index.js.map +1 -1
  44. package/lib/module/index.web.js +103 -64
  45. package/lib/module/index.web.js.map +1 -1
  46. package/lib/module/indexeddb-backend.js +73 -23
  47. package/lib/module/indexeddb-backend.js.map +1 -1
  48. package/lib/module/internal.js +24 -3
  49. package/lib/module/internal.js.map +1 -1
  50. package/lib/module/shared.js +2 -2
  51. package/lib/module/shared.js.map +1 -1
  52. package/lib/module/storage-core.js +218 -234
  53. package/lib/module/storage-core.js.map +1 -1
  54. package/lib/module/storage-events.js.map +1 -1
  55. package/lib/module/storage-platform.js +4 -0
  56. package/lib/module/storage-platform.js.map +1 -0
  57. package/lib/module/storage-runtime.js +3 -21
  58. package/lib/module/storage-runtime.js.map +1 -1
  59. package/lib/module/testing.js +5 -3
  60. package/lib/module/testing.js.map +1 -1
  61. package/lib/module/web-backend-contract.js +22 -0
  62. package/lib/module/web-backend-contract.js.map +1 -0
  63. package/lib/typescript/capabilities.d.ts +11 -0
  64. package/lib/typescript/capabilities.d.ts.map +1 -0
  65. package/lib/typescript/core/durability.d.ts +27 -0
  66. package/lib/typescript/core/durability.d.ts.map +1 -0
  67. package/lib/typescript/core/metrics.d.ts +12 -0
  68. package/lib/typescript/core/metrics.d.ts.map +1 -0
  69. package/lib/typescript/index.d.ts +5 -2
  70. package/lib/typescript/index.d.ts.map +1 -1
  71. package/lib/typescript/index.web.d.ts +8 -4
  72. package/lib/typescript/index.web.d.ts.map +1 -1
  73. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  74. package/lib/typescript/internal.d.ts +3 -0
  75. package/lib/typescript/internal.d.ts.map +1 -1
  76. package/lib/typescript/shared.d.ts +2 -1
  77. package/lib/typescript/shared.d.ts.map +1 -1
  78. package/lib/typescript/storage-core.d.ts +3 -3
  79. package/lib/typescript/storage-core.d.ts.map +1 -1
  80. package/lib/typescript/storage-events.d.ts +2 -2
  81. package/lib/typescript/storage-events.d.ts.map +1 -1
  82. package/lib/typescript/storage-platform.d.ts +12 -0
  83. package/lib/typescript/storage-platform.d.ts.map +1 -0
  84. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  85. package/lib/typescript/testing.d.ts +5 -1
  86. package/lib/typescript/testing.d.ts.map +1 -1
  87. package/lib/typescript/web-backend-contract.d.ts +10 -0
  88. package/lib/typescript/web-backend-contract.d.ts.map +1 -0
  89. package/lib/typescript/web-storage-backend.d.ts +1 -1
  90. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  91. package/package.json +3 -2
  92. package/src/capabilities.ts +36 -0
  93. package/src/core/durability.ts +170 -0
  94. package/src/core/metrics.ts +133 -0
  95. package/src/index.ts +66 -40
  96. package/src/index.web.ts +158 -86
  97. package/src/indexeddb-backend.ts +91 -20
  98. package/src/internal.ts +50 -4
  99. package/src/shared.ts +7 -6
  100. package/src/storage-core.ts +317 -285
  101. package/src/storage-events.ts +2 -1
  102. package/src/storage-platform.ts +16 -0
  103. package/src/storage-runtime.ts +13 -47
  104. package/src/testing.ts +14 -7
  105. package/src/web-backend-contract.ts +34 -0
  106. 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,15 +102,51 @@ 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;
@@ -153,22 +196,29 @@ export async function createIndexedDBBackend(
153
196
  }
154
197
  };
155
198
 
156
- tx.oncomplete = () => resolve();
157
- tx.onerror = () =>
199
+ tx.oncomplete = () => {
200
+ resolve();
201
+ };
202
+ tx.onerror = () => {
158
203
  reject(tx.error ?? new Error("Failed to load IndexedDB entries."));
204
+ };
159
205
  });
160
206
 
161
- function trackWrite(tx: IDBTransaction): void {
207
+ function trackWrite(tx: IDBTransaction, keys: string[] | null): void {
162
208
  const pending = new Promise<void>((resolve) => {
163
- tx.oncomplete = () => resolve();
209
+ tx.oncomplete = () => {
210
+ resolve();
211
+ };
164
212
  tx.onerror = () => {
165
213
  handleAsyncError(
214
+ keys,
166
215
  tx.error ?? new Error("Failed to persist IndexedDB transaction."),
167
216
  );
168
217
  resolve();
169
218
  };
170
219
  tx.onabort = () => {
171
220
  handleAsyncError(
221
+ keys,
172
222
  tx.error ?? new Error("IndexedDB transaction was aborted."),
173
223
  );
174
224
  resolve();
@@ -186,9 +236,10 @@ export async function createIndexedDBBackend(
186
236
  try {
187
237
  const tx = db.transaction(storeName, "readwrite");
188
238
  tx.objectStore(storeName).put(value, key);
189
- trackWrite(tx);
239
+ trackWrite(tx, [key]);
190
240
  } catch {
191
241
  handleAsyncError(
242
+ [key],
192
243
  new Error(`Failed to queue IndexedDB write for "${key}".`),
193
244
  );
194
245
  }
@@ -198,9 +249,10 @@ export async function createIndexedDBBackend(
198
249
  try {
199
250
  const tx = db.transaction(storeName, "readwrite");
200
251
  tx.objectStore(storeName).delete(key);
201
- trackWrite(tx);
252
+ trackWrite(tx, [key]);
202
253
  } catch {
203
254
  handleAsyncError(
255
+ [key],
204
256
  new Error(`Failed to queue IndexedDB delete for "${key}".`),
205
257
  );
206
258
  }
@@ -210,9 +262,9 @@ export async function createIndexedDBBackend(
210
262
  try {
211
263
  const tx = db.transaction(storeName, "readwrite");
212
264
  tx.objectStore(storeName).clear();
213
- trackWrite(tx);
265
+ trackWrite(tx, null);
214
266
  } catch {
215
- handleAsyncError(new Error("Failed to queue IndexedDB clear."));
267
+ handleAsyncError(null, new Error("Failed to queue IndexedDB clear."));
216
268
  }
217
269
  }
218
270
 
@@ -254,8 +306,10 @@ export async function createIndexedDBBackend(
254
306
  },
255
307
  setMany(entries): void {
256
308
  assertOpen();
309
+ const keys: string[] = [];
257
310
  entries.forEach(([key, value]) => {
258
311
  cache.set(key, value);
312
+ keys.push(key);
259
313
  });
260
314
  try {
261
315
  const tx = db.transaction(storeName, "readwrite");
@@ -264,9 +318,12 @@ export async function createIndexedDBBackend(
264
318
  store.put(value, key);
265
319
  publish({ key, newValue: value });
266
320
  });
267
- trackWrite(tx);
321
+ trackWrite(tx, keys);
268
322
  } catch {
269
- handleAsyncError(new Error("Failed to queue IndexedDB batch write."));
323
+ handleAsyncError(
324
+ null,
325
+ new Error("Failed to queue IndexedDB batch write."),
326
+ );
270
327
  }
271
328
  },
272
329
  removeMany(keys: string[]): void {
@@ -281,9 +338,12 @@ export async function createIndexedDBBackend(
281
338
  store.delete(key);
282
339
  publish({ key, newValue: null });
283
340
  });
284
- trackWrite(tx);
341
+ trackWrite(tx, keys);
285
342
  } catch {
286
- handleAsyncError(new Error("Failed to queue IndexedDB batch delete."));
343
+ handleAsyncError(
344
+ null,
345
+ new Error("Failed to queue IndexedDB batch delete."),
346
+ );
287
347
  }
288
348
  },
289
349
  size(): number {
@@ -303,14 +363,25 @@ export async function createIndexedDBBackend(
303
363
  return;
304
364
  }
305
365
 
306
- const [error] = pendingErrors.splice(0);
307
- 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
+ );
308
378
  },
309
379
  close(): void {
310
380
  if (closed) {
311
381
  return;
312
382
  }
313
383
  closed = true;
384
+ removeLifecycleFlush();
314
385
  subscribers.clear();
315
386
  channel?.close();
316
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,8 +123,17 @@ 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);
@@ -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 =