#![doc = include_str!("../README.md")]
#![forbid(unsafe_code)]
mod config;
mod ports;
mod runtime;
mod tracing_setup;
pub use config::EnvConfig;
pub use ports::{
ContextDefer, D1Database, FetchClient, KvStorePort, RateLimitPort, ScheduleDefer, WorkersClock,
client_ip,
};
pub use runtime::Cloudflare;
pub use tracing_setup::install_tracing;
use cratefield_core::Harness;
use std::sync::Arc;
use tower::ServiceExt;
use worker::{Context, Env, Request as WorkerRequest, Response as WorkerResponse};
fn check_module_config_once(harness: &Harness, config: &dyn cratefield_core::Config) {
use std::sync::atomic::{AtomicBool, Ordering};
static CHECKED: AtomicBool = AtomicBool::new(false);
if CHECKED.swap(true, Ordering::Relaxed) {
return;
}
for module in harness.modules() {
if let Err(err) = module.validate_config(config) {
worker::console_error!(
"[config] module `{}` has invalid configuration: {err}",
module.name()
);
}
}
}
pub async fn serve(
harness: &Harness,
runtime: &Cloudflare,
mut req: WorkerRequest,
env: Env,
ctx: Context,
) -> worker::Result<WorkerResponse> {
install_tracing();
let ports = runtime.ports(&env, Arc::new(ContextDefer(ctx)));
check_module_config_once(harness, ports.config.as_ref());
let router = harness.router(ports);
let bytes = req.bytes().await?;
let method = http::Method::from_bytes(req.method().to_string().as_bytes())
.map_err(|err| worker::Error::RustError(err.to_string()))?;
let mut builder = http::Request::builder()
.method(method)
.uri(req.url()?.to_string());
{
let headers = req.headers();
for (name, value) in headers {
builder = builder.header(name.as_str(), value.as_str());
}
}
let buffered = builder
.body(axum::body::Body::from(bytes))
.map_err(|err| worker::Error::RustError(err.to_string()))?;
let response = router
.oneshot(buffered)
.await
.map_err(|err| worker::Error::RustError(err.to_string()))?;
response_to_worker(response).await
}
async fn response_to_worker(
response: http::Response<axum::body::Body>,
) -> worker::Result<WorkerResponse> {
let (parts, body) = response.into_parts();
let bytes = axum::body::to_bytes(body, MAX_RESPONSE_BUFFER)
.await
.map_err(|err| worker::Error::RustError(err.to_string()))?;
let mut out =
WorkerResponse::from_bytes(bytes.as_ref().to_vec())?.with_status(parts.status.as_u16());
{
let worker_headers = out.headers_mut();
for name in parts.headers.keys() {
let _ = worker_headers.delete(name.as_str());
for value in parts.headers.get_all(name) {
let _ = worker_headers.append(name.as_str(), value.to_str().unwrap_or_default());
}
}
}
Ok(out)
}
const MAX_RESPONSE_BUFFER: usize = 1024 * 1024;
pub async fn serve_scheduled(
harness: &Harness,
runtime: &Cloudflare,
event: worker::ScheduledEvent,
env: Env,
ctx: worker::ScheduleContext,
) {
install_tracing();
let cron = event.cron();
let ports = runtime.ports(&env, Arc::new(ScheduleDefer(ctx)));
check_module_config_once(harness, ports.config.as_ref());
for module in harness.modules() {
let module_ctx = harness.module_context(module.as_ref(), &ports);
if let Err(err) = module.scheduled(&module_ctx, &cron).await {
tracing::error!(
module = module.name(),
cron = %cron,
error = %err,
"scheduled module work failed",
);
}
}
}