diff --git a/.changeset/array-module-refactor.md b/.changeset/array-module-refactor.md
index 9e080ba24..b14046002 100644
--- a/.changeset/array-module-refactor.md
+++ b/.changeset/array-module-refactor.md
@@ -30,6 +30,7 @@ if (isNonEmptyArray(mutableArr)) { ... }
### New Functions
- **`arrayFrom`** — creates a readonly array from an iterable or by generating elements with a length and mapper
+- **`arrayFromAsync`** — creates a readonly array from an async iterable (or iterable of promises) and awaits all values
- **`flatMapArray`** — maps each element to an array and flattens the result, preserving non-empty type when applicable
- **`concatArrays`** — concatenates two arrays, returning non-empty when at least one input is non-empty
- **`sortArray`** — returns a new sorted array (wraps `toSorted`), preserving non-empty type
diff --git a/.gitignore b/.gitignore
index 29db6827f..304fb8bd8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,6 +20,19 @@ coverage
tmp
__screenshots__
*.tsBuildInfo
+*.tsbuildinfo
.generated
.ai/
+.vite
+next-env.d.ts
+
+# apps/relay
+apps/relay/data/*
+!apps/relay/data/.gitkeep
+
+# examples/react-expo
+examples/react-expo/.expo/
+examples/react-expo/expo-env.d.ts
+examples/react-expo/ios
+examples/react-expo/android
diff --git a/README.md b/README.md
index dd33342ed..a0e9558d2 100644
--- a/README.md
+++ b/README.md
@@ -14,39 +14,55 @@ Evolu is a TypeScript library and local-first platform.
## 🪴 Project Activity
-
+
## Integration Matrix
-Coverage snapshot date: `2026-02-27` (from `bun run test:coverage` and `bun run test:coverage:bun`).
-
-| Package | Supported Versions | Implementation Status | Coverage (Statements / Branches) | Notes |
-| --- | --- | --- | --- | --- |
-| `@evolu/common` | Node `>=24.0.0` | Stable core | `94.47% / 89.57%` | Main engine + local-first protocol/runtime. |
-| `@evolu/web` | `@evolu/common ^7.4.1` | Stable | `99.33% / 93.71%` | Browser runtime (Worker/SharedWorker/Web Locks path). |
-| `@evolu/nodejs` | Node `>=24.0.0`, `@evolu/common ^7.4.1` | Stable | `95.74% / 87.50%` | Includes relay adapter hardening (WS lifecycle + subscribe/broadcast/unsubscribe + restart coverage). |
-| `@evolu/react-web` | React `>=19`, React DOM `>=19`, `@evolu/web ^2.4.0` | Stable thin adapter | `100% / 100%` | Thin web integration wrapper. |
-| `@evolu/react-native` | React Native `>=0.84`, Expo `>=55`, `@op-engineering/op-sqlite >=12` | Lane-gated hardening | `100.00% / 100.00%` (lane gate) | Strict file gates (`react-native` + `expo`) are enforced at `100/100/100/100` for scoped source files. |
-| `@evolu/react` | React `>=19` | Wrapper support | `0% / 0%` | Hook wrappers; coverage expansion planned. |
-| `@evolu/vue` | Vue `>=3.5.29` | Wrapper support | `0% / 0%` | Composition API wrappers; coverage expansion planned. |
-| `@evolu/svelte` | Svelte `>=5.53.3`, `@evolu/web ^2.4.0` | Wrapper support | `0% / 0%` | Store-based wrappers; coverage expansion planned. |
-| `@evolu/bun` (private) | `@evolu/common ^7.4.1`, Bun `1.3.x` | Experimental adapter | `100% / 100%` | Measured via Bun coverage runner on `BunDbWorker.ts`. |
+Coverage snapshot date: `2026-03-01` (from `bun run verify`).
+
+| Package | Baseline | Status |
+| ---------------------- | --------------------------------------- | -------------------- |
+| `@evolu/common` | Node `>=24.0.0` | Stable core |
+| `@evolu/web` | Browser + `@evolu/common ^7.4.1` | Stable |
+| `@evolu/nodejs` | Node `>=24.0.0` + `@evolu/common ^7.4.1` | Stable |
+| `@evolu/react-web` | React `>=19` + `@evolu/web ^2.4.0` | Thin adapter |
+| `@evolu/react-native` | RN `>=0.84`, Expo `>=55` | Lane-gated hardening |
+| `@evolu/react` | React `>=19` | Wrapper support |
+| `@evolu/vue` | Vue `>=3.5.29` | Wrapper support |
+| `@evolu/svelte` | Svelte `>=5.53.3` | Wrapper support |
+| `@evolu/bun` (private) | Bun `1.3.x` + `@evolu/common ^7.4.1` | Experimental adapter |
+
+Coverage notes (Statements / Branches):
+
+- `@evolu/common`: `94.46% / 89.64%`
+- `@evolu/web`: `98.90% / 93.30%`
+- `@evolu/nodejs`: `95.74% / 85.71%`
+- `@evolu/react-native`: `99.32% / 98.17%` + strict `react-native`/`expo` file gates at `100/100/100/100`
+- `@evolu/bun` (private): `100% / 100%` (`BunDbWorker.ts`)
+- Wrapper packages (`@evolu/react`, `@evolu/vue`, `@evolu/svelte`) are still coverage-expansion candidates.
## Planned Integrations (Roadmap View)
-| Integration | Fit | Priority | Expected Path | Main Risk / Blocker |
-| --- | --- | --- | --- | --- |
-| Next.js (App Router) | Very high | P0 | Official `@evolu/react-web` guide + production example for Server/Client boundaries. | SSR/client boundary handling and Worker lifecycle in edge runtimes. |
-| TanStack Start | Very high | P0 | Use `@evolu/react` + `@evolu/web`, focus on SSR/client boundary docs and example app. | SSR edge cases (worker lifecycle and hydration boundary). |
-| Astro | High | P0 | Client-island integration on top of `@evolu/web`, starter template + docs. | Island hydration timing and worker boot ordering. |
-| SvelteKit | High | P1 | `@evolu/svelte` + `@evolu/web` reference app with SSR-aware browser-only init. | Avoiding server-side execution for browser worker primitives. |
-| Nuxt 3 | High | P1 | Vue composables + client-only plugin/module (`@evolu/vue` + `@evolu/web`). | Nitro/SSR split and client plugin ordering. |
-| Remix / React Router | High | P1 | React adapter with explicit browser init boundaries and route loader guidance. | Loader/action patterns can accidentally cross server/client boundary. |
-| Tauri | High | P1 | Web runtime in WebView + optional Rust-side relay bridge for desktop sync scenarios. | Packaging/runtime differences across desktop targets. |
-| Electron | High | P1 | Reuse web runtime in renderer + optional Node relay bridge in main process. | Multi-process lifecycle and secure IPC boundaries. |
-| Capacitor (Ionic) | Medium | P2 | Reuse web runtime in WebView first, then mobile storage/perf hardening. | Mobile WebView storage consistency and background lifecycle constraints. |
-| Flutter | Medium/Low | P2 | Separate adapter/SDK (likely not a thin wrapper) or protocol-level bridge. | Different runtime/language model (Dart), no direct reuse of TS hooks. |
+| Integration | Priority | Focus |
+| -------------------- | -------- | ---------------------------------------------- |
+| Next.js (App Router) | P0 | Official web guide + SSR/client boundary docs |
+| TanStack Start | P0 | React/web adapter docs + production example |
+| Astro | P0 | Client-island starter + worker boot guidance |
+| SvelteKit | P1 | Browser-only init reference app |
+| Nuxt 3 | P1 | Client plugin/module integration path |
+| Remix / React Router | P1 | Explicit browser init in route patterns |
+| Tauri | P1 | WebView runtime + optional relay bridge |
+| Electron | P1 | Renderer runtime + optional main-process relay |
+| Capacitor (Ionic) | P2 | WebView-first runtime hardening |
+| Flutter | P2 | Separate SDK/bridge exploration |
+
+Main blockers to track:
+
+- SSR/client boundaries and hydration order in framework runtimes.
+- Worker lifecycle consistency across browser, edge, and desktop shells.
+- Desktop packaging/process boundaries (Electron/Tauri).
+- Mobile WebView storage consistency and background lifecycle.
Current recommendation:
@@ -63,15 +79,15 @@ Current recommendation:
Third-party runtime dependencies used by `@evolu/common`:
-| Dependency | Why It Is Used |
-| --- | --- |
-| `@noble/ciphers` | Audited cryptographic ciphers for encryption flows. |
-| `@noble/hashes` | Audited hash primitives used by protocol/auth internals. |
-| `@scure/bip39` | Mnemonic handling for owner/account recovery flows. |
+| Dependency | Why It Is Used |
+| ----------------- | ------------------------------------------------------------- |
+| `@noble/ciphers` | Audited cryptographic ciphers for encryption flows. |
+| `@noble/hashes` | Audited hash primitives used by protocol/auth internals. |
+| `@scure/bip39` | Mnemonic handling for owner/account recovery flows. |
| `disposablestack` | Disposable stack compatibility utility for cleanup semantics. |
-| `kysely` | Typed SQL query builder integration. |
-| `msgpackr` | Binary message serialization for protocol payloads. |
-| `zod` | Runtime schema validation and parsing. |
+| `kysely` | Typed SQL query builder integration. |
+| `msgpackr` | Binary message serialization for protocol payloads. |
+| `zod` | Runtime schema validation and parsing. |
Dependency policy:
diff --git a/apps/relay/package.json b/apps/relay/package.json
index e9d7641a2..95e5dfc50 100644
--- a/apps/relay/package.json
+++ b/apps/relay/package.json
@@ -19,7 +19,7 @@
},
"devDependencies": {
"@evolu/tsconfig": "workspace:*",
- "@types/node": "^25.3.2",
+ "@types/node": "^25.3.3",
"typescript": "^5.9.3"
},
"engines": {
diff --git a/bun.lock b/bun.lock
index 78688cc3d..64c199deb 100644
--- a/bun.lock
+++ b/bun.lock
@@ -29,7 +29,7 @@
},
"devDependencies": {
"@evolu/tsconfig": "workspace:*",
- "@types/node": "^25.3.2",
+ "@types/node": "^25.3.3",
"typescript": "^5.9.3",
},
},
@@ -155,7 +155,7 @@
"devDependencies": {
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.2.1",
- "@types/node": "^25.3.2",
+ "@types/node": "^25.3.3",
"@types/react": "~19.2.14",
"@types/react-dom": "~19.2.3",
"postcss": "^8.5.6",
@@ -319,7 +319,7 @@
"@evolu/common": "workspace:*",
"@evolu/tsconfig": "workspace:*",
"@types/better-sqlite3": "^7.6.13",
- "@types/node": "^25.3.2",
+ "@types/node": "^25.3.3",
"@types/ws": "^8.18.1",
"typescript": "^5.9.3",
},
@@ -346,6 +346,15 @@
"packages/react-native": {
"name": "@evolu/react-native",
"version": "14.3.0",
+ "dependencies": {
+ "set.prototype.difference": "^1.1.7",
+ "set.prototype.intersection": "^1.1.8",
+ "set.prototype.isdisjointfrom": "^1.1.5",
+ "set.prototype.issubsetof": "^1.1.4",
+ "set.prototype.issupersetof": "^1.1.3",
+ "set.prototype.symmetricdifference": "^1.1.3",
+ "set.prototype.union": "^1.1.3",
+ },
"devDependencies": {
"@evolu/common": "workspace:*",
"@evolu/react": "workspace:*",
@@ -358,7 +367,7 @@
"react": "19.2.4",
"react-native": "0.84.1",
"react-native-nitro-modules": "0.34.1",
- "react-native-sensitive-info": "6.0.0-rc.11",
+ "react-native-sensitive-info": "6.0.0-rc.12",
"react-native-svg": "15.15.3",
"typescript": "^5.9.3",
},
@@ -470,7 +479,7 @@
"devDependencies": {
"@evolu/common": "workspace:*",
"@evolu/tsconfig": "workspace:*",
- "@types/sharedworker": "^0.0.211",
+ "@types/sharedworker": "^0.0.221",
"@types/web-locks-api": "^0.0.5",
"typescript": "^5.9.3",
"user-agent-data-types": "^0.4.2",
@@ -1038,7 +1047,7 @@
"@harperfast/extended-iterable": ["@harperfast/extended-iterable@1.0.3", "", {}, "sha512-sSAYhQca3rDWtQUHSAPeO7axFIUJOI6hn1gjRC5APVE1a90tuyT8f5WIgRsFhhWA7htNkju2veB9eWL6YHi/Lw=="],
- "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
+ "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
@@ -1486,9 +1495,9 @@
"@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="],
- "@tabler/icons": ["@tabler/icons@3.37.1", "", {}, "sha512-neLCWkuyNHEPXCyYu6nbN4S3g/59BTa4qyITAugYVpq1YzYNDOZooW7/vRWH98ZItXAudxdKU8muFT7y1PqzuA=="],
+ "@tabler/icons": ["@tabler/icons@3.38.0", "", {}, "sha512-FdETQSpQ3lN7BEjEUzjKhsfTDCamrvMDops4HEMphTm3DmkIFpThoODn8XXZ8Q9MhjshIvphIYVHHB7zpq167w=="],
- "@tabler/icons-react": ["@tabler/icons-react@3.37.1", "", { "dependencies": { "@tabler/icons": "" }, "peerDependencies": { "react": ">= 16" } }, "sha512-R7UE71Jji7i4Su56Y9zU1uYEBakUejuDJvyuYVmBuUoqp/x3Pn4cv2huarexR3P0GJ2eHg4rUj9l5zccqS6K/Q=="],
+ "@tabler/icons-react": ["@tabler/icons-react@3.38.0", "", { "dependencies": { "@tabler/icons": "3.38.0" }, "peerDependencies": { "react": ">= 16" } }, "sha512-kR5wv+m4+GgmnSszg3rQd6SrTFAQ/XnQC/yTwIfuRJSfqB12KoIC7fPbIijFgOHTFlBN5DARnN0IVrR7KYG6/A=="],
"@tailwindcss/forms": ["@tailwindcss/forms@0.5.11", "", { "dependencies": { "mini-svg-data-uri": "^1.2.3" }, "peerDependencies": { "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA=="],
@@ -1614,7 +1623,7 @@
"@types/nlcst": ["@types/nlcst@2.0.3", "", { "dependencies": { "@types/unist": "*" } }, "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA=="],
- "@types/node": ["@types/node@25.3.2", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-RpV6r/ij22zRRdyBPcxDeKAzH43phWVKEjL2iksqo1Vz3CuBUrgmPpPhALKiRfU7OMCmeeO9vECBMsV0hMTG8Q=="],
+ "@types/node": ["@types/node@25.3.3", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ=="],
"@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="],
@@ -1626,7 +1635,7 @@
"@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="],
- "@types/sharedworker": ["@types/sharedworker@0.0.211", "", {}, "sha512-2etmIyOQqCpQG5mF3XZs4BZYct7cNtnTrfzQSL79dEwpFl3Z6qOqskzUKOf1fSq3d23BwkqTbN9ikZynzjYXzA=="],
+ "@types/sharedworker": ["@types/sharedworker@0.0.221", "", {}, "sha512-SZabQSTltxflklf3aJrFZrZf5h099r5IENsvv3a5pi+NKPyv8osnWsPLjrQsbiMrVlhv80MOCzY8sOsfEZwN1A=="],
"@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="],
@@ -1926,7 +1935,7 @@
"camelcase": ["camelcase@8.0.0", "", {}, "sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA=="],
- "caniuse-lite": ["caniuse-lite@1.0.30001774", "", {}, "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA=="],
+ "caniuse-lite": ["caniuse-lite@1.0.30001775", "", {}, "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
@@ -1962,7 +1971,7 @@
"cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="],
- "cli-truncate": ["cli-truncate@5.1.1", "", { "dependencies": { "slice-ansi": "^7.1.0", "string-width": "^8.0.0" } }, "sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A=="],
+ "cli-truncate": ["cli-truncate@5.2.0", "", { "dependencies": { "slice-ansi": "^8.0.0", "string-width": "^8.2.0" } }, "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw=="],
"cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
@@ -2164,7 +2173,7 @@
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
- "enhanced-resolve": ["enhanced-resolve@5.19.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg=="],
+ "enhanced-resolve": ["enhanced-resolve@5.20.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ=="],
"enquirer": ["enquirer@2.4.1", "", { "dependencies": { "ansi-colors": "^4.1.1", "strip-ansi": "^6.0.1" } }, "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ=="],
@@ -2804,9 +2813,9 @@
"metro-resolver": ["metro-resolver@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ=="],
- "metro-runtime": ["metro-runtime@0.83.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-sWj9KN311yG22Zv0kVbAp9dorB9HtTThvQKsAn6PLxrVrz+1UBsLrQSxjE/s4PtzDi1HABC648jo4K9Euz/5jw=="],
+ "metro-runtime": ["metro-runtime@0.83.5", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-f+b3ue9AWTVlZe2Xrki6TAoFtKIqw30jwfk7GQ1rDUBQaE0ZQ+NkiMEtb9uwH7uAjJ87U7Tdx1Jg1OJqUfEVlA=="],
- "metro-source-map": ["metro-source-map@0.83.4", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.4", "nullthrows": "^1.1.1", "ob1": "0.83.4", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-pPbmQwS0zgU+/0u5KPkuvlsQP0V+WYQ9qNshqupIL720QRH0vS3QR25IVVtbunofEDJchI11Q4QtIbmUyhpOBw=="],
+ "metro-source-map": ["metro-source-map@0.83.5", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.5", "nullthrows": "^1.1.1", "ob1": "0.83.5", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-VT9bb2KO2/4tWY9Z2yeZqTUao7CicKAOps9LUg2aQzsz+04QyuXL3qgf1cLUVRjA/D6G5u1RJAlN1w9VNHtODQ=="],
"metro-symbolicate": ["metro-symbolicate@0.83.3", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.3", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw=="],
@@ -2972,7 +2981,7 @@
"nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="],
- "ob1": ["ob1@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-9JiflaRKCkxKzH8uuZlax72cHzZ8iFLsNIORFOAKDgZUOfvfwYWOVS0ezGLzPp/yEhVktD+PTTImC0AAehSOBw=="],
+ "ob1": ["ob1@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-vNKPYC8L5ycVANANpF/S+WZHpfnRWKx/F3AYP4QMn6ZJTh+l2HOrId0clNkEmua58NB9vmI9Qh7YOoV/4folYg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
@@ -3126,7 +3135,7 @@
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
- "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
+ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -3178,7 +3187,7 @@
"react-native-screens": ["react-native-screens@4.24.0", "", { "dependencies": { "react-freeze": "^1.0.0", "warn-once": "^0.1.0" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-SyoiGaDofiyGPFrUkn1oGsAzkRuX1JUvTD9YQQK3G1JGQ5VWkvHgYSsc1K9OrLsDQxN7NmV71O0sHCAh8cBetA=="],
- "react-native-sensitive-info": ["react-native-sensitive-info@6.0.0-rc.11", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-nBVUcjXK4T2KjdH+nIZaCgS0HbX8AtiOWSvAdkZEoOvnUxpo+l+r9dvSWcIPbhj/EHLiZmTM4WbEATLokwe5tQ=="],
+ "react-native-sensitive-info": ["react-native-sensitive-info@6.0.0-rc.12", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-3G0W+AZEt79tfN1SAJsIZHB6wSe5o5fKfvbSqsPSutcHB3lV3ebgNvNu6PK44YmSmSdYU5hL3J3ceTXOgkOjXA=="],
"react-native-svg": ["react-native-svg@15.15.3", "", { "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3", "warn-once": "0.1.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-/k4KYwPBLGcx2f5d4FjE+vCScK7QOX14cl2lIASJ28u4slHHtIhL0SZKU7u9qmRBHxTCKPoPBtN6haT1NENJNA=="],
@@ -3398,7 +3407,7 @@
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
- "slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
+ "slice-ansi": ["slice-ansi@8.0.0", "", { "dependencies": { "ansi-styles": "^6.2.3", "is-fullwidth-code-point": "^5.1.0" } }, "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg=="],
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
@@ -3968,11 +3977,11 @@
"@react-native/community-cli-plugin/@react-native/dev-middleware": ["@react-native/dev-middleware@0.84.1", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.84.1", "@react-native/debugger-shell": "0.84.1", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.2.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-Z83ra+Gk6ElAhH3XRrv3vwbwCPTb04sPPlNpotxcFZb5LtRQZwT91ZQEXw3GOJCVIFp9EQ/gj8AQbVvtHKOUlQ=="],
- "@react-native/community-cli-plugin/metro": ["metro@0.83.4", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.33.3", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.4", "metro-cache": "0.83.4", "metro-cache-key": "0.83.4", "metro-config": "0.83.4", "metro-core": "0.83.4", "metro-file-map": "0.83.4", "metro-resolver": "0.83.4", "metro-runtime": "0.83.4", "metro-source-map": "0.83.4", "metro-symbolicate": "0.83.4", "metro-transform-plugins": "0.83.4", "metro-transform-worker": "0.83.4", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-eBkAtcob+YmvSLL+/rsFiK8dHNfDbQA2/pi0lnxg3E6LLtUpwDfdGJ9WBWXkj0PVeOhoWQyj9Rt7s/+6k/GXuA=="],
+ "@react-native/community-cli-plugin/metro": ["metro@0.83.5", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.33.3", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-config": "0.83.5", "metro-core": "0.83.5", "metro-file-map": "0.83.5", "metro-resolver": "0.83.5", "metro-runtime": "0.83.5", "metro-source-map": "0.83.5", "metro-symbolicate": "0.83.5", "metro-transform-plugins": "0.83.5", "metro-transform-worker": "0.83.5", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-BgsXevY1MBac/3ZYv/RfNFf/4iuW9X7f4H8ZNkiH+r667HD9sVujxcmu4jvEzGCAm4/WyKdZCuyhAcyhTHOucQ=="],
- "@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.4", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.4", "metro-cache": "0.83.4", "metro-core": "0.83.4", "metro-runtime": "0.83.4", "yaml": "^2.6.1" } }, "sha512-ydOgMNI9aT8l2LOTOugt1FvC7getPKG9uJo9Vclg9/RWJxbwkBF/FMBm6w5gH8NwJokSmQrbNkojXPn7nm0kGw=="],
+ "@react-native/community-cli-plugin/metro-config": ["metro-config@0.83.5", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.83.5", "metro-cache": "0.83.5", "metro-core": "0.83.5", "metro-runtime": "0.83.5", "yaml": "^2.6.1" } }, "sha512-JQ/PAASXH7yczgV6OCUSRhZYME+NU8NYjI2RcaG5ga4QfQ3T/XdiLzpSb3awWZYlDCcQb36l4Vl7i0Zw7/Tf9w=="],
- "@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.4" } }, "sha512-EE+j/imryd3og/6Ly9usku9vcTLQr2o4IDax/izsr6b0HRqZK9k6f5SZkGkOPqnsACLq6csPCx+2JsgF9DkVbw=="],
+ "@react-native/community-cli-plugin/metro-core": ["metro-core@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.83.5" } }, "sha512-YcVcLCrf0ed4mdLa82Qob0VxYqfhmlRxUS8+TO4gosZo/gLwSvtdeOjc/Vt0pe/lvMNrBap9LlmvZM8FIsMgJQ=="],
"@react-native/dev-middleware/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
@@ -4112,7 +4121,7 @@
"dmg-license/ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
- "electron/@types/node": ["@types/node@24.10.15", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-BgjLoRuSr0MTI5wA6gMw9Xy0sFudAaUuvrnjgGx9wZ522fYYLA5SYJ+1Y30vTcJEG+DRCyDHx/gzQVfofYzSdg=="],
+ "electron/@types/node": ["@types/node@24.11.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-fPxQqz4VTgPI/IQ+lj9r0h+fDR66bzoeMGHp8ASee+32OSGIkeASsoZuJixsQoVef1QJbeubcPBxKk22QVoWdw=="],
"electron-builder/ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
@@ -4184,6 +4193,8 @@
"log-update/cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="],
+ "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="],
+
"log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"log-update/wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
@@ -4210,7 +4221,7 @@
"metro-file-map/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="],
- "metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.4", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-clyWAXDgkDHPwvldl95pcLTrJIqUj9GbZayL8tfeUs69ilsIUBpVym2lRd/8l3/8PIHCInxL868NvD2Y7OqKXg=="],
+ "metro-source-map/metro-symbolicate": ["metro-symbolicate@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA=="],
"metro-symbolicate/metro-source-map": ["metro-source-map@0.83.3", "", { "dependencies": { "@babel/traverse": "^7.25.3", "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", "@babel/types": "^7.25.2", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.83.3", "nullthrows": "^1.1.1", "ob1": "0.83.3", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg=="],
@@ -4420,29 +4431,29 @@
"@react-native/community-cli-plugin/metro/jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="],
- "@react-native/community-cli-plugin/metro/metro-babel-transformer": ["metro-babel-transformer@0.83.4", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.33.3", "nullthrows": "^1.1.1" } }, "sha512-xfNtsYIigybqm9xVL3ygTYYNFyYTMf2lGg/Wt+znVGtwcjXoRPG80WlL5SS09ZjYVei3MoE920i7MNr7ukSULA=="],
+ "@react-native/community-cli-plugin/metro/metro-babel-transformer": ["metro-babel-transformer@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.33.3", "nullthrows": "^1.1.1" } }, "sha512-d9FfmgUEVejTiSb7bkQeLRGl6aeno2UpuPm3bo3rCYwxewj03ymvOn8s8vnS4fBqAPQ+cE9iQM40wh7nGXR+eA=="],
- "@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.83.4", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.4" } }, "sha512-Pm6CiksVms0cZNDDe/nFzYr1xpXzJLOSwvOjl4b3cYtXxEFllEjD6EeBgoQK5C8yk7U54PcuRaUAFSvJ+eCKbg=="],
+ "@react-native/community-cli-plugin/metro/metro-cache": ["metro-cache@0.83.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.5" } }, "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng=="],
- "@react-native/community-cli-plugin/metro/metro-cache-key": ["metro-cache-key@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Y8E6mm1alkYIRzmfkOdrwXMzJ4HKANYiZE7J2d3iYTwmnLIQG+aoIpvla+bo6LRxH1Gm3qjEiOl+LbxvPCzIug=="],
+ "@react-native/community-cli-plugin/metro/metro-cache-key": ["metro-cache-key@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-Ycl8PBajB7bhbAI7Rt0xEyiF8oJ0RWX8EKkolV1KfCUlC++V/GStMSGpPLwnnBZXZWkCC5edBPzv1Hz1Yi0Euw=="],
- "@react-native/community-cli-plugin/metro/metro-file-map": ["metro-file-map@0.83.4", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-RSZLpGQhW9topefjJ9dp77Ff7BP88b17sb/YjxLHC1/H0lJVYYC9Cgqua21Vxe4RUJK2z64hw72g+ySLGTCawA=="],
+ "@react-native/community-cli-plugin/metro/metro-file-map": ["metro-file-map@0.83.5", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-ZEt8s3a1cnYbn40nyCD+CsZdYSlwtFh2kFym4lo+uvfM+UMMH+r/BsrC6rbNClSrt+B7rU9T+Te/sh/NL8ZZKQ=="],
- "@react-native/community-cli-plugin/metro/metro-resolver": ["metro-resolver@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-drWdylyNqgdaJufz0GjU/ielv2hjcc6piegjjJwKn8l7A/72aLQpUpOHtP+GMR+kOqhSsD4MchhJ6PSANvlSEw=="],
+ "@react-native/community-cli-plugin/metro/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="],
- "@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.4", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-clyWAXDgkDHPwvldl95pcLTrJIqUj9GbZayL8tfeUs69ilsIUBpVym2lRd/8l3/8PIHCInxL868NvD2Y7OqKXg=="],
+ "@react-native/community-cli-plugin/metro/metro-symbolicate": ["metro-symbolicate@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.83.5", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-EMIkrjNRz/hF+p0RDdxoE60+dkaTLPN3vaaGkFmX5lvFdO6HPfHA/Ywznzkev+za0VhPQ5KSdz49/MALBRteHA=="],
- "@react-native/community-cli-plugin/metro/metro-transform-plugins": ["metro-transform-plugins@0.83.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-c0ROVcyvdaGPUFIg2N5nEQF4xbsqB2p1PPPhVvK1d/Y7ZhBAFiwQ75so0SJok32q+I++lc/hq7IdPCp2frPGQg=="],
+ "@react-native/community-cli-plugin/metro/metro-transform-plugins": ["metro-transform-plugins@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-KxYKzZL+lt3Os5H2nx7YkbkWVduLZL5kPrE/Yq+Prm/DE1VLhpfnO6HtPs8vimYFKOa58ncl60GpoX0h7Wm0Vw=="],
- "@react-native/community-cli-plugin/metro/metro-transform-worker": ["metro-transform-worker@0.83.4", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.4", "metro-babel-transformer": "0.83.4", "metro-cache": "0.83.4", "metro-cache-key": "0.83.4", "metro-minify-terser": "0.83.4", "metro-source-map": "0.83.4", "metro-transform-plugins": "0.83.4", "nullthrows": "^1.1.1" } }, "sha512-6I81IZLeU/0ww7OBgCPALFl0OE0FQwvIuKCtuViSiKufmislF7kVr7IHH9GYtQuZcnualQ82gYeQ11KzZQTouw=="],
+ "@react-native/community-cli-plugin/metro/metro-transform-worker": ["metro-transform-worker@0.83.5", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.83.5", "metro-babel-transformer": "0.83.5", "metro-cache": "0.83.5", "metro-cache-key": "0.83.5", "metro-minify-terser": "0.83.5", "metro-source-map": "0.83.5", "metro-transform-plugins": "0.83.5", "nullthrows": "^1.1.1" } }, "sha512-8N4pjkNXc6ytlP9oAM6MwqkvUepNSW39LKYl9NjUMpRDazBQ7oBpQDc8Sz4aI8jnH6AGhF7s1m/ayxkN1t04yA=="],
"@react-native/community-cli-plugin/metro/mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="],
"@react-native/community-cli-plugin/metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
- "@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.4", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.4" } }, "sha512-Pm6CiksVms0cZNDDe/nFzYr1xpXzJLOSwvOjl4b3cYtXxEFllEjD6EeBgoQK5C8yk7U54PcuRaUAFSvJ+eCKbg=="],
+ "@react-native/community-cli-plugin/metro-config/metro-cache": ["metro-cache@0.83.5", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.83.5" } }, "sha512-oH+s4U+IfZyg8J42bne2Skc90rcuESIYf86dYittcdWQtPfcaFXWpByPyTuWk3rR1Zz3Eh5HOrcVImfEhhJLng=="],
- "@react-native/community-cli-plugin/metro-core/metro-resolver": ["metro-resolver@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-drWdylyNqgdaJufz0GjU/ielv2hjcc6piegjjJwKn8l7A/72aLQpUpOHtP+GMR+kOqhSsD4MchhJ6PSANvlSEw=="],
+ "@react-native/community-cli-plugin/metro-core/metro-resolver": ["metro-resolver@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-7p3GtzVUpbAweJeCcUJihJeOQl1bDuimO5ueo1K0BUpUtR41q5EilbQ3klt16UTPPMpA+tISWBtsrqU556mY1A=="],
"@rollup/plugin-babel/@rollup/pluginutils/@types/estree": ["@types/estree@0.0.39", "", {}, "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="],
@@ -4608,6 +4619,10 @@
"log-update/cli-cursor/restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="],
+ "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
+
+ "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="],
+
"log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
@@ -4830,7 +4845,7 @@
"@react-native/community-cli-plugin/metro/jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="],
- "@react-native/community-cli-plugin/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.4", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-KmZnpxfj0nPIRkbBNTc6xul5f5GPvWL5kQ1UkisB7qFkgh6+UiJG+L4ukJ2sK7St6+8Za/Cb68MUEYkUouIYcQ=="],
+ "@react-native/community-cli-plugin/metro/metro-transform-worker/metro-minify-terser": ["metro-minify-terser@0.83.5", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-Toe4Md1wS1PBqbvB0cFxBzKEVyyuYTUb0sgifAZh/mSvLH84qA1NAWik9sISWatzvfWf3rOGoUoO5E3f193a3Q=="],
"@react-native/community-cli-plugin/metro/mime-types/mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
diff --git a/examples/react-nextjs/package.json b/examples/react-nextjs/package.json
index 4e6c7f163..8ae01ea4d 100644
--- a/examples/react-nextjs/package.json
+++ b/examples/react-nextjs/package.json
@@ -21,7 +21,7 @@
"devDependencies": {
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/postcss": "^4.2.1",
- "@types/node": "^25.3.2",
+ "@types/node": "^25.3.3",
"@types/react": "~19.2.14",
"@types/react-dom": "~19.2.3",
"postcss": "^8.5.6",
diff --git a/packages/common/src/Array.ts b/packages/common/src/Array.ts
index 9b13a07c6..832f497b2 100644
--- a/packages/common/src/Array.ts
+++ b/packages/common/src/Array.ts
@@ -191,6 +191,35 @@ export function arrayFrom(
: [...iterableOrLength];
}
+/**
+ * Better `Array.fromAsync`.
+ *
+ * Returns a readonly array and awaits promised items from sync or async
+ * iterables.
+ *
+ * ### Example
+ *
+ * ```ts
+ * await arrayFromAsync(new Set([1, 2, 3])); // ReadonlyArray
+ * await arrayFromAsync(
+ * (async function* () {
+ * yield Promise.resolve(1);
+ * yield Promise.resolve(2);
+ * })(),
+ * ); // [1, 2]
+ * ```
+ *
+ * Unlike `Array.fromAsync`, there's no map parameter — map the result with
+ * {@link mapArray} or use
+ * {@link https://web.dev/blog/baseline-iterator-helpers | iterator helpers}
+ * directly on iterables.
+ *
+ * @group Constructors
+ */
+export const arrayFromAsync = async (
+ iterable: AsyncIterable | Iterable>,
+): Promise> => Array.fromAsync(iterable);
+
/**
* Checks if an array is non-empty and narrows its type to {@link NonEmptyArray}
* or {@link NonEmptyReadonlyArray} based on the input.
diff --git a/packages/common/src/Polyfills.ts b/packages/common/src/Polyfills.ts
index 1f9c0cfb2..b1889772e 100644
--- a/packages/common/src/Polyfills.ts
+++ b/packages/common/src/Polyfills.ts
@@ -8,10 +8,20 @@ import "disposablestack/auto";
import { lazyVoid } from "./Function.js";
/**
- * Installs polyfills for resource management.
+ * Installs polyfills required by `@evolu/common`.
*
- * Polyfills `Symbol.dispose`, `Symbol.asyncDispose`, `DisposableStack`,
- * `AsyncDisposableStack`, and `SuppressedError`.
+ * Installs resource-management polyfills (`Symbol.dispose`,
+ * `Symbol.asyncDispose`, `DisposableStack`, `AsyncDisposableStack`, and
+ * `SuppressedError`), which are not yet supported by Safari and React Native.
+ *
+ * Evolu currently does not require any additional polyfills. If that changes,
+ * this is where they will be installed.
+ *
+ * `@evolu/react-native` has its own `Polyfills` module and its
+ * `installPolyfills` calls this function first, then installs React Native
+ * specific polyfills.
+ *
+ * Call this explicitly from the app entry point.
*
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Resource_management
* @see https://github.com/es-shims/DisposableStack
diff --git a/packages/common/test/Array.test.ts b/packages/common/test/Array.test.ts
index 37b5916ff..0251e8313 100644
--- a/packages/common/test/Array.test.ts
+++ b/packages/common/test/Array.test.ts
@@ -2,6 +2,7 @@ import { describe, expect, expectTypeOf, test } from "vitest";
import {
appendToArray,
arrayFrom,
+ arrayFromAsync,
concatArrays,
dedupeArray,
emptyArray,
@@ -92,6 +93,36 @@ describe("Constants", () => {
expect(result).toEqual([0, 10, 20, 30]);
});
});
+
+ describe("arrayFromAsync", () => {
+ test("creates array from async iterable", async () => {
+ const asyncIterable = {
+ async *[Symbol.asyncIterator]() {
+ yield await Promise.resolve(1);
+ yield await Promise.resolve(2);
+ yield await Promise.resolve(3);
+ },
+ };
+
+ const result = await arrayFromAsync(asyncIterable);
+ expect(result).toEqual([1, 2, 3]);
+ });
+
+ test("awaits promised values from sync iterable", async () => {
+ const result = await arrayFromAsync([
+ Promise.resolve(1),
+ Promise.resolve(2),
+ Promise.resolve(3),
+ ]);
+
+ expect(result).toEqual([1, 2, 3]);
+ });
+
+ test("returns readonly array", async () => {
+ const result = await arrayFromAsync([Promise.resolve("x")]);
+ expectTypeOf(result).toEqualTypeOf>();
+ });
+ });
});
describe("Type Guards", () => {
diff --git a/packages/common/test/_browserSetup.ts b/packages/common/test/_browserSetup.ts
index 8b37a1d4d..63a3ecce8 100644
--- a/packages/common/test/_browserSetup.ts
+++ b/packages/common/test/_browserSetup.ts
@@ -1,4 +1,2 @@
-// Ensure polyfills are loaded before any tests run in browsers that need them
-import { installPolyfills } from "../src/Polyfills.js";
-
-installPolyfills();
+// Ensure resource-management polyfills are loaded before browser tests run.
+import "disposablestack/auto";
diff --git a/packages/nodejs/package.json b/packages/nodejs/package.json
index 45b04b888..7691d64dc 100644
--- a/packages/nodejs/package.json
+++ b/packages/nodejs/package.json
@@ -35,7 +35,7 @@
"@evolu/common": "workspace:*",
"@evolu/tsconfig": "workspace:*",
"@types/better-sqlite3": "^7.6.13",
- "@types/node": "^25.3.2",
+ "@types/node": "^25.3.3",
"@types/ws": "^8.18.1",
"typescript": "^5.9.3"
},
diff --git a/packages/react-native/package.json b/packages/react-native/package.json
index 619eb5d52..dd41a4001 100644
--- a/packages/react-native/package.json
+++ b/packages/react-native/package.json
@@ -24,6 +24,11 @@
"react-native": "./dist/src/index.js",
"default": "./dist/src/index.js"
},
+ "./Polyfills": {
+ "types": "./dist/src/Polyfills.d.ts",
+ "react-native": "./dist/src/Polyfills.js",
+ "default": "./dist/src/Polyfills.js"
+ },
"./expo-sqlite": {
"types": "./dist/src/exports/expo-sqlite.d.ts",
"react-native": "./dist/src/exports/expo-sqlite.js",
@@ -48,6 +53,9 @@
".": [
"./dist/src/index.d.ts"
],
+ "Polyfills": [
+ "./dist/src/Polyfills.d.ts"
+ ],
"expo-sqlite": [
"./dist/src/exports/expo-sqlite.d.ts"
],
@@ -69,6 +77,15 @@
"prepack": "bun run build",
"clean": "rimraf .turbo node_modules dist coverage"
},
+ "dependencies": {
+ "set.prototype.difference": "^1.1.7",
+ "set.prototype.intersection": "^1.1.8",
+ "set.prototype.isdisjointfrom": "^1.1.5",
+ "set.prototype.issubsetof": "^1.1.4",
+ "set.prototype.issupersetof": "^1.1.3",
+ "set.prototype.symmetricdifference": "^1.1.3",
+ "set.prototype.union": "^1.1.3"
+ },
"devDependencies": {
"@evolu/common": "workspace:*",
"@evolu/react": "workspace:*",
@@ -81,7 +98,7 @@
"react": "19.2.4",
"react-native": "0.84.1",
"react-native-nitro-modules": "0.34.1",
- "react-native-sensitive-info": "6.0.0-rc.11",
+ "react-native-sensitive-info": "6.0.0-rc.12",
"react-native-svg": "15.15.3",
"typescript": "^5.9.3"
},
diff --git a/packages/react-native/src/Polyfills.ts b/packages/react-native/src/Polyfills.ts
new file mode 100644
index 000000000..ea58f63ba
--- /dev/null
+++ b/packages/react-native/src/Polyfills.ts
@@ -0,0 +1,323 @@
+import { installPolyfills as installCommonPolyfills } from "@evolu/common";
+import difference from "set.prototype.difference";
+import intersection from "set.prototype.intersection";
+import isDisjointFrom from "set.prototype.isdisjointfrom";
+import isSubsetOf from "set.prototype.issubsetof";
+import isSupersetOf from "set.prototype.issupersetof";
+import symmetricDifference from "set.prototype.symmetricdifference";
+import union from "set.prototype.union";
+
+let areSetPolyfillsInstalled = false;
+
+const installSetPolyfills = (): void => {
+ if (areSetPolyfillsInstalled) return;
+
+ difference.shim();
+ intersection.shim();
+ isDisjointFrom.shim();
+ isSubsetOf.shim();
+ isSupersetOf.shim();
+ symmetricDifference.shim();
+ union.shim();
+
+ areSetPolyfillsInstalled = true;
+};
+
+/** Installs polyfills required by Evolu in React Native runtimes. */
+export const installPolyfills = (): void => {
+ installCommonPolyfills();
+ installSetPolyfills();
+ installPromisePolyfills();
+ installAbortControllerPolyfills();
+};
+
+const installPromisePolyfills = () => {
+ const PromiseStatic = Promise as PromiseConstructor & {
+ withResolvers?: () => PromiseWithResolvers;
+ try?: (
+ func: (...args: ReadonlyArray) => unknown,
+ ...args: ReadonlyArray
+ ) => Promise;
+ };
+
+ // @see https://github.com/facebook/hermes/pull/1452
+ if (typeof PromiseStatic.withResolvers !== "function") {
+ PromiseStatic.withResolvers = () => {
+ let resolve: (value: T | PromiseLike) => void = () => {};
+ let reject: (reason?: unknown) => void = () => {};
+ const promise = new Promise((res, rej) => {
+ resolve = res;
+ reject = rej;
+ });
+ return { promise, resolve, reject };
+ };
+ }
+
+ // @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/try
+ if (typeof PromiseStatic.try !== "function") {
+ PromiseStatic.try = (
+ func: (...args: ReadonlyArray) => unknown,
+ ...args: ReadonlyArray
+ ): Promise =>
+ new Promise((resolve, reject) => {
+ try {
+ resolve(func(...args));
+ } catch (error) {
+ reject(error);
+ }
+ });
+ }
+};
+
+interface AbortControllerConstructor {
+ new (): AbortController;
+ readonly prototype: AbortController;
+}
+
+interface AbortSignalConstructor {
+ readonly prototype: AbortSignal;
+ abort?: (reason?: unknown) => AbortSignal;
+ timeout?: (milliseconds: number) => AbortSignal;
+ any?: (signals: Array) => AbortSignal;
+}
+
+interface AbortControllerPrototype extends AbortController {
+ abort: (reason?: unknown) => void;
+ readonly __evoluAbortReasonPatched?: true;
+}
+
+interface AbortSignalWithReason extends AbortSignal {
+ readonly reason: unknown;
+}
+
+interface AnyControllersRegistry {
+ readonly add: (controller: AbortController) => void;
+ readonly remove: (controller: AbortController) => void;
+}
+
+interface WeakRefLike {
+ deref(): T | undefined;
+}
+
+type ControllerRef = WeakRefLike;
+
+const abortReasonBySignal = new WeakMap();
+const anyControllersBySignal = new WeakMap<
+ AbortSignal,
+ AnyControllersRegistry
+>();
+
+const installAbortControllerPolyfills = (): void => {
+ installAbortReasonPolyfill(
+ globalThis.AbortController as AbortControllerConstructor,
+ globalThis.AbortSignal as AbortSignalConstructor,
+ );
+ installAbortSignalStaticMethods(
+ globalThis.AbortController as AbortControllerConstructor,
+ globalThis.AbortSignal as AbortSignalConstructor,
+ );
+};
+
+const installAbortReasonPolyfill = (
+ abortController: AbortControllerConstructor,
+ abortSignal: AbortSignalConstructor,
+): void => {
+ const hasNativeReason = "reason" in abortSignal.prototype;
+ if (hasNativeReason) return;
+
+ Object.defineProperty(abortSignal.prototype, "reason", {
+ configurable: true,
+ enumerable: false,
+ get(this: AbortSignal): unknown {
+ return abortReasonBySignal.get(this);
+ },
+ });
+
+ const prototype = abortController.prototype as AbortControllerPrototype;
+ if (prototype.__evoluAbortReasonPatched) return;
+
+ const nativeAbort = prototype.abort;
+ prototype.abort = function (this: AbortController, reason?: unknown): void {
+ const signal = this.signal;
+ const normalizedReason =
+ abortReasonBySignal.get(signal) ??
+ (reason === undefined ? createAbortError() : reason);
+
+ if (!abortReasonBySignal.has(signal)) {
+ abortReasonBySignal.set(signal, normalizedReason);
+ }
+
+ nativeAbort.call(this, normalizedReason);
+ };
+
+ Object.defineProperty(prototype, "__evoluAbortReasonPatched", {
+ value: true,
+ configurable: true,
+ enumerable: false,
+ writable: false,
+ });
+};
+
+const installAbortSignalStaticMethods = (
+ abortController: AbortControllerConstructor,
+ abortSignal: AbortSignalConstructor,
+): void => {
+ if (typeof abortSignal.abort !== "function") {
+ Object.defineProperty(abortSignal, "abort", {
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ value: (reason?: unknown): AbortSignal => {
+ const controller = new abortController();
+ controller.abort(reason);
+ return controller.signal;
+ },
+ });
+ }
+
+ if (typeof abortSignal.timeout !== "function") {
+ Object.defineProperty(abortSignal, "timeout", {
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ value: (milliseconds: number): AbortSignal => {
+ const controller = new abortController();
+ const timeoutId = globalThis.setTimeout(() => {
+ controller.abort(createTimeoutError(milliseconds));
+ }, milliseconds);
+
+ controller.signal.addEventListener(
+ "abort",
+ () => {
+ globalThis.clearTimeout(timeoutId);
+ },
+ { once: true },
+ );
+
+ return controller.signal;
+ },
+ });
+ }
+
+ if (typeof abortSignal.any !== "function") {
+ Object.defineProperty(abortSignal, "any", {
+ configurable: true,
+ enumerable: false,
+ writable: true,
+ value: (signals: ReadonlyArray): AbortSignal =>
+ createAbortSignalAny(abortController, signals),
+ });
+ }
+};
+
+const createAbortSignalAny = (
+ abortController: AbortControllerConstructor,
+ signals: ReadonlyArray,
+): AbortSignal => {
+ const controller = new abortController();
+ if (signals.length === 0) return controller.signal;
+
+ for (const signal of signals) {
+ if (signal.aborted) {
+ controller.abort(readSignalReason(signal));
+ return controller.signal;
+ }
+ }
+
+ const sources = Array.from(new Set(signals));
+
+ for (const signal of sources) {
+ const registry =
+ anyControllersBySignal.get(signal) ??
+ createAnyControllersRegistry(signal);
+ anyControllersBySignal.set(signal, registry);
+ registry.add(controller);
+ }
+
+ controller.signal.addEventListener(
+ "abort",
+ () => {
+ for (const signal of sources) {
+ anyControllersBySignal.get(signal)?.remove(controller);
+ }
+ },
+ { once: true },
+ );
+
+ return controller.signal;
+};
+
+const createAnyControllersRegistry = (
+ sourceSignal: AbortSignal,
+): AnyControllersRegistry => {
+ let refs: Array = [];
+
+ const cleanup = (): void => {
+ refs = refs.filter((ref) => {
+ const controller = ref.deref();
+ return controller != null && !controller.signal.aborted;
+ });
+
+ if (refs.length === 0) {
+ sourceSignal.removeEventListener("abort", onAbort);
+ anyControllersBySignal.delete(sourceSignal);
+ }
+ };
+
+ const onAbort = (): void => {
+ const reason = readSignalReason(sourceSignal);
+
+ for (const ref of refs) {
+ const controller = ref.deref();
+ if (!controller || controller.signal.aborted) continue;
+ controller.abort(reason);
+ }
+
+ refs = [];
+ sourceSignal.removeEventListener("abort", onAbort);
+ anyControllersBySignal.delete(sourceSignal);
+ };
+
+ sourceSignal.addEventListener("abort", onAbort, { once: true });
+
+ return {
+ add: (controller) => {
+ refs.push(new globalThis.WeakRef(controller));
+ cleanup();
+ },
+ remove: (controller) => {
+ refs = refs.filter((ref) => {
+ const candidate = ref.deref();
+ return candidate != null && candidate !== controller;
+ });
+ cleanup();
+ },
+ };
+};
+
+const readSignalReason = (signal: AbortSignal): unknown => {
+ if ("reason" in signal) {
+ return (signal as AbortSignalWithReason).reason;
+ }
+ return createAbortError();
+};
+
+const createTimeoutError = (milliseconds: number): Error =>
+ createNamedError("TimeoutError", `signal timed out after ${milliseconds} ms`);
+
+const createAbortError = (): Error =>
+ createNamedError("AbortError", "This operation was aborted");
+
+const createNamedError = (name: string, message: string): Error => {
+ if (typeof globalThis.DOMException === "function") {
+ try {
+ return new globalThis.DOMException(message, name);
+ } catch {
+ // Some runtimes expose DOMException but cannot construct it reliably.
+ }
+ }
+
+ const error = new Error(message) as Error & { name: string };
+ error.name = name;
+ return error;
+};
diff --git a/packages/react-native/src/SetPrototypeShimModules.d.ts b/packages/react-native/src/SetPrototypeShimModules.d.ts
new file mode 100644
index 000000000..4216ee194
--- /dev/null
+++ b/packages/react-native/src/SetPrototypeShimModules.d.ts
@@ -0,0 +1,38 @@
+interface SetPrototypeShimModule {
+ shim: () => void;
+}
+
+declare module "set.prototype.difference" {
+ const difference: SetPrototypeShimModule;
+ export default difference;
+}
+
+declare module "set.prototype.intersection" {
+ const intersection: SetPrototypeShimModule;
+ export default intersection;
+}
+
+declare module "set.prototype.isdisjointfrom" {
+ const isDisjointFrom: SetPrototypeShimModule;
+ export default isDisjointFrom;
+}
+
+declare module "set.prototype.issubsetof" {
+ const isSubsetOf: SetPrototypeShimModule;
+ export default isSubsetOf;
+}
+
+declare module "set.prototype.issupersetof" {
+ const isSupersetOf: SetPrototypeShimModule;
+ export default isSupersetOf;
+}
+
+declare module "set.prototype.symmetricdifference" {
+ const symmetricDifference: SetPrototypeShimModule;
+ export default symmetricDifference;
+}
+
+declare module "set.prototype.union" {
+ const union: SetPrototypeShimModule;
+ export default union;
+}
diff --git a/packages/react-native/src/index.ts b/packages/react-native/src/index.ts
index 962d6add3..a9c3b86d7 100644
--- a/packages/react-native/src/index.ts
+++ b/packages/react-native/src/index.ts
@@ -1,3 +1,4 @@
export * from "./components/EvoluIdenticon.js";
+export * from "./Polyfills.js";
export * from "./Task.js";
export * from "./Worker.js";
diff --git a/packages/react-native/src/shared.ts b/packages/react-native/src/shared.ts
index 1dabbb86b..df3032b44 100644
--- a/packages/react-native/src/shared.ts
+++ b/packages/react-native/src/shared.ts
@@ -1,11 +1,13 @@
import {
type ConsoleDep,
type CreateSqliteDriverDep,
+ createConsole,
createConsoleStoreOutput,
createInMemoryLeaderLock,
createLocalAuth,
createRandomBytes,
createRun,
+ createWebSocket,
type LocalAuth,
type ReloadAppDep,
type SecureStorage,
@@ -36,24 +38,34 @@ const leaderLock = createInMemoryLeaderLock();
export const createEvoluDeps = (
deps: ReloadAppDep & CreateSqliteDriverDep & Partial,
): EvoluDeps => {
- const consoleStoreOutput = createConsoleStoreOutput();
-
// Worker-side Run lives as long as the app. When RN supports real workers,
// this moves to the worker entry point (like web's Worker.worker.ts).
- const workerRun = createRun({
- consoleStoreOutputEntry: consoleStoreOutput.entry,
- createMessagePort,
- createSqliteDriver: deps.createSqliteDriver,
- leaderLock,
- });
+ const createWorkerRun = () => {
+ const consoleStoreOutput = createConsoleStoreOutput();
+ const workerConsole = createConsole({
+ output: consoleStoreOutput,
+ ...(deps.console && { level: deps.console.getLevel() }),
+ });
+
+ return createRun({
+ console: workerConsole,
+ consoleStoreOutputEntry: consoleStoreOutput.entry,
+ createMessagePort,
+ createWebSocket,
+ createSqliteDriver: deps.createSqliteDriver,
+ leaderLock,
+ });
+ };
const createDbWorker: CreateDbWorker = (): DbWorker =>
createWorker((self) => {
- workerRun(initDbWorker(self));
+ const dbWorkerRun = createWorkerRun();
+ dbWorkerRun(initDbWorker(self));
});
const sharedWorker = createSharedWorker((self) => {
- workerRun(initSharedWorker(self));
+ const sharedWorkerRun = createWorkerRun();
+ sharedWorkerRun(initSharedWorker(self));
});
return createCommonEvoluDeps({
diff --git a/packages/react-native/test/Polyfills.test.ts b/packages/react-native/test/Polyfills.test.ts
new file mode 100644
index 000000000..12b3cf1c4
--- /dev/null
+++ b/packages/react-native/test/Polyfills.test.ts
@@ -0,0 +1,554 @@
+import { afterEach, beforeEach, describe, expect, test } from "vitest";
+import { installPolyfills } from "../src/Polyfills.js";
+
+interface GlobalAbort {
+ readonly AbortController: typeof globalThis.AbortController | undefined;
+ readonly AbortSignal: typeof globalThis.AbortSignal | undefined;
+ readonly DOMException: typeof globalThis.DOMException | undefined;
+}
+
+interface PromiseStatics {
+ withResolvers?: () => {
+ promise: Promise;
+ resolve: (value: unknown) => void;
+ reject: (reason?: unknown) => void;
+ };
+ try?: (
+ func: (...args: ReadonlyArray) => unknown,
+ ...args: ReadonlyArray
+ ) => Promise;
+}
+
+type FakeAbortListener = () => void;
+
+interface FakeAbortSignal {
+ aborted: boolean;
+ addEventListener: (
+ type: string,
+ listener: FakeAbortListener,
+ options?: { readonly once?: boolean },
+ ) => void;
+ removeEventListener: (type: string, listener: FakeAbortListener) => void;
+ dispatchAbort: () => void;
+ getListenerCount: () => number;
+}
+
+const createFakeAbortRuntime = (): {
+ readonly AbortSignal: typeof AbortSignal;
+ readonly AbortController: typeof AbortController;
+ readonly getAbortCallCount: () => number;
+} => {
+ let abortCallCount = 0;
+
+ class TestAbortSignal {
+ public aborted = false;
+ private readonly listeners = new Set();
+
+ public addEventListener(
+ _type: string,
+ listener: FakeAbortListener,
+ _options?: { readonly once?: boolean },
+ ): void {
+ this.listeners.add(listener);
+ }
+
+ public removeEventListener(
+ _type: string,
+ listener: FakeAbortListener,
+ ): void {
+ this.listeners.delete(listener);
+ }
+
+ public dispatchAbort(): void {
+ if (this.aborted) return;
+ this.aborted = true;
+ for (const listener of this.listeners) listener();
+ }
+
+ public getListenerCount(): number {
+ return this.listeners.size;
+ }
+ }
+
+ class TestAbortController {
+ public readonly signal =
+ new TestAbortSignal() as unknown as AbortController["signal"];
+
+ public abort(): void {
+ abortCallCount += 1;
+ (this.signal as unknown as FakeAbortSignal).dispatchAbort();
+ }
+ }
+
+ return {
+ AbortController: TestAbortController as unknown as typeof AbortController,
+ AbortSignal: TestAbortSignal as unknown as typeof AbortSignal,
+ getAbortCallCount: () => abortCallCount,
+ };
+};
+
+const setAbortGlobals = (globals: GlobalAbort): void => {
+ if (globals.AbortController === undefined) {
+ delete (globalThis as { AbortController?: typeof AbortController })
+ .AbortController;
+ } else {
+ (
+ globalThis as { AbortController?: typeof AbortController }
+ ).AbortController = globals.AbortController;
+ }
+
+ if (globals.AbortSignal === undefined) {
+ delete (globalThis as { AbortSignal?: typeof AbortSignal }).AbortSignal;
+ } else {
+ (globalThis as { AbortSignal?: typeof AbortSignal }).AbortSignal =
+ globals.AbortSignal;
+ }
+
+ if (globals.DOMException === undefined) {
+ delete (globalThis as { DOMException?: typeof DOMException }).DOMException;
+ } else {
+ (globalThis as { DOMException?: typeof DOMException }).DOMException =
+ globals.DOMException;
+ }
+};
+
+describe("installPolyfills", () => {
+ let originalGlobals: GlobalAbort;
+ let originalPromiseWithResolvers: PropertyDescriptor | undefined;
+ let originalPromiseTry: PropertyDescriptor | undefined;
+
+ beforeEach(() => {
+ originalGlobals = {
+ AbortController: globalThis.AbortController,
+ AbortSignal: globalThis.AbortSignal,
+ DOMException: globalThis.DOMException,
+ };
+
+ originalPromiseWithResolvers = Object.getOwnPropertyDescriptor(
+ Promise,
+ "withResolvers",
+ );
+ originalPromiseTry = Object.getOwnPropertyDescriptor(Promise, "try");
+ });
+
+ afterEach(() => {
+ setAbortGlobals(originalGlobals);
+
+ if (originalPromiseWithResolvers === undefined) {
+ delete (Promise as PromiseStatics).withResolvers;
+ } else {
+ Object.defineProperty(
+ Promise,
+ "withResolvers",
+ originalPromiseWithResolvers,
+ );
+ }
+
+ if (originalPromiseTry === undefined) {
+ delete (Promise as PromiseStatics).try;
+ } else {
+ Object.defineProperty(Promise, "try", originalPromiseTry);
+ }
+ });
+
+ test("polyfills Promise.withResolvers", async () => {
+ delete (Promise as PromiseStatics).withResolvers;
+
+ installPolyfills();
+
+ const PromiseStatic = Promise as PromiseStatics;
+ expect(typeof PromiseStatic.withResolvers).toBe("function");
+
+ const withResolvers = PromiseStatic.withResolvers;
+ if (!withResolvers)
+ throw new Error("Promise.withResolvers was not polyfilled");
+
+ const { promise, resolve } = withResolvers();
+ resolve("ok");
+
+ await expect(promise).resolves.toBe("ok");
+ });
+
+ test("polyfills Promise.try and forwards arguments", async () => {
+ delete (Promise as PromiseStatics).try;
+
+ installPolyfills();
+
+ const PromiseStatic = Promise as PromiseStatics;
+ expect(typeof PromiseStatic.try).toBe("function");
+
+ const promiseTry = PromiseStatic.try;
+ if (!promiseTry) throw new Error("Promise.try was not polyfilled");
+
+ const result = await promiseTry(
+ (a, b) => `${String(a)}-${String(b)}`,
+ "a",
+ 1,
+ );
+ expect(result).toBe("a-1");
+ });
+
+ test("Promise.try rejects when callback throws", async () => {
+ delete (Promise as PromiseStatics).try;
+
+ installPolyfills();
+
+ const PromiseStatic = Promise as PromiseStatics;
+ const error = new Error("boom");
+ const promiseTry = PromiseStatic.try;
+ if (!promiseTry) throw new Error("Promise.try was not polyfilled");
+
+ await expect(
+ promiseTry(() => {
+ throw error;
+ }),
+ ).rejects.toBe(error);
+ });
+
+ test("does not override existing Promise static methods", () => {
+ const withResolvers = () => {
+ const promise = Promise.resolve("existing");
+ return {
+ promise,
+ resolve: () => undefined,
+ reject: () => undefined,
+ };
+ };
+ const promiseTry = () => Promise.resolve("existing");
+
+ (Promise as PromiseStatics).withResolvers = withResolvers;
+ (Promise as PromiseStatics).try = promiseTry;
+
+ installPolyfills();
+
+ const PromiseStatic = Promise as PromiseStatics;
+ expect(PromiseStatic.withResolvers).toBe(withResolvers);
+ expect(PromiseStatic.try).toBe(promiseTry);
+ });
+
+ test("polyfills reason propagation", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const controller = new globalThis.AbortController();
+ const reason = new Error("stop");
+ controller.abort(reason);
+
+ expect((controller.signal as { readonly reason: unknown }).reason).toBe(
+ reason,
+ );
+ });
+
+ test("creates AbortError reason when none is provided", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: undefined });
+
+ installPolyfills();
+
+ const controller = new globalThis.AbortController();
+ controller.abort();
+
+ const reason = (controller.signal as { readonly reason: Error }).reason;
+ expect(reason.name).toBe("AbortError");
+ expect(reason.message).toBe("This operation was aborted");
+ });
+
+ test("polyfills AbortSignal.abort", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ abort: (reason?: unknown) => AbortSignal;
+ }
+ ).abort("manual");
+
+ expect(signal.aborted).toBe(true);
+ expect((signal as { readonly reason: unknown }).reason).toBe("manual");
+ });
+
+ test("polyfills AbortSignal.timeout without DOMException", async () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: undefined });
+
+ installPolyfills();
+
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ timeout: (milliseconds: number) => AbortSignal;
+ }
+ ).timeout(1);
+
+ await new Promise((resolve) => globalThis.setTimeout(resolve, 5));
+
+ const reason = (signal as { readonly reason: Error }).reason;
+ expect(signal.aborted).toBe(true);
+ expect(reason.name).toBe("TimeoutError");
+ });
+
+ test("polyfills AbortSignal.any with first aborted reason", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const controller1 = new globalThis.AbortController();
+ const controller2 = new globalThis.AbortController();
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ }
+ ).any([controller1.signal, controller2.signal]);
+
+ const reason = new Error("cancelled");
+ controller2.abort(reason);
+
+ expect(signal.aborted).toBe(true);
+ expect((signal as { readonly reason: unknown }).reason).toBe(reason);
+ });
+
+ test("is idempotent and does not re-patch abort", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+ installPolyfills();
+
+ const controller = new globalThis.AbortController();
+ controller.abort(new Error("stop"));
+
+ expect(runtime.getAbortCallCount()).toBe(1);
+ });
+
+ test("does not override existing AbortSignal static methods", () => {
+ const runtime = createFakeAbortRuntime();
+ const abort = () => ({ aborted: true }) as AbortSignal;
+ const timeout = () => ({ aborted: false }) as AbortSignal;
+ const any = () => ({ aborted: false }) as AbortSignal;
+
+ Object.assign(runtime.AbortSignal, { abort, timeout, any });
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const AbortSignalStatic = globalThis.AbortSignal as typeof AbortSignal & {
+ abort: typeof abort;
+ timeout: typeof timeout;
+ any: typeof any;
+ };
+
+ expect(AbortSignalStatic.abort).toBe(abort);
+ expect(AbortSignalStatic.timeout).toBe(timeout);
+ expect(AbortSignalStatic.any).toBe(any);
+ });
+
+ test("AbortSignal.any handles empty input", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ }
+ ).any([]);
+
+ expect(signal.aborted).toBe(false);
+ });
+
+ test("AbortSignal.any dedupes duplicate source signals", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const sourceController = new globalThis.AbortController();
+ const sourceSignal = sourceController.signal as unknown as FakeAbortSignal;
+
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ }
+ ).any([
+ sourceController.signal,
+ sourceController.signal,
+ sourceController.signal,
+ ]);
+
+ expect(sourceSignal.getListenerCount()).toBeLessThanOrEqual(1);
+
+ const reason = new Error("duplicate-source");
+ sourceController.abort(reason);
+
+ expect(signal.aborted).toBe(true);
+ expect((signal as { readonly reason: unknown }).reason).toBe(reason);
+ });
+
+ test("AbortSignal.any uses first already-aborted signal in input order", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const firstController = new globalThis.AbortController();
+ const secondController = new globalThis.AbortController();
+
+ const firstReason = new Error("first");
+ const secondReason = new Error("second");
+ firstController.abort(firstReason);
+ secondController.abort(secondReason);
+
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ }
+ ).any([firstController.signal, secondController.signal]);
+
+ expect(signal.aborted).toBe(true);
+ expect((signal as { readonly reason: unknown }).reason).toBe(firstReason);
+ });
+
+ test("AbortSignal.any uses AbortError when aborted signal has no reason", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: undefined });
+
+ installPolyfills();
+
+ const signalWithoutReason = {
+ aborted: true,
+ addEventListener: () => undefined,
+ removeEventListener: () => undefined,
+ } as unknown as AbortSignal;
+
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ }
+ ).any([signalWithoutReason]);
+
+ const reason = (signal as { readonly reason: Error }).reason;
+ expect(reason.name).toBe("AbortError");
+ });
+
+ test("falls back to Error when DOMException constructor throws", async () => {
+ const runtime = createFakeAbortRuntime();
+ const throwingDomException = (() => {
+ throw new Error("broken DOMException");
+ }) as unknown as typeof DOMException;
+
+ setAbortGlobals({ ...runtime, DOMException: throwingDomException });
+
+ installPolyfills();
+
+ const signal = (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ timeout: (milliseconds: number) => AbortSignal;
+ }
+ ).timeout(1);
+
+ await new Promise((resolve) => globalThis.setTimeout(resolve, 5));
+
+ const reason = (signal as { readonly reason: Error }).reason;
+ expect(reason).toBeInstanceOf(Error);
+ expect(reason.name).toBe("TimeoutError");
+ });
+
+ test("AbortSignal.any does not add unbounded listeners to a long-lived source", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ installPolyfills();
+
+ const sourceController = new globalThis.AbortController();
+ const sourceSignal = sourceController.signal as unknown as FakeAbortSignal;
+
+ const AbortSignalStatic = globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ };
+
+ for (let i = 0; i < 100; i += 1) {
+ AbortSignalStatic.any([sourceController.signal]);
+ }
+
+ expect(sourceSignal.getListenerCount()).toBeLessThanOrEqual(1);
+ });
+
+ test("AbortSignal.any tolerates stale aborted references", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ const originalWeakRef = globalThis.WeakRef;
+ let callCount = 0;
+
+ (globalThis as { WeakRef?: unknown }).WeakRef = function (
+ this: unknown,
+ controller: AbortController,
+ ) {
+ const preAbortedController = new globalThis.AbortController();
+ preAbortedController.abort("already-done");
+
+ return {
+ deref: () => {
+ callCount += 1;
+ return callCount === 1 ? controller : preAbortedController;
+ },
+ };
+ };
+
+ try {
+ installPolyfills();
+
+ const sourceController = new globalThis.AbortController();
+ (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ }
+ ).any([sourceController.signal]);
+
+ expect(() => sourceController.abort("ignored")).not.toThrow();
+ } finally {
+ (globalThis as { WeakRef?: unknown }).WeakRef = originalWeakRef;
+ }
+ });
+
+ test("AbortSignal.any tolerates cleared weak refs", () => {
+ const runtime = createFakeAbortRuntime();
+ setAbortGlobals({ ...runtime, DOMException: globalThis.DOMException });
+
+ const originalWeakRef = globalThis.WeakRef;
+
+ let callCount = 0;
+
+ (globalThis as { WeakRef?: unknown }).WeakRef = function (
+ this: unknown,
+ controller: AbortController,
+ ) {
+ return {
+ deref: () => {
+ callCount += 1;
+ return callCount === 1 ? controller : undefined;
+ },
+ };
+ };
+
+ try {
+ installPolyfills();
+
+ const sourceController = new globalThis.AbortController();
+ (
+ globalThis.AbortSignal as typeof AbortSignal & {
+ any: (signals: ReadonlyArray) => AbortSignal;
+ }
+ ).any([sourceController.signal]);
+
+ expect(() => sourceController.abort("ignored")).not.toThrow();
+ } finally {
+ (globalThis as { WeakRef?: unknown }).WeakRef = originalWeakRef;
+ }
+ });
+});
diff --git a/packages/react-native/test/Shared.test.ts b/packages/react-native/test/Shared.test.ts
deleted file mode 100644
index 280946087..000000000
--- a/packages/react-native/test/Shared.test.ts
+++ /dev/null
@@ -1,101 +0,0 @@
-import { describe, expect, test, vi } from "vitest";
-import { createEvoluDeps, createSharedLocalAuth } from "../src/shared.js";
-
-const mocks = vi.hoisted(() => {
- const workerRun = vi.fn();
- return {
- createCommonEvoluDeps: vi.fn((deps) => deps),
- createConsoleStoreOutput: vi.fn(() => ({ entry: "console-entry" })),
- createInMemoryLeaderLock: vi.fn(() => "leader-lock"),
- createLocalAuth: vi.fn((deps) => ({ kind: "local-auth", deps })),
- createMessageChannel: vi.fn(() => "message-channel"),
- createMessagePort: vi.fn(() => "message-port"),
- createRandomBytes: vi.fn(() => "random-bytes"),
- createRun: vi.fn(() => workerRun),
- createSharedWorker: vi.fn((init) => {
- init({ kind: "shared-self" } as any);
- return { kind: "shared-worker" };
- }),
- createWorker: vi.fn((init) => {
- init({ kind: "db-self" } as any);
- return { kind: "db-worker" };
- }),
- initDbWorker: vi.fn((self) => ({ kind: "db-task", self })),
- initSharedWorker: vi.fn((self) => ({ kind: "shared-task", self })),
- workerRun,
- };
-});
-
-vi.mock("@evolu/common", () => ({
- createConsoleStoreOutput: mocks.createConsoleStoreOutput,
- createInMemoryLeaderLock: mocks.createInMemoryLeaderLock,
- createLocalAuth: mocks.createLocalAuth,
- createRandomBytes: mocks.createRandomBytes,
- createRun: mocks.createRun,
-}));
-
-vi.mock("@evolu/common/local-first", () => ({
- createEvoluDeps: mocks.createCommonEvoluDeps,
- initDbWorker: mocks.initDbWorker,
- initSharedWorker: mocks.initSharedWorker,
-}));
-
-vi.mock("../src/Worker.js", () => ({
- createMessageChannel: mocks.createMessageChannel,
- createMessagePort: mocks.createMessagePort,
- createSharedWorker: mocks.createSharedWorker,
- createWorker: mocks.createWorker,
-}));
-
-describe("shared react-native deps", () => {
- test("createEvoluDeps wires worker bootstrap through common deps", () => {
- const reloadApp = vi.fn();
- const createSqliteDriver = vi.fn() as any;
-
- const deps = createEvoluDeps({ createSqliteDriver, reloadApp } as any);
- const dbWorker = (deps as any).createDbWorker();
-
- expect(dbWorker).toEqual({ kind: "db-worker" });
- expect(mocks.createRun).toHaveBeenCalledWith(
- expect.objectContaining({
- consoleStoreOutputEntry: "console-entry",
- createMessagePort: mocks.createMessagePort,
- createSqliteDriver,
- leaderLock: "leader-lock",
- }),
- );
- expect(mocks.initSharedWorker).toHaveBeenCalledWith({
- kind: "shared-self",
- });
- expect(mocks.initDbWorker).toHaveBeenCalledWith({ kind: "db-self" });
- expect(mocks.workerRun).toHaveBeenCalledWith({
- kind: "shared-task",
- self: { kind: "shared-self" },
- });
- expect(mocks.workerRun).toHaveBeenCalledWith({
- kind: "db-task",
- self: { kind: "db-self" },
- });
- expect(mocks.createCommonEvoluDeps).toHaveBeenCalledWith(
- expect.objectContaining({
- createDbWorker: expect.any(Function),
- createMessageChannel: mocks.createMessageChannel,
- reloadApp,
- sharedWorker: { kind: "shared-worker" },
- }),
- );
- });
-
- test("createSharedLocalAuth passes random bytes and secure storage", () => {
- const secureStorage = { getItem: vi.fn() } as any;
- const localAuth = createSharedLocalAuth(secureStorage);
-
- expect(localAuth).toEqual({
- kind: "local-auth",
- deps: {
- randomBytes: "random-bytes",
- secureStorage,
- },
- });
- });
-});
diff --git a/packages/react-native/test/shared.test.ts b/packages/react-native/test/shared.test.ts
new file mode 100644
index 000000000..fd8423d3c
--- /dev/null
+++ b/packages/react-native/test/shared.test.ts
@@ -0,0 +1,89 @@
+import { describe, expect, test, vi } from "vitest";
+
+const createRun = vi.fn((deps: object) => {
+ const run = vi.fn((task: unknown) => task);
+ Object.assign(run, { deps });
+ return run;
+});
+
+const createConsoleStoreOutput = vi.fn(() => {
+ const entry = Symbol("entry");
+ return {
+ entry,
+ write: vi.fn(),
+ };
+});
+
+const createConsole = vi.fn((config: object) => ({
+ ...config,
+ child: vi.fn(),
+ getLevel: () => "debug",
+ setLevel: vi.fn(),
+}));
+
+const createCommonEvoluDeps = vi.fn((deps: object) => deps);
+const initDbWorker = vi.fn(() => Symbol("initDbWorkerTask"));
+const initSharedWorker = vi.fn(() => Symbol("initSharedWorkerTask"));
+
+vi.mock("@evolu/common", () => ({
+ createConsole,
+ createConsoleStoreOutput,
+ createInMemoryLeaderLock: vi.fn(() => Symbol("leaderLock")),
+ createRandomBytes: vi.fn(() => vi.fn()),
+ createRun,
+ createWebSocket: vi.fn(),
+}));
+
+vi.mock("@evolu/common/local-first", () => ({
+ createEvoluDeps: createCommonEvoluDeps,
+ initDbWorker,
+ initSharedWorker,
+}));
+
+vi.mock("../src/Worker.js", () => ({
+ createMessageChannel: vi.fn(),
+ createMessagePort: vi.fn(),
+ createSharedWorker: vi.fn((init: (self: object) => void) => {
+ const self = {};
+ init(self);
+ return { port: {}, [Symbol.dispose]: vi.fn() };
+ }),
+ createWorker: vi.fn((init: (self: object) => void) => {
+ const self = {};
+ init(self);
+ return { postMessage: vi.fn(), [Symbol.dispose]: vi.fn() };
+ }),
+}));
+
+describe("createEvoluDeps", () => {
+ test("uses isolated run/store for shared worker and db workers", async () => {
+ const { createEvoluDeps } = await import("../src/shared.js");
+
+ const deps = createEvoluDeps({
+ reloadApp: vi.fn(),
+ createSqliteDriver: vi.fn(),
+ console: { getLevel: () => "debug" },
+ } as never);
+
+ expect(createRun).toHaveBeenCalledTimes(1);
+ expect(createConsoleStoreOutput).toHaveBeenCalledTimes(1);
+ expect(initSharedWorker).toHaveBeenCalledTimes(1);
+
+ (deps as { createDbWorker: () => object }).createDbWorker();
+
+ expect(createRun).toHaveBeenCalledTimes(2);
+ expect(createConsoleStoreOutput).toHaveBeenCalledTimes(2);
+ expect(initDbWorker).toHaveBeenCalledTimes(1);
+
+ const sharedRunDeps = createRun.mock.calls[0][0] as {
+ consoleStoreOutputEntry: symbol;
+ };
+ const dbRunDeps = createRun.mock.calls[1][0] as {
+ consoleStoreOutputEntry: symbol;
+ };
+
+ expect(sharedRunDeps.consoleStoreOutputEntry).not.toBe(
+ dbRunDeps.consoleStoreOutputEntry,
+ );
+ });
+});
diff --git a/packages/web/package.json b/packages/web/package.json
index 56591eae0..1f54bfcd7 100644
--- a/packages/web/package.json
+++ b/packages/web/package.json
@@ -41,7 +41,7 @@
"devDependencies": {
"@evolu/common": "workspace:*",
"@evolu/tsconfig": "workspace:*",
- "@types/sharedworker": "^0.0.211",
+ "@types/sharedworker": "^0.0.221",
"@types/web-locks-api": "^0.0.5",
"typescript": "^5.9.3",
"user-agent-data-types": "^0.4.2"