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

- Repobeats analytics image + Repobeats analytics image

## 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"