moirai-pal 0.9.2

Platform Abstraction Layer for Moirai async I/O operations
Documentation
//! Bounded native observation state; callbacks borrow it only synchronously.

use crate::webview_startup::{RecordAdmission, RequestId, RequestSlot, Selection, TraceBudget};
use serde_json::{Value, json};
use std::{
    cell::RefCell,
    rc::{Rc, Weak},
};

use super::protocol::{Reply, Stage};

pub(super) type Shared = Rc<RefCell<State>>;
pub(super) type Borrowed = Weak<RefCell<State>>;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Document {
    Outside,
    Loading,
    Available,
}

pub(super) struct State {
    pub(super) selection: Selection,
    pub(super) budget: TraceBudget,
    pub(super) slot: RequestSlot,
    pub(super) reply: Option<(RequestId, Stage, Reply)>,
    pub(super) document: Document,
    pub(super) navigation: Option<u64>,
    pub(super) failed: bool,
    pub(super) samples: u64,
    pub(super) stale: u64,
    pub(super) last: Vec<Option<Value>>,
    metadata_sent: bool,
}

impl State {
    pub(super) fn new(selection: Selection) -> Self {
        let last = vec![None; selection.selectors.len()];
        Self {
            selection,
            last,
            budget: TraceBudget::default(),
            slot: RequestSlot::default(),
            reply: None,
            document: Document::Outside,
            navigation: None,
            failed: false,
            samples: 0,
            stale: 0,
            metadata_sent: false,
        }
    }

    pub(super) fn record(&mut self, value: &Value) -> bool {
        match self.budget.record(value) {
            Ok(RecordAdmission::Admitted(encoded)) => {
                emit(&encoded);
                true
            }
            Ok(RecordAdmission::Exhausted { first: true }) => {
                self.metadata("quota_exhausted", false);
                false
            }
            Ok(RecordAdmission::Exhausted { first: false }) => false,
            Err(_) => {
                self.failed = true;
                false
            }
        }
    }

    pub(super) fn error(&mut self, operation: &str, error: &windows::core::Error) {
        self.record(&json!({"kind":"native_error","operation":operation,
            "hresult":error.code().0,"message":error.to_string()}));
    }

    pub(super) fn navigation_started(&mut self, id: u64, uri: &str) {
        self.invalidate_nodes();
        self.navigation = Some(id);
        self.document = if uri == self.selection.start_uri {
            Document::Loading
        } else {
            Document::Outside
        };
    }

    pub(super) fn navigation_completed(&mut self, id: u64, success: bool) -> bool {
        let current = self.navigation == Some(id);
        if current {
            self.document = if success && self.document != Document::Outside {
                Document::Available
            } else {
                Document::Outside
            };
        }
        current
    }

    pub(super) fn dom_loaded(&mut self, id: u64) -> bool {
        let current = self.navigation == Some(id);
        if current && self.document != Document::Outside {
            self.document = Document::Available;
        }
        current
    }

    pub(super) fn invalidate_nodes(&mut self) {
        if let Err(error) = self.slot.invalidate() {
            self.failed = true;
            self.record(
                &json!({"kind":"state_error","operation":"invalidate","message":error.to_string()}),
            );
        }
        self.last.fill(None);
    }

    pub(super) fn changed(&mut self, selector: usize, value: Value) {
        if self.last[selector].as_ref() != Some(&value) {
            let observation = json!({"kind":"dom_status","selector":self.selection.selectors[selector],
                "generation":self.slot.snapshot().generation.to_string(),"value":value});
            if self.record(&observation) {
                self.last[selector] = Some(value);
            }
        }
    }

    pub(super) fn metadata(&mut self, kind: &str, cleanup_failed: bool) {
        if kind == "closed" && self.metadata_sent {
            return;
        }
        let budget = self.budget.snapshot();
        let request = self.slot.snapshot();
        let encoded = json!({"kind":kind,"records":budget.records,"bytes":budget.bytes,
            "omitted":budget.omitted.to_string(),"exhausted":budget.exhausted,
            "generation":request.generation.to_string(),"pending":request.pending,
            "closed":request.closed,"samples":self.samples.to_string(),"stale":self.stale.to_string(),
            "failed":self.failed,"cleanup_failed":cleanup_failed}).to_string();
        emit(&encoded);
        if kind == "closed" {
            self.metadata_sent = true;
        }
    }
}

pub(super) fn observe(
    state: &Borrowed,
    operation: &str,
    action: impl FnOnce(&mut State) -> windows::core::Result<()>,
) -> windows::core::Result<()> {
    let Some(state) = state.upgrade() else {
        return Ok(());
    };
    let mut state = state.try_borrow_mut().map_err(|_| {
        super::super::error::callback_error(
            "WebView2 startup observation state is already borrowed",
        )
    })?;
    if state.slot.snapshot().closed {
        return Ok(());
    }
    if let Err(error) = action(&mut state) {
        state.error(operation, &error);
        return Err(error);
    }
    Ok(())
}

fn emit(encoded: &str) {
    tracing::debug!(target: crate::webview_startup::TRACE_TARGET,
        event = "webview_startup_observation", observation = %encoded);
}

#[cfg(test)]
mod tests;