Cloudflare Workers
The Workers template compiles the same pages, actions, and streams to wasm, and the worker hands each request to the axum Router.
Deploy from the template
These commands need a Cloudflare account, Rust 1.99 or later, and Node.js 22 or later. Node.js runs wrangler, Cloudflare's command-line tool, which is published only on npm, and npx fetches it on first use, so the project has no package.json. cargo generate copies the Workers template into my-worker and names the package and the worker after it, as it does for the container.
cargo install cargo-generate
cargo generate --git https://github.com/imaimai17468/hypersig templates/cloudflare --name my-worker
cd my-worker
npx wrangler dev
npx wrangler login
npx wrangler deploywrangler dev builds the worker and runs it at http://localhost:8787 in workerd, the runtime Workers use. The first build installs worker-build, the tool that compiles the crate to wasm, and the wasm target, so it takes a few minutes. wrangler login opens the browser to sign in to Cloudflare. wrangler deploy uploads the worker and prints its address, https://my-worker.<your subdomain>.workers.dev, and on an account without a workers.dev subdomain it asks to register one first.
What the template holds
| File | What it does |
|---|---|
src/lib.rs | The worker below. The +1 button counts in the browser, the second button doubles the count on the worker, and a stream sends the worker's time every second. |
Cargo.toml | A library that worker-build compiles, with axum without its default features, which need tokio's networking. |
wrangler.toml | The worker's name, and the build command, which installs worker-build when it is missing and runs it. |
rust-toolchain.toml | Names the wasm target, which rustup adds to the default toolchain on the first build. |
use std::time::Duration;
use axum::Router;
use futures::{Stream, stream};
use hypersig::html::{Stylesheet, button, div, h1, p, span};
use hypersig::{
Action, DateFormat, DateStyle, IntlLocale, Page, Patches, Route, RouterExt, Signals,
StreamAction, TimeStyle, Timestamp,
};
use serde::{Deserialize, Serialize};
use tower_service::Service as _;
use worker::send::SendFuture;
use worker::{Context, Delay, Env, HttpRequest, event};
#[derive(Signals, Serialize, Deserialize)]
struct Counter {
count: i32,
now: Timestamp,
}
hypersig::routes! {
fn pages(Router<()>);
const INDEX: Route = "/" => index;
}
const DOUBLE: Action<Counter, i32> = Action::post("/double");
const CLOCK: StreamAction<Counter> = StreamAction::new("/clock");
const TIME: DateFormat =
DateFormat::date_time(IntlLocale::En, DateStyle::Medium, TimeStyle::Medium);
const STYLE: Stylesheet = Stylesheet::new(
"
body { font-family: system-ui, sans-serif; max-width: 32rem; margin: 3rem auto; padding: 0 1rem; }
button { font-size: 1rem; padding: .4rem .9rem; }
",
);
async fn index((): (), (): ()) -> Page<Counter> {
let count = Counter::count();
let initial = Counter {
count: 0,
now: Timestamp::now(),
};
Page::new("hypersig on Cloudflare Workers", initial)
.style(STYLE)
.child(
div()
.on_load(CLOCK.call().reconnect())
.child(h1().text("Counter"))
.child(
p().child(button().on_click(count.set(count + 1)).text("+1"))
.child(span().bind_text(count.to_text()))
.child(
button()
.on_click(DOUBLE.call_with(count))
.text("Double on the worker"),
),
)
.child(p().bind_text(Counter::now().format_date(&TIME))),
)
}
async fn double((): (), count: i32) -> Patches<Counter> {
Counter::count().patch(count.saturating_mul(2)).into()
}
/// The worker's time, once a second.
fn clock((): (), (): ()) -> impl Stream<Item = Patches<Counter>> {
stream::unfold((), |()| async {
// `Delay` waits on a JavaScript timer, which is not `Send`. A worker
// runs on one thread, so the wrapper never moves it to another.
SendFuture::new(Delay::from(Duration::from_secs(1))).await;
Some((Counter::now().patch(Timestamp::now()).into(), ()))
})
}
#[event(fetch)]
async fn fetch(
request: HttpRequest,
_env: Env,
_context: Context,
) -> worker::Result<axum::http::Response<axum::body::Body>> {
let mut app = pages(Router::new())
.hypersig_runtime()
.action(DOUBLE, double)
.stream(CLOCK, clock);
Ok(app.call(request).await?)
}Turn a server project into a worker
A server project becomes a worker when its package turns from a binary into a library that worker-build compiles, and its main into the fetch function above. The pages, actions, and streams stay as they are.
| File | What changes |
|---|---|
Cargo.toml | Add [lib] with crate-type = ["cdylib"]. Give axum default-features = false, add worker with the features http and axum and tower-service, and remove tokio. |
src/main.rs | Rename it to src/lib.rs, and replace #[tokio::main] async fn main with a fetch function like the template's, which builds the same Router and hands it the request. |
wrangler.toml, rust-toolchain.toml | Copy them from templates/cloudflare, and set name in wrangler.toml to the worker's name. |
Code that uses tokio changes too. A stream that waits with tokio::time::sleep waits with worker::Delay, as the clock above does, and state in the router's state does not outlast the isolate, as the next section says. Then npx wrangler dev runs the project.
What changes on Workers
- Requests may run in different isolates, so a value kept in memory may be gone at the next request. Keep the state requests share in KV, D1, or a Durable Object.
- A stream sends no keep-alive comments, because axum times them with tokio, which has no timer on this target. A stream that waits between patches waits with
worker::Delay, wrapped inSendFuturebecause a JavaScript timer is notSend. hypersig_runtime()sends the runtime uncompressed. The Workers runtime compresses a body that names aContent-Encodingagain, so the file compressed with Brotli at build time would reach the browser compressed twice.Timestamp::nowreads the clock with JavaScript'sDate.now(), becauseSystemTime::nowpanics on this target.