diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 39c49bc..8aef4df 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -14,15 +14,29 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: sbt/setup-sbt@v1 # setup sbt
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
- name: Run tests
run: sbt +test
+
+ sim-smoke:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: sbt/setup-sbt@v1
+ - uses: actions/checkout@v5
+ - uses: actions/setup-node@v5
+ with:
+ node-version: '24'
+ - name: Link Scala.js bundle into JVM resources
+ run: sbt copySimJs
+ - name: Smoke-test the simulation bundle via node
+ run: node bifunctor-tagless/js/sim-smoke.js
+
build-native:
runs-on: ubuntu-latest
steps:
- uses: sbt/setup-sbt@v1 # setup sbt
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v5
- name: Build native app
- run: sbt "project bifunctor-tagless; GraalVMNativeImage/packageBin"
+ run: sbt "project bifunctor-taglessJVM; GraalVMNativeImage/packageBin"
- name: Check native app
- run: ./bifunctor-tagless/target/graalvm-native-image/bifunctor-tagless :help
+ run: ./bifunctor-tagless/jvm/target/graalvm-native-image/bifunctor-tagless :help
diff --git a/.gitignore b/.gitignore
index 7c003ed..1997e8b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -126,3 +126,16 @@ hs_err_pid*
### Scala template
*.class
*.log
+
+# Project-local sbt/coursier/ivy caches used by the containerised dev loop.
+.cache/
+
+# direnv-materialised devShell
+.direnv/
+
+# Scala.js bundle copied here by the `copySimJs` sbt task — regenerated on
+# demand from bifunctor-tagless-js. Not checked in.
+bifunctor-tagless/jvm/src/main/resources/webapp/main.js
+bifunctor-tagless/jvm/src/main/resources/webapp/*.js.map
+bifunctor-tagless/jvm/src/main/resources/webapp/internal-*.js
+
diff --git a/README.md b/README.md
index 93fa7f2..7756d1b 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,54 @@ curl -X GET http://localhost:8080/ladder
curl -X GET http://localhost:8080/profile/50753a00-5e2e-4a2f-94b0-e6721b0a3cc4
```
+### Reproducible toolchain via Nix (optional)
+
+A `flake.nix` is included. With Nix flakes enabled you can drop into a shell
+that has the exact JDK, sbt, Scala 3 and Node versions used by this project:
+
+```bash
+nix develop # one-off shell
+direnv allow # automatic — uses the bundled .envrc
+```
+
+### Perfect simulation in the browser (`bifunctor-tagless` only)
+
+The `bifunctor-tagless` variant is cross-built for the JVM and Scala.js. The
+same `LadderApi`/`ProfileApi` http4s routes that the JVM server exposes are
+also assembled in the browser into an in-process `LocalDispatcher`, which a
+`@JSExportTopLevel("LeaderboardSim")` object surfaces to JavaScript. The JS
+graph is configured with `Repo -> Dummy`, so it uses the same in-memory
+repositories that the JVM tests use — no network, no postgres, no docker.
+
+A small demo UI in `bifunctor-tagless/jvm/src/main/resources/webapp/` lets you
+call each endpoint with a radio toggle between **production** (real HTTP) and
+**simulation** (the in-page Scala.js build).
+
+To use it, run the single convenience script — it builds the Scala.js bundle,
+copies it next to the UI, and starts the server in dummy mode:
+
+```bash
+./launch-sim
+```
+
+Then open . You can also open
+`bifunctor-tagless/jvm/src/main/resources/webapp/index.html` directly via
+`file://` (CORS on the server allows the `null` origin used by `file://`).
+
+If you prefer the steps separately:
+
+```bash
+sbt copySimJs # build + copy the Scala.js bundle
+./launcher -u repo:dummy :leaderboard # start the server
+```
+
+The "Production" radio talks to `http://localhost:8080`; the "Simulation"
+radio calls `window.LeaderboardSim.call(method, path, body)`, which runs the
+exact same request through the in-browser http4s routes. State only persists
+within each mode — flipping back and forth is itself a useful demonstration
+that the simulation is a clean process that knows nothing about the real
+server's state.
+
#### Note
If `./launcher` command fails for you with some cryptic stack trace, there's most likely an issue with your Docker. First of all, check that you have `docker` and `contrainerd` daemons running. If you're using something else than Ubuntu, please stick to the relevant [installation page](https://docs.docker.com/engine/install/):
@@ -61,7 +109,7 @@ Both of them should have `Active: active (running)` status. If your problem isn'
Use `sbt` to build a native Linux binary with GraalVM NativeImage under Docker:
```bash
-sbt bifunctor-tagless/GraalVMNativeImage/packageBin
+sbt bifunctor-taglessJVM/GraalVMNativeImage/packageBin
```
If you want to build the app using local `native-image` executable (e.g. on a Mac), comment out the `graalVMNativeImageGraalVersion` key in `build.sbt` first.
@@ -69,13 +117,13 @@ If you want to build the app using local `native-image` executable (e.g. on a Ma
To test the native app with dummy repositories run:
```bash
-./bifunctor-tagless/target/graalvm-native-image/bifunctor-tagless -u scene:managed -u repo:dummy :leaderboard
+./bifunctor-tagless/jvm/target/graalvm-native-image/bifunctor-tagless -u scene:managed -u repo:dummy :leaderboard
```
To test the native app with production repositories in Docker run:
```bash
-./bifunctor-tagless/target/graalvm-native-image/bifunctor-tagless -u scene:managed -u repo:prod :leaderboard
+./bifunctor-tagless/jvm/target/graalvm-native-image/bifunctor-tagless -u scene:managed -u repo:prod :leaderboard
```
Notes:
diff --git a/bifunctor-tagless/js/sim-smoke.js b/bifunctor-tagless/js/sim-smoke.js
new file mode 100755
index 0000000..c05bba7
--- /dev/null
+++ b/bifunctor-tagless/js/sim-smoke.js
@@ -0,0 +1,114 @@
+#!/usr/bin/env node
+/**
+ * Smoke-test the in-browser simulation bundle outside the browser.
+ *
+ * Loads the linked Scala.js bundle from the JVM project's
+ * `resources/webapp/main.js` (produced by `sbt copySimJs`) and exercises the
+ * same four endpoints the README's `curl` snippets call: submit a score, get
+ * the ladder, set a profile, get the profile. Each call should return HTTP
+ * 200; mismatches fail the run with a non-zero exit code.
+ *
+ * Run from the repo root:
+ *
+ * sbt copySimJs
+ * node bifunctor-tagless/js/sim-smoke.js
+ *
+ * scala.js NoModule mode emits `let LeaderboardSim` at script top level. This
+ * is intentionally script-scoped (so a browser
+
+