pax-chassis-web 0.39.0

Platform-specific chassis allowing Pax cartridges to be executed as Web apps
Documentation
use js_sys::Date;
use pax_designtime::messages::{
    DevClientInspectTreeResponse, DevClientLogEntry, DevClientLogsResponse, DevClientLookRequest,
    DevClientLookResponse, DevClientRawCapture, DevClientRayCastResponse,
    DevClientReplaceNodeResponse, DevClientRequest, DevClientResponse,
    DevClientSelectorQueryResponse,
};
use pax_message::{NativeMessage, ScreenshotPatch};
use pax_runtime::designtime_support::{
    apply_designtime_replace_node_subtemplate, apply_designtime_userland_reload,
    build_designtime_inspect_tree_payload, build_designtime_ray_cast_payload,
    build_designtime_selector_query_payload,
};

use crate::read_dev_console_entries_json;
use crate::PaxChassisWeb;
use miniz_oxide::deflate::compress_to_vec_zlib;

pub(super) struct PendingWebDevLookRequest {
    pub request: DevClientLookRequest,
    pub captures: Vec<DevClientRawCapture>,
    pub next_capture_at_ms: u128,
    pub deadline_ms: u128,
    pub capture_in_flight: Option<u32>,
}

pub(super) fn next_web_dev_capture_id(next_capture_id: &mut u32) -> u32 {
    let capture_id = *next_capture_id;
    *next_capture_id = (*next_capture_id).wrapping_add(1);
    if *next_capture_id < 1_000_000 {
        *next_capture_id = 1_000_000;
    }
    capture_id
}

impl PaxChassisWeb {
    pub(super) fn update_userland_component(&mut self) {
        let mut engine = self.engine.borrow_mut();
        apply_designtime_userland_reload(
            &mut engine,
            self.userland_definition_to_instance_traverser.as_ref(),
            &self.designtime_manager,
        );
    }

    pub(super) fn process_pending_dev_client_requests(&mut self) {
        let requests = self
            .designtime_manager
            .borrow_mut()
            .take_dev_client_requests();
        for request in requests {
            match request {
                DevClientRequest::Look(request) => {
                    let now_ms = web_dev_now_ms();
                    self.pending_dev_look_requests.insert(
                        request.request_id.clone(),
                        PendingWebDevLookRequest {
                            next_capture_at_ms: now_ms,
                            deadline_ms: now_ms.saturating_add(request.duration_ms as u128),
                            request,
                            captures: vec![],
                            capture_in_flight: None,
                        },
                    );
                }
                DevClientRequest::InspectTree(request) => {
                    let payload = {
                        let engine = self.engine.borrow();
                        build_designtime_inspect_tree_payload(
                            &engine,
                            self.userland_definition_to_instance_traverser.as_ref(),
                            request.max_depth.map(|value| value as i64).unwrap_or(-1),
                        )
                    };
                    let response = DevClientResponse::InspectTree(DevClientInspectTreeResponse {
                        request_id: request.request_id,
                        status: payload.status,
                        node_count: payload.node_count,
                        tree_json: payload.tree_json,
                        error: payload.error,
                    });
                    if let Err(err) = self
                        .designtime_manager
                        .borrow_mut()
                        .send_dev_client_response(response)
                    {
                        log::warn!("failed to send web inspect-tree response: {err}");
                    }
                }
                DevClientRequest::RayCast(request) => {
                    let payload = {
                        let engine = self.engine.borrow();
                        build_designtime_ray_cast_payload(
                            &engine,
                            self.userland_definition_to_instance_traverser.as_ref(),
                            request.x,
                            request.y,
                            request.hit_invisible,
                        )
                    };
                    let response = DevClientResponse::RayCast(DevClientRayCastResponse {
                        request_id: request.request_id,
                        status: payload.status,
                        x: request.x,
                        y: request.y,
                        hit_invisible: request.hit_invisible,
                        node_count: payload.node_count,
                        nodes_json: payload.nodes_json,
                        error: payload.error,
                    });
                    if let Err(err) = self
                        .designtime_manager
                        .borrow_mut()
                        .send_dev_client_response(response)
                    {
                        log::warn!("failed to send web ray-cast response: {err}");
                    }
                }
                DevClientRequest::SelectorQuery(request) => {
                    let payload = {
                        let engine = self.engine.borrow();
                        build_designtime_selector_query_payload(
                            &engine,
                            self.userland_definition_to_instance_traverser.as_ref(),
                            &request.selector,
                        )
                    };
                    let response =
                        DevClientResponse::SelectorQuery(DevClientSelectorQueryResponse {
                            request_id: request.request_id,
                            status: payload.status,
                            selector: request.selector,
                            node_count: payload.node_count,
                            nodes_json: payload.nodes_json,
                            error: payload.error,
                        });
                    if let Err(err) = self
                        .designtime_manager
                        .borrow_mut()
                        .send_dev_client_response(response)
                    {
                        log::warn!("failed to send web selector response: {err}");
                    }
                }
                DevClientRequest::ReplaceNode(request) => {
                    let payload = apply_designtime_replace_node_subtemplate(
                        self.userland_definition_to_instance_traverser.as_ref(),
                        &self.designtime_manager,
                        &request.component_type_id,
                        request.template_node_id,
                        &request.subtemplate,
                    );
                    let response = DevClientResponse::ReplaceNode(DevClientReplaceNodeResponse {
                        request_id: request.request_id,
                        status: payload.status,
                        component_type_id: payload.component_type_id,
                        template_node_id: payload.template_node_id,
                        reload_scope: payload.reload_scope,
                        reloaded_template_node_id: payload.reloaded_template_node_id,
                        source_path: payload.source_path,
                        error: payload.error,
                    });
                    if let Err(err) = self
                        .designtime_manager
                        .borrow_mut()
                        .send_dev_client_response(response)
                    {
                        log::warn!("failed to send web replace-node response: {err}");
                    }
                }
                DevClientRequest::Logs(request) => {
                    let request_id = request.request_id;
                    let since_seq = request.since_seq;
                    let limit = request.limit.unwrap_or(200);
                    let response = match serde_json::from_str::<serde_json::Value>(
                        &read_dev_console_entries_json(since_seq, limit),
                    ) {
                        Ok(payload) => {
                            let entries = payload
                                .get("entries")
                                .cloned()
                                .and_then(|value| {
                                    serde_json::from_value::<Vec<DevClientLogEntry>>(value).ok()
                                })
                                .unwrap_or_default();
                            let next_seq = payload
                                .get("next_seq")
                                .and_then(|value| value.as_u64())
                                .unwrap_or(1);
                            let oldest_seq =
                                payload.get("oldest_seq").and_then(|value| value.as_u64());
                            DevClientResponse::Logs(DevClientLogsResponse {
                                request_id,
                                status: "ok".to_string(),
                                entries,
                                next_seq,
                                oldest_seq,
                                error: None,
                            })
                        }
                        Err(err) => DevClientResponse::Logs(DevClientLogsResponse {
                            request_id,
                            status: "error".to_string(),
                            entries: vec![],
                            next_seq: 1,
                            oldest_seq: None,
                            error: Some(format!("failed to read console log buffer: {err}")),
                        }),
                    };
                    if let Err(err) = self
                        .designtime_manager
                        .borrow_mut()
                        .send_dev_client_response(response)
                    {
                        log::warn!("failed to send web logs response: {err}");
                    }
                }
            }
        }
    }

    pub(super) fn collect_completed_dev_look_captures(&mut self) {
        let screenshot_map = self.engine.borrow().runtime_context.get_screenshot_map();
        let mut screenshot_map = screenshot_map.borrow_mut();
        let mut completed_request_ids = vec![];
        let mut responses = vec![];

        for (request_id, pending_request) in self.pending_dev_look_requests.iter_mut() {
            let Some(capture_id) = pending_request.capture_in_flight else {
                continue;
            };
            let Some(screenshot) = screenshot_map.remove(&capture_id) else {
                continue;
            };

            pending_request.capture_in_flight = None;
            let captured_at_ms = web_dev_now_ms();
            let compressed_bytes = compress_to_vec_zlib(&screenshot.data, 6);
            let (rgba_bytes, compression) = if compressed_bytes.len() < screenshot.data.len() {
                (compressed_bytes, Some("zlib".to_string()))
            } else {
                (screenshot.data, None)
            };
            pending_request.captures.push(DevClientRawCapture {
                rgba_bytes,
                compression,
                width: screenshot.width,
                height: screenshot.height,
                captured_at_ms,
            });

            if pending_request.request.period_ms == 0
                || captured_at_ms >= pending_request.deadline_ms
            {
                completed_request_ids.push(request_id.clone());
                responses.push(DevClientResponse::Look(DevClientLookResponse {
                    request_id: request_id.clone(),
                    status: "ok".to_string(),
                    captures: pending_request.captures.clone(),
                    error: None,
                }));
            } else {
                pending_request.next_capture_at_ms =
                    captured_at_ms.saturating_add(pending_request.request.period_ms as u128);
            }
        }

        drop(screenshot_map);

        for request_id in completed_request_ids {
            self.pending_dev_look_requests.remove(&request_id);
        }
        for response in responses {
            if let Err(err) = self
                .designtime_manager
                .borrow_mut()
                .send_dev_client_response(response)
            {
                log::warn!("failed to send web look response: {err}");
            }
        }
    }

    pub(super) fn schedule_due_dev_look_captures(&mut self) {
        let now_ms = web_dev_now_ms();
        let mut capture_requests = vec![];
        for pending_request in self.pending_dev_look_requests.values_mut() {
            if pending_request.capture_in_flight.is_some()
                || pending_request.next_capture_at_ms > now_ms
            {
                continue;
            }

            let capture_id = next_web_dev_capture_id(&mut self.next_dev_capture_id);
            pending_request.capture_in_flight = Some(capture_id);
            capture_requests.push((capture_id, pending_request.request.scale));
        }

        if capture_requests.is_empty() {
            return;
        }

        let engine = self.engine.borrow();
        for (capture_id, scale) in capture_requests {
            engine
                .runtime_context
                .enqueue_native_message(NativeMessage::Screenshot(ScreenshotPatch {
                    id: capture_id,
                    scale: Some(scale),
                }));
        }
    }
}

fn web_dev_now_ms() -> u128 {
    Date::now() as u128
}