1use std::collections::HashMap;
5
6use axum::extract::State;
7use axum::http::{header, StatusCode};
8use axum::response::IntoResponse;
9use axum::Json;
10use serde_json::json;
11
12use anvil_core::Container;
13use anvil_core::Error;
14
15use crate::component::Ctx;
16use crate::morph;
17use crate::registry;
18use crate::request::UpdateRequest;
19use crate::response::{ComponentResult, Effects, IslandHtml, UpdateResponse};
20use crate::snapshot::{self, Memo};
21
22pub const RUNTIME_JS: &[u8] = include_bytes!("../dist/spark.min.js");
23
24pub async fn runtime_js() -> impl IntoResponse {
26 (
27 StatusCode::OK,
28 [
29 (
30 header::CONTENT_TYPE,
31 "application/javascript; charset=utf-8",
32 ),
33 (header::CACHE_CONTROL, "public, max-age=31536000, immutable"),
34 ],
35 RUNTIME_JS,
36 )
37}
38
39pub async fn update(
42 State(container): State<Container>,
43 Json(req): Json<UpdateRequest>,
44) -> Result<impl IntoResponse, Error> {
45 let (app_key, encrypt) = crate::render::signing();
46 let mut out = UpdateResponse {
47 components: Vec::with_capacity(req.components.len()),
48 };
49
50 for comp in req.components {
51 let envelope = snapshot::decode(&comp.snapshot, &app_key).map_err(Error::from)?;
52 let entry = registry::resolve(&envelope.memo.class).map_err(Error::from)?;
53 let mut boxed = (entry.load)(&envelope.data).map_err(Error::from)?;
54
55 let mut ctx = Ctx::new(Some(container.clone()));
56
57 if !comp.updates.is_empty() {
58 boxed
59 .state
60 .apply_writes(&comp.updates, &mut ctx)
61 .await
62 .map_err(Error::from)?;
63 }
64
65 let mut requested_island: Option<String> = None;
66 for call in comp.calls {
67 ctx.island = call.island.clone();
68 boxed
69 .state
70 .dispatch_call(&call.method, call.params, &mut ctx)
71 .await
72 .map_err(Error::from)?;
73 if let Some(island) = ctx.island.take() {
74 requested_island = Some(island);
75 }
76 }
77
78 let next_memo = Memo {
80 id: envelope.memo.id.clone(),
81 class: envelope.memo.class.clone(),
82 view: envelope.memo.view.clone(),
83 listeners: (entry.listeners)(),
84 errors: if ctx.errors.is_empty() {
85 None
86 } else {
87 Some(serde_json::to_value(&ctx.errors).unwrap_or(serde_json::Value::Null))
88 },
89 };
90 let (html, wire) = crate::render::rerender(&boxed, &next_memo).map_err(Error::from)?;
91 let full_html = crate::render::wrap_rerender(&html, &next_memo, &wire);
92
93 let islands = if let Some(island_name) = requested_island.as_deref() {
94 if let Some(island_html) = morph::slice_island(&full_html, island_name) {
95 vec![IslandHtml {
96 name: island_name.to_string(),
97 html: island_html,
98 }]
99 } else {
100 Vec::new()
101 }
102 } else {
103 Vec::new()
104 };
105
106 let effects = Effects {
107 dispatched: std::mem::take(&mut ctx.dispatched),
108 emitted: std::mem::take(&mut ctx.emitted),
109 redirect: ctx.redirect.clone(),
110 errors: std::mem::take(&mut ctx.errors)
111 .into_iter()
112 .collect::<HashMap<_, _>>(),
113 islands,
114 };
115
116 out.components.push(ComponentResult {
117 snapshot: wire,
118 html: full_html,
119 effects,
120 });
121 }
122
123 let _ = encrypt; Ok(Json(out))
125}
126
127pub async fn channel_auth() -> impl IntoResponse {
131 (
132 StatusCode::OK,
133 Json(json!({
134 "auth": "spark:placeholder",
135 "channel_data": null,
136 })),
137 )
138}