diff --git a/.agents/skills/effect-machine-pr/SKILL.md b/.agents/skills/effect-machine-pr/SKILL.md index 3a25da2..c34a8e8 100644 --- a/.agents/skills/effect-machine-pr/SKILL.md +++ b/.agents/skills/effect-machine-pr/SKILL.md @@ -17,27 +17,26 @@ Ship the requested change through a green, merged pull request. Treat invocation ## Implement the complete change -1. Inspect the relevant public API, internals, tests, documentation, examples, and analogous Effect implementation before editing. +1. Inspect the relevant public API, internals, tests, documentation, and analogous Effect implementation before editing. 2. Implement the long-term design, including generic and optimized semantics where applicable. Follow the Cluster ownership boundary in `AGENTS.md`. -3. Add observable runtime tests, typetests, differential tests, JSDocs, README or guide updates, and example changes required to make the feature complete. +3. Add observable runtime tests, typetests, differential tests, JSDocs, README, and guide updates required to make the feature complete. 4. Add a changeset when required. Use minor for a public addition or breaking change and patch for a compatible fix or implementation improvement. Never use major before 1.0. 5. Keep the changeset user-facing: explain the resulting API and direct migration, not implementation history or external inspiration. ## Validate locally 1. Run `pnpm check`. -2. Run `pnpm check` in every affected package directly below `examples/`. -3. Run `pnpm perf:types` for changes that affect the public TypeScript API or inference. -4. Always run `pnpm perf:runtime` before publishing this full-lifecycle workflow. Treat the local run as a functional smoke test; the CI base-versus-PR comparison is authoritative. -5. Fix every failure and repeat all affected checks. Treat correctness, type safety, runtime performance, and memory regressions as blockers. -6. Audit the final diff, formatting, generated files, public terminology, changeset level, and working-tree scope. +2. Run `pnpm perf:types` for changes that affect the public TypeScript API or inference. +3. Always run `pnpm perf:runtime` before publishing this full-lifecycle workflow. Treat the local run as a functional smoke test; the CI base-versus-PR comparison is authoritative. +4. Fix every failure and repeat all affected checks. Treat correctness, type safety, runtime performance, and memory regressions as blockers. +5. Audit the final diff, formatting, generated files, public terminology, changeset level, and working-tree scope. ## Publish and merge 1. Stage only the intended files, commit with a concise outcome-oriented message, and confirm the worktree is clean. 2. Fetch the remote base again before pushing. If it advanced materially, integrate it safely and repeat affected validation. 3. Push the branch and open a ready pull request using the repository template. Record the changeset decision and validation without copying noisy local benchmark tables. -4. Monitor every required check, example job, and type/runtime performance workflow. Inspect the base-versus-PR reports, not only the aggregate green status. +4. Monitor every required check and type/runtime performance workflow. Inspect the base-versus-PR reports, not only the aggregate green status. 5. Diagnose failures from their logs, implement in-scope fixes, rerun relevant local checks, push, and continue monitoring. Never weaken semantics, inference, tests, or thresholds to make CI pass. 6. Merge only when the pull request is current, mergeable, and every required check is green. Use the repository's normal squash-and-branch-cleanup convention unless instructed otherwise. 7. Verify the pull request's remote state even if the merge command reports a local worktree checkout error. Confirm the merge commit is present on `origin/` and the remote feature branch was removed. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 752c404..ea6e6e9 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -10,7 +10,6 @@ ## Validation - [ ] `pnpm check` -- [ ] Relevant example checks, when examples changed - [ ] Automated type-performance measurement passed or was not required - [ ] Automated runtime- and memory-performance measurement passed or was not required diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0bb2f8..d775148 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,68 +43,3 @@ jobs: github.head_ref == 'changeset-release/main' && github.event.pull_request.user.login == 'github-actions[bot]' run: echo "Changesets release pull requests consume their changeset files." - - discover-examples: - runs-on: ubuntu-latest - outputs: - matrix: ${{ steps.examples.outputs.examples }} - required: ${{ steps.examples.outputs.examples_required }} - steps: - - uses: actions/checkout@v7 - with: - fetch-depth: 0 - - id: examples - run: >- - node scripts/ci-changes.mjs - --base ${{ github.event.pull_request.base.sha }} - --head ${{ github.event.pull_request.head.sha }} - --github-output "$GITHUB_OUTPUT" - - example: - needs: discover-examples - if: needs.discover-examples.outputs.required == 'true' - strategy: - fail-fast: false - matrix: - include: ${{ fromJSON(needs.discover-examples.outputs.matrix) }} - name: example (${{ matrix.example }}) - runs-on: ubuntu-latest - defaults: - run: - working-directory: ${{ matrix.directory }} - steps: - - uses: actions/checkout@v7 - - uses: pnpm/action-setup@v6 - with: - package_json_file: ${{ matrix.directory }}/package.json - - uses: actions/setup-node@v7 - with: - node-version: 24 - cache: pnpm - cache-dependency-path: | - pnpm-lock.yaml - ${{ matrix.directory }}/pnpm-lock.yaml - - run: pnpm --dir ../.. install --frozen-lockfile - - run: pnpm --dir ../.. build - - run: pnpm install --frozen-lockfile - - run: pnpm check - - examples: - if: always() - needs: [discover-examples, example] - name: examples - runs-on: ubuntu-latest - steps: - - name: Require every example check to pass - env: - DISCOVERY_RESULT: ${{ needs.discover-examples.result }} - EXAMPLES_REQUIRED: ${{ needs.discover-examples.outputs.required }} - EXAMPLE_RESULT: ${{ needs.example.result }} - run: | - test "$DISCOVERY_RESULT" = "success" - if test "$EXAMPLES_REQUIRED" = "true"; then - test "$EXAMPLE_RESULT" = "success" - else - test "$EXAMPLE_RESULT" = "skipped" - echo "Examples are not affected by this pull request." >> "$GITHUB_STEP_SUMMARY" - fi diff --git a/AGENTS.md b/AGENTS.md index 726d5fe..4dbef30 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,14 +61,6 @@ pnpm perf:runtime Use the pull request performance workflows to compare against the base branch; do not draw conclusions from a single noisy benchmark run. Record the relevant checks and performance result in the pull request. -When an example changes, run its own check from the example directory: - -```sh -pnpm check -``` - -Every package directly below `examples/` must have a `check` script and a committed lockfile. - ## Pull request conventions - Add or update a changeset for changes under `src/` or changes to `package.json`, following the changelog-writing guide in `.changeset/README.md`. diff --git a/README.md b/README.md index 6439473..215e148 100644 --- a/README.md +++ b/README.md @@ -679,20 +679,6 @@ import { MachineTest } from "@typeonce/effect-machine/testing" Each ESM entrypoint is independent and tree-shakeable. -## Examples - -Every package directly under [`examples/`](./examples) has its own lockfile and -`check` script. - -| Example | What it demonstrates | -| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| [Playground](./examples/playground) | Five focused React examples: atomic turnstile commands, state-scoped traffic-light timers, hierarchical microwave safety, a resource-owned media player, and a worker-hosted machine synchronized across tabs | -| [Pokémon](./examples/pokemon) | Compound workflow states, invoked child machines, typed emissions, Atom reactivity, and a live Effect service | -| [Platformer](./examples/platformer) | Nested parallel statecharts, typed deep history, raised events, state-scoped timers, deterministic model tests, and a playable SVG adapter | - -The playground is the shortest path from one concept to working code. The -standalone examples show larger composition and ownership boundaries. - ## Reference and development - [API reference](https://effect-machine.typeonce.dev) diff --git a/examples/platformer/.gitignore b/examples/platformer/.gitignore deleted file mode 100644 index de4d1f0..0000000 --- a/examples/platformer/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -dist -node_modules diff --git a/examples/platformer/README.md b/examples/platformer/README.md deleted file mode 100644 index e6bc449..0000000 --- a/examples/platformer/README.md +++ /dev/null @@ -1,92 +0,0 @@ -# Platformer statechart example - -A small, standalone SVG and Vite demo in which `@typeonce/effect-machine` owns -a platformer character's legal behavior. One compact adapter provides keyboard -input, gravity, a floor, and visible SVG transforms. - -```sh -pnpm install --frozen-lockfile -pnpm dev -``` - -Run the production verification with: - -```sh -pnpm check -``` - -## Controls - -- **A/D** or **arrow keys** — move -- **W**, **up**, or **Space** — jump; press again once in the air for a double jump -- Touch either wall and jump — turn and kick away; repeat after returning to a wall -- **S** or **down** — duck while grounded; dive while airborne -- **P** — pause and resume the exact playable configuration through deep history -- **R** — reset - -## Statechart - -`Character` is parallel: `locomotion`, `facing`, and `contact` update -independently. The locomotion region switches between `Playing` and `Paused`. -Inside `Playing`, `Grounded` and `Airborne` are mutually exclusive. Each branch -is compound again: - -```text -Character (parallel) -├─ locomotion -│ ├─ Playing -│ │ ├─ Grounded: Standing | Running | Ducking | Landing -│ │ ├─ Airborne (parallel) -│ │ │ ├─ motion: Jumping | Falling | Diving -│ │ │ └─ airJump: GroundLock | WallLock | Ready | Spent -│ │ └─ resume (deep history) -│ └─ Paused -├─ facing: Left | Right -└─ contact: NoWall | LeftWall | RightWall -``` - -`Pause` exits `Playing`, which records its current deep configuration. Physics -stops while `Paused`. `Resume` targets `Playing.resume`, restoring both the -active descendants and their typed values: for example, an airborne wall jump -returns with its `originY`, `startedAt`, `push`, jump kind, and air-jump lock. -This is one saved configuration, not an undo stack; pausing again replaces the -previous history. The history implementation also supplies a typed default -`Playing` snapshot for the case where the history node is targeted before the -region has ever been exited. - -State-scoped invocations follow normal statechart entry/exit semantics. Pausing -cancels an active landing or air-jump timer, and restoring that state starts its -invocation again. History restores state configuration and values, not elapsed -wall-clock time or the adapter's past events. - -State payloads live only where they are valid: `Landing` owns impact and resume -direction, while `Airborne` owns only the jump origin. Air-jump availability is -modeled entirely as state: lock states own cancellable readiness timers, -`Ready` is the only state that authorizes a double jump, and `Spent` makes a -second one unrepresentable. Entering `Airborne` exercises a complete nested -parallel target by selecting both `motion` and `airJump` regions. - -Wall contact is an independent top-level region, so the live chart can show -`Grounded + LeftWall` at a floor corner without confusing that combination with -an airborne wall jump. The `Grounded` handler always produces an ordinary jump; -only `Airborne` interprets the wall sample as a wall jump. It turns and pushes -away, refreshes the air jump through `WallLock`, and the same wall may be used -again after physically returning to it. Movement phases own their timestamps, -and both landing and capability locks demonstrate state-scoped -inline `invoke: (from) => from.timer(...)` chains. - -Keyboard commands and physics facts share a typed `Schema.TaggedUnion` -protocol. The adapter executes velocity and floor collision, then reports -`ApexReached`, `Landed`, and `WallContact`. `JumpPressed` includes the current -wall sample, but the active `Grounded` or `Airborne` branch decides its meaning. -Typed internal events coordinate orthogonal regions: `TryAirJump` is accepted -only by `Ready`, while `DoubleJump` and `WallJump` update motion, capability, -and facing without shared flags. The SVG box only reflects the active snapshot; -it never decides behavior. - -## Visuals - -The character is a few inline SVG shapes. Each state maps to one typed transform -and body color in `src/game.ts`; after the double jump is spent, a purple accent -persists across falling and diving. This keeps the example focused on the -machine rather than an art or rendering pipeline. diff --git a/examples/platformer/index.html b/examples/platformer/index.html deleted file mode 100644 index 5315be9..0000000 --- a/examples/platformer/index.html +++ /dev/null @@ -1,162 +0,0 @@ - - - - - - - Orbit Courier — Effect Machine Platformer - - -
-
-
-

@typeonce/effect-machine · playable example

-

Orbit Courier

-

A tiny platformer where every pose is a typed state.

-
-
machine online
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- AD move - W/Space jump ×2 - S duck / dive - P pause / resume - R reset -
-
- - -
- -

- A small adapter owns coordinates and gravity. The machine owns legal behavior, and the box simply transforms to - show its active state. Pause exits Playing; resume targets its deep-history node and restores the - exact grounded or airborne configuration, including state-local values. -

-
- - - diff --git a/examples/platformer/package.json b/examples/platformer/package.json deleted file mode 100644 index b939cf3..0000000 --- a/examples/platformer/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@typeonce/effect-machine-example-platformer", - "version": "0.0.0", - "private": true, - "description": "Playable platformer statechart example using @typeonce/effect-machine", - "type": "module", - "scripts": { - "dev": "vite", - "test": "vitest run", - "typecheck": "tsc --noEmit", - "build": "pnpm typecheck && vite build", - "preview": "vite preview", - "check": "pnpm test && pnpm build" - }, - "dependencies": { - "@typeonce/effect-machine": "file:../..", - "effect": "4.0.0-rc.111" - }, - "devDependencies": { - "typescript": "6.0.3", - "vite": "8.1.5", - "vitest": "4.1.10" - }, - "packageManager": "pnpm@10.17.1", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } -} diff --git a/examples/platformer/pnpm-lock.yaml b/examples/platformer/pnpm-lock.yaml deleted file mode 100644 index 47d7591..0000000 --- a/examples/platformer/pnpm-lock.yaml +++ /dev/null @@ -1,899 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - effect: 4.0.0-rc.111 - -importers: - - .: - dependencies: - '@typeonce/effect-machine': - specifier: file:../.. - version: file:../..(effect@4.0.0-rc.111) - effect: - specifier: 4.0.0-rc.111 - version: 4.0.0-rc.111 - devDependencies: - typescript: - specifier: 6.0.3 - version: 6.0.3 - vite: - specifier: 8.1.5 - version: 8.1.5(yaml@2.9.0) - vitest: - specifier: 4.1.10 - version: 4.1.10(vite@8.1.5(yaml@2.9.0)) - -packages: - - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} - cpu: [x64] - os: [win32] - - '@napi-rs/wasm-runtime@1.2.1': - resolution: {integrity: sha512-KjZdi8Q1wh89gsVmghvbrMgWl6ZWmRmHV6wjB7/g4Zf0dyO+hH3neZUtuDNPO00qq5YE5RITVWvrIZKRaAmzGQ==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 - - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - - '@typeonce/effect-machine@file:../..': - resolution: {directory: ../.., type: directory} - engines: {node: '>=20'} - peerDependencies: - effect: 4.0.0-rc.111 - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - effect@4.0.0-rc.111: - resolution: {integrity: sha512-ASd5L58EIR0CUNueZNKKjSsyOCd+2alxOAIaTcHaqkJkPsaYSsw5Cg/cfANk5K4Jr2YsX756xvX11shzqsreWA==} - - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} - - fast-check@4.9.0: - resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} - engines: {node: '>=12.17.0'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - msgpackr-extract@3.0.4: - resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} - hasBin: true - - msgpackr@2.0.5: - resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} - engines: {node: ^10 || ^12 || >=14} - - pure-rand@8.4.2: - resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} - - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.2.4: - resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} - engines: {node: '>=18'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} - engines: {node: '>=14.0.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - -snapshots: - - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - optional: true - - '@napi-rs/wasm-runtime@1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.139.0': {} - - '@rolldown/binding-android-arm64@1.1.5': - optional: true - - '@rolldown/binding-darwin-arm64@1.1.5': - optional: true - - '@rolldown/binding-darwin-x64@1.1.5': - optional: true - - '@rolldown/binding-freebsd-x64@1.1.5': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.1.5': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-x64-musl@1.1.5': - optional: true - - '@rolldown/binding-openharmony-arm64@1.1.5': - optional: true - - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.1(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.1.5': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@standard-schema/spec@1.1.0': {} - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - - '@typeonce/effect-machine@file:../..(effect@4.0.0-rc.111)': - dependencies: - effect: 4.0.0-rc.111 - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.9': {} - - '@vitest/expect@4.1.10': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - chai: 6.2.2 - tinyrainbow: 3.1.1 - - '@vitest/mocker@4.1.10(vite@8.1.5(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.1.5(yaml@2.9.0) - - '@vitest/pretty-format@4.1.10': - dependencies: - tinyrainbow: 3.1.1 - - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.10': {} - - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 - - assertion-error@2.0.1: {} - - chai@6.2.2: {} - - convert-source-map@2.0.0: {} - - detect-libc@2.1.2: {} - - effect@4.0.0-rc.111: - dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 4.9.0 - msgpackr: 2.0.5 - - es-module-lexer@2.3.1: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - expect-type@1.4.0: {} - - fast-check@4.9.0: - dependencies: - pure-rand: 8.4.2 - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - fsevents@2.3.3: - optional: true - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - msgpackr-extract@3.0.4: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 - optional: true - - msgpackr@2.0.5: - optionalDependencies: - msgpackr-extract: 3.0.4 - - nanoid@3.3.16: {} - - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - - obug@2.1.4: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.5: {} - - postcss@8.5.25: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - pure-rand@8.4.2: {} - - rolldown@1.1.5: - dependencies: - '@oxc-project/types': 0.139.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@4.2.0: {} - - tinybench@2.9.0: {} - - tinyexec@1.2.4: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tinyrainbow@3.1.1: {} - - tslib@2.8.1: - optional: true - - typescript@6.0.3: {} - - vite@8.1.5(yaml@2.9.0): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.1.5 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - yaml: 2.9.0 - - vitest@4.1.10(vite@8.1.5(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.2.4 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.1.5(yaml@2.9.0) - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - msw - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - yaml@2.9.0: - optional: true diff --git a/examples/platformer/pnpm-workspace.yaml b/examples/platformer/pnpm-workspace.yaml deleted file mode 100644 index e9d7426..0000000 --- a/examples/platformer/pnpm-workspace.yaml +++ /dev/null @@ -1,10 +0,0 @@ -packages: - - "." - -overrides: - effect: 4.0.0-rc.111 - -minimumReleaseAge: 1440 -minimumReleaseAgeExclude: - - effect - - vite@8.1.5 diff --git a/examples/platformer/src/game.ts b/examples/platformer/src/game.ts deleted file mode 100644 index b6544db..0000000 --- a/examples/platformer/src/game.ts +++ /dev/null @@ -1,181 +0,0 @@ -import type { Axis, CharacterEvent, CharacterSnapshot, LocomotionMode } from "./machine.ts" -import { airJumpMode, facingDirection, isPaused, locomotionState } from "./machine.ts" - -const FLOOR = 324 -const SIZE = 30 - -const pose = { - Standing: "translate(0 0)", - Running: "rotate(6 15 30)", - Jumping: "rotate(-12 15 15)", - Falling: "rotate(12 15 15)", - Ducking: "translate(0 15) scale(1 .5)", - Diving: "rotate(90 15 15) scale(.8 1.15)", - Landing: "translate(-3 12) scale(1.2 .6)", - Paused: "translate(0 0)" -} as const satisfies Record - -export class GameAdapter { - private readonly position = { x: 84, y: FLOOR - SIZE, vy: 0 } - private readonly held = new Set() - private snapshot: CharacterSnapshot | undefined - private previousMode: LocomotionMode = "Standing" - private lastJumpAt = -1 - private wallPushSpeed = 0 - private wallPushUntil = 0 - private reportedWall: Axis | undefined - private grounded = true - private apexReported = false - - constructor( - private readonly send: (event: CharacterEvent) => void, - private readonly player: SVGGElement, - private readonly playerPose: SVGGElement - ) { - window.addEventListener("keydown", this.onKeyDown) - window.addEventListener("keyup", this.onKeyUp) - } - - setSnapshot(snapshot: CharacterSnapshot) { - this.snapshot = snapshot - } - - step(seconds: number) { - if (this.snapshot === undefined) return - const dt = Math.min(seconds, 1 / 30) - const state = locomotionState(this.snapshot) - - if (state._tag === "Paused") { - this.player.dataset.paused = "true" - return - } - - delete this.player.dataset.paused - const mode = state._tag - const p = this.position - - if (state._tag === "Jumping" && state.startedAt !== this.lastJumpAt) { - p.vy = -430 - this.wallPushSpeed = state.push * 330 - this.wallPushUntil = state.push === 0 ? 0 : state.startedAt + 240 - this.grounded = false - this.apexReported = false - this.lastJumpAt = state.startedAt - } - if (mode === "Diving" && this.previousMode !== "Diving") p.vy = 610 - - const inputSpeed = mode === "Ducking" || mode === "Landing" ? 0 : this.axis * (this.grounded ? 190 : 135) - const speed = performance.now() < this.wallPushUntil ? this.wallPushSpeed : inputSpeed - if (!this.grounded) p.vy += 1_180 * dt - p.x = Math.max(0, Math.min(640 - SIZE, p.x + speed * dt)) - p.y += p.vy * dt - - if (this.jumpWall !== this.reportedWall) { - this.reportedWall = this.jumpWall - this.send({ _tag: "WallContact", wall: this.reportedWall }) - } - - if (!this.apexReported && p.vy >= 0 && mode === "Jumping") { - this.apexReported = true - this.send({ _tag: "ApexReached", y: Math.round(p.y) }) - } - if (!this.grounded && p.y >= FLOOR - SIZE) { - const impact = Math.round(p.vy) - Object.assign(p, { y: FLOOR - SIZE, vy: 0 }) - this.grounded = true - this.send({ _tag: "Landed", impact, axis: this.axis, at: performance.now() }) - } - - const x = facingDirection(this.snapshot) === "Left" ? p.x + SIZE : p.x - const flip = facingDirection(this.snapshot) === "Left" ? " scale(-1 1)" : "" - this.player.setAttribute("transform", `translate(${x.toFixed(1)} ${p.y.toFixed(1)})${flip}`) - this.playerPose.setAttribute("transform", pose[mode]) - this.player.dataset.mode = mode - this.player.dataset.airJump = airJumpMode(this.snapshot) === "AirJumpSpent" ? "spent" : "ready" - if (state._tag === "Jumping") this.player.dataset.jumpKind = state.kind - else delete this.player.dataset.jumpKind - this.previousMode = mode - } - - reset() { - Object.assign(this.position, { x: 84, y: FLOOR - SIZE, vy: 0 }) - this.held.clear() - this.grounded = true - this.previousMode = "Standing" - this.lastJumpAt = -1 - this.wallPushUntil = 0 - this.reportedWall = undefined - } - - destroy() { - window.removeEventListener("keydown", this.onKeyDown) - window.removeEventListener("keyup", this.onKeyUp) - } - - private get axis(): Axis { - const left = this.held.has("KeyA") || this.held.has("ArrowLeft") - const right = this.held.has("KeyD") || this.held.has("ArrowRight") - return left === right ? 0 : left ? -1 : 1 - } - - private get wall(): Axis { - return this.position.x <= 1 ? -1 : this.position.x >= 640 - SIZE - 1 ? 1 : 0 - } - - private get jumpWall(): Axis { - return performance.now() < this.wallPushUntil ? 0 : this.wall - } - - private readonly onKeyDown = (event: KeyboardEvent) => { - if (!this.isGameKey(event.code)) return - event.preventDefault() - - if (event.code === "KeyP") { - if (event.repeat || this.snapshot === undefined) return - this.send(isPaused(this.snapshot) ? { _tag: "Resume" } : { _tag: "Pause", at: performance.now() }) - return - } - - if (this.snapshot !== undefined && isPaused(this.snapshot) && event.code !== "KeyR") return - - const previousAxis = this.axis - this.held.add(event.code) - if (previousAxis !== this.axis) this.send({ _tag: "Move", axis: this.axis, at: performance.now() }) - if (event.repeat) return - - if (event.code === "Space" || event.code === "KeyW" || event.code === "ArrowUp") { - this.send({ _tag: "JumpPressed", at: performance.now(), y: this.position.y, wall: this.jumpWall }) - } else if (event.code === "KeyS" || event.code === "ArrowDown") { - this.send({ _tag: "DownPressed", at: performance.now() }) - } else if (event.code === "KeyR") { - this.reset() - this.send({ _tag: "Reset" }) - } - } - - private readonly onKeyUp = (event: KeyboardEvent) => { - const previousAxis = this.axis - this.held.delete(event.code) - if (this.snapshot !== undefined && isPaused(this.snapshot)) return - if (previousAxis !== this.axis) this.send({ _tag: "Move", axis: this.axis, at: performance.now() }) - if (event.code === "KeyS" || event.code === "ArrowDown") { - this.send({ _tag: "DownReleased", axis: this.axis, at: performance.now() }) - } - } - - private isGameKey(code: string) { - return [ - "KeyA", - "KeyD", - "KeyW", - "KeyS", - "KeyP", - "KeyR", - "Space", - "ArrowLeft", - "ArrowRight", - "ArrowUp", - "ArrowDown" - ].includes(code) - } -} diff --git a/examples/platformer/src/machine.test.ts b/examples/platformer/src/machine.test.ts deleted file mode 100644 index 502ceeb..0000000 --- a/examples/platformer/src/machine.test.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { Machine } from "@typeonce/effect-machine" -import { MachineTest } from "@typeonce/effect-machine/testing" -import { Effect } from "effect" -import { describe, expect, it } from "vitest" -import { makeMermaidRenderer } from "../../../test/machine/visualization/mermaid.ts" -import { makeTextRenderer } from "../../../test/machine/visualization/text.ts" -import { - airJumpMode, - type CharacterEvent, - CharacterEvents, - CharacterMachine, - type CharacterSnapshot, - facingDirection, - locomotionMode, - wallContact -} from "./machine.ts" - -const renderMachine = makeTextRenderer(Machine) -const renderMermaid = makeMermaidRenderer(Machine) - -const playingSnapshot = (snapshot: CharacterSnapshot) => { - const locomotion = snapshot.states.locomotion.state - if (locomotion.path !== "Character.locomotion.Playing") { - throw new Error(`Expected Playing, received ${locomotion.path}`) - } - return locomotion -} - -const runPlan = (effect: Effect.Effect) => - Effect.runPromise(effect as unknown as Effect.Effect) - -const same = (left: unknown, right: unknown) => JSON.stringify(left) === JSON.stringify(right) - -const invariant = MachineTest.invariants(CharacterMachine) - -const pauseAndResumePreserveOrthogonalRegions = invariant.step( - "pause and resume only replace the locomotion region", - ({ after, before, event }) => - event._tag !== "Pause" && event._tag !== "Resume" || - same(before.states.facing, after.states.facing) && same(before.states.contact, after.states.contact) || - `${event._tag} changed facing or wall contact` -) - -const resetIsCanonical = invariant.step( - "reset restores the canonical character configuration", - ({ after, event }) => - event._tag !== "Reset" || - locomotionMode(after) === "Standing" && - facingDirection(after) === "Right" && - wallContact(after) === "NoWall" || - "reset did not restore Standing + Right + NoWall" -) - -const wallJumpFacesItsPush = invariant.state( - "wall jumps face and push away from the sampled wall", - ({ snapshot }) => { - const locomotion = snapshot.states.locomotion.state - if ( - locomotion.path !== "Character.locomotion.Playing" || - locomotion.state.path !== "Character.locomotion.Playing.Airborne" || - locomotion.state.states.motion.state.path !== "Character.locomotion.Playing.Airborne.motion.Jumping" || - locomotion.state.states.motion.state.value.kind !== "Wall" - ) return true - - const push = locomotion.state.states.motion.state.value.push - const facing = facingDirection(snapshot) - return push === 1 && facing === "Right" || push === -1 && facing === "Left" || - `wall jump push ${push} disagrees with facing ${facing}` - } -) - -const resumeRestoresDeepHistory = invariant.trace( - "resume restores the exact locomotion snapshot captured by pause", - ({ trace }) => { - for (const step of trace.steps) { - if ( - step.event._tag !== "Resume" || - step.before.states.locomotion.state.path !== "Character.locomotion.Paused" || - step.after.states.locomotion.state.path !== "Character.locomotion.Playing" - ) continue - - const pause = trace.steps - .slice(0, step.index) - .reverse() - .find((candidate) => - candidate.event._tag === "Pause" && - candidate.before.states.locomotion.state.path === "Character.locomotion.Playing" && - candidate.after.states.locomotion.state.path === "Character.locomotion.Paused" - ) - if (pause === undefined) return "resume had no matching pause transition" - if (!same(pause.before.states.locomotion.state, step.after.states.locomotion.state)) { - return "resume changed the deep locomotion state captured by pause" - } - } - return true - } -) - -const laws = [ - pauseAndResumePreserveOrthogonalRegions, - resetIsCanonical, - wallJumpFacesItsPush, - resumeRestoresDeepHistory -] - -// Exploration scenarios retain decoded events for trace inspection. -const EventValue = { - Resume: (): CharacterEvent => ({ _tag: "Resume" }), - Pause: (fields: { readonly at: number }): CharacterEvent => ({ _tag: "Pause", ...fields }), - Reset: (): CharacterEvent => ({ _tag: "Reset" }), - JumpPressed: ( - fields: { readonly at: number; readonly y: number; readonly wall: -1 | 0 | 1 } - ) => ({ _tag: "JumpPressed", ...fields } as const), - Landed: ( - fields: { readonly impact: number; readonly axis: -1 | 0 | 1; readonly at: number } - ) => ({ _tag: "Landed", ...fields } as const), - ApexReached: (fields: { readonly y: number }) => ({ _tag: "ApexReached", ...fields } as const), - DownPressed: (fields: { readonly at: number }) => ({ _tag: "DownPressed", ...fields } as const) -} - -const explorationEvents = ({ snapshot }: MachineTest.ExplorationStateContext) => { - const locomotion = snapshot.states.locomotion.state - if (locomotion.path === "Character.locomotion.Paused") { - return [EventValue.Resume()] - } - - const pauseAndReset = [EventValue.Pause({ at: 70 }), EventValue.Reset()] - if (locomotion.state.path === "Character.locomotion.Playing.Grounded") { - return [ - EventValue.JumpPressed({ at: 20, y: 100, wall: -1 }), - EventValue.JumpPressed({ at: 20, y: 100, wall: 0 }), - EventValue.JumpPressed({ at: 20, y: 100, wall: 1 }), - ...pauseAndReset - ] - } - - const motion = locomotion.state.states.motion.state - const airborne = [ - EventValue.JumpPressed({ at: 30, y: 80, wall: -1 }), - EventValue.JumpPressed({ at: 30, y: 80, wall: 1 }), - EventValue.Landed({ impact: 12, axis: 0, at: 60 }), - ...pauseAndReset - ] - return motion.path === "Character.locomotion.Playing.Airborne.motion.Jumping" - ? [ - EventValue.ApexReached({ y: 50 }), - EventValue.DownPressed({ at: 40 }), - ...airborne - ] - : motion.path === "Character.locomotion.Playing.Airborne.motion.Falling" - ? [EventValue.DownPressed({ at: 40 }), ...airborne] - : airborne -} - -describe("platformer history integration", () => { - it("renders registered and candidate events separately from states", async () => { - await runPlan(Effect.gen(function*() { - const initial = yield* Machine.planInitial(CharacterMachine) - const definitions = Machine.transitionDefinitions(CharacterMachine) - const rendered = renderMachine(CharacterMachine, initial.state) - const mermaid = renderMermaid(CharacterMachine, initial.state) - - expect(definitions).toHaveLength(28) - expect(definitions).toContainEqual({ - source: "Character.locomotion.Playing.Airborne.airJump", - trigger: { type: "event", event: "WallJump" }, - reenter: true, - acceptance: "required", - branches: [{ - type: "direct", - target: "Character.locomotion.Playing.Airborne.airJump.AirJumpWallLock", - selection: { - path: "Character.locomotion.Playing.Airborne.airJump.AirJumpWallLock", - kind: "state", - scope: "local" - } - }] - }) - expect(definitions.every(({ branches }) => branches.length > 0)).toBe(true) - expect(rendered).toContain("◇ on: WallJump [reenter]") - expect(rendered).toContain("└┄ → AirJumpWallLock") - expect(rendered).toContain( - "Candidate events: Move, DownPressed, JumpPressed, Pause, WallJump, WallContact, Reset" - ) - expect(rendered).not.toContain("Observed event samples") - expect(mermaid).toMatch(/^stateDiagram-v2\n direction LR/) - expect(mermaid).toContain("state_13 --> state_15: WallJump [reenter]") - expect(mermaid).toContain("state_23 --> state_25: WallContact [Left wall]") - expect(mermaid).toContain("state_23 --> state_24: WallContact [No wall]") - expect(mermaid).not.toContain("∅") - })) - }) - - it("resumes the exact grounded leaf and its state-local value", async () => { - await runPlan(Effect.gen(function*() { - const initial = yield* Machine.planInitial(CharacterMachine) - const ducking = yield* Machine.plan( - CharacterMachine, - initial.state, - CharacterEvents.DownPressed({ at: 10 }) - ) - const beforePause = playingSnapshot(ducking.next) - - const paused = yield* Machine.plan( - CharacterMachine, - ducking.next, - CharacterEvents.Pause({ at: 20 }) - ) - const pausedLocomotion = paused.next.states.locomotion.state - expect(pausedLocomotion.path).toBe("Character.locomotion.Paused") - expect(pausedLocomotion.value).toEqual({ _tag: "Paused", pausedAt: 20 }) - - const resumed = yield* Machine.plan(CharacterMachine, paused.next, CharacterEvents.Resume()) - expect(playingSnapshot(resumed.next)).toEqual(beforePause) - expect(resumed.next.states.facing.state.path).toBe("Character.facing.Right") - expect(resumed.next.states.contact.state.path).toBe("Character.contact.NoWall") - })) - }) - - it("resumes both airborne parallel regions and all nested values", async () => { - await runPlan(Effect.gen(function*() { - const initial = yield* Machine.planInitial(CharacterMachine) - const airborne = yield* Machine.plan( - CharacterMachine, - initial.state, - CharacterEvents.JumpPressed({ at: 100, y: 207, wall: -1 }) - ) - const falling = yield* Machine.plan( - CharacterMachine, - airborne.next, - CharacterEvents.ApexReached({ y: 91 }) - ) - const beforePause = playingSnapshot(falling.next) - - const paused = yield* Machine.plan( - CharacterMachine, - falling.next, - CharacterEvents.Pause({ at: 180 }) - ) - expect(paused.next.history?.["Character.locomotion.Playing.resume"]?.active).toEqual([ - "Character", - "Character.locomotion", - "Character.locomotion.Playing", - "Character.locomotion.Playing.Airborne", - "Character.locomotion.Playing.Airborne.motion", - "Character.locomotion.Playing.Airborne.motion.Falling", - "Character.locomotion.Playing.Airborne.airJump", - "Character.locomotion.Playing.Airborne.airJump.AirJumpGroundLock" - ]) - - const resumed = yield* Machine.plan(CharacterMachine, paused.next, CharacterEvents.Resume()) - const restored = playingSnapshot(resumed.next) - expect(restored).toEqual(beforePause) - - if (restored.state.path !== "Character.locomotion.Playing.Airborne") { - throw new Error(`Expected Airborne, received ${restored.state.path}`) - } - expect(restored.state.value).toEqual({ _tag: "Airborne", originY: 207 }) - expect(restored.state.states.motion.state.value).toEqual({ _tag: "Falling", apexY: 91 }) - expect(restored.state.states.airJump.state.path).toBe( - "Character.locomotion.Playing.Airborne.airJump.AirJumpGroundLock" - ) - })) - }) -}) - -describe("platformer semantic exploration", () => { - it("checks history and orthogonal-region laws across the public state space", async () => { - await runPlan(Effect.gen(function*() { - const explored = yield* MachineTest.explore(CharacterMachine, { - events: explorationEvents, - stateKey: ({ snapshot }) => JSON.stringify(snapshot), - invariants: laws - }) - - expect(explored.completeness).toEqual({ _tag: "Complete" }) - - const wallJump = yield* MachineTest.assertReachable( - explored, - "an airborne wall jump", - ({ snapshot }) => { - const locomotion = snapshot.states.locomotion.state - return locomotion.path === "Character.locomotion.Playing" && - locomotion.state.path === "Character.locomotion.Playing.Airborne" && - locomotion.state.states.motion.state.path === - "Character.locomotion.Playing.Airborne.motion.Jumping" && - locomotion.state.states.motion.state.value.kind === "Wall" - } - ) - expect(wallJump.trace.scenario.events).toHaveLength(2) - - const pausedAirborne = yield* MachineTest.assertReachable( - explored, - "paused airborne deep history", - ({ snapshot }) => - snapshot.states.locomotion.state.path === "Character.locomotion.Paused" && - snapshot.history?.["Character.locomotion.Playing.resume"]?.active.includes( - "Character.locomotion.Playing.Airborne" - ) === true - ) - expect(pausedAirborne.trace.scenario.events).toHaveLength(2) - - yield* MachineTest.assertUnreachable( - explored, - "AirJumpReady without the internal timer event", - ({ snapshot }) => airJumpMode(snapshot) === "AirJumpReady" - ) - })) - }) -}) diff --git a/examples/platformer/src/machine.ts b/examples/platformer/src/machine.ts deleted file mode 100644 index d9b2147..0000000 --- a/examples/platformer/src/machine.ts +++ /dev/null @@ -1,492 +0,0 @@ -import { Machine } from "@typeonce/effect-machine" -import { Schema } from "effect" - -// Domain schemas are shared by state payloads and the public physics protocol. -export const Axis = Schema.Literals([-1, 0, 1]) -export type Axis = typeof Axis.Type -const JumpKind = Schema.Literals(["Ground", "Double", "Wall"]) - -const State = Schema.TaggedUnion({ - Paused: { pausedAt: Schema.Number }, - Running: { startedAt: Schema.Number }, - Ducking: { startedAt: Schema.Number }, - Landing: { impact: Schema.Number, resumeAxis: Axis, landedAt: Schema.Number }, - Airborne: { originY: Schema.Number }, - Jumping: { startedAt: Schema.Number, push: Axis, kind: JumpKind }, - Falling: { apexY: Schema.Number }, - Diving: { startedAt: Schema.Number } -}) - -// Inputs and physics facts are one runtime-decoded, statically typed protocol. -export const CharacterEvents = Machine.events( - Schema.TaggedUnion({ - Move: { axis: Axis, at: Schema.Number }, - JumpPressed: { at: Schema.Number, y: Schema.Number, wall: Axis }, - WallContact: { wall: Axis }, - DownPressed: { at: Schema.Number }, - DownReleased: { axis: Axis, at: Schema.Number }, - ApexReached: { y: Schema.Number }, - Landed: { impact: Schema.Number, axis: Axis, at: Schema.Number }, - Pause: { at: Schema.Number }, - Resume: {}, - Reset: {} - }) -) - -const InternalEvents = Machine.internalEvents( - Schema.TaggedUnion({ - LandingSettled: {}, - AirJumpUnlocked: {}, - TryAirJump: { at: Schema.Number }, - DoubleJump: { at: Schema.Number }, - WallJump: { at: Schema.Number, push: Axis } - }) -) - -const awayFrom = (wall: Axis): Axis => (wall === -1 ? 1 : wall === 1 ? -1 : 0) - -export const CharacterStates = Machine.states({ - Character: { - type: "parallel", - states: { - locomotion: { - initial: "Playing", - states: { - Playing: { - initial: "Grounded", - states: { - Grounded: { - initial: "Standing", - states: { - Standing: {}, - Running: State.cases.Running, - Ducking: State.cases.Ducking, - Landing: State.cases.Landing - } - }, - Airborne: { - schema: State.cases.Airborne, - type: "parallel", - states: { - motion: { - initial: "Jumping", - states: { - Jumping: State.cases.Jumping, - Falling: State.cases.Falling, - Diving: State.cases.Diving - } - }, - airJump: { - initial: "AirJumpGroundLock", - states: { - AirJumpGroundLock: {}, - AirJumpWallLock: {}, - AirJumpReady: {}, - AirJumpSpent: {} - } - } - } - }, - resume: { - type: "history", - history: "deep" - } - } - }, - Paused: State.cases.Paused - } - }, - facing: { - initial: "Right", - states: { - Left: {}, - Right: {} - } - }, - contact: { - initial: "NoWall", - states: { - NoWall: {}, - LeftWall: {}, - RightWall: {} - } - } - } - } -}) - -export const CharacterMachine = Machine.make({ - id: "PlatformerCharacter", - states: CharacterStates.states, - events: CharacterEvents, - internalEvents: InternalEvents, - initial: (to) => - to.Character.initial.resolve(({ target }) => - target.from((character) => - character - .locomotion.from((locomotion) => - locomotion.Playing.from((playing) => playing.Grounded.from((grounded) => grounded.Standing.from())) - ) - .facing.from((facing) => facing.Right.from()) - .contact.from((contact) => contact.NoWall.from()) - ) - ) -}).handle({ - Character: { - on: { - Reset: (to) => - to.full.Character().resolve(({ target }) => - target.from((character) => - character - .locomotion.from((locomotion) => - locomotion.Playing.from((playing) => playing.Grounded.from((grounded) => grounded.Standing.from())) - ) - .facing.from((facing) => facing.Right.from()) - .contact.from((contact) => contact.NoWall.from()) - ) - ) - }, - states: { - locomotion: { - states: { - Playing: { - history: { - resume: { - default: ({ target }) => - target.Character.from((character) => - character - .locomotion.from((locomotion) => - locomotion.Playing.from((playing) => - playing.Grounded.from((grounded) => grounded.Standing.from()) - ) - ) - .facing.from((facing) => facing.Right.from()) - .contact.from((contact) => contact.NoWall.from()) - ) - } - }, - on: { - Pause: (to) => - to.branch.Character.locomotion.Paused().resolve(({ event, target }) => - target.from({ pausedAt: event.at }) - ) - }, - states: { - Grounded: { - on: { - JumpPressed: (to) => - to.full.Character().resolve(({ event, target }) => - target.from((character) => - character - .locomotion.from((locomotion) => - locomotion.Playing.from((playing) => - playing.Airborne.from({ originY: event.y }, (airborne) => - airborne - .motion.from((motion) => - motion.Jumping.from({ startedAt: event.at, push: 0, kind: "Ground" }) - ) - .airJump.from((airJump) => airJump.AirJumpGroundLock.from())) - ) - ) - .facing.from((facing) => facing.Right.from()) - .contact.from((contact) => - event.wall === -1 - ? contact.LeftWall.from() - : event.wall === 1 - ? contact.RightWall.from() - : contact.NoWall.from() - ) - ) - ) - }, - states: { - Standing: { - on: { - Move: (to) => - to.branches({ - moving: { target: to.local.Running() }, - unchanged: { target: to.none } - }).resolve(({ event, select }) => - event.axis === 0 - ? select.unchanged() - : select.moving.from({ startedAt: event.at }) - ), - DownPressed: (to) => - to.local.Ducking().resolve(({ event, target }) => target.from({ startedAt: event.at })) - } - }, - Running: { - on: { - Move: (to) => - to.branches({ - stopped: { target: to.local.Standing() }, - unchanged: { target: to.none } - }).resolve(({ event, select }) => - event.axis === 0 - ? select.stopped.from() - : select.unchanged() - ), - DownPressed: (to) => - to.local.Ducking().resolve(({ event, target }) => target.from({ startedAt: event.at })) - } - }, - Ducking: { - on: { - DownReleased: (to) => - to.branches({ - stopped: { target: to.local.Standing() }, - running: { target: to.local.Running() } - }).resolve(({ event, select }) => - event.axis === 0 - ? select.stopped.from() - : select.running.from({ startedAt: event.at }) - ) - } - }, - Landing: { - invoke: (from) => - from.timer("landing-settle", "140 millis").onDone((to) => - to.none.resolve((_, enqueue) => { - enqueue.raise(InternalEvents.LandingSettled()) - return undefined - }) - ), - on: { - LandingSettled: (to) => - to.branches({ - stopped: { target: to.local.Standing() }, - running: { target: to.local.Running() } - }).resolve(({ state, select }) => - state.resumeAxis === 0 - ? select.stopped.from() - : select.running.from({ startedAt: state.landedAt + 140 }) - ), - Move: (to) => - to.local.Landing().resolve(({ event, state, target }) => { - const { _tag: _, ...fields } = state - return target.from({ ...fields, resumeAxis: event.axis }) - }) - } - } - } - }, - Airborne: { - on: { - JumpPressed: (to) => - to.none.resolve(({ event }, enqueue) => { - const push = awayFrom(event.wall) - enqueue.raise( - push === 0 - ? InternalEvents.TryAirJump({ at: event.at }) - : InternalEvents.WallJump({ at: event.at, push }) - ) - return undefined - }), - Landed: (to) => - to.branch.Character.locomotion.Playing.Grounded().resolve(({ event, target }) => - target.from((grounded) => - grounded.Landing.from({ - impact: event.impact, - resumeAxis: event.axis, - landedAt: event.at - }) - ) - ) - }, - states: { - motion: { - on: { - DoubleJump: (to) => - to.local.Jumping().resolve(({ event, target }) => - target.from({ startedAt: event.at, push: 0, kind: "Double" }) - ), - WallJump: (to) => - to.local.Jumping().resolve(({ event, target }) => - target.from({ startedAt: event.at, push: event.push, kind: "Wall" }) - ) - }, - states: { - Jumping: { - on: { - ApexReached: (to) => - to.local.Falling().resolve(({ event, target }) => target.from({ apexY: event.y })), - DownPressed: (to) => - to.local.Diving().resolve(({ event, target }) => target.from({ startedAt: event.at })) - } - }, - Falling: { - on: { - DownPressed: (to) => - to.local.Diving().resolve(({ event, target }) => target.from({ startedAt: event.at })) - } - }, - Diving: {} - } - }, - airJump: { - on: { - WallJump: (to) => - to.local.AirJumpWallLock().resolve(({ target }) => target.from(), { reenter: true }) - }, - states: { - AirJumpGroundLock: { - invoke: (from) => - from.timer("ground-air-jump-unlock", "120 millis").onDone((to) => to.local.AirJumpReady()), - on: {} - }, - AirJumpWallLock: { - invoke: (from) => - from.timer("wall-air-jump-unlock", "240 millis").onDone((to) => to.local.AirJumpReady()), - on: {} - }, - AirJumpReady: { - on: { - TryAirJump: (to) => - to.local.AirJumpSpent().resolve(({ event, target }, enqueue) => { - enqueue.raise(InternalEvents.DoubleJump({ at: event.at })) - return target.from() - }) - } - }, - AirJumpSpent: {} - } - } - } - } - } - }, - Paused: { - on: { - Resume: (to) => to.history.Character.locomotion.Playing.resume - } - } - } - }, - facing: { - states: { - Left: { - on: { - Move: (to) => - to.branches({ right: { target: to.local.Right() }, unchanged: { target: to.none } }).resolve(( - { event, select } - ) => event.axis === 1 ? select.right.from() : select.unchanged()), - WallJump: (to) => - to.branches({ right: { target: to.local.Right() }, unchanged: { target: to.none } }).resolve(( - { event, select } - ) => event.push === 1 ? select.right.from() : select.unchanged()) - } - }, - Right: { - on: { - Move: (to) => - to.branches({ left: { target: to.local.Left() }, unchanged: { target: to.none } }).resolve(( - { event, select } - ) => event.axis === -1 ? select.left.from() : select.unchanged()), - WallJump: (to) => - to.branches({ left: { target: to.local.Left() }, unchanged: { target: to.none } }).resolve(( - { event, select } - ) => event.push === -1 ? select.left.from() : select.unchanged()) - } - } - } - }, - contact: { - on: { - WallContact: (to) => - to.branches({ - leftWall: { title: "Left wall", target: to.local.LeftWall() }, - rightWall: { title: "Right wall", target: to.local.RightWall() }, - noWall: { title: "No wall", target: to.local.NoWall() } - }).resolve(({ event, select }) => - event.wall === -1 - ? select.leftWall.from() - : event.wall === 1 - ? select.rightWall.from() - : select.noWall.from() - ) - }, - states: { - NoWall: {}, - LeftWall: {}, - RightWall: {} - } - } - } - } -}) - -export type CharacterSnapshot = Machine.Machine.Snapshot -export type CharacterEvent = Machine.Machine.InputEvent - -export const isPaused = (snapshot: CharacterSnapshot) => - snapshot.states.locomotion.state.path === "Character.locomotion.Paused" - -export const locomotionState = (snapshot: CharacterSnapshot) => { - const locomotion = snapshot.states.locomotion.state - if (locomotion.path === "Character.locomotion.Paused") { - return locomotion.value - } - - const playing = locomotion.state - if (playing.path === "Character.locomotion.Playing.Airborne") { - return playing.states.motion.state.value - } - return playing.state.path === "Character.locomotion.Playing.Grounded.Standing" - ? { _tag: "Standing" as const } - : playing.state.value -} - -export type LocomotionMode = ReturnType["_tag"] - -export const locomotionMode = (snapshot: CharacterSnapshot): LocomotionMode => locomotionState(snapshot)._tag - -export const locomotionBranch = (snapshot: CharacterSnapshot) => { - const locomotion = snapshot.states.locomotion.state - if (locomotion.path === "Character.locomotion.Paused") return "Paused" as const - return locomotion.state.path === "Character.locomotion.Playing.Grounded" ? "Grounded" as const : "Airborne" as const -} - -export const airJumpMode = (snapshot: CharacterSnapshot) => { - const locomotion = snapshot.states.locomotion.state - if ( - locomotion.path !== "Character.locomotion.Playing" || - locomotion.state.path !== "Character.locomotion.Playing.Airborne" - ) return undefined - - const path = locomotion.state.states.airJump.state.path - if (path === "Character.locomotion.Playing.Airborne.airJump.AirJumpGroundLock") return "AirJumpGroundLock" as const - if (path === "Character.locomotion.Playing.Airborne.airJump.AirJumpWallLock") return "AirJumpWallLock" as const - if (path === "Character.locomotion.Playing.Airborne.airJump.AirJumpReady") return "AirJumpReady" as const - return "AirJumpSpent" as const -} - -export const wallContact = (snapshot: CharacterSnapshot) => { - const path = snapshot.states.contact.state.path - if (path === "Character.contact.NoWall") return "NoWall" as const - return path === "Character.contact.LeftWall" ? "LeftWall" as const : "RightWall" as const -} - -export const facingDirection = (snapshot: CharacterSnapshot) => - snapshot.states.facing.state.path === "Character.facing.Left" ? "Left" as const : "Right" as const - -export const activeStateData = (snapshot: CharacterSnapshot) => { - const locomotion = snapshot.states.locomotion.state - if (locomotion.path === "Character.locomotion.Paused") { - const { _tag: _paused, ...pausedData } = locomotion.value - return pausedData - } - - const playing = locomotion.state - if (playing.path === "Character.locomotion.Playing.Grounded") { - if (playing.state.path === "Character.locomotion.Playing.Grounded.Standing") return {} - const { _tag: _leaf, ...leafData } = playing.state.value - return leafData - } - const { _tag: _branch, ...branchData } = playing.value - const { _tag: _motion, ...motionData } = playing.states.motion.state.value - return { - ...branchData, - ...motionData, - airJump: airJumpMode(snapshot) - } -} diff --git a/examples/platformer/src/main.ts b/examples/platformer/src/main.ts deleted file mode 100644 index 4598149..0000000 --- a/examples/platformer/src/main.ts +++ /dev/null @@ -1,89 +0,0 @@ -import "./styles.css" -import { Machine } from "@typeonce/effect-machine" -import { Effect, Fiber, Stream } from "effect" -import { GameAdapter } from "./game.ts" -import { - activeStateData, - airJumpMode, - type CharacterEvent, - CharacterMachine, - type CharacterSnapshot, - facingDirection, - isPaused, - locomotionBranch, - locomotionMode, - wallContact -} from "./machine.ts" - -const requiredElement = (selector: string) => { - const element = document.querySelector(selector) - if (element === null) throw new Error(`Missing element: ${selector}`) - return element -} - -const modeLabel = requiredElement("#active-mode") -const stateData = requiredElement("#state-data") -const lastEvent = requiredElement("#last-event") -const gameView = requiredElement("#game") - -const showEvent = (event: CharacterEvent) => { - const { _tag, ...payload } = event - const detail = Object.keys(payload).length === 0 ? "" : ` ${JSON.stringify(payload)}` - lastEvent.textContent = `${_tag}${detail}` -} - -let deliver: ((event: CharacterEvent) => void) | undefined -const pending: Array = [] -const send = (event: CharacterEvent) => { - showEvent(event) - deliver === undefined ? pending.push(event) : deliver(event) -} - -const game = new GameAdapter( - send, - requiredElement("#player"), - requiredElement("#player-pose") -) - -const publish = (next: CharacterSnapshot) => { - game.setSnapshot(next) - const mode = locomotionMode(next) - const facing = facingDirection(next) - const contact = wallContact(next) - const airJump = airJumpMode(next) - const paused = isPaused(next) - modeLabel.textContent = [mode, airJump, contact, facing].filter(Boolean).join(" · ") - stateData.textContent = JSON.stringify(activeStateData(next)) - gameView.classList.toggle("is-paused", paused) - - const active = new Set([mode, facing, contact, locomotionBranch(next)]) - if (!paused) active.add("Playing") - if (airJump !== undefined) active.add(airJump) - document.querySelectorAll("[data-node]").forEach((node) => { - node.classList.toggle("is-active", active.has(node.dataset.node ?? "")) - }) -} - -const program = Effect.gen(function*() { - const ref = yield* Machine.start(CharacterMachine) - deliver = (event) => Effect.runFork(ref.send(event).pipe(Effect.catchTag("StoppedError", () => Effect.void))) - publish(yield* ref.state) - for (const event of pending.splice(0)) yield* ref.send(event) - yield* Stream.runForEach(ref.changes, ({ state }) => Effect.sync(() => publish(state))) -}) - -const fiber = Effect.runFork(program) - -let previous = performance.now() - -const frame = (now: number) => { - game.step((now - previous) / 1_000) - previous = now - requestAnimationFrame(frame) -} - -requestAnimationFrame(frame) -window.addEventListener("beforeunload", () => { - game.destroy() - Effect.runFork(Fiber.interrupt(fiber)) -}) diff --git a/examples/platformer/src/styles.css b/examples/platformer/src/styles.css deleted file mode 100644 index 40bc109..0000000 --- a/examples/platformer/src/styles.css +++ /dev/null @@ -1,411 +0,0 @@ -:root { - color: #e9fff9; - background: #0a1021; - font-family: - Inter, - ui-sans-serif, - system-ui, - -apple-system, - BlinkMacSystemFont, - "Segoe UI", - sans-serif; - font-synthesis: none; -} - -* { - box-sizing: border-box; -} - -body { - margin: 0; - min-width: 320px; - min-height: 100vh; - background: - radial-gradient(circle at 12% 0%, rgb(46 196 182 / 16%), transparent 32rem), - radial-gradient(circle at 92% 18%, rgb(255 107 107 / 10%), transparent 28rem), #0a1021; -} - -.app-shell { - width: min(1180px, calc(100% - 32px)); - margin: 0 auto; - padding: 36px 0 50px; -} -.hero { - display: flex; - align-items: end; - justify-content: space-between; - gap: 24px; - margin-bottom: 22px; -} -.eyebrow { - margin: 0 0 8px; - color: #55d6be; - font: - 700 11px/1.2 ui-monospace, - monospace; - letter-spacing: 0.12em; - text-transform: uppercase; -} -h1 { - margin: 0; - font-size: clamp(42px, 7vw, 76px); - line-height: 0.9; - letter-spacing: -0.065em; -} -.lede { - margin: 14px 0 0; - color: #9aabc1; - font-size: 15px; -} -.status-light { - display: flex; - gap: 8px; - align-items: center; - color: #92a6bc; - font: - 650 11px ui-monospace, - monospace; - text-transform: uppercase; -} -.status-light span { - width: 7px; - height: 7px; - border-radius: 50%; - background: #55d6be; - box-shadow: 0 0 14px #55d6be; -} - -.demo-grid { - display: grid; - grid-template-columns: minmax(0, 1.65fr) minmax(310px, 0.85fr); - gap: 16px; -} -.game-card, -.debug-card { - overflow: hidden; - border: 1px solid #243452; - border-radius: 18px; - background: rgb(16 24 46 / 88%); - box-shadow: 0 24px 70px rgb(0 0 0 / 28%); -} -#game { - display: block; - width: 100%; - aspect-ratio: 16 / 9; - background: #10182e; -} -.stars { - opacity: 0.75; -} -.skyline { - fill: #263d5d; -} -.ground { - fill: #273a54; -} -.ground-top { - fill: #55d6be; -} -.wall { - fill: #55d6be; - opacity: 0.75; -} -#player-pose { - transition: transform 80ms ease-out; -} -#player .body { - fill: #2ec4b6; - stroke: #102641; - stroke-width: 3; -} -#player .face { - fill: #fff1c1; -} -#player .eye { - fill: #14213d; -} -#player .scarf { - fill: #ff6b6b; -} -#player[data-mode="Jumping"] .body { - fill: #63e6d6; -} -#player[data-jump-kind="Double"] .body { - fill: #b388ff; -} -#player[data-mode="Falling"] .body { - fill: #70a9ff; -} -#player[data-mode="Diving"] .body { - fill: #ff8b7f; -} -#player[data-mode="Landing"] .body { - fill: #ffd166; -} -#player[data-air-jump="spent"] .scarf { - fill: #c77dff; - filter: drop-shadow(0 0 3px #c77dff); -} -#player[data-paused="true"] { - opacity: 0.55; -} -.pause-overlay { - pointer-events: none; - opacity: 0; - transition: opacity 140ms ease; -} -.pause-overlay rect { - fill: rgb(10 16 33 / 72%); -} -.pause-overlay text { - text-anchor: middle; - font-family: ui-monospace, monospace; -} -.pause-title { - fill: #e9fff9; - font-size: 30px; - font-weight: 800; - letter-spacing: 0.12em; -} -.pause-copy { - fill: #55d6be; - font-size: 11px; -} -#game.is-paused .pause-overlay { - opacity: 1; -} -.controls { - display: flex; - flex-wrap: wrap; - gap: 14px 20px; - padding: 13px 16px; - border-top: 1px solid #243452; - color: #8fa2b9; - font-size: 11px; -} -.controls span { - display: flex; - align-items: center; - gap: 5px; -} -kbd { - min-width: 22px; - padding: 4px 6px; - border: 1px solid #3b506e; - border-bottom-width: 2px; - border-radius: 5px; - color: #d8fff6; - background: #1b2942; - font: - 700 10px ui-monospace, - monospace; - text-align: center; -} - -.debug-card { - padding: 16px; -} -.panel-heading { - display: flex; - justify-content: space-between; - margin-bottom: 14px; - color: #8196ad; - font: - 700 10px ui-monospace, - monospace; - letter-spacing: 0.1em; - text-transform: uppercase; -} -.chart { - padding: 12px; - border: 1px solid #273a58; - border-radius: 12px; - background: #0d162b; -} -.chart-root { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 10px; - border: 1px solid #3e5775; - border-radius: 7px; - color: #e9fff9; - background: #182742; - font: - 700 12px ui-monospace, - monospace; -} -.chart-root small { - color: #55d6be; - font-size: 9px; - text-transform: uppercase; -} -.regions { - display: grid; - gap: 9px; - margin-top: 9px; -} -.regions section { - padding: 9px; - border-left: 2px solid #334865; - background: #111d33; -} -.regions h2 { - margin: 0 0 8px; - color: #7389a1; - font: - 700 9px ui-monospace, - monospace; - letter-spacing: 0.1em; - text-transform: uppercase; -} -.branch + .branch { - margin-top: 7px; -} -.play-state-row { - margin-bottom: 7px; -} -.playing-region { - padding-left: 7px; - border-left: 1px solid #304766; -} -.branch h3 { - display: inline-block; - margin: 0 0 5px; - padding: 3px 6px; - border-radius: 4px; - color: #7890a9; - font: - 700 9px ui-monospace, - monospace; -} -.parallel-label { - display: inline-block; - margin-left: 4px; - color: #55d6be; - font: - 700 8px ui-monospace, - monospace; - letter-spacing: 0.06em; - text-transform: uppercase; -} -.nested-region { - padding: 6px; - border-left: 1px solid #304766; - background: #0e192d; -} -.nested-region + .nested-region { - margin-top: 4px; -} -.nested-region small { - display: block; - margin-bottom: 4px; - color: #637c98; - font: - 700 8px ui-monospace, - monospace; - letter-spacing: 0.08em; - text-transform: uppercase; -} -.state-row { - display: flex; - flex-wrap: wrap; - gap: 4px; -} -.state-row span { - padding: 5px 6px; - border: 1px solid #2b3c59; - border-radius: 5px; - color: #70869e; - background: #131f35; - font: - 650 9px ui-monospace, - monospace; - transition: 120ms ease; -} -.state-row .history-node { - border-style: dashed; - border-color: #8a6bc4; - color: #b89de8; - background: rgb(138 107 196 / 8%); -} -[data-node].is-active { - border-color: #55d6be !important; - color: #dffff8 !important; - background: rgb(46 196 182 / 16%) !important; - box-shadow: 0 0 0 2px rgb(46 196 182 / 8%); -} -.facing-row { - display: grid; - grid-template-columns: 1fr 1fr; -} -.facing-row span { - text-align: center; -} - -.telemetry { - display: grid; - gap: 0; - margin: 14px 0 0; -} -.telemetry div { - display: grid; - grid-template-columns: 105px 1fr; - gap: 12px; - padding: 9px 2px; - border-top: 1px solid #23334f; -} -.telemetry dt { - color: #6f849c; - font: - 700 9px ui-monospace, - monospace; - letter-spacing: 0.04em; - text-transform: uppercase; -} -.telemetry dd { - min-width: 0; - margin: 0; - overflow-wrap: anywhere; - color: #c8d8e7; - font: - 600 10px/1.4 ui-monospace, - monospace; -} -code { - color: #ff9292; - font-family: ui-monospace, monospace; -} -.note { - max-width: 790px; - margin: 18px 4px 0; - color: #72879f; - font-size: 12px; - line-height: 1.7; -} - -@media (max-width: 850px) { - .demo-grid { - grid-template-columns: 1fr; - } - .debug-card { - min-height: 0; - } -} - -@media (max-width: 540px) { - .app-shell { - width: min(100% - 16px, 1180px); - padding-top: 24px; - } - .hero { - align-items: start; - flex-direction: column; - } - .controls { - gap: 10px; - } - .telemetry div { - grid-template-columns: 90px 1fr; - } -} diff --git a/examples/platformer/src/vite-env.d.ts b/examples/platformer/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/examples/platformer/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/examples/platformer/tsconfig.json b/examples/platformer/tsconfig.json deleted file mode 100644 index ed48e42..0000000 --- a/examples/platformer/tsconfig.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "useDefineForClassFields": true, - "module": "ESNext", - "lib": ["ES2022", "DOM", "DOM.Iterable"], - "skipLibCheck": true, - "moduleResolution": "Bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "strict": true, - "noUncheckedIndexedAccess": true - }, - "include": ["src"] -} diff --git a/examples/platformer/vite.config.ts b/examples/platformer/vite.config.ts deleted file mode 100644 index 20ed627..0000000 --- a/examples/platformer/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from "vite" - -export default defineConfig({ - server: { - port: 5174 - } -}) diff --git a/examples/playground/.gitignore b/examples/playground/.gitignore deleted file mode 100644 index 1502ec2..0000000 --- a/examples/playground/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -dist -node_modules - diff --git a/examples/playground/README.md b/examples/playground/README.md deleted file mode 100644 index 2111dd3..0000000 --- a/examples/playground/README.md +++ /dev/null @@ -1,50 +0,0 @@ -# Effect Machine playground - -Five complete React examples, ordered from a two-state machine to browser -transport integration. - -## Run - -```sh -pnpm install --frozen-lockfile -pnpm dev -``` - -Validate tests, types, and the production bundle with: - -```sh -pnpm check -``` - -## Examples - -| Route | Machine concept | Integration concept | -| ---------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------------- | -| `/turnstile` | Atomic states, typed commands, ignored events | Service-free `AtomMachine` | -| `/traffic-light` | Internal events, cancellable state-scoped timers, re-entry | Reactive timer-driven rendering | -| `/microwave` | Compound hierarchy and unrepresentable invalid states | Safety-oriented controls | -| `/media-player` | Resource-owned compound states and an independent settings region | Shared Atom runtime, DOM audio, Web Audio service | -| `/worker-tabs` | A machine hosted outside the UI thread | Schema-validated worker messages and `BroadcastChannel` synchronization | - -Each route keeps its machine, adapter, and supporting protocol beside the page. -The machines own legal behavior; components project snapshots and send typed -public commands. - -## Notable boundaries - -- The traffic light exposes `Reset` publicly while timer deliveries stay in - `internalEvents`. -- The microwave stores elapsed time only on `Cooking`, where it is valid. - `Cooking` is nested below `Closed`, so opening the door exits and interrupts - cooking and `Cooking + Open` cannot be represented. -- The media player keeps browser APIs behind an Effect service. Its invoked - transport is nested below the registered audio session that it requires, - while sound settings remain an independent parallel region. Invoked work - returns typed internal events to the deterministic transition core. -- The worker validates unknown incoming messages with Effect Schema before - forwarding public events. Tabs replicate commands and exchange a typed - synchronization state when a tab joins. - -`src/examples/examples.test.ts` covers the smaller machines, including virtual -clock advancement for inline invocation timers. The media player keeps focused model and -property coverage in its own directory. diff --git a/examples/playground/index.html b/examples/playground/index.html deleted file mode 100644 index 1817520..0000000 --- a/examples/playground/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - Effect Machine examples - - -
- - - - diff --git a/examples/playground/package.json b/examples/playground/package.json deleted file mode 100644 index 97d1260..0000000 --- a/examples/playground/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "@typeonce/effect-machine-example-playground", - "version": "0.0.0", - "private": true, - "description": "React playground for building @typeonce/effect-machine examples", - "type": "module", - "scripts": { - "dev": "vite", - "test": "vitest run", - "typecheck": "tsc --noEmit", - "build": "pnpm typecheck && vite build", - "preview": "vite preview", - "check": "pnpm test && pnpm build" - }, - "dependencies": { - "@effect/atom-react": "4.0.0-rc.111", - "@tanstack/react-router": "1.170.18", - "@typeonce/effect-machine": "file:../..", - "effect": "4.0.0-rc.111", - "react": "19.2.7", - "react-dom": "19.2.7", - "scheduler": "0.27.0" - }, - "devDependencies": { - "@effect/vitest": "4.0.0-rc.111", - "@types/react": "19.2.17", - "@types/react-dom": "19.2.2", - "@vitejs/plugin-react": "6.0.4", - "typescript": "6.0.3", - "vite": "8.1.5", - "vitest": "4.1.10" - }, - "packageManager": "pnpm@10.17.1", - "engines": { - "node": "^20.19.0 || >=22.12.0" - } -} diff --git a/examples/playground/pnpm-lock.yaml b/examples/playground/pnpm-lock.yaml deleted file mode 100644 index 63ddef3..0000000 --- a/examples/playground/pnpm-lock.yaml +++ /dev/null @@ -1,1099 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -overrides: - effect: 4.0.0-rc.111 - '@effect/atom-react': 4.0.0-rc.111 - '@effect/vitest': 4.0.0-rc.111 - -importers: - - .: - dependencies: - '@effect/atom-react': - specifier: 4.0.0-rc.111 - version: 4.0.0-rc.111(effect@4.0.0-rc.111)(react@19.2.7)(scheduler@0.27.0) - '@tanstack/react-router': - specifier: 1.170.18 - version: 1.170.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@typeonce/effect-machine': - specifier: file:../.. - version: file:../..(effect@4.0.0-rc.111) - effect: - specifier: 4.0.0-rc.111 - version: 4.0.0-rc.111 - react: - specifier: 19.2.7 - version: 19.2.7 - react-dom: - specifier: 19.2.7 - version: 19.2.7(react@19.2.7) - scheduler: - specifier: 0.27.0 - version: 0.27.0 - devDependencies: - '@effect/vitest': - specifier: 4.0.0-rc.111 - version: 4.0.0-rc.111(effect@4.0.0-rc.111)(vitest@4.1.10(vite@8.1.5(yaml@2.9.0))) - '@types/react': - specifier: 19.2.17 - version: 19.2.17 - '@types/react-dom': - specifier: 19.2.2 - version: 19.2.2(@types/react@19.2.17) - '@vitejs/plugin-react': - specifier: 6.0.4 - version: 6.0.4(vite@8.1.5(yaml@2.9.0)) - typescript: - specifier: 6.0.3 - version: 6.0.3 - vite: - specifier: 8.1.5 - version: 8.1.5(yaml@2.9.0) - vitest: - specifier: 4.1.10 - version: 4.1.10(vite@8.1.5(yaml@2.9.0)) - -packages: - - '@effect/atom-react@4.0.0-rc.111': - resolution: {integrity: sha512-MQgX2+ayms/J686g19vV2AkLSocMaubZmFf2VFrWN3fwPg60XXsaPtlQKdcsIy/McxVGZYgYUPXBVVtcmUjjSA==} - peerDependencies: - effect: 4.0.0-rc.111 - react: '>=19.2.7 <20.0.0' - scheduler: '>=0.27.0 <0.28.0' - - '@effect/vitest@4.0.0-rc.111': - resolution: {integrity: sha512-YDaEVT+grREBVMzykRNFtJwGxy02achzT0WXZYwMJA4ukzuB9krkgQ5roc3N6zhC3NQq/j4IyM32/Pcov7AhHw==} - peerDependencies: - effect: 4.0.0-rc.111 - vitest: '>=4.1.0 <5.0.0' - - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} - cpu: [arm64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} - cpu: [x64] - os: [darwin] - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} - cpu: [arm64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} - cpu: [arm] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} - cpu: [x64] - os: [linux] - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} - cpu: [x64] - os: [win32] - - '@napi-rs/wasm-runtime@1.2.2': - resolution: {integrity: sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.3 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.3 - - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} - - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [android] - - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [darwin] - - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [darwin] - - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [freebsd] - - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm] - os: [linux] - - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [openharmony] - - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [arm64] - os: [win32] - - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [x64] - os: [win32] - - '@rolldown/pluginutils@1.0.1': - resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} - - '@standard-schema/spec@1.1.0': - resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} - - '@tanstack/history@1.162.0': - resolution: {integrity: sha512-79pf/RkhteYZTRgcR4F9kbk84P2N8rugQJswxfIqovlbRiT3yI7eBE+5QorIrZaOKktsgzRlXh1l/du/xpl4iA==} - engines: {node: '>=20.19'} - - '@tanstack/react-router@1.170.18': - resolution: {integrity: sha512-wpbGYZEp/fmz1q4bn7BD8VZ+/VZ7GBqSJv5V969pU+chP8y7dquWDmKTFMohvUegb9lg12m1uPVvD6kB2wORvQ==} - engines: {node: '>=20.19'} - peerDependencies: - react: '>=18.0.0 || >=19.0.0' - react-dom: '>=18.0.0 || >=19.0.0' - - '@tanstack/react-store@0.9.3': - resolution: {integrity: sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - '@tanstack/router-core@1.171.15': - resolution: {integrity: sha512-IILCDcLaItMZQ2jEmCABHY1Nhjjn5XUvwpQp3e4Nmu+vfg0BgYFuu/QASz2SwE2ZNbVMrvt8X/wxa+Gg5aErxA==} - engines: {node: '>=20.19'} - - '@tanstack/store@0.9.3': - resolution: {integrity: sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw==} - - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - - '@typeonce/effect-machine@file:../..': - resolution: {directory: ../.., type: directory} - engines: {node: '>=20'} - peerDependencies: - effect: 4.0.0-rc.111 - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.9': - resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} - - '@types/react-dom@19.2.2': - resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} - peerDependencies: - '@types/react': ^19.2.0 - - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} - - '@vitejs/plugin-react@6.0.4': - resolution: {integrity: sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==} - engines: {node: ^20.19.0 || >=22.12.0} - peerDependencies: - '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 - babel-plugin-react-compiler: ^1.0.0 - vite: ^8.0.0 - peerDependenciesMeta: - '@rolldown/plugin-babel': - optional: true - babel-plugin-react-compiler: - optional: true - - '@vitest/expect@4.1.10': - resolution: {integrity: sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==} - - '@vitest/mocker@4.1.10': - resolution: {integrity: sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==} - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@4.1.10': - resolution: {integrity: sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==} - - '@vitest/runner@4.1.10': - resolution: {integrity: sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==} - - '@vitest/snapshot@4.1.10': - resolution: {integrity: sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==} - - '@vitest/spy@4.1.10': - resolution: {integrity: sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==} - - '@vitest/utils@4.1.10': - resolution: {integrity: sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - chai@6.2.2: - resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} - engines: {node: '>=18'} - - convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - - cookie-es@3.1.1: - resolution: {integrity: sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==} - - csstype@3.2.3: - resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - - effect@4.0.0-rc.111: - resolution: {integrity: sha512-ASd5L58EIR0CUNueZNKKjSsyOCd+2alxOAIaTcHaqkJkPsaYSsw5Cg/cfANk5K4Jr2YsX756xvX11shzqsreWA==} - - es-module-lexer@2.3.1: - resolution: {integrity: sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==} - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - expect-type@1.4.0: - resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} - engines: {node: '>=12.0.0'} - - fast-check@4.9.0: - resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} - engines: {node: '>=12.17.0'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - isbot@5.2.1: - resolution: {integrity: sha512-dJ+LpKyClQZ7NG+j3OensC/mAZkGpukE9YUrgPYvAZj2doVL0edfDgywTUh5CXa0o+nW9a1V9e5+CJTX8+SxRw==} - engines: {node: '>=18'} - - lightningcss-android-arm64@1.33.0: - resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [android] - - lightningcss-darwin-arm64@1.33.0: - resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [darwin] - - lightningcss-darwin-x64@1.33.0: - resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [darwin] - - lightningcss-freebsd-x64@1.33.0: - resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [freebsd] - - lightningcss-linux-arm-gnueabihf@1.33.0: - resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm] - os: [linux] - - lightningcss-linux-arm64-gnu@1.33.0: - resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - lightningcss-linux-arm64-musl@1.33.0: - resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [linux] - libc: [musl] - - lightningcss-linux-x64-gnu@1.33.0: - resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [glibc] - - lightningcss-linux-x64-musl@1.33.0: - resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [linux] - libc: [musl] - - lightningcss-win32-arm64-msvc@1.33.0: - resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} - engines: {node: '>= 12.0.0'} - cpu: [arm64] - os: [win32] - - lightningcss-win32-x64-msvc@1.33.0: - resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} - engines: {node: '>= 12.0.0'} - cpu: [x64] - os: [win32] - - lightningcss@1.33.0: - resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} - engines: {node: '>= 12.0.0'} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - msgpackr-extract@3.0.4: - resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} - hasBin: true - - msgpackr@2.0.5: - resolution: {integrity: sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA==} - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - node-gyp-build-optional-packages@5.2.2: - resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} - hasBin: true - - obug@2.1.4: - resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} - engines: {node: '>=12.20.0'} - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.5: - resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} - engines: {node: '>=12'} - - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} - engines: {node: ^10 || ^12 || >=14} - - pure-rand@8.4.2: - resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} - - react-dom@19.2.7: - resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} - peerDependencies: - react: ^19.2.7 - - react@19.2.7: - resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} - engines: {node: '>=0.10.0'} - - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - - scheduler@0.27.0: - resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - - seroval-plugins@1.6.0: - resolution: {integrity: sha512-CbR5DP5DPicpd9RwRUzka7hi4x1577eGFXJVBW409LXqqJNst99JSUTZ8CXcnskQX7laPOWpmzliq0gBZEGlrQ==} - engines: {node: '>=10'} - peerDependencies: - seroval: ^1.0 - - seroval@1.6.0: - resolution: {integrity: sha512-TBwwKfscTEgnBEWmYKKeCcmCGmrJi0LV6qNUY//WBA3MDesh/zfn+KOMq/ckpxM4gZ0ouAE706A1eenekM2sug==} - engines: {node: '>=10'} - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@4.2.0: - resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@1.3.0: - resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} - engines: {node: '>=18'} - - tinyglobby@0.2.17: - resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} - engines: {node: '>=12.0.0'} - - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} - engines: {node: '>=14.0.0'} - - tslib@2.8.1: - resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} - - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - - use-sync-external-store@1.6.0: - resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - - vite@8.1.5: - resolution: {integrity: sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 - esbuild: ^0.27.0 || ^0.28.0 - jiti: '>=1.21.0' - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: '>=0.54.8' - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - peerDependenciesMeta: - '@types/node': - optional: true - '@vitejs/devtools': - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@4.1.10: - resolution: {integrity: sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.10 - '@vitest/browser-preview': 4.1.10 - '@vitest/browser-webdriverio': 4.1.10 - '@vitest/coverage-istanbul': 4.1.10 - '@vitest/coverage-v8': 4.1.10 - '@vitest/ui': 4.1.10 - happy-dom: '*' - jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@opentelemetry/api': - optional: true - '@types/node': - optional: true - '@vitest/browser-playwright': - optional: true - '@vitest/browser-preview': - optional: true - '@vitest/browser-webdriverio': - optional: true - '@vitest/coverage-istanbul': - optional: true - '@vitest/coverage-v8': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - - yaml@2.9.0: - resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} - engines: {node: '>= 14.6'} - hasBin: true - -snapshots: - - '@effect/atom-react@4.0.0-rc.111(effect@4.0.0-rc.111)(react@19.2.7)(scheduler@0.27.0)': - dependencies: - effect: 4.0.0-rc.111 - react: 19.2.7 - scheduler: 0.27.0 - - '@effect/vitest@4.0.0-rc.111(effect@4.0.0-rc.111)(vitest@4.1.10(vite@8.1.5(yaml@2.9.0)))': - dependencies: - effect: 4.0.0-rc.111 - vitest: 4.1.10(vite@8.1.5(yaml@2.9.0)) - - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': - optional: true - - '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': - optional: true - - '@napi-rs/wasm-runtime@1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@oxc-project/types@0.139.0': {} - - '@rolldown/binding-android-arm64@1.1.5': - optional: true - - '@rolldown/binding-darwin-arm64@1.1.5': - optional: true - - '@rolldown/binding-darwin-x64@1.1.5': - optional: true - - '@rolldown/binding-freebsd-x64@1.1.5': - optional: true - - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - optional: true - - '@rolldown/binding-linux-arm64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-arm64-musl@1.1.5': - optional: true - - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-s390x-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-x64-gnu@1.1.5': - optional: true - - '@rolldown/binding-linux-x64-musl@1.1.5': - optional: true - - '@rolldown/binding-openharmony-arm64@1.1.5': - optional: true - - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) - optional: true - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - optional: true - - '@rolldown/binding-win32-x64-msvc@1.1.5': - optional: true - - '@rolldown/pluginutils@1.0.1': {} - - '@standard-schema/spec@1.1.0': {} - - '@tanstack/history@1.162.0': {} - - '@tanstack/react-router@1.170.18(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@tanstack/history': 1.162.0 - '@tanstack/react-store': 0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7) - '@tanstack/router-core': 1.171.15 - isbot: 5.2.1 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - - '@tanstack/react-store@0.9.3(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': - dependencies: - '@tanstack/store': 0.9.3 - react: 19.2.7 - react-dom: 19.2.7(react@19.2.7) - use-sync-external-store: 1.6.0(react@19.2.7) - - '@tanstack/router-core@1.171.15': - dependencies: - '@tanstack/history': 1.162.0 - cookie-es: 3.1.1 - seroval: 1.6.0 - seroval-plugins: 1.6.0(seroval@1.6.0) - - '@tanstack/store@0.9.3': {} - - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - - '@typeonce/effect-machine@file:../..(effect@4.0.0-rc.111)': - dependencies: - effect: 4.0.0-rc.111 - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.9': {} - - '@types/react-dom@19.2.2(@types/react@19.2.17)': - dependencies: - '@types/react': 19.2.17 - - '@types/react@19.2.17': - dependencies: - csstype: 3.2.3 - - '@vitejs/plugin-react@6.0.4(vite@8.1.5(yaml@2.9.0))': - dependencies: - '@rolldown/pluginutils': 1.0.1 - vite: 8.1.5(yaml@2.9.0) - - '@vitest/expect@4.1.10': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - chai: 6.2.2 - tinyrainbow: 3.1.1 - - '@vitest/mocker@4.1.10(vite@8.1.5(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.1.5(yaml@2.9.0) - - '@vitest/pretty-format@4.1.10': - dependencies: - tinyrainbow: 3.1.1 - - '@vitest/runner@4.1.10': - dependencies: - '@vitest/utils': 4.1.10 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - '@vitest/utils': 4.1.10 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@4.1.10': {} - - '@vitest/utils@4.1.10': - dependencies: - '@vitest/pretty-format': 4.1.10 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 - - assertion-error@2.0.1: {} - - chai@6.2.2: {} - - convert-source-map@2.0.0: {} - - cookie-es@3.1.1: {} - - csstype@3.2.3: {} - - detect-libc@2.1.2: {} - - effect@4.0.0-rc.111: - dependencies: - '@standard-schema/spec': 1.1.0 - fast-check: 4.9.0 - msgpackr: 2.0.5 - - es-module-lexer@2.3.1: {} - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.9 - - expect-type@1.4.0: {} - - fast-check@4.9.0: - dependencies: - pure-rand: 8.4.2 - - fdir@6.5.0(picomatch@4.0.5): - optionalDependencies: - picomatch: 4.0.5 - - fsevents@2.3.3: - optional: true - - isbot@5.2.1: {} - - lightningcss-android-arm64@1.33.0: - optional: true - - lightningcss-darwin-arm64@1.33.0: - optional: true - - lightningcss-darwin-x64@1.33.0: - optional: true - - lightningcss-freebsd-x64@1.33.0: - optional: true - - lightningcss-linux-arm-gnueabihf@1.33.0: - optional: true - - lightningcss-linux-arm64-gnu@1.33.0: - optional: true - - lightningcss-linux-arm64-musl@1.33.0: - optional: true - - lightningcss-linux-x64-gnu@1.33.0: - optional: true - - lightningcss-linux-x64-musl@1.33.0: - optional: true - - lightningcss-win32-arm64-msvc@1.33.0: - optional: true - - lightningcss-win32-x64-msvc@1.33.0: - optional: true - - lightningcss@1.33.0: - dependencies: - detect-libc: 2.1.2 - optionalDependencies: - lightningcss-android-arm64: 1.33.0 - lightningcss-darwin-arm64: 1.33.0 - lightningcss-darwin-x64: 1.33.0 - lightningcss-freebsd-x64: 1.33.0 - lightningcss-linux-arm-gnueabihf: 1.33.0 - lightningcss-linux-arm64-gnu: 1.33.0 - lightningcss-linux-arm64-musl: 1.33.0 - lightningcss-linux-x64-gnu: 1.33.0 - lightningcss-linux-x64-musl: 1.33.0 - lightningcss-win32-arm64-msvc: 1.33.0 - lightningcss-win32-x64-msvc: 1.33.0 - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - msgpackr-extract@3.0.4: - dependencies: - node-gyp-build-optional-packages: 5.2.2 - optionalDependencies: - '@msgpackr-extract/msgpackr-extract-darwin-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-darwin-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-arm64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-linux-x64': 3.0.4 - '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.4 - optional: true - - msgpackr@2.0.5: - optionalDependencies: - msgpackr-extract: 3.0.4 - - nanoid@3.3.16: {} - - node-gyp-build-optional-packages@5.2.2: - dependencies: - detect-libc: 2.1.2 - optional: true - - obug@2.1.4: {} - - pathe@2.0.3: {} - - picocolors@1.1.1: {} - - picomatch@4.0.5: {} - - postcss@8.5.25: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - pure-rand@8.4.2: {} - - react-dom@19.2.7(react@19.2.7): - dependencies: - react: 19.2.7 - scheduler: 0.27.0 - - react@19.2.7: {} - - rolldown@1.1.5: - dependencies: - '@oxc-project/types': 0.139.0 - '@rolldown/pluginutils': 1.0.1 - optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - - scheduler@0.27.0: {} - - seroval-plugins@1.6.0(seroval@1.6.0): - dependencies: - seroval: 1.6.0 - - seroval@1.6.0: {} - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@4.2.0: {} - - tinybench@2.9.0: {} - - tinyexec@1.3.0: {} - - tinyglobby@0.2.17: - dependencies: - fdir: 6.5.0(picomatch@4.0.5) - picomatch: 4.0.5 - - tinyrainbow@3.1.1: {} - - tslib@2.8.1: - optional: true - - typescript@6.0.3: {} - - use-sync-external-store@1.6.0(react@19.2.7): - dependencies: - react: 19.2.7 - - vite@8.1.5(yaml@2.9.0): - dependencies: - lightningcss: 1.33.0 - picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.1.5 - tinyglobby: 0.2.17 - optionalDependencies: - fsevents: 2.3.3 - yaml: 2.9.0 - - vitest@4.1.10(vite@8.1.5(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.5(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.3.1 - expect-type: 1.4.0 - magic-string: 0.30.21 - obug: 2.1.4 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.2.0 - tinybench: 2.9.0 - tinyexec: 1.3.0 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.1.5(yaml@2.9.0) - why-is-node-running: 2.3.0 - transitivePeerDependencies: - - msw - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 - - yaml@2.9.0: - optional: true diff --git a/examples/playground/pnpm-workspace.yaml b/examples/playground/pnpm-workspace.yaml deleted file mode 100644 index 87dc7f5..0000000 --- a/examples/playground/pnpm-workspace.yaml +++ /dev/null @@ -1,13 +0,0 @@ -packages: - - "." - -overrides: - effect: 4.0.0-rc.111 - "@effect/atom-react": 4.0.0-rc.111 - "@effect/vitest": 4.0.0-rc.111 - -minimumReleaseAge: 1440 -minimumReleaseAgeExclude: - - effect - - "@effect/atom-react" - - "@effect/vitest" diff --git a/examples/playground/src/components/ExamplePage.tsx b/examples/playground/src/components/ExamplePage.tsx deleted file mode 100644 index e031dbd..0000000 --- a/examples/playground/src/components/ExamplePage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import type { ReactNode } from "react" - -export function ExamplePage({ - title, - summary, - machineFile, - children -}: { - readonly title: string - readonly summary: string - readonly machineFile: string - readonly children: ReactNode -}) { - return ( -
-
-

Interactive example

-

{title}

-

{summary}

- {machineFile} -
-
{children}
-
- ) -} diff --git a/examples/playground/src/examples/examples.test.ts b/examples/playground/src/examples/examples.test.ts deleted file mode 100644 index 5693d9a..0000000 --- a/examples/playground/src/examples/examples.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { assert, describe, it } from "@effect/vitest" -import { Machine } from "@typeonce/effect-machine" -import { MachineTest } from "@typeonce/effect-machine/testing" -import { Effect } from "effect" -import { MicrowaveMachine } from "./microwave/machine.ts" -import { TrafficLightMachine } from "./traffic-light/machine.ts" -import { TurnstileMachine } from "./turnstile/machine.ts" -import { SharedMachine, SharedTransportEvents } from "./worker-tabs/machine.ts" - -describe("playground machines", () => { - it.effect("accepts only the command enabled by the current turnstile state", () => - Effect.gen(function*() { - const trace = yield* MachineTest.run(TurnstileMachine, { - events: [ - { _tag: "GatePushed" }, - { _tag: "CoinInserted" }, - { _tag: "CoinInserted" }, - { _tag: "GatePushed" } - ] - }) - - yield* MachineTest.verify(TurnstileMachine, trace) - assert.deepStrictEqual(trace.steps.map(({ plan }) => plan.microsteps.length > 0), [false, true, false, true]) - assert.strictEqual(trace.final.path, "Locked") - })) - - it.effect("advances state-scoped traffic-light timers with the runtime clock", () => - Effect.gen(function*() { - const ref = yield* Machine.start(TrafficLightMachine) - const probe = yield* MachineTest.probe(TrafficLightMachine, ref) - const transcript = yield* MachineTest.verifyCausalCommands( - probe, - [MachineTest.advanceCommand(4_000), MachineTest.advanceCommand(1_000)], - { - invariants: [], - await: ({ index }) => - probe.await.until((snapshot) => snapshot.state.path === (index === 0 ? "RedYellow" : "Green")) - } - ) - - assert.strictEqual(transcript.final.state.path, "Green") - yield* ref.stop - })) - - it.effect("makes cooking with an open door unreachable", () => - Effect.gen(function*() { - const trace = yield* MachineTest.run(MicrowaveMachine, { - events: [ - { _tag: "PowerPressed" }, - { _tag: "DoorOpened" }, - { _tag: "PowerPressed" }, - { _tag: "DoorClosed" }, - { _tag: "PowerPressed" } - ] - }) - - yield* MachineTest.verify(MicrowaveMachine, trace) - const opened = trace.steps[1]?.after - assert.strictEqual(opened?.state.path, "Oven.Open") - assert.deepStrictEqual(trace.steps[2]?.before, trace.steps[2]?.after) - assert.strictEqual(trace.final.state.path, "Oven.Closed") - if (trace.final.state.path === "Oven.Closed") { - assert.strictEqual(trace.final.state.state.path, "Oven.Closed.Cooking") - } - })) - - it.effect("restores worker state from a tab synchronization command", () => - Effect.gen(function*() { - const trace = yield* MachineTest.run(SharedMachine, { - events: [ - SharedTransportEvents.Started(), - SharedTransportEvents.Incremented(), - SharedTransportEvents.Synchronized({ active: false, count: 12 }) - ] - }) - - yield* MachineTest.verify(SharedMachine, trace) - assert.strictEqual(trace.final.path, "Idle") - assert.strictEqual(trace.final.value.count, 12) - })) -}) diff --git a/examples/playground/src/examples/media-player/MediaPlayerPage.tsx b/examples/playground/src/examples/media-player/MediaPlayerPage.tsx deleted file mode 100644 index 2ed1419..0000000 --- a/examples/playground/src/examples/media-player/MediaPlayerPage.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import { useAtomSet, useAtomValue } from "@effect/atom-react" -import { Match } from "effect" -import { useCallback, useEffect, useState } from "react" -import { ExamplePage } from "../../components/ExamplePage.tsx" -import { mediaPlayerAtom, mediaPlayerViewAtom, setMediaPlayerElement } from "./atoms.ts" -import { MediaPlayerEvents } from "./machine.ts" - -export function MediaPlayerPage() { - const viewResult = useAtomValue(mediaPlayerViewAtom) - const send = useAtomSet(mediaPlayerAtom.send) - const setAudioElement = useAtomSet(setMediaPlayerElement) - const [source, setSource] = useState<{ readonly name: string; readonly url: string }>() - - const registerAudioElement = useCallback( - (audioRef: HTMLAudioElement | null) => { - setAudioElement(audioRef) - }, - [setAudioElement] - ) - - useEffect( - () => () => { - if (source !== undefined) URL.revokeObjectURL(source.url) - }, - [source] - ) - - return ( - - {Match.value(viewResult).pipe( - Match.tagsExhaustive({ - Initial: () =>
Starting the media player machine…
, - Failure: () =>
The media player machine failed to start.
, - Success: ({ value: view }) => { - const { session, settings, status, transport } = view - const { canPause, canPlay, canRestart, isPlaying, loudness, playback } = transport - const loudnessLevel = Math.min(100, Math.round((loudness?.rms ?? 0) * 220)) - const peakLevel = Math.min(100, Math.round((loudness?.peak ?? 0) * 100)) - - return ( -
-
-
-

Effect audio session

-

{source?.name ?? "Choose a local audio track"}

-
-
- {transport.name} - -
-
- -
- ) - } - }) - )} -
- ) -} diff --git a/examples/playground/src/examples/media-player/atoms.ts b/examples/playground/src/examples/media-player/atoms.ts deleted file mode 100644 index 81d678c..0000000 --- a/examples/playground/src/examples/media-player/atoms.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { AtomMachine } from "@typeonce/effect-machine/reactivity" -import { Effect, Match } from "effect" -import { Atom } from "effect/unstable/reactivity" -import { MediaPlayerEvents, MediaPlayerMachine } from "./machine.ts" -import { MediaPlayer } from "./service.ts" - -const mediaPlayerRuntime = Atom.runtime(MediaPlayer.layer) - -export const mediaPlayerAtom = AtomMachine.bind(mediaPlayerRuntime).make(MediaPlayerMachine) - -export const setMediaPlayerElement = mediaPlayerRuntime.fn((audioRef: HTMLAudioElement | null, get) => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.register(audioRef) - yield* get.setResult( - mediaPlayerAtom.send, - audioRef === null ? MediaPlayerEvents.AudioElementUnmounted() : MediaPlayerEvents.AudioElementMounted() - ) - }) -) - -export const mediaPlayerViewAtom = Atom.mapResult(mediaPlayerAtom.snapshot, ({ state, status }) => { - const transportDefaults = { - playback: { currentTime: 0 }, - loudness: null, - error: null, - isPlaying: false, - isBuffering: false, - canPlay: false, - canPause: false, - canRestart: false - } as const - - const session = state.states.session.state - - const transport = session.path === "Player.session.Registered" - ? Match.value(session.state).pipe( - Match.discriminatorsExhaustive("path")({ - "Player.session.Registered.Empty": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Empty" as const, - value - }), - "Player.session.Registered.Loading": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Loading" as const, - value - }), - "Player.session.Registered.Ready": ({ state }) => - Match.value(state).pipe( - Match.discriminatorsExhaustive("path")({ - "Player.session.Registered.Ready.Paused": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Paused" as const, - value, - playback: value, - canPlay: true, - canRestart: true - }), - "Player.session.Registered.Ready.Playing": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Playing" as const, - value, - playback: value, - loudness: value.loudness, - isPlaying: true, - canPause: true, - canRestart: true - }), - "Player.session.Registered.Ready.Buffering": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Buffering" as const, - value, - playback: value, - isBuffering: true, - canPause: true, - canRestart: true - }), - "Player.session.Registered.Ready.Restarting": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Restarting" as const, - value, - playback: value - }), - "Player.session.Registered.Ready.Ended": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Ended" as const, - value, - playback: value, - canPlay: true, - canRestart: true - }) - }) - ), - "Player.session.Registered.Failed": ({ path, value }) => ({ - ...transportDefaults, - path, - name: "Failed" as const, - value, - error: value.message - }) - }) - ) - : { - ...transportDefaults, - path: session.path, - name: "Unavailable" as const, - value: session.value - } - - return { - status, - session: session.path, - transport, - settings: Match.value(state.states.settings.state).pipe( - Match.discriminatorsExhaustive("path")({ - "Player.settings.Audible": ({ path, value }) => ({ - path, - value, - volume: value.volume, - playbackRate: value.playbackRate, - muted: false - }), - "Player.settings.Muted": ({ path, value }) => ({ - path, - value, - volume: value.volume, - playbackRate: value.playbackRate, - muted: true - }) - }) - ) - } -}) diff --git a/examples/playground/src/examples/media-player/machine.test.ts b/examples/playground/src/examples/media-player/machine.test.ts deleted file mode 100644 index 3f903cc..0000000 --- a/examples/playground/src/examples/media-player/machine.test.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { assert, describe, it } from "@effect/vitest" -import { Machine } from "@typeonce/effect-machine" -import { MachineTest } from "@typeonce/effect-machine/testing" -import { Effect, Graph } from "effect" -import { FastCheck } from "effect/testing" -import { MediaPlayerMachine } from "./machine.ts" - -const everyPublicEvent: ReadonlyArray> = [ - { _tag: "AudioElementMounted" }, - { _tag: "AudioElementUnmounted" }, - { _tag: "SourceSelected", url: "https://example.com/audio.mp3" }, - { _tag: "PlayRequested" }, - { _tag: "PauseRequested" }, - { _tag: "RestartRequested" }, - { _tag: "VolumeChanged", volume: 0.4 }, - { _tag: "PlaybackRateChanged", playbackRate: 1.5 }, - { _tag: "MuteRequested" }, - { _tag: "UnmuteRequested" } -] - -const generated = MachineTest.scenarios(MediaPlayerMachine, { - eventsArbitrary: FastCheck.array(FastCheck.constantFrom(...everyPublicEvent), { maxLength: 30 }) -}) - -const same = (left: unknown, right: unknown) => JSON.stringify(left) === JSON.stringify(right) - -const invariant = MachineTest.invariants(MediaPlayerMachine) - -const parallelRegionsAreIndependent = invariant.step( - "session and settings commands stay in their own parallel region", - ({ after, before, event }) => { - const sessionUnchanged = same(before.states.session.state, after.states.session.state) - const settingsUnchanged = same(before.states.settings.state, after.states.settings.state) - if (["VolumeChanged", "PlaybackRateChanged", "MuteRequested", "UnmuteRequested"].includes(event._tag)) { - return sessionUnchanged || `settings event ${event._tag} changed the session region` - } - return settingsUnchanged || `session event ${event._tag} changed the settings region` - } -) - -const settingsUpdatesAreExact = invariant.step( - "settings commands preserve unrelated values", - ({ after, before, event }) => { - const previous = before.states.settings.state - const next = after.states.settings.state - switch (event._tag) { - case "VolumeChanged": - return Object.is(next.value.volume, event.volume) && - Object.is(next.value.playbackRate, previous.value.playbackRate) || - "volume update changed playback rate or stored the wrong volume" - case "PlaybackRateChanged": - return Object.is(next.value.playbackRate, event.playbackRate) && - Object.is(next.value.volume, previous.value.volume) || - "playback-rate update changed volume or stored the wrong rate" - case "MuteRequested": - return next.path === "Player.settings.Muted" && - Object.is(next.value.volume, previous.value.volume) && - Object.is(next.value.playbackRate, previous.value.playbackRate) || - "mute did not preserve sound settings" - case "UnmuteRequested": - return next.path === "Player.settings.Audible" && - Object.is(next.value.volume, previous.value.volume) && - Object.is(next.value.playbackRate, previous.value.playbackRate) || - "unmute did not preserve sound settings" - default: - return true - } - } -) - -const laws = [parallelRegionsAreIndependent, settingsUpdatesAreExact] - -describe("media-player statechart model", () => { - it.effect.prop( - "keeps every schema-generated scenario structurally valid", - { scenario: generated.arbitrary }, - ({ scenario }) => - MachineTest.run(MediaPlayerMachine, scenario).pipe( - Effect.tap((trace) => MachineTest.verify(MediaPlayerMachine, trace)), - Effect.tap((trace) => MachineTest.assertInvariants(MediaPlayerMachine, trace, laws)), - Effect.tap((trace) => - Effect.sync(() => { - assert.strictEqual(trace.final.path, "Player") - assert.strictEqual(trace.final.states.session.state.path.startsWith("Player.session."), true) - assert.strictEqual(trace.final.states.settings.state.path.startsWith("Player.settings."), true) - }) - ), - Effect.asVoid - ), - { fastCheck: { numRuns: 100, seed: 24_061 } } - ) - - it.effect("covers the public protocol and its reachable states", () => - Effect.gen(function*() { - const trace = yield* MachineTest.run(MediaPlayerMachine, { events: everyPublicEvent }) - yield* MachineTest.verify(MediaPlayerMachine, trace) - - const coverage = MachineTest.coverage(MediaPlayerMachine, trace) - - assert.strictEqual(coverage.events.hit, everyPublicEvent.length) - assert.strictEqual(coverage.events.missing, 0) - assert.strictEqual(coverage.transitions.definitions.hit > 0, true) - assert.strictEqual(coverage.transitions.branches.hit > 0, true) - - const activated = new Set(coverage.states.activation.hits.map(({ path }) => path)) - assert.strictEqual(activated.has("Player.session.Unregistered"), true) - assert.strictEqual(activated.has("Player.session.Registered"), true) - assert.strictEqual(activated.has("Player.session.Registered.Empty"), true) - assert.strictEqual(activated.has("Player.settings.Audible"), true) - assert.strictEqual(activated.has("Player.settings.Muted"), true) - })) - - it.effect("records every planned event in the observed graph", () => - Effect.gen(function*() { - const trace = yield* MachineTest.run(MediaPlayerMachine, { events: everyPublicEvent }) - const observed = yield* MachineTest.observedGraph(MediaPlayerMachine, trace) - const eventEdges = Array.from(Graph.edges(observed.graph), ([, edge]) => edge).filter( - ({ data }) => data._tag === "Event" - ) - - assert.strictEqual(Graph.nodeCount(observed.graph) > 1, true) - assert.strictEqual(Graph.edgeCount(observed.graph), everyPublicEvent.length + 1) - assert.strictEqual(eventEdges.length, everyPublicEvent.length) - assert.strictEqual(observed.starts.length, 1) - })) - - it.effect("explores public behavior and preserves the internal completion boundary", () => - Effect.gen(function*() { - const explored = yield* MachineTest.explore(MediaPlayerMachine, { - events: () => everyPublicEvent, - stateKey: ({ snapshot }) => - JSON.stringify({ - session: snapshot.states.session.state, - settings: snapshot.states.settings.state - }), - invariants: laws - }) - - assert.deepStrictEqual(explored.completeness, { _tag: "Complete" }) - - const registeredAndMuted = yield* MachineTest.assertReachable( - explored, - "registered element while muted", - ({ configuration }) => - configuration.includes("Player.session.Registered") && - configuration.includes("Player.settings.Muted") - ) - assert.deepStrictEqual(registeredAndMuted.trace.scenario.events, [ - everyPublicEvent[0], - { _tag: "MuteRequested" } - ]) - - yield* MachineTest.assertUnreachable( - explored, - "transport active while the audio element is unregistered", - ({ configuration }) => - configuration.includes("Player.session.Unregistered") && - configuration.some((path) => path.startsWith("Player.session.Registered.")) - ) - })) -}) diff --git a/examples/playground/src/examples/media-player/machine.ts b/examples/playground/src/examples/media-player/machine.ts deleted file mode 100644 index c8d982a..0000000 --- a/examples/playground/src/examples/media-player/machine.ts +++ /dev/null @@ -1,455 +0,0 @@ -import { Machine } from "@typeonce/effect-machine" -import { Effect, Match, Schema, Stream } from "effect" -import { MediaPlayer } from "./service.ts" - -interface PlaybackData { - readonly currentTime: number -} - -const State = Schema.TaggedUnion({ - Loading: { url: Schema.String }, - Paused: { currentTime: Schema.Number }, - Playing: { - currentTime: Schema.Number, - loudness: Schema.NullOr(Schema.Struct({ - rms: Schema.Number, - peak: Schema.Number, - decibels: Schema.Number - })) - }, - Buffering: { currentTime: Schema.Number }, - Restarting: { currentTime: Schema.Number }, - Ended: { currentTime: Schema.Number }, - Failed: { message: Schema.String }, - Audible: { - volume: Schema.Number, - playbackRate: Schema.Number - }, - Muted: { - volume: Schema.Number, - playbackRate: Schema.Number - } -}) - -export const MediaPlayerEvents = Machine.events( - Schema.TaggedUnion({ - AudioElementMounted: {}, - AudioElementUnmounted: {}, - SourceSelected: { url: Schema.String }, - PlayRequested: {}, - PauseRequested: {}, - RestartRequested: {}, - VolumeChanged: { volume: Schema.Number }, - PlaybackRateChanged: { playbackRate: Schema.Number }, - MuteRequested: {}, - UnmuteRequested: {} - }) -) - -const MediaPlayerInternalEvents = Machine.internalEvents( - Schema.TaggedUnion({ - LoadSucceeded: {}, - RestartSucceeded: {}, - MediaWaiting: {}, - MediaCanPlay: {}, - PlaybackEnded: { currentTime: Schema.Number }, - TimeUpdated: { currentTime: Schema.Number }, - MediaFailed: { message: Schema.String }, - LoudnessMeasured: { - rms: Schema.Number, - peak: Schema.Number, - decibels: Schema.Number - }, - OperationFailed: { message: Schema.String } - }) -) - -const MediaPlayerStates = Machine.states({ - Player: { - type: "parallel", - states: { - session: { - initial: "Unregistered", - states: { - Unregistered: {}, - Registered: { - initial: "Empty", - states: { - Empty: {}, - Loading: State.cases.Loading, - Ready: { - initial: "Paused", - states: { - Paused: State.cases.Paused, - Playing: State.cases.Playing, - Buffering: State.cases.Buffering, - Restarting: State.cases.Restarting, - Ended: State.cases.Ended - } - }, - Failed: State.cases.Failed - } - } - } - }, - settings: { - initial: "Audible", - states: { - Audible: State.cases.Audible, - Muted: State.cases.Muted - } - } - } - } -}) - -const updatePlaybackData = ( - state: PlaybackData, - patch: Partial -): PlaybackData => ({ - currentTime: patch.currentTime ?? state.currentTime -}) - -export const MediaPlayerMachine = Machine.make({ - id: "MediaPlayer", - states: MediaPlayerStates.states, - events: MediaPlayerEvents, - internalEvents: MediaPlayerInternalEvents, - initial: (to) => - to.Player.initial.resolve(({ target }) => - target.from((player) => - player - .session.from((session) => session.Unregistered.from()) - .settings.from((settings) => - settings.Audible.from({ - volume: 1, - playbackRate: 1 - }) - ) - ) - ) -}).handle({ - Player: { - states: { - session: { - on: { - AudioElementMounted: (to) => - to.local.Registered.initial.resolve(({ target }) => target.from(), { reenter: true }), - AudioElementUnmounted: (to) => - to.local.Unregistered().resolve(({ target }) => target.from(), { reenter: true }) - }, - states: { - Unregistered: {}, - - Registered: { - invoke: (from) => - from.stream("media-element-events", () => - Stream.unwrap( - Effect.map(MediaPlayer, (mediaPlayer) => mediaPlayer.events) - )).onElement((to) => - to.none.resolve(({ element }, enqueue) => { - Match.value(element).pipe( - Match.tagsExhaustive({ - Waiting: () => enqueue.raise(MediaPlayerInternalEvents.MediaWaiting()), - CanPlay: () => enqueue.raise(MediaPlayerInternalEvents.MediaCanPlay()), - Ended: ({ currentTime }) => - enqueue.raise(MediaPlayerInternalEvents.PlaybackEnded({ currentTime })), - TimeUpdated: ({ currentTime }) => - enqueue.raise(MediaPlayerInternalEvents.TimeUpdated({ currentTime })), - Failed: ({ message }) => enqueue.raise(MediaPlayerInternalEvents.MediaFailed({ message })) - }) - ) - }) - ).onDone((to) => to.none).onFailure((to) => - to.local.Failed().resolve(({ error, target }) => target.from({ message: error.message })) - ), - on: { - SourceSelected: (to) => - to.local.Loading().resolve(({ event, target }) => target.from({ url: event.url }), { reenter: true }), - - MediaFailed: (to) => - to.local.Failed().resolve(({ event, target }) => target.from({ message: event.message })), - - OperationFailed: (to) => - to.local.Failed().resolve(({ event, target }) => target.from({ message: event.message })) - }, - states: { - Empty: {}, - - Loading: { - invoke: (from) => - from.effect("load-audio", ({ state }) => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.load(state.url) - })).onDone((to) => - to.none.resolve((_, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.LoadSucceeded()) - return undefined - }) - ).onFailure((to) => - to.none.resolve(({ error }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) - return undefined - }) - ), - on: { - LoadSucceeded: (to) => to.local.Ready.initial - } - }, - - Ready: { - initialize: ({ builder }) => builder.from({ currentTime: 0 }), - states: { - Paused: { - invoke: (from) => - from.effect("pause-audio", () => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.pause - })).onDone((to) => to.none).onFailure((to) => - to.none.resolve(({ error }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) - return undefined - }) - ), - on: { - PlayRequested: (to) => - to.local.Playing().resolve(({ state, target }) => - target.from({ - ...updatePlaybackData(state, {}), - loudness: null - }) - ), - - RestartRequested: (to) => - to.local.Restarting().resolve(({ state, target }) => target.from(updatePlaybackData(state, {}))) - } - }, - - Playing: { - invoke: ( - from - ) => [ - from.effect("play-audio", () => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.play - })).onDone((to) => to.none).onFailure((to) => - to.none.resolve(({ error }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) - return undefined - }) - ), - from.stream("analyze-audio", () => - Stream.unwrap( - Effect.map(MediaPlayer, (mediaPlayer) => mediaPlayer.loudness) - )).onElement((to) => - to.none.resolve(({ element }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.LoudnessMeasured(element)) - }) - ).onDone((to) => to.none).onFailure((to) => - to.none.resolve(({ error }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) - }) - ) - ], - on: { - PauseRequested: (to) => - to.local.Paused().resolve(({ state, target }) => target.from(updatePlaybackData(state, {}))), - - RestartRequested: (to) => - to.local.Restarting().resolve(({ state, target }) => - target.from(updatePlaybackData(state, {})) - ), - - MediaWaiting: (to) => - to.local.Buffering().resolve(({ state, target }) => target.from(updatePlaybackData(state, {}))), - - PlaybackEnded: (to) => - to.local.Ended().resolve(({ event, state, target }) => - target.from(updatePlaybackData(state, { currentTime: event.currentTime })) - ), - - TimeUpdated: (to) => - to.local.Playing().resolve(({ event, state, target }) => - target.from({ - currentTime: event.currentTime, - loudness: state.loudness - }) - ), - - LoudnessMeasured: (to) => - to.local.Playing().resolve(({ event, state, target }) => - target.from({ - currentTime: state.currentTime, - loudness: { - rms: event.rms, - peak: event.peak, - decibels: event.decibels - } - }) - ) - } - }, - - Buffering: { - on: { - MediaCanPlay: (to) => - to.local.Playing().resolve(({ state, target }) => - target.from({ - ...updatePlaybackData(state, {}), - loudness: null - }) - ), - - PauseRequested: (to) => - to.local.Paused().resolve(({ state, target }) => target.from(updatePlaybackData(state, {}))), - - RestartRequested: (to) => - to.local.Restarting().resolve(({ state, target }) => - target.from(updatePlaybackData(state, {})) - ), - - PlaybackEnded: (to) => - to.local.Ended().resolve(({ event, state, target }) => - target.from(updatePlaybackData(state, { currentTime: event.currentTime })) - ), - - TimeUpdated: (to) => - to.local.Buffering().resolve(({ event, state, target }) => - target.from(updatePlaybackData(state, { currentTime: event.currentTime })) - ) - } - }, - - Restarting: { - invoke: (from) => - from.effect("restart-audio", () => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.restart - })).onDone((to) => - to.none.resolve((_, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.RestartSucceeded()) - return undefined - }) - ).onFailure((to) => - to.none.resolve(({ error }, enqueue) => { - enqueue.raise(MediaPlayerInternalEvents.OperationFailed({ message: error.message })) - return undefined - }) - ), - on: { - RestartSucceeded: (to) => - to.local.Playing().resolve(({ target }) => target.from({ currentTime: 0, loudness: null })), - - TimeUpdated: (to) => - to.local.Restarting().resolve(({ event, state, target }) => - target.from(updatePlaybackData(state, { currentTime: event.currentTime })) - ) - } - }, - - Ended: { - on: { - PlayRequested: (to) => - to.local.Restarting().resolve(({ state, target }) => - target.from(updatePlaybackData(state, {})) - ), - - RestartRequested: (to) => - to.local.Restarting().resolve(({ state, target }) => target.from(updatePlaybackData(state, {}))) - } - } - } - }, - - Failed: { - invoke: (from) => - from.effect("report-error", ({ state }) => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.reportError(state.message) - })).onDone((to) => to.none) - } - } - } - } - }, - - settings: { - initialize: ({ builder }) => - builder.from({ - volume: 1, - playbackRate: 1 - }), - states: { - Audible: { - invoke: (from) => - from.effect("apply-audio-settings", ({ state }) => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.applySettings({ ...state, muted: false }) - })).onDone((to) => to.none), - on: { - VolumeChanged: (to) => - to.local.Audible().resolve(({ event, state, target }) => - target.from({ - volume: event.volume, - playbackRate: state.playbackRate - }), { reenter: true }), - - PlaybackRateChanged: (to) => - to.local.Audible().resolve(({ event, state, target }) => - target.from({ - volume: state.volume, - playbackRate: event.playbackRate - }), { reenter: true }), - - MuteRequested: (to) => - to.local.Muted().resolve(({ state, target }) => - target.from({ - volume: state.volume, - playbackRate: state.playbackRate - }) - ) - } - }, - - Muted: { - invoke: (from) => - from.effect("apply-audio-settings", ({ state }) => - Effect.gen(function*() { - const mediaPlayer = yield* MediaPlayer - yield* mediaPlayer.applySettings({ ...state, muted: true }) - })).onDone((to) => to.none), - on: { - VolumeChanged: (to) => - to.local.Muted().resolve(({ event, state, target }) => - target.from({ - volume: event.volume, - playbackRate: state.playbackRate - }), { reenter: true }), - - PlaybackRateChanged: (to) => - to.local.Muted().resolve(({ event, state, target }) => - target.from({ - volume: state.volume, - playbackRate: event.playbackRate - }), { reenter: true }), - - UnmuteRequested: (to) => - to.local.Audible().resolve(({ state, target }) => - target.from({ - volume: state.volume, - playbackRate: state.playbackRate - }) - ) - } - } - } - } - } - } -}) diff --git a/examples/playground/src/examples/media-player/service.ts b/examples/playground/src/examples/media-player/service.ts deleted file mode 100644 index 2e3c0de..0000000 --- a/examples/playground/src/examples/media-player/service.ts +++ /dev/null @@ -1,281 +0,0 @@ -import { Context, Data, Effect, Fiber, Layer, Option, Ref, Schedule, ScopedRef, Stream, SynchronizedRef } from "effect" - -export interface AudioSettings { - readonly volume: number - readonly muted: boolean - readonly playbackRate: number -} - -export interface LoudnessSample { - readonly rms: number - readonly peak: number - readonly decibels: number -} - -export class MediaPlayerError extends Data.TaggedError("MediaPlayerError")<{ - readonly operation: "register" | "observe" | "load" | "play" | "pause" | "restart" | "analyze" - readonly message: string -}> {} - -type MediaElementEvent = Data.TaggedEnum<{ - Waiting: {} - CanPlay: {} - Ended: { readonly currentTime: number } - TimeUpdated: { readonly currentTime: number } - Failed: { readonly message: string } -}> - -const MediaElementEvent = Data.taggedEnum() - -type AudioGraph = Data.TaggedEnum<{ - Registered: { - readonly audioRef: HTMLAudioElement - } - Loaded: { - readonly audioRef: HTMLAudioElement - readonly audioContext: AudioContext - readonly trackSource: MediaElementAudioSourceNode - readonly analyserNode: AnalyserNode - } -}> - -const AudioGraph = Data.taggedEnum() - -interface AudioSession { - readonly graph: SynchronizedRef.SynchronizedRef -} - -const fail = ( - operation: MediaPlayerError["operation"], - message: string -): Effect.Effect => Effect.fail(new MediaPlayerError({ operation, message })) - -export class MediaPlayer extends Context.Service()( - "effect-machine/playground/MediaPlayer", - { - make: Effect.gen(function*() { - const settingsRef = yield* Ref.make({ volume: 1, muted: false, playbackRate: 1 }) - const sessionRef = yield* ScopedRef.make>(() => Option.none()) - - const session = (operation: MediaPlayerError["operation"]) => - ScopedRef.get(sessionRef).pipe( - Effect.flatMap( - Option.match({ - onNone: () => fail(operation, "The audio element is not registered"), - onSome: Effect.succeed - }) - ) - ) - - const applySettings = (audioRef: HTMLAudioElement, settings: AudioSettings) => - Effect.sync(() => { - audioRef.volume = settings.volume - audioRef.muted = settings.muted - audioRef.playbackRate = settings.playbackRate - }) - - const resume = (audioContext: AudioContext, operation: "play" | "restart") => - audioContext.state === "suspended" - ? Effect.tryPromise({ - try: () => audioContext.resume(), - catch: () => new MediaPlayerError({ operation, message: "The audio context could not be resumed" }) - }) - : Effect.void - - return { - register: (audioRef: HTMLAudioElement | null) => - ScopedRef.set( - sessionRef, - audioRef === null - ? Effect.succeed(Option.none()) - : Effect.acquireRelease( - Effect.gen(function*() { - yield* applySettings(audioRef, yield* Ref.get(settingsRef)) - return Option.some({ - graph: yield* SynchronizedRef.make(AudioGraph.Registered({ audioRef })) - }) - }), - Option.match({ - onNone: () => Effect.void, - onSome: ({ graph }) => - SynchronizedRef.get(graph).pipe( - Effect.flatMap((current) => - AudioGraph.$is("Loaded")(current) - ? Effect.all([ - Effect.sync(() => { - current.trackSource.disconnect() - current.analyserNode.disconnect() - }), - current.audioContext.state === "closed" - ? Effect.void - : Effect.promise(() => current.audioContext.close()).pipe(Effect.ignore) - ], { discard: true }) - : Effect.void - ) - ) - }) - ) - ), - - events: Stream.unwrap( - Effect.gen(function*() { - const { graph } = yield* session("observe") - const audioRef = (yield* SynchronizedRef.get(graph)).audioRef - return Stream.mergeAll([ - Stream.fromEventListener(audioRef, "waiting").pipe( - Stream.map((): MediaElementEvent => MediaElementEvent.Waiting()) - ), - Stream.fromEventListener(audioRef, "canplay").pipe( - Stream.map((): MediaElementEvent => MediaElementEvent.CanPlay()) - ), - Stream.fromEventListener(audioRef, "ended").pipe( - Stream.map((): MediaElementEvent => MediaElementEvent.Ended({ currentTime: audioRef.currentTime })) - ), - Stream.fromEventListener(audioRef, "timeupdate").pipe( - Stream.map((): MediaElementEvent => - MediaElementEvent.TimeUpdated({ currentTime: audioRef.currentTime }) - ) - ), - Stream.fromEventListener(audioRef, "error").pipe( - Stream.map((): MediaElementEvent => - MediaElementEvent.Failed({ - message: audioRef.error?.message ?? "The selected audio file could not be loaded" - }) - ) - ) - ], { concurrency: "unbounded" }) - }) - ), - - load: (url: string) => - Effect.gen(function*() { - const { graph } = yield* session("load") - yield* SynchronizedRef.updateEffect(graph, (current) => - Effect.gen(function*() { - const audioRef = current.audioRef - const loaded = Stream.fromEventListener(audioRef, "loadeddata").pipe( - Stream.take(1), - Stream.runHead, - Effect.asVoid - ) - const failed = Stream.fromEventListener(audioRef, "error").pipe( - Stream.take(1), - Stream.runHead, - Effect.flatMap(() => - fail("load", audioRef.error?.message ?? "The selected audio file could not be loaded") - ) - ) - const waiting = yield* Stream.merge(loaded.pipe(Stream.fromEffect), failed.pipe(Stream.fromEffect)) - .pipe(Stream.runHead, Effect.forkChild({ startImmediately: true })) - - yield* Effect.try({ - try: () => { - audioRef.src = url - audioRef.load() - }, - catch: () => new MediaPlayerError({ operation: "load", message: "The audio file could not load" }) - }) - yield* Fiber.join(waiting) - - if (AudioGraph.$is("Loaded")(current)) return current - - const AudioContextConstructor = window.AudioContext ?? - (window as Window & { readonly webkitAudioContext?: typeof AudioContext }).webkitAudioContext - if (AudioContextConstructor === undefined) { - return yield* fail("load", "Web Audio is not supported by this browser") - } - - return yield* Effect.try({ - try: () => { - const audioContext = new AudioContextConstructor() - const trackSource = audioContext.createMediaElementSource(audioRef) - const analyserNode = audioContext.createAnalyser() - analyserNode.fftSize = 256 - trackSource.connect(analyserNode) - analyserNode.connect(audioContext.destination) - return AudioGraph.Loaded({ audioRef, audioContext, trackSource, analyserNode }) - }, - catch: () => - new MediaPlayerError({ operation: "load", message: "The audio graph could not be connected" }) - }) - })) - }), - - applySettings: (settings: AudioSettings) => - Effect.gen(function*() { - yield* Ref.set(settingsRef, settings) - const current = yield* ScopedRef.get(sessionRef) - if (Option.isSome(current)) { - yield* SynchronizedRef.get(current.value.graph).pipe( - Effect.flatMap((graph) => applySettings(graph.audioRef, settings)) - ) - } - }), - - play: Effect.gen(function*() { - const { graph } = yield* session("play") - const current = yield* SynchronizedRef.get(graph) - if (!AudioGraph.$is("Loaded")(current)) return yield* fail("play", "The audio graph is not ready") - yield* resume(current.audioContext, "play") - yield* Effect.tryPromise({ - try: () => current.audioRef.play(), - catch: () => new MediaPlayerError({ operation: "play", message: "Playback could not be started" }) - }) - }), - - pause: Effect.gen(function*() { - const { graph } = yield* session("pause") - const current = yield* SynchronizedRef.get(graph) - yield* Effect.try({ - try: () => current.audioRef.pause(), - catch: () => new MediaPlayerError({ operation: "pause", message: "Playback could not be paused" }) - }) - }), - - restart: Effect.gen(function*() { - const { graph } = yield* session("restart") - const current = yield* SynchronizedRef.get(graph) - if (!AudioGraph.$is("Loaded")(current)) return yield* fail("restart", "The audio graph is not ready") - yield* resume(current.audioContext, "restart") - yield* Effect.tryPromise({ - try: async () => { - current.audioRef.currentTime = 0 - await current.audioRef.play() - }, - catch: () => new MediaPlayerError({ operation: "restart", message: "Playback could not be restarted" }) - }) - }), - - loudness: Stream.unwrap( - Effect.gen(function*() { - const { graph } = yield* session("analyze") - const current = yield* SynchronizedRef.get(graph) - if (!AudioGraph.$is("Loaded")(current)) { - return yield* fail("analyze", "The audio analyser is not ready") - } - const samples = new Uint8Array(current.analyserNode.fftSize) - return Stream.fromEffectSchedule( - Effect.sync(() => { - current.analyserNode.getByteTimeDomainData(samples) - let sum = 0 - let peak = 0 - for (const value of samples) { - const normalized = (value - 128) / 128 - sum += normalized * normalized - peak = Math.max(peak, Math.abs(normalized)) - } - const rms = Math.sqrt(sum / samples.length) - return { rms, peak, decibels: 20 * Math.log10(Math.max(rms, 0.0001)) } satisfies LoudnessSample - }), - Schedule.spaced("100 millis") - ) - }) - ), - - reportError: (message: string) => Effect.logError(message) - } - }) - } -) { - static readonly layer = Layer.effect(this)(this.make) -} diff --git a/examples/playground/src/examples/microwave/MicrowavePage.tsx b/examples/playground/src/examples/microwave/MicrowavePage.tsx deleted file mode 100644 index c6417f0..0000000 --- a/examples/playground/src/examples/microwave/MicrowavePage.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { useAtomSet, useAtomValue } from "@effect/atom-react" -import { Match } from "effect" -import { ExamplePage } from "../../components/ExamplePage.tsx" -import { microwaveAtom } from "./atoms.ts" -import { MicrowaveEvents } from "./machine.ts" - -export function MicrowavePage() { - const stateResult = useAtomValue(microwaveAtom.state) - const send = useAtomSet(microwaveAtom.send) - - return ( - - {Match.value(stateResult).pipe( - Match.tagsExhaustive({ - Initial: () =>
Starting the microwave…
, - Failure: () =>
The microwave failed to start.
, - Success: ({ value: state }) => { - const oven = state.state - const open = oven.path === "Oven.Open" - const cooking = oven.path === "Oven.Closed" && oven.state.path === "Oven.Closed.Cooking" - const elapsedSeconds = cooking ? oven.state.value.elapsedSeconds : 0 - const engineName = cooking ? "Cooking" : "Idle" - const doorName = open ? "Open" : "Closed" - - return ( -
-
- - -
-
-

Hierarchical safety state

-

{cooking ? `Cooking · ${elapsedSeconds}s` : open ? "Idle · door open" : "Idle · door closed"}

-

- Engine: {engineName} · Door: {doorName} -

-
- - -
-

- {open - ? "Power is disabled while the door is open." - : "Opening the door while cooking stops the engine."} -

-
-
- ) - } - }) - )} -
- ) -} diff --git a/examples/playground/src/examples/microwave/atoms.ts b/examples/playground/src/examples/microwave/atoms.ts deleted file mode 100644 index 2e6ff9f..0000000 --- a/examples/playground/src/examples/microwave/atoms.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { AtomMachine } from "@typeonce/effect-machine/reactivity" -import { MicrowaveMachine } from "./machine.ts" - -export const microwaveAtom = AtomMachine.make(MicrowaveMachine) diff --git a/examples/playground/src/examples/microwave/machine.ts b/examples/playground/src/examples/microwave/machine.ts deleted file mode 100644 index 95edca5..0000000 --- a/examples/playground/src/examples/microwave/machine.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Machine } from "@typeonce/effect-machine" -import { Schema } from "effect" - -export const MicrowaveState = Schema.TaggedUnion({ - Cooking: { elapsedSeconds: Schema.Number } -}) - -export const MicrowaveEvents = Machine.events( - Schema.TaggedUnion({ - PowerPressed: {}, - DoorOpened: {}, - DoorClosed: {} - }) -) - -export const MicrowaveStates = Machine.states({ - Oven: { - initial: "Closed", - states: { - Closed: { - initial: "Idle", - states: { - Idle: {}, - Cooking: MicrowaveState.cases.Cooking - } - }, - Open: {} - } - } -}) - -export const MicrowaveMachine = Machine.make({ - id: "Microwave", - states: MicrowaveStates.states, - events: MicrowaveEvents, - initial: (to) => - to.Oven.initial.resolve(({ target }) => target.from((oven) => oven.Closed.from((closed) => closed.Idle.from()))) -}).handle({ - Oven: { - states: { - Closed: { - on: { - DoorOpened: (to) => to.branch.Oven.Open() - }, - states: { - Idle: { - on: { - PowerPressed: (to) => to.local.Cooking().resolve(({ target }) => target.from({ elapsedSeconds: 0 })) - } - }, - Cooking: { - invoke: (from) => - from.timer("cooking-second", "1 second").onDone((to) => - to.local.Cooking().resolve(({ state, target }) => - target.from({ elapsedSeconds: state.elapsedSeconds + 1 }) - ) - ), - on: { - PowerPressed: (to) => to.local.Idle() - } - } - } - }, - Open: { - on: { - DoorClosed: (to) => to.local.Closed.initial - } - } - } - } -}) diff --git a/examples/playground/src/examples/traffic-light/TrafficLightPage.tsx b/examples/playground/src/examples/traffic-light/TrafficLightPage.tsx deleted file mode 100644 index a70c5d7..0000000 --- a/examples/playground/src/examples/traffic-light/TrafficLightPage.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useAtomSet, useAtomValue } from "@effect/atom-react" -import { Match } from "effect" -import { ExamplePage } from "../../components/ExamplePage.tsx" -import { trafficLightAtom } from "./atoms.ts" -import { trafficLightDurations, TrafficLightEvents } from "./machine.ts" - -export function TrafficLightPage() { - const stateResult = useAtomValue(trafficLightAtom.state) - const send = useAtomSet(trafficLightAtom.send) - - return ( - - {Match.value(stateResult).pipe( - Match.tagsExhaustive({ - Initial: () =>
Starting the signal cycle…
, - Failure: () =>
The traffic light failed to start.
, - Success: ({ value: state }) => { - const signal = state.path - const red = signal === "Red" || signal === "RedYellow" - const yellow = signal === "RedYellow" || signal === "Yellow" - const green = signal === "Green" - const duration = trafficLightDurations[signal] - - return ( -
-
- - - -
-
-

Current state

-

{signal === "RedYellow" ? "Red + yellow" : signal}

-

- The next transition is scheduled in {(duration / 1_000).toFixed(1)} seconds by a state-scoped{` `} - an inline after invocation. -

-
- -
-
- -
-
-
- ) - } - }) - )} -
- ) -} diff --git a/examples/playground/src/examples/traffic-light/atoms.ts b/examples/playground/src/examples/traffic-light/atoms.ts deleted file mode 100644 index b47e61e..0000000 --- a/examples/playground/src/examples/traffic-light/atoms.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { AtomMachine } from "@typeonce/effect-machine/reactivity" -import { TrafficLightMachine } from "./machine.ts" - -export const trafficLightAtom = AtomMachine.make(TrafficLightMachine) diff --git a/examples/playground/src/examples/traffic-light/machine.ts b/examples/playground/src/examples/traffic-light/machine.ts deleted file mode 100644 index ed66f8b..0000000 --- a/examples/playground/src/examples/traffic-light/machine.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Machine } from "@typeonce/effect-machine" -import { Schema } from "effect" - -export const TrafficLightEvents = Machine.events( - Schema.TaggedUnion({ - Reset: {} - }) -) - -export const trafficLightDurations = { - Red: 4_000, - RedYellow: 1_000, - Green: 4_000, - Yellow: 1_500 -} as const - -export const TrafficLightStates = Machine.states({ - Red: {}, - RedYellow: {}, - Green: {}, - Yellow: {} -}) - -export const TrafficLightMachine = Machine.make({ - id: "TrafficLight", - states: TrafficLightStates.states, - events: TrafficLightEvents, - initial: (to) => to.Red() -}).handle({ - Red: { - invoke: (from) => from.timer("red-timer", trafficLightDurations.Red).onDone((to) => to.full.RedYellow()), - on: { - Reset: (to) => to.full.Red().resolve(({ target }) => target.from(), { reenter: true }) - } - }, - RedYellow: { - invoke: (from) => from.timer("red-yellow-timer", trafficLightDurations.RedYellow).onDone((to) => to.full.Green()), - on: { - Reset: (to) => to.full.Red() - } - }, - Green: { - invoke: (from) => from.timer("green-timer", trafficLightDurations.Green).onDone((to) => to.full.Yellow()), - on: { - Reset: (to) => to.full.Red() - } - }, - Yellow: { - invoke: (from) => from.timer("yellow-timer", trafficLightDurations.Yellow).onDone((to) => to.full.Red()), - on: { - Reset: (to) => to.full.Red() - } - } -}) diff --git a/examples/playground/src/examples/turnstile/TurnstilePage.tsx b/examples/playground/src/examples/turnstile/TurnstilePage.tsx deleted file mode 100644 index b827527..0000000 --- a/examples/playground/src/examples/turnstile/TurnstilePage.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import { useAtomSet, useAtomValue } from "@effect/atom-react" -import { Match } from "effect" -import { useState } from "react" -import { ExamplePage } from "../../components/ExamplePage.tsx" -import { turnstileAtom } from "./atoms.ts" -import { TurnstileEvents } from "./machine.ts" - -export function TurnstilePage() { - const stateResult = useAtomValue(turnstileAtom.state) - const send = useAtomSet(turnstileAtom.send) - const [lastCommand, setLastCommand] = useState("Insert a coin to unlock the gate.") - - return ( - - {Match.value(stateResult).pipe( - Match.tagsExhaustive({ - Initial: () =>
Starting the turnstile…
, - Failure: () =>
The turnstile failed to start.
, - Success: ({ value: state }) => { - const locked = state.path === "Locked" - return ( -
-