One file that says what an engine is
Sources/VoyagerCore/EngineContract.swift opens with its job: "everything the parent may ask of an engine, and every decision an engine hands back to the parent". Tabs, sessions, trust, identities and the Pippa host know these types and nothing below them. The header says everything in the file runs on the main actor, because "a page is a thing on screen." That holds for the protocols: the page, its delegates, the panel and the engine are all marked @MainActor. It does not hold for the plain values. They are Sendable structs and enums with no actor of their own, which is what lets the session writer serialize a PageState on its own background queue.
The file holds two kinds of thing, and telling them apart is most of the design.
Plain values the parent reasons on
PageStatus: address, title, loading and progress, back and forward, secure or not, the icon, and whether the page is making sound. It isEquatable, so a test compares a whole status in one line.PageState: a page's place (address, history, scroll position) as opaque bytes, tagged with the name of the engine that wrote them. The parent stores them and gives them back, and never looks inside.PageOrigin: scheme, host and port, lowercased on the way in. Its comment calls it "what trust is decided on".PermissionKind,PermissionRequest,PermissionDecision: the closed list of what a page may be granted, the frame that asks together with the page it sits in, and an answer of grant, deny or ask.
None of these mentions WebKit. They describe what a browser is about in Swift's own vocabulary, and because they are values they travel into tests and session files as easily as an integer does.
Objects the engine makes, held through protocols
| Protocol | What it is | Who implements it |
|---|---|---|
BrowserEngine | "the thing that turns an identity into pages": makes pages and panels, removes an identity's data, builds one cookie header on request | WebKitEngine, and the tests' FakeEngine |
EnginePage | One page of one identity: load, find, zoom, mute, capture its state, read it for Pippa, close | WebKitPage |
EnginePageDelegate | "The decisions that are Voyager's to make, and the facts a page reports" | the parent's BrowserTab |
EngineDownloadDelegate | Where a download goes, and how it is doing | the parent's DownloadsModel |
PanelSurface and its delegate | cwkPippa's own panel, which "is not a tab, has no identity of Dad's, and never leaves its origin" | WebKitPanel; the parent's PippaPanelHost |
Look at the direction. Questions go down: the parent tells a page to load, asks for its state before a tab sleeps, asks what Dad sees when Pippa needs the page. Decisions come up: when a page wants the microphone, a new window, a dialog or a file, the engine does not decide. It asks its delegate, and the delegate is the parent.
Downloads get a protocol of their own for one reason the file states: "a download goes on after its tab is closed". A download that belonged to a page would die with the page.
One decision, end to end
Follow a single microphone request. WebKit asks the child. The child turns WebKit's security origin and frame into a PermissionRequest that carries both the asking frame's origin and the top page's. BrowserTab answers in one line, window?.services.trust.decision(for: request) ?? .ask, and the child maps grant, deny and ask back onto WebKit's own answer type. The trust rule never touches a WebKit type, so its tests build a PermissionRequest by hand and never make a page at all.
Defaults for an engine that cannot
Protocol extensions in the file give a smaller engine something sensible to say. "An engine that cannot tell is never busy." An engine without an extension host has no extension pages. A panel that cannot count its matches answers nil. These defaults let the contract grow without breaking every implementation at once, and they are why the test fake can stay short.
It has grown. The file was 251 lines in the first brick and is 746 now, across 17 commits, and each addition came with the feature that needed it: PanelSurface with Pippa at the side of the window (a449ddd), reloadFromOrigin() with the hard reload (aaa8bcf), cookiesDidChange with the family catalog's sign-in (ac9bfc3), isBusy() with idle tabs that sleep (3638f97), FindMatches with the find bar's "3 of 17" (24cd64b), isMuted with the tab's speaker (9e3f985).
Polymorphism as a building plan
Dad reads the world through objects, and here the reading is literal. The contract is the parent, the engine is a child that fills it in, and BrowserServices holds its engine as any BrowserEngine, so nothing above that line knows which child it was given. The first plan (224e256) drew two children under one root: Chromium on the Mac, WebKit on iPhone and iPad. Dad's WebKit ruling the same night took one child away, and the architecture kept the root with one sentence: "Dad's ruling that engines are swappable children stands, and a second child would enter here. None is planned."