feat: bootstrap namespace-driven Tessera renderer

This commit is contained in:
2026-10-04 19:32:04 +08:00
commit 2f4c8f0fe2
14 changed files with 4262 additions and 0 deletions
+306
View File
@@ -0,0 +1,306 @@
use std::{
io::Read,
sync::{Arc, RwLock},
thread,
};
use anyhow::{Context as _, Result};
use tiny_http::{Header, Method, Request, Response, Server, StatusCode};
use winit::event_loop::EventLoopProxy;
use crate::{
app::AppEvent,
compiler,
document::{NamespaceState, RenderDocument},
};
pub const API_ADDRESS: &str = "127.0.0.1:4737";
const MAX_CODE_SIZE: u64 = 1_048_576;
pub fn start(
namespace: Arc<RwLock<NamespaceState>>,
proxy: EventLoopProxy<AppEvent>,
) -> Result<()> {
let server = Server::http(API_ADDRESS)
.map_err(|error| anyhow::anyhow!(error))
.with_context(|| format!("bind Tessera API to {API_ADDRESS}"))?;
thread::Builder::new()
.name("tessera-agent-api".to_owned())
.spawn(move || {
for request in server.incoming_requests() {
handle(request, &namespace, &proxy);
}
})
.context("start Tessera agent API thread")?;
Ok(())
}
fn handle(
mut request: Request,
namespace: &Arc<RwLock<NamespaceState>>,
proxy: &EventLoopProxy<AppEvent>,
) {
let method = request.method().clone();
let path = request
.url()
.split('?')
.next()
.unwrap_or(request.url())
.to_owned();
let response = if method == Method::Get && path == "/agent" {
match namespace.read() {
Ok(namespace) => markdown(StatusCode(200), agent_guide(&namespace)),
Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"),
}
} else if let Some(addressed_namespace) = namespace_from_path(&path).map(str::to_owned) {
code_request(&mut request, method, &addressed_namespace, namespace, proxy)
} else if method == Method::Options {
text(StatusCode(204), "", "text/plain")
} else {
error_response(StatusCode(404), "route not found; start with GET /agent")
};
let _ = request.respond(response);
}
fn code_request(
request: &mut Request,
method: Method,
addressed_namespace: &str,
namespace: &Arc<RwLock<NamespaceState>>,
proxy: &EventLoopProxy<AppEvent>,
) -> Response<std::io::Cursor<Vec<u8>>> {
let Ok(current) = namespace.read() else {
return error_response(StatusCode(500), "namespace lock is poisoned");
};
if addressed_namespace != current.name {
return error_response(
StatusCode(404),
format!(
"namespace `{addressed_namespace}` is not active; current namespace is `{}`",
current.name
),
);
}
if method == Method::Get {
let mut response = text(
StatusCode(200),
&current.document.source,
"text/plain; charset=utf-8",
);
add_header(
&mut response,
"X-Tessera-Revision",
&current.revision.to_string(),
);
return response;
}
drop(current);
match method {
Method::Put => match read_code(request) {
Ok(code) => match compiler::compile(&code, addressed_namespace) {
Ok(document) => {
if let Err(error) = document.validate() {
return compile_error(error.to_string());
}
match namespace.write() {
Ok(mut current) => {
current.revision += 1;
current.document = document;
let revision = current.revision;
drop(current);
let _ = proxy.send_event(AppEvent::DocumentUpdated);
json(
StatusCode(200),
serde_json::json!({
"ok": true,
"namespace": addressed_namespace,
"revision": revision,
"diagnostics": []
}),
)
}
Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"),
}
}
Err(error) => compile_error(error.to_string()),
},
Err(error) => error_response(StatusCode(400), error.to_string()),
},
Method::Delete => match namespace.write() {
Ok(mut current) => {
current.revision += 1;
current.document = RenderDocument::default();
let revision = current.revision;
drop(current);
let _ = proxy.send_event(AppEvent::DocumentUpdated);
json(
StatusCode(200),
serde_json::json!({
"ok": true,
"namespace": addressed_namespace,
"revision": revision
}),
)
}
Err(_) => error_response(StatusCode(500), "namespace lock is poisoned"),
},
Method::Options => text(StatusCode(204), "", "text/plain"),
_ => error_response(StatusCode(405), "code supports GET, PUT, and DELETE"),
}
}
fn namespace_from_path(path: &str) -> Option<&str> {
path.strip_prefix("/v1/namespaces/")?
.strip_suffix("/code")
.filter(|namespace| !namespace.is_empty() && !namespace.contains('/'))
}
fn read_code(request: &mut Request) -> Result<String> {
if request
.body_length()
.is_some_and(|length| length as u64 > MAX_CODE_SIZE)
{
anyhow::bail!("code exceeds the 1 MiB limit");
}
let mut code = String::new();
request
.as_reader()
.take(MAX_CODE_SIZE + 1)
.read_to_string(&mut code)
.context("read code")?;
if code.len() as u64 > MAX_CODE_SIZE {
anyhow::bail!("code exceeds the 1 MiB limit");
}
Ok(code)
}
fn agent_guide(namespace: &NamespaceState) -> String {
format!(
r#"# Tessera Agent API
Current namespace: `{name}`
Current revision: `{revision}`
Tessera stores code by namespace. It has no files. Submit code only; Tessera parses,
checks, compiles, and renders it.
## Code CRUD
- Read: `GET http://{address}/v1/namespaces/{name}/code`
- Create or update: `PUT http://{address}/v1/namespaces/{name}/code`
- Delete: `DELETE http://{address}/v1/namespaces/{name}/code`
Send PUT bodies as `text/plain; charset=utf-8`.
## Minimal syntax
```tessera
namespace {name}
use tessera.ui::{{Column, Row, Spacer, Text}}
export scene Main {{
render {{
Column(gap: 12, padding: 24) {{
Text("Hello", role: .title)
Text("Rendered by Tessera")
}}
}}
}}
```
Supported by this syntax experiment: one exported scene, one render root,
`Column`, `Row`, `Spacer`, `Text`, `gap`, `padding`, and `.title`.
Compilation errors reject the update and preserve the last rendered revision.
"#,
name = namespace.name,
revision = namespace.revision,
address = API_ADDRESS
)
}
fn compile_error(message: String) -> Response<std::io::Cursor<Vec<u8>>> {
json(
StatusCode(422),
serde_json::json!({
"ok": false,
"diagnostics": [{
"severity": "error",
"message": message
}]
}),
)
}
fn error_response(
status: StatusCode,
message: impl Into<String>,
) -> Response<std::io::Cursor<Vec<u8>>> {
json(status, serde_json::json!({ "error": message.into() }))
}
fn json(status: StatusCode, value: serde_json::Value) -> Response<std::io::Cursor<Vec<u8>>> {
text(
status,
&value.to_string(),
"application/json; charset=utf-8",
)
}
fn markdown(status: StatusCode, value: String) -> Response<std::io::Cursor<Vec<u8>>> {
text(status, &value, "text/markdown; charset=utf-8")
}
fn text(status: StatusCode, value: &str, content_type: &str) -> Response<std::io::Cursor<Vec<u8>>> {
let mut response = Response::from_string(value).with_status_code(status);
add_header(&mut response, "Content-Type", content_type);
add_header(
&mut response,
"Access-Control-Allow-Origin",
"http://127.0.0.1",
);
add_header(
&mut response,
"Access-Control-Allow-Methods",
"GET, PUT, DELETE, OPTIONS",
);
add_header(
&mut response,
"Access-Control-Allow-Headers",
"Content-Type",
);
response
}
fn add_header(response: &mut Response<std::io::Cursor<Vec<u8>>>, name: &str, value: &str) {
if let Ok(header) = Header::from_bytes(name, value) {
response.add_header(header);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_namespace_only_from_code_routes() {
assert_eq!(
namespace_from_path("/v1/namespaces/app.main/code"),
Some("app.main")
);
assert_eq!(namespace_from_path("/v1/namespaces/app.main"), None);
assert_eq!(namespace_from_path("/v1/namespaces/a/b/code"), None);
}
#[test]
fn guide_contains_only_code_crud_routes() {
let guide = agent_guide(&NamespaceState::default());
assert!(guide.contains("Code CRUD"));
assert!(guide.contains("PUT http://127.0.0.1:4737"));
assert!(!guide.contains("display_list"));
}
}