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}