--- url: https://wvlet.org/uni/book.md --- # The Uni Book *A guided tour of Scala 3 application development with Uni.* This book teaches you to build real applications with [Uni](https://github.com/wvlet/uni) on Scala 3, across the JVM, Scala.js, and Scala Native. It is written for developers who already read Scala comfortably and want to understand not only *how* Uni works but *why* its pieces are shaped the way they are. The Book is narrative. It is meant to be read front to back at first, and used as a jumping-off point afterwards. The short-form API documentation under [Guide](/guide/) and the module references (e.g. [Core](/core/), [HTTP](/http/)) stay where they are, and the Book links into them when you need depth. ## How to read it * Work from Part I to the end of Part II in order. Everything that follows assumes the mental model built in those two parts. * Each part opens with a short intro page; each chapter ends with a *"what you have, what comes next"* recap. If the recap doesn't feel true yet, re-read the chapter before moving on. * Type the examples. Reading them is not the same exercise. ## Table of contents ### [Foreword](./foreword) ### Part I — Getting Started * [Chapter 1: Getting Started](./ch01-00-getting-started) * [1.1 Installation](./ch01-01-installation) * [1.2 Hello, Uni!](./ch01-02-hello-uni) ### Part II — Building a CLI Application * [Chapter 2: A URL Fetcher](./ch02-00-cli-app) ### Part III — Core Concepts * [Chapter 3: Wiring with Design](./ch03-00-design) * [Chapter 4: Testing with UniTest](./ch04-00-testing) * [Chapter 5: Logging That Finds You](./ch05-00-logging) * [Chapter 6: Data In, Data Out — JSON & MessagePack](./ch06-00-data) ### Part IV — Async & Control Flow * [Chapter 7: Rx, the Composable Stream](./ch07-00-rx) * [Chapter 8: Living With Failure — Retry, Circuit Breakers, Resources](./ch08-00-control) ### Part V — HTTP & RPC * [Chapter 9: HTTP Clients and Servers](./ch09-00-http) * [Chapter 10: Typed RPC](./ch10-00-rpc) ### Part VI — Cross-Platform Development * [Chapter 11: One Codebase, Three Runtimes](./ch11-00-cross-platform) ### Part VII — Scala.js in the JavaScript Ecosystem * [Chapter 12: Calling NPM Modules from Scala.js](./ch12-00-npm-facades) * [Chapter 13: Bundling with Vite](./ch13-00-vite) ### Part VIII — Scala Native and the C/Rust World * [Chapter 14: Calling C from Scala Native](./ch14-00-calling-c) * [Chapter 15: Exposing Scala Native to C and Rust](./ch15-00-exposing-native) ### Part IX — Capstone * [Chapter 16: A Bookmarks Service](./ch16-00-capstone) ### Appendices * [Appendix A: Scala 3 Syntax Notes for This Book](./appendix-a-scala3) * [Appendix B: Uni and Airframe — History and Relationship](./appendix-b-airframe) * [Appendix C: Glossary](./appendix-c-glossary) ## Status All parts are drafted, Foreword through the appendices. The book is best read front to back the first time; afterwards, use it as a jumping-off point into the [reference docs](/guide/). --- --- url: https://wvlet.org/uni/book/foreword.md --- # Foreword Scala 3 is a small, sharp language. The standard library is careful, the type system is expressive, and the compiler will happily tell you when your ideas do not fit together. Those are the good parts. The parts the standard library leaves to you are the ones every real application still has to answer: * How do my objects get wired together, and when are they cleaned up? * How do I structure logging so that when a request goes wrong, I find the one line that mattered? * How do I ship an HTTP client that retries the right errors, and no others? * How do I write one piece of business logic that runs in the JVM, in a browser, and in a compiled binary? **Uni** is the collection of small, opinionated libraries that Scala applications reach for to answer those questions. It is a distillation of code from the [Airframe](https://github.com/wvlet/airframe) project, refined for Scala 3 and for a future where Scala.js and Scala Native are first-class targets, not afterthoughts. This book is the long-form counterpart to Uni's reference docs. The reference answers *"what does this API do?"*. This book answers *"how do I build real applications with these pieces, and why are the pieces shaped this way?"*. By the end of it, you should be able to open an empty directory and sketch, in your head, the file layout and the dependency graph of a reasonable Scala 3 service — and have opinions about each choice. You will build two things as you read: 1. A small CLI application (Part II). 2. A single codebase that compiles to the JVM, to JavaScript, and to a native binary (Part VI). Along the way the book will explain, honestly, when Uni is the right tool and when a different tool would serve you better. No library earns trust by hiding its edges. Thank you for spending time with this. Let's start. — *The Uni team* [Next → Chapter 1: Getting Started](./ch01-00-getting-started) --- --- url: https://wvlet.org/uni/book/ch01-00-getting-started.md --- # 1. Getting Started Before you can write Uni code, your machine needs a JDK, an sbt that knows how to fetch Scala 3, and a project wired to pull Uni in. This part of the book gets you there, then writes enough of a program to prove everything is hooked up. Two chapters, in order: * [1.1 Installation](./ch01-01-installation) — prerequisites, a minimal `build.sbt`, and how to pull Uni into JVM-only, Scala.js, and Scala Native projects. * [1.2 Hello, Uni!](./ch01-02-hello-uni) — your first runnable Uni program. It is only a dozen lines, but by the end of it you will have met the two concepts that thread through the rest of the book: **logging** and **Design** (object wiring). By the end of Part I, a fresh clone of your project should compile and run. Part II then turns that starting point into a real command-line application. [← Foreword](./foreword) | [Next → 1.1 Installation](./ch01-01-installation) --- --- url: https://wvlet.org/uni/book/ch01-01-installation.md --- # 1.1 Installation Uni is a library, not a framework. You add it to a Scala 3 project with a single line in `build.sbt` and you are done. This chapter walks you through getting the tools, creating a minimal project, and confirming the setup. If you already have Scala 3 and sbt installed, jump to [Adding Uni to a project](#adding-uni-to-a-project). ## Prerequisites You need three things: | Tool | Minimum version | Why | |------|-----------------|-----| | JDK | 17 | Scala 3 targets modern JVMs. 17 is the LTS floor most of the ecosystem is on. | | sbt | 1.9 | The build tool. Anything older may not know about current Scala.js / Native plugins. | | Scala 3 | 3.9 or newer | Uni is compiled with Scala 3.9 (the current LTS), and an older compiler cannot read its TASTy. | A JDK 17+ and sbt 1.9+ will pull the right Scala 3 compiler on demand. ### Installing the JDK and sbt The single easiest path on any OS is [Coursier](https://get-coursier.io/): ```bash cs setup ``` That command installs a JDK, `sbt`, `scala`, and sets up your `PATH`. If you prefer native package managers: * **macOS:** `brew install sbt openjdk@21` * **Linux (Debian/Ubuntu):** install `sbt` from the [official APT repo](https://www.scala-sbt.org/download.html) and `openjdk-21-jdk` from your distribution. * **Windows:** use [Scoop](https://scoop.sh/) or the [sbt MSI installer](https://www.scala-sbt.org/download.html). Confirm both tools: ```bash $ java --version openjdk 21.0.4 2024-07-16 LTS $ sbt --version sbt script version: 1.9.8 ``` ## Adding Uni to a project Create a directory and drop two files in it. ``` hello-uni/ ├── build.sbt └── src/main/scala/Main.scala ``` `build.sbt`: ```scala val scala3Version = "3.9.0" ThisBuild / scalaVersion := scala3Version lazy val hello = project .in(file(".")) .settings( name := "hello-uni", libraryDependencies += "org.wvlet.uni" %% "uni" % "2026.1.35" ) ``` `src/main/scala/Main.scala`: ```scala @main def main(): Unit = println("hello") ``` Run it: ```bash $ sbt run [info] running main hello ``` The first `sbt run` will download Scala 3 and Uni's transitive dependencies. Subsequent runs are fast. > **Why no "framework initialization" step?** Uni is deliberately a > library — a collection of small pieces you pull from. There is no > bootstrap ceremony, no annotation processor to wire up, no agent to > install. The moment sbt resolves the dependency, you can import and > use any of it. ## Cross-platform projects Most Uni code works unchanged on **JVM**, **Scala.js**, and **Scala Native**. On **sbt 2.x**, cross-build with Uni's own [sbt-uni-crossproject](/build/sbt-uni-crossproject) plugin (the portable-scala plugins are not published for sbt 2.x): ```scala // project/plugins.sbt addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.5.12") addSbtPlugin("org.wvlet.uni" % "sbt-uni-crossproject" % "2026.1.35") ``` ```scala // build.sbt lazy val hello = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .in(file(".")) .settings( scalaVersion := "3.9.0", libraryDependencies += "org.wvlet.uni" %% "uni" % "2026.1.35" ) ``` Two parts of that snippet are worth a moment. `.crossType(CrossType.Pure)` says *"all of this project's sources are shared across platforms"*. With `Pure`, your project looks like this on disk: ``` hello/ ├── build.sbt ├── .jvm/ ← JVM-specific code (rarely needed with Uni) ├── .js/ ← Scala.js-specific code (rarely needed with Uni) ├── .native/ ← Scala Native-specific code (rarely needed with Uni) └── src/ ├── main/scala/ ← shared code, compiled for all three platforms └── test/scala/ ← shared tests ``` The `.jvm`, `.js`, and `.native` directories are per-platform project roots. Each is a distinct sbt subproject with its own `target/`, and each is where **platform-specific** code goes when you need it: `.jvm/src/main/scala` is seen only by the JVM build, `.js/src/main/scala` only by the Scala.js build, and `.native/src/main/scala` only by the Scala Native build. The code everyone shares lives at the module root under `src/main/scala` and `src/test/scala`. > **With Uni, you rarely have to.** Uni's public API — `Design`, > `Logger`, `Http`, `Rx`, `JSON`, `MessagePack` — is the same on every > platform. Where a platform difference exists (filesystem access, > timers, threading), Uni exposes one abstraction that resolves to the > right implementation under the covers. For a typical Uni > application, *all* of your code can live in the shared > `src/main/scala`, and that is usually the whole story. The > `./src/main/scala` folders stay empty unless you reach for > a genuinely platform-only dependency — a JVM-only Java library, a > browser DOM API, a native syscall. You will see examples of that in > Chapter 11. On sbt 2.x the `%%` operator tells sbt: *pick the version of `uni` that was built for whichever platform I am compiling for right now* (it encodes the platform suffix, so the older `%%%` is no longer needed). Your Scala source does not change between platforms; only the build setting does. We will build a real cross-platform codebase in Chapter 11. For now know that it is one cross-plugin, one `CrossType.Pure`, and a `%%` away — and that everything you write goes in `src/main/scala`. ## IDE setup Uni uses only standard Scala 3 features, so any Scala 3-aware IDE works. The common choices: * **IntelliJ IDEA** with the Scala plugin. * **VS Code** with [Metals](https://scalameta.org/metals/). * **Neovim** with Metals. Open the project directory, let the IDE import the build, and you are done. ## What you have, what comes next You now have: * A JDK, sbt, and Scala 3 on your `PATH`. * A `build.sbt` that pulls Uni in as a dependency. * Proof that `sbt run` works on an empty program. The [next chapter](./ch01-02-hello-uni) replaces that `println` with actual Uni code — and in doing so meets the two ideas that thread through the rest of the book: **logging** and **object wiring**. [← 1. Getting Started](./ch01-00-getting-started) | [Next → 1.2 Hello, Uni!](./ch01-02-hello-uni) --- --- url: https://wvlet.org/uni/book/ch01-02-hello-uni.md --- # 1.2 Hello, Uni! With Uni on the classpath, the shortest useful program you can write is one that logs a line. That seems small, but it introduces two of Uni's core ideas — the **LogSupport trait** and **source-location capture** — and sets up the next step, where we let **Design** wire objects together for us. ## The smallest Uni program Replace `src/main/scala/Main.scala` from the previous chapter with: ```scala import wvlet.uni.log.LogSupport object Main extends LogSupport: def main(args: Array[String]): Unit = info("Hello, Uni!") ``` Run it: ```bash $ sbt run 2026-04-18 09:00:00.000-0700 info [Main] Hello, Uni! - (Main.scala:5) ``` Four things are worth pausing on. ### `LogSupport` is a trait, not a wrapper `LogSupport` mixes in two things: a `logger` bound to the enclosing class name, and methods (`info`, `warn`, `error`, `debug`, `trace`) that call it. You do not construct a `Logger`; you declare that this class *has* one. That is the whole of the logging API for day-to-day code. ### The logger name is the class name The output starts with `[Main]`. That is the simple name of the class that mixed in `LogSupport`. When you search logs later, the grep is on a string you can see right here in the source file. ### The source location is captured automatically The `(Main.scala:5)` at the end of the line is free. Uni captures it at compile time using Scala 3's `scala.quoted.Quotes` API, so there is no stack-walking cost at runtime. When a production incident makes you ask *"which line produced this?"*, the answer is already printed. ### Scala 3 syntax is the default No `new`, no `class Main { ... }` braces, no `extends App`. The Scala 3 indentation style is what Uni uses in its own code and what this book will use throughout. If you prefer braces, the compiler still accepts them — but the examples here will stay in the indented style. ## Adding a collaborator: wire it by hand A real program has more than one class. Let's add a `Greeter` and have `Main` use it. ```scala import wvlet.uni.log.LogSupport class Greeter extends LogSupport: def greet(name: String): Unit = info(s"Hello, ${name}!") object Main extends LogSupport: def main(args: Array[String]): Unit = val greeter = Greeter() greeter.greet("Uni") ``` Output: ``` info [Greeter] Hello, Uni! - (Main.scala:5) ``` Two things changed. The logger name shifted to `[Greeter]` (because the `info` call now happens inside the `Greeter` class), and `Main` now constructs the `Greeter` explicitly with `Greeter()`. In a three-line program, manual construction is fine. In a real application it is not: * Every caller needs to know how every callee is built. * When a collaborator grows a dependency ("now `Greeter` needs a `Config`"), every caller changes. * Swapping an implementation in a test means passing a different constructor argument in every call site. Uni offers a better tool. ## Letting Design do the wiring The same program, this time wired by `Design`: ```scala import wvlet.uni.design.Design import wvlet.uni.log.LogSupport class Greeter extends LogSupport: def greet(name: String): Unit = info(s"Hello, ${name}!") @main def main(): Unit = val design = Design.newDesign .bindSingleton[Greeter] design.build[Greeter] { greeter => greeter.greet("Uni") } ``` Run it again — same output, but now something structural has changed. * `Design.newDesign` gives you an empty, immutable wiring configuration. * `.bindSingleton[Greeter]` says *"when someone asks for a `Greeter`, build one and share it."* No instance is created yet. * `design.build[Greeter] { greeter => ... }` opens a **session**, builds the `Greeter` (this is the moment it is actually constructed), hands it to your block, and then shuts the session down when the block returns. Any resources attached to the session are released at that shutdown. You did not have to tell `Main` how `Greeter` is constructed. You did not have to thread a `Greeter` through intermediate functions. You asked for one, and you got one. ### Why this instead of `Greeter()`? Fair question — this is exactly the point. With three lines of code, `Greeter()` is less ceremony. What `Design` buys you shows up when: * `Greeter` gains a dependency (a `Config`, an `HttpClient`). You only update the `Design`, not every call site. * You want a different `Greeter` in tests (say, one that captures messages). You replace one line in a test-specific design. * You want to tie *shutdown behavior* to *construction* (close the HTTP pool when the session ends). Design has a hook for that. We will meet all three of those in Part III. For now, treat `Design.newDesign.bindSingleton[X].build[X] { x => ... }` as the shape your `main` methods will have from here on. ## What you have, what comes next You can now: * Print log lines from a Scala 3 program with source locations. * Construct one object, then three, using `Design` instead of by hand. * Read a Uni-idiomatic `main` method and recognize its shape. The [next chapter](./ch02-00-cli-app) takes these primitives and builds something you would actually run: a small command-line tool that fetches a URL and prints what it gets back. That single program will introduce argument parsing, HTTP, and the first failure modes Uni helps you handle. [← 1.1 Installation](./ch01-01-installation) | [Next → 2. A URL Fetcher](./ch02-00-cli-app) --- --- url: https://wvlet.org/uni/book/ch02-00-cli-app.md --- # 2. A URL Fetcher This chapter builds one complete program — a small command-line tool that fetches a URL, prints the status line, and optionally dumps the body. It is the first real Uni app we write, and the point is to meet four pieces together, in context: * **Launcher** — turn command-line arguments into a typed object. * **HttpClient** — make a request and inspect the response. * **Design** — wire the two together so they can be replaced independently. * **UniTest** — write a fast, dependency-free test. You will not learn everything about any of these in this chapter; Parts III–V take each one apart. What you will get is a working mental shape: *what goes where, and why*. ## What we are building A tool called `fetch` that behaves like this: ```bash $ fetch https://httpbin.org/get 200 OK (https://httpbin.org/get) $ fetch --show-body https://httpbin.org/get 200 OK (https://httpbin.org/get) { "args": {}, "headers": { ... }, ... } $ fetch Missing URL. Usage: fetch [--show-body] ``` Small, but it has the three pieces every real CLI has: it parses args, does I/O, and reports success or failure. ## Step 1: Parse arguments with Launcher Create `src/main/scala/Fetch.scala` and start with the argument shape: ```scala import wvlet.uni.cli.launcher.{Launcher, argument, option} case class FetchArgs( @option(prefix = "--show-body", description = "Print response body") showBody: Boolean = false, @argument(name = "url", description = "URL to fetch") url: String = "" ) @main def fetch(args: String*): Unit = val parsed = Launcher.execute[FetchArgs](args.toArray) if parsed == null then return // user requested --help; Launcher already printed it println(s"url='${parsed.url}', showBody=${parsed.showBody}") ``` Try it: ```bash $ sbt "runMain fetch --show-body https://example.com" url='https://example.com', showBody=true ``` Three things to notice. **`FetchArgs` is a plain case class.** The `@option` and `@argument` annotations are metadata; the class itself is data. This is a recurring Uni shape: configuration and input are ordinary Scala types, and a separate tool (`Launcher`) populates them. **Defaults live with the field.** `showBody = false` is how you say "this flag is optional". `url = ""` is a sentinel we will check below. **`Launcher.execute[FetchArgs](args)` does two things.** It parses, and it constructs — returning a `FetchArgs` ready to use. If you passed malformed flags, it prints help and exits; if the user passed `--help` (or invoked the program with no arguments, which is the default help trigger) `Launcher` prints the annotation-derived usage text and returns `null`. The `if parsed == null then return` line is how you let that help-message path out of your `main`. > **Why annotate the fields instead of calling a builder?** > The annotation form keeps the case class *the* source of truth for > what the CLI accepts. There is no second list to keep in sync, no > place for the "declared flag" and "consumed flag" to drift apart. ## Step 2: Fetch the URL Add a service that does the actual HTTP call. The service takes its `HttpSyncClient` as a constructor parameter instead of creating one inside — so whoever builds `UrlFetcher` decides how long the client lives: ```scala import wvlet.uni.http.{HttpSyncClient, Request} import wvlet.uni.log.LogSupport class UrlFetcher(client: HttpSyncClient) extends LogSupport: def fetch(url: String, showBody: Boolean): Unit = info(s"GET ${url}") val response = client.send(Request.get(url)) println(s"${response.status} (${url})") if showBody then println(response.contentAsString.getOrElse("")) ``` A few things to notice. `Http.client.newSyncClient` (which we will call in the next step) produces a client with sensible defaults: connection pooling, JSON content negotiation, and — importantly — a retry policy. If `httpbin.org` returns a 5xx or the TCP connection flakes, the client retries with exponential backoff before giving up. We lean on that in Chapter 8. `response.status` is a typed `HttpStatus` (not a number), so you can pattern-match on it without digging through strings. `response.contentAsString` returns `Option[String]` — some responses genuinely have no body (a `204 No Content`, for instance), and the type reflects that. We unwrap with `.getOrElse("")` at the print site. Because `UrlFetcher` no longer owns its client, we can tell `Design` to own it instead — and to close it cleanly when the session ends. ## Step 3: Wire it with Design Now connect the pieces in `main`: ```scala import wvlet.uni.cli.launcher.{Launcher, argument, option} import wvlet.uni.design.Design import wvlet.uni.http.{Http, HttpSyncClient, Request} import wvlet.uni.log.LogSupport case class FetchArgs( @option(prefix = "--show-body", description = "Print response body") showBody: Boolean = false, @argument(name = "url", description = "URL to fetch") url: String = "" ) class UrlFetcher(client: HttpSyncClient) extends LogSupport: def fetch(url: String, showBody: Boolean): Unit = info(s"GET ${url}") val response = client.send(Request.get(url)) println(s"${response.status} (${url})") if showBody then println(response.contentAsString.getOrElse("")) @main def fetch(args: String*): Unit = val parsed = Launcher.execute[FetchArgs](args.toArray) if parsed == null then return // --help was handled by Launcher if parsed.url.isEmpty then println("Missing URL. Usage: fetch [--show-body] ") sys.exit(1) val design = Design.newDesign .bindInstance[HttpSyncClient](Http.client.newSyncClient) .onShutdown(_.close()) .bindSingleton[UrlFetcher] design.build[UrlFetcher] { fetcher => fetcher.fetch(parsed.url, parsed.showBody) } ``` Run the real thing: ```bash $ sbt "runMain fetch https://httpbin.org/get" info [UrlFetcher] GET https://httpbin.org/get - (Fetch.scala:15) 200 OK (https://httpbin.org/get) ``` You now have the spine of every Uni application you will write: 1. **Parse** (`Launcher.execute`). 2. **Wire** (`Design.newDesign...`). 3. **Do** (`design.build[EntryPoint] { entry => ... }`). Everything else — more services, more wiring, more complex flows — slots into that spine. ## Why the extra ceremony? Right now you could inline `UrlFetcher` into `main` and save six lines. So what does the indirection buy? **Replaceability.** In the next step we will test this without actually hitting the network. That means swapping the *real* `UrlFetcher` for a stand-in. If `main` constructed `UrlFetcher` directly, the test would have to call `main` with monkey-patched globals. With `Design`, the swap is one line. **Lifecycle.** You saw a small example of this already: `.bindInstance[HttpSyncClient](...).onShutdown(_.close())` attaches the client's cleanup to the session `design.build` opens. When the block returns — whether normally or by throwing — the session shuts down and the client's `close()` runs. A real app might add a database connection, a server, and a scheduler, each with its own hook. One block controls when they all wind down. **Locality of wiring.** The entire dependency graph of your program is visible in one `Design.newDesign...` block. Growing from one service to ten does not scatter construction logic across the codebase. ## Step 4: A test, without the network Add `src/test/scala/FetchTest.scala`: ```scala import wvlet.uni.cli.launcher.Launcher import wvlet.uni.test.UniTest class FetchTest extends UniTest: test("parses --show-body and URL") { val args = Launcher.execute[FetchArgs](Array("--show-body", "https://example.com")) args.showBody shouldBe true args.url shouldBe "https://example.com" } test("defaults showBody to false") { val args = Launcher.execute[FetchArgs](Array("https://example.com")) args.showBody shouldBe false } test("treats missing URL as empty string") { val args = Launcher.execute[FetchArgs](Array.empty) args.url shouldBe "" } ``` Run it: ```bash $ sbt test [info] FetchTest: [info] - parses --show-body and URL [info] - defaults showBody to false [info] - treats missing URL as empty string ``` No network, no mocks, no fixtures. The arg-parsing layer is a pure function from `Array[String]` to `FetchArgs`, and Uni keeps it that way. For testing `UrlFetcher` itself *without* the network, we would replace its HTTP client with a stand-in via a test-only `Design`. We will do exactly that in Chapter 4 once we have met `Design`'s override mechanism. ## What you have, what comes next You have written a Scala 3 CLI tool end to end: * Typed CLI arguments with `@option` and `@argument`. * Real HTTP calls through `Http.client.newSyncClient`. * Object wiring and lifecycle through `Design.build`. * A test suite that runs in milliseconds and has no external dependencies. You have also met the shape every Uni program shares: **parse, wire, do, clean up**. The rest of the book takes each of those layers apart in depth. Next, [Part III](./ch03-00-design) goes back to the beginning and explains **Design** properly — why constructor wiring beats the alternatives, how singletons and sessions interact, and how to override bindings for tests and environments. [← 1.2 Hello, Uni!](./ch01-02-hello-uni) | [Next → 3. Wiring with Design](./ch03-00-design) --- --- url: https://wvlet.org/uni/book/ch03-00-design.md --- # 3. Wiring with Design Your app has a `UserService`, an `OrderService`, and a `ReportService`. All three need the same database connection. How do you get the connection to them — without making it a global, and without threading it by hand through every constructor at every call site? This is the question `Design` answers. You met it in Chapter 2 as the *wire* step; this chapter explains it properly. By the end you should be able to read an unfamiliar `Design.newDesign…` block and know exactly what it says. ## A first wiring Two services, one shared dependency: ```scala import wvlet.uni.design.Design class Database: def query(sql: String): Seq[String] = Seq("alice", "bob") class UserService(db: Database): def listUsers(): Seq[String] = db.query("select name from users") @main def main = val design = Design.newDesign .bindSingleton[Database] .bindSingleton[UserService] design.build[UserService] { users => println(users.listUsers()) } ``` ```bash $ sbt run List(alice, bob) ``` `UserService` takes its `Database` as a constructor parameter. It never constructs one, never looks one up — it just declares that it needs one. The wiring lives somewhere else entirely. ## Walking the code **Each class names its dependencies in its constructor.** `UserService(db: Database)` is a complete, honest statement of what the class needs to do its job. There is no hidden `Database.getInstance()` call buried three methods deep. If you can construct a `UserService`, you have everything it needs; if you can't, the compiler tells you which type is missing. **A `Design` is a plan, not a container.** `Design.newDesign` starts an empty plan. `bindSingleton[Database]` adds a line to it: "when something needs a `Database`, build one and share it." `bindSingleton[UserService]` adds another. A `Design` is an immutable value — every `bind…` call returns a new `Design`, so you can build them up, pass them around, and combine them without anyone mutating yours. **`build` turns the plan into objects.** `design.build[UserService] { … }` reads the plan, constructs the graph from the bottom up (the `Database` first, then the `UserService` that needs it), and hands you the root. When the block returns, the graph is torn down. ### Why not just pass it as a constructor argument yourself? You could. `UserService(Database())` works for two classes. The reason to reach for `Design` shows up at scale and under change: * **No globals.** A `Database` reachable through a global is reachable from anywhere, which means *anything* can depend on it without saying so. Dependencies you can't see are dependencies you can't test or replace. Constructor parameters keep the dependency graph honest. * **No service locator.** A `locator.get[Database]` hides the dependency from the type system — you discover a missing binding when the call runs, not when it compiles. `Design` resolves the graph from constructor *types*, so a missing piece is a wiring error you can find before shipping. * **One place to look.** The entire dependency graph of your program is one `Design.newDesign…` block. Growing from two services to twenty does not scatter `new` calls across the codebase. ## The four ways to bind `bindSingleton` is the common case, but you reach for different bindings depending on how the value is produced. ```scala import wvlet.uni.design.Design // 1. Build it for me, once, and share it. Design.newDesign.bindSingleton[Database] // 2. Use this exact object I already have. Design.newDesign.bindInstance[Database](Database()) // 3. This type is a trait — use this implementation for it. trait UserRepo class InMemoryUserRepo extends UserRepo Design.newDesign.bindImpl[UserRepo, InMemoryUserRepo] // 4. Build it with a function that itself needs dependencies. case class Config(url: String) Design.newDesign .bindInstance[Config](Config("jdbc:db")) .bindProvider[Config, Database] { config => Database() } ``` | Binding | Use it when | |---------|-------------| | `bindSingleton[A]` | `A` is a class Design can construct; you want one shared instance | | `bindInstance[A](v)` | you already hold the value (config, a pre-built client) | | `bindImpl[A, B]` | `A` is an interface and `B` is the implementation to use | | `bindProvider[D, A](d => a)` | building `A` needs other bound values | `bindImpl` is the seam that makes a program configurable: bind `UserRepo` to `InMemoryUserRepo` in development and to `PostgresUserRepo` in production, and nothing that *uses* `UserRepo` changes. See the full [binding reference](/core/design#binding-summary) for the complete set. ## Sessions and lifecycle `design.build[A] { … }` is shorthand for opening a **session**, building the graph inside it, and closing it when the block ends. A session is the lifetime of a set of wired objects. That lifetime is where cleanup hooks attach. Bind a value with an `onStart`/`onShutdown` hook and the session runs them at the edges: ```scala import wvlet.uni.design.Design class Server: def start(): Unit = println("listening") def stop(): Unit = println("stopped") val design = Design.newDesign .bindSingleton[Server] .onStart(_.start()) // when the session starts .onShutdown(_.stop()) // when the session ends — even on exception design.build[Server] { server => // server.start() has already run here println("serving requests") } // server.stop() has run by here ``` ```bash $ sbt run listening serving requests stopped ``` The shutdown hook runs whether the block returns normally or throws, so resources don't leak on the error path. Chapter 2 used the compact form of this — `.onShutdown(_.close())` on the HTTP client — and it is the same mechanism. For a type that is already `AutoCloseable`, Design can call `close()` for you; the [lifecycle reference](/core/design#lifecycle-management) lists every hook and the order they fire in. ```text design.build[App] { app => … } │ ├─ construct the graph (dependencies first) ├─ onStart hooks run │ ├──▶ your block runs (the body of build { }) │ ├─ onShutdown hooks run (even if the block threw) └─ session closed ``` When you need the session itself — to open short-lived **child sessions**, say, one per web request, while singletons live for the whole process — use `design.withSession { session => … }` instead of `build`. [Session management](/core/design#session-management) covers child sessions in depth. ## Overriding a design A `Design` is a value, and two designs combine with `+`. When both bind the same type, the one on the right wins. That single rule is the foundation of testing and per-environment configuration: ```scala // Production wiring, defined once. val appDesign = Design.newDesign .bindSingleton[Database] .bindSingleton[UserService] // A test swaps in a fake Database — everything else is unchanged. val testDesign = appDesign + Design.newDesign.bindInstance[Database](FakeDatabase()) testDesign.build[UserService] { users => // users is the real UserService, wired to FakeDatabase } ``` `UserService` does not know or care that its `Database` was replaced — it asked for a `Database`, and it got one. This is the override mechanism Chapter 4 builds on to test services without their real dependencies. See [Testing with Design](/core/design#testing) for the patterns. ## What you have, what comes next You can now read and write a `Design`: * Classes declare dependencies as **constructor parameters**; `Design` supplies them. * **`bindSingleton` / `bindInstance` / `bindImpl` / `bindProvider`** cover how a value is produced. * **`build`** opens a session, wires the graph, and tears it down; **`onStart` / `onShutdown`** hook the edges of that lifetime. * **`+`** overrides a design, which is how tests swap real dependencies for fakes. Next, [Chapter 4](./ch04-00-testing) puts that override seam to work immediately: now that you can wire a program, you can *test* it — swapping a real dependency for a fake without touching the code under test. [← 2. A URL Fetcher](./ch02-00-cli-app) | [Next → 4. Testing with UniTest](./ch04-00-testing) --- --- url: https://wvlet.org/uni/book/ch04-00-testing.md --- # 4. Testing with UniTest A service under test needs a database, a payment gateway, and the current time. The reflex is to mock all three. Uni pushes you the other way: run the *real* code, and substitute as narrowly as you can. This chapter is about writing tests that way — and why it produces tests you can trust. ## A first test Extend `UniTest`, declare cases with `test`, and assert with `shouldBe`: ```scala import wvlet.uni.test.UniTest class CalculatorTest extends UniTest: test("adds two numbers") { val result = 2 + 3 result shouldBe 5 } test("a list contains a value") { Seq("a", "b", "c") shouldContain "b" } ``` ```bash $ sbt test [info] CalculatorTest: [info] - adds two numbers [info] - a list contains a value ``` `test("name") { … }` is a case; the block is the body. There is no annotation, no separate runner to register. The assertion vocabulary is small and reads as English: `shouldBe`, `shouldNotBe`, `shouldContain`, and `shouldMatch { case … => }` for matching a shape rather than a value. The [UniTest reference](/core/unitest) has the full list. ## Substitute, don't mock The interesting question is the service with dependencies. You met the mechanism already, in [Chapter 3](./ch03-00-design): a `Design` is a value, and `+` overrides it. A test takes the real application wiring and swaps in exactly one piece. ```scala import wvlet.uni.design.Design import wvlet.uni.test.UniTest class UserServiceTest extends UniTest: test("looks up a user by id") { // appDesign is the real production wiring from Chapter 3. val testDesign = appDesign + Design.newDesign.bindInstance[Database](FakeDatabase()) testDesign.build[UserService] { users => users.findUser("123") shouldBe Some("Alice") } } ``` `UserService` is the real class, running its real logic. Only the `Database` underneath it is a fake — an in-process stand-in with no network. Every other binding stays production. The test exercises the code that ships, not a hollowed-out copy of it. ## Why not mock? A mock encodes *your belief* about how a dependency behaves: "when I call `query`, it returns these rows." The test then checks your code against that belief. If your belief is wrong — the real database orders results differently, throws a different exception, paginates — the mock-based test passes anyway, and the bug ships. You tested the mock, not the system. Uni's stance is to keep the real objects and replace only what you must: the genuinely external edge (a network, a clock, a payment processor) with an in-process fake you control. Everything between your entry point and that edge is real, so the test catches the integration mistakes a mock would paper over. A fake `Database` that actually stores and returns rows tells you more than a mock that returns a canned list. This is why Uni ships no mocking framework and the guides steer you away from one. The tool you need is `Design` override, which you already have. ## One suite, three platforms Because `UniTest` is itself cross-platform, a test you write once runs under the JVM, Scala.js, and Scala Native — the same `sbt test` story as [Chapter 11](./ch11-00-cross-platform), now for your tests. A suite over shared logic verifies all three builds at once, so "compiles on Native" becomes "*passes* on Native" without a second test to write. ## Fast on purpose Notice what these tests don't do: spin up a container, open a socket, hit a real clock. A `Design`-substituted suite is in-process and dependency-free, which is why it finishes in well under a second. That speed is not a bonus — it is the point. A suite you can run on every save gets run on every save; a slow one gets run before lunch, if then. Keeping tests fast keeps them used, and the substitution approach is what keeps them fast. ## What you have, what comes next You can now test a Uni application the way it's meant to be tested: * **`extends UniTest`** with `test("…") { … }` and a small assertion vocabulary — `shouldBe`, `shouldContain`, `shouldMatch`. * **`Design` override (`+`)** substitutes one dependency and keeps the rest real — the alternative to mocking. * One suite runs on **all three platforms**, and stays **fast** because it's in-process. Testing comes this early in the book on purpose. With `Design` from [Chapter 3](./ch03-00-design) in one hand and `UniTest` in the other, you can now *prove* a service works — before you've even met logging. Every feature the rest of the book introduces is something you can immediately write a fast, dependency-free test for. Next, [Chapter 5](./ch05-00-logging) adds the other thing every service wants from the start: logging that tells you not just *what* happened but *where*. [← 3. Wiring with Design](./ch03-00-design) | [Next → 5. Logging That Finds You](./ch05-00-logging) --- --- url: https://wvlet.org/uni/book/ch05-00-logging.md --- # 5. Logging That Finds You A log line that reads `save failed` is almost useless if your codebase has twenty `save` methods. The first question you ask is always the same: *where did this come from?* This chapter is about a logger that answers that for you, for free. ## Logging in one line Mix in `LogSupport` and call a level method: ```scala import wvlet.uni.log.LogSupport class OrderService extends LogSupport: def placeOrder(id: String): Unit = info(s"Placing order ${id}") // ... do the work ... ``` ```bash $ sbt run 2024-01-15 10:30:45.123+0900 info [OrderService] Placing order A-100 - (OrderService.scala:5) ``` Look at the end of that line: `(OrderService.scala:5)`. The log tells you the exact file and line that produced it. You did not pass it; you did not configure it. That is the whole idea. ## Walking the code **`LogSupport` is a mixin.** Extending it gives the class `trace`, `debug`, `info`, `warn`, and `error` methods and a logger named after the class (`[OrderService]`). There is no `private val logger = LoggerFactory.getLogger(getClass)` line to copy-paste and get wrong. **The five levels are ordered.** From quietest to loudest: `trace`, `debug`, `info`, `warn`, `error`. You set a threshold and everything at or above it prints. The [levels reference](/core/logging#log-levels) has the full table. **The source location is captured at compile time.** `(file:line)` is not found by walking a stack trace at runtime — that would be slow and would break under inlining. Uni captures it with a Scala 3 macro when your code compiles, so it is exact and free. This is the "finds you" payoff: in a real incident, the location turns "something failed" into "*this line* failed," and you skip the search. ## You don't guard log calls In many logging frameworks you learn to write this, because building the message is expensive and you don't want to pay for it when `debug` is off: ```scala // The defensive pattern you do NOT need here: if logger.isDebugEnabled then logger.debug(s"State dump: ${expensiveSnapshot()}") ``` With Uni you write the obvious thing: ```scala debug(s"State dump: ${expensiveSnapshot()}") ``` The level methods are `inline` macros, so the argument is only evaluated when the level is enabled. If `debug` is off, `expensiveSnapshot()` never runs and there is no allocation — the call compiles down to nothing. The guard is built in, so the readable form is also the fast form. See [zero-overhead logging](/core/logging#zero-overhead-logging-with-scala-macros) for what the macro expands to. ## Errors carry their cause Pass the exception as a second argument and the stack trace rides along: ```scala import wvlet.uni.log.LogSupport class PaymentService extends LogSupport: def charge(amount: Int): Unit = try gateway.charge(amount) catch case e: Exception => error(s"Charge of ${amount} failed", e) // message + full stack trace throw e ``` The log keeps your message *and* the exception's stack trace together, so you see the human-readable context and the machine detail in one record. ## Why the message is a sentence, not a struct Some ecosystems push you toward structured logs — every line a bag of key/value fields, meant to be parsed by a machine. Uni's logger is deliberately *not* that. A log line here is a sentence written for a person reading it during an incident: prose, with the source location and any exception attached. That is a choice, not an omission. Uni's position is that logs are **developer context**, not your metrics or analytics pipeline. When you need machine-readable records — counters, traces, events to aggregate — emit them explicitly to a system built for it, and keep logs for the human who is paging through them at 2 a.m. Mixing the two jobs tends to serve neither well. ## Tuning what prints You usually leave the defaults alone, but two knobs cover most needs: ```scala import wvlet.uni.log.{Logger, LogLevel} Logger.setDefaultLogLevel(LogLevel.DEBUG) // global threshold Logger("OrderService").setLogLevel(LogLevel.TRACE) // one logger, louder ``` A class that mixes in `LogSupport` already has a logger named after it, so you can dial one noisy component up or down without touching the others. Outside a class, `Logger("name")` gives you a named logger directly. To send logs to a rotating file — on the JVM, Node.js, or Native — point a [`FileLogHandler`](/core/logging#writing-logs-to-a-file) at a path. ## What you have, what comes next Logging is now a one-line, zero-ceremony tool: * **`extends LogSupport`** gives any class `trace`/`debug`/`info`/`warn`/`error` and a logger named after it. * Every message carries its **`(file:line)`**, captured at compile time. * Level methods are **macros**, so unguarded `debug(...)` calls cost nothing when disabled. * Logs are **human context** by design; `error(msg, e)` keeps the cause attached. Next, [Chapter 6](./ch06-00-data) turns to data crossing your program's edges — parsing JSON, writing MessagePack, and deriving both from your case classes with one line. [← 4. Testing with UniTest](./ch04-00-testing) | [Next → 6. Data In, Data Out](./ch06-00-data) --- --- url: https://wvlet.org/uni/book/ch06-00-data.md --- # 6. Data In, Data Out — JSON & MessagePack Every service eats and produces data. A request arrives as a JSON body; a cache stores a MessagePack frame; a config file is a blob of text. This chapter is about the boundary where bytes become Scala values and back — and how Uni crosses it without per-type boilerplate. There are two situations, and Uni has a tool for each: * You **don't** know (or control) the shape — a third-party response, a config blob. Parse it into a JSON tree and navigate. * You **do** own the shape — it's your case class. Derive a codec once and round-trip it to JSON *or* MessagePack. ## When you don't own the shape: parse and navigate `JSON.parse` turns text into a navigable tree: ```scala import wvlet.uni.json.* val doc = JSON.parse(""" { "user": { "id": 123, "name": "Alice" }, "posts": [ { "title": "Hello" }, { "title": "World" } ] } """) val name = doc("user")("name").toStringValue // "Alice" val id = doc("user")("id").toLongValue // 123 val first = doc("posts")(0)("title").toStringValue // "Hello" ``` You index into objects with `(key)` and arrays with `(index)`, then read a leaf with a typed accessor like `toStringValue` or `toLongValue`. When a path might be missing, navigate with `/` and pattern-match or use `Option` instead of asserting it exists. The [JSON reference](/core/json) covers the full DSL, including building and mutating trees. This is the right tool for *loosely* structured data. But when the shape is yours, threading `("user")("name")` through your code throws away the types you already have. ## When you own the shape: derive a codec Define the data as case classes and add `derives Weaver`: ```scala import wvlet.uni.weaver.Weaver case class User(id: Long, name: String) derives Weaver val alice = User(123, "Alice") val json = Weaver.toJson(alice) // {"id":123,"name":"Alice"} val back = Weaver.fromJson[User](json) // User(123, Alice) ``` ```bash $ sbt run {"id":123,"name":"Alice"} User(123,Alice) ``` `derives Weaver` asks the compiler to build a serializer for `User` from its structure. No annotations on each field, no hand-written `toJson`/`fromJson`, no companion-object ceremony. Nested case classes, collections, `Option`, enums, and sealed traits all derive the same way — see the [Weaver reference](/core/weaver) for the full type list. ## The same type, as binary Here is the part that pays off: the *same* `Weaver[User]` also speaks MessagePack, a compact binary encoding. You don't define a second codec — you call a different method: ```scala // Continues the User from above. val bytes: Array[Byte] = Weaver.weave(alice) // MessagePack val user = Weaver.unweave[User](bytes) ``` Reach for MessagePack when the bytes matter: a cache entry, a queue message, an internal RPC payload. It is smaller and faster to parse than JSON, and it is binary-safe. Reach for JSON when a human or a browser is on the other end. Either way, your *type* is the same; only the format at the edge changes. ## Why one codec for two formats It would be easy to ship a JSON library and a separate MessagePack library, each with its own derivation. Uni deliberately does not. The insight is that **the wire format is a boundary decision, not a modeling decision.** `User` is `User` whether it travels as text or as bytes — so you describe it once, and choose the encoding where the data actually leaves your program. That keeps the choice cheap. Storing sessions in Redis and decide MessagePack is leaner? Change `toJson` to `weave` at that one boundary; `User` does not move. A reader does not learn two derivation systems, and the two formats cannot drift apart, because they are the same `Weaver`. ::: tip Cross-platform, by construction `derives Weaver` resolves at **compile time** — no runtime reflection. That is what lets the identical codec run on the JVM, in the browser (Scala.js), and as a native binary. A reflection-based serializer would not survive the trip to Scala.js or Native; a derived one does. We return to this theme in [Chapter 11](./ch11-00-cross-platform). ::: ## What you have, what comes next You can now move data across your program's edges: * **`JSON.parse`** for data whose shape you don't own — navigate a tree with `(key)` / `(index)` and typed accessors. * **`derives Weaver`** for data you do own — one derivation, no per-field boilerplate. * **`Weaver.toJson` / `fromJson`** for text, **`Weaver.weave` / `unweave`** for MessagePack — same codec, format chosen at the boundary. * Derivation is compile-time, so the codec is cross-platform. That closes Part III: you can wire a program ([Design](./ch03-00-design)), see what it does ([Logging](./ch05-00-logging)), and move data through it. Next, [Part IV](./ch07-00-rx) makes those services *react* — `Rx`, Uni's composable stream, for values that change over time. [← 5. Logging That Finds You](./ch05-00-logging) | [Next → 7. Rx, the Composable Stream](./ch07-00-rx) --- --- url: https://wvlet.org/uni/book/ch07-00-rx.md --- # 7. Rx, the Composable Stream Two things in your program refuse to sit still. An async HTTP call does not return a `Response` — it returns one *later*. A configuration value or a UI field changes *while the app runs*. Both are "a value that isn't simply here right now," and Uni models both with one type: `Rx`. This chapter is about that type and the handful of operators that make it compose. ## A value that changes The simplest `Rx` is a variable you can watch: ```scala import wvlet.uni.rx.Rx val count = Rx.variable(0) count.subscribe { v => println(s"count is now ${v}") } count := 1 count := 2 ``` ```bash $ sbt run count is now 0 count is now 1 count is now 2 ``` `subscribe` runs your function once with the current value and again on every change. `:=` sets a new value. You are not polling and you are not wiring callbacks by hand — you describe what should happen when the value changes, and `Rx` makes it happen. ## Walking the code **`Rx[A]` is a description, not a result.** An `Rx[A]` says "this will produce zero or more `A` values over time." It does nothing until someone *runs* it — `subscribe` (react to every value) or `run` (run an effect) or `await` (block for the value, JVM only). Nothing fires until then, which means you can build an `Rx` pipeline freely and pay only when you attach a consumer. **`subscribe` hands you a `Cancelable`.** Watching a stream is a resource: it keeps a subscription alive. The return value is a `Cancelable` you call when you're done. This is the one piece of bookkeeping `Rx` asks of you — hold the handle, cancel it when the work that needed it ends. ```scala val sub = count.subscribe(v => println(v)) // ... later, when you no longer care ... sub.cancel ``` ## Composing without unwrapping The point of `Rx` is that you transform the *stream*, not the values one at a time. The operators look exactly like the ones on `Option` and `List`, because they mean the same thing: ```scala import wvlet.uni.rx.Rx val count = Rx.variable(0) val label: Rx[String] = count .filter(_ % 2 == 0) // keep even values .map(n => s"even: ${n}") // turn each into a label label.subscribe(println) count := 1 // filtered out — nothing prints count := 2 // prints: even: 2 ``` `map` transforms each value, `filter` drops the ones you don't want, and `flatMap` lets one stream depend on another (run a second `Rx` for each value of the first). You build a pipeline once; every value that flows through `count` takes the whole path. The [operators reference](/rx/operators) lists the full set, including `zip` to combine streams and `recover` to handle errors. ## Where Rx shows up: async results Back in Chapter 2 the *synchronous* client returned an `HttpResponse` directly. The async client returns the value-over-time version of exactly that: ```scala import wvlet.uni.http.{Http, Request} val client = Http.client.newAsyncClient val title: Rx[String] = client .send(Request.get("https://example.com")) // Rx[HttpResponse] .map(_.status.toString) // Rx[String] title.subscribe(println) ``` The HTTP response is "a value that isn't here yet," so the async client gives you an `Rx[HttpResponse]`. You compose it with the same `map` you used on the counter — there is no separate callback API, no second mental model for "async" versus "reactive." One type covers both, which is why `Rx` is the backbone of async code across Uni: HTTP, WebSockets, and server-sent events all surface as `Rx`. ## Why Rx instead of Future or callbacks A `Future` starts running the moment you create it and can only ever deliver one value. A callback delivers many values but composes badly — nest two and you have a pyramid. `Rx` is chosen to avoid both traps: * **Lazy.** Building an `Rx` runs nothing. You assemble the pipeline, then decide when (and whether) to start it. A `Future` has already left the gate. * **One-shot *and* streaming.** The same type and operators serve a single async result and an endless event stream. You learn one thing. * **Cancellable.** `subscribe`/`run` return a `Cancelable`, so a stream you no longer need stops cleanly — no leaked timer, no listener firing into a dead component. * **Cross-platform.** `Rx` runs the same on the JVM, in the browser, and on Native, so async code moves between platforms unchanged. That cancellation handle is also how `Rx` cooperates with [Design](./ch03-00-design): give a session ownership of a subscription and closing the session cancels it, so streams wind down with the objects that created them. The [concurrency reference](/rx/concurrency) covers the threading and cancellation model in depth. ## What you have, what comes next You can now describe values that change and results that arrive later with one tool: * **`Rx[A]`** is a lazy description of zero or more `A` over time; nothing runs until you `subscribe`/`run`/`await`. * **`Rx.variable` + `:=`** is a value you can watch; **`map` / `filter` / `flatMap`** compose the stream. * **`subscribe` returns a `Cancelable`** — hold it, cancel it when done. * The **async HTTP client** returns `Rx[HttpResponse]`, so async and reactive code share one model. Next, [Chapter 8](./ch08-00-control) is about what happens when those calls *fail* — retries, circuit breakers, and resource lifetimes that clean up after themselves. [← 6. Data In, Data Out](./ch06-00-data) | [Next → 8. Living With Failure](./ch08-00-control) --- --- url: https://wvlet.org/uni/book/ch08-00-control.md --- # 8. Living With Failure — Retry, Circuit Breakers, Resources A call across a network will fail. Not *if* — *when*. The interesting question is what you do about it, and there are three different answers depending on the failure: * It was a blip — the packet dropped, the server hiccuped. **Retry it.** * The backend is genuinely down — every call is failing. **Stop hammering it.** * The call failed and left a connection open. **Clean it up anyway.** Uni gives you one primitive for each: `Retry`, `CircuitBreaker`, and `Control.withResource`. ## Retry a transient failure Wrap the flaky call in a retry policy and `run` it: ```scala import wvlet.uni.control.Retry val result = Retry .withBackOff(maxRetry = 3) .run { callFlakyService() // runs up to 4 times: 1 try + 3 retries } ``` `withBackOff` waits longer between each attempt — a tenth of a second, then more, then more — instead of retrying in a tight loop. That spacing matters: a service that just hiccuped needs a moment, and a hundred clients retrying in lockstep would knock it over again the instant it recovers. For that lockstep problem specifically, `Retry.withJitter()` randomizes the delays so clients don't synchronize. If every attempt fails, the policy gives up by throwing `MaxRetryException`. The [retry reference](/control/retry) covers tuning the intervals and choosing *which* errors to retry. ### Why you can't retry everything Retry has a precondition that is easy to miss: the operation must be **idempotent** — safe to run more than once. Reading a URL is idempotent; running it twice returns the same page. `POST /charge-card` is *not* — run it twice and you've billed the customer twice. Before you wrap a call in `Retry`, ask: if this runs twice, is the world the same as if it ran once? If not, retrying turns a transient failure into a correctness bug. Make the call idempotent first (an idempotency key, an upsert), *then* retry it. ## Stop calling a backend that's down Retry handles a blip. It is exactly the wrong tool when a backend is *actually* down: now every request retries four times before failing, so you've quadrupled the load on a service that's already on the floor, and every caller waits through the full backoff to get an error it could have had instantly. A `CircuitBreaker` watches the failure rate and, once it crosses a threshold, "opens" — failing calls instantly without touching the backend at all: ```scala import wvlet.uni.control.{CircuitBreaker, CircuitBreakerOpenException} val breaker = CircuitBreaker.withConsecutiveFailures(5) try breaker.run { callBackend() } catch case e: CircuitBreakerOpenException => // fail fast — the breaker is open, we didn't even try useFallback() ``` It runs a small state machine: **closed** (calls pass through), **open** (calls fail instantly), and **half-open** (after a cooldown, it lets one trial call through to see if the backend recovered). The point is to *fail fast* while a dependency is down — protecting both the caller, who gets an immediate answer, and the backend, which gets room to recover instead of a retry storm. `withConsecutiveFailures(5)` trips after five failures in a row; `withFailureThreshold` trips on a failure *rate*. See the [circuit breaker reference](/control/circuit-breaker) for the states and recovery policy. ## Clean up no matter what Both tools above can fail the call. When they do, anything the call opened — a file, a socket, a connection — still needs to close. `Control.withResource` guarantees it: ```scala import wvlet.uni.control.Control Control.withResource(openConnection()) { conn => conn.query("select 1") } // conn.close() runs here — even if query threw ``` This is the loan pattern: `withResource` lends you the resource for the duration of the block and closes it when the block exits, on the normal path *and* the exception path. You can't forget the `close()`, because you never wrote it. If that mechanism feels familiar, it should — it is the same guarantee `Design`'s `onShutdown` gives a whole session ([Chapter 3](./ch03-00-design)). `Control.withResource` is the local, block-scoped version; `Design` is the application-scoped version. Reach for `Control.withResource` when a resource lives for one operation, and for a `Design` lifecycle hook when it lives for the program. ## Putting them together A hardened call to a flaky dependency uses all three at once: a `Control.withResource` for the connection, a `CircuitBreaker` to fail fast when the dependency is down, and `Retry` to ride out the blips that get through: ```scala import wvlet.uni.control.{CircuitBreaker, Control, Retry} val breaker = CircuitBreaker.withConsecutiveFailures(5) def fetch(url: String): String = breaker.run { Retry.withBackOff(maxRetry = 3).run { Control.withResource(openConnection()) { conn => conn.get(url) } } } ``` Read it from the inside out: the resource is always cleaned up, retries absorb transient failures, and the breaker stops the whole thing from piling onto a backend that's already down. Each layer does one job, and they compose because each is an ordinary value wrapping an ordinary block. ## What you have, what comes next You can now write code that survives the network: * **`Retry.withBackOff(...).run { }`** absorbs transient failures — *only for idempotent operations*. * **`CircuitBreaker.run { }`** fails fast when a backend is down, so you stop making it worse. * **`Control.withResource(r) { }`** closes resources on every path, the block-scoped cousin of `Design`'s `onShutdown`. That closes Part IV: your services can react ([Rx](./ch07-00-rx)) and recover (this chapter). Next, [Part V](./ch09-00-http) puts them on the network for real — building HTTP clients and servers, then typed RPC on top. [← 7. Rx, the Composable Stream](./ch07-00-rx) | [Next → 9. HTTP Clients and Servers](./ch09-00-http) --- --- url: https://wvlet.org/uni/book/ch09-00-http.md --- # 9. HTTP Clients and Servers Chapter 2 made a single HTTP call to get a CLI tool working. This chapter takes HTTP seriously from both sides: the **client** that calls out, and the **server** that answers. The two share a request/response model, so once you know one, the other reads naturally. ## The client, synchronous first The simplest client blocks until the response arrives: ```scala import wvlet.uni.http.{Http, Request} val client = Http.client.newSyncClient val response = client.send(Request.get("https://httpbin.org/get")) println(response.status) // 200 OK println(response.contentAsString.getOrElse("")) // the body ``` `Request.get(url)` builds a request; `client.send` runs it and returns an `HttpResponse`. `response.status` is a typed `HttpStatus`, and `response.contentAsString` is an `Option[String]` because some responses have no body. You build other verbs the same way — `Request.post(url)`, then `.withJsonContent("""{"name":"Alice"}""")` to attach a body. Start with the sync client. It is the easy thing to reason about: one call, one result, top to bottom. Reach for async only when you need concurrency. ## The client, asynchronously The async client returns the value-over-time version of the response — an `Rx[HttpResponse]`, exactly the shape from [Chapter 7](./ch07-00-rx): ```scala import wvlet.uni.http.{Http, Request} val client = Http.client.newAsyncClient client .send(Request.get("https://httpbin.org/get")) .map(_.contentAsString.getOrElse("")) .subscribe(println) ``` Because the result is an `Rx`, you compose it with `map` and `subscribe` instead of blocking. The same operators you learned for streams work here — async HTTP is not a separate world. Both clients come configured with sensible defaults, including the retry policy from [Chapter 8](./ch08-00-control). Tune them with `withXxx` setters before creating the client: ```scala val client = Http.client .withConnectTimeoutMillis(5000) .withMaxRetry(3) .newSyncClient ``` ## The server A server answers requests. At its simplest, a handler is a function from `Request` to a response — and that form runs on **every** platform: ```scala import wvlet.uni.http.{Request, Response} import wvlet.uni.http.netty.NettyServer import wvlet.uni.rx.Rx NettyServer .withPort(8080) .withRxHandler { (request: Request) => Rx.single(Response.ok(s"You asked for ${request.path}")) } .start { server => println(s"listening on ${server.localPort}") } ``` `start { server => … }` runs the server for the duration of the block and shuts it down cleanly when the block ends — the same lifecycle shape as `Design.build`. On Scala.js and Native you swap `NettyServer` for `NodeServer` or `NativeServer`; the builder API is identical. ## Routing without a switch statement A real API has many endpoints, and you don't want one giant `match` on `request.path`. On the JVM, annotate the methods of a controller and let the router dispatch: ```scala import wvlet.uni.http.HttpMethod import wvlet.uni.http.router.{Endpoint, Router} import wvlet.uni.http.netty.{NettyServer, RouterHandler} class UserController: @Endpoint(HttpMethod.GET, "/users/:id") def getUser(id: String): String = s"""{"id":"${id}"}""" @Endpoint(HttpMethod.GET, "/users") def listUsers(): Seq[String] = Seq("alice", "bob") val router = Router.of[UserController] NettyServer .withPort(8080) .withRxHandler(RouterHandler(router)) .start { _ => /* serve */ } ``` `Router.of[UserController]` reads the annotations at compile time and builds the route table; path parameters like `:id` bind to method parameters by name, and a returned `Seq` or case class is serialized to JSON for you (via the [Weaver](./ch06-00-data) machinery from Chapter 6). The [server reference](/http/server) covers query parameters, filters, and combining controllers. ::: tip JVM-only routing The annotation router (`@Endpoint`, `Router.of`, `RouterHandler`) is a JVM/Netty feature. On Scala.js / Native, route inside a functional `withRxHandler` by matching on `request.path`. The server *itself* runs on all three platforms; only the annotation-based routing is JVM-only. ::: ## One client, three platforms The client code at the top of this chapter compiles and runs unchanged on the JVM, in the browser and Node.js (Scala.js), and as a native binary — because `Http.client` resolves to the right transport for each runtime (`java.net.http`, the Fetch API, or libcurl). You write the request once. The one constraint worth knowing: browsers have no synchronous HTTP, so in browser code use `newAsyncClient`. [Chapter 11](./ch11-00-cross-platform) is about this property in general; the [client reference](/http/client) has the per-platform backend table. For responses too large to hold in memory, or for a server pushing updates to a client, see [streaming and server-sent events](/http/sse). ## What you have, what comes next You can now build both ends of an HTTP exchange: * **`Http.client.newSyncClient`** for blocking calls, **`newAsyncClient`** for `Rx[HttpResponse]`. * A **server** from a functional handler (every platform) or an **annotation router** (`@Endpoint` + `Router.of`, JVM). * The same client source runs on JVM, Scala.js, and Native. Next, [Chapter 10](./ch10-00-rpc) removes the hand-written URLs and JSON entirely: define a trait once and get a typed client and server that speak it. [← 8. Living With Failure](./ch08-00-control) | [Next → 10. Typed RPC](./ch10-00-rpc) --- --- url: https://wvlet.org/uni/book/ch10-00-rpc.md --- # 10. Typed RPC In Chapter 9 both ends of the call wrote out the contract by hand: the server picked the path `/users/:id` and shaped the JSON; the client typed the same path and parsed the same JSON back. Nothing connects those two copies. Rename a field on the server and the client keeps compiling — and breaks at runtime, in production, against the one response you didn't test. RPC closes that gap. You write the contract *once*, as a Scala trait, and both sides are checked against it by the compiler. ## Define the contract once The API is an ordinary trait. The server implements it; the client will call it. ```scala case class User(id: Long, name: String, email: String) derives Weaver trait UserService: def getUser(id: Long): User def createUser(name: String, email: String): User class UserServiceImpl extends UserService: def getUser(id: Long): User = User(id, "Alice", "alice@example.com") def createUser(name: String, email: String): User = User(1L, name, email) ``` There is no annotation on the methods, no per-endpoint glue. `getUser` is a normal method; RPC will turn it into a network call without you describing the wire shape. ## Serve it `RPCRouter.of[UserService]` reads the trait at compile time and builds the routes; `RPCHandler` turns that router into something a server can run: ```scala import wvlet.uni.http.rpc.RPCRouter import wvlet.uni.http.netty.{NettyServer, RPCHandler} val router = RPCRouter.of[UserService](UserServiceImpl()) NettyServer .withPort(8080) .withRxHandler(RPCHandler(router)) .start { _ => /* serving */ } ``` Each method becomes a `POST /UserService/{methodName}` endpoint. You never chose those paths — they fall out of the trait, which is the point: there is no path for the client and server to disagree about. ```text trait UserService ← the one contract ╱ ╲ RPCRouter.of[T](impl) generated client (server side) (caller side) ╲ ╱ POST /UserService/getUser (MessagePack on the wire) ``` ## Call it like a local method On the client side, the `sbt-uni` plugin generates a typed client from the *same* trait (see [client generation](/http/rpc#generating-a-client) for the build setup). Calling a remote method then looks like calling a local one: ```scala import wvlet.uni.http.Http import com.example.client.UserServiceClient val client = UserServiceClient.SyncClient( Http.client.withBaseUri("http://localhost:8080").newSyncClient ) val user: User = client.getUser(42) // a network round-trip, fully typed ``` `client.getUser(42)` is checked by the compiler against `UserService`. Pass a `String` where the trait wants a `Long`, or rename `getUser` on the server, and the *client* stops compiling — the failure moves from a 2 a.m. production page to your build. That is the entire value proposition: the contract is one artifact, and a violation is a compile error. ## What crosses the wire Arguments and return values are serialized with [Weaver](./ch06-00-data), the same derivation you met in Chapter 6 — which is why `User` has `derives Weaver`. Over the wire RPC uses MessagePack by default: it is compact and fast, and since both ends were generated from one trait, there is no human reading the bytes who would prefer JSON. The encoding is an internal detail you mostly never see. Errors cross the boundary as data too. When the server throws, the client sees an `RPCException` carrying a structured `RPCStatus` — a typed code like `NOT_FOUND` or `INVALID_ARGUMENT` — rather than a bare HTTP number you have to interpret. The [RPC reference](/http/rpc#rpc-status-codes) lists the status categories. ## When RPC, when REST RPC is the right tool when **you own both ends** — two of your own services, or your frontend and backend, evolving together. The shared trait is a feature: it keeps them in lockstep. It is the *wrong* tool when the other end is not yours to recompile. A public API consumed by third parties, a webhook, anything that must speak a stable, language-neutral contract — that wants plain [HTTP/REST](./ch09-00-http), where the wire format *is* the interface and no client needs your trait. Use RPC inside your system; use REST at its public edge. ## What you have, what comes next You can now connect two services without hand-written glue: * **A trait is the contract** — implemented on the server, generated into a client, checked by the compiler on both sides. * **`RPCRouter.of[T](impl)` + `RPCHandler`** serves it; **`sbt-uni`** generates the typed client. * Payloads ride **Weaver/MessagePack**; errors arrive as typed **`RPCStatus`**. * Reach for RPC when you own both ends, REST at the public edge. That closes Part V. Next, [Part VI](./ch11-00-cross-platform) steps back to the property that has been quietly true this whole time: nearly everything you've written runs on three different runtimes from one codebase. [← 9. HTTP Clients and Servers](./ch09-00-http) | [Next → 11. One Codebase, Three Runtimes](./ch11-00-cross-platform) --- --- url: https://wvlet.org/uni/book/ch11-00-cross-platform.md --- # 11. One Codebase, Three Runtimes Here is something that has been quietly true for the whole book: almost every snippet you've written — `Design`, `Rx`, `Weaver`, the HTTP client — already runs on the JVM, in the browser through Scala.js, and as a native binary through Scala Native. You didn't do anything special to make that happen. This chapter explains *how*, and what to do for the handful of things that genuinely can't be shared. ## One build, three targets A cross-platform module is declared once with `crossProject`: ```scala // build.sbt import sbtcrossproject.CrossPlugin.autoImport.{crossProject, CrossType} lazy val app = crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .settings( libraryDependencies += "org.wvlet.uni" %% "uni" % "2026.1.35" ) ``` That one declaration produces three projects — `appJVM`, `appJS`, `appNative`. On sbt 2.x the familiar `%%` operator already encodes the platform: it pulls in the build of `uni` that matches each target (`_sjs1` for Scala.js, `_native0.5` for Native). (On sbt 1.x you would write `%%%` instead.) `sbt appJVM/run`, `sbt appJS/fastLinkJS`, and `sbt appNative/run` build the same source three ways. ## Where code lives `CrossType.Pure` gives each module these source roots: ``` app/ src/main/scala/ # shared — compiled for all three platforms .jvm/src/main/scala/ # JVM-only .js/src/main/scala/ # Scala.js-only .native/src/main/scala/ # Native-only ``` The vast majority of your code goes in the shared `src/` and is compiled three times. The `.jvm` / `.js` / `.native` folders hold only the code that *must* differ per platform. If you never need them, you never create them — and most application logic never does. ```text src/main/scala/ ← shared logic, compiled 3× │ ┌────────────────┼────────────────┐ + .jvm/ + .js/ + .native/ ← platform-only code ▼ ▼ ▼ JVM Scala.js Scala Native (Netty, JDK) (browser, Node) (native binary) ``` ## Why most code doesn't care The reason the shared folder stays large is that Uni already wraps the platform-specific pieces behind cross-platform APIs: * File I/O goes through the [FileSystem](/core/filesystem) abstraction (`IOPath`), not `java.nio.file.Path` — so reading a file is the same call everywhere. * The [HTTP client](./ch09-00-http) resolves to `java.net.http`, the Fetch API, or libcurl depending on the runtime, behind one `Http.client`. * `Rx`, `Design`, and `Weaver` are pure Scala with no platform assumptions. Because your business logic talks to these abstractions instead of to JDK classes directly, it lands in the shared folder and compiles everywhere unchanged. That is the whole trick: depend on the abstraction, and the platform detail is someone else's problem. ## Isolating what you truly can't share Sometimes you need a real platform primitive — a JVM library with no JS equivalent, a browser DOM call, a native syscall. The pattern is to declare a small shared interface and put one implementation in each platform folder: ```scala // src/main/scala — shared: the shape everyone agrees on trait Clock: def nowMillis(): Long // .jvm/src/main/scala — the JVM implementation object PlatformClock extends Clock: def nowMillis(): Long = System.currentTimeMillis() ``` Each platform folder supplies its own `PlatformClock`; the shared code only ever sees the `Clock` interface. The seam is exactly the one from [Chapter 3](./ch03-00-design) — program against an interface, bind the implementation elsewhere — applied to platforms instead of test doubles. The payoff is that the gap is caught at **compile time**: if a platform is missing its implementation, that platform's build fails. You don't discover a missing piece when a user in a browser hits it; you discover it when `appJS` won't compile. The type system, not a test matrix, keeps the three builds honest. ::: tip A few things are inherently platform-bound Browsers have no synchronous HTTP and no filesystem; native binaries have no DOM. Uni surfaces these honestly — for example, the sync HTTP client throws in the browser rather than pretending. When a capability only exists on one platform, keep the code that uses it in that platform's folder. ::: ## Reaching into a platform's ecosystem A platform folder isn't only for the JDK or a syscall — it's the door to that runtime's whole *foreign* ecosystem. The `.js` folder is where you call JavaScript packages from npm; the `.native` folder is where you call C, C++, and Rust. Both use the same idea as the `Clock` above — a typed Scala declaration of the foreign code you use, kept in the platform folder that has it — and each gets a dedicated Part later in this book: * **[Part VII](./ch12-00-npm-facades)** — wiring Scala.js to npm modules with hand-written facades, bundled by Vite. * **[Part VIII](./ch14-00-calling-c)** — calling C libraries from Scala Native, and exposing your Scala Native code *as* a library that C, C++, and Rust can call. So the shared folder holds your portable logic, and each platform folder can reach as deep into that platform's native world as you need — without either concern leaking into the other. ## What you have, what comes next You can now ship one codebase to three runtimes: * **`crossProject(JVMPlatform, JSPlatform, NativePlatform)`** with `%%` dependencies builds the same source three ways. * Shared code lives in **`src/`**; only genuinely platform-specific code goes in **`.jvm` / `.js` / `.native`**. * Uni's abstractions (FileSystem, HTTP, Rx, Design, Weaver) keep most code in the shared folder. * Unshareable primitives hide behind a **shared interface with a per-platform implementation**, and missing platforms fail at compile time. * Each platform folder is also the door to that runtime's ecosystem — npm ([Part VII](./ch12-00-npm-facades)) from `.js`, C/Rust ([Part VIII](./ch14-00-calling-c)) from `.native`. Next, [Part VII](./ch12-00-npm-facades) follows the Scala.js side of this story out into the JavaScript ecosystem — calling npm packages from Scala — and [Part VIII](./ch14-00-calling-c) does the same for Scala Native and the C/Rust world. [← 10. Typed RPC](./ch10-00-rpc) | [Next → 12. Calling NPM Modules from Scala.js](./ch12-00-npm-facades) --- --- url: https://wvlet.org/uni/book/ch12-00-npm-facades.md --- # 12. Calling NPM Modules from Scala.js You're building a browser app with Uni ([Chapter 11](./ch11-00-cross-platform)), and you need something the JavaScript world already solved: a Markdown renderer, a charting library, a date picker. It's on npm, written in JavaScript, with no Scala types. How do you call it from Scala 3 without giving up type safety — and without dragging in a code generator? The answer is a **facade**: a few lines of Scala that declare the shape of the JavaScript you actually use. You write it by hand, and it is smaller than you expect. ## A facade is a typed declaration Say you want [`marked`](https://www.npmjs.com/package/marked), which turns Markdown into HTML. In JavaScript you'd write: ```javascript import { marked } from 'marked' marked.parse('# Hello') // "

Hello

" ``` The Scala.js facade for exactly that is: ```scala import scala.scalajs.js import scala.scalajs.js.annotation.JSImport @js.native @JSImport("marked", "marked") object Marked extends js.Object: def parse(markdown: String): String = js.native ``` And you call it like any Scala object: ```scala val html = Marked.parse("# Hello") // "

Hello

" ``` That's the whole thing. Four pieces are doing the work: * **`@JSImport("marked", "marked")`** says where the value comes from: the named export `marked` from the npm package `marked`. Scala.js turns this into a real `import { marked } from "marked"` in its JavaScript output. * **`@js.native`** marks this as a declaration, not an implementation — the code lives on the JavaScript side. * **`extends js.Object`** types it as a JavaScript object. * **`= js.native`** is the method body you don't write, because the real one is in the npm package. You declared *only* `parse`, because that's the only method you call. A facade describes your usage, not the whole library — if you later need `marked.lexer`, you add one line. ## Default exports and other shapes Packages export values in a few shapes, and `@JSImport` has a form for each. A **named** export uses its name (as above). A **default** export uses `JSImport.Default`: ```scala import scala.scalajs.js import scala.scalajs.js.annotation.JSImport // JavaScript: import confetti from 'canvas-confetti'; confetti() @js.native @JSImport("canvas-confetti", JSImport.Default) object Confetti extends js.Object: def apply(): Unit = js.native ``` A method that *returns* a JavaScript object gets its own `@js.native` trait — no `@JSImport`, because you don't import it, you receive it: ```scala @js.native @JSImport("some-db", JSImport.Default) object DB extends js.Object: def open(path: String): Connection = js.native @js.native trait Connection extends js.Object: def query(sql: String): js.Array[js.Any] = js.native ``` JavaScript values map to `js`-prefixed types: `js.Object`, `js.Array[A]`, `js.Function`, `js.Dictionary[A]` for string-keyed maps, and `js.Any` when you genuinely don't care. When even a typed facade is more ceremony than a one-off call deserves, `js.Dynamic` is the escape hatch — fully untyped, method calls resolved at runtime: ```scala import scala.scalajs.js val mod = js.Dynamic.global.someGlobal mod.doThing("arg") // no types, no facade — use sparingly ``` Reach for `js.Dynamic` to prototype; promote to a real facade once you know which three methods you actually call. ## Why not ScalablyTyped? [ScalablyTyped](https://scalablytyped.org/) generates Scala facades automatically from a package's TypeScript definitions. It is impressive, and for this book's purposes it is the wrong default. The cost is out of proportion to the need. A typical npm package ships thousands of lines of `.d.ts`; ScalablyTyped turns that into a large generated Scala library that your build must convert and compile, every clean build, whether or not you call more than three of its functions. That's slower builds, a heavier toolchain, and generated names you didn't choose leaking into your code. What you actually need from `marked` was one method. The hand-written facade for it is four lines you can read, that compile instantly, and that say exactly what your program depends on. The trade is real — you write the declarations yourself instead of having them generated — but for the handful of calls a typical app makes into a JS library, four honest lines beat ten thousand generated ones. Write the facade. ## What you have, what comes next You can now call JavaScript libraries from Scala with types you control: * A **facade** is a `@js.native @JSImport(...)` declaration of just the parts of a package you use. * **`JSImport.Default`** for default exports, **`"name"`** for named ones; returned objects get their own `@js.native` trait. * **`js.Dynamic`** is the untyped escape hatch for prototyping. * Skip **ScalablyTyped** — a few hand-written lines beat a generated library for the calls a real app makes. But there's a missing piece. Your facade says `@JSImport("marked", …)`, and Scala.js faithfully emits `import { marked } from "marked"` — yet nothing has *installed* `marked` or resolved that import for the browser. That is the bundler's job. Next, [Chapter 13](./ch13-00-vite) wires it all together with Vite. [← 11. One Codebase, Three Runtimes](./ch11-00-cross-platform) | [Next → 13. Bundling with Vite](./ch13-00-vite) --- --- url: https://wvlet.org/uni/book/ch13-00-vite.md --- # 13. Bundling with Vite Your facade from [Chapter 12](./ch12-00-npm-facades) says `@JSImport("marked", "marked")`, and Scala.js dutifully emits `import { marked } from "marked"` in its output. But nothing has installed `marked`, and a browser has no idea what the bare name `"marked"` resolves to. Something has to fetch the package, follow its imports, and produce JavaScript a browser can load. That something is a **bundler**, and [Vite](https://vitejs.dev/) is a good one. This chapter wires the two worlds together: Scala.js on one side emitting ES module imports, Vite on the other resolving them against npm. ## Step 1: emit ES modules For `@JSImport` to become a real `import` a bundler can follow, Scala.js must emit ES modules. Set the module kind once in `build.sbt`: ```scala import org.scalajs.linker.interface.ModuleKind lazy val app = project .enablePlugins(ScalaJSPlugin) .settings( scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) } ) ``` This is the one setting that makes the whole chain work. Without it, Scala.js can't emit the `import { marked } from "marked"` that Vite needs to see. (Uni's own JS builds use exactly this setting.) `sbt app/fastLinkJS` then produces a folder of ES modules under `target/scala-3.x/app-fastopt/`, with `main.js` as the entry. The production command is `app/fullLinkJS`, which emits an optimized build under `app-opt/`. ## Step 2: install the npm package Vite resolves `"marked"` the npm way — from `node_modules`. So install it, along with Vite itself: ```bash $ npm install marked $ npm install --save-dev vite ``` This is the half of the contract Scala can't fulfill: the facade *names* the package, and `npm install` *provides* it. ## Step 3: a Vite entry point Vite serves an `index.html` that loads one JavaScript entry module: ```html ``` `main.js` is the seam between the npm world and the Scala.js world. It imports your styles and any npm modules you want to set up, then pulls in the Scala.js output to start your app: ```javascript // main.js import './style.css' // Start the Scala.js application (the fastLinkJS output entry module) import('./target/scala-3.x/app-fastopt/main.js') ``` When Scala.js code runs and hits your `Marked.parse(...)` facade, the emitted `import { marked } from "marked"` resolves through Vite to the installed package. The facade, the linker, and the bundler are now one pipeline: Scala types on top, npm code underneath. ## Step 4: run it Two processes, one for each compiler: ```bash $ sbt "~app/fastLinkJS" # recompile Scala.js on change $ npx vite # dev server with hot reload ``` `vite` serves the app with instant hot-module reload; the `~` keeps Scala.js relinking as you edit. For production, `sbt app/fullLinkJS` followed by `npx vite build` emits a minified bundle to `dist/`. A minimal `vite.config.js` needs almost nothing: ```javascript import { defineConfig } from "vite" export default defineConfig({ base: "./", }) ``` ## When a module won't bundle cleanly Most npm packages bundle without fuss. Two situations need a nudge, and both have a one-line fix. **A library that's easier to load as a global.** Some packages (editors, WASM modules) are simplest to load in `main.js` and hand to Scala.js through the `window` object. Expose it: ```javascript // main.js import { marked } from 'marked' window.marked = marked ``` and facade it with `@JSGlobal` instead of `@JSImport`: ```scala import scala.scalajs.js import scala.scalajs.js.annotation.JSGlobal @js.native @JSGlobal("marked") object Marked extends js.Object: def parse(markdown: String): String = js.native ``` This `window` bridge is how the [Wvlet playground](https://github.com/wvlet/wvlet/tree/main/wvlet-ui-playground) wires its Monaco editor — `main.js` sets `window.MonacoEditor`, and Scala reads it via `@JSGlobal`. **A package with a dependency the browser can't use.** A library may pull in a Node-only module the bundler can't resolve. Point Vite at a browser-safe replacement with `resolve.alias`: ```javascript export default defineConfig({ resolve: { alias: { // swap a Node-only dep for a browser stub "node-only-pkg": "/stubs/browser-stub.js", }, }, }) ``` The playground does this for `koffi` (a native-binding library its DuckDB backend needs on Node but never on the web) — aliasing it to a stub that throws if anything ever calls it. Reach for `alias` only when a transitive dependency fights the browser; you won't need it for ordinary UI packages. ## What you have, what comes next You can now ship a Scala.js app that uses the npm ecosystem: * **`ModuleKind.ESModule`** makes Scala.js emit imports a bundler can follow. * **`npm install`** provides the package your facade named. * A **`main.js` entry** imports the Scala.js output and any npm setup; Vite resolves the bare imports and serves the app. * **`@JSGlobal` + `window`** bridges stubborn modules, and **`resolve.alias`** swaps out browser-hostile dependencies. That completes Part VII — Scala.js is now a full participant in the JavaScript ecosystem, with types on your side of the boundary and npm on the other. Next, [Part VIII](./ch14-00-calling-c) does the mirror image for the other end of the platform spectrum: Scala Native and the C/Rust world — calling C libraries from Scala, and exposing your Scala as a library that C, C++, and Rust can call. [← 12. Calling NPM Modules from Scala.js](./ch12-00-npm-facades) | [Next → 14. Calling C from Scala Native](./ch14-00-calling-c) --- --- url: https://wvlet.org/uni/book/ch14-00-calling-c.md --- # 14. Calling C from Scala Native Compile a Uni app with Scala Native ([Chapter 11](./ch11-00-cross-platform)) and you get a real native binary — and with it, direct access to the entire C ecosystem: `libcurl`, `sqlite`, `openssl`, anything with a C ABI. No JNI, no subprocess, no serialization across a boundary. You call a C function the way you call a Scala one, once you've declared it. This chapter is the Scala-Native counterpart to [Chapter 12](./ch12-00-npm-facades): there you wrote a facade for a JavaScript module; here you write one for a C library. ## An `@extern` binding To call C, you declare the functions you need in an `@extern` object and tell the linker which library provides them. Here is the shape, taken from how Uni's own Native HTTP client binds `libcurl`: ```scala import scala.scalanative.unsafe.* @link("curl") @extern object curl: @name("curl_easy_init") def easyInit(): Ptr[Byte] = extern @name("curl_easy_setopt") def easySetOpt(handle: Ptr[Byte], option: CInt, value: CString): CInt = extern @name("curl_easy_perform") def easyPerform(handle: Ptr[Byte]): CInt = extern @name("curl_easy_strerror") def easyStrError(code: CInt): CString = extern @name("curl_easy_cleanup") def easyCleanup(handle: Ptr[Byte]): Unit = extern // Option codes come from curl.h; 10002 is CURLOPT_URL val CURLOPT_URL: CInt = 10002 ``` Four pieces, mirroring the JavaScript facade from Chapter 12: * **`@link("curl")`** tells the linker to link `libcurl` into the binary — the native equivalent of `npm install`. * **`@extern`** marks the object as a set of declarations whose bodies live in that C library. * **`@name("curl_easy_init")`** maps a Scala method to the actual C symbol, so your Scala name can be idiomatic while the binding still finds `curl_easy_init`. * **`= extern`** is the body you don't write, because C already did. You declare only the handful of functions you call, exactly as with a JS facade. `libcurl` exports hundreds; a working HTTP client needs a dozen. C constants (like `CURLOPT_URL`) don't cross the boundary at all — you copy their values out of the header file into ordinary Scala `val`s. ## C types in Scala C has its own types, and Scala Native gives you a vocabulary for them from `scala.scalanative.unsafe`: | C | Scala Native | |---|--------------| | `int` | `CInt` | | `char*` (string) | `CString` | | `void*` / opaque pointer | `Ptr[Byte]` | | `size_t` | `CSize` | | a `struct` | `CStruct2[A, B]`, `CStruct3[...]`, … | A `CString` is a pointer to bytes, not a Scala `String` — the two are different worlds, and you convert at the boundary. An opaque C handle (like libcurl's `CURL*`) is just a `Ptr[Byte]` you pass back to the library; you never look inside it. ## Crossing the string boundary Two helpers move strings across. `fromCString` reads a C string into a Scala `String`. `toCString` does the reverse — but it has to allocate memory somewhere, and where decides how long the result lives. For a string you pass *into* a C call and don't need afterward, allocate it in a `Zone`: a scoped arena that frees everything in it when the block ends. ```scala import scala.scalanative.unsafe.* Zone.acquire { implicit z => val handle = curl.easyInit() curl.easySetOpt(handle, CURLOPT_URL, toCString("https://example.com")) curl.easyPerform(handle) } // every toCString allocation in this zone is freed here ``` Reading a result back out uses `fromCString`: ```scala val message: String = fromCString(curl.easyStrError(code)) ``` The `Zone` is the key idea for memory safety at the boundary: temporary C allocations get a clear lifetime tied to a block, so they can't leak and can't be used after they're freed. (You'll see the one case where `Zone` is *wrong* — a value handed back to C that must outlive the call — in [Chapter 15](./ch15-00-exposing-native).) ## A Uni service over the binding The binding compiles, but nothing about it feels like the code you've written in the rest of this book: it traffics in pointers and integer return codes, and a failure is a `CInt` you have to remember to check. Don't spread that through your application. Wrap the binding in one ordinary Scala class, and let Uni take over from there. ```scala import scala.scalanative.unsafe.* import wvlet.uni.log.LogSupport class CurlError(code: Int, message: String) extends Exception(s"curl error ${code}: ${message}") class Downloader extends LogSupport with AutoCloseable: private val handle = curl.easyInit() def fetch(url: String): Unit = Zone.acquire { implicit z => curl.easySetOpt(handle, CURLOPT_URL, toCString(url)) val code = curl.easyPerform(handle) if code != 0 then val message = fromCString(curl.easyStrError(code)) error(s"GET ${url} failed: ${message}") throw CurlError(code, message) debug(s"GET ${url} succeeded") } override def close(): Unit = curl.easyCleanup(handle) ``` Notice what this class is: the *only* place where both vocabularies appear. Above it, pure Scala (`fetch(url: String)`, exceptions, `LogSupport`); below it, pure C (`Ptr`, `CString`, return codes). The mixing of the two worlds is the wrapper's entire job, and it happens here and nowhere else — everything C-shaped is translated at this edge: * **Error codes become exceptions.** C reports failure by returning a nonzero `CInt`; the compiler doesn't force anyone to look at it. The wrapper checks once, converts the code to a message with `easyStrError`, and throws — callers can't silently ignore a failed download. * **`LogSupport` works here like everywhere else.** The logging you learned in [Chapter 5](./ch05-00-logging) is pure Scala, so it cross-compiles to Native unchanged — `error` and `debug` at the C boundary behave exactly as they did on the JVM. When a native binary misbehaves in production, the log line with curl's own error message is what you'll want. * **Callers see `fetch(url: String)`.** No `Ptr`, no `CString`, no `Zone`. The rest of the application cannot tell — and should not care — that a C library sits underneath. ## Give the C handle a lifecycle There is one problem left: `handle` is C memory. Scala's garbage collector doesn't know about it, so nothing frees it unless `close()` runs. That is precisely what `Design`'s lifecycle hooks from [Chapter 3](./ch03-00-design) are for: ```scala import wvlet.uni.design.Design val design = Design .newDesign .bindSingleton[Downloader] .onShutdown(_.close()) design.build[Downloader] { downloader => downloader.fetch("https://example.com") } // session ends here: close() runs, curl_easy_cleanup frees the handle ``` The session guarantees `close()` runs exactly once, when the session ends — the same deterministic teardown you'd want for a database connection, applied to a C resource. And because `Downloader` is now just a binding in a design, tests can substitute it the way [Chapter 3](./ch03-00-design) substituted a database: put a trait in front of it, `bindImpl` the C-backed class in the production design, and bind a stub in the test design — no test ever opens a real curl handle. This layering — raw `@extern` facade at the bottom, one safe wrapper class, Uni services on top — is how Uni's own Native HTTP client is built, and it is the shape to copy for any C library you bind. ## Why bind C directly? On the JVM, reaching a C library means JNI: a separate C shim, a build step, and a marshaling layer between the JVM and native code. Scala Native removes the gap — your Scala compiles to native code already, so an `@extern` call is a direct call, with no bridge and no per-call overhead. This is not a toy capability. Uni's Scala Native HTTP client *is* a `libcurl` binding written exactly this way — the same `@link` / `@extern` / `Zone` pattern above, in production, behind the same `Http.client` API you used on the JVM in [Chapter 9](./ch09-00-http). The cross-platform client works on Native because someone wrote this facade once; you can wrap any C library the same way. ## What you have, what comes next You can now call C libraries from Scala Native — and make them feel like Uni code: * An **`@extern` object** with **`@link`** and **`@name`** declares the C functions you use — a facade, like Chapter 12's, but for C. * **C types** (`CInt`, `CString`, `Ptr`, `CStruct…`) describe the values; a `CString` is not a Scala `String`. * **`Zone.acquire`** + **`toCString`** allocate temporary arguments with a scoped lifetime; **`fromCString`** reads results back. * **One wrapper class** turns error codes into exceptions and logs through `LogSupport`; the rest of the app never sees a `Ptr`. * **`Design` + `onShutdown`** give the C handle a deterministic lifecycle, and a trait in front of the wrapper keeps it swappable in tests. Next, [Chapter 15](./ch15-00-exposing-native) turns the arrow around: instead of Scala calling C, you'll expose your Scala Native code *as* a C library that Rust, C, and C++ can call. [← 13. Bundling with Vite](./ch13-00-vite) | [Next → 15. Exposing Scala Native to C and Rust](./ch15-00-exposing-native) --- --- url: https://wvlet.org/uni/book/ch15-00-exposing-native.md --- # 15. Exposing Scala Native to C and Rust You've written something valuable in Scala — a parser, a query compiler, a domain calculation — and now a Rust service and a C++ application both want to call it. Porting it twice is the wrong answer. Instead, compile your Scala Native code into a **C-ABI library** and let any language that speaks C link against it. This is [Chapter 14](./ch14-00-calling-c) in reverse: there Scala called into C; here C, C++, and Rust call into Scala. ## Export a function Mark a method `@exported` with the C symbol name you want, and Scala Native gives it a C calling convention: ```scala import scala.scalanative.unsafe.* object MyLib: @exported("greet") def greet(name: CString): CString = val who = fromCString(name) // C string → Scala String toCStringHeap(s"Hello, ${who}!") // Scala String → C string (see below) ``` `fromCString` reads the incoming `char*` into a Scala `String`, the same helper from Chapter 14. The interesting part is the return value. ## Returning data: not in a Zone In Chapter 14 you allocated temporary C strings in a `Zone`, which frees them when the block ends. For a value you *return to C*, that is exactly wrong: the `Zone` would free the string the instant `greet` returns, and the caller would read freed memory. A returned value has to outlive the call, so you allocate it on the heap with `malloc` and hand ownership across the boundary: ```scala import scala.scalanative.unsafe.* import scala.scalanative.unsigned.* import scala.scalanative.libc.stdlib def toCStringHeap(str: String): CString = val bytes = str.getBytes("UTF-8") val buffer = stdlib.malloc((bytes.length + 1).toCSize) var i = 0 while i < bytes.length do buffer(i) = bytes(i) i += 1 buffer(bytes.length) = 0.toByte buffer.asInstanceOf[CString] ``` This is the one genuinely subtle rule of exporting: **arguments use a `Zone`, return values use the heap.** Wvlet's real C library uses exactly this split — `fromCString` on the way in, a `malloc`-backed `toCString` on the way out. Heap memory raises the question a `Zone` answered for you: who frees it? The answer at a C boundary is a convention — the library that `malloc`s must be the one that `free`s — so a well-behaved library also exports a matching free function. You'll see one in the next section. ## Ship Uni logic, not toy strings `greet` shows the mechanics, but nobody builds a library to concatenate strings. The real question is: your Scala logic works on *rich* values — case classes, collections — and the C ABI only speaks integers and pointers. How do you get a `PriceRequest` across? You don't define C structs for it. You pass **JSON strings** and let [Weaver](./ch06-00-data) do the translating — and you keep the two worlds in two separate files, meeting at a plain Scala `String`. The first file is your actual library. It is pure Uni code: no `CString`, no `Ptr`, no `unsafe` import. Following [Chapter 11](./ch11-00-cross-platform)'s layout it lives in the shared `src/` folder, because nothing in it is native-specific: ```scala // src/ — shared code. Compiles on JVM, JS, and Native alike. import wvlet.uni.log.LogSupport import wvlet.uni.weaver.Weaver case class PriceRequest(product: String, quantity: Int) derives Weaver case class PriceQuote(product: String, quantity: Int, total: Double) derives Weaver object PricingService extends LogSupport: def quote(requestJson: String): String = val request = Weaver.fromJson[PriceRequest](requestJson) debug(s"Quoting ${request.quantity} x ${request.product}") val response = PriceQuote(request.product, request.quantity, request.quantity * 9.99) Weaver.toJson(response) ``` The second file is the C bridge, and it is *only* the bridge. Each exported function is one line: convert, delegate, convert back. It goes in the `.native` folder, because it can compile nowhere else: ```scala // .native/ — the C bridge. Conversions only; no logic lives here. import scala.scalanative.unsafe.* import scala.scalanative.libc.stdlib object PricingLib: @exported("pricing_quote") def quote(requestJson: CString): CString = toCStringHeap(PricingService.quote(fromCString(requestJson))) @exported("pricing_free") def free(str: CString): Unit = stdlib.free(str) ``` The split is the point: * **The two worlds meet in exactly one file.** `PricingService` speaks Scala (`String` in, `String` out); `PricingLib` speaks C (`CString` in, `CString` out); `fromCString` / `toCStringHeap` are the only words they exchange. When something goes wrong at the boundary, there is one short file to look in — the same one-place rule Chapter 14 applied to the `Downloader` wrapper, pointing the other way. * **The service never left the ordinary Uni world.** The logging from [Chapter 5](./ch05-00-logging), the `derives Weaver` codecs from [Chapter 6](./ch06-00-data), `Design`-built services if the logic needs them — all available, because `PricingService` is just Scala. And because it lives in shared `src/`, you can unit-test it with UniTest on the JVM ([Chapter 4](./ch04-00-testing)) — no native toolchain, no C caller — or serve the same logic over RPC ([Chapter 10](./ch10-00-rpc)). The C ABI is one more front door, not a fork of your code. * **JSON is the boundary contract.** Every language that can call C can also build a JSON string, so callers get rich structured input and output without you maintaining a C struct layout for each type. Add a field to `PriceQuote` and existing callers keep working. * **`pricing_free` completes the ownership story.** The returned string was `malloc`ed inside the Scala library, so the Scala library exports the function that frees it. Callers treat the pair as open/close: call `pricing_quote`, read the result, call `pricing_free`. Only static object methods can be exported, and their parameter and return types must be C-representable (primitives and pointers) — which is exactly why the JSON-string shape works so well. ## Build it as a library By default Scala Native produces an executable. A `nativeConfig` setting tells it to produce a library instead — dynamic (`.so` / `.dylib`) or static (`.a`): ```scala import scala.scalanative.build.BuildTarget // Dynamic library: libmylib.so / libmylib.dylib lazy val mylib = project .enablePlugins(ScalaNativePlugin) .settings( libraryDependencies += "org.wvlet.uni" %% "uni" % "2026.1.35", nativeConfig ~= { _.withBuildTarget(BuildTarget.libraryDynamic).withBaseName("mylib") } ) ``` The `uni` dependency is the same line as any Native project from [Chapter 11](./ch11-00-cross-platform) — Weaver and logging compile into the library like any other Scala code. Swap `libraryDynamic` for `BuildTarget.libraryStatic` to get a static `libmylib.a` instead. `sbt mylib/nativeLink` then emits the shared library, and Scala Native also generates a C header declaring your exported functions. One initialization rule: the Scala Native runtime must start before the first exported call. A *dynamic* library does this itself — Scala Native generates a constructor that runs when the library loads. A *static* library can't, so C callers linking `libmylib.a` must call the generated `ScalaNativeInit()` once (it returns `0` on success) before anything else. ## Call it from Rust Rust links the library and declares the exported functions in an `extern "C"` block — its native equivalent of Chapter 14's `@extern` object, pointing the other way: ```rust use std::ffi::{CStr, CString}; use std::os::raw::c_char; extern "C" { fn pricing_quote(request_json: *const c_char) -> *mut c_char; fn pricing_free(response_json: *mut c_char); } fn main() { let request = CString::new(r#"{"product":"keyboard","quantity":3}"#).unwrap(); unsafe { let response = pricing_quote(request.as_ptr()); println!("{}", CStr::from_ptr(response).to_str().unwrap()); pricing_free(response); } } ``` The Rust side mirrors the memory rules from the Scala side exactly: `CString::new` plays the role of `toCString` for the argument (freed by Rust when `request` drops), and the response — `malloc`ed inside the Scala library — goes back through `pricing_free`. Compile it against the library by pointing the linker at the output directory and naming the lib, then run it with the shared library on the loader's path: ```bash $ sbt mylib/nativeLink $ rustc -L target/scala-3.x -lmylib test.rs -o test $ LD_LIBRARY_PATH=target/scala-3.x ./test # DYLD_LIBRARY_PATH on macOS {"product":"keyboard","quantity":3,"total":29.97} ``` `-L` is the search path (where `libmylib.so` lives) and `-lmylib` is the library — the same two flags whatever the consuming language is. At run time the dynamic loader needs the same directory, hence `LD_LIBRARY_PATH`. ## Call it from C and C++ C and C++ are the same story: declare the functions, link the library. ```c // test.c #include char* pricing_quote(const char* request_json); void pricing_free(char* response_json); int main() { char* response = pricing_quote("{\"product\":\"keyboard\",\"quantity\":3}"); printf("%s\n", response); pricing_free(response); return 0; } ``` ```bash $ gcc -L target/scala-3.x -lmylib test.c -o test # g++ for C++ ``` Wvlet ships its query compiler this way, in exactly the JSON-in/JSON-out shape you just built: one Scala Native module exports `wvlet_compile_query_json`, which takes a JSON string of arguments and returns a `char*` of JSON (`{"success":true,"sql":...}`), builds to `libwvlet`, and its [test suite](https://github.com/wvlet/wvlet/tree/main/wvc-lib) links the same `.so` from Rust, C, *and* C++ — three languages, one implementation, no ports. ## Why ship Scala as a native library The usual ways to share logic across languages are a network service (now you operate a server and pay serialization on every call) or a rewrite (now you maintain the same logic twice and they drift). Exporting a C-ABI library is neither: the consumer makes a direct in-process function call into your compiled Scala, and there is exactly one implementation to maintain. Because the contract is the C ABI — the lingua franca every systems language speaks — you are not committing to Rust or to C++. Anything that can call C can call your Scala. You write the logic once, in the language you prefer, and hand it to everyone else as a `.so`. ## What you have, what comes next You can now ship Uni-powered Scala to the systems world: * **`@exported("name")`** gives a method a C ABI; **`fromCString`** reads arguments. * Return values go on the **heap** (`malloc`), never a `Zone` — and the library exports the matching **free function**, because whoever `malloc`s must `free`. * **Two files, one meeting point**: a pure-Scala service (`String` in, `String` out, Uni everywhere) in shared `src/`, and a conversions-only C bridge in `.native/` — `fromCString` / `toCStringHeap` are the only words the two worlds exchange. * **JSON strings are the boundary contract**: `Weaver.fromJson` inside the service on the way in, `Weaver.toJson` on the way out. * **`BuildTarget.libraryDynamic` / `libraryStatic`** + `nativeLink` produce a `.so` / `.dylib` / `.a` and a C header; static libraries need one **`ScalaNativeInit()`** call before use. * **`-L -l`** links it from Rust, C, or C++ alike. That closes Part VIII — and, with it, Uni's reach into both neighboring ecosystems: the JavaScript world through Scala.js ([Part VII](./ch12-00-npm-facades)) and the C/Rust world through Scala Native. One library, three runtimes, and a door into each runtime's native ecosystem. From here, the appendices collect supporting material: [Appendix A](./appendix-a-scala3) on Scala 3 syntax, [Appendix B](./appendix-b-airframe) on Uni and Airframe, and [Appendix C](./appendix-c-glossary), the glossary. [← 14. Calling C from Scala Native](./ch14-00-calling-c) | [Next → 16. Capstone — A Bookmarks Service](./ch16-00-capstone) --- --- url: https://wvlet.org/uni/book/ch16-00-capstone.md --- # 16. Capstone — A Bookmarks Service You've met every piece of Uni on its own. This chapter sets the platform-specific side-tracks aside and builds one small, complete thing with the core stack — a service that saves and lists bookmarks — so you can see how the pieces fit into a single program. By the end you'll have a typed RPC service, wired with Design, hardened with a retry, and tested without a network. Nothing here is new; the point is the *assembly*. ## The data and the contract Start with the data — a `Bookmark` — and the contract clients call, a trait. Both `derive Weaver` so they cross the wire as [Chapter 6](./ch06-00-data) showed: ```scala import wvlet.uni.weaver.Weaver case class Bookmark(id: Long, url: String, title: String) derives Weaver trait BookmarkService: def add(url: String, title: String): Bookmark def list(): Seq[Bookmark] ``` The trait is the whole API. Server and client are both checked against it, so they cannot drift ([Chapter 10](./ch10-00-rpc)). ## The implementation The implementation keeps an in-memory store. It takes nothing it doesn't need in its constructor — here, just a starting `Seq` of seed bookmarks, which makes it trivial to construct in a test: ```scala import java.util.concurrent.atomic.AtomicReference class BookmarkServiceImpl(seed: Seq[Bookmark]) extends BookmarkService: private val store = AtomicReference(seed.toVector) private val nextId = java.util.concurrent.atomic.AtomicLong(seed.size + 1L) def add(url: String, title: String): Bookmark = val bookmark = Bookmark(nextId.getAndIncrement(), url, title) store.updateAndGet(_ :+ bookmark) bookmark def list(): Seq[Bookmark] = store.get() ``` ## Wiring it with Design `Design` ([Chapter 3](./ch03-00-design)) decides how the service is built. Binding the seed as an instance and the service as a singleton is the entire dependency graph: ```scala import wvlet.uni.design.Design val appDesign = Design.newDesign .bindInstance[Seq[Bookmark]](Seq.empty) .bindSingleton[BookmarkServiceImpl] ``` That `appDesign` is the one artifact production and tests share. The test later swaps just the seed; everything else stays real. ## Serving it over RPC Turn the implementation into routes with `RPCRouter.of`, wrap it in an `RPCHandler`, and serve it on Netty ([Chapter 9](./ch09-00-http), [Chapter 10](./ch10-00-rpc)). The server runs inside the Design session, so it is built and torn down with everything else: ```scala import wvlet.uni.http.rpc.RPCRouter import wvlet.uni.http.netty.{NettyServer, RPCHandler} appDesign.build[BookmarkServiceImpl] { service => val router = RPCRouter.of[BookmarkService](service) NettyServer .withPort(8080) .withRxHandler(RPCHandler(router)) .start { server => println(s"Bookmarks service on ${server.localAddress}") server.awaitTermination() } } ``` A generated `BookmarkServiceClient` (set up as in [client generation](/http/rpc#generating-a-client)) then calls `add`/`list` as if they were local methods. ## Hardening one call Suppose `add` should fetch the page title when the caller omits it. That's a network call, and network calls fail — so wrap it in a retry ([Chapter 8](./ch08-00-control)), and only for this idempotent GET: ```scala import wvlet.uni.control.Retry import wvlet.uni.http.{Http, Request} class TitleFetcher(client: wvlet.uni.http.HttpSyncClient): def titleOf(url: String): String = Retry.withBackOff(maxRetry = 3).run { val html = client.send(Request.get(url)).contentAsString.getOrElse("") extractTitle(html).getOrElse(url) } private def extractTitle(html: String): Option[String] = val open = html.indexOf("") val close = html.indexOf("") if open >= 0 && close > open then Some(html.substring(open + 7, close)) else None ``` `TitleFetcher` takes its `HttpSyncClient` as a constructor parameter, so Design owns the client's lifecycle (`bindInstance(...).onShutdown(_.close())`, as in [Chapter 3](./ch03-00-design)) and a test can hand it a stand-in. ## Testing it, without a network Because the wiring is a value, the test takes `appDesign` and overrides exactly one binding — the seed — leaving the real `BookmarkServiceImpl` in place ([Chapter 4](./ch04-00-testing)): ```scala import wvlet.uni.design.Design import wvlet.uni.test.UniTest class BookmarkServiceTest extends UniTest: test("add appends and list returns everything") { val seeded = appDesign + Design.newDesign.bindInstance[Seq[Bookmark]]( Seq(Bookmark(1, "https://wvlet.org", "Wvlet")) ) seeded.build[BookmarkServiceImpl] { service => service.list().size shouldBe 1 val added = service.add("https://scala-lang.org", "Scala") service.list().map(_.title) shouldContain "Scala" added.id shouldBe 2 } } ``` The real `add`/`list` logic runs; only the seed is a fixture. No server, no socket — the suite finishes in milliseconds. ## What you built In one chapter you assembled the whole core stack into a working service: * **Data + contract** as `derives Weaver` case classes and a trait. * **Design** for the dependency graph, shared by production and tests. * **RPC + Netty** to serve the trait, with a generated typed client. * **Retry** around the one call that touches the network. * **UniTest + Design override** to test the real logic without a network. That is the shape of a Uni application — *parse, wire, do, clean up* from [Chapter 2](./ch02-00-cli-app), grown up. From here, the platform Parts ([VII](./ch12-00-npm-facades), [VIII](./ch14-00-calling-c)) take the same code to the browser and to native binaries, and the [appendices](./appendix-a-scala3) collect the supporting material. [← 15. Exposing Scala Native to C and Rust](./ch15-00-exposing-native) | [Next → Appendix A: Scala 3 Syntax Notes](./appendix-a-scala3) --- --- url: https://wvlet.org/uni/book/appendix-a-scala3.md --- # Appendix A: Scala 3 Syntax Notes for This Book This book uses a handful of Scala 3 features in nearly every example. If you are coming from Scala 2, Java, or another typed language, this appendix is the short reference for the syntax you'll see — just enough to read the chapters comfortably. ## Indentation-based syntax Scala 3 lets you replace braces with significant indentation. A class body, method body, or block opens with `:` (for definitions) or is simply indented: ```scala class Greeter: def greet(name: String): String = val message = s"Hello, ${name}" message ``` Braces still work and are still useful — for a short lambda passed to a method, the book often keeps them: `design.build[App] { app => … }`. Use whichever reads more clearly; they mean the same thing. ## `@main` entry points A program's entry point is a method annotated with `@main`, not a magic signature on an object: ```scala @main def run(): Unit = println("started") ``` Arguments become method parameters. `@main def fetch(args: String*)` gives you the command-line arguments directly, which is how the CLI app in [Chapter 2](./ch02-00-cli-app) starts. ## `given` / `using` — contextual parameters A `using` parameter is supplied by the compiler from a matching `given` in scope, instead of being passed explicitly at the call site: ```scala given Weaver[User] = Weaver.of[User] def toJson[A](a: A)(using weaver: Weaver[A]): String = weaver.toJson(a) toJson(user) // the Weaver[User] given is found and passed for you ``` This is how [Weaver](./ch06-00-data) codecs and other typeclass-style values travel through the code without cluttering every signature. A `derives` clause (`case class User(...) derives Weaver`) generates the `given` for you. ## Enums and pattern matching Scala 3 enums declare a closed set of cases, which you then match on: ```scala enum Color: case Red, Green, Blue def hex(c: Color): String = c match case Color.Red => "#f00" case Color.Green => "#0f0" case Color.Blue => "#00f" ``` The compiler warns if a `match` misses a case, so adding `Color.Black` later points you at every place that needs updating. Uni uses enums for closed sets like log levels and RPC status codes. ## Extension methods An `extension` adds methods to a type you don't own: ```scala extension (s: String) def shout: String = s.toUpperCase + "!" "hello".shout // "HELLO!" ``` Several Uni APIs read fluently because of extensions — the `shouldBe` in [Chapter 4](./ch04-00-testing)'s tests is an extension method on any value. ## Why `new` is almost never needed Scala 3 lets you construct a class by calling it like a function, so the book writes `StringBuilder()` and `FakeDatabase()`, not `new StringBuilder()`. You'll only see `new` in this book for the rare anonymous-class case (overriding methods inline, as with a `WebSocketHandler`). If you wrote Scala 2, dropping `new` is the habit to unlearn first. [← 16. Capstone — A Bookmarks Service](./ch16-00-capstone) | [Next → Appendix B: Uni and Airframe](./appendix-b-airframe) --- --- url: https://wvlet.org/uni/book/appendix-b-airframe.md --- # Appendix B: Uni and Airframe — History and Relationship If you've worked in the Scala ecosystem, the ideas in this book may feel familiar. That is not an accident: Uni is a refinement of code that began in the [Airframe](https://github.com/wvlet/airframe) project. This appendix explains the relationship — useful background if you have existing Airframe code, and reassurance that what you've learned transfers. ## Where Uni comes from Airframe is a broad Scala library and ecosystem built up over many years, spanning dependency wiring, logging, serialization, RPC, and more, across Scala 2 and Scala 3. Uni takes the most-used, most-foundational pieces of that work and re-presents them as a single, cohesive library built **for Scala 3 from the start**. The mental models you've built reading this book — `Design` as object wiring, `Rx` as a composable stream, `Surface` for compile-time type information, codecs derived rather than hand-written — all originated in Airframe. They carry over because they are the same ideas, sharpened. ## What changed Uni is a refinement, and the refinements pull in one direction: smaller and more modern. * **Scala 3 only.** Uni drops Scala 2 cross-building, so its APIs can use `given`/`using`, `enum`, `derives`, and indentation syntax directly instead of working around their absence. The [syntax notes](./appendix-a-scala3) are the features this buys. * **Minimal dependencies.** Uni keeps its dependency surface deliberately small, so adding it to a project doesn't pull in a framework. * **Trimmed scope.** Pieces of Airframe that were niche, or better served by a dedicated tool, are left out rather than carried forward. Uni aims to be the *essential* set, not the exhaustive one. * **A fresh package root.** Uni lives under `wvlet.uni.*`, distinct from Airframe's `wvlet.airframe.*`. ## If you have Airframe code Because Uni and Airframe occupy different package roots, they are separate libraries and can sit in the same build during a migration — you are not forced to move everything at once. The conceptual translation is usually direct: an Airframe `Design` and a Uni `Design` express the same wiring idea, an Airframe codec and a Uni `Weaver` solve the same serialization problem. The exact mapping — which APIs are identical, which were renamed, which were dropped — is a moving target better served by the project's migration notes than by a printed table here. Start from the concept you know in Airframe, find its chapter in this book, and you'll usually recognize the Uni shape immediately. [← Appendix A: Scala 3 Syntax Notes](./appendix-a-scala3) | [Next → Appendix C: Glossary](./appendix-c-glossary) --- --- url: https://wvlet.org/uni/book/appendix-c-glossary.md --- # Appendix C: Glossary Short definitions of terms as this book uses them. The intent is to make cross-references precise, not to define every word in Scala. Each entry points to the chapter where the concept is developed. **Binding** — One entry in a [Design](#design): a rule for how to produce a value of some type. Created with `bindSingleton`, `bindInstance`, `bindImpl`, or `bindProvider`. See [Chapter 3](./ch03-00-design). **Cancelable** — The handle returned by `Rx.subscribe` / `Rx.run`. Calling `cancel` tears down the subscription and stops further values. Holding and releasing it is how `Rx` avoids leaked streams. See [Chapter 7](./ch07-00-rx). **Circuit breaker** — A control-flow primitive that stops calling a backend once its failure rate crosses a threshold, "opening" to fail fast until the backend recovers. See [Chapter 8](./ch08-00-control). **Codec** — A bidirectional translator between bytes (JSON or MessagePack) and Scala values. In Uni the codec is a [Weaver](#weaver). See [Chapter 6](./ch06-00-data). **Cross-project** — An sbt module declared once with `crossProject` and compiled for the JVM, Scala.js, and Scala Native. Shared code lives in `src/`; platform-specific code in `.jvm` / `.js` / `.native`. See [Chapter 11](./ch11-00-cross-platform). **Design** — An immutable description of how types are wired together — which implementations satisfy which dependencies, and which values live as singletons. A `Design` is a value: building it runs nothing. See [Chapter 3](./ch03-00-design). **Launcher** — The annotation-driven CLI argument parser. It populates a plain case class from `Array[String]` using `@option` and `@argument`. See [Chapter 2](./ch02-00-cli-app). **LogSupport** — The trait a class mixes in to get `trace` / `debug` / `info` / `warn` / `error` methods and a logger named after the class. Log messages carry their source `(file:line)`, captured at compile time. See [Chapter 5](./ch05-00-logging). **Resource** — A control primitive (`Resource.withResource`) that lends you a resource for a block and closes it on exit, on both the normal and exception paths — the loan pattern. See [Chapter 8](./ch08-00-control). **Retry** — A policy (`Retry.withBackOff`) that re-runs a failing operation with increasing delays. Safe only for idempotent operations. See [Chapter 8](./ch08-00-control). **RPC** — Calling a remote service through a shared Scala trait, so the client and server are checked against one contract by the compiler. See [Chapter 10](./ch10-00-rpc). **Rx** — Uni's abstraction for a value that arrives later or changes over time: a lazy description of zero or more values that you compose with `map` / `flatMap` / `filter` and consume with `subscribe`. See [Chapter 7](./ch07-00-rx). **RxVar** — A mutable `Rx` you can watch: created with `Rx.variable`, updated with `:=`, and observed by subscribers. See [Chapter 7](./ch07-00-rx). **Session** — The runtime context `Design.build` (or `withSession`) opens: it owns the singletons it constructs and runs their `onStart` / `onShutdown` lifecycle hooks. A session has a lifetime; closing it tears the graph down. See [Chapter 3](./ch03-00-design). **Surface** — Compile-time type reflection. It captures a type's structure (fields, parameters, type arguments) for the codec layer to use, without runtime reflection — which is what lets derivation work on Scala.js and Native. See [Chapter 6](./ch06-00-data). **Weaver** — Uni's derivation-based codec. One `Weaver[A]` serializes a type to JSON *or* MessagePack; you get it with `derives Weaver`. See [Chapter 6](./ch06-00-data). [← Appendix B: Uni and Airframe](./appendix-b-airframe) | [Back to the Book](./) --- --- url: https://wvlet.org/uni/guide.md --- # Introduction **Uni is the standard library Scala 3 didn't ship with.** Logging, object wiring, JSON & MessagePack, reactive streams, HTTP clients and servers, RPC, and a browser UI toolkit — small, composable pieces that work *identically* on the **JVM**, in the **browser and Node.js** (Scala.js), and as **native binaries** (Scala Native), with almost no external dependencies. It distills the most-used building blocks of the [Airframe](https://github.com/wvlet/airframe) ecosystem into one cohesive, Scala-3-first library. ::: tip Get going in 30 seconds Add one dependency ([Installation](./installation)) and you have all of it — no à-la-carte modules to assemble. ::: ## A quick taste Object wiring, structured logging, and lifecycle management — three things you usually reach for three libraries for — in a handful of lines: ```scala import wvlet.uni.design.Design import wvlet.uni.log.LogSupport class GreeterService extends LogSupport: def greet(name: String): String = info(s"Greeting ${name}") // logs with source file + line s"Hello, ${name}!" @main def main = Design.newDesign .bindSingleton[GreeterService] .build[GreeterService] { greeter => // wired, started, and cleaned up println(greeter.greet("Uni")) } ``` ## Why Uni? * 🪶 **Minimal dependencies** — foundational utilities without dragging in a heavy framework. * 🌍 **One codebase, three runtimes** — the same source compiles for JVM, Scala.js, and Scala Native. * 🧩 **Composable** — small, focused APIs designed to combine, not to lock you in. * ⚡ **Scala 3 first** — built around `derives`, `given`, enums, and macros. * 🔋 **Production-tested** — battle-hardened code, refined from the Airframe ecosystem. ## Uni and Airframe Uni is the Scala 3 successor of [Airframe](https://github.com/wvlet/airframe). The two projects have different goals: * **Uni** supports only Scala 3. Dropping Scala 2 lets it make full use of Scala 3 features in its APIs and implementation. It also bundles the core utilities into a few modules (`uni`, `uni-netty`, `uni-test`) instead of many `airframe-*` artifacts. * **Airframe** supports both Scala 2 and Scala 3, and it remains maintained. There is no plan to deprecate it. ::: tip Which one should I use? For a new Scala 3 project, use Uni. If your project still needs Scala 2, or you depend on Airframe modules that Uni doesn't cover, keep using Airframe. See the [Airframe migration table](./for-ai-agents#how-do-i-migrate-from-airframe) for how APIs map between them. ::: ## What's inside #### Core | Module | Description | |--------|-------------| | [Design](/core/design) | Object wiring with lifecycle management | | [Logging](/core/logging) | Logging with source-location tracking | | [Surface](/core/surface) | Compile-time type introspection | | [FileSystem](/core/filesystem) | Cross-platform file I/O with `IOPath` | #### Data & Serialization | Module | Description | |--------|-------------| | [JSON](/core/json) | Pure-Scala JSON parser and DSL | | [MessagePack](/core/msgpack) | Compact binary serialization | | [Weaver](/core/weaver) | Derivation-based codecs — JSON / MessagePack / `Map` from one `derives Weaver` | #### Async & Control Flow | Module | Description | |--------|-------------| | [Rx](/rx/) | Reactive streams and async data flows | | [Control](/control/) | Retry, circuit breaker, rate limiter, cache, resources | | [BackgroundTask](/control/background-task) | Cancellable, progress-pollable background workers | #### HTTP & RPC | Module | Description | |--------|-------------| | [HTTP](/http/) | Cross-platform client **and** server (Netty / Node.js / Native) | | [RPC](/http/rpc) | Type-safe remote calls, with client code generation | | [WebSocket](/http/websocket) | Cross-platform bidirectional WebSocket client | | [Server-Sent Events](/http/sse) | One-way server→client streaming | #### UI & CLI | Module | Description | |--------|-------------| | [CLI](/cli/) | Terminal styling, progress bars, command launching | | [Web UI](/dom/) | Reactive browser UIs with RxElement (Scala.js) | #### Testing | Module | Description | |--------|-------------| | [UniTest](/core/unitest) | Lightweight, expressive, cross-platform test framework | ## Next steps * 📦 [**Installation**](./installation) — add Uni to your sbt / Scala CLI project * 🧭 [**Design Principles**](./principles) — the architecture and the *why* * 🔍 [**Core Utilities**](/core/) — start exploring the foundational APIs --- --- url: https://wvlet.org/uni/guide/installation.md --- # Installation ## Requirements * Scala 3.9 (LTS) or later. uni's artifacts are compiled with Scala 3.9, and older compilers cannot read their TASTy. * sbt 1.9+ or sbt 2.x ## Adding Dependencies Add uni to your `build.sbt`: ```scala // Core utilities (object wiring, logging, JSON, HTTP, Rx, etc.) libraryDependencies += "org.wvlet.uni" %% "uni" % "2026.1.35" ``` ### HTTP Server on the JVM To run an HTTP server on the JVM, add the Netty-based server module (it pulls in `uni` transitively): ```scala // Netty-based HTTP server (JVM only) libraryDependencies += "org.wvlet.uni" %% "uni-netty" % "2026.1.35" ``` See the [REST Server guide](../http/server.md) for usage. On Scala.js and Scala Native, the HTTP server backends (`NodeServer` / `NativeServer`) are included in `uni` itself — no extra module is needed. ### Testing Framework Add [UniTest](../core/unitest.md) as a test dependency and register its test framework: ```scala libraryDependencies += "org.wvlet.uni" %% "uni-test" % "2026.1.35" % Test testFrameworks += new TestFramework("wvlet.uni.test.Framework") ``` ## Cross-Platform Projects For Scala.js or Scala Native projects on sbt 1.x, use `%%%`: ```scala // Scala.js / Scala Native (sbt 1.x) libraryDependencies += "org.wvlet.uni" %%% "uni" % "2026.1.35" libraryDependencies += "org.wvlet.uni" %%% "uni-test" % "2026.1.35" % Test ``` On sbt 2.x, `%%` resolves the platform-specific artifact for all platforms, so `%%%` is not needed. ::: tip Scala Native and libcurl No system libraries are needed to build a Scala Native binary against uni. Only reaching for the HTTP client pulls in libcurl, which must then be present as a shared library — see [Linking libcurl on Scala Native](../http/client.md#linking-libcurl-on-scala-native). ::: ## Imports Common imports for getting started: ```scala // Object wiring import wvlet.uni.design.Design // Logging import wvlet.uni.log.{LogSupport, Logger, LogLevel} // JSON import wvlet.uni.json.JSON // HTTP import wvlet.uni.http.{Http, HttpRequest, HttpResponse} // Reactive streams import wvlet.uni.rx.Rx // Control flow import wvlet.uni.control.{Retry, CircuitBreaker, Resource} ``` ## Verifying Installation Create a simple test to verify the installation: ```scala import wvlet.uni.log.LogSupport object Main extends App with LogSupport: info("uni is working!") ``` Run with: ```bash sbt run ``` You should see log output with your message. ## IDE Support uni works with all major Scala IDEs: * **IntelliJ IDEA** with Scala plugin * **VS Code** with Metals * **Neovim** with Metals The library uses standard Scala 3 features, so IDE support is seamless. --- --- url: https://wvlet.org/uni/guide/principles.md --- # Design Principles uni is built on a set of core principles that guide its design and implementation. ## Minimal Dependencies uni avoids heavy external dependencies. The core module has minimal dependencies, making it suitable for: * Applications with strict dependency requirements * Libraries that don't want to impose dependencies on users * Environments where classpath size matters (Scala.js, Native) ## Cross-Platform First Every feature is designed to work across: | Platform | Support | |----------|---------| | JVM | Full support | | Scala.js | Browser and Node.js | | Scala Native | Native binaries | Platform-specific code is isolated in `.jvm`, `.js`, and `.native` directories. ## Compile-Time Safety uni leverages Scala 3's type system for compile-time guarantees: ```scala // Object wiring is type-safe val design = Design.newDesign .bindImpl[UserRepository, PostgresUserRepository] // Surface provides compile-time type reflection val surface = Surface.of[User] ``` ## Composable Design Small, focused utilities that combine well: ```scala // Combine retry with HTTP client val response = Retry .withBackoff(maxRetry = 3) .run { httpClient.send(request) } // Combine Rx with resource management Rx.interval(1, TimeUnit.SECONDS) .take(10) .map(i => fetchData(i)) .subscribe(println) ``` ## Lifecycle Management Proper resource lifecycle through Design sessions: ```scala val design = Design.newDesign .bindSingleton[DatabasePool] .onStart(_.connect()) .onShutdown(_.close()) design.withSession { session => // DatabasePool is connected val pool = session.build[DatabasePool] // Use pool... } // Automatically closed ``` ## Functional Patterns Embrace functional programming patterns: * Immutable data structures * Pure functions where possible * Effect handling with Rx * Pattern matching over type checks ```scala // Transform with pattern matching json.transform { case Success(value) => processValue(value) case Failure(error) => handleError(error) } // Functional error handling Rx.single(riskyOperation()) .recover { case e: NetworkError => fallbackValue } .subscribe(handleResult) ``` ## Configuration Classes Config classes use a `withXXX` pattern for immutable updates: ```scala val config = HttpClientConfig() .withConnectTimeout(5000) .withReadTimeout(30000) .withMaxRetries(3) ``` Optional fields support `noXXX` methods: ```scala val config = HttpClientConfig() .noRetry // Disable retry .noLogging // Disable request logging ``` ## Avoid Try\[A] Return Types Instead of returning `Try[A]`, uni uses: * Direct exceptions for programming errors * `Either[Error, A]` for expected failures * `Rx[A]` for async operations with error handling This makes error handling explicit while avoiding the overhead of wrapping every result. --- --- url: https://wvlet.org/uni/guide/for-ai-agents.md description: >- How AI coding agents should pick and use Uni, the Scala 3 utility library for dependency injection, logging, JSON/MessagePack, HTTP, RPC, MCP servers, and testing on JVM, Scala.js, and Scala Native. --- # Uni for AI Agents **Uni is a single Scala 3 library (`org.wvlet.uni`) that covers dependency injection, logging, JSON/MessagePack serialization, reactive streams, HTTP client/server, typed RPC, MCP servers, and unit testing, all with the same API on the JVM, Scala.js, and Scala Native.** It is the Scala 3 successor of [Airframe](https://github.com/wvlet/airframe). This page is written for AI coding agents (Claude Code, Codex, Cursor, Copilot, and others) and for the people who configure them. It sums up when to choose Uni, the canonical imports, and the mistakes agents most often make. ## Machine-readable docs | URL | Contents | |-----|----------| | [`/uni/llms.txt`](https://wvlet.org/uni/llms.txt) | Index of every documentation page, with links to Markdown versions | | [`/uni/llms-full.txt`](https://wvlet.org/uni/llms-full.txt) | The full documentation in one Markdown file | | `.md` | Markdown version of a page, e.g. [`/uni/guide/for-ai-agents.md`](https://wvlet.org/uni/guide/for-ai-agents.md). A section index such as `/uni/guide/` is published as `/uni/guide.md`. | ## How do I install the Uni skill? The `wvlet/uni` repository is a Claude Code plugin marketplace. Its `uni` plugin adds an Agent Skill with the canonical imports, sbt setup, verified snippets, and the mistakes listed on this page. Claude Code loads it whenever a task touches Uni or asks for DI, logging, JSON, HTTP, RPC, MCP, or tests in Scala 3. ```text /plugin marketplace add wvlet/uni /plugin install uni@uni ``` On Claude Code 2.1.275 or later, `/plugin install uni --marketplace wvlet/uni` does both in one step. Agents without a plugin system can read [`llms-full.txt`](https://wvlet.org/uni/llms-full.txt) instead. ## When should I use Uni? Choose Uni when a Scala 3 project needs one or more of the following and you want few transitive dependencies: * Dependency injection with lifecycle management (`Design`), instead of a framework * Structured logging with source locations (`LogSupport`) * JSON or MessagePack codecs derived from case classes (`derives Weaver`) * An HTTP client that works on the JVM, Node.js/browser, and native binaries * An HTTP/RPC server (Netty, JVM) or a stdio MCP server for AI tools * A test framework that runs on all three platforms (`UniTest`) Do not mix in Airframe artifacts for the same features. Uni replaces them. ## How do I add Uni to a build? ```scala // build.sbt libraryDependencies ++= Seq( "org.wvlet.uni" %% "uni" % "2026.1.35", "org.wvlet.uni" %% "uni-test" % "2026.1.35" % Test ) testFrameworks += new TestFramework("wvlet.uni.test.Framework") // JVM-only Netty HTTP server libraryDependencies += "org.wvlet.uni" %% "uni-netty" % "2026.1.35" ``` Uni requires Scala `3.9.0` or later. On sbt 2.x, use `%%` for all platforms. On sbt 1.x, use `%%%` for Scala.js and Scala Native projects. ## What are the canonical imports? | Task | Import | Entry point | |------|--------|-------------| | Dependency injection | `wvlet.uni.design.Design` | `Design.newDesign.bindSingleton[A].build[A] { a => ... }` | | Logging | `wvlet.uni.log.LogSupport` | `class A extends LogSupport` then `info(...)`, `debug(...)` | | JSON / MessagePack codec | `wvlet.uni.weaver.Weaver` | `case class P(...) derives Weaver`, `Weaver.toJson(p)`, `Weaver.fromJson[P](json)` | | JSON tree | `wvlet.uni.json.JSON` | `JSON.parse(text)` | | Reactive streams | `wvlet.uni.rx.Rx` | `Rx.variable(v)`, `Rx.single(v)`, `.map`, `.flatMap` | | HTTP client | `wvlet.uni.http.{Http, Request}` | `Http.client.newSyncClient.send(Request.get(url))` | | HTTP server (JVM, `uni-netty`) | `wvlet.uni.http.netty.NettyServer` | See [REST Server](../http/server) | | Typed RPC | `wvlet.uni.http.rpc.{RPCRouter, RPCClient}` | See [RPC](../http/rpc) | | MCP server | `wvlet.uni.mcp.*` | See [Build an MCP Server](../mcp/) | | Retry / circuit breaker | `wvlet.uni.control.{Retry, CircuitBreaker}` | `Retry.withBackOff().run { ... }` | | Unit tests | `wvlet.uni.test.*` | `class T extends UniTest: test("...") { x shouldBe y }` | ## How do I migrate from Airframe? | Airframe | Uni | |----------|-----| | `wvlet.airframe.Design` / `newDesign` | `wvlet.uni.design.Design` / `Design.newDesign` | | `wvlet.log.LogSupport` | `wvlet.uni.log.LogSupport` | | `wvlet.airframe.codec.MessageCodec` | `wvlet.uni.weaver.Weaver` (`derives Weaver`) | | `wvlet.airframe.surface.Surface` | `wvlet.uni.surface.Surface` | | `wvlet.airframe.rx.Rx` | `wvlet.uni.rx.Rx` | | `wvlet.airframe.http.Http` | `wvlet.uni.http.Http` | | `wvlet.airframe.control.Retry` | `wvlet.uni.control.Retry` | | `wvlet.airspec.AirSpec` | `wvlet.uni.test.UniTest` | Uni uses a separate package root (`wvlet.uni.*`), so both libraries can coexist during a migration. See [Appendix B: Uni and Airframe](../book/appendix-b-airframe) for background. ## What mistakes do agents make with Uni? * **Using Scala 2 syntax.** Uni is Scala 3 only. Use indentation syntax, `given`/`using`, `enum`, and `derives`. * **Using ScalaTest matchers in UniTest.** Write `(x >= 1) shouldBe true`, not `x should be >= 1`. For type checks, write `v shouldMatch { case _: T => }` instead of `asInstanceOf`. * **Importing only `UniTest`.** Use `import wvlet.uni.test.*`. `shouldBe defined` and `shouldBe empty` need matcher objects from that package. * **Checking `isSuccessful` after `send`.** The HTTP client throws for non-2xx responses. Most 4xx statuses throw `HttpException`. Retryable statuses (5xx, 429\) throw `HttpMaxRetryException` after the retries, and it is not a subclass of `HttpException`. Failed RPC client calls throw the same exceptions. * **Hand-writing JSON codecs.** Add `derives Weaver` to the case class. * **Adding a DI framework, a logging backend, or a JSON library next to Uni.** Uni already provides them. Adding another creates two ways to do one thing. ## Example: a wired service with logging and JSON ```scala import wvlet.uni.design.Design import wvlet.uni.log.LogSupport import wvlet.uni.weaver.Weaver case class Greeting(name: String, message: String) derives Weaver class GreeterService extends LogSupport: def greet(name: String): String = info(s"Greeting ${name}") Weaver.toJson(Greeting(name, s"Hello, ${name}!")) @main def main = Design.newDesign .bindSingleton[GreeterService] .build[GreeterService] { greeter => println(greeter.greet("Uni")) } ``` --- --- url: https://wvlet.org/uni/core.md --- # Core Utilities The `uni` module provides essential utilities for Scala application development. ## Overview | Component | Description | |-----------|-------------| | [Design](./design) | Object wiring with lifecycle management | | [UniTest](./unitest) | Lightweight testing framework | | [Logging](./logging) | Structured logging with LogSupport | | [JSON](./json) | Pure Scala JSON parser and DSL | | [MessagePack](./msgpack) | Binary serialization format | | [Weaver](./weaver) | Derivation-based serialization (JSON / MessagePack / Map) | | [Surface](./surface) | Compile-time type introspection | | [FileSystem](./filesystem) | Cross-platform file I/O with IOPath | | [Utilities](./utilities) | IDs (UUIDv7, ULID, NanoId), encodings (Base62, CrockfordBase32), value types (DataSize, Count, ElapsedTime), timing (StopWatch, Timer) | ## Quick Start ```scala import wvlet.uni.design.Design import wvlet.uni.log.LogSupport import wvlet.uni.json.JSON // Service with logging class UserService extends LogSupport: def findUser(id: String): Option[User] = debug(s"Finding user: ${id}") // Implementation None // Object wiring with Design val design = Design.newDesign .bindSingleton[UserService] design.build[UserService] { service => service.findUser("123") } // JSON parsing val json = JSON.parse("""{"name": "Alice", "age": 30}""") val name = json("name").toStringValue // "Alice" ``` ## Package Structure ``` wvlet.uni.design - Object wiring framework wvlet.uni.log - Logging framework wvlet.uni.json - JSON processing wvlet.uni.msgpack - MessagePack serialization wvlet.uni.surface - Type reflection wvlet.uni.weaver - Object serialization wvlet.uni.io - Cross-platform file system wvlet.uni.util - ID generators and value types ``` ## Cross-Platform Support All core utilities work across JVM, Scala.js, and Scala Native platforms. | Feature | JVM | JS | Native | |---------|-----|-----|--------| | Design | ✓ | ✓ | ✓ | | Logging | ✓ | ✓ | ✓ | | JSON | ✓ | ✓ | ✓ | | MessagePack | ✓ | ✓ | ✓ | | Surface | ✓ | ✓ | ✓ | | FileSystem | ✓ | ✓ | ✓ | | Utilities | ✓ | ✓ | ✓ | --- --- url: https://wvlet.org/uni/core/design.md --- # Design Design is a framework for building and wiring complex objects at runtime. It provides a declarative way to define how objects are constructed and connected, managing their lifecycles automatically. ## Core Concepts Design is built around two fundamental abstractions: * **Design**: An immutable configuration that maps types to their implementations or instances * **Session**: A runtime context that manages singleton instances and object lifecycles The framework emphasizes constructor-based wiring - dependencies are provided through class constructors, keeping your application code decoupled from the framework itself. ## Quick Start ### Step 1: Define Classes with Dependencies ```scala import wvlet.uni.design.Design case class AppConfig(appName: String) class MyApp(private val config: AppConfig): def greet(): String = s"Hello from ${config.appName}!" ``` ### Step 2: Create a Design ```scala val design = Design.newDesign .bindInstance[AppConfig](AppConfig("My Application")) ``` ### Step 3: Build and Use ```scala design.build[MyApp] { app => println(app.greet()) // Prints: Hello from My Application! } // Session automatically starts and shuts down ``` ## Design Design is an immutable object for holding object bindings and their lifecycle hooks: ```scala import wvlet.uni.design.Design val design: Design = Design.newDesign .bindImpl[A, AImpl] // Bind A to AImpl (singleton) .bindInstance[B](B(1)) // Bind to a specific instance .bindSingleton[S] // Bind S as a singleton .bindEagerSingleton[ES] // Eagerly initialized singleton .bindProvider { (d1: D1) => P(d1) } // Provider with 1 dependency .bindProvider { (d1: D1, d2: D2) => // Provider with 2 dependencies P(d1, d2) } .bindProvider { (d1: D1, d2: D2, d3: D3) => // Up to 5 dependencies supported P(d1, d2, d3) } ``` ### Binding Summary | Method | Description | |--------|-------------| | `bindImpl[A, AImpl]` | Bind interface A to implementation AImpl (singleton) | | `bindInstance[A](obj)` | Bind to a specific pre-created instance | | `bindSingleton[A]` | Bind A as a lazily-initialized singleton | | `bindEagerSingleton[A]` | Bind A as an eagerly-initialized singleton | | `bindProvider { (d1: D1) => A(d1) }` | Bind using a factory function with dependencies | ## Lifecycle Management Design provides comprehensive lifecycle management for your objects. ### Lifecycle Hooks ```scala val design = Design.newDesign .bindSingleton[DatabasePool] .onInit { pool => // Called when the object is first created pool.initialize() } .onStart { pool => // Called when the session starts pool.connect() } .onShutdown { pool => // Called when the session shuts down pool.close() } ``` ### Complete Lifecycle Order 1. `onInit`: Object initialization (called once when created) 2. `onInject`: After object is injected 3. `onStart`: When session starts 4. `afterStart`: After all onStart hooks complete 5. `beforeShutdown`: Before shutdown begins 6. `onShutdown`: Session shutdown (cleanup) ### AutoCloseable Support Classes implementing `AutoCloseable` are automatically closed on session shutdown: ```scala class MyDB(name: String) extends AutoCloseable: private val conn = openConnection(name) // Your connection logic override def close(): Unit = conn.close() // MyDB.close() called automatically when session ends design.bindSingleton[MyDB] ``` ## Session Management Sessions hold singleton instances and manage their lifecycles. ### Automatic Session Management The recommended approach - session lifecycle is handled automatically: ```scala design.build[App] { app => // Session started, app is ready to use app.run() } // Session automatically shuts down here ``` ### Manual Session Control For more control over the session lifecycle: ```scala val session = design.newSession try session.start() val app = session.build[App] app.run() finally session.shutdown() ``` ### Type-Safe Return Values Use `run` to return values from the session scope: ```scala val result: Int = design.run { (service: ComputeService) => service.calculate() } ``` ### Accessing the Session Inject the session itself to register dynamically created resources: ```scala import wvlet.uni.design.Session class MyApp(session: Session): def openDB(name: String): MyDB = val db = MyDB(name) session.register(db) // Register for lifecycle management db ``` ### Child Sessions Create child sessions for request-scoped lifecycles: ```scala import wvlet.uni.design.Session class MyServer(session: Session): def handleRequest() = val requestDesign = Design.newDesign .bindSingleton[RequestContext] session.withChildSession(requestDesign) { childSession => val ctx = childSession.build[RequestContext] // RequestContext is scoped to this request processRequest(ctx) } // RequestContext is cleaned up here ``` ## Design Composition Designs are immutable and can be combined: ```scala val loggingDesign = Design.newDesign .bindImpl[Logger, ConsoleLogger] val databaseDesign = Design.newDesign .bindImpl[DatabaseService, PostgresDatabase] val serviceDesign = Design.newDesign .bindImpl[UserService, UserServiceImpl] // Combine all designs val fullDesign = loggingDesign + databaseDesign + serviceDesign ``` Later bindings override earlier ones: ```scala val baseDesign = Design.newDesign .bindImpl[Cache, InMemoryCache] val redisDesign = Design.newDesign .bindImpl[Cache, RedisCache] // RedisCache will be used for Cache val combined = baseDesign + redisDesign ``` ## Production Mode In production mode, all singletons are initialized eagerly at session start: ```scala design .withProductionMode .build[App] { app => // All singletons already initialized app.run() } ``` This helps catch configuration errors early and ensures predictable startup behavior. ## Generic Type Binding Design distinguishes between different generic type parameters: ```scala // These are distinct bindings design .bindInstance[Seq[Int]](Seq(1, 2, 3)) .bindInstance[Seq[String]](Seq("a", "b", "c")) .bindInstance[Map[String, Int]](Map("x" -> 1)) ``` ## Tagged Type Binding Use tagged types to bind multiple instances of the same type: ```scala import wvlet.uni.surface.tag.* // Define tags as traits trait Production trait Staging case class DatabaseConfig(url: String) // Service that needs both configs class MigrationService( prodDb: DatabaseConfig @@ Production, stagingDb: DatabaseConfig @@ Staging ): def migrate(): Unit = println(s"Migrating from ${stagingDb.url} to ${prodDb.url}") // Bind different configs with tags val design = Design.newDesign .bindInstance[DatabaseConfig @@ Production](DatabaseConfig("jdbc:prod://...")) .bindInstance[DatabaseConfig @@ Staging](DatabaseConfig("jdbc:staging://...")) design.build[MigrationService] { service => service.migrate() } ``` ### Primitive Type Tagging Useful for configuration values: ```scala import wvlet.uni.surface.tag.* trait Port trait Host class ServerConfig(host: String @@ Host, port: Int @@ Port): def address: String = s"${host}:${port}" val design = Design.newDesign .bindInstance[String @@ Host]("localhost") .bindInstance[Int @@ Port](8080) ``` ## Testing Design makes testing straightforward by allowing you to override bindings: ```scala // Production design val prodDesign = Design.newDesign .bindImpl[EmailService, SmtpEmailService] .bindImpl[UserRepository, PostgresUserRepository] // Test design - override specific bindings val testDesign = prodDesign .bindImpl[EmailService, MockEmailService] .bindImpl[UserRepository, InMemoryUserRepository] // Use in tests testDesign.build[UserService] { service => val result = service.createUser("test@example.com") result.email shouldBe "test@example.com" } ``` ## Debugging ### Enable Debug Logging Add to your `log.properties`: ``` wvlet.uni.design=debug ``` This shows detailed information about binding resolution and object creation. ### Suppress Lifecycle Logging For cleaner output in tests: ```scala design .noLifeCycleLogging .build[App] { app => ... } ``` ## Best Practices 1. **Use traits for interfaces** - Enables easy substitution in tests 2. **Create designs at the entry point** - Main method or test setup 3. **Pass dependencies through constructors** - Keeps classes testable and explicit 4. **Use singletons for shared resources** - Database pools, HTTP clients, caches 5. **Leverage lifecycle hooks** - For proper resource initialization and cleanup 6. **Compose designs** - Build modular, reusable design configurations 7. **Prefer tagged types over type aliases** - For type-safe multi-binding of same types ## Common Patterns ### Application Configuration ```scala case class Config(host: String, port: Int) class Server(config: Config): def start(): Unit = println(s"Starting server on ${config.host}:${config.port}") val design = Design.newDesign .bindInstance[Config](Config("localhost", 8080)) .bindSingleton[Server] // Override for testing val testDesign = design .bindInstance[Config](Config("localhost", 18080)) ``` ### Resource Management ```scala case class DbConfig(url: String) class DatabaseConnection(config: DbConfig) extends AutoCloseable: private var conn: Connection = _ def connect(): Unit = conn = DriverManager.getConnection(config.url) def query(sql: String): ResultSet = conn.createStatement().executeQuery(sql) override def close(): Unit = if conn != null then conn.close() val design = Design.newDesign .bindSingleton[DatabaseConnection] .onStart(_.connect()) // close() called automatically via AutoCloseable ``` ### Multi-Handler Pattern ```scala trait RequestHandler: def handle(request: Request): Response class ApiHandler extends RequestHandler: def handle(request: Request): Response = ??? class StaticHandler extends RequestHandler: def handle(request: Request): Response = ??? class Router(apiHandler: ApiHandler, staticHandler: StaticHandler): def route(path: String, request: Request): Response = path match case p if p.startsWith("/api") => apiHandler.handle(request) case _ => staticHandler.handle(request) val design = Design.newDesign .bindSingleton[ApiHandler] .bindSingleton[StaticHandler] .bindSingleton[Router] ``` ## Known Limitations ### Type Aliases in Provider Bindings Type aliases may not work correctly in provider bindings because they are resolved at compile time: ```scala case class Service(value: String) // This may cause issues type MyString = String Design.newDesign .bindInstance[MyString]("hello") .bindProvider { (s: MyString) => Service(s) } // May fail ``` **Solution**: Use tagged types instead: ```scala import wvlet.uni.surface.tag.* case class Service(value: String) trait EnvTag Design.newDesign .bindInstance[String @@ EnvTag]("hello") .bindProvider { (s: String @@ EnvTag) => Service(s) } // Works correctly ``` --- --- url: https://wvlet.org/uni/core/unitest.md --- # UniTest UniTest is a lightweight testing framework for Scala 3. It provides a simple, expressive API for writing tests with minimal boilerplate while supporting cross-platform development (JVM, Scala.js, Scala Native). ## Quick Start Add the dependency to your build: ```scala libraryDependencies += "org.wvlet.uni" %% "uni-test" % "2026.1.35" % Test testFrameworks += new TestFramework("wvlet.uni.test.Framework") ``` Write your first test: ```scala import wvlet.uni.test.UniTest class MyTest extends UniTest: test("addition works") { (1 + 1) shouldBe 2 } ``` Run tests: ```bash sbt test ``` ## Assertions in UniTest | Syntax | Meaning | |:-------|:--------| | `assert(x == y)` | Check x equals y | | `assertEquals(a, b, delta)` | Check Float/Double equality with tolerance | | `intercept[E] { ... }` | Catch an exception of type E | | `x shouldBe y` | Check x == y (supports deep equality for Array, Seq) | | `x shouldNotBe y` | Check x != y | | `x shouldBe null` | Check x is null | | `x shouldNotBe null` | Check x is not null | | `x shouldBe defined` | Check x.isDefined == true (Option, Seq, String) | | `x shouldBe empty` | Check x.isEmpty == true (Option, Seq, String) | | `x shouldBeTheSameInstanceAs y` | Check x eq y (reference equality) | | `x shouldNotBeTheSameInstanceAs y` | Check x ne y (reference inequality) | | `x shouldMatch { case .. => }` | Check x matches the pattern | | `x shouldContain y` | Check x contains y | | `x shouldNotContain y` | Check x does not contain y | | `fail("reason")` | Fail the test | | `ignore("reason")` | Ignore the test | | `cancel("reason")` | Cancel the test (e.g., setup failure) | | `pending("reason")` | Mark test as pending | | `pendingUntil("reason")` | Pending until condition is fixed | | `skip("reason")` | Skip the test (e.g., platform-specific) | ## Writing Tests ### Basic Test Structure ```scala import wvlet.uni.test.UniTest class CalculatorTest extends UniTest: test("add two numbers") { val result = 2 + 3 result shouldBe 5 } test("subtract two numbers") { val result = 5 - 3 result shouldBe 2 } ``` ### Nested Tests Tests can be nested for better organization: ```scala class MathTest extends UniTest: test("arithmetic operations") { val x = 10 test("addition") { (x + 5) shouldBe 15 } test("multiplication") { (x * 2) shouldBe 20 } } ``` Nested test names include their parent context: `"arithmetic operations / addition"`. ## Assertions ### Equality | Syntax | Description | |--------|-------------| | `a shouldBe b` | Assert `a` equals `b` | | `a shouldNotBe b` | Assert `a` does not equal `b` | ```scala test("equality assertions") { 1 + 1 shouldBe 2 "hello" shouldBe "hello" List(1, 2, 3) shouldBe List(1, 2, 3) 1 shouldNotBe 2 "hello" shouldNotBe "world" } ``` ### Null Checks ```scala test("null checks") { val value: String = null value shouldBe null val nonNull = "hello" nonNull shouldNotBe null } ``` ### Defined and Empty | Syntax | Description | |--------|-------------| | `a shouldBe defined` | Assert Option is Some, collection/string is non-empty | | `a shouldBe empty` | Assert Option is None, collection/string is empty | `defined` and `empty` are objects in the `wvlet.uni.test` package, so import `wvlet.uni.test.*` (not only `UniTest`) to use them. ```scala test("defined and empty") { val some: Option[Int] = Some(42) val none: Option[Int] = None some shouldBe defined none shouldBe empty some shouldNotBe empty none shouldNotBe defined List(1, 2, 3) shouldBe defined List.empty[Int] shouldBe empty "hello" shouldBe defined "" shouldBe empty } ``` ### Collection Assertions | Syntax | Description | |--------|-------------| | `collection shouldContain element` | Assert collection contains element | | `collection shouldNotContain element` | Assert collection does not contain element | ```scala test("collection assertions") { val list = List(1, 2, 3) list shouldContain 2 list shouldNotContain 5 } ``` ### String Assertions ```scala test("string assertions") { val str = "Hello, World!" str shouldContain "World" str shouldNotContain "Goodbye" str.startsWith("Hello") shouldBe true str.endsWith("!") shouldBe true } ``` ### Pattern Matching Use `shouldMatch` for type-safe pattern matching assertions: ```scala case class User(name: String, age: Int) test("pattern matching") { val result: Any = User("Alice", 30) result shouldMatch { case User(name, age) => name shouldBe "Alice" age shouldBe 30 } // Type checking result shouldMatch { case _: User => } } ``` Prefer `shouldMatch` over `asInstanceOf` for type-safe assertions. ### Reference Equality ```scala test("reference equality") { val a = List(1, 2, 3) val b = a val c = List(1, 2, 3) a shouldBeTheSameInstanceAs b a shouldNotBeTheSameInstanceAs c } ``` ### Floating Point Comparisons ```scala test("floating point with delta") { assertEquals(0.1 + 0.2, 0.3, 0.0001) assertEquals(3.14159f, 3.14f, 0.01f) } ``` ### Exception Testing ```scala test("exception handling") { val e = intercept[IllegalArgumentException] { require(false, "invalid argument") } e.getMessage shouldContain "invalid argument" } ``` ### Boolean Assertions For comparisons, use boolean expressions with `shouldBe true`: ```scala test("comparisons") { val value = 42 (value > 10) shouldBe true (value < 100) shouldBe true (value >= 42) shouldBe true (value <= 42) shouldBe true (value >= 0 && value <= 100) shouldBe true } ``` ## Test Control ### Skip Tests ```scala test("platform-specific test") { if isScalaJS then skip("Not supported on Scala.js") // JVM-only code, e.g., accessing a system property System.getProperty("java.version") shouldNotBe null } ``` ### Pending Tests ```scala test("future feature") { pending("Implementation not complete yet") } test("blocked by issue") { pendingUntil("Issue #123 is fixed") } ``` ### Flaky Tests Mark tests as flaky to convert failures to skipped status: ```scala test("network test", Flaky) { // Failures become skipped instead of failed fetchFromNetwork() shouldBe "expected" } // Or wrap specific code test("partially flaky") { val stableResult = computeLocally() stableResult shouldBe 42 flaky { val unstableResult = fetchFromNetwork() unstableResult shouldBe "expected" } } ``` ### Cancel and Ignore ```scala test("requires setup") { if !setupComplete then cancel("Setup failed, cannot run test") runTest() } test("deprecated test") { ignore("This test is no longer relevant") } ``` ### Platform Detection ```scala test("platform-specific behavior") { if isScalaJVM then // JVM-specific assertions System.getProperty("os.name") shouldBe defined if isScalaJS then // Scala.js-specific assertions skip("JVM-only test") if isScalaNative then // Scala Native-specific assertions skip("JVM-only test") } ``` ## Design Integration Use Design for object wiring in tests: ```scala import wvlet.uni.test.UniTest import wvlet.uni.design.Design class UserServiceTest extends UniTest: val testDesign = Design.newDesign .bindImpl[UserRepository, InMemoryUserRepository] .bindImpl[UserService, UserServiceImpl] test("create user") { testDesign.build[UserService] { service => val user = service.createUser("Alice") user.name shouldBe "Alice" } } test("find user") { testDesign.build[UserService] { service => service.createUser("Bob") val found = service.findByName("Bob") found shouldBe defined } } ``` ## Property-Based Testing UniTest ships a self-contained property-based testing toolkit — no external dependency — and it's part of the base `UniTest` trait. Any test class gets `forAll` automatically. Call it with a body that holds for every randomly generated input; a failing run is automatically *shrunk* to a minimal counter-example, and the seed is reported so the run can be replayed. ```scala import wvlet.uni.test.UniTest class ListLawsTest extends UniTest: test("addition is commutative") { forAll { (a: Int, b: Int) => (a + b) shouldBe (b + a) } } test("reverse is an involution") { forAll { (xs: List[Int]) => xs.reverse.reverse shouldBe xs } } ``` ### Generators Generators live in `wvlet.uni.test.check.Gen`. Every `forAll` body receives randomly generated values — by default drawn from the type's implicit `Arbitrary` instance, or from a `Gen` you supply explicitly. ```scala import wvlet.uni.test.check.Gen test("positive integers") { forAll(Gen.posNum[Int]) { n => (n > 0) shouldBe true } } test("bounded integers") { forAll(Gen.chooseNum(1, 100)) { n => (n >= 1 && n <= 100) shouldBe true } } ``` Common combinators: | Factory | Produces | |---------|----------| | `Gen.const(x)` | Always `x` | | `Gen.chooseNum[T](min, max)` | Uniform integer in `[min, max]` | | `Gen.posNum[T]` | Positive, non-zero value | | `Gen.oneOf(a, b, c)` | Uniform choice from values | | `Gen.oneOfGen(g1, g2)` | Uniform choice from generators | | `Gen.frequency(w1 -> g1, w2 -> g2)` | Weighted choice from generators | | `Gen.listOf(g)` / `Gen.nonEmptyListOf(g)` | List of `g`, size-aware | | `Gen.listOfN(n, g)` | List of exactly `n` elements | | `Gen.setOf(g)` / `Gen.mapOf(gK, gV)` | Set / Map, size-aware | | `Gen.option(g)` | `Some(g)` or `None` | | `Gen.alphaChar` / `.numChar` / `.alphaNumChar` / `.asciiChar` | Single character | | `Gen.alphaStr` / `.numStr` / `.alphaNumStr` / `.asciiStr` | Size-aware string | | `Gen.identifier` | `[A-Za-z][A-Za-z0-9]*` | | `Gen.sized(n => g)` / `Gen.resize(n, g)` | Read or override the current size | Generators compose like `Option`s: `g.map`, `g.flatMap`, `g.filter`, `g.zip`, plus for-comprehensions. ```scala val evenPositive: Gen[Int] = Gen.posNum[Int].map(_ * 2) val labeledInt: Gen[(String, Int)] = for label <- Gen.alphaStr value <- Gen.chooseNum(0, 100) yield (label, value) ``` ### Arbitrary and derivation Implicit `Arbitrary[T]` instances drive the argument-less `forAll` overload. Defaults ship for primitives, `String`, `Array[Byte]`, `List`, `Vector`, `Option`, and small tuples. For your own types, use Scala 3's `derives`: ```scala import wvlet.uni.test.check.Arbitrary case class Point(x: Int, y: Int) derives Arbitrary sealed trait Shape derives Arbitrary case class Circle(r: Int) extends Shape case class Rect(w: Int, h: Int) extends Shape case class Triangle(a: Int, b: Int, c: Int) extends Shape test("shapes round-trip through the renderer") { forAll { (s: Shape) => Renderer.parse(Renderer.show(s)) shouldBe s } } ``` Derivation is recursive: a `derives Arbitrary` on a sealed trait picks up each case class automatically, and fields of user types that themselves have an `Arbitrary` given (or a `Mirror`) are threaded through. To summon a default generator explicitly, use `Arbitrary.arbitrary[T]`: ```scala forAll(Arbitrary.arbitrary[String]) { s => s.length >= 0 shouldBe true } ``` ### Shrinking When a property fails, the runner shrinks the counter-example to a minimal value. Default shrinkers ship for numeric primitives, `Char`, `String`, `Array[Byte]`, `List`, `Option`, and tuples up to arity 5. ```scala test("sum is never negative for non-negative inputs") { forAll { (xs: List[Int]) => xs.filter(_ >= 0).sum >= 0 shouldBe true } } ``` If this property were broken, the error message would point at a minimal list — typically `List(...)` with one or two elements — rather than the raw random failing input. User types get an empty shrinker by default, so `forAll[MyCaseClass]` compiles even without a shrink instance. To customize, provide a given: ```scala import wvlet.uni.test.check.Shrink given Shrink[Point] = Shrink { p => LazyList(Point(0, 0), Point(p.x, 0), Point(0, p.y)) } ``` ### Preconditions with `==>` / `implies` Use `==>` (or its English alias `implies`) to discard samples that don't satisfy a precondition. The runner retries with a new sample and fails the test only if the discard budget (default: 500) runs out. ```scala test("division by non-zero is self-inverse") { forAll { (a: Int, b: Int) => (b != 0) ==> { ((a * b) / b) shouldBe a } } } test("reads the same way in prose") { forAll { (xs: List[Int]) => xs.nonEmpty implies { xs.head shouldBe xs(0) } } } ``` ### Statistics: `classify` and `collect` Break down what the generator is actually producing to make sure your property is exercising the cases you care about. Counts print after the run. ```scala test("partition respects predicate") { forAll(Gen.listOf(Gen.chooseNum(-100, 100))) { xs => classify(xs.isEmpty, "empty") classify(xs.size > 50, "large") collect(s"size=${xs.size / 10 * 10}") val (neg, nonNeg) = xs.partition(_ < 0) (neg ++ nonNeg).sorted shouldBe xs.sorted } } ``` Output (example): ``` [property] 42 × size=10, 31 × size=0, 18 × large, 9 × size=20, 7 × empty ``` ### Configuration Override `propertyConfig` on the test class to change sample count, seed, size range, discard limit, or shrink budget for every `forAll` in that class. For a one-off tweak of the sample count, override `minSuccessfulTests`. ```scala import wvlet.uni.test.check.PropertyConfig class HeavyPropertyTest extends UniTest: override protected def propertyConfig: PropertyConfig = PropertyConfig.default .withMinSuccessful(500) .withSize(0, 256) .withSeed(1_234L) // reproducible runs ``` | Knob | Method | Default | |------|--------|---------| | Samples | `withMinSuccessful(n)` | 100 | | Discard limit | `withMaxDiscarded(n)` | 500 | | Size range | `withSize(min, max)` | `0..100` | | Shrink budget | `withMaxShrinks(n)` | 1000 | | Seed | `withSeed(s)` | `System.nanoTime()` | Every failure includes the seed. To reproduce a flaky failure, copy the seed from the error message into `propertyConfig.withSeed(...)` and rerun — the exact same samples are drawn. ## Running Tests ### sbt Commands ```bash # Run all tests sbt test # Run tests in a specific module sbt coreJVM/test # Run a specific test class sbt "my-project/testOnly *MyTest" # Run with debug logging sbt "coreJVM/testOnly * -- -l debug" # Run with trace logging for specific package sbt "test -- -l:wvlet.uni.*=trace" # Filter tests by name sbt "test -- -t:ChatSession" ``` ::: tip Scala.js tests in a real browser To run Scala.js tests in a real browser (headless Chromium/Firefox/WebKit) with ES module support — for DOM, web, and Electron code — see the [sbt-uni-playwright Plugin](/build/sbt-uni-playwright). ::: ### Test Output Test results are displayed with status symbols: | Symbol | Status | Description | |--------|--------|-------------| | `+` | Success | Test passed | | `-` | Failure | Assertion failed | | `x` | Error | Unexpected exception | | `~` | Skipped | Test was skipped | | `?` | Pending | Test not implemented | | `!` | Cancelled | Setup failed | ## Best Practices 1. **Avoid mocks** - Use real implementations or in-memory versions 2. **Use Design for wiring** - Leverage object wiring for test isolation 3. **Prefer `shouldMatch`** - Use pattern matching for type checks instead of `asInstanceOf` 4. **One behavior per test** - Keep tests focused on a single behavior 5. **Descriptive names** - Test names should describe the expected behavior 6. **Use boolean expressions** - For comparisons: `(x > 5) shouldBe true` ## Cross-Platform Support UniTest works across all Scala 3 platforms: | Feature | JVM | Scala.js | Scala Native | |---------|:---:|:--------:|:------------:| | Basic assertions | ✓ | ✓ | ✓ | | Nested tests | ✓ | ✓ | ✓ | | Flaky tests | ✓ | ✓ | ✓ | | Property-based testing | ✓ | ✓ | ✓ | | IDE integration | ✓ | - | - | ## Package ```scala import wvlet.uni.test.UniTest // Property-based testing is part of UniTest — nothing else to import. // For custom generators / shrinkers: import wvlet.uni.test.check.{Arbitrary, Gen, Shrink, PropertyConfig} ``` --- --- url: https://wvlet.org/uni/core/logging.md --- # Logging uni provides a comprehensive logging framework with structured logging support. ## LogSupport Trait The easiest way to add logging is to extend `LogSupport`: ```scala import wvlet.uni.log.LogSupport class MyService extends LogSupport: def process(data: String): Unit = info(s"Processing: ${data}") debug("Detailed information") try riskyOperation() catch case e: Exception => error("Operation failed", e) ``` ## Log Levels Available log levels in order of severity: | Level | Method | Use Case | |-------|--------|----------| | ERROR | `error()` | Errors requiring attention | | WARN | `warn()` | Potential issues | | INFO | `info()` | General information | | DEBUG | `debug()` | Debugging details | | TRACE | `trace()` | Fine-grained tracing | ## Logger Configuration ### Setting Log Levels ```scala import wvlet.uni.log.{Logger, LogLevel} // Set global default Logger.setDefaultLogLevel(LogLevel.DEBUG) // Set for specific logger Logger("MyService").setLogLevel(LogLevel.TRACE) ``` ### Creating Named Loggers ```scala val logger = Logger("MyApp") logger.info("Application started") logger.debug("Configuration loaded") ``` ## Logging with Exceptions Include stack traces in error logs: ```scala try riskyOperation() catch case e: IOException => error("IO operation failed", e) case e: Exception => error(s"Unexpected error: ${e.getMessage}", e) ``` ## Zero-Overhead Logging with Scala Macros The logging methods use Scala 3 `inline` macros, which means: 1. **Automatic lazy evaluation**: The message is only evaluated if the log level is enabled 2. **Zero overhead**: If the log level is disabled, there is no runtime cost for creating log messages ```scala // expensiveComputation() is only called if DEBUG is enabled debug(s"Result: ${expensiveComputation()}") ``` Unlike traditional logging frameworks, you don't need to wrap expensive computations with level checks. ## Source Location Log messages automatically include source location at the end of the message: ``` 2024-01-15 10:30:45.123+0900 info [MyService] Processing: data - (MyService.scala:14) ``` The source code location `(file:line)` is captured at compile time using Scala macros. ## Writing Logs to a File `FileLogHandler` writes log records to a file with automatic rotation. It runs unchanged on JVM, Scala.js (Node.js), and Scala Native, using the [FileSystem](/core/filesystem) abstraction underneath. ```scala import wvlet.uni.log.{FileLogHandler, FileLogHandlerConfig, Logger} val handler = FileLogHandler("app.log") Logger.setDefaultHandler(handler) ``` By default, `FileLogHandler` rotates daily and whenever the active file exceeds 100 MB, keeps the most recent 100 rotated files, and gzips each rotated file. Rotated files are named `{stem}-YYYY-MM-DD.{index}.log.gz` and sit next to the active log. ### Tuning rotation Configure the handler through `FileLogHandlerConfig`: ```scala import wvlet.uni.log.{FileLogHandler, FileLogHandlerConfig} val config = FileLogHandlerConfig("app.log") .withMaxSizeInBytes(10 * 1024 * 1024) // 10 MB per file .withMaxNumberOfFiles(30) // keep 30 rotated files .withCompressRotated(true) // gzip rotated files val handler = FileLogHandler(config) ``` `FileLogHandlerConfig` ships with both `withXxx` setters and these escape hatches: | Builder | Effect | |---------|--------| | `withPath(p)` | Change the active log file path. | | `withMaxSizeInBytes(n)` | Rotate once the active file exceeds `n` bytes. Default 100 MB. | | `withMaxNumberOfFiles(n)` | Keep at most `n` rotated files; oldest are deleted on rotation. Default 100. | | `withFormatter(f)` | Override the log formatter. Default `AppLogFormatter`. | | `withLogFileExt(ext)` | Override the active-log extension (default `".log"`). | | `withCompressRotated(b)` | Toggle gzip of rotated files. Default `true`. | | `noCompression` | Disable gzip; rotated files keep their configured extension (default `.log`). | | `noRotation` | Disable both size and count limits — write a single file forever. | ### Writing without rotation When you just want a plain file sink (for example, a short-lived CLI that appends to a debug log), use `noRotation`: ```scala val handler = FileLogHandler( FileLogHandlerConfig("debug.log").noRotation ) ``` ## Combining Multiple Handlers A `Logger` can fan log records out to several handlers at once — each with its own formatter and (optionally) its own level. Use this when you want, say, terminal-friendly output for humans and a rotated file for forensics on the same logger. ```scala import wvlet.uni.log.{ ConsoleLogHandler, FileLogHandler, FileLogHandlerConfig, LogFormatter, LogLevel, Logger } val console = ConsoleLogHandler(LogFormatter.AppLogFormatter) val file = FileLogHandler( FileLogHandlerConfig("app.log").withFormatter(LogFormatter.SourceCodeLogFormatter) ) val root = Logger.rootLogger root.clearHandlers root.addHandler(console) root.addHandler(file) ``` `addHandler` accepts any `java.util.logging.Handler`, so the same pattern wires up `BufferedLogHandler` (for tests), `NullHandler`, or any custom handler you write. ### Different formats per sink Each handler carries its own `LogFormatter`. The bundled ones are: | Formatter | Shape | Typical use | |-----------|-------|-------------| | `AppLogFormatter` | Color, timestamp, level, logger | Default for interactive consoles. | | `SourceCodeLogFormatter` | Adds the `(file:line)` source pin | Files / forensic logs where you'll grep later. | | `IntelliJLogFormatter` | Renders source as an IntelliJ-clickable link | Local dev runs inside IntelliJ. | | `PlainSourceCodeLogFormatter` | Like `SourceCodeLogFormatter` without ANSI color | Files / CI logs that mangle ANSI. | | `SimpleLogFormatter` | Level + message only | Smoke tests, prototypes. | | `BareFormatter` | Message only | When the surrounding tool already adds context. | | `TSVLogFormatter` | Tab-separated fields | Quick offline analysis with `cut` / `awk`. | | `ThreadLogFormatter` | Adds the thread name | Debugging concurrency. | ### Different levels per sink Handlers respect `java.util.logging.Handler#setLevel`, so you can let the logger itself be permissive and let each handler decide what to keep. `LogLevel.jlLevel` bridges to the underlying `java.util.logging` level: ```scala root.setLogLevel(LogLevel.DEBUG) // permissive at the source console.setLevel(LogLevel.INFO.jlLevel) // console stays quiet file.setLevel(LogLevel.DEBUG.jlLevel) // file captures everything ``` This is the right way to keep a noisy `DEBUG` trail on disk without flooding the terminal a human is reading. ## Best Practices 1. **Use appropriate levels** - Don't log everything as INFO 2. **Include context** - Log relevant data for debugging 3. **Avoid sensitive data** - Don't log passwords, tokens, etc. 4. **Use structured data** - Include key-value pairs for parsing 5. **Keep messages concise** - Clear, actionable messages ## Example: Service with Logging ```scala import wvlet.uni.log.LogSupport class OrderService(db: Database) extends LogSupport: def createOrder(userId: String, items: List[Item]): Order = info(s"Creating order for user: ${userId}, items: ${items.size}") val order = try val order = db.createOrder(userId, items) info(s"Order created: ${order.id}") order catch case e: DatabaseException => error(s"Failed to create order for user: ${userId}", e) throw e debug(s"Order details: ${order}") order ``` --- --- url: https://wvlet.org/uni/core/json.md --- # JSON Processing uni includes a pure Scala JSON parser with a convenient DSL for querying and manipulating JSON data. ## Parsing JSON ```scala import wvlet.uni.json.* val jsonString = """ { "user": { "id": 123, "name": "Alice", "email": "alice@example.com" }, "posts": [ {"id": 1, "title": "Hello World"}, {"id": 2, "title": "Scala Tips"} ] } """ val json = JSON.parse(jsonString) ``` ## Extracting Values ### Direct Access ```scala // Access nested values val userId = json("user")("id").toLongValue // 123 val userName = json("user")("name").toStringValue // "Alice" // Access array elements val firstPost = json("posts")(0)("title").toStringValue // "Hello World" ``` ### Path Navigation Use `/` for path-based navigation: ```scala val email = (json / "user" / "email").toStringValue // "alice@example.com" // Navigate into arrays val titles = (json / "posts" / "title").values // Seq("Hello World", "Scala Tips") ``` ### Safe Access with Option ```scala val email: Option[String] = for user <- json.get("user") email <- user.get("email") yield email.toStringValue ``` ## Value Types JSON values have specific types: ```scala json match case JSONObject(map) => // Object with fields case JSONArray(items) => // Array of values case JSONString(s) => // String value case JSONLong(n) => // Integer value case JSONDouble(d) => // Floating point case JSONBoolean(b) => // Boolean case _: JSONNull => // Null value ``` ## Creating JSON ### Programmatic Construction ```scala import wvlet.uni.json.* val userJson = JSONObject(Seq( "id" -> JSONLong(456), "name" -> JSONString("Bob"), "active" -> JSONBoolean(true), "scores" -> JSONArray(IndexedSeq( JSONLong(95), JSONLong(87), JSONLong(92) )) )) ``` ### Converting to String ```scala val jsonStr = userJson.toJSON // {"id":456,"name":"Bob","active":true,"scores":[95,87,92]} ``` ## Working with Arrays ```scala val posts = json("posts") // Get all items val allPosts = posts.values // Map over items val titles = posts.values.map(p => p("title").toStringValue) // Filter items val filteredPosts = posts.values.filter(p => p("id").toLongValue > 1 ) ``` ## Error Handling JSON parsing can fail: ```scala try val json = JSON.parse(invalidJson) catch case e: JSONParseException => println(s"Parse error: ${e.getMessage}") ``` Safe parsing with Try: ```scala import scala.util.Try val result = Try(JSON.parse(maybeInvalidJson)) result match case scala.util.Success(json) => // Use json case scala.util.Failure(e) => // Handle parse failure ``` ## JSONC Support uni's JSON parser supports [JSONC](https://jsonc.org/) (JSON with Comments) out of the box. Line comments (`//`), block comments (`/* */`), and trailing commas are all accepted by `JSON.parse`: ```scala val config = JSON.parse(""" { // Database configuration "host": "localhost", // server address "port": 5432, "options": { /* Connection pool settings */ "minConnections": 5, "maxConnections": 20, } } """) ``` ### Comment Round-Tripping Comments are preserved as mutable metadata on value nodes for round-tripping: ```scala val v = JSON.parse("""{ "key": "value" // important }""") // Comments are attached to nearby value nodes val keyVal = v("key") keyVal.trailingComment // Some(JSONComment("// important")) // toJSON outputs valid JSON (no comments) v.toJSON // {"key":"value"} // JSON.format pretty-prints with comments JSON.format(v) // { // "key": "value" // important // } ``` ### Comment Attachment Rules * **New-line comments** are attached as `leadingComments` on the **next** value node * **Inline comments** (same line) are attached as `trailingComment` on the **previous** value node ### JSONComment ```scala val comment = JSONComment("// server host") comment.commentBody // "server host" comment.isLineComment // true comment.isBlockComment // false ``` ## YAML Output Convert JSON to YAML format: ```scala import wvlet.uni.json.YAMLFormatter val yaml = YAMLFormatter.toYAML(json) // user: // id: 123 // name: Alice // email: alice@example.com ``` ## Best Practices 1. **Use path navigation** for nested access 2. **Handle missing keys** with `get` or `Option` 3. **Validate structure** before deep traversal 4. **Use pattern matching** for type-safe extraction --- --- url: https://wvlet.org/uni/core/msgpack.md --- # MessagePack Serialization MessagePack is a binary serialization format that's more compact and faster than JSON. ## Basic Usage ### Packing Data ```scala import wvlet.uni.msgpack.spi.MessagePack // Create a packer val packer = MessagePack.newPacker() // Pack primitive values packer.packString("Alice") packer.packInt(30) packer.packBoolean(true) // Get the bytes val bytes = packer.toByteArray ``` ### Unpacking Data ```scala // Create an unpacker val unpacker = MessagePack.newUnpacker(bytes) // Unpack in the same order val name = unpacker.unpackString() // "Alice" val age = unpacker.unpackInt() // 30 val active = unpacker.unpackBoolean() // true ``` ## Packing Complex Structures ### Arrays ```scala val packer = MessagePack.newPacker() // Pack array header with size packer.packArrayHeader(3) packer.packInt(1) packer.packInt(2) packer.packInt(3) ``` ### Maps ```scala val packer = MessagePack.newPacker() // Pack map header with size packer.packMapHeader(2) packer.packString("name") packer.packString("Alice") packer.packString("age") packer.packInt(30) ``` ## Unpacking Complex Structures ### Arrays ```scala val unpacker = MessagePack.newUnpacker(bytes) val size = unpacker.unpackArrayHeader() val items = (0 until size).map(_ => unpacker.unpackInt()) ``` ### Maps ```scala val unpacker = MessagePack.newUnpacker(bytes) val size = unpacker.unpackMapHeader() val map = (0 until size).map { _ => val key = unpacker.unpackString() val value = unpacker.unpackInt() key -> value }.toMap ``` ## Supported Types | Type | Pack Method | Unpack Method | |------|-------------|---------------| | Boolean | `packBoolean` | `unpackBoolean` | | Int | `packInt` | `unpackInt` | | Long | `packLong` | `unpackLong` | | Float | `packFloat` | `unpackFloat` | | Double | `packDouble` | `unpackDouble` | | String | `packString` | `unpackString` | | Binary | `packBinaryHeader` + write | `unpackBinaryHeader` + read | | Nil | `packNil` | `unpackNil` | ## Object Weaving For automatic serialization of case classes, use Weaver: ```scala import wvlet.uni.weaver.Weaver // Using derives clause case class User(name: String, age: Int) derives Weaver // Or explicit derivation case class Address(city: String, country: String) given Weaver[Address] = Weaver.of[Address] // Serialize to MessagePack bytes val userBytes = Weaver.weave(User("Alice", 30)) val addressBytes = Weaver.weave(Address("Tokyo", "Japan")) // Deserialize from bytes val user = Weaver.unweave[User](userBytes) val address = Weaver.unweave[Address](addressBytes) ``` ## MessagePack vs JSON | Aspect | MessagePack | JSON | |--------|-------------|------| | Format | Binary | Text | | Size | Smaller | Larger | | Speed | Faster | Slower | | Human-readable | No | Yes | | Schema | Optional | None | Use MessagePack when: * Performance is critical * Bandwidth is limited * Human readability isn't needed Use JSON when: * Debugging/logging * API responses * Configuration files ## Best Practices 1. **Match pack/unpack order** - Data must be unpacked in the same order it was packed 2. **Include length headers** - For arrays and maps 3. **Use Weaver** - For automatic case class serialization 4. **Handle format errors** - Wrap unpacking in try-catch --- --- url: https://wvlet.org/uni/core/weaver.md --- # Weaver `Weaver[A]` is uni's derivation-based serialization framework. A single `Weaver` instance can read and write a value of type `A` as **MessagePack**, **JSON**, or a loosely-typed `Map[String, Any]` — so you derive the codec once and pick the wire format at the call site. Weaver is the engine behind JSON/MessagePack serialization in the [HTTP client](../http/client) and [RPC](../http/rpc) layers, and it builds on [Surface](./surface) for compile-time type information. ```scala import wvlet.uni.weaver.Weaver case class Person(name: String, age: Int) derives Weaver val json = Weaver.toJson(Person("Alice", 30)) // {"name":"Alice","age":30} val back = Weaver.fromJson[Person](json) // Person(Alice,30) ``` ## Deriving a Weaver A `Weaver[A]` is derived at compile time for case classes, enums, and sealed traits. There are three equivalent ways to obtain one: ```scala // 1. derives clause (recommended for types you own) case class Person(name: String, age: Int) derives Weaver // 2. Weaver.of for an explicit given given Weaver[Person] = Weaver.of[Person] // 3. Inline at the call site val w: Weaver[Person] = Weaver.of[Person] ``` Most API methods take the `Weaver[A]` as a `using` parameter, so a `derives` clause or a `given` in scope is all you need. ## Serialization Formats The same derived `Weaver` exposes every format. The companion-object methods resolve the `Weaver[A]` implicitly: ```scala import wvlet.uni.weaver.Weaver case class User(name: String, age: Int) derives Weaver val user = User("Alice", 30) // JSON val json: String = Weaver.toJson(user) val u1: User = Weaver.fromJson[User](json) // MessagePack (compact binary) val bytes = Weaver.weave(user) // Array[Byte] val u2 = Weaver.unweave[User](bytes) // Map[String, Any] — handy with YAML/HOCON-style parsers val map: Map[String, Any] = Weaver.toMap(user) val u3 = Weaver.fromMap[User](map) ``` The instance methods on `Weaver[A]` mirror these and add a couple of lower-overhead entry points when you already hold a `Weaver`: | Method | Direction | Format | |--------|-----------|--------| | `toJson(v)` / `fromJson(s)` | encode / decode | JSON string | | `weave(v)` / `unweave(b)` | encode / decode | MessagePack `Array[Byte]` | | `toMap(v)` / `fromMap(m)` | encode / decode | `Map[String, Any]` | | `toMsgPack(v)` | encode | MessagePack `Array[Byte]` | | `fromJSONValue(v)` | decode | parsed [`JSONValue`](./json) (no string roundtrip) | | `pack(p, v, c)` / `unpack(u, c)` | encode / decode | low-level [MessagePack](./msgpack) packer/unpacker | ## Supported Types Weaver resolves codecs for these out of the box (cross-platform): * **Primitives**: `Int`, `Long`, `Short`, `Byte`, `Double`, `Float`, `Boolean`, `Char`, `String`, `BigInt`, `BigDecimal` * **Common value types**: `UUID`, `java.time.Instant`, [`ULID`](./utilities#ulid), [`ElapsedTime`](./utilities#elapsedtime), `scala.concurrent.duration.Duration`, `java.net.URI` * **Collections**: `Option`, `Seq`/`List`/`Vector`/`IndexedSeq`, `Set`, `Map`, `ListMap`, `Array`, `Either`, tuples * **Java collections**: `java.util.List`, `java.util.Set`, `java.util.Map` * **Case classes** with any combination of the above * **Enums and sealed traits** (see [ADTs](#enums-and-sealed-traits)) ### JVM-only types On the JVM, additional `given` weavers are available by importing them explicitly (they live in `JvmWeaver` because they depend on JDK types not available on Scala.js / Native): ```scala import wvlet.uni.weaver.codec.JvmWeaver.given // adds: ZonedDateTime, OffsetDateTime, LocalDate, LocalDateTime, // java.time.Duration, java.io.File, java.net.URL, // java.nio.file.Path, java.util.Optional[A] ``` ## Enums and Sealed Traits Scala 3 enums encode as their **case name** string: ```scala enum Color derives Weaver: case Red, Green, Blue Weaver.toJson(Color.Green) // "Green" Weaver.fromJson[Color](""""Blue"""") // Color.Blue ``` Sealed traits (ADTs) encode as a **flat object with a discriminator field** added alongside the case's own fields. The default discriminator is `@type`: ```scala sealed trait Shape derives Weaver case class Circle(radius: Double) extends Shape case class Rectangle(w: Double, h: Double) extends Shape case object Unknown extends Shape Weaver.toJson(Circle(1.0)) // {"@type":"Circle","radius":1.0} Weaver.toJson(Unknown) // {"@type":"Unknown"} ``` Change the discriminator via [`WeaverConfig`](#configuration): ```scala import wvlet.uni.weaver.WeaverConfig val cfg = WeaverConfig().withDiscriminatorFieldName("kind") Weaver.toJson(Circle(1.0), cfg) // {"kind":"Circle","radius":1.0} ``` ## Configuration `WeaverConfig` is an optional last argument on every method (it defaults to `WeaverConfig()`): | Field | Default | Effect | |-------|---------|--------| | `discriminatorFieldName` | `"@type"` | Field name used to tag sealed-trait cases | ```scala import wvlet.uni.weaver.WeaverConfig val config = WeaverConfig().withDiscriminatorFieldName("type") val json = Weaver.toJson(shape, config) ``` ## Runtime Weavers from Surface When you only have a [`Surface`](./surface) — for example deriving a codec for a method's parameter types in the RPC layer — build a `Weaver` at runtime instead of compile time: ```scala import wvlet.uni.weaver.Weaver import wvlet.uni.surface.Surface val surface = Surface.of[Person] val weaver = Weaver.fromSurface(surface) // Weaver[?] ``` `fromSurface` caches results, supports self-recursive types (e.g. `class Tree(children: List[Tree])`), and is what the RPC framework uses to wire up codecs without compile-time type information. For types that aren't directly supported, `fromSurface` falls back to a lossy empty-object weaver. Use `Weaver.fromSurfaceOpt(surface)`, which returns `None` when the resulting weaver tree contains that fallback at any position, if you want to detect the gap and choose a different encoding. See the [ADR](https://github.com/wvlet/uni/blob/main/adr/2026-05-04-fromsurfaceopt-and-innerweavers.md) for the rationale. ## Custom Weavers Provide a `given Weaver[A]` to control encoding for a type Weaver can't derive (or shouldn't derive losslessly — e.g. an open abstract type). Implement `pack` and `unpack` against the low-level [MessagePack](./msgpack) packer/unpacker: ```scala import wvlet.uni.weaver.{Weaver, WeaverConfig, WeaverContext} import wvlet.uni.msgpack.spi.{Packer, Unpacker} opaque type Email = String given Weaver[Email] with def pack(p: Packer, v: Email, config: WeaverConfig): Unit = p.packString(v) def unpack(u: Unpacker, context: WeaverContext): Unit = context.setObject(u.unpackString) ``` Once in scope, the custom weaver participates in derivation: any case class with an `Email` field will use it automatically. ## Cross-Platform Support Weaver works on JVM, Scala.js, and Scala Native. The core type set is identical on all three; only the `JvmWeaver` extras above are JVM-specific. --- --- url: https://wvlet.org/uni/core/surface.md --- # Type Introspection Surface provides compile-time type reflection for Scala 3, enabling introspection of types without runtime reflection. ## Basic Usage ```scala import wvlet.uni.surface.Surface case class User(id: Long, name: String, email: Option[String]) val surface = Surface.of[User] println(surface.name) // "User" println(surface.fullName) // "com.example.User" ``` ## Inspecting Parameters ```scala val surface = Surface.of[User] for param <- surface.params do println(s"${param.name}: ${param.surface.name}") // id: Long // name: String // email: Option[String] ``` ## Generic Types Surface handles generic types: ```scala val listSurface = Surface.of[List[String]] println(listSurface.name) // "List[String]" val mapSurface = Surface.of[Map[String, Int]] println(mapSurface.name) // "Map[String,Int]" ``` ## Type Aliases Surface resolves type aliases: ```scala type UserId = Long val surface = Surface.of[UserId] println(surface.name) // "Long" ``` ## Methods Inspect method signatures: ```scala trait UserService: def findUser(id: Long): Option[User] def createUser(name: String, email: String): User val methods = Surface.methodsOf[UserService] for m <- methods do println(s"${m.name}(${m.args.map(_.name).mkString(", ")}): ${m.returnType.name}") // findUser(id): Option[User] // createUser(name, email): User ``` ## Zero Values Surface can provide zero/default values: ```scala import wvlet.uni.surface.Zero Zero.of[Int] // 0 Zero.of[String] // "" Zero.of[Option[Int]] // None Zero.of[List[String]] // Nil Zero.of[Map[K, V]] // Map.empty ``` ## Use Cases ### Object Wiring Surface powers uni's Design system: ```scala // Design uses Surface to understand type structure val design = Design.newDesign .bindImpl[UserService, UserServiceImpl] ``` ### Serialization Weaver uses Surface for automatic serialization: ```scala // Surface describes the structure for serialization val json = Weaver.toJson(user) val restored = Weaver.fromJson[User](json) ``` ### Validation Build validators based on type structure: ```scala import scala.collection.mutable.ListBuffer def validate[T](value: T)(using surface: Surface): List[String] = val errors = ListBuffer[String]() for param <- surface.params do val fieldValue = param.get(value) if param.surface.name == "String" && fieldValue == "" then errors += s"${param.name} cannot be empty" errors.toList ``` ## Annotations Surface captures annotations, with their literal arguments, on parameters, methods, and classes with a public constructor: ```scala import scala.annotation.StaticAnnotation class label(val value: String) extends StaticAnnotation @label("account") case class User( @required id: Long, @maxLength(100) name: String ) val surface = Surface.of[User] for param <- surface.params do // Check if @required is present val required = param.hasAnnotation("required") // Class-level annotations, matched by simple or full name surface.findAnnotation("label").flatMap(_.getAs[String]("value")) // Some("account") ``` ## Performance Surface uses compile-time macros, so: * **No runtime reflection** - Fast at runtime * **Compile-time verification** - Errors caught during compilation * **Cross-platform** - Works on JVM, JS, Native ## Best Practices 1. **Cache Surface instances** when used repeatedly 2. **Use at compile time** when possible 3. **Prefer Surface.of\[T]** over runtime reflection 4. **Leverage Zero** for default values in serialization --- --- url: https://wvlet.org/uni/core/io.md --- # IO The `IO` object is uni's unified I/O facade, providing both file system operations and subprocess execution in a single entry point. ```scala import wvlet.uni.io.IO ``` ## File System Operations `IO` exposes all cross-platform file system operations from `FileSystem`: ```scala // Read and write files — paths can be plain strings val content = IO.readString("file.txt") IO.writeString("file.txt", "hello", WriteMode.Create) // Check existence and type IO.exists("file.txt") // true IO.isFile("file.txt") // true IO.isDirectory("some/dir") // false // Directory operations IO.list("some/dir") IO.createDirectory("some/nested/dir") // Copy, move, delete IO.copy("src.txt", "dst.txt") IO.move("old.txt", "new.txt") IO.delete("file.txt") IO.deleteRecursively("some/dir") // Temporary files val tmp = IO.createTempFile("prefix", ".txt", None) val tmpDir = IO.createTempDirectory("prefix", None) // Platform info IO.currentDirectory IO.homeDirectory IO.tempDirectory // Use IO.path(...) to build an IOPath for composition or to inspect parts val path = IO.path("some") / "nested" / "dir" / "file.txt" println(path.fileName) // "file.txt" ``` See [FileSystem](filesystem.md) for the full file system API reference. ## Subprocess Execution The `IO` object provides three methods for running external commands: | Method | Behavior | |--------|----------| | `IO.run` | Execute and capture output, return `CommandResult` | | `IO.call` | Like `run`, but throws on non-zero exit code | | `IO.spawn` | Start process without waiting, return `Process` handle | ### IO.run Execute a command and capture its output. Accepts a single command line string (automatically tokenized) or separate arguments: ```scala import wvlet.uni.io.IO // Single command line string — tokenized automatically val result = IO.run("ls -la /tmp") // Separate arguments val result = IO.run("ls", "-la", "/tmp") println(result.exitCode) // 0 println(result.stdout) // file listing println(result.stderr) // empty println(result.isSuccess) // true ``` Quoted arguments are handled correctly: ```scala IO.run("sh -c 'echo hello world'") IO.run("""grep -r "TODO" src/""") ``` Non-zero exit codes are returned without throwing: ```scala val result = IO.run("sh -c 'exit 42'") result.exitCode // 42 result.isSuccess // false ``` ### IO.call Like `run`, but throws `NonZeroExitCodeException` on failure: ```scala import wvlet.uni.io.{IO, NonZeroExitCodeException} val result = IO.call("echo", "hello") result.stdout.trim // "hello" // Throws NonZeroExitCodeException try IO.call("sh", "-c", "exit 1") catch case e: NonZeroExitCodeException => println(s"Exit code: ${e.exitCode}") ``` ### IO.spawn Start a process without waiting for completion. Returns a `Process` handle with access to stdin/stdout/stderr streams: ```scala import wvlet.uni.io.IO val proc = IO.spawn("cat") proc.stdin.write("hello".getBytes("UTF-8")) proc.stdin.close() val output = String(proc.stdout.readAllBytes(), "UTF-8") proc.waitFor() // 0 ``` Timed waiting to avoid hanging: ```scala import java.util.concurrent.TimeUnit val proc = IO.spawn("long-running-command") if proc.waitFor(5, TimeUnit.SECONDS) then println(s"Exited with: ${proc.exitValue()}") else proc.destroyForcibly() ``` ## ProcessConfig All three methods accept a `ProcessConfig` for advanced options: ```scala import wvlet.uni.io.{IO, ProcessConfig} val config = ProcessConfig.default .withWorkingDirectory(IO.path("/tmp")) .withEnv("MY_VAR", "value") .withRedirectErrorToOutput(true) val result = IO.run(Seq("my-command", "--flag"), config) ``` | Option | Description | |--------|-------------| | `withWorkingDirectory(dir)` | Set the working directory | | `withEnv(key, value)` | Add an environment variable | | `withEnv(vars)` | Add multiple environment variables | | `withInheritIO(true)` | Inherit parent's stdin/stdout/stderr | | `withRedirectErrorToOutput(true)` | Merge stderr into stdout | ## Platform Support | Feature | JVM | Scala Native | Scala.js | |---------|-----|--------------|----------| | File operations | Supported | Supported | Node.js only (async recommended) | | Subprocess execution | Supported | Supported | Not supported | --- --- url: https://wvlet.org/uni/core/filesystem.md --- # FileSystem Cross-platform file I/O abstraction for JVM, Scala.js (Node.js and browser), and Scala Native. Uni's `IO` object is the single entry point for file operations — it exposes path construction, reads, writes, directory listings, metadata, and async variants uniformly across platforms. ```scala import wvlet.uni.io.IO // Read and write files — pass paths as plain strings val content = IO.readString("config.json") IO.writeString("output.txt", "hello") // Use IO.path(...) to build an IOPath for composition or to inspect parts val path = IO.path("/home/user") / "documents" / "file.txt" println(path.fileName) // "file.txt" println(path.extension) // "txt" ``` ## Paths For most `IO` operations — read/write, list, copy, move, delete, exists, watch, and the gzip/zip helpers — you can pass paths as plain `String`s and skip the `IO.path(...)` wrapper. `IOPath` is uni's cross-platform path abstraction — reach for it (via `IO.path(...)`) when you need composition (`/`), path queries (`fileName`, `parent`, `extension`, ...), or path arithmetic (`normalize`, `relativeTo`). A few APIs that take an `Option[IOPath]` (such as the `directory` parameter of [`createTempFile`](#temporary-files)) still expect an `IOPath`. ### Creating Paths ```scala import wvlet.uni.io.IO // From string val path = IO.path("/home/user/file.txt") // Join segments val joined = IO.path("home", "user", "file.txt") // Using / operator val composed = IO.path("/home") / "user" / "file.txt" // Well-known directories val cwd = IO.currentDirectory val home = IO.homeDirectory val tmp = IO.tempDirectory ``` ### Path Properties ```scala val path = IO.path("/home/user/project/README.md") path.fileName // "README.md" path.baseName // "README" path.extension // "md" path.isAbsolute // true path.segments // Seq("home", "user", "project", "README.md") path.parent // Some(IO.path("/home/user/project")) ``` ### Path Operations ```scala // Normalize . and .. IO.path("/home/user/../admin/./config").normalize // IO.path("/home/admin/config") // Relative path val base = IO.path("/home/user") val file = IO.path("/home/user/project/src/Main.scala") file.relativeTo(base) // IO.path("project/src/Main.scala") // POSIX format (always forward slashes) path.posixPath // "/home/user/project/README.md" // Checks path.startsWith(IO.path("/home")) // true path.endsWith(IO.path("README.md")) // true ``` ## Reading and Writing Files ### Reading Files ```scala import wvlet.uni.io.IO // Read as string (UTF-8) val text: String = IO.readString("data.txt") // Read as bytes val bytes: Array[Byte] = IO.readBytes("data.txt") // Read line by line val lines: Seq[String] = IO.readLines("data.txt") ``` ### Writing Files ```scala import wvlet.uni.io.{IO, WriteMode} // Create or overwrite (default) IO.writeString("output.txt", "hello world") // Create new file, fail if exists IO.writeString("output.txt", "content", WriteMode.CreateNew) // Append to file IO.writeString("output.txt", "more content", WriteMode.Append) // Write bytes IO.writeBytes("output.bin", Array[Byte](1, 2, 3)) // Append shorthand IO.appendString("output.txt", "\nnew line") ``` ### WriteMode | Mode | Description | |------|-------------| | `CreateNew` | Create a new file, fail if it already exists | | `Create` | Create or truncate the file (default) | | `Append` | Append to the file, create if it doesn't exist | ## Directory Operations ### Listing Files ```scala import wvlet.uni.io.{IO, ListOptions} // List immediate children val files = IO.list("src") // Recursive listing val allFiles = IO.list("src", ListOptions().withRecursive(true)) // Filter by extension val scalaFiles = IO.list("src", ListOptions() .withRecursive(true) .withExtensions("scala") ) // Filter by glob pattern val testFiles = IO.list("src", ListOptions() .withRecursive(true) .withGlob("**/*Test.scala") ) // Limit depth val shallow = IO.list("src", ListOptions() .withRecursive(true) .withMaxDepth(2) ) // Include hidden files val withHidden = IO.list("src", ListOptions().withIncludeHidden(true)) ``` ### Creating, Deleting, Copying, Moving ```scala import wvlet.uni.io.{IO, CopyOptions} // Create directory (including parents) IO.createDirectory("a/b/c") // Create only if it doesn't exist IO.createDirectoryIfNotExists("output") // Delete file or empty directory IO.delete("temp.txt") // Delete recursively IO.deleteRecursively("build") // Delete if exists (no error if missing) IO.deleteIfExists("maybe.txt") // Copy IO.copy("src.txt", "dst.txt") IO.copy("srcDir", "dstDir", CopyOptions().withOverwrite(true).withPreserveAttributes(true) ) // Move / rename IO.move("old.txt", "new.txt") IO.move("old.txt", "new.txt", overwrite = true) ``` ### Temporary Files ```scala // Temporary file val tmpFile = IO.createTempFile(prefix = "data", suffix = ".json") // Temporary directory val tmpDir = IO.createTempDirectory(prefix = "work") // In a specific directory val custom = IO.createTempFile( prefix = "report", suffix = ".csv", directory = Some(IO.path("output")) ) ``` ## File Metadata ```scala import wvlet.uni.io.{IO, FileType} val info = IO.info("README.md") info.fileType // FileType.File info.size // file size in bytes info.lastModified // Option[Instant] info.createdAt // Option[Instant] info.isFile // true info.isDirectory // false info.isReadable // true info.isWritable // true info.isExecutable // false info.isHidden // false // Existence checks IO.exists("README.md") // true IO.isFile("README.md") // true IO.isDirectory("src") // true ``` ### FileType | Type | Description | |------|-------------| | `File` | Regular file | | `Directory` | Directory | | `SymbolicLink` | Symbolic link | | `Other` | Other file type | | `NotFound` | Path does not exist | ## Async Operations All async operations return `Future[...]` and work on every platform, including browsers. ```scala import wvlet.uni.io.{IO, IOPath, FileInfo} import scala.concurrent.Future // Async read/write val content: Future[String] = IO.readStringAsync("data.txt") val bytes: Future[Array[Byte]] = IO.readBytesAsync("image.png") val written: Future[Unit] = IO.writeStringAsync("output.txt", "hello") // Async directory listing val files: Future[Seq[IOPath]] = IO.listAsync("src") // Async metadata val info: Future[FileInfo] = IO.infoAsync("file.txt") val exists: Future[Boolean] = IO.existsAsync("file.txt") ``` `IOPath` and `FileInfo` are the underlying types — import them when you need explicit type annotations. ## Gzip Compression Cross-platform gzip utilities for compressing and decompressing data. ```scala import wvlet.uni.io.{IO, Gzip} // In-memory compression val data = "hello world".getBytes("UTF-8") val compressed = Gzip.compress(data) val decompressed = Gzip.decompress(compressed) // File-based compression (streaming) Gzip.compressFile("large.txt", "large.txt.gz") Gzip.decompressFile("large.txt.gz", "large.txt") ``` ## Cross-Platform Support | Operation | JVM | JS (Node) | JS (Browser) | Native | |-----------|-----|-----------|-------------|--------| | Sync read/write | Yes | Yes | No | Yes | | Async read/write | Yes | Yes | Yes | Yes | | Directory operations | Yes | Yes | No | Yes | | File metadata | Yes | Yes | No | Yes | | Temporary files | Yes | Yes | No | Yes | | Gzip | Yes | Yes | No | Yes | Sync operations throw `UnsupportedOperationException` in browser environments. Use async operations for cross-platform code that needs to run in browsers. ## Best Practices 1. **Use `IO` as the single entry point** — `IO.readString(...)`, `IO.list(...)`, etc. Pass paths as plain strings, and use `IO.path(...)` only when you need an `IOPath` value. 2. **Prefer async operations** for code that must run on all platforms 3. **Use `ListOptions`** with glob or extension filters instead of filtering results manually 4. **Use `WriteMode.CreateNew`** when you want to avoid overwriting existing files 5. **Use `deleteIfExists`** to avoid error handling for missing files --- --- url: https://wvlet.org/uni/core/result.md --- # Result `Result[A]` is Uni's standard container for "a value that may have failed". It is a Rust-flavored counterpart to `scala.util.Try`: the error channel is always a `Throwable`, and the combinators match [`Rx`](/rx/)'s vocabulary so interop is mechanical. ```scala import wvlet.uni.util.Result ``` `Result` lives in `uni-core` and is available on the JVM, Scala.js, and Scala Native. ## When to Use Result Use `Result[A]` at API boundaries where failure is an expected outcome that callers need to handle as a value — parsers, external lookups, settled `Rx` evaluations — while keeping `throw` / `try` / `catch` inside internal code. Uni deliberately does **not** introduce a type-level error channel (like ZIO's `E` parameter): Scala's exception mechanism already carries the error type, and fixing `E = Throwable` keeps the shape single-parameter like `Option[A]` and `Rx[A]`. | Situation | Recommended type | |--------------------------------------------------------|-------------------------| | Internal helper that can throw | Exceptions + `NonFatal` | | Public API that distinguishes "failed" from "returned" | `Result[A]` | | Async stream that produces zero or more values | `Rx[A]` | ## Constructing a Result `Result.apply` evaluates a block and wraps any non-fatal exception. Fatal errors (e.g. `InterruptedException`, `VirtualMachineError`) escape — this matches `scala.util.control.NonFatal`. ```scala val parsed: Result[Int] = Result(Integer.parseInt("42")) // Result.Success(42) val failed: Result[Int] = Result(Integer.parseInt("not-a-number")) // Result.Failure(java.lang.NumberFormatException: For input string: "not-a-number") ``` Direct constructors and conversions are also available: ```scala Result.success(1) // Result.Success(1) Result.failure(new RuntimeException("no")) // Result.Failure(...) Result.fromTry(scala.util.Try("parse this".toInt)) Result.fromEither(Right[Throwable, Int](42)) Result.fromOption(Option.empty[Int], new NoSuchElementException("empty")) ``` ## Composing Results `map`, `flatMap`, and `filter` work like any other monadic container, and they **catch any exceptions thrown inside the supplied function**, turning them into `Failure`. This is the "Rx-like propagation" property: exceptions never bypass the wrapper. ```scala val r: Result[Int] = for a <- Result(Integer.parseInt("10")) b <- Result(Integer.parseInt("20")) if a + b > 0 yield a + b // Result.Success(30) ``` A `Failure` anywhere in the chain short-circuits the rest — no need to thread the error manually. ## Handling Failure `recover` and `recoverWith` mirror the same-named methods on `Rx` and only fire when the partial function matches the wrapped throwable: ```scala val recovered: Result[Int] = Result(Integer.parseInt("nope")).recover { case _: NumberFormatException => 0 } // Result.Success(0) val retried: Result[Int] = Result.failure(new java.io.IOException("timeout")).recoverWith { case _: java.io.IOException => Result(42) } // Result.Success(42) ``` Other handlers: ```scala Result.failure(boom).mapError(new IllegalStateException(_)) // rewrap result.fold(onFailure = _.getMessage, onSuccess = _.toString) result.getOrElse(0) result.orElse(Result.success(0)) ``` ## Conversions `Result` interoperates with the standard Scala types: ```scala result.toOption // Option[A] — Failure becomes None result.toEither // Either[Throwable, A] result.toTry // scala.util.Try[A] result.get // A; rethrows on Failure ``` ## Rx Interop `Rx[A].materialize` reifies each upstream event as a `Result[A]`, so errors are delivered as values instead of terminating the stream: ```scala import wvlet.uni.rx.Rx val events: Rx[Result[Int]] = Rx.exception[Int](new RuntimeException("boom")).materialize // Emits Result.Failure(RuntimeException("boom")) ``` To go the other way, use `Rx.fromResult` (or the `toRx` extension on `Result`, available after importing `wvlet.uni.rx.*`): ```scala import wvlet.uni.rx.* val rx1: Rx[Int] = Rx.fromResult(Result.success(1)) val rx2: Rx[Int] = Result.success(1).toRx ``` `Result.Failure(e)` converts to an `Rx` that emits `OnError(e)` — the same termination signal a thrown exception would produce. --- --- url: https://wvlet.org/uni/core/utilities.md --- # Utilities Human-readable value types and ID generators for common programming tasks. ## UUIDv7 Time-ordered UUIDs (RFC 9562) — the recommended ID format for internal use. UUIDv7 embeds a millisecond-precision Unix timestamp, is monotonically sortable, and can be compactly encoded with Base62. ```scala import wvlet.uni.util.UUIDv7 // Generate a new UUIDv7 val id = UUIDv7.newUUIDv7() println(id) // e.g., "019078e5-3f6a-7b1c-8d4e-2f9a0b1c3d5e" // Compact 22-character Base62 representation (URL-safe, sort-order preserving) val short: String = id.toBase62 // e.g., "0K9mFb3xYz1Qw5Rv8NpJ2a" // Parse back from Base62 val recovered = UUIDv7.fromBase62(short) ``` ### Timestamps and Conversion ```scala val id = UUIDv7.newUUIDv7() // Extract timestamp val millis: Long = id.timestamp // Convert to/from java.util.UUID val uuid: java.util.UUID = id.toUUID val back: UUIDv7 = UUIDv7.fromUUID(uuid) // Binary representation val bytes: Array[Byte] = UUIDv7.toBytes(id) // 16 bytes val fromBytes: UUIDv7 = UUIDv7.fromBytes(bytes) // Parse from standard UUID string val parsed = UUIDv7.fromString("019078e5-3f6a-7b1c-8d4e-2f9a0b1c3d5e") ``` ### Custom Generator ```scala // Create an independent generator (useful for testing or isolated contexts) val gen = UUIDv7.createGenerator() val id = gen.newUUIDv7() // With a custom random source val gen2 = UUIDv7.createGenerator(new scala.util.Random(42)) ``` ## Base62 Sort-order preserving encoding using the `0-9A-Za-z` alphabet (62 characters). Encodes 128-bit values to compact 22-character URL-safe strings. Use `UUIDv7.toBase62` / `UUIDv7.fromBase62` for the common case. ```scala import wvlet.uni.util.{Base62, UUIDv7} // Most common usage: via UUIDv7 val id = UUIDv7.newUUIDv7() val encoded: String = id.toBase62 // 22 chars val decoded: UUIDv7 = UUIDv7.fromBase62(encoded) // Encode / decode raw 128-bit values val raw: String = Base62.encode128bits(0x0123456789abcdefL, 0xfedcba9876543210L) val (hi, low): (Long, Long) = Base62.decode128bits(raw) // Validate Base62.isValid("0K9mFb3xYz1Qw5Rv8NpJ2a") // true ``` ## CrockfordBase32 [Crockford's Base32](https://www.crockford.com/base32.html) encoding — the case-insensitive, human-friendly alphabet (it excludes `I`, `L`, `O`, `U` to avoid ambiguity) that backs [ULID](#ulid). Use it directly when you need to encode raw 128-bit or 48-bit values in the same format. ```scala import wvlet.uni.util.CrockfordBase32 // Encode / decode a 128-bit value (26 characters) val s: String = CrockfordBase32.encode128bits(hi = 0x0123456789abcdefL, low = 0xfedcba9876543210L) val (hi, low): (Long, Long) = CrockfordBase32.decode128bits(s) // Decode a 48-bit value (e.g. a ULID timestamp prefix) val millis: Long = CrockfordBase32.decode48bits("01ARZ3NDEK") // Validate CrockfordBase32.isValidBase32("01arz3ndektsv4rrffq69g5fav") // true ``` For ID generation prefer [ULID](#ulid) (which encodes with this alphabet) or [Base62](#base62); reach for `CrockfordBase32` only when you need the raw codec. ## NanoId Compact, URL-safe random IDs for cases where time-ordering is not needed. Useful as user-facing tokens, short links, or external keys that map back to an internal UUIDv7. ```scala import wvlet.uni.util.NanoId // Generate a default NanoId (21 chars, A-Za-z0-9_-) val id: String = NanoId.generate() // e.g., "V1StGXR8_Z5jdHi6B-myT" // Custom size val short = NanoId.generate(10) // Custom alphabet and size val hex = NanoId.generate("0123456789abcdef", 32) ``` ::: tip ID Strategy Use **UUIDv7** as the internal primary key — it is time-ordered, monotonic, and embeds a timestamp. For user-facing URLs and APIs, either: * Expose the UUIDv7 in **Base62** format (22 chars, sortable, reversible), or * Generate a **NanoId** as an external key and maintain a mapping to the internal UUIDv7. ::: ## ULID Universally Unique Lexicographically Sortable Identifiers — 128-bit IDs encoded as 26-character Crockford Base32 strings. ULID predates UUIDv7 and serves a similar purpose; **prefer UUIDv7 for new projects**. ```scala import wvlet.uni.util.ULID // Generate a new ULID val id = ULID.newULID println(id) // e.g., "01arz3ndektsv4rrffq69g5fav" ``` ### Parsing and Validation ```scala val parsed = ULID.fromString("01arz3ndektsv4rrffq69g5fav") ULID.isValid("01arz3ndektsv4rrffq69g5fav") // true ``` ### Extracting Timestamps ```scala val id = ULID.newULID val millis: Long = id.epochMillis val instant: java.time.Instant = id.toInstant val historical = ULID.ofMillis(1609459200000L) ``` ### Converting to/from UUID ```scala import java.util.UUID val ulid = ULID.newULID val uuid: UUID = ulid.toUUID val back: ULID = ULID.fromUUID(uuid) val bytes: Array[Byte] = ulid.toBytes // 16 bytes val fromBytes: ULID = ULID.fromBytes(bytes) ``` ## PrefixedULID A [ULID](#ulid) tagged with a human-readable type prefix — handy for self-describing IDs like `user:01arz3ndektsv4rrffq69g5fav` where the prefix identifies the entity. The string form is `:` (delimiter `:`), and values sort by prefix first, then by the embedded ULID timestamp. ```scala import wvlet.uni.util.PrefixedULID // Generate val id: PrefixedULID = PrefixedULID.newPrefixedULID("user") println(id) // "user:01arz3ndektsv4rrffq69g5fav" // Or directly as a string val s: String = PrefixedULID.newPrefixedULIDString("order") // Access parts id.prefix // "user" id.ulid // the underlying ULID // Parse back (splits on the last ':' so prefixes may contain ':') val parsed = PrefixedULID.fromString("user:01arz3ndektsv4rrffq69g5fav") ``` ## DataSize Human-readable data size representation with parsing and unit conversion. ```scala import wvlet.uni.util.DataSize // Parse from string val size = DataSize("128MB") // Create from bytes val fromBytes = DataSize(1073741824L) // 1GB // Auto-select best unit val succinct = DataSize.succinct(1536000L) println(succinct) // "1.46MB" ``` ### Unit Conversion ```scala val size = DataSize("2.5GB") size.toBytes // 2684354560L size.valueOf(DataSizeUnit.MEGABYTE) // 2560.0 size.convertTo(DataSizeUnit.MEGABYTE) // DataSize(2560.0, MEGABYTE) size.mostSuccinctDataSize // DataSize(2.5, GIGABYTE) ``` ### Units | Unit | Symbol | Bytes | |------|--------|-------| | `BYTE` | B | 1 | | `KILOBYTE` | kB | 1,024 | | `MEGABYTE` | MB | 1,048,576 | | `GIGABYTE` | GB | 1,073,741,824 | | `TERABYTE` | TB | 1,099,511,627,776 | | `PETABYTE` | PB | 1,125,899,906,842,624 | ## Count Human-readable large number representation. ```scala import wvlet.uni.util.Count // Parse from string val count = Count("1.5M") // Auto-select best unit val succinct = Count.succinct(1500000L) println(succinct) // "1.50M" // Raw value count.toLong // 1500000L ``` ### Unit Conversion ```scala val count = Count(2500000L) count.mostSuccinctCount // Count(2.50, MILLION) count.convertTo(CountUnit.THOUSAND) // Count(2500, THOUSAND) count.valueOf(CountUnit.MILLION) // 2.5 ``` ### Units | Unit | Symbol | Value | |------|--------|-------| | `ONE` | (none) | 1 | | `THOUSAND` | K | 1,000 | | `MILLION` | M | 1,000,000 | | `BILLION` | B | 1,000,000,000 | | `TRILLION` | T | 1,000,000,000,000 | | `QUADRILLION` | Q | 1,000,000,000,000,000 | ## ElapsedTime Human-readable duration representation with unit conversion. ```scala import wvlet.uni.util.ElapsedTime // Parse from string val duration = ElapsedTime("5ms") val timeout = ElapsedTime("30s") val window = ElapsedTime("2.5h") // Create from measurements val fromNanos = ElapsedTime.succinctNanos(1500000L) println(fromNanos) // "1.50ms" val fromMillis = ElapsedTime.succinctMillis(3600000L) println(fromMillis) // "1.00h" // Measure elapsed time val start = System.nanoTime() // ... do work ... val elapsed = ElapsedTime.nanosSince(start) println(elapsed) // e.g., "42.30ms" ``` ### Unit Conversion ```scala val duration = ElapsedTime("90s") duration.toMillis // 90000.0 duration.valueIn(java.util.concurrent.TimeUnit.MINUTES) // 1.5 duration.convertToMostSuccinctTimeUnit // ElapsedTime(1.50, MINUTES) ``` ### Units | Unit | Symbol | Example | |------|--------|---------| | `NANOSECONDS` | ns | `"500ns"` | | `MICROSECONDS` | us | `"100us"` | | `MILLISECONDS` | ms | `"5ms"` | | `SECONDS` | s | `"30s"` | | `MINUTES` | m | `"5m"` | | `HOURS` | h | `"2.5h"` | | `DAYS` | d | `"1d"` | ## StopWatch A simple elapsed-time timer. It starts running on creation and reports seconds as a `Double`. ```scala import wvlet.uni.util.StopWatch val sw = StopWatch() // ... do work ... val seconds: Double = sw.getElapsedTime // elapsed since creation/reset sw.stop // pause; returns the last interval sw.resume // continue measuring sw.reset // restart from zero println(sw.reportElapsedTime) // e.g. "0.42 sec." ``` ## Timer For micro-benchmarking, extend the `Timer` trait and wrap code in `time` / nested `block`. A `TimeReport` captures total and per-block timing, and `repeat` runs blocks multiple times to smooth out JIT/GC noise. ```scala import wvlet.uni.util.Timer class Benchmark extends Timer: val report = time("search", repeat = 10) { block("linear") { // candidate A } block("binary") { // candidate B } } // Inspect per-block statistics report("linear").average // average seconds over the repeats report("binary").min // min / max / median / elapsedSeconds also available val b = Benchmark() println(b.report) // formatted multi-line timing report ``` ## Best Practices 1. **Use UUIDv7** for internal IDs — time-ordered, monotonic, with an embedded timestamp 2. **Use Base62** encoding for user-facing UUIDv7 — compact (22 chars), URL-safe, sort-order preserving 3. **Use NanoId** for opaque external tokens — map them back to internal UUIDv7 when needed 4. **Use succinct formatting** in logs and UIs for readability (`DataSize.succinct`, `Count.succinct`) 5. **Parse user input** with the string constructors — they handle common formats (`"128MB"`, `"1.5M"`, `"30s"`) 6. All value types implement `Comparable` for sorting and comparison --- --- url: https://wvlet.org/uni/control.md --- # Control Flow uni provides utilities for handling failures gracefully and managing resources properly. ## Overview | Component | Description | |-----------|-------------| | [Retry](./retry) | Automatic retry with backoff strategies | | [Circuit Breaker](./circuit-breaker) | Prevent cascade failures | | [Rate Limiter](./rate-limiter) | Throttle operation throughput (JVM) | | [Cache](./cache) | TTL and LRU caching | | [Resource](./resource) | Safe resource acquisition and release | | [Background Task](./background-task) | Cancellable, progress-pollable background worker | ## Quick Example ```scala import wvlet.uni.control.{Retry, CircuitBreaker, Resource} // Retry a flaky operation val result = Retry.withBackoff(maxRetry = 3).run { callExternalService() } // Protect with circuit breaker val breaker = CircuitBreaker(maxFailures = 5) val protected = breaker.protect { riskyOperation() } // Safe resource handling Resource.withResource(openConnection()) { conn => useConnection(conn) } // Automatically closed ``` ## When to Use ### Retry * Network calls that may fail transiently * Database operations during high load * API calls with rate limiting ### Circuit Breaker * External service dependencies * Preventing cascade failures * Graceful degradation ### Caching * Expensive computations * Frequently accessed data * Rate-limited API responses ### Resource Management * File handles * Database connections * Network sockets ## Package ```scala import wvlet.uni.control.* ``` --- --- url: https://wvlet.org/uni/control/retry.md --- # Retry Logic Handle transient failures by automatically retrying operations. ## Basic Retry ```scala import wvlet.uni.control.Retry // Retry up to 3 times with exponential backoff val result = Retry.withBackOff(maxRetry = 3).run { callExternalService() } ``` ## Factory Methods ### Exponential Backoff ```scala // Default exponential backoff val result = Retry.withBackOff(maxRetry = 5).run { operation() } // With custom initial interval val result = Retry .withBackOff(initialIntervalMillis = 100) .withMaxRetry(5) .run { operation() } ``` ### Jitter Add randomness to prevent thundering herd: ```scala val result = Retry.withJitter(maxRetry = 3).run { operation() } ``` ### Bounded Backoff Limit total wait time: ```scala val retry = Retry.withBoundedBackoff( initialIntervalMillis = 1000, maxTotalWaitMillis = 50000 ) val result = retry.run { operation() } ``` ## Retry on Specific Conditions Use `retryOn` with a partial function returning `ResultClass`: ```scala import wvlet.uni.control.{Retry, ResultClass} val result = Retry .withBackOff(maxRetry = 3) .retryOn { case e: IllegalStateException => warn(e.getMessage) ResultClass.retryableFailure(e) } .run { operation() } ``` ## Error Classification Use `withErrorClassifier` for custom error handling: ```scala import wvlet.uni.control.{Retry, ResultClass} import scala.concurrent.TimeoutException val result = Retry .withBackOff(initialIntervalMillis = 10) .withMaxRetry(3) .withErrorClassifier { case e: TimeoutException => ResultClass.retryableFailure(e).withExtraWaitMillis(100) } .run { operation() } ``` ### Extra Wait Options ```scala // Add fixed extra wait time ResultClass.retryableFailure(e).withExtraWaitMillis(100) // Add proportional extra wait time (20% of base interval) ResultClass.retryableFailure(e).withExtraWaitFactor(0.2) ``` ## Retry Configuration ```scala val retry = Retry .withBackOff(initialIntervalMillis = 100) .withMaxRetry(10) .noRetryLogging // Disable retry logging val result = retry.run { operation() } ``` ### Switching Strategies ```scala // Start with jitter, switch to backoff val r = Retry.withJitter().withBackOff(initialIntervalMillis = 3) // Switch back to jitter val j = r.withJitter(initialIntervalMillis = 20) // Change max retry val m = j.withMaxRetry(100) ``` ## Before Retry Hook Execute code before each retry: ```scala import wvlet.uni.control.Retry.RetryContext val result = Retry .withBackOff(initialIntervalMillis = 0) .beforeRetry { (ctx: RetryContext) => logger.info(s"Retry ${ctx.retryCount}, next wait: ${ctx.nextWaitMillis}ms") } .run { operation() } ``` ## Run with Context Pass context through retry attempts: ```scala import wvlet.uni.control.Retry.RetryContext val result = Retry .withBackOff(initialIntervalMillis = 0) .beforeRetry { (ctx: RetryContext) => // Access the context ctx.context shouldBe Some("hello world") } .runWithContext("hello world") { operation() } ``` ## Handling Exhausted Retries ```scala import wvlet.uni.control.Retry.MaxRetryException try Retry.withBackOff(maxRetry = 3).run { failingOperation() } catch case e: MaxRetryException => logger.error(s"Failed after ${e.retryContext.retryCount} retries") logger.error(s"Last error: ${e.retryContext.lastError}") fallbackBehavior() ``` ## Best Practices 1. **Set reasonable limits** - Don't retry forever 2. **Use exponential backoff** - Give services time to recover 3. **Add jitter** - Prevent synchronized retries with `withJitter` 4. **Classify errors** - Only retry transient failures using `retryOn` 5. **Log retries** - Use `beforeRetry` for debugging 6. **Set timeouts** - Combine with request timeouts ## Common Patterns ### Retry with Fallback ```scala import wvlet.uni.control.Retry.MaxRetryException val result = try Retry.withBackOff(maxRetry = 3).run { primaryService.call() } catch case _: MaxRetryException => fallbackService.call() ``` ### Retry with Metrics ```scala import wvlet.uni.control.Retry.RetryContext var attempts = 0 val result = Retry .withBackOff(maxRetry = 5) .beforeRetry { (ctx: RetryContext) => metrics.recordRetry(ctx.retryCount) } .run { attempts += 1 operation() } metrics.recordTotalAttempts(attempts) ``` --- --- url: https://wvlet.org/uni/control/circuit-breaker.md --- # Circuit Breaker Prevent cascade failures by stopping requests to failing services. ## How It Works The circuit breaker has three states: 1. **Closed** - Requests flow normally 2. **Open** - Requests are rejected immediately 3. **Half-Open** - Limited requests allowed to test recovery ``` ┌─────────┐ │ Closed │ ◄── Normal operation └────┬────┘ │ Failures exceed threshold ▼ ┌─────────┐ │ Open │ ◄── Rejecting requests └────┬────┘ │ Reset timeout expires ▼ ┌──────────┐ │Half-Open │ ◄── Testing recovery └────┬─────┘ │ Success│ Failure │ │ ▼ ▼ Closed Open ``` ## Factory Methods ### Default Circuit Breaker ```scala import wvlet.uni.control.CircuitBreaker val cb = CircuitBreaker.default val result = cb.run { externalServiceCall() } ``` ### Failure Threshold Open circuit when failures exceed threshold within a window: ```scala // Open when 2 out of 5 recent calls fail val cb = CircuitBreaker.withFailureThreshold(2, 5) val result = cb.run { externalServiceCall() } ``` ### Consecutive Failures Open circuit after consecutive failures: ```scala // Open after 2 consecutive failures val cb = CircuitBreaker.withConsecutiveFailures(2) val result = cb.run { externalServiceCall() } ``` ### Always Closed (Disabled) Disable circuit breaker for testing: ```scala val cb = CircuitBreaker.alwaysClosed // Never opens, even with many failures for i <- 0 to 10 do cb.recordFailure(new Exception()) cb.isConnected shouldBe true ``` ## Running Code with Circuit Breaker Use `run` to execute code through the circuit breaker: ```scala val cb = CircuitBreaker.withFailureThreshold(1, 2) // Successful execution cb.run { operation() } // If the operation throws, it's recorded as a failure try cb.run[Any] { throw new TimeoutException() } catch case e: TimeoutException => // Handle exception ``` ## Handling Open Circuit ```scala import wvlet.uni.control.{CircuitBreaker, CircuitBreakerOpenException} val cb = CircuitBreaker.withFailureThreshold(1, 2) try cb.run { externalService.call() } catch case e: CircuitBreakerOpenException => // Circuit is open, use fallback fallbackResponse() ``` ## Circuit State Check and control the current state: ```scala val cb = CircuitBreaker.default // Check state cb.state match case CircuitBreaker.CLOSED => println("Circuit is healthy") case CircuitBreaker.OPEN => println("Circuit is open, requests blocked") case CircuitBreaker.HALF_OPEN => println("Circuit is testing recovery") // Check if connected if cb.isConnected then println("Circuit breaker allows connections") ``` ### Manual State Control ```scala val cb = CircuitBreaker.default cb.state shouldBe CircuitBreaker.CLOSED cb.isConnected shouldBe true // Manually open the circuit cb.open() cb.state shouldBe CircuitBreaker.OPEN cb.isConnected shouldBe false // Set to half-open for probing cb.halfOpen() cb.state shouldBe CircuitBreaker.HALF_OPEN cb.isConnected shouldBe true // Close the circuit cb.close() cb.state shouldBe CircuitBreaker.CLOSED cb.isConnected shouldBe true ``` ## Standalone Usage Record successes and failures manually: ```scala val cb = CircuitBreaker.default // Verify connection before making request cb.verifyConnection() try val result = externalService.call() cb.recordSuccess() result catch case e: Throwable => cb.recordFailure(e) throw e ``` ## With Retry Combine circuit breaker with retry: ```scala import wvlet.uni.control.{CircuitBreaker, Retry} val cb = CircuitBreaker.withFailureThreshold(5, 10) val result = Retry.withBackOff(maxRetry = 3).run { cb.run { externalService.call() } } ``` ## Use Cases ### External Service Protection ```scala class PaymentService(cb: CircuitBreaker): def processPayment(payment: Payment): Result = cb.run { paymentGateway.charge(payment) } ``` ### Database Protection ```scala val dbBreaker = CircuitBreaker.withConsecutiveFailures(3) def queryDatabase(sql: String): Result = dbBreaker.run { database.execute(sql) } ``` ### API Gateway ```scala val apiBreaker = CircuitBreaker.withFailureThreshold(2, 5) def callApi(request: Request): Response = try apiBreaker.run { httpClient.send(request) } catch case _: CircuitBreakerOpenException => Response.serviceUnavailable() ``` ## Best Practices 1. **Tune thresholds** - Based on normal failure rates 2. **Use failure threshold** - For services with variable error rates 3. **Use consecutive failures** - For services where any failure is significant 4. **Monitor state changes** - Alert on frequent opens 5. **Use fallbacks** - Provide degraded functionality 6. **Test failure scenarios** - Verify circuit behavior --- --- url: https://wvlet.org/uni/control/rate-limiter.md --- # Rate Limiter Control the rate at which operations are performed. Unlike a semaphore that limits *concurrent* operations, `RateLimiter` caps the *throughput* (e.g. 100 ops/sec). `RateLimiter` is JVM-only and lives in `wvlet.uni.control`. ```scala import wvlet.uni.control.RateLimiter ``` ## Choosing an Algorithm | Algorithm | Bursts | Best For | |----------------|----------------|-------------------------------------------------------| | Token Bucket | Allowed | Smooth average rate with occasional bursts (default) | | Fixed Window | At boundaries | Simple per-window quotas (e.g. "100 requests/minute") | | Sliding Window | None | Strict rolling-window guarantees | When in doubt, start with **token bucket** — it is the most flexible and the cheapest (lock-free). ## Token Bucket Tokens refill at a steady rate up to a bucket capacity (`burstSize`). Each operation consumes one or more tokens. Permits bursts up to the bucket size and then enforces the steady rate. ```scala val limiter = RateLimiter .newBuilder .withPermitsPerSecond(100.0) .withBurstSize(20) .build() // Blocks if the rate is exceeded; returns the wait time in ms val waitedMs = limiter.acquire() // Non-blocking variant if limiter.tryAcquire() then callExternalService() ``` ### Run a Block under a Limit ```scala val result = limiter.withLimit { callExternalService() } ``` ### Acquire Multiple Permits Useful when operations have different costs (e.g. a bulk API call): ```scala limiter.acquireN(5) limiter.withLimitN(10) { bulkUpload(batch) } ``` ### Builder Options ```scala val limiter = RateLimiter .newBuilder .withPermitsPerSecond(50.0) // steady rate .withBurstSize(10) // max tokens stored .withTicker(Ticker.systemTicker) // injectable for tests .build() ``` ### Steady Output (Leaky-Bucket Behavior) A token bucket with `burstSize = 1` refuses to burst, producing the smooth, drain-at-constant-rate behavior commonly associated with a leaky bucket: ```scala val smooth = RateLimiter.newBuilder .withPermitsPerSecond(10.0) .withBurstSize(1) .build() ``` ## Fixed Window Allows up to `maxOperations` per discrete window; the counter resets at window boundaries. Simple and memory-efficient, but can spike at boundaries. ```scala import java.util.concurrent.TimeUnit val limiter = RateLimiter.fixedWindow( maxOperations = 100, windowDuration = 1, unit = TimeUnit.MINUTES ) ``` ## Sliding Window Allows up to `maxOperations` in any rolling `windowDuration`. More accurate than fixed window, at the cost of tracking per-request timestamps. ```scala val limiter = RateLimiter.slidingWindow( maxOperations = 100, windowDuration = 1, unit = TimeUnit.MINUTES ) ``` ## Unlimited (No-op) Useful as a default or in tests: ```scala val limiter = RateLimiter.unlimited ``` ## Inspecting State All algorithms expose: ```scala limiter.ratePerSecond // configured rate limiter.availablePermits // current permits available limiter.estimatedWaitTimeMillis // projected wait for the next permit ``` ## Testing with a Manual Ticker Inject `Ticker.manualTicker` to advance virtual time deterministically. Exercise the limiter via `tryAcquire` / `availablePermits` — the ticker controls token refill, not blocking: ```scala import wvlet.uni.control.{RateLimiter, Ticker} val ticker = Ticker.manualTicker val limiter = RateLimiter .newBuilder .withPermitsPerSecond(1.0) .withBurstSize(1) .withTicker(ticker) .build() limiter.tryAcquire() shouldBe true limiter.tryAcquire() shouldBe false ticker.advance(1_000_000_000L) // advance 1 second limiter.tryAcquire() shouldBe true ``` > **Note:** `acquire` / `acquireN` / `withLimit` still block on real wall clock > time via `Thread.sleep`, even with a manual ticker. Test the blocking path > with a system ticker and short intervals, or stick to `tryAcquire` for fully > deterministic tests. ## Composing with Retry and Circuit Breaker `RateLimiter` composes naturally with the other control primitives. A common pattern for calling a rate-limited external API: ```scala import wvlet.uni.control.{CircuitBreaker, RateLimiter, Retry} val limiter = RateLimiter.newBuilder.withPermitsPerSecond(10.0).build() val breaker = CircuitBreaker.withConsecutiveFailures(5) val result = Retry.withBackOff(maxRetry = 3).run { breaker.run { limiter.withLimit { callExternalService() } } } ``` ## Best Practices 1. **Pick the right algorithm** — token bucket for throughput smoothing, sliding window for strict quotas, fixed window when memory is tight. 2. **Size the burst deliberately** — a large burst increases tail pressure on downstream services; `burstSize = 1` enforces strict smoothing. 3. **Prefer `tryAcquire` at system boundaries** — shed load explicitly instead of blocking caller threads indefinitely. 4. **Inject a `Ticker` in tests** — use `Ticker.manualTicker` with `tryAcquire` to drive refill deterministically; note that `acquire` still uses real sleeps. 5. **Combine with `CircuitBreaker` and `Retry`** — rate limiting alone does not protect against downstream outages. --- --- url: https://wvlet.org/uni/control/cache.md --- # Caching uni provides caching through Rx operators and dedicated cache utilities. ## Rx Cache Cache the last value in a reactive stream: ```scala import wvlet.uni.rx.Rx import java.util.concurrent.TimeUnit // Cache the result of an expensive operation val cached = Rx.single(expensiveComputation()) .cache // Multiple subscriptions reuse the cached value cached.subscribe(println) // Computes cached.subscribe(println) // Uses cached ``` ## TTL Cache Expire cached values after a duration: ```scala val cached = Rx.single(fetchData()) .cache .expireAfterWrite(5, TimeUnit.MINUTES) // Value is recomputed after 5 minutes ``` ## Manual Cache Management ```scala val cached = Rx.single(fetchData()).cache // Check current cached value cached.getCurrent match case Some(value) => println(s"Cached: ${value}") case None => println("No cached value") ``` ## Periodic Refresh Combine interval with cache: ```scala import java.util.concurrent.TimeUnit // Refresh data every 30 seconds val refreshing = Rx .interval(30, TimeUnit.SECONDS) .flatMap(_ => Rx.single(fetchLatestData())) .cache // Subscribers always get the latest cached value refreshing.subscribe { data => updateUI(data) } ``` ## Request Deduplication Use cache to deduplicate concurrent requests: ```scala class DataService: private val cache = Rx.single(loadData()).cache def getData: Rx[Data] = cache ``` ## Cache Patterns ### Lazy Initialization ```scala val lazyData = Rx.single { logger.info("Loading data...") loadExpensiveData() }.cache // Data is loaded only on first access ``` ### Conditional Refresh ```scala def getWithRefresh(forceRefresh: Boolean): Rx[Data] = if forceRefresh then Rx.single(fetchData()) else cachedData ``` ### Fallback on Failure ```scala val robust = Rx.single(fetchFromPrimary()) .recover { case _: NetworkException => cachedFallback.getCurrent.getOrElse(defaultValue) } .cache ``` ## Best Practices 1. **Set appropriate TTLs** - Balance freshness vs. performance 2. **Handle cache misses** - Provide fallback values 3. **Monitor hit rates** - Tune cache parameters 4. **Consider memory** - Cache size impacts memory usage 5. **Thread safety** - Rx cache is thread-safe ## LRU Cache (Custom) For more advanced caching, implement LRU: ```scala import scala.collection.mutable class LRUCache[K, V](maxSize: Int): private val cache = mutable.LinkedHashMap[K, V]() def get(key: K): Option[V] = cache.get(key).map { value => // Move to end (most recently used) cache.remove(key) cache.put(key, value) value } def put(key: K, value: V): Unit = cache.remove(key) cache.put(key, value) if cache.size > maxSize then cache.remove(cache.head._1) ``` --- --- url: https://wvlet.org/uni/control/resource.md --- # Resource Management Safely acquire and release resources with automatic cleanup. ## Basic Usage ```scala import wvlet.uni.control.Control Control.withResource(openFile("data.txt")) { file => processFile(file) } // File is automatically closed ``` ## Multiple Resources ```scala Control.withResources( openDatabase(), openFile("config.txt") ) { (db, file) => processWithResources(db, file) } // Both resources are closed in reverse order ``` ## AutoCloseable Support Works with any `AutoCloseable`: ```scala import java.io.{BufferedReader, FileReader} Control.withResource(BufferedReader(FileReader("data.txt"))) { reader => reader.lines().forEach(println) } ``` ## Nested Resources ```scala Control.withResource(openConnection()) { conn => Control.withResource(conn.createStatement()) { stmt => Control.withResource(stmt.executeQuery(sql)) { rs => processResultSet(rs) } } } ``` ## Error Handling Resources are closed even if an exception occurs: ```scala try Control.withResource(openFile("data.txt")) { file => if someCondition then throw RuntimeException("Processing failed") processFile(file) } catch case e: RuntimeException => // Handle error, file is already closed logger.error("Error", e) ``` ## Custom Resource Types Implement `AutoCloseable` for custom types: ```scala class DatabasePool extends AutoCloseable: private val connections = scala.collection.mutable.ListBuffer[Connection]() def getConnection(): Connection = ??? def close(): Unit = // Release all connections connections.foreach(_.close()) Control.withResource(DatabasePool()) { pool => val conn = pool.getConnection() // Use connection } ``` ## Resource in Rx Combine with reactive streams: ```scala import wvlet.uni.rx.Rx def readLines(path: String): Rx[String] = Rx.single { Control.withResource(scala.io.Source.fromFile(path)) { source => source.getLines().toList } }.flatMap(lines => Rx.fromSeq(lines)) ``` ## Loan Pattern The `withResource` method implements the loan pattern: ```scala def withDatabase[T](f: Database => T): T = Control.withResource(Database.connect()) { db => f(db) } // Usage val users = withDatabase { db => db.query("SELECT * FROM users") } ``` ## Resource with Design Integrate with Design for lifecycle management: ```scala import wvlet.uni.design.Design val design = Design.newDesign .bindSingleton[DatabasePool] .onStart(_.initialize()) .onShutdown(_.close()) design.withSession { session => val pool = session.build[DatabasePool] // Pool is initialized and will be closed } ``` ## Best Practices 1. **Always use withResource** for closeable resources 2. **Handle exceptions** appropriately 3. **Close in reverse order** for nested resources 4. **Prefer Design lifecycle** for application-scoped resources 5. **Log cleanup failures** but don't mask original errors ## Common Resources | Resource | Usage | |----------|-------| | Files | `BufferedReader`, `BufferedWriter` | | Database | `Connection`, `Statement`, `ResultSet` | | Network | `Socket`, `HttpClient` | | Streams | `InputStream`, `OutputStream` | --- --- url: https://wvlet.org/uni/control/background-task.md --- # Background Task `BackgroundTask[A, P]` runs a unit of work on a background worker that is **cooperatively cancellable** and **progress-pollable** from another thread. It is the non-`Rx` counterpart to a fiber: the body is a plain side-effecting block (no effect monad), so it fits long-running, blocking work — a database query, a file scan, a REPL evaluation — that you want to cancel or report progress on. * `A` — the result type the body returns * `P` — the progress type the body reports ```scala import wvlet.uni.control.BackgroundTask val task = BackgroundTask.start[Int, Int] { ctx => var sum = 0 for i <- 1 to 100 do ctx.checkCancelled() // abort cooperatively if cancelled sum += i ctx.reportProgress(i) // publish progress (0–100) sum } val result = task.await() // Result[Int] — blocks until done println(result.getOrElse(0)) // 5050 ``` ## Starting a Task `BackgroundTask.start` takes a body `TaskContext[P] => A` and returns immediately with a handle: ```scala def start[A, P](body: TaskContext[P] => A): BackgroundTask[A, P] ``` On **JVM** and **Scala Native** the body runs on a daemon thread, so the caller can poll, cancel, and block concurrently. On **Scala.js** there are no threads, so the body runs **inline during `start`** and has already completed when `start` returns — see [Platform behavior](#platform-behavior). ## The Task Context (body side) The body receives a `TaskContext[P]` to cooperate with the caller: | Method | Purpose | |--------|---------| | `isCancelled: Boolean` | `true` once `cancel()` was called — poll it in loops | | `checkCancelled(): Unit` | Throws `TaskCancelledException` if cancelled — call at safe points | | `reportProgress(p: P): Unit` | Publish the latest progress to the caller | | `onCancel(hook: () => Unit): Unit` | Register a hook to interrupt an in-flight blocking call | Cancellation is **cooperative**: `cancel()` only sets a flag. The body must poll `isCancelled` / call `checkCancelled()` at safe points (typically loop iterations) for cancellation to take effect. For a call already blocked in native/FFI code that can't poll a flag, register an `onCancel` hook — it runs on the caller's thread when `cancel()` is invoked, and is the escape hatch for interrupting work in flight: ```scala val task = BackgroundTask.start[QueryResult, Unit] { ctx => val handle = nativeQuery.start() ctx.onCancel(() => nativeQuery.interrupt(handle)) // e.g. duckdb_interrupt nativeQuery.awaitResult(handle) } ``` ## The Task Handle (caller side) | Method | Blocks? | Returns | |--------|---------|---------| | `progress: Option[P]` | no | latest reported progress, or `None` | | `progressStream: Rx[P]` | no | push stream of progress, completing when the task ends | | `poll: Option[Result[A]]` | no | `None` while running, `Some(result)` once finished | | `await(): Result[A]` | yes | the terminal result (blocks until done) | | `cancel(): Unit` | no | signals cooperative cancellation, runs `onCancel` hooks | | `isCancelled: Boolean` | no | whether `cancel()` was called | | `isDone: Boolean` | no | whether the task has terminated | ### Results `await()` and `poll` return a [`Result[A]`](../core/result) — `Success(value)` or `Failure(error)`. The body's failures are **recorded, not rethrown**, so `await()` always returns rather than throwing: ```scala task.await() match case Result.Success(value) => println(s"Done: ${value}") case Result.Failure(e) => println(s"Failed: ${e.getMessage}") ``` A cancelled task whose body calls `checkCancelled()` completes as a `Failure(TaskCancelledException)`. ### Polling vs. streaming progress Use the non-blocking `progress` snapshot to render a progress bar from another thread: ```scala while !task.isDone do task.progress.foreach(p => render(p)) Thread.sleep(100) ``` Or subscribe to `progressStream` for a push feed that completes with the task: ```scala task.progressStream.subscribe { p => println(s"progress: ${p}") } ``` The stream holds only the latest value (it does not buffer), so an unconsumed `progressStream` won't accumulate updates. ## Cancellation ```scala val task = BackgroundTask.start[Unit, Int] { ctx => while !ctx.isCancelled do doOneStep() } // later, from another thread: task.cancel() task.await() // returns once the body observes the flag and exits ``` `cancel()` after the task has already completed is a **no-op** — `onCancel` hooks are drained on completion, so a late cancel can't fire them against an unrelated operation. ## Platform behavior | Platform | Worker | Concurrency | |----------|--------|-------------| | JVM | daemon thread | full — poll / cancel / `await` from another thread | | Scala Native | daemon thread | full | | Scala.js | inline during `start` | none — body completes before `start` returns | On Scala.js the API is identical, but because the body runs inline: * the task is already done when `start` returns, so `cancel()` is effectively a no-op; * a `progressStream` subscriber attached afterwards sees only the **final** reported value, not a live feed — use the `progress` snapshot instead. For the full rationale (why JS can't host a true worker thread, the catch-`Throwable`-and-signal contract that keeps `await` from hanging, and the `progressStream` design), see the [ADR](https://github.com/wvlet/uni/blob/main/adr/2026-06-20-background-task.md). --- --- url: https://wvlet.org/uni/http.md --- # HTTP Framework uni provides a cross-platform HTTP client and server — running on the JVM (Netty), Scala.js (Fetch / Node.js), and Scala Native (libcurl) — with automatic retry, streaming support, and reactive integration. ## Overview | Component | Description | |-----------|-------------| | [HTTP Client](./client) | Sync and async HTTP clients | | [REST Server](./server) | Cross-platform HTTP server (Netty / Node.js / Native) with routing | | [Router](./router) | Type-safe route definitions | | [RPC](./rpc) | Type-safe remote calls over HTTP, with client code generation | | [Server-Sent Events](./sse) | One-way server→client streaming | | [WebSocket Client](./websocket) | Cross-platform bidirectional WebSocket client | | [Retry Strategies](./retry) | Automatic retry for transient failures | ## Quick Start ```scala import wvlet.uni.http.{Http, Request} // Create a client val client = Http.client.newSyncClient // Make a GET request val response = client.send(Request.get("https://api.example.com/users")) println(response.contentAsString) // Close the client client.close() ``` ## Async Client with Rx ```scala import wvlet.uni.http.Http val asyncClient = Http.client.newAsyncClient asyncClient .send(Request.get("https://api.example.com/data")) .map(_.contentAsString) .subscribe { content => println(s"Received: ${content}") } ``` ## Key Features * **Cross-Platform** - Works on JVM, Scala.js, and Scala Native * **Automatic Retry** - Built-in retry with exponential backoff * **Streaming** - Stream response bodies as byte chunks * **Reactive** - Async client returns `Rx[HttpResponse]` * **Configurable** - Timeouts, retries, headers ## Client Types | Type | Method | Use Case | |------|--------|----------| | Sync | `newSyncClient` | Simple blocking requests | | Async | `newAsyncClient` | Non-blocking with Rx | ## Package ```scala import wvlet.uni.http.* ``` --- --- url: https://wvlet.org/uni/http/client.md --- # HTTP Client uni provides both synchronous and asynchronous HTTP clients. ## Creating Clients ### Sync Client ```scala import wvlet.uni.http.Http val client = Http.client.newSyncClient ``` ### Async Client ```scala val asyncClient = Http.client.newAsyncClient ``` ## Platform Backends The same `Http.client` API runs on all three Scala platforms. uni selects the right transport for the runtime automatically — you write the request once and it works on JVM, Scala.js, and Scala Native. | Platform | Sync (`newSyncClient`) | Async (`newAsyncClient`) | Underlying transport | |----------|------------------------|--------------------------|----------------------| | **JVM** | `JavaHttpChannel` | `JavaHttpAsyncChannel` | `java.net.http.HttpClient` (Java 11+) | | **Scala.js** | `NodeSyncHttpChannel` *(Node.js only)* | `FetchChannel` | Fetch API (browser + Node), Node `worker_threads` for sync | | **Scala Native** | `CurlChannel` | `CurlAsyncChannel` | libcurl | Both client types share the same `send` / `sendStreaming` interface, so most code is platform-agnostic. A few platform constraints are worth knowing: * **Browsers have no synchronous HTTP.** On Scala.js, `newSyncClient` works only under Node.js (it uses `worker_threads` + `Atomics.wait` on a `SharedArrayBuffer`). In a browser, calling it throws `NotImplementedError` — use `newAsyncClient`, which returns `Rx[HttpResponse]`, instead. For the background on the Node sync implementation, see the [ADR](https://github.com/wvlet/uni/blob/main/adr/2026-05-14-nodejs-sync-http.md). * **Scala Native requires libcurl**, both to link and at runtime; the factory initializes it globally the first time a client is created. See [Linking libcurl on Scala Native](#linking-libcurl-on-scala-native) below. * **The Scala Native async client wraps the blocking libcurl call.** Each attempt blocks the thread that runs it (the subscriber for the first attempt, the Rx scheduler for retries), and `sendStreaming` delivers the whole body as a single chunk. * **The async client is the cross-platform common denominator.** If you target the browser, prefer the async API everywhere so the same code compiles for every platform. To plug in a custom transport (testing, a different HTTP library), set a channel factory globally: ```scala import wvlet.uni.http.Http Http.setDefaultChannelFactory(myChannelFactory) ``` ## Linking libcurl on Scala Native You need libcurl **only if your binary actually uses the HTTP client.** The `@link("curl")` annotation that pulls libcurl in rides on `CurlBindings`, which Scala Native's dead-code elimination drops when nothing reaches it. A Native project that only uses, say, `LogSupport` links with no libcurl installed at all. When you do use the client, install libcurl as a **shared library** and Scala Native adds `-lcurl` for you — no `nativeConfig` entry needed. ::: warning libcurl must be dynamically linked uni looks `curl_easy_setopt` and `curl_easy_getinfo` up at runtime, among the modules already loaded into the process. A statically linked libcurl (`libcurl.a`, or vcpkg's `*-windows-static` triplets) exports no symbols to find, so the binary links but aborts on its first request with `uni_curl_shim: libcurl symbol 'curl_easy_setopt' not found in this process`. Use the shared library. Why the lookup happens at runtime rather than through an ordinary `extern` is the subject of [this ADR](https://github.com/wvlet/uni/blob/main/adr/2026-07-06-curl-shim-weak-linking.md) — in short, it is what lets curl-free Native projects link at all. ::: ### Installing libcurl ::: code-group ```bash [Debian / Ubuntu] # The -dev package is what provides the unversioned libcurl.so symlink # that `-lcurl` resolves against. sudo apt-get install -y libcurl4-openssl-dev ``` ```bash [macOS] # libcurl ships with the system; nothing to install. # For a newer version than Apple's: brew install curl ``` ```powershell [Windows] vcpkg install curl:x64-windows # or arm64-windows # Scala Native turns @link("curl") into `curl.lib`, but vcpkg installs the # import library as `libcurl.lib`, so give the linker the name it looks for. $root = "$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows" Copy-Item "$root\lib\libcurl.lib" "$root\lib\curl.lib" -Force # `lib` is where the linker searches; `bin` is where the built binary finds # libcurl.dll at runtime. $env:LIB = "$root\lib;$env:LIB" $env:PATH = "$root\bin;$env:PATH" ``` ::: Alpine and other musl images need `curl-dev`. Distroless or `scratch` runtime images must still carry `libcurl.so` and its TLS dependencies, since the lookup resolves against the loaded shared library — statically linking your Scala Native binary does not fold libcurl into it. ## Making Requests ### GET Request ```scala import wvlet.uni.http.Request val response = client.send(Request.get("https://api.example.com/users")) println(response.status) // HttpStatus println(response.contentAsString) // Response body ``` ### POST Request ```scala val request = Request .post("https://api.example.com/users") .withJsonContent("""{"name": "Alice"}""") val response = client.send(request) ``` ### With Headers ```scala val request = Request .get("https://api.example.com/data") .withBearerToken("token123") .withAccept("application/json") val response = client.send(request) ``` ### Multipart Upload (multipart/form-data) Use `withMultipart` to upload files alongside form fields in a single request. The boundary and `Content-Type: multipart/form-data; boundary=...` header are generated automatically. ```scala import wvlet.uni.http.{Request, MultipartPart, ContentType} val avatarBytes: Array[Byte] = readBytes("avatar.png") val request = Request .post("https://api.example.com/upload") .withMultipart(Seq( MultipartPart.field("name", "alice"), MultipartPart.file("avatar", "avatar.png", avatarBytes, ContentType.ImagePng) )) val response = client.send(request) ``` `MultipartPart.file` defaults to `application/octet-stream` when no content type is given. `MultipartPart.field` is a plain form value serialized as UTF-8. To add custom per-part headers, construct the case classes directly: ```scala val request = Request .post("https://api.example.com/upload") .withMultipart(Seq( MultipartPart.FilePart( name = "report", filename = "q1.pdf", bytes = pdfBytes, contentType = ContentType.ApplicationPdf, headers = HttpMultiMap("X-Checksum-Sha256" -> checksum) ) )) ``` For a fixed boundary (for example in snapshot tests), use the builder: ```scala import wvlet.uni.http.Multipart val mp = Multipart .builder() .withBoundary("----fixed-boundary") .addField("name", "alice") .addFile("avatar", "avatar.png", avatarBytes, ContentType.ImagePng) .build() val request = Request.post("/upload").withMultipartContent(mp) ``` **Scope note**: `multipart/form-data` is intended for bundling form fields and small-to-moderate files (avatars, PDFs, a few MB at most) in a single request. All parts are held in memory. Large-file or resumable uploads (S3 multipart, tus.io) use separate protocols and are out of scope for this API. ## Content Types and Headers The examples above use two small helper types. `ContentType` is an `application/...`-style media type. It provides named constants for the common ones, so you avoid typo-prone string literals: ```scala import wvlet.uni.http.ContentType ContentType.ApplicationJson // "application/json" ContentType.ApplicationPdf // "application/pdf" ContentType.ApplicationOctetStream ContentType.ImagePng // "image/png" ContentType.TextPlain ContentType.TextEventStream // "text/event-stream" (SSE) ContentType("application/vnd.api+json") // any custom type ``` `HttpMultiMap` is the case-insensitive multi-valued map used for headers and form fields (a header can legitimately appear more than once). Build one from pairs, and add to it without mutating the original: ```scala import wvlet.uni.http.HttpMultiMap val headers = HttpMultiMap("Accept" -> "application/json") val more = headers + ("X-Request-Id" -> "abc123") ``` You rarely construct an `HttpMultiMap` directly — request builders like `withBearerToken` and `withAccept` manage headers for you — but it's the type you reach for when adding custom per-part headers to a multipart upload. ## Response Handling ```scala val response = client.send(request) // Status response.isSuccessful // true for 2xx response.statusCode // 200, 201, etc. // Headers response.header("Content-Type") // Option[String] // Body response.contentAsString // Option[String] response.contentAsBytes // Option[Array[Byte]] ``` ## Async Requests ```scala val asyncClient = Http.client.newAsyncClient asyncClient .send(Request.get("https://api.example.com/data")) .map { response => response.contentAsString } .subscribe { content => println(content) } ``` ## Streaming Responses Stream large responses as byte chunks: ```scala asyncClient .sendStreaming(Request.get("https://example.com/large-file")) .subscribe { chunk: Array[Byte] => processChunk(chunk) } ``` ## Client Configuration ```scala val client = Http.client .withConnectTimeoutMillis(5000) // 5 seconds .withReadTimeoutMillis(30000) // 30 seconds .withMaxRetry(3) .newSyncClient ``` ## Disabling Retry ```scala // Create a client with no retries val clientNoRetry = Http.client.noRetry.newSyncClient // Or configure zero retries val clientNoRetry2 = Http.client.withMaxRetry(0).newSyncClient ``` ## Error Handling `send` returns only 2xx responses and throws for any other status: * A non-retryable status (most 4xx) throws `HttpException`. * A retryable status (5xx, 408, 429, and others) is retried, then throws `HttpMaxRetryException`, which is not a subclass of `HttpException`. This happens with `noRetry` too. Both exceptions carry the response, including its body: ```scala import wvlet.uni.http.{HttpException, HttpMaxRetryException} try client.send(request) catch case e: HttpException => println(s"HTTP error: ${e.statusCode}, body: ${e.response.flatMap(_.contentAsString)}") case e: HttpMaxRetryException => println(s"Gave up after retries: ${e.response.status}") ``` ## Best Practices 1. **Reuse clients** - Create once, use many times 2. **Close clients** - Call `close()` when done 3. **Set timeouts** - Prevent hanging requests 4. **Handle errors** - Check status codes 5. **Use async** - For non-blocking operations ## Example: REST Client ```scala class ApiClient(baseUrl: String): private val client = Http.client.newSyncClient def getUser(id: String): User = // send throws HttpException for a 4xx such as 404 val response = client.send( Request.get(s"${baseUrl}/users/${id}") ) Weaver.fromJson[User](response.contentAsString.getOrElse("{}")) def createUser(user: User): User = val response = client.send( Request .post(s"${baseUrl}/users") .withJsonContent(Weaver.toJson(user)) ) Weaver.fromJson[User](response.contentAsString.getOrElse("{}")) def close(): Unit = client.close() ``` --- --- url: https://wvlet.org/uni/http/server.md --- # REST Server uni provides HTTP servers on all three Scala platforms — Netty on the JVM, the Node.js `http` module on Scala.js, and a native socket server on Scala Native — behind one shared configuration API. On the JVM you additionally get a type-safe, annotation-based router framework for building REST APIs. ::: tip Which API is cross-platform? **Functional handlers** (`withHandler` / `withRxHandler`) and **filters** work on every platform. The **annotation router** (`@Endpoint`, `Router.of[T]`, `RouterHandler`) and **WebSocket routes** are **JVM/Netty-only** today — see [Cross-Platform Servers](#cross-platform-servers). ::: ## Quick Start On the JVM, add the `uni-netty` module to your build (it pulls in `uni` transitively). On Scala.js and Scala Native, the server backends are included in `uni` itself: ```scala // JVM only: Netty-based HTTP server libraryDependencies += "org.wvlet.uni" %% "uni-netty" % "2026.1.35" ``` ```scala import wvlet.uni.http.{HttpMethod, Request, Response} import wvlet.uni.http.router.{Endpoint, Router} import wvlet.uni.http.netty.{NettyServer, RouterHandler} // Define a controller class UserController: @Endpoint(HttpMethod.GET, "/users") def listUsers(): String = """["alice", "bob"]""" @Endpoint(HttpMethod.GET, "/users/:id") def getUser(id: String): String = s"""{"id": "${id}"}""" // Create router and start server val router = Router.of[UserController] val handler = RouterHandler(router) NettyServer .withPort(8080) .withRxHandler(handler) .start { server => println(s"Server running on port ${server.localPort}") // Server runs until the block completes } ``` ## Cross-Platform Servers Each platform has its own server entry point, but they expose the **same configuration API** and all start an [`HttpServer`](#server-lifecycle): | Platform | Entry point | Import | Backend | |----------|-------------|--------|---------| | **JVM** | `NettyServer` | `wvlet.uni.http.netty.NettyServer` (module `uni-netty`) | Netty | | **Scala.js** | `NodeServer` | `wvlet.uni.http.NodeServer` | Node.js `http` | | **Scala Native** | `NativeServer` | `wvlet.uni.http.NativeServer` | native sockets | All three share these builder methods: ```scala .withPort(8080) // 0 picks a random free port .withHost("0.0.0.0") // default 127.0.0.1 (loopback only) .withName("my-server") .withHandler(f: Request => Response) // sync handler .withRxHandler(f: Request => Rx[Response]) // async handler .withFilter(filter) // RxHttpFilter .start { server => ... } // block form, auto-stops .start() // returns the HttpServer ``` A handler that compiles and runs identically on every platform: ```scala // JVM example — swap NettyServer for NodeServer / NativeServer on JS / Native import wvlet.uni.http.{Request, Response} import wvlet.uni.http.netty.NettyServer NettyServer .withPort(8080) .withRxHandler { request => Rx.single(Response.ok(s"Hello from ${request.path}")) } .start { server => println(s"Listening on ${server.localAddress}") } ``` ::: warning JVM-only features The annotation router (`@Endpoint`, `Router.of[T]`, `RouterHandler`), [server-side WebSocket](#websocket), and the Netty tuning knobs (`withMaxContentLength`, native transport, graceful shutdown) exist only on the JVM. On Scala.js / Scala Native, route requests yourself inside a functional handler: ```scala import wvlet.uni.http.NodeServer // or NativeServer NodeServer .withPort(8080) .withRxHandler { request => request.path match case "/health" => Rx.single(Response.ok("ok")) case s"/users/${id}" => Rx.single(Response.ok(s"user ${id}")) case _ => Rx.single(Response.notFound) } .start() ``` ::: The rest of this page uses `NettyServer` and the JVM annotation router, which is the richest path. The request/response model, response conversion, and filters described below apply to functional handlers on every platform. ## Defining Endpoints Use the `@Endpoint` annotation to mark controller methods as HTTP endpoints: ```scala import wvlet.uni.http.HttpMethod import wvlet.uni.http.router.Endpoint class ApiController: @Endpoint(HttpMethod.GET, "/health") def health(): String = "ok" @Endpoint(HttpMethod.POST, "/data") def createData(request: Request): Response = val body = request.content.toContentString Response.ok(s"Received: ${body}") @Endpoint(HttpMethod.PUT, "/items/:id") def updateItem(id: String, request: Request): Response = Response.ok(s"Updated item ${id}") @Endpoint(HttpMethod.DELETE, "/items/:id") def deleteItem(id: String): Response = Response.noContent ``` Only `wvlet.uni.http.router.Endpoint` defines a route. An annotation from another framework with the same simple name (e.g., one kept on an API trait for documentation generation) is ignored, and a uni `@Endpoint` whose method or path cannot be read fails when the router is built. ### Path Parameters Use `:paramName` syntax in the path to capture path segments: ```scala class ItemController: @Endpoint(HttpMethod.GET, "/items/:id") def getItem(id: String): String = s"Item ${id}" @Endpoint(HttpMethod.GET, "/users/:userId/posts/:postId") def getUserPost(userId: String, postId: String): String = s"User ${userId}, Post ${postId}" ``` Path parameter names must match the method parameter names. ### Query Parameters Method parameters that don't match path parameters are automatically bound from query string: ```scala class SearchController: @Endpoint(HttpMethod.GET, "/search") def search(query: String, limit: Int = 10): String = s"Searching for '${query}' with limit ${limit}" ``` Request: `GET /search?query=hello&limit=20` Supported parameter types: * `String` * `Int`, `Long`, `Short`, `Byte` * `Double`, `Float` * `Boolean` * `Option[T]` for optional parameters ### Request Object Access Add a `Request` parameter to access the full request: ```scala class MyController: @Endpoint(HttpMethod.POST, "/upload") def upload(request: Request): Response = val contentType = request.headers.get("Content-Type") val body = request.content.toContentString Response.ok(s"Received ${body.length} bytes") ``` ### Request Body Parameters not bound from the path or query string are read from the request body, decoded with a [Weaver](../core/weaver.md): ```scala case class Message(role: String, text: String) case class MessagesRequest(model: String, messages: Seq[Message]) class ChatController: // The whole JSON body decodes into `request`; `httpRequest` is the raw Request @Endpoint(HttpMethod.POST, "/v1/messages") def messages(request: MessagesRequest, httpRequest: Request): Reply = ??? // Several body parameters: each is read from the JSON object field of its name @Endpoint(HttpMethod.POST, "/projects/:id/rename") def rename(id: String, name: String, tags: Seq[String]): Project = ??? ``` * When exactly one parameter, other than `Request`, `RequestAttributes`, and path parameters, is not a primitive, the whole body decodes into it, and the other parameters come from the query string. A collection or map parameter takes the whole body only when it is the only such parameter. Otherwise the body must be an object, and each parameter is read from the field of the same name. Unknown fields are ignored. * Content types: JSON (`application/json`, `*+json`, or no `Content-Type`), and MessagePack (`application/msgpack` or `application/x-msgpack`). * Custom codecs: a `given Weaver[A]` in `A`'s companion object, or imported where `Router.of` is called, is used when the parameter type is `A`, or a case class with an `A` field. Weavers are resolved when `Router.of` is compiled, so a given that is only in scope elsewhere is not seen. For `Option[A]`, `Seq[A]`, or a sealed trait, define a given for that type to get the same. * Errors return 400 and never echo the body: malformed JSON or MessagePack, a value of the wrong shape, or a missing body for a non-`Option` object parameter. A required body in another content type (e.g. a form post) returns 415. * A method that also takes `Request` may read the body itself: a body that does not fit its other parameters is an error only when a required parameter is left without a value. A body without a `Content-Type` that does not parse is always left alone. ## Request and Response ### Request Properties ```scala request.method // HttpMethod (GET, POST, etc.) request.uri // Full URI string request.path // Path portion of URI request.headers // HttpHeaders request.content // HttpContent (request body) request.getQueryParam(name) // Option[String] ``` ### Response Factory Methods ```scala Response.ok // 200 OK Response.ok("body") // 200 with text body Response.created // 201 Created Response.noContent // 204 No Content Response.badRequest("message") // 400 Bad Request Response.notFound // 404 Not Found Response.notFound("message") // 404 with message Response.internalServerError(msg) // 500 Internal Server Error ``` ### Response Content ```scala Response.ok .withTextContent("Hello") // text/plain .withJsonContent("""{"a": 1}""") // application/json .withBytesContent(bytes) // application/octet-stream .withContent(HttpContent.json(str)) // Custom content .addHeader("X-Custom", "value") // Add header ``` ## Response Conversion Controller methods can return various types that are automatically converted to responses: | Return Type | Conversion | |-------------|------------| | `Response` | Returned as-is | | `Rx[Response]` | Async response | | `String` | 200 OK with text/plain | | `Seq[T]` | 200 OK with JSON array | | `Map[K, V]` | 200 OK with JSON object | | `Option[T]` | Value or 204 No Content | | `Unit` | 204 No Content | | Other types | Serialized to JSON | ```scala class DataController: @Endpoint(HttpMethod.GET, "/text") def getText(): String = "plain text" @Endpoint(HttpMethod.GET, "/list") def getList(): Seq[String] = Seq("a", "b", "c") @Endpoint(HttpMethod.GET, "/map") def getMap(): Map[String, Int] = Map("count" -> 42) @Endpoint(HttpMethod.GET, "/async") def getAsync(): Rx[Response] = Rx.single(Response.ok("async result")) ``` ## Filters Use `RxHttpFilter` to intercept requests and responses: ```scala import wvlet.uni.http.{RxHttpFilter, RxHttpHandler} val loggingFilter = RxHttpFilter { (request, next) => println(s"Request: ${request.method} ${request.path}") next .handle(request) .map { response => println(s"Response: ${response.status}") response } } val authFilter = RxHttpFilter { (request, next) => request.headers.get("Authorization") match case Some(token) if isValid(token) => next.handle(request.addHeader("X-User-Id", extractUserId(token))) case _ => Rx.single(Response.unauthorized.withTextContent("Missing or invalid token")) } ``` ### Handling Errors in a Filter An exception thrown by a controller method reaches the enclosing filters as a failed `Rx`, the same as an exception inside a returned `Rx`. A filter can `recover` from it and map the original exception to a response, so error classification can live in one place: ```scala val errorFilter = RxHttpFilter { (request, next) => next .handle(request) .recover { case e: IllegalArgumentException => Response.badRequest(e.getMessage) } } ``` An exception that no filter recovers from becomes a generic `500 Internal Server Error` (see [Security Defaults](#security-defaults)). Because a failed `Rx` skips `map`, a filter that decorates every response (logging, CORS headers) never sees this 500. Place it outside an error filter that recovers, or handle the failure itself with `recover`/`tapOnFailure`. A malformed path or query parameter is answered with 400 by `RouterHandler` itself, before any exception reaches a filter. ### Applying Filters ```scala // Apply filter to server NettyServer .withPort(8080) .withFilter(loggingFilter) .withFilter(authFilter) .withRxHandler(handler) .start() // Or compose filters val combinedFilter = loggingFilter.andThen(authFilter) ``` ### Router-Level Filters Define filters that apply to specific controllers: ```scala class LogFilter extends RxHttpFilter: def apply(request: Request, next: RxHttpHandler): Rx[Response] = println(s"Handling: ${request.path}") next.handle(request) val router = Router .filter[LogFilter] .andThen(Router.of[UserController]) ``` ### Passing Data from a Filter to Controllers A filter can attach server-side attributes to the request, e.g. the identity an authentication filter resolved. Attributes never come from the client: a request decoded by the server starts with none, so a client cannot spoof them with headers. Since `Request` is immutable, each attribute stays with the request it was set on. ```scala import wvlet.uni.http.{Request, RequestAttribute, RequestAttributes, RxHttpFilter} object Auth: // Keys compare by identity: share this instance rather than re-creating it by name val Identity: RequestAttribute[User] = RequestAttribute("identity") val filter = RxHttpFilter { (request, next) => authenticate(request) match case Some(user) => next.handle(request.withAttribute(Identity, user)) case None => Rx.single(Response.unauthorized) } class ApiController: @Endpoint(HttpMethod.GET, "/me") def me(request: Request): User = request.attribute(Auth.Identity).get // Or take the attributes alone @Endpoint(HttpMethod.GET, "/me/name") def name(attributes: RequestAttributes): String = attributes.get(Auth.Identity).get.name ``` The Netty server sets `RequestAttribute.RemoteAddress` (`host:port` of the connected peer, with IPv6 hosts in brackets, which is the load balancer when there is one). `RequestAttributes.toString` prints only the attribute names, so logging a request does not leak their values. ### Access Log `HttpAccessLogFilter` writes one entry per request, using the field names of Airframe's `HttpLogger` (`method`, `path`, `status_code`, `response_time_ms`, `request_size`, `response_size`, `remote_address`, `request_header`, `response_header`, `exception_class`, ...): ```scala import wvlet.uni.http.{HttpAccessLogConfig, HttpAccessLogFilter, HttpAccessLogWriter} import wvlet.uni.log.FileLogHandlerConfig val accessLog = HttpAccessLogFilter( HttpAccessLogWriter.jsonFile(FileLogHandlerConfig("log/http_server.json")), HttpAccessLogConfig() .withStreamWriter(HttpAccessLogWriter.jsonFile(FileLogHandlerConfig("log/http_stream.json"))) ) NettyServer .withFilter(accessLog.andThen(authFilter)) // outermost, so it sees the final response .withRxHandler(RouterHandler(router)) .start() // In an inner filter or a controller HttpAccessLogFilter.addEntry(request, "user_id", user.id) ``` * `authorization`, `proxy-authorization`, `cookie`, `set-cookie`, and `x-api-key` are logged as `[redacted]`. `withRedactedHeaders(...)` replaces the list, and `withExcludedHeaders(...)` leaves headers out. * A controller exception is logged with status 500 and its class and message, and is then rendered by the server as usual. * An SSE response gets a second entry when the stream ends (`stream_event_count`, `stream_bytes`, `stream_duration_ms`), written to the stream writer. * `jsonFile` writes JSON lines with daily and size-based rotation. For Fluentd or another sink, implement `HttpAccessLogWriter.write(entry: Map[String, Any])`. * Requests rejected before filters run (oversized or malformed bodies, read timeouts, the host check) are not logged. ## Server Configuration ### NettyServerConfig Options ```scala NettyServer .withPort(8080) // Port to listen on (0 for random) .withHost("0.0.0.0") // Bind address (default: 127.0.0.1) .withName("my-server") // Server name for logging .withMaxContentLength(1024 * 1024) // Max request body size (default: 64KB) .withMaxHeaderSize(8192) // Max header size .withMaxInitialLineLength(4096) // Max request line length .withRequestReadTimeoutMillis(30000) // Close connections that stall before a full request .withMaxDecompressedRequestSize(64 * 1024 * 1024) // Max decoded size of a gzip/deflate body .noNativeTransport // Disable native transport (epoll/kqueue) .withHandler(handler) .start() ``` ### Request Decompression The Netty server decodes request bodies sent with `Content-Encoding: gzip` or `deflate` (and the `x-gzip` / `x-deflate` aliases) before any filter or handler sees them, and removes the `Content-Encoding` header. Other encodings such as `br` pass through unchanged. * A compressed body that decodes to more than `withMaxDecompressedRequestSize` bytes (default 64 MiB) gets a plain-text `413`, and the connection is closed. Uncompressed requests are not counted against this limit. * A malformed compressed body gets a plain-text `400`, and the connection is closed. * The decoded body must also fit in `withMaxContentLength`, so raise both for large compressed uploads. * `.noRequestDecompression` passes the compressed bytes to the handler, e.g. to authenticate a request before inflating it. ### Security Defaults All three backends start with these defaults: | Default | Why | Opt out | |---------|-----|---------| | Bind to `127.0.0.1` | An unauthenticated dev or sidecar server is not reachable from the network | `.withHost("0.0.0.0")` for Docker, Kubernetes, or any server other hosts must reach | | 500 responses say only `Internal Server Error`; RPC errors say `Internal error` | Exception messages can leak file paths, SQL, hostnames, or credentials. The full exception is logged on the server | `.withDetailedErrors(true)` on the server config, which also covers `RouterHandler` controller exceptions; `RPCHandler(router).withDetailedErrors(true)` for RPC | | On a loopback server, requests whose `Host` is not `localhost`, `*.localhost`, `127.0.0.1` or `[::1]` get 403 | DNS rebinding: a web page can point its own host name at 127.0.0.1 and then read the local server as same-origin. The browser still sends the page's host name in `Host` | `.withAllowedHosts("app.example.com", "*.internal.example")` to replace the list, `.noHostCheck` to accept any. A server bound to `0.0.0.0` accepts any host unless `withAllowedHosts` is set | | WebSocket upgrades from another browser origin get 403 | Browsers attach cookies to cross-origin WebSocket handshakes (cross-site WebSocket hijacking) | `.withAllowedOrigins("https://app.example.com")`, or `"*"` for any origin | | Bounded request size | A single huge request cannot exhaust memory | Netty `.withMaxContentLength` (and `.withMaxDecompressedRequestSize` for gzip/deflate bodies, default 64 MiB), Node `.withMaxContentLength` (default 1MB, oversized bodies get 413), Native `.withMaxRequestSize` | | Request read timeout (Netty: 30s) | Bounds slowloris clients and idle keep-alive connections | `.withRequestReadTimeoutMillis(...)` / `.noRequestReadTimeout`. The Native backend has its own `idleTimeoutMillis`/`readTimeoutMillis` | | Bounded long-lived connections | Idle Server-Sent Events streams (and WebSockets on Native) cannot exhaust threads or starve ordinary requests; over the limit they get 503 | `.withMaxStreamingConnections(n)`. Netty default: 256 SSE streams. Native default: `workerThreads` minus up to 4 workers kept for ordinary requests | | CR, LF, or NUL in a header name or value is rejected | Prevents HTTP response splitting when a header echoes user input | — (`addHeader`/`setHeader` throw `IllegalArgumentException`) | Requests without an `Origin` header (CLI tools, server-to-server clients) and same-origin pages are always accepted for WebSocket. A malformed path or query parameter (e.g. `?limit=abc` for an `Int`) returns 400 rather than 500. TLS is not built in: terminate HTTPS at a reverse proxy or load balancer. ### Tuning a Public API Server The defaults suit a small internal service. A public API server usually reviews these together: | Setting | Default | When to change | |---------|---------|----------------| | `.withMaxContentLength(n)` | 64 KB | Raise it to the largest request body the API accepts, e.g. LLM prompts with images. A decompressed body must also fit in this limit | | `.withRequestReadTimeoutMillis(ms)` | 30 s | The same timer closes idle keep-alive connections. Keep it longer than the idle timeout of the load balancer in front of the server, or the server closes connections the balancer still reuses | | `.withMaxStreamingConnections(n)` | 256 | The maximum number of concurrent SSE streams per server; streams beyond it get 503. Size it to the expected number of concurrent streaming clients | | `.withHost("0.0.0.0")` / `.withAllowedHosts(...)` | `127.0.0.1`, loopback host check | Required behind a load balancer or in a container | | `.withDetailedErrors(true)` | off | Keep it off in production; the full exception is logged on the server | ### Server Lifecycle ```scala // Start and get server instance val server = NettyServer .withPort(8080) .withHandler(handler) .start() println(s"Running on port ${server.localPort}") server.isRunning // true // Stop when done server.stop() server.isRunning // false ``` ### Block-based Server For simpler use cases, use the block form that automatically stops the server: ```scala NettyServer .withPort(8080) .withHandler(handler) .start { server => // Server runs while this block executes println(s"Server on port ${server.localPort}") Thread.sleep(60000) // Run for 1 minute } // Server automatically stopped here ``` ## Simple Handler For simple cases without routing, use a direct handler: ```scala NettyServer .withPort(8080) .withHandler { request => Response.ok(s"Hello from ${request.path}") } .start() ``` Or with async responses: ```scala NettyServer .withPort(8080) .withRxHandler { request => Rx.single(Response.ok("async response")) } .start() ``` ## WebSocket The Netty server supports WebSocket connections. WebSocket routes are registered by path, separately from the REST router, because connections are stateful and bidirectional rather than request/response. A fresh `WebSocketHandler` is created per connection, so it may hold per-connection state; all of its callbacks have no-op defaults, so you override only what you need. ```scala import wvlet.uni.http.{WebSocketContext, WebSocketHandler} import wvlet.uni.http.netty.NettyServer NettyServer .withPort(8080) .withWebSocketRoute("/ws/echo") { request => new WebSocketHandler: override def onOpen(ctx: WebSocketContext): Unit = ctx.send("welcome") override def onTextMessage(ctx: WebSocketContext, message: String): Unit = ctx.send(s"echo: ${message}") override def onBinaryMessage(ctx: WebSocketContext, message: Array[Byte]): Unit = ctx.send(message) override def onClose(ctx: WebSocketContext): Unit = println("connection closed") override def onError(ctx: WebSocketContext, e: Throwable): Unit = println(s"error: ${e.getMessage}") } .start() ``` ### WebSocketHandler Callbacks | Callback | When it fires | |----------|---------------| | `onOpen(ctx)` | After the handshake completes, before any message | | `onTextMessage(ctx, message)` | A complete text message arrived | | `onBinaryMessage(ctx, message)` | A complete binary message arrived | | `onClose(ctx)` | The connection closed (client- or server-initiated); delivered exactly once | | `onError(ctx, e)` | A callback or the connection raised an error | ### WebSocketContext The `ctx` passed to each callback controls the connection. Its `send`/`close` methods are safe to call from any thread: ```scala ctx.request // the original HTTP upgrade request ctx.send("hello") // send a text message ctx.send(Array[Byte](1, 2, 3)) // send a binary message ctx.close() // close with 1000 (normal closure) ctx.close(1001, "going away") // close with a status code and reason ``` ### Allowed Origins Before the route filter runs, the server checks the browser `Origin` of the upgrade. Same-origin pages and clients that send no `Origin` are accepted; any other origin gets 403. Allow a front end served from another origin (e.g. a Vite dev server) explicitly: ```scala NettyServer .withPort(8080) .withAllowedOrigins("http://localhost:5173", "https://app.example.com") .withWebSocketRoute("/ws") { _ => handler } ``` ### Gating the Upgrade With a Filter Pass an `RxHttpFilter` to authenticate or otherwise gate the upgrade handshake. A 2xx response allows the upgrade; a non-2xx response (or an empty `Rx`) rejects it. Attributes the filter adds to the request are visible via `ctx.request`: ```scala import wvlet.uni.http.{Response, RxHttpFilter, WebSocketContext, WebSocketHandler} import wvlet.uni.http.netty.NettyServer import wvlet.uni.rx.Rx val authFilter = RxHttpFilter { (request, next) => request.header("Authorization") match case Some(token) if isValid(token) => next.handle(request.addHeader("X-User", userOf(token))) case _ => Rx.single(Response.unauthorized) } NettyServer .withPort(8080) .withWebSocketRoute("/ws/secure", authFilter) { request => new WebSocketHandler: override def onOpen(ctx: WebSocketContext): Unit = ctx.send(s"hello ${ctx.request.header("X-User").getOrElse("anonymous")}") } .start() ``` ### Frame Size Inbound messages are aggregated from continuation frames up to a configurable limit (1 MB by default): ```scala NettyServer .withWebSocketMaxFrameSize(4 * 1024 * 1024) // 4 MB .withWebSocketRoute("/ws") { _ => new WebSocketHandler {} } .start() ``` WebSocket support is server-side and JVM (Netty) only. WebSocket routes coexist with REST endpoints on the same server and port. For connecting *to* a WebSocket endpoint, see the cross-platform [WebSocket Client](./websocket). ## Combining Controllers Combine multiple controllers into a single router: ```scala class UserController: @Endpoint(HttpMethod.GET, "/users") def listUsers(): String = "[]" class ItemController: @Endpoint(HttpMethod.GET, "/items") def listItems(): String = "[]" val router = Router.of[UserController] .andThen(Router.of[ItemController]) val handler = RouterHandler(router) ``` ## Example: Complete REST API ```scala import wvlet.uni.http.{HttpMethod, Request, Response} import wvlet.uni.http.router.{Endpoint, Router} import wvlet.uni.http.RxHttpFilter import wvlet.uni.http.netty.{NettyServer, RouterHandler} import wvlet.uni.rx.Rx case class User(id: String, name: String) class UserController: private var users = Map( "1" -> User("1", "Alice"), "2" -> User("2", "Bob") ) @Endpoint(HttpMethod.GET, "/users") def listUsers(): Seq[User] = users.values.toSeq @Endpoint(HttpMethod.GET, "/users/:id") def getUser(id: String): Response = users.get(id) match case Some(user) => Response.ok.withJsonContent(s"""{"id":"${user.id}","name":"${user.name}"}""") case None => Response.notFound(s"User ${id} not found") @Endpoint(HttpMethod.POST, "/users") def createUser(request: Request): Response = // Parse and create user Response.created.withJsonContent("""{"id":"3","name":"New User"}""") @Endpoint(HttpMethod.DELETE, "/users/:id") def deleteUser(id: String): Response = users -= id Response.noContent val loggingFilter = RxHttpFilter { (request, next) => val start = System.currentTimeMillis() next.handle(request).map { response => val duration = System.currentTimeMillis() - start println(s"${request.method} ${request.path} -> ${response.status} (${duration}ms)") response } } val router = Router.of[UserController] val handler = RouterHandler(router) NettyServer .withPort(8080) .withFilter(loggingFilter) .withRxHandler(handler) .start { server => println(s"API running at http://localhost:${server.localPort}") Thread.currentThread().join() // Run indefinitely } ``` --- --- url: https://wvlet.org/uni/http/router.md --- # Router The router framework extracts endpoint definitions from controllers at compile-time and matches incoming requests to handler methods. ## Router Builder ### Creating a Router Use `Router.of[T]` to extract routes from a controller class: ```scala import wvlet.uni.http.HttpMethod import wvlet.uni.http.router.{Endpoint, Router} class UserController: @Endpoint(HttpMethod.GET, "/users") def listUsers(): String = "[]" @Endpoint(HttpMethod.GET, "/users/:id") def getUser(id: String): String = s"""{"id":"${id}"}""" val router = Router.of[UserController] // Check extracted routes println(router.routeSummary) // Router: // GET /users -> listUsers // GET /users/:id -> getUser ``` The macro extracts all methods annotated with `@Endpoint` and creates route definitions. ### Combining Routers Use `andThen` to combine multiple routers: ```scala class UserController: @Endpoint(HttpMethod.GET, "/users") def listUsers(): String = "[]" class ItemController: @Endpoint(HttpMethod.GET, "/items") def listItems(): String = "[]" class HealthController: @Endpoint(HttpMethod.GET, "/health") def health(): String = "ok" val router = Router.of[HealthController] .andThen(Router.of[UserController]) .andThen(Router.of[ItemController]) ``` Routes are matched in the order they are added. ### Empty Router Create an empty router to combine with others: ```scala val baseRouter = Router.empty val fullRouter = baseRouter .andThen(Router.of[Controller1]) .andThen(Router.of[Controller2]) ``` ## Controller Provider The `ControllerProvider` interface determines how controller instances are created. ### SimpleControllerProvider The default provider creates instances using reflection: ```scala import wvlet.uni.http.router.SimpleControllerProvider import wvlet.uni.http.netty.RouterHandler val router = Router.of[UserController] val handler = RouterHandler(router) // Uses SimpleControllerProvider // Equivalent to: val handler2 = RouterHandler(router, SimpleControllerProvider()) ``` Controllers must have a no-argument constructor. ### MapControllerProvider For pre-configured instances (useful for testing or object wiring): ```scala import wvlet.uni.http.router.MapControllerProvider class UserController(userService: UserService): @Endpoint(HttpMethod.GET, "/users") def listUsers(): Seq[User] = userService.findAll() // Create controller with dependencies val userService = UserService() val controller = UserController(userService) // Register pre-created instance val provider = MapControllerProvider(controller) val handler = RouterHandler(router, provider) ``` Multiple controllers: ```scala val provider = MapControllerProvider( UserController(userService), ItemController(itemService), HealthController() ) ``` ### Custom Provider Implement `ControllerProvider` for custom instantiation logic: ```scala import wvlet.uni.http.router.ControllerProvider import wvlet.uni.surface.Surface class DIControllerProvider(container: DIContainer) extends ControllerProvider: override def get(surface: Surface): Any = container.getInstance(surface.rawType) override def getFilter(surface: Surface): Any = container.getInstance(surface.rawType) ``` ## Route Matching ### Match Order Routes are matched sequentially in the order they were added: ```scala val router = Router.of[SpecificController] // Matched first .andThen(Router.of[GeneralController]) // Matched second ``` More specific routes should be added before general ones. ### Path Matching Routes match when: 1. HTTP method matches exactly 2. Number of path segments matches 3. Each segment matches (literal or parameter) ```scala // Route: GET /users/:id // Matches: GET /users/123 -> id = "123" // Matches: GET /users/alice -> id = "alice" // No match: GET /users/123/posts (different segment count) // No match: POST /users/123 (different method) ``` ### Path Components Paths are parsed into components: | Pattern | Component Type | Matches | |---------|---------------|---------| | `users` | Literal | Exact string "users" | | `:id` | Parameter | Any string (captured) | ```scala // GET /users/:userId/posts/:postId // Matches: GET /users/1/posts/42 // Extracted: userId = "1", postId = "42" ``` ## Router with Filters Add filters that apply to the routes of a router: ```scala import wvlet.uni.http.RxHttpFilter class AuthFilter extends RxHttpFilter: def apply(request: Request, next: RxHttpHandler): Rx[Response] = if isAuthenticated(request) then next.handle(request) else Rx.single(Response.unauthorized) val router = Router .filter[AuthFilter] .andThen(Router.of[ProtectedController]) ``` The filter is instantiated using the same `ControllerProvider` as controllers. A filter applies only to the routes it wraps, so you can compose public and protected routes, and nest filters. Nested filters run outer-to-inner on the request: ```scala val router = Router .of[PublicController] // no filter .andThen( Router .filter[AuthFilter] // runs first for /protected and /inference .andThen( Router .of[ProtectedController] .andThen(Router.filter[VersionFilter].andThen(Router.of[InferenceController])) ) ) ``` Filters shared by every route also run for requests that match no route. Other filters run after their route is matched. A filter's exception reaches the outer filters as a failed `Rx`. ## RouterHandler The `RouterHandler` connects a router to the Netty server: ```scala import wvlet.uni.http.netty.{RouterHandler, NettyServer} val router = Router.of[MyController] val handler = RouterHandler(router) NettyServer .withPort(8080) .withRxHandler(handler) .start() ``` ### With Custom Provider ```scala val handler = RouterHandler(router, myControllerProvider) ``` ### With Pre-registered Controllers ```scala val handler = RouterHandler.withControllers( router, controller1, controller2 ) ``` ## Request Mapping The `HttpRequestMapper` binds request data to method parameters in this order: 1. **Path parameters** - From URL path (`:id` -> value) 2. **Query parameters** - From URL query string 3. **Default values** - Method parameter defaults 4. **Request object** - If parameter type is `Request` ```scala @Endpoint(HttpMethod.GET, "/search/:category") def search( category: String, // From path: /search/books query: String, // From query: ?query=scala limit: Int = 10, // From query or default request: Request // The full request ): Response = ??? ``` Request: `GET /search/books?query=scala&limit=20` * `category` = "books" * `query` = "scala" * `limit` = 20 * `request` = full Request object --- --- url: https://wvlet.org/uni/http/rpc.md --- # RPC Trait-based RPC framework with automatic route generation, type-safe serialization, and structured error handling. ```scala import wvlet.uni.http.rpc.* // Define a service trait trait UserService: def getUser(id: Long): User def createUser(name: String, email: String): User // Create a router from the implementation val router = RPCRouter.of[UserService](UserServiceImpl()) // Generates: POST /.UserService/getUser, POST /.UserService/createUser ``` ## Defining an RPC Service Define your service API as a Scala trait, then provide an implementation: ```scala case class User(id: Long, name: String, email: String) trait UserService: def getUser(id: Long): User def createUser(name: String, email: String): User def listUsers(limit: Int = 10): Seq[User] class UserServiceImpl extends UserService: def getUser(id: Long): User = User(id, "Alice", "alice@example.com") def createUser(name: String, email: String): User = User(1L, name, email) def listUsers(limit: Int): Seq[User] = Seq(User(1L, "Alice", "alice@example.com")) ``` Methods can return `Rx[T]` for asynchronous responses — the router unwraps the return type automatically: ```scala import wvlet.uni.rx.Rx trait AsyncUserService: def getUser(id: Long): Rx[User] ``` ## Creating an RPC Router `RPCRouter.of[T]` uses compile-time reflection to extract method metadata and build routes: ```scala val impl = UserServiceImpl() val router = RPCRouter.of[UserService](impl) // Inspect generated routes router.routes.foreach { route => println(s"${route.path}") } // /com.example.api.UserService/getUser // /com.example.api.UserService/createUser // /com.example.api.UserService/listUsers // Look up a specific route val route = router.findRoute("getUser") ``` All routes use `POST` with the path format `/{serviceName}/{methodName}`, where `serviceName` is the trait's fully qualified name. Every public method of `T` becomes an endpoint, including methods inherited from your own base traits (`trait UserService extends CrudApi[User]` serves the `CrudApi` methods too). Methods inherited from library traits (`scala.*`, `java.*`, uni's logging mixins such as `LogSupport`) are never exposed. Annotate a method with `@rpcIgnore` to keep it out of the RPC endpoints: ```scala trait UserService: def getUser(id: Long): User @rpcIgnore def cacheStats: String = "..." // callable in-process only ``` `RPCClient` applies the same rules, so client and server agree on the endpoint set. ### Request Format Parameters are sent as JSON in the request body: ```json { "request": { "id": 42 } } ``` Parameter names are matched flexibly — `userId`, `user_id`, and `user-id` all match the same parameter. Missing optional parameters (`Option[T]`) default to `None`, and parameters with default values use those defaults. ## Generating a Client Instead of hand-writing request URLs and JSON, generate a **type-safe client** from the same service trait with the `sbt-uni` plugin. The generated client mirrors the trait's methods, so calls are checked by the compiler. ::: tip JVM build-time only Client generation runs at build time on the JVM via sbt. The *generated* client is ordinary code that compiles for any platform the trait supports. ::: ### Enable the plugin In `project/plugins.sbt`: ```scala addSbtPlugin("org.wvlet.uni" % "sbt-uni" % "2026.1.35") ``` In `build.sbt`, enable `UniPlugin` on the project that should contain the generated client, depend on the project that defines the service trait, and list your generation targets: ```scala lazy val app = project .enablePlugins(UniPlugin) .dependsOn(api) // project containing UserService .settings( uniHttpClients := Seq("com.example.api.UserService:rpc") ) ``` `uniHttpClients` is wired into `Compile / sourceGenerators`, so clients are regenerated and compiled automatically on the next `compile`. Generation runs in-process (no forked JVM) and skips files whose content is unchanged. ### Target spec format Each entry is `"fullyQualifiedTrait:clientType[:targetPackage]"`: | Part | Values | Notes | |------|--------|-------| | `clientType` | `sync`, `async`, `both`, `rpc` | `rpc` is an alias for `sync` | | `targetPackage` | optional | defaults to the trait's package + `.client` | ```scala uniHttpClients := Seq( "com.example.api.UserService:rpc", // SyncClient, default package "com.example.api.OrderService:both:com.example.client" // Sync + Async clients ) ``` Override the output directory with `uniHttpCodegenOutDir` (defaults to `Compile / sourceManaged`). ### Using the generated client Generation produces an `object Client` containing a `SyncClient` and/or `AsyncClient`, each wrapping an [HTTP client](./client). Construct one over a configured client pointed at the server's base URL: ```scala import wvlet.uni.http.Http import com.example.client.UserServiceClient // Sync — methods return the declared types directly val sync = UserServiceClient.SyncClient( Http.client.withBaseUri("http://localhost:8080").newSyncClient ) val user: User = sync.getUser(42) // Async — methods return Rx[...] val async = UserServiceClient.AsyncClient( Http.client.withBaseUri("http://localhost:8080").newAsyncClient ) async.getUser(42).map(user => println(user.name)) ``` Under the hood the generated methods delegate to `RPCClient`, which serializes arguments and decodes responses with [Weaver](../core/weaver) — the same wire format the [RPC router](#creating-an-rpc-router) expects, so client and server stay in sync. ## RPC Status Codes `RPCStatus` provides structured error codes organized by category: | Category | HTTP Status | Retryable | Description | |----------|-------------|-----------|-------------| | `SUCCESS` (S) | 2xx | — | Request completed successfully | | `USER_ERROR` (U) | 4xx | No | Client error | | `INTERNAL_ERROR` (I) | 5xx | Yes | Server error | | `RESOURCE_EXHAUSTED` (R) | 429 | Yes (with backoff) | Rate/quota limits exceeded | ### Common Status Codes ```scala import wvlet.uni.http.rpc.RPCStatus // User errors (not retryable) RPCStatus.INVALID_REQUEST_U1 // 400 - Malformed request RPCStatus.INVALID_ARGUMENT_U2 // 400 - Bad parameter values RPCStatus.NOT_FOUND_U5 // 404 - Resource not found RPCStatus.ALREADY_EXISTS_U6 // 409 - Resource already exists RPCStatus.UNAUTHENTICATED_U13 // 401 - Authentication required RPCStatus.PERMISSION_DENIED_U14 // 403 - Forbidden // Server errors (retryable) RPCStatus.INTERNAL_ERROR_I0 // 500 - Generic server error RPCStatus.UNAVAILABLE_I2 // 503 - Service unavailable RPCStatus.TIMEOUT_I3 // 504 - Timeout // Resource errors (retry with backoff) RPCStatus.EXCEEDED_RATE_LIMIT_R2 // 429 - Rate limited ``` ## Error Handling ### Throwing RPC Errors ```scala import wvlet.uni.http.rpc.{RPCStatus, RPCException} class UserServiceImpl extends UserService: def getUser(id: Long): User = if id <= 0 then throw RPCStatus.INVALID_ARGUMENT_U2 .newException(s"Invalid user id: ${id}") // ... ``` ### RPCException `RPCException` carries a status code and serializes to JSON or MessagePack for wire transport: ```scala val ex = RPCStatus.NOT_FOUND_U5.newException("User not found") // Serialize val json: String = ex.toJson val response = ex.toResponse // HTTP response with status header // Deserialize val recovered = RPCException.fromJson(json) val fromResp = RPCException.fromResponse(response) // Application-specific error codes val appEx = RPCStatus.USER_ERROR_U0 .newException("Insufficient balance", appErrorCode = Some(1001)) ``` ### Stack Traces The error body includes the server-side stack trace as a `stackTrace` array of frames (`className`, `methodName`, `fileName`, `lineNumber`), the same shape as Airframe RPC. `RPCException.fromJson`, `fromMsgPack`, and `fromResponse` restore these frames on the client exception. Stack traces are sent for every status except `UNAUTHENTICATED_U13` and `PERMISSION_DENIED_U14`. They reveal internal class names, file names, and line numbers to clients, so call `noStackTrace` to hide them for a specific error: ```scala throw RPCStatus.INVALID_ARGUMENT_U2.newException("Invalid input").noStackTrace ``` Unexpected (non-`RPCException`) exceptions from a handler are wrapped as `INTERNAL_ERROR_I0` with no message or stack trace, unless the dispatcher was created with `withDetailedErrors(true)`. ## Best Practices 1. **No method overloading** in RPC traits — the router rejects overloaded method names 2. **Use specific status codes** (`NOT_FOUND_U5`) over generic ones (`USER_ERROR_U0`) 3. **Keep service traits in a shared module** so clients can reference the same interface 4. **Use `Rx[T]` return types** for async operations to avoid blocking server threads 5. **Use default parameter values** to maintain backward compatibility when adding new parameters --- --- url: https://wvlet.org/uni/http/openapi.md --- # OpenAPI Generate an [OpenAPI 3.0.3](https://spec.openapis.org/oas/v3.0.3) document from the `@Endpoint` routes of a [Router](./router.md). ## From Code ```scala import wvlet.uni.http.{HttpMethod, Request} import wvlet.uni.http.openapi.{OpenAPI, OpenAPIConfig} import wvlet.uni.http.router.{Endpoint, Router} class UserApi: @Endpoint(HttpMethod.GET, "/v1/users/:id", description = "Get a user by id") def getUser(id: String, verbose: Boolean = false): User = ??? @Endpoint(HttpMethod.POST, "/v1/users") def createUser(request: CreateUserRequest, httpRequest: Request): User = ??? val yaml: String = OpenAPI .of( Router.of[UserApi], OpenAPIConfig() .withTitle("User API") .withVersion("1.0.0") .withSchemaPackagePrefix("com.example.api.") ) .toYAML ``` `OpenAPI.of(Seq(router1, router2), config)` combines several routers. `.toJSON` renders JSON instead of YAML. Both are available on JVM, Scala.js, and Scala Native. | `OpenAPIConfig` | Default | Meaning | |---|---|---| | `withTitle` / `withVersion` | `API` / `0.0.1` | `info.title` and `info.version` | | `withSchemaPackagePrefix(p)` / `noSchemaPackagePrefix()` | none | Strip `p` from schema names (`com.example.api.model.User` → `model.User`); other types use their simple name | | `withErrorResponses(codes*)` | `400, 500` | Error responses every operation refers to, defined once in `components/responses` | ## Mapping * **Paths:** `/v1/users/:id` becomes `/v1/users/{id}`. * **Operations:** `operationId` and `summary` are the method name, so method names must be unique across the routers (a duplicate throws `IllegalArgumentException`). `description` comes from `@Endpoint(description = ...)`, or else from the method's `@description` (`wvlet.uni.mcp.description`, or any annotation named `description` with a `value`, such as Airframe's), and `tags` is the controller's simple name. * **Parameters** follow the router's [request body rules](./server.md#request-body): * path parameters are `in: path`; * a parameter that takes the whole body becomes the `requestBody` (JSON and MessagePack), and the primitives next to it are `in: query`; * otherwise, for POST, PUT, and PATCH the body is an object with one property per parameter. For other methods, primitives are `in: query` and any object parameters form the body object, since the router reads them from the body whatever the method; * `Request` and `RequestAttributes` parameters are not part of the API; * a parameter is required unless it has a default value, is an `Option`, or is a collection or map not marked `@required`. * **Content types:** request bodies list `application/json` and `application/x-msgpack`. Responses never list `application/x-msgpack`, because the router does not produce it; see Responses for the content types they do list. * **Responses:** `200` with the return type's schema (`Rx[A]` and `Option[A]` are unwrapped; an `Option` also lists `204`, the response for `None`). `String` is `text/plain`, `Array[Byte]` is `application/octet-stream`, `Unit` is `204`, and a `Response` return has no schema. * **Schemas:** case classes become `$ref`s into `components/schemas`, including recursive ones. Two types that would get the same schema name (e.g. `a.User` and `b.User` without a package prefix) fail generation with `IllegalArgumentException`; generic types are named like `Page_User_`. `Seq`/`Set`/`Array` are arrays, `Map[String, A]` is an object with `additionalProperties`, and enums are strings (their case names are not listed). Numbers carry a `format` (`int32`, `int64`, `float`, `double`), fields carry their `@description`, and a field's default is emitted as `default` when it is a number (other than `BigInt`/`BigInteger`), `Boolean`, `Char`, or `String`. Class-level `@description` is not emitted. ## With sbt-uni The `uniOpenAPI` task writes the document from the build. It runs the generator in a forked JVM on the project's runtime classpath, so it uses the uni version your app depends on, and the build never loads your classes. ```scala // project/plugins.sbt addSbtPlugin("org.wvlet.uni" % "sbt-uni" % "2026.1.35") // build.sbt lazy val server = project .enablePlugins(UniPlugin) .settings( // Scala object members that return a Router uniOpenAPIRouters := Seq("com.example.api.Routes.router"), uniOpenAPITitle := "Example API", // default: project name uniOpenAPIVersion := version.value, // default uniOpenAPISchemaPackagePrefix := Some("com.example.api."), uniOpenAPIErrorResponses := Seq(400, 500, 503), // default: Seq(400, 500) uniOpenAPIFormat := "yaml", // or "json" uniOpenAPITargetDir := baseDirectory.value / "doc" // default: target/openapi ) ``` ```scala package com.example.api object Routes: val router: Router = Router.of[UserApi].andThen(Router.of[AdminApi]) ``` Run `sbt server/uniOpenAPI` to write `doc/openapi.yaml`. Packaging (`sbt server/package`) also regenerates it; with no `uniOpenAPIRouters` the task does nothing. A router spec can also name an object that is itself a `Router`. --- --- url: https://wvlet.org/uni/http/electron.md --- # Desktop Apps (Electron) Build [Electron](https://www.electronjs.org/) desktop apps with Uni and Scala.js, and let the UI call services in the main process using Uni's [RPC](/http/rpc) — tunneled over Electron's IPC instead of a network socket. No HTTP server, no open ports. The transport lives in the `wvlet.uni.electron` package and is **Scala.js only** (`uni/.js`). A complete, runnable reference app is in [`examples/electron-app`](https://github.com/wvlet/uni/tree/main/examples/electron-app). ::: tip New to this? Start with the [Electron Tutorial](/http/electron-tutorial) — a step-by-step setup from an empty directory. This page is the API reference for what the tutorial wires together. ::: ## How it fits together Electron runs three contexts. Uni occupies two of them; the third is a tiny hand-written bridge: ```mermaid sequenceDiagram participant R as Renderer · Chromium participant P as Preload bridge participant M as Main · Node.js Note over R: ElectronRenderer.install()
Http.client.newAsyncClient R->>P: window.uniRPC.request(payload) P->>M: ipcRenderer.invoke('uni-rpc', payload) Note over M: RPCDispatcher → RPCRouter
your service impl M-->>P: response payload P-->>R: Promise resolves ``` * **Renderer** serializes each RPC request to a plain object and calls the preload bridge. * **Preload** forwards it via `ipcRenderer.invoke` (context isolation stays on). * **Main** dispatches it through the same transport-neutral `RPCDispatcher` that the HTTP server uses, and resolves the response payload. Because the wire format is identical to HTTP RPC, your existing service traits and clients work unchanged. ## The shared service Define the service trait once and share it between both processes. Returning `Rx[A]` keeps calls asynchronous, which suits IPC: ```scala import wvlet.uni.rx.Rx case class CounterState(value: Int) trait CounterApi: def get(): Rx[CounterState] def increment(amount: Int): Rx[CounterState] def reset(): Rx[CounterState] ``` ## Main process Implement the service and register it on Electron's `ipcMain`. Electron objects are passed in as values — this Scala module never has to `require("electron")`: ```scala import wvlet.uni.electron.ElectronRPCServer import wvlet.uni.http.rpc.RPCRouter import wvlet.uni.rx.Rx import scala.scalajs.js import scala.scalajs.js.annotation.JSExportTopLevel class CounterApiImpl extends CounterApi: private var value = 0 def get(): Rx[CounterState] = Rx.single(CounterState(value)) def increment(amount: Int): Rx[CounterState] = { value += amount; Rx.single(CounterState(value)) } def reset(): Rx[CounterState] = { value = 0; Rx.single(CounterState(value)) } object MainProcess: @JSExportTopLevel("wireMainProcess") def wireMainProcess(ipcMain: js.Dynamic): Unit = ElectronRPCServer.serve(ipcMain, RPCRouter.of[CounterApi](CounterApiImpl())) ``` Pass several routers to serve multiple services over the one channel: `ElectronRPCServer.serve(ipcMain, RPCRouter.of[A](a), RPCRouter.of[B](b))`. Electron delivers IPC calls from every frame of every window, including embedded iframes. By default the server accepts calls only from top-level frames; a rejected call fails with `PERMISSION_DENIED_U14`. Restrict callers to your own pages with `ElectronRPCServerConfig`: ```scala import wvlet.uni.electron.ElectronRPCServerConfig ElectronRPCServer.serve( ipcMain, ElectronRPCServerConfig() .withAllowedOrigins("file://", "http://localhost:5173"), // packaged app + Vite dev server RPCRouter.of[CounterApi](CounterApiImpl()) ) ``` An origin matches the sender frame URL as a prefix at a path boundary, so `http://localhost:5173` does not match `http://localhost:51730`. `withSenderFilter(frame => ...)` adds a custom check of the `WebFrameMain`, and `withAllowSubFrames(true)` lets iframes call. The JS main entry hands `ipcMain` over once the app is ready: ```js import { app, ipcMain } from 'electron' import { wireMainProcess } from 'scalajs:main.js' app.whenReady().then(() => { wireMainProcess(ipcMain) // … createWindow() … }) ``` ## Preload bridge The single function the renderer transport expects. Keep context isolation on: ```js import { contextBridge, ipcRenderer } from 'electron' contextBridge.exposeInMainWorld('uniRPC', { request: (payload) => ipcRenderer.invoke('uni-rpc', payload) }) ``` ## Renderer Call `ElectronRenderer.install()` once at startup. Afterward every `Http.client.newAsyncClient` (and any generated RPC `AsyncClient`) sends through the IPC bridge: ```scala import wvlet.uni.electron.ElectronRenderer import wvlet.uni.http.Http import wvlet.uni.http.rpc.RPCClient import wvlet.uni.surface.Surface import scala.scalajs.js.annotation.JSExportTopLevel object RendererApp: private val rpc = RPCClient.build(Surface.of[CounterApi], Surface.methodsOf[CounterApi]) private lazy val client = Http.client.newAsyncClient @JSExportTopLevel("main") def main(): Unit = ElectronRenderer.install() // wires window.uniRPC as the HTTP channel factory rpc.callAsync[CounterState](client, "increment", Seq(1)).run { state => println(s"counter = ${state.value}") } ``` `install()` defaults to `window.uniRPC`; pass a different bridge object if your preload exposes another name: `ElectronRenderer.install(myBridge)`. ::: tip Async only A sandboxed renderer has no synchronous IPC, so the Electron channel implements only the async path. Use `Http.client.newAsyncClient`; `newSyncClient` throws. ::: ## Build pipeline The example uses [`electron-vite`](https://electron-vite.org/) with [`@scala-js/vite-plugin-scalajs`](https://www.scala-js.org/doc/tutorial/scalajs-vite.html). The plugin links a Scala.js sbt project and exposes it to the bundle as `import 'scalajs:main.js'`; its `projectID` selects which project (here, `main` and `renderer`): ```js // electron.vite.config.mjs import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import scalaJSPlugin from '@scala-js/vite-plugin-scalajs' export default defineConfig({ main: { plugins: [externalizeDepsPlugin(), scalaJSPlugin({ cwd: '.', projectID: 'main' })] }, preload: { plugins: [externalizeDepsPlugin()] }, renderer: { plugins: [scalaJSPlugin({ cwd: '.', projectID: 'renderer' })] } }) ``` Both Scala.js projects emit ES modules and export functions (no `main` initializer): ```scala scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) } scalaJSUseMainModuleInitializer := false ``` Then: ```bash pnpm dev # electron-vite: compile Scala.js, start the renderer dev server, launch Electron pnpm package # electron-vite build, then electron-builder → installers in dist/ ``` Packaging is handled by [`electron-builder`](https://www.electron.build/) (dmg / nsis / AppImage). ::: warning JS dependency Uni's Scala.js code uses `java.security.SecureRandom` (via ULID in the serialization layer), so a desktop app pulls in `scalajs-java-securerandom` transitively from `uni`. If you depend on a Uni release older than this feature, add it yourself: ```scala libraryDependencies += ("org.scala-js" %%% "scalajs-java-securerandom" % "1.0.0").cross(CrossVersion.for3Use2_13) ``` ::: See the full project — sbt build, Vite config, preload, packaging — in [`examples/electron-app`](https://github.com/wvlet/uni/tree/main/examples/electron-app). --- --- url: https://wvlet.org/uni/http/electron-tutorial.md --- # Electron Tutorial: Setup from Scratch A step-by-step walkthrough that builds a small **Electron desktop app** with Uni and Scala.js from an empty directory. The UI (renderer) calls a service in the main process using Uni's [RPC](/http/rpc), tunneled over Electron IPC — no HTTP server, no open ports. By the end you'll have a counter window whose **+1 / +10 / Reset** buttons each make a real RPC call to the main process. For the API reference behind each piece, see [Desktop Apps (Electron)](/http/electron). The finished project is in [`examples/electron-app`](https://github.com/wvlet/uni/tree/main/examples/electron-app). ## What you're building Electron runs three contexts; Uni occupies two and a tiny hand-written script bridges the third: ```mermaid sequenceDiagram participant R as Renderer · Scala.js participant P as Preload bridge participant M as Main · Scala.js R->>P: window.uniRPC.request(payload) P->>M: ipcRenderer.invoke('uni-rpc', payload) Note over M: ElectronRPCServer.serve(ipcMain)
RPCRouter.of CounterApi → CounterApiImpl M-->>P: response payload P-->>R: Promise resolves ``` ## Prerequisites * JDK 11+ and `sbt` on your `PATH` (the Scala.js Vite plugin shells out to `sbt`) * Node.js 20+ and `pnpm` (or npm) ## Step 1: the sbt build Three Scala.js modules: a shared `api`, the `main` process, and the `renderer`. All emit ES modules (so Vite can bundle them) and export functions instead of running a `main`: ```scala // build.sbt val uniVersion = sys.props.getOrElse("uni.version", "2026.1.35") val scala3 = "3.9.0" ThisBuild / scalaVersion := scala3 lazy val commonSettings = Seq( scalaVersion := scala3, scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.ESModule) }, scalaJSUseMainModuleInitializer := false, libraryDependencies += "org.wvlet.uni" %%% "uni" % uniVersion ) lazy val api = project.in(file("api")).enablePlugins(ScalaJSPlugin).settings(commonSettings) lazy val main = project.in(file("main")).enablePlugins(ScalaJSPlugin).settings(commonSettings).dependsOn(api) lazy val renderer = project.in(file("renderer")).enablePlugins(ScalaJSPlugin).settings(commonSettings).dependsOn(api) ``` ```scala // project/plugins.sbt addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.22.0") ``` ``` // project/build.properties sbt.version=1.12.13 ``` ## Step 2: the shared service Define the RPC service once; both processes depend on `api`. Returning `Rx[A]` keeps calls asynchronous, which suits IPC: ```scala // api/src/main/scala/example/api/CounterApi.scala package example.api import wvlet.uni.rx.Rx case class CounterState(value: Int) trait CounterApi: def get(): Rx[CounterState] def increment(amount: Int): Rx[CounterState] def reset(): Rx[CounterState] ``` ## Step 3: the main process Implement the service and register it on Electron's `ipcMain`. `ipcMain` is **passed in as a value** — this Scala module never has to `require("electron")`, which keeps it free of bundler coupling: ```scala // main/src/main/scala/example/main/MainProcess.scala package example.main import example.api.{CounterApi, CounterState} import wvlet.uni.electron.ElectronRPCServer import wvlet.uni.http.rpc.RPCRouter import wvlet.uni.rx.Rx import scala.scalajs.js import scala.scalajs.js.annotation.JSExportTopLevel class CounterApiImpl extends CounterApi: private var value = 0 def get(): Rx[CounterState] = Rx.single(CounterState(value)) def increment(amount: Int): Rx[CounterState] = { value += amount; Rx.single(CounterState(value)) } def reset(): Rx[CounterState] = { value = 0; Rx.single(CounterState(value)) } object MainProcess: @JSExportTopLevel("wireMainProcess") def wireMainProcess(ipcMain: js.Dynamic): Unit = ElectronRPCServer.serve(ipcMain, RPCRouter.of[CounterApi](CounterApiImpl())) ``` The Electron main entry (plain JS) creates the window and hands `ipcMain` to the Scala module once the app is ready: ```js // src/main/index.js import { app, BrowserWindow, ipcMain } from 'electron' import { join } from 'node:path' import { wireMainProcess } from 'scalajs:main.js' function createWindow() { const win = new BrowserWindow({ width: 480, height: 380, webPreferences: { // electron-vite emits an ESM preload (.mjs) because package.json is "type": "module". preload: join(__dirname, '../preload/index.mjs'), contextIsolation: true, nodeIntegration: false, // ESM preloads require the sandbox off; the contextBridge boundary still isolates the renderer. sandbox: false } }) if (process.env.ELECTRON_RENDERER_URL) win.loadURL(process.env.ELECTRON_RENDERER_URL) else win.loadFile(join(__dirname, '../renderer/index.html')) } app.whenReady().then(() => { wireMainProcess(ipcMain) // register the RPC services before opening any window createWindow() app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit() }) ``` ## Step 4: the preload bridge The single function the renderer transport expects. Context isolation stays on — the renderer never gets direct Node access: ```js // src/preload/index.js import { contextBridge, ipcRenderer } from 'electron' contextBridge.exposeInMainWorld('uniRPC', { request: (payload) => ipcRenderer.invoke('uni-rpc', payload) }) ``` ## Step 5: the renderer Call `ElectronRenderer.install()` once at startup; afterward every async client (including generated RPC stubs) rides over IPC. Here we build the client directly from the shared trait and render the UI with uni's reactive DOM toolkit (`wvlet.uni.dom`): ```scala // renderer/src/main/scala/example/renderer/RendererApp.scala package example.renderer import example.api.{CounterApi, CounterState} import wvlet.uni.dom.all.* import wvlet.uni.dom.all.given import wvlet.uni.electron.ElectronRenderer import wvlet.uni.http.Http import wvlet.uni.http.rpc.RPCClient import wvlet.uni.rx.{Rx, RxVar} import wvlet.uni.surface.Surface import scala.language.implicitConversions import scala.scalajs.js.annotation.JSExportTopLevel object RendererApp: @JSExportTopLevel("main") def main(): Unit = ElectronRenderer.install() // wire window.uniRPC as the HTTP channel factory CounterUI().renderTo("app") class CounterUI extends RxElement: private val rpc = RPCClient.build(Surface.of[CounterApi], Surface.methodsOf[CounterApi]) private lazy val client = Http.client.newAsyncClient private val count = Rx.variable(0) override def render: RxElement = div( h1("Uni Counter"), p(count.map(_.toString)), // re-renders when count changes button( onclick -> { () => rpc.callAsync[CounterState](client, "increment", Seq(1)).run(show) }, "+1" ) ) private def show(s: CounterState): Unit = count := s.value override def onMount(node: Any): Unit = rpc.callAsync[CounterState](client, "get", Seq.empty).run(show) ``` The renderer HTML loads the Scala.js module and provides the mount point: ```html Uni Electron Counter
``` ```js // src/renderer/index.js import { main } from 'scalajs:main.js' main() ``` ::: tip Styling The example styles the renderer with [Tailwind CSS](https://tailwindcss.com/) v4 via `@tailwindcss/vite`, pointing `@source` at the Scala sources so Tailwind finds the `cls` class strings used in `RendererApp.scala`. It's optional — plain CSS works too. ::: ## Step 6: the build pipeline [`electron-vite`](https://electron-vite.org/) drives the three Vite builds (main / preload / renderer). [`@scala-js/vite-plugin-scalajs`](https://www.scala-js.org/doc/tutorial/scalajs-vite.html) links a Scala.js project and exposes it as `import 'scalajs:main.js'`; its `projectID` picks the sbt project: ```js // electron.vite.config.mjs import { defineConfig, externalizeDepsPlugin } from 'electron-vite' import scalaJSPlugin from '@scala-js/vite-plugin-scalajs' export default defineConfig({ main: { plugins: [externalizeDepsPlugin(), scalaJSPlugin({ cwd: '.', projectID: 'main' })] }, preload: { plugins: [externalizeDepsPlugin()] }, renderer: { plugins: [scalaJSPlugin({ cwd: '.', projectID: 'renderer' })] } }) ``` ```json // package.json { "name": "uni-electron-example", "type": "module", "main": "out/main/index.js", "scripts": { "dev": "electron-vite dev", "build": "electron-vite build", "package": "electron-vite build && electron-builder" }, "devDependencies": { "@scala-js/vite-plugin-scalajs": "^1.0.0", "electron": "^34.0.0", "electron-builder": "^25.1.8", "electron-vite": "^2.3.0", "vite": "^5.4.11" }, "pnpm": { "onlyBuiltDependencies": ["electron", "esbuild"] } } ``` ## Step 7: run and package ```bash pnpm install pnpm dev # compiles Scala.js, starts the renderer dev server, launches Electron (hot reload) ``` Click the buttons — each is an RPC call to the main process, which owns the counter. To ship installers (dmg / nsis / AppImage via [electron-builder](https://www.electron.build/)): ```bash pnpm package # → installers under dist/ ``` ## Gotchas worth knowing * **`scalajs-java-securerandom`** — Uni's Scala.js code reaches `java.security.SecureRandom` (via ULID in the serialization layer). Recent Uni releases pull it in transitively; on older releases add it yourself, or linking fails with "non-existent class java.security.SecureRandom": ```scala libraryDependencies += ("org.scala-js" %%% "scalajs-java-securerandom" % "1.0.0").cross(CrossVersion.for3Use2_13) ``` * **ESM preload path** — under `"type": "module"`, electron-vite emits the preload as `index.mjs`. Point `preload:` at `../preload/index.mjs`, and set `sandbox: false` (ESM preloads don't load in a sandboxed renderer; `contextIsolation` still isolates it). * **Renderer is async-only** — a sandboxed renderer has no synchronous IPC, so use `Http.client.newAsyncClient`. `newSyncClient` throws. ## Where to go next * [Desktop Apps (Electron)](/http/electron) — the transport reference (channels, server, marshaling). * [RPC](/http/rpc) — defining services, generated clients, error handling. * [`examples/electron-app`](https://github.com/wvlet/uni/tree/main/examples/electron-app) — the complete, runnable project this tutorial distills. --- --- url: https://wvlet.org/uni/http/sse.md --- # Server-Sent Events Server-Sent Events (SSE) enable streaming server push over HTTP. Uni provides a model for creating, parsing, and handling SSE streams. ## ServerSentEvent Model ```scala import wvlet.uni.http.ServerSentEvent // Simple data event val event = ServerSentEvent.data("Hello, world!") // Event with type and ID val typed = ServerSentEvent .data("user logged in") .withEvent("login") .withId("evt-001") // Set reconnection interval (milliseconds) val withRetry = ServerSentEvent .data("status update") .withRetry(5000) ``` ### Fields | Field | Type | Description | |-------|------|-------------| | `data` | `String` | Event payload (required). Multiline supported. | | `event` | `Option[String]` | Event type (None if omitted) | | `id` | `Option[String]` | Event identifier for resumption | | `retry` | `Option[Long]` | Reconnection time in milliseconds | ### Wire Format `toContent` converts an event to the SSE wire format: ```scala val event = ServerSentEvent .data("line1\nline2") .withId("123") .withEvent("update") .withRetry(5000) println(event.toContent) // id: 123 // event: update // retry: 5000 // data: line1 // data: line2 // ``` Multiline data is automatically split into separate `data:` lines per the SSE specification. ### Builder Methods All fields support fluent builder methods: ```scala val event = ServerSentEvent.data("payload") .withId("id-1") // Set id .withEvent("type") // Set event type .withRetry(3000) // Set retry interval .withData("new data") // Replace data // Remove optional fields event.noId event.noEvent event.noRetry ``` ## Parsing SSE Streams `ServerSentEventParser` parses the SSE wire format into `ServerSentEvent` objects. ### One-Shot Parsing ```scala import wvlet.uni.http.ServerSentEventParser val raw = """id: 1 event: update data: hello data: world """ val events = ServerSentEventParser.parse(raw) // Seq( // ServerSentEvent(id = Some("1"), event = Some("update"), data = "hello"), // ServerSentEvent(data = "world") // ) ``` ### Incremental Parsing For streaming data, create a parser instance and feed chunks as they arrive: ```scala import wvlet.uni.http.ServerSentEventParser val parser = ServerSentEventParser() // Feed partial data — returns complete events found so far val events1 = parser.feed("data: hel") // Seq() — incomplete val events2 = parser.feed("lo\n\n") // Seq(ServerSentEvent(data = "hello")) // Feed more data val events3 = parser.feed("data: world\n\n") // Flush at end of stream to emit any pending event val last: Option[ServerSentEvent] = parser.flush() // Reset to reuse the parser parser.reset() ``` The parser handles comments (lines starting with `:`), multi-line `data:` fields, and unknown fields gracefully. ## Best Practices 1. **Use `id` for resumable streams** — clients can send `Last-Event-ID` to resume after disconnection 2. **Set `retry`** to control client reconnection behavior 3. **Use `event` types** to multiplex different message kinds on a single stream 4. **Keep payloads small** — SSE is text-based; for large binary data, send a reference and fetch separately --- --- url: https://wvlet.org/uni/http/websocket.md --- # WebSocket Client uni includes a cross-platform WebSocket **client** that runs on the JVM, Scala.js, and Scala Native. It connects to `ws://` / `wss://` endpoints and uses the same `WebSocketHandler` / `WebSocketContext` abstraction as the [server-side WebSocket](./server#websocket) support, so handler code is symmetric on both ends. ## Connecting `Http.webSocketClient` returns the platform's `WebSocketClient`. Call `connect` with the URL and a handler; it returns an `Rx[WebSocketContext]` that emits the open context once the handshake completes (or fails with the handshake error): ```scala import wvlet.uni.http.{Http, WebSocketContext, WebSocketHandler} val handler = new WebSocketHandler: override def onTextMessage(ctx: WebSocketContext, message: String): Unit = println(s"received: ${message}") Http.webSocketClient .connect("ws://localhost:8080/ws/echo", handler) .subscribe { ctx => ctx.send("hello") // connection is open here } ``` The handler's `onOpen(ctx)` fires for the same event, so you can also do the initial send there instead of in `subscribe`. ## Handler Callbacks The client implements the same `WebSocketHandler` as the server. Every callback has a no-op default, so override only what you need: | Callback | When it fires | |----------|---------------| | `onOpen(ctx)` | the handshake completed | | `onTextMessage(ctx, message)` | a text message arrived | | `onBinaryMessage(ctx, message)` | a binary message arrived (`Array[Byte]`) | | `onClose(ctx)` | the connection closed; delivered exactly once | | `onError(ctx, e)` | a **handler callback** threw | ::: tip Errors vs. close Transport/protocol failures map to `onClose`, not `onError` — `onError` is reserved for exceptions thrown inside your own callbacks. A failure *before* the connection opens surfaces through the `connect` stream (as an `Rx` error). ::: ## Sending and Closing The `WebSocketContext` controls the connection; its `send`/`close` methods are safe to call from any thread: ```scala ctx.request // the original HTTP upgrade Request ctx.send("hello") // text frame ctx.send(Array[Byte](1, 2, 3)) // binary frame ctx.close() // normal closure (1000) ctx.close(1001, "going away") // close with a status code and reason ``` ## Heartbeat (ping/pong) A client-side ping/pong heartbeat detects a dead-but-idle connection. It is **off by default** and enabled by passing a ping interval (in milliseconds) as the third argument to `connect`: ```scala // Ping an idle connection every 30s; close it if the peer stops responding. Http.webSocketClient.connect("wss://example.com/ws", handler, pingIntervalMillis = 30000) ``` When enabled, an idle interval sends a `Ping`; if the next interval is still idle with the ping unanswered, the client closes the connection (status `1011`, "ping timeout"). Any inbound frame proves liveness and clears a pending ping. ::: warning Not available on Scala.js The browser/Node global `WebSocket` API cannot send protocol pings, so `pingIntervalMillis` is **ignored on Scala.js**. Use it on JVM/Native, or implement application-level keepalive messages if you need liveness detection in the browser. ::: ## Platform Support | Platform | Backend | Transport | Notes | |----------|---------|-----------|-------| | **JVM** | `JavaWebSocketClient` | `java.net.http.WebSocket` (Java 11+) | `ws://` and `wss://`; heartbeat supported | | **Scala.js** | `JSWebSocketClient` | global `WebSocket` | browsers or **Node.js ≥ 22**; `pingIntervalMillis` ignored | | **Scala Native** | `NativeWebSocketClient` | raw POSIX socket | `ws://` only (no TLS); host must be an IP or `localhost` (no DNS); max frame 1 MB | `Http.webSocketClient` resolves to the right backend automatically. On a platform without WebSocket support the base client throws `NotImplementedError`, but all three shipped platforms provide a backend. ## See Also * [Server-side WebSocket](./server#websocket) — register `WebSocketHandler` routes on a Netty server (JVM). * [Server-Sent Events](./sse) — one-way server→client streaming when you don't need a bidirectional channel. --- --- url: https://wvlet.org/uni/http/retry.md --- # HTTP Retry Strategies uni's HTTP client includes automatic retry for transient failures. ## Default Retry Behavior Retry is enabled by default with sensible defaults: ```scala val client = Http.client.newSyncClient // Automatically retries on transient failures val response = client.send(request) ``` ## Configuring Retries ```scala // Set max retry count val client = Http.client .withConfig(HttpClientConfig().withMaxRetries(5)) .newSyncClient // Or per-request val response = client.withMaxRetry(3).send(request) ``` ## Disabling Retry ```scala // Disable for specific request val response = client.noRetry.send(request) // Or globally val config = HttpClientConfig().withMaxRetries(0) ``` ## Retryable Conditions The HTTP client automatically retries on: | Condition | Description | |-----------|-------------| | Connection errors | Network unreachable, connection refused | | Timeouts | Connect or read timeout | | 429 Too Many Requests | Rate limiting | | 503 Service Unavailable | Temporary outage | | 502 Bad Gateway | Upstream errors | | 504 Gateway Timeout | Upstream timeout | ## Non-Retryable Conditions The following are not retried: | Condition | Description | |-----------|-------------| | 4xx Client errors | 400, 401, 403, 404, etc. (except 429) | | 5xx Server errors | 500, 501 (except 502, 503, 504) | | SSL errors | Certificate validation failures | ## Exception Classification ```scala import wvlet.uni.http.HttpExceptionClassifier // Check if an exception is retryable HttpExceptionClassifier.isRetryable(exception) match case true => // Will be retried case false => // Will not be retried ``` ## Backoff Strategy The HTTP client uses exponential backoff: 1. First retry: ~100ms 2. Second retry: ~200ms 3. Third retry: ~400ms 4. ...with jitter to prevent thundering herd ## Handling Max Retry Exceeded ```scala import wvlet.uni.http.HttpMaxRetryException try val response = client.send(request) catch case e: HttpMaxRetryException => logger.error(s"Failed after ${e.retryCount} retries") logger.error(s"Last error: ${e.lastException.getMessage}") // Use fallback or fail ``` ## Custom Retry Logic For more control, use the control module: ```scala import wvlet.uni.control.Retry val response = Retry .withBackoff(maxRetry = 10) .withInitialInterval(500) .retryOn { case _: IOException => true case e: HttpException if e.status.code == 429 => true case _ => false } .run { client.noRetry.send(request) } ``` ## Rate Limiting Handle rate limits gracefully: ```scala import wvlet.uni.control.Retry val response = Retry .withBackoff(maxRetry = 5) .beforeRetry { (e, retryCount) => e match case HttpException(status, _) if status.code == 429 => // Wait longer for rate limits Thread.sleep(retryCount * 1000) case _ => () } .run { client.send(request) } ``` ## Combining with Circuit Breaker ```scala import wvlet.uni.control.CircuitBreaker val breaker = CircuitBreaker(maxFailures = 5) val response = breaker.protect { client.send(request) // Has built-in retry } ``` ## Best Practices 1. **Use default retry** for most cases 2. **Disable for idempotent checks** like auth validation 3. **Monitor retry rates** to detect service issues 4. **Set reasonable limits** to avoid long waits 5. **Log retry attempts** for debugging --- --- url: https://wvlet.org/uni/mcp.md --- # Build Your Own MCP Server (stdio) `wvlet.uni.mcp` turns a plain Scala service trait into an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server: every public method becomes a **tool** that AI agents (Claude Code, Claude Desktop, and other MCP clients) can discover and call. The server speaks JSON-RPC 2.0 over **stdio** and runs on all three platforms: * **JVM** — run with `sbt run` or package a jar * **Scala Native** — ship a single self-contained binary (no JVM, instant startup) * **Scala.js** — run on Node.js No extra dependency is needed beyond `uni` itself. ## 1. Create a project ```scala // build.sbt scalaVersion := "3.9.0" libraryDependencies += "org.wvlet.uni" %% "uni" % "2026.1.35" ``` ## 2. Define a service trait Any trait works. Method parameters and return values are (de)serialized with uni's [Weaver](/core/weaver), so primitives, `Option`, `Seq`, `Map`, and case classes are all supported. Add `@description` annotations — Surface does not capture Scaladoc, so this is how tool and parameter descriptions reach the client: ```scala import wvlet.uni.mcp.description case class Forecast(city: String, temperature: Double, condition: String) trait WeatherService: @description("Return the current weather forecast for a city") def forecast( @description("City name, e.g. Tokyo") city: String, @description("Temperature unit") unit: String = "celsius" ): Forecast class WeatherServiceImpl extends WeatherService: def forecast(city: String, unit: String): Forecast = Forecast(city, 22.5, "sunny") ``` ## 3. Start the server ```scala import wvlet.uni.mcp.MCPServer @main def start(): Unit = MCPServer() .withName("weather") .withVersion("0.1.0") .withTools[WeatherService](WeatherServiceImpl()) .serveStdio() ``` That's the whole server. `serveStdio()` reads newline-delimited JSON-RPC messages from stdin and writes responses to stdout. On JVM and Scala Native it blocks until the client closes stdin; on Scala.js (Node.js) it returns immediately and the server runs on the event loop. ::: tip Logs go to stderr MCP reserves stdout for protocol messages. `serveStdio()` routes uni's logging to stderr (on Scala.js it replaces the default `console.log` handler), so `info(...)` / `debug(...)` calls in your tools are safe. ::: ## How methods become tools * Every **public method** of the registered trait is exposed as a tool named after the method. Tool names must match `[a-zA-Z0-9_-]{1,128}` and be unique across all registered services — violations fail at `withTools` time. * The tool's **input schema** is derived from the method signature: | Scala type | JSON Schema | |------------|-------------| | `Int`, `Long`, `Short`, `Byte`, `BigInt` | `integer` | | `Float`, `Double` | `number` | | `Boolean` | `boolean` | | `String`, `Char`, enums, `Instant`, `UUID`, `ULID` | `string` | | `Seq[A]`, `Set[A]`, `Array[A]` | `array` with `items` | | `Map[String, A]` | `object` with `additionalProperties` | | case classes | nested `object` schemas | * Parameters with a **default value** or an **`Option` type** are optional; everything else is listed in `required`. * A method returning `Rx[A]` is awaited and serialized like `A` — use it for async tools. * Results are returned to the client as text content: `String` results as plain text, everything else as JSON. Exceptions thrown by a tool become `isError: true` results; malformed arguments are reported as JSON-RPC `-32602` errors. Multiple services can be registered on one server with repeated `withTools[...]` calls. ## 4. Package it **JVM** — point the client at sbt (simplest during development): ```json { "command": "sbt", "args": ["--error", "run"] } ``` **Scala Native** — build a single binary. With `sbt-scala-native` enabled (`enablePlugins(ScalaNativePlugin)`): ``` sbt> nativeLink ``` The linked binary (path is printed by sbt) is a self-contained executable — copy it anywhere and use it as the `command` directly. This is the best distribution story: no JVM, no Node, millisecond startup. **Scala.js (Node.js)** — with `sbt-scalajs` enabled and `scalaJSUseMainModuleInitializer := true`: ``` sbt> fullLinkJS ``` Run the emitted script (path printed by sbt) with `node`. ## 5. Register with an MCP client Any MCP client can launch the server; each has its own registration flow. **Claude Code** — register the command with the `claude mcp add` CLI: ``` claude mcp add weather -- /path/to/weather-server ``` To share the server with everyone working on a repository, use the project scope (`claude mcp add --scope project ...`), which records it in a `.mcp.json` file at the project root: ```json { "mcpServers": { "weather": { "type": "stdio", "command": "/path/to/weather-server" } } } ``` **Claude Desktop** — add the same `mcpServers` entry to `claude_desktop_config.json` (Settings → Developer → Edit Config). Other clients (VS Code, Cursor, ...) have equivalent settings; consult their documentation for the file location and exact schema. ## 6. Try it out The MCP Inspector gives you an interactive UI to list and call your tools: ``` npx @modelcontextprotocol/inspector /path/to/weather-server ``` Or drive a session by hand — the protocol is just JSON lines: ``` $ ./weather-server {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}} {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"tools":{}},"serverInfo":{"name":"weather","version":"0.1.0"}}} {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"forecast","arguments":{"city":"Tokyo"}}} {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"city\":\"Tokyo\",\"temperature\":22.5,\"condition\":\"sunny\"}"}],"isError":false}} ``` ## HTTP transport The same server can speak MCP's **Streamable HTTP** transport: one endpoint that answers each POSTed JSON-RPC message with `application/json` (202 for notifications). `httpHandler` exposes it as a standard uni [`RxHttpHandler`](/http/server), so it mounts on the HTTP server of every platform: ```scala val mcp = MCPServer() .withName("weather") .withTools[WeatherService](WeatherServiceImpl()) // JVM (uni-netty) NettyServer.withPort(8080).withRxHandler(mcp.httpHandler).start() // Scala.js (Node.js) NodeServer.withPort(8080).withRxHandler(mcp.httpHandler).start() // Scala Native NativeServer.withPort(8080).withRxHandler(mcp.httpHandler).start() ``` On the JVM, `NettyServer` lives in the `uni-netty` module — add it to your build (Scala.js and Scala Native servers are included in `uni` itself): ```scala libraryDependencies += "org.wvlet.uni" %% "uni-netty" % "2026.1.35" ``` Register an HTTP server by its URL instead of a command — in Claude Code: ``` claude mcp add --transport http weather http://localhost:8080/mcp ``` or in a project-scope `.mcp.json`: ```json { "mcpServers": { "weather": { "type": "http", "url": "http://localhost:8080/mcp" } } } ``` ::: warning Origin checking As required by the MCP spec (DNS-rebinding protection), when a request carries an `Origin` header only localhost origins are accepted by default. Serving browser-based clients from another origin requires registering it explicitly: ```scala MCPServer().withAllowedOrigins("https://app.example.com") ``` ::: The server is stateless: no `Mcp-Session-Id` is issued, and since a tools-only server never initiates messages, the optional SSE streaming and GET event stream are not offered (GET returns 405). ## Scope and roadmap The current implementation covers the MCP **tools** capability over the **stdio** and **Streamable HTTP** transports (protocol version `2025-06-18`; `2025-03-26` clients are also accepted). Resources, prompts, and SSE streaming for server-initiated notifications are planned as follow-ups. Under the hood, dispatch reuses uni's transport-neutral [RPC](/http/rpc) layer, so behavior (parameter matching, default values, error statuses) is identical to uni RPC. --- --- url: https://wvlet.org/uni/rx.md --- # Reactive Streams uni provides a lightweight reactive streams implementation for handling asynchronous data flows. ## Overview | Component | Description | |-----------|-------------| | [Basics](./basics) | Creating and subscribing to streams | | [Operators](./operators) | Transforming and combining streams | ## What is Rx? Rx (Reactive Extensions) provides a way to work with asynchronous data streams using functional operators like `map`, `filter`, and `flatMap`. ```scala import wvlet.uni.rx.Rx // Create a stream of values val numbers = Rx.fromSeq(1 to 10) // Transform with operators val result = numbers .filter(_ % 2 == 0) // Keep even numbers .map(_ * 10) // Multiply by 10 .subscribe(println) // Print: 20, 40, 60, 80, 100 ``` ## Key Concepts ### Rx\[A] A stream that emits values of type `A`: ```scala val stream: Rx[Int] = Rx.fromSeq(1 to 5) ``` ### RxVar\[A] A mutable variable that emits updates: ```scala val counter = Rx.variable(0) counter.subscribe(println) counter := 1 // Emits 1 counter := 2 // Emits 2 ``` ### RxOption\[A] A stream of optional values: ```scala val maybeValue = Rx.option(Some("hello")) maybeValue.subscribe { case Some(v) => println(v) case None => println("empty") } ``` ## Quick Examples ### Interval Timer ```scala import java.util.concurrent.TimeUnit val ticker = Rx.interval(1, TimeUnit.SECONDS) .take(5) .subscribe(i => println(s"Tick: ${i}")) // Prints: Tick: 0, Tick: 1, ... Tick: 4 ``` ### Combining Streams ```scala val a = Rx.variable(1) val b = Rx.variable(2) a.join(b) .map((x, y) => x + y) .subscribe(sum => println(s"Sum: ${sum}")) a := 10 // Prints: Sum: 12 b := 20 // Prints: Sum: 30 ``` ### Error Handling ```scala Rx.single(riskyOperation()) .recover { case _: NetworkError => fallbackValue } .subscribe(handleResult) ``` ## Package ```scala import wvlet.uni.rx.* ``` --- --- url: https://wvlet.org/uni/rx/basics.md --- # Rx Basics Learn the fundamentals of reactive streams in uni. ## Creating Streams ### Single Value ```scala import wvlet.uni.rx.Rx // Immediate value val single = Rx.single(42) // Lazy evaluation val lazy = Rx.single { println("Computing...") expensiveComputation() } ``` ### From Sequence ```scala val numbers = Rx.fromSeq(1 to 10) val items = Rx.sequence("a", "b", "c") ``` ### Empty Stream ```scala val empty = Rx.empty[Int] ``` ### From Try ```scala import scala.util.{Success, Failure} val fromSuccess = Rx.fromTry(Success(42)) val fromFailure = Rx.fromTry(Failure(Exception("error"))) ``` ### From Future ```scala import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global val fromFuture = Rx.future(Future { Thread.sleep(100) "result" }) ``` ## Reactive Variables ### Creating Variables ```scala val counter = Rx.variable(0) val name = Rx.variable("Alice") ``` ### Updating Variables ```scala counter := 5 // Set to 5 counter.update(_ + 1) // Increment ``` ### Subscribing to Changes ```scala counter.subscribe { value => println(s"Counter is now: ${value}") } counter := 1 // Prints: Counter is now: 1 counter := 2 // Prints: Counter is now: 2 ``` ## Subscribing to Streams ### Basic Subscription ```scala val cancelable = Rx.fromSeq(1 to 5).subscribe { value => println(value) } ``` ### Canceling Subscriptions ```scala val cancelable = stream.subscribe(println) // Later... cancelable.cancel() ``` ### Running to Completion ```scala // Run and handle each value stream.run { value => processValue(value) } ``` ### Awaiting Result (JVM only) ```scala val result = Rx.single(computeValue()).await ``` ## Stream Events Streams emit three types of events: 1. **OnNext(value)** - A value is emitted 2. **OnError(exception)** - An error occurred 3. **OnCompletion** - Stream finished ```scala import wvlet.uni.rx.{OnNext, OnError, OnCompletion} stream.run { case OnNext(value) => println(s"Value: ${value}") case OnError(e) => println(s"Error: ${e.getMessage}") case OnCompletion => println("Done") } ``` ## Timers and Intervals ### One-time Delay ```scala import java.util.concurrent.TimeUnit // Emit after 1 second val delayed = Rx.timer(1, TimeUnit.SECONDS) delayed.subscribe(_ => println("Timer fired!")) ``` ### Periodic Interval ```scala // Emit every 500ms val ticker = Rx.interval(500, TimeUnit.MILLISECONDS) .take(10) // Limit to 10 emissions .subscribe(i => println(s"Tick ${i}")) ``` ## Option Streams ### Creating RxOption ```scala val maybeValue = Rx.option(Some(42)) val noValue = Rx.none ``` ### Converting to RxOption ```scala val optionStream = Rx.variable[Option[Int]](None).toOption ``` ## Queues Push values manually: ```scala val queue = Rx.queue[Int]() queue.subscribe(println) queue.add(1) // Prints: 1 queue.add(2) // Prints: 2 queue.add(3) // Prints: 3 ``` ## Best Practices 1. **Cancel subscriptions** when no longer needed 2. **Use variables** for mutable state 3. **Prefer lazy evaluation** with `Rx.single` 4. **Handle errors** with `recover` 5. **Limit infinite streams** with `take` --- --- url: https://wvlet.org/uni/rx/operators.md --- # Stream Operators uni provides a rich set of operators for transforming and combining reactive streams. ## Transformation ### map Transform each value: ```scala val doubled = numbers.map(_ * 2) ``` ### flatMap Transform to another stream: ```scala val expanded = numbers.flatMap(n => Rx.fromSeq(1 to n)) ``` ### filter Keep matching values: ```scala val evens = numbers.filter(_ % 2 == 0) ``` ### transform Handle both success and failure: ```scala val handled = stream.transform { case Success(v) => processValue(v) case Failure(e) => handleError(e) } ``` ## Combining Streams ### zip Combine when both emit (waits for pairs): ```scala val a = Rx.fromSeq(1 to 3) val b = Rx.fromSeq("a", "b", "c") a.zip(b).subscribe { (num, letter) => println(s"${num}-${letter}") } // 1-a, 2-b, 3-c ``` ### join Combine on any change (latest values): ```scala val x = Rx.variable(1) val y = Rx.variable(2) x.join(y).subscribe { (a, b) => println(s"${a} + ${b} = ${a + b}") } x := 10 // Prints: 10 + 2 = 12 y := 20 // Prints: 10 + 20 = 30 ``` ### concat Chain streams sequentially: ```scala val first = Rx.fromSeq(1 to 3) val second = Rx.fromSeq(4 to 6) first.concat(second).subscribe(println) // 1, 2, 3, 4, 5, 6 ``` ## Error Handling ### recover Replace errors with values: ```scala val safe = risky.recover { case _: NetworkError => defaultValue } ``` ### recoverWith Replace errors with another stream: ```scala val withFallback = primary.recoverWith { case _: NetworkError => fallbackStream } ``` ### transformFailure Transform specific errors: ```scala val mapped = stream.transformFailure { case e: IOException => new NetworkException(e) } ``` ## Flow Control ### take Limit emissions: ```scala val first5 = stream.take(5) ``` ### throttleFirst Emit first in time window: ```scala import java.util.concurrent.TimeUnit // Prevent double-clicks val throttled = clicks.throttleFirst(500, TimeUnit.MILLISECONDS) ``` ### throttleLast / sample Emit last in time window: ```scala val sampled = stream.sample(100, TimeUnit.MILLISECONDS) ``` ### timeout Fail if no value in time: ```scala val withTimeout = stream.timeout(5, TimeUnit.SECONDS) ``` ## Caching ### cache Cache the last value: ```scala val cached = expensive.cache cached.subscribe(println) // Computes cached.subscribe(println) // Uses cached ``` ### expireAfterWrite TTL cache: ```scala val ttlCache = stream.cache.expireAfterWrite(5, TimeUnit.MINUTES) ``` ## Side Effects ### tap Observe without modifying: ```scala val logged = stream.tap(v => logger.debug(s"Value: ${v}")) ``` ### tapOn Observe with Try: ```scala val logged = stream.tapOn { case Success(v) => logger.info(s"Success: ${v}") case Failure(e) => logger.error("Failed", e) } ``` ### tapOnFailure Observe only errors: ```scala val logged = stream.tapOnFailure { e => logger.error("Error occurred", e) } ``` ## Starting Values ### startWith Prepend a value: ```scala val withDefault = stream.startWith(0) ``` ### startWith (Seq) Prepend multiple values: ```scala val withDefaults = stream.startWith(Seq(-2, -1, 0)) ``` ## Aggregation ### lastOption Get the last value as Option: ```scala val last: RxOption[Int] = stream.lastOption ``` ### toSeq (JVM only) Collect all values: ```scala val all: Seq[Int] = Rx.fromSeq(1 to 10).toSeq ``` ## Operator Chaining Combine operators fluently: ```scala val result = Rx.interval(100, TimeUnit.MILLISECONDS) .take(100) .filter(_ % 2 == 0) .map(_ * 10) .throttleLast(500, TimeUnit.MILLISECONDS) .tap(v => logger.debug(s"Processing: ${v}")) .recover { case _ => -1 } .cache ``` ## Best Practices 1. **Order matters** - filter before map when possible 2. **Use throttle** for UI events 3. **Cache expensive** computations 4. **Handle errors** at appropriate levels 5. **Clean up** subscriptions to avoid leaks --- --- url: https://wvlet.org/uni/rx/concurrency.md --- # Rx Concurrency Lightweight concurrency primitives for Rx, inspired by cats-effect. All primitives are non-blocking and work across JVM, Scala.js, and Scala Native. ## RxRef Atomic mutable reference with lock-free compare-and-swap semantics. Unlike `Rx.variable`, RxRef does **not** emit change events to subscribers — it is purely a concurrent state container. ```scala import wvlet.uni.rx.{Rx, RxRef} val ref = RxRef(0) // Basic get/set ref.get // Rx[Int] ref.set(42) // Rx[Unit] // Atomic update ref.update(_ + 1) // Rx[Unit] — retries on contention // Get previous value while setting new ref.getAndSet(10) // Rx[Int] — returns old value // Get and update atomically ref.getAndUpdate(_ * 2) // Rx[Int] — returns old value ref.updateAndGet(_ * 2) // Rx[Int] — returns new value // Atomic modify with result extraction ref.modify { current => val next = current + 1 (next, s"was ${current}") // (newState, result) } // Rx[String] // Compare-and-set ref.compareAndSet(expected = 10, newValue = 20) // Rx[Boolean] // Synchronous access (use with caution) ref.unsafeGet // Int ``` ## RxDeferred Single-value synchronization primitive. A `RxDeferred` starts empty and can be completed exactly once. Callers of `get` suspend until a value is available. ```scala import wvlet.uni.rx.{Rx, RxDeferred} val deferred = RxDeferred[String]() // Complete with a value (returns true on first completion) deferred.complete("done") // Rx[Boolean] // Get the value (suspends until completed) deferred.get // Rx[String] // Non-blocking check deferred.tryGet // Rx[Option[String]] deferred.isCompleted // Rx[Boolean] // Complete with an error deferred.completeWithError(Exception("failed")) // Rx[Boolean] ``` ### Example: Coordinating Async Work ```scala val result = RxDeferred[Int]() // Producer val producer = Rx { val value = computeExpensiveResult() result.complete(value) } // Consumer (suspends until producer completes) val consumer = result.get.map { value => println(s"Got result: ${value}") } ``` ## RxFiber Lightweight fiber for concurrent computation. Fibers are much cheaper than OS threads and are managed by the Rx scheduler. ```scala import wvlet.uni.rx.{Rx, RxFiber} // Fork a computation val fiber: Rx[RxFiber[Int]] = Rx { expensiveComputation() }.start // Wait for result fiber.flatMap(_.join) // Rx[Int] // Cancel a fiber fiber.flatMap(_.cancel) // Rx[Unit] // Non-blocking poll fiber.flatMap(_.poll) // Rx[Option[Try[Int]]] // Check cancellation fiber.flatMap(_.isCancelled) // Rx[Boolean] ``` ### Extension Methods ```scala import wvlet.uni.rx.Rx val computation = Rx { heavyWork() } // Fork to default scheduler computation.start // Rx[RxFiber[A]] // Fork to a specific scheduler computation.startOn(scheduler) // Rx[RxFiber[A]] // Fire and forget computation.startAndForget // Rx[Unit] // Run on blocking scheduler (for file/network I/O) computation.evalOnBlocking // Rx[A] ``` ## RxSemaphore Permit-based concurrency control. Limits the number of concurrent operations without blocking OS threads. ```scala import wvlet.uni.rx.RxSemaphore // Create with 3 permits val sem = RxSemaphore(3) // Bracket pattern (recommended) sem.withPermit { Rx { accessSharedResource() } } // Rx[A] — acquire before, release after (even on error) // Multiple permits sem.withPermitN(2) { Rx { heavyOperation() } } // Manual acquire/release sem.acquire // Rx[Unit] — suspends if no permits available sem.release // Rx[Unit] // Non-blocking sem.tryAcquire // Rx[Boolean] // Inspection sem.available // Rx[Long] — current available permits sem.waiting // Rx[Int] — number of waiting acquires ``` ### Example: Rate-Limiting Concurrent Requests ```scala val limiter = RxSemaphore(5) // Max 5 concurrent requests def fetchUrl(url: String): Rx[String] = limiter.withPermit { Rx { httpClient.get(url) } } // All URLs are fetched with at most 5 concurrent requests val results = urls.map(fetchUrl) ``` ## RxScheduler Platform-specific execution environment for Rx computations. ```scala import wvlet.uni.rx.RxScheduler // Platform-dependent default scheduler val default = RxScheduler.default // Blocking I/O scheduler (cached thread pool) val blocking = RxScheduler.blocking // Custom parallelism val custom = RxScheduler(parallelism = 4) // Schedule delayed execution val cancel = default.schedule(1000, java.util.concurrent.TimeUnit.MILLISECONDS) { println("delayed!") } // Periodic execution val periodic = default.scheduleAtFixedRate( initialDelay = 0, period = 5000, unit = java.util.concurrent.TimeUnit.MILLISECONDS ) { println("tick") } // Cancel scheduled tasks cancel.cancel() periodic.cancel() ``` ### Platform Behavior | Platform | Default Scheduler | Blocking Scheduler | |----------|------------------|--------------------| | JVM | Work-stealing thread pool | Cached thread pool | | Scala.js | Microtask queue | Same (single-threaded) | | Scala Native | Fixed-size thread pool | Fixed-size thread pool | ## Best Practices 1. **Use `withPermit`** over manual acquire/release — it guarantees cleanup on errors 2. **Use `RxDeferred`** for one-time signaling between producers and consumers 3. **Use `evalOnBlocking`** for file I/O, network calls, and other blocking operations 4. **Keep fibers lightweight** — avoid long-running blocking calls inside fibers 5. **Use `RxRef`** for shared mutable state; use `Rx.variable` when you need change notifications --- --- url: https://wvlet.org/uni/cli.md --- # CLI Utilities uni provides utilities for building command-line applications with styled output, progress indicators, and command launching. ## Overview | Component | Description | |-----------|-------------| | [Terminal Styling](./tint) | Colorful terminal output with Tint | | [Progress Indicators](./progress) | Progress bars and spinners | | [Command Launcher](./launcher) | Type-safe CLI argument parsing | ## Quick Start ### Colored Output ```scala import wvlet.uni.cli.Tint println(Tint.green("Success!")) println(Tint.red.bold("Error!")) println(Tint.yellow.bgBlue("Warning")) ``` ### Progress Bar ```scala import wvlet.uni.cli.ProgressBar val bar = ProgressBar(total = 100).start() for i <- 1 to 100 do Thread.sleep(50) bar.increment() bar.finish() ``` ### Command Launcher ```scala import wvlet.uni.cli.launcher.* case class MyApp( @option(prefix = "-n,--name", description = "User name") name: String = "world", @option(prefix = "-v,--verbose", description = "Enable verbose output") verbose: Boolean = false ) // Parse command-line arguments val app = Launcher.execute[MyApp](args) if app.verbose then println(s"Running with name: ${app.name}") println(s"Hello, ${app.name}!") ``` ## Features * **Cross-Platform** - Works on JVM and Scala Native * **Automatic Detection** - Detects terminal capabilities * **Fluent API** - Method chaining for styling * **Type-Safe** - Compile-time argument validation ## Package ```scala import wvlet.uni.cli.* import wvlet.uni.cli.launcher.* ``` --- --- url: https://wvlet.org/uni/cli/tint.md --- # Terminal Styling Tint provides a fluent API for styling terminal text with colors and modifiers. ## Basic Usage ```scala import wvlet.uni.cli.Tint // Single color println(Tint.red("Error message")) println(Tint.green("Success!")) println(Tint.blue("Information")) // With modifiers println(Tint.red.bold("Bold red")) println(Tint.yellow.underline("Underlined")) println(Tint.cyan.italic("Italic text")) ``` ## Colors ### Foreground Colors ```scala Tint.black("text") Tint.red("text") Tint.green("text") Tint.yellow("text") Tint.blue("text") Tint.magenta("text") Tint.cyan("text") Tint.white("text") Tint.gray("text") ``` ### Bright Colors ```scala Tint.brightRed("text") Tint.brightGreen("text") Tint.brightYellow("text") Tint.brightBlue("text") Tint.brightMagenta("text") Tint.brightCyan("text") Tint.brightWhite("text") ``` ### Background Colors ```scala Tint.bgRed("text") Tint.bgGreen("text") Tint.bgYellow("text") Tint.bgBlue("text") Tint.bgMagenta("text") Tint.bgCyan("text") Tint.bgWhite("text") ``` ## Text Modifiers ```scala Tint.bold("Bold text") Tint.dim("Dimmed text") Tint.italic("Italic text") Tint.underline("Underlined text") Tint.strikethrough("Strikethrough") Tint.inverse("Inverted colors") Tint.hidden("Hidden text") ``` ## Chaining Styles Combine multiple styles: ```scala Tint.red.bold("Red and bold") Tint.green.underline("Green underlined") Tint.yellow.bgBlue.bold("Yellow on blue, bold") Tint.cyan.italic.underline("Cyan italic underlined") ``` ## Extended Colors ### RGB Colors ```scala Tint.rgb(255, 87, 51)("Custom RGB") Tint.bgRgb(100, 149, 237)("Custom background") ``` ### Hex Colors ```scala Tint.hex("#FF5733")("Orange-red") Tint.hex("#00FF00")("Lime green") Tint.bgHex("#663399")("Purple background") ``` ### 256-Color Palette ```scala Tint.ansi256(208)("Color 208") Tint.bgAnsi256(57)("Background 57") ``` ## Reusable Styles Create style presets: ```scala val errorStyle = Tint.red.bold val successStyle = Tint.green val warningStyle = Tint.yellow.bold val infoStyle = Tint.cyan println(errorStyle("Failed to connect")) println(successStyle("Operation completed")) println(warningStyle("Disk space low")) println(infoStyle("Processing...")) ``` ## Disabling Colors For non-TTY output or CI environments: ```scala val style = Tint.red.bold // Disable for this style val plain = style.noColor println(plain("No colors")) // Or conditionally val isInteractive = System.console() != null val tint = if isInteractive then Tint.red else Tint().noColor ``` ## Example: CLI Output ```scala import wvlet.uni.cli.Tint object MyCLI: val error = Tint.red.bold val success = Tint.green val highlight = Tint.cyan.bold val dim = Tint.gray def printStatus(name: String, status: String, ok: Boolean): Unit = val statusStyle = if ok then success else error val symbol = if ok then "✓" else "✗" println(s"${dim("[")}${statusStyle(symbol)}${dim("]")} ${name}: ${statusStyle(status)}") def main(args: Array[String]): Unit = printStatus("Database", "Connected", true) printStatus("Cache", "Timeout", false) printStatus("API", "Ready", true) ``` Output: ``` [✓] Database: Connected [✗] Cache: Timeout [✓] API: Ready ``` ## Best Practices 1. **Define styles once** and reuse them 2. **Detect terminal** support before using colors 3. **Provide plain fallback** for non-TTY environments 4. **Use semantic names** (errorStyle, not redBold) 5. **Keep it readable** - don't overuse colors --- --- url: https://wvlet.org/uni/cli/progress.md --- # Progress Indicators Display progress bars and spinners for long-running operations. ## Progress Bar ### Basic Usage ```scala import wvlet.uni.cli.ProgressBar val bar = ProgressBar(total = 100).start() for i <- 1 to 100 do Thread.sleep(50) bar.increment() bar.finish() ``` ### Custom Styling ```scala import wvlet.uni.cli.{ProgressBar, Tint} val bar = ProgressBar(total = 1000) .withWidth(30) // Bar width .withPrefix("Downloading: ") // Text before bar .withSuffix(" files") // Text after bar .withShowPercent(true) // Show percentage .withShowCount(true) // Show count (e.g., 50/100) .withColor(Tint.cyan) // Bar color .start() ``` ### Update Progress ```scala val bar = ProgressBar(total = 100).start() // Set to specific value bar.update(50) // Increment by 1 bar.increment() // Increment by specific amount bar.increment(10) // Check if complete if bar.isComplete then bar.finish() ``` ### Success and Failure ```scala val bar = ProgressBar(total = 100).start() try for i <- 1 to 100 do bar.increment() riskyOperation(i) bar.finish() // Success catch case e: Exception => bar.fail() // Failure throw e ``` ## Configuration Options | Option | Description | Default | |--------|-------------|---------| | `total` | Total items to process | Required | | `width` | Width of the bar in characters | 40 | | `complete` | Character for completed portion | █ | | `incomplete` | Character for incomplete portion | ░ | | `prefix` | Text before the bar | "" | | `suffix` | Text after the bar | "" | | `showPercent` | Show percentage | true | | `showCount` | Show item count | false | | `color` | Bar color | Tint.green | | `hideCursor` | Hide cursor during progress | true | ## Spinner For indeterminate operations: ```scala import wvlet.uni.cli.Spinner val spinner = Spinner() .withText("Loading...") .start() // Do work... Thread.sleep(3000) spinner.stop() ``` ### Spinner Configuration ```scala import wvlet.uni.cli.{Spinner, SpinnerStyle, Tint} val spinner = Spinner() .withText("Loading...") .withSpinner(SpinnerStyle.Line) .withColor(Tint.magenta) .withEnabled(true) .withHideCursor(true) ``` ### Spinner Styles ```scala import wvlet.uni.cli.{Spinner, SpinnerStyle} // Different animation styles Spinner().withSpinner(SpinnerStyle.Default) Spinner().withSpinner(SpinnerStyle.Line) ``` ### Start with Text Override ```scala val spinner = Spinner().withText("Original") // Override text when starting val running = spinner.start("Processing...") running.text shouldBe "Processing..." running.stop() ``` ### Success and Failure States ```scala val spinner = Spinner().withText("Processing...").start() // Mark as successful with message spinner.succeed("Done!") // Or mark as failed with message spinner.fail("Error occurred") ``` ### Disabling Spinner ```scala // Disable spinner completely val spinner = Spinner().noSpinner.start("Test") spinner.isSpinning shouldBe true spinner.stop() // Disable color output val spinner = Spinner().noColor ``` ## Disabling Progress For non-interactive environments: ```scala val isInteractive = System.console() != null val bar = ProgressBar(total = 100) .withEnabled(isInteractive) .start() // Works the same, but no output if disabled bar.increment() bar.finish() ``` ## Example: File Processing ```scala import wvlet.uni.cli.{ProgressBar, Tint} import java.io.File def processFiles(files: List[File]): Unit = val bar = ProgressBar(total = files.size) .withPrefix("Processing: ") .withShowCount(true) .withColor(Tint.blue) .start() for file <- files do processFile(file) bar.increment() bar.finish() println(Tint.green("All files processed!")) ``` ## Example: Download Progress ```scala def downloadWithProgress(url: String, totalBytes: Long): Unit = val bar = ProgressBar(total = totalBytes) .withPrefix("Downloading: ") .withWidth(50) .start() var downloaded = 0L while downloaded < totalBytes do val chunk = downloadChunk(url, downloaded) downloaded += chunk.length bar.update(downloaded) bar.finish() ``` ## Example: Spinner with Result ```scala import wvlet.uni.cli.Spinner def fetchData(): Result = val spinner = Spinner() .withText("Fetching data...") .start() try val result = api.fetchData() spinner.succeed("Data fetched successfully") result catch case e: Exception => spinner.fail(s"Failed: ${e.getMessage}") throw e ``` ## Best Practices 1. **Set realistic totals** - Know the work upfront 2. **Update frequently enough** - But not too often 3. **Handle interruption** - Call finish() or fail() 4. **Disable in CI** - Check for TTY 5. **Use spinners** for unknown duration 6. **Use succeed/fail** - Provide clear completion status --- --- url: https://wvlet.org/uni/cli/launcher.md --- # Command Launcher Build type-safe CLI applications with automatic argument parsing using annotations. ## Basic Usage ```scala import wvlet.uni.cli.launcher.* case class MyApp( @option(prefix = "-n,--name", description = "User name") name: String = "world", @option(prefix = "-c,--count", description = "Repeat count") count: Int = 1, @option(prefix = "-v,--verbose", description = "Enable verbose output") verbose: Boolean = false ) // Parse command-line arguments val app = Launcher.execute[MyApp](args) println(s"Hello, ${app.name}! (${app.count} times)") ``` Command line: ```bash $ myapp --name Alice --count 3 Hello, Alice! (3 times) $ myapp -n Bob -c 2 -v Hello, Bob! (2 times) ``` ## Annotations ### @option Define command-line options: ```scala case class App( @option(prefix = "-v,--verbose", description = "Enable verbose output") verbose: Boolean = false, @option(prefix = "-n,--count", description = "Count") count: Int = 0, @option(prefix = "-m,--message", description = "Message") message: String = "" ) ``` ### @argument Define positional arguments: ```scala case class App( @option(prefix = "-v,--verbose", description = "Verbose") verbose: Boolean = false, @argument(name = "file", description = "Input file") file: String = "" ) ``` ```bash $ app --verbose input.txt ``` ### @command Define commands and sub-commands: ```scala @command(description = "Git-like CLI") class GitCommand( @option(prefix = "-v,--verbose", description = "Verbose") verbose: Boolean = false ): @command(description = "Initialize repository") def init( @argument(name = "dir", description = "Directory") dir: String = "." ): String = s"init ${dir}" @command(description = "Clone repository") def clone( @argument(name = "url", description = "Repository URL") url: String, @option(prefix = "--depth", description = "Clone depth") depth: Option[Int] = None ): String = s"clone ${url}" @command(isDefault = true) def help(): String = "show help" ``` ```bash $ git init myrepo $ git clone https://example.com/repo --depth 1 ``` ## Argument Types ### String Options ```scala case class App( @option(prefix = "-m,--message", description = "Message") message: String = "" ) ``` ### Numeric Options ```scala case class App( @option(prefix = "-n,--count", description = "Count") count: Int = 0, @option(prefix = "-r,--rate", description = "Rate") rate: Double = 0.0 ) ``` ### Boolean Flags ```scala case class App( @option(prefix = "-v,--verbose", description = "Verbose") verbose: Boolean = false ) ``` ```bash $ app -v # verbose = true $ app --verbose # verbose = true ``` ### Optional Values ```scala case class App( @option(prefix = "-c,--config", description = "Config file") config: Option[String] = None, @option(prefix = "-p,--port", description = "Port") port: Option[Int] = None ) ``` ### Multi-Value Options ```scala case class App( @option(prefix = "-f,--file", description = "Input files") files: Seq[String] = Seq.empty, @option(prefix = "-t,--tag", description = "Tags") tags: Seq[String] = Seq.empty ) ``` ```bash $ app -f a.txt -f b.txt -t foo -t bar ``` ### Key-Value Options ```scala case class App( @option(prefix = "-D", description = "System property") props: Seq[KeyValue] = Seq.empty ) ``` ```bash $ app -Dfoo=bar -Dbaz=qux # props = Seq(KeyValue("foo", "bar"), KeyValue("baz", "qux")) ``` ## Nested Options Group related options: ```scala case class GlobalOptions( @option(prefix = "-v,--verbose", description = "Verbose") verbose: Boolean = false, @option(prefix = "--config", description = "Config") config: Option[String] = None ) case class App( global: GlobalOptions, @argument(name = "target", description = "Target") target: String = "" ) ``` ```bash $ app --verbose --config app.conf target.txt ``` ## Option Formats The launcher supports multiple option formats: ```bash $ app --count 100 # Long option with space $ app --count=100 # Long option with equals $ app -n 100 # Short option with space $ app -n100 # Short option without space ``` ## Help Generation Help is automatically generated: ```scala val launcher = Launcher.of[MyApp] launcher.printHelp ``` A help flag applies to the command it follows: `app --help` prints the top-level help, while `app compile --help` prints the options of the `compile` sub-command. ## Configuration ```scala val launcher = Launcher.of[MyApp] .withHelpPrefixes(Seq("-h", "--help", "-?")) .withShowHelpOnNoArgs(true) ``` ## Executing Commands ```scala // Simple execution - returns parsed instance val app = Launcher.execute[MyApp](args) // Full control with result val launcher = Launcher.of[MyApp] val result = launcher.execute(args) if result.showedHelp then System.exit(0) // For commands with methods val result = Launcher.of[GitCommand].execute(Array("init", "myrepo")) result.executedMethod match case Some((name, returnValue)) => println(s"Executed ${name}: ${returnValue}") case None => println("No method executed") ``` ## Example: Complete CLI ```scala import wvlet.uni.cli.launcher.* import wvlet.uni.cli.Tint @command(description = "File management tool") class FileTool( @option(prefix = "-v,--verbose", description = "Verbose output") verbose: Boolean = false ): @command(description = "Copy files") def copy( @argument(name = "source", description = "Source file") source: String, @argument(name = "dest", description = "Destination") dest: String ): Unit = if verbose then println(s"Copying ${source} to ${dest}") // Implementation... println(Tint.green("Done!")) @command(description = "Move files") def move( @argument(name = "source", description = "Source file") source: String, @argument(name = "dest", description = "Destination") dest: String ): Unit = if verbose then println(s"Moving ${source} to ${dest}") // Implementation... println(Tint.green("Done!")) @command(description = "Delete files") def delete( @argument(name = "path", description = "File path") path: String, @option(prefix = "-f,--force", description = "Force delete") force: Boolean = false ): Unit = if verbose then println(s"Deleting ${path}") // Implementation... println(Tint.green("Done!")) object Main: def main(args: Array[String]): Unit = Launcher.of[FileTool].execute(args) ``` Usage: ```bash $ filetool copy source.txt dest.txt --verbose $ filetool move old.txt new.txt $ filetool delete temp.txt --force $ filetool --help $ filetool move --help ``` ## Best Practices 1. **Use descriptive prefixes** - Include both short and long forms 2. **Provide defaults** for optional arguments 3. **Add descriptions** to all options and arguments 4. **Group related options** using nested case classes 5. **Use `@command(isDefault = true)`** for default sub-command --- --- url: https://wvlet.org/uni/dom.md --- # Web UI (RxElement) `wvlet.uni.dom` is a reactive DOM toolkit for building browser UIs in Scala.js. Components are plain Scala classes, the HTML/SVG DSL is type-checked, and any [`Rx`](../rx/) value embedded in the markup re-renders the affected DOM in place when it changes — no virtual DOM, no separate template language. ::: warning Scala.js only This module lives under `uni/.js` and targets the browser. There is no JVM or Scala Native counterpart. Add the `uni` dependency to a Scala.js project to use it. ::: ## Setup Every example below assumes these imports. The `given` import enables embedding `String`, `Int`, `Rx`, `Seq`, and `Option` directly as children/attribute values; `implicitConversions` is required by Scala 3 for those conversions. ```scala import wvlet.uni.dom.all.* import wvlet.uni.dom.all.given import wvlet.uni.rx.Rx import scala.language.implicitConversions ``` ## Hello World Extend `RxElement` and implement the single abstract method `def render`, then mount it with the `renderTo` extension: ```scala class Hello extends RxElement: override def render: RxElement = div("Hello, World!") Hello().renderTo("app") // mounts into
(created if absent) ``` `renderTo(nodeId)` returns an [`RxDomNode`](#mounting-and-teardown). If no element with that id exists, it creates a `
` and appends it to `document.body`. ## Components ### RxElement `RxElement` is the component base. Override `render` to return the markup; add lifecycle hooks as needed (all default to no-ops): | Hook | When | |------|------| | `def render: RxElement` | produces the markup (required) | | `beforeRender: Unit` | before the first render | | `onMount(node: Any): Unit` | after the element is attached to the DOM (`node` is the `dom.Node`) | | `beforeUnmount: Unit` | before the element is detached | ```scala class Panel extends RxElement: override def beforeRender: Unit = println("about to render") override def onMount(node: Any): Unit = println("mounted") override def render: RxElement = div(cls -> "panel", "content") ``` Components nest by embedding one in another's markup: ```scala class Inner extends RxElement: override def render: RxElement = span("inner") class Outer extends RxElement: override def render: RxElement = div(Inner()) ``` ### RxComponent `RxComponent` is a `trait` for "chrome" that wraps caller-provided content. Override `render(content: RxElement)` and invoke it with `apply(content)` (or `apply()` for empty content): ```scala object MainFrame extends RxComponent: override def render(content: RxElement): RxElement = div(cls -> "main-frame", content) MainFrame(span("editor")).renderTo("app") // chrome wraps "editor" ``` ## HTML DSL ### Tags HTML tags are values (`div`, `span`, `h1`–`h6`, `p`, `a`, `img`, `ul`, `li`, `table`, `form`, `input`, `button`, `select`, …). Calling a tag with children and attributes builds an element: ```scala div(cls -> "container", id -> "main", h1("Title"), p("Some content"), a(href -> "/next", "Go next") ) ``` ### Attributes Attributes use `name -> value` (or call syntax `name(value)`): ```scala input(tpe -> "email", placeholder -> "you@example.com", required.noValue) div(data("user-id") -> "123", data("role") -> "admin") // data-* attributes ``` A few names are suffixed to avoid clashing with same-named tags — the **tag** keeps the bare name: | Use this attribute | for the HTML attribute | because the bare name is the tag | |--------------------|------------------------|----------------------------------| | `cls` (or `` `class` ``) | `class` | — | | `styleAttr` | `style` | `style` is the `