Skip to main content

verso_tile/
api.rs

1//! The engine-agnostic flip contract (was the `verso-api` crate).
2//!
3//! A *flip* re-presents the same page through a different engine with the user's
4//! place and session carried across (see
5//! `verso_docs/technical_architecture/2026-06-10_compatibility_view_charter.md`).
6//! This module is the engine-agnostic contract: the portable view-state moved across
7//! a flip, plus the donor / back / receiver traits. It depends on no engine and no
8//! GPU layer. Per-engine adapters ([`crate::scry`], [`crate::serval`], ...) bridge concrete
9//! engines to these traits; the [`crate::flip`] orchestrator pairs a donor with a
10//! receiver and runs the flip choreography.
11
12/// Which layers of view-state a carrier moves. Layers degrade, never block: a
13/// carrier moves `donor.donates() & receiver.receives()`, and any layer outside
14/// that intersection is simply dropped.
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub struct LayerSet(u8);
17
18impl LayerSet {
19    pub const NAV: Self = Self(1 << 0);
20    pub const FORM: Self = Self(1 << 1);
21    pub const SESSION: Self = Self(1 << 2);
22    pub const DOM: Self = Self(1 << 3);
23    pub const VISUAL: Self = Self(1 << 4);
24
25    /// The empty set.
26    pub const fn empty() -> Self {
27        Self(0)
28    }
29    /// Every layer.
30    pub const fn all() -> Self {
31        Self(0b1_1111)
32    }
33    /// Does this set contain every layer in `other`?
34    pub const fn contains(self, other: Self) -> bool {
35        self.0 & other.0 == other.0
36    }
37    /// Layers in both sets — what a `(donor, receiver)` pair can actually carry.
38    pub const fn intersect(self, other: Self) -> Self {
39        Self(self.0 & other.0)
40    }
41    /// Layers in either set. The const companion to [`intersect`](Self::intersect),
42    /// for building a fixed layer set (e.g. an adapter's `RECEIVES`) in a const.
43    pub const fn union(self, other: Self) -> Self {
44        Self(self.0 | other.0)
45    }
46    /// Is the set empty?
47    pub const fn is_empty(self) -> bool {
48        self.0 == 0
49    }
50}
51
52impl core::ops::BitOr for LayerSet {
53    type Output = Self;
54    fn bitor(self, rhs: Self) -> Self {
55        Self(self.0 | rhs.0)
56    }
57}
58
59/// A cookie's `SameSite` attribute (RFC 6265bis). Carried so the gating semantics
60/// survive a flip; `None` means unspecified (engines default it to `Lax`).
61#[derive(Clone, Copy, Debug, PartialEq, Eq)]
62pub enum SameSite {
63    Strict,
64    Lax,
65    None,
66}
67
68/// A cookie in engine-agnostic terms, mirroring the RFC 6265bis record (the shape
69/// the [`cookie`](https://crates.io/crates/cookie) crate uses, which both Mere's
70/// netfetcher jar and Servo's net jar build on). Adapters convert to/from the
71/// engine's own cookie type; carrying the full record keeps a flip's session lossless.
72#[derive(Clone, Debug, Default, PartialEq)]
73pub struct Cookie {
74    pub name: String,
75    pub value: String,
76    pub domain: String,
77    pub path: String,
78    pub secure: bool,
79    pub http_only: bool,
80    /// `SameSite` gating, or `None` when unspecified.
81    pub same_site: Option<SameSite>,
82    /// Absolute expiry in Unix seconds (the normalized form of `Expires` / `Max-Age`),
83    /// or `None` for a session cookie.
84    pub expires: Option<f64>,
85    /// `Partitioned` (CHIPS): the cookie is keyed to the top-level site.
86    pub partitioned: bool,
87}
88
89/// Form field values keyed by a stable selector. Best-effort across engines.
90#[derive(Clone, Debug, Default)]
91pub struct FormValues(pub Vec<(String, String)>);
92
93/// An opaque reference to the donor's last rendered frame, resolved by the host
94/// compositor. Texture plumbing stays the compositor's (charter §2); verso only
95/// *refers* to a frame so a flip can cross-fade instead of flashing.
96#[derive(Clone, Copy, Debug, PartialEq, Eq)]
97pub struct FrameHandle(pub u64);
98
99/// The rich state a glass-box primary (serval, nematic) can export. Layered so it
100/// degrades gracefully (charter §3.1): a receiver takes the layers it supports and
101/// ignores the rest.
102#[derive(Clone, Debug, Default)]
103pub struct PortableViewState {
104    /// Current document URL.
105    pub url: Option<String>,
106    /// Document scroll offset in device px.
107    pub scroll: (f32, f32),
108    /// Form field values.
109    pub form: Option<FormValues>,
110    /// Cookies for the origin (one-shot at flip time; the substrate splits after).
111    pub cookies: Vec<Cookie>,
112    /// Serialized outerHTML — the degrade path when the URL is not refetchable.
113    pub dom_snapshot: Option<String>,
114    /// The donor's last frame, for the cross-fade.
115    pub visual: Option<FrameHandle>,
116}
117
118/// The lean locator a black-box secondary (scry, weld, graft) can surface for a
119/// flip-back. The receiver re-roots from this — it re-fetches the URL and renders
120/// from source — and never reconstructs a live document out of the black box.
121#[derive(Clone, Debug, Default)]
122pub struct BackState {
123    pub url: String,
124    pub scroll: (f32, f32),
125    pub form: Option<FormValues>,
126    pub cookies: Vec<Cookie>,
127}
128
129/// What a receiver is asked to present.
130pub enum Carry {
131    /// Primary → secondary: the rich forward carry.
132    Forward(PortableViewState),
133    /// Secondary → primary: the lean locator to re-root from.
134    Back(BackState),
135}
136
137/// A glass-box primary engine that can export its full live state. Implemented by
138/// serval and nematic through their `verso-*` adapters.
139pub trait FlipDonor {
140    /// The layers this donor can export.
141    fn donates(&self) -> LayerSet;
142    /// Capture the current live state.
143    fn capture(&self) -> PortableViewState;
144}
145
146/// A black-box secondary engine that can only surface a locator for flip-back.
147///
148/// Secondaries implement `FlipBack` and **never** [`FlipDonor`]. That is the
149/// charter's no-chain invariant (§4) expressed in the type system: there is no path
150/// to forward-donate a document to another secondary, so a flip is always one hop
151/// (primary ↔ secondary), re-rooting at the lossless source rather than chaining.
152pub trait FlipBack {
153    /// Extract the cheap locator (URL, scroll, form, cookies) the black box exposes.
154    fn extract(&self) -> BackState;
155}
156
157/// An engine that can host a flipped page. A secondary receiver consumes a
158/// [`Carry::Forward`] by navigating; a primary receiver consumes a [`Carry::Back`]
159/// by re-fetching the URL and re-rendering from source.
160pub trait FlipReceiver {
161    /// The layers this receiver can apply.
162    fn receives(&self) -> LayerSet;
163    /// Present the carried state.
164    fn present(&mut self, carry: Carry);
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn layerset_intersection_is_the_carried_set() {
173        let donor = LayerSet::all();
174        let receiver = LayerSet::NAV | LayerSet::SESSION | LayerSet::VISUAL;
175        let carried = donor.intersect(receiver);
176        assert!(carried.contains(LayerSet::NAV));
177        assert!(carried.contains(LayerSet::SESSION));
178        assert!(!carried.contains(LayerSet::DOM));
179    }
180
181    #[test]
182    fn empty_and_all_bound_the_lattice() {
183        assert!(LayerSet::empty().is_empty());
184        assert!(LayerSet::all().contains(LayerSet::DOM));
185        assert!(LayerSet::all().contains(LayerSet::VISUAL));
186    }
187}