Skip to content
C.W.K.
Lesson 02 of 05 · published

The Root Contract

~15 min · protocols, polymorphism, architecture, contracts

Level 0New Tab
0 XP0/44 lessons0/13 achievements
0/100 XP to next level100 XP to go0% complete

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 is Equatable, 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

ProtocolWhat it isWho 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 requestWebKitEngine, and the tests' FakeEngine
EnginePageOne page of one identity: load, find, zoom, mute, capture its state, read it for Pippa, closeWebKitPage
EnginePageDelegate"The decisions that are Voyager's to make, and the facts a page reports"the parent's BrowserTab
EngineDownloadDelegateWhere a download goes, and how it is doingthe parent's DownloadsModel
PanelSurface and its delegatecwkPippa'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."

Principle: An abstraction with one implementation has to pay rent today. This one pays twice: the parent is tested as plain values, and the Mac and the phone differ only below the line.

Code

cwkVoyager, Sources/VoyagerCore/EngineContract.swift: the engine, as the parent sees it (verbatim, lines 619-657)·swift
/// An engine: the thing that turns an identity into pages.
@MainActor
public protocol BrowserEngine: AnyObject {
    /// A short stable name; it tags the page states this engine writes.
    var name: String { get }
    /// For diagnostics. The engine belongs to the operating system; Voyager
    /// reports its version and never pins it.
    var version: String { get }

    var downloadDelegate: (any EngineDownloadDelegate)? { get set }

    /// A new page for `identity`, at the place `state` describes when it is
    /// this engine's own.
    func makePage(identity: IdentityID, restoring state: PageState?) -> any EnginePage

    /// A page for an extension's own address, loaded with the extension
    /// context's own web view configuration — the only one WebKit serves
    /// those pages to. Nil when the engine hosts no such extension.
    /// 1Password's Add browser opens its extension's app page by a
    /// `chrome-extension://` address, routed here.
    func makePage(forExtensionPageAt url: URL, identity: IdentityID) -> (any EnginePage)?
    func extensionPageURL(for url: URL) -> URL?
    func canonicalExtensionPageURL(for url: URL) -> URL?

    /// A panel that may load `origin` and nothing else. Its page may use the
    /// microphone without a prompt: it is Pippa's own voice surface, on the
    /// origin Dad configured for her.
    func makePanel(origin: PageOrigin) -> any PanelSurface

    /// Removes one site's cookies and storage from one identity's store.
    func removeData(of identity: IdentityID, host: String) async

    /// Removes an identity's whole store: every cookie and sign-in. No page
    /// of that identity may still be open.
    func removeData(of identity: IdentityID) async throws

    /// The last private window closed. Drop only the ephemeral session's
    /// store synchronously, before another private window can open.
    func endPrivateSession()
cwkVoyager, Sources/VoyagerCore/EngineContract.swift: what a page may be granted, and by whom it is judged (verbatim, lines 89-118)·swift
/// The closed list of things a page may be granted (invariant 6). A new kind
/// is added here by name, with its reason, and nowhere else.
public enum PermissionKind: String, Sendable, Codable, CaseIterable {
    case microphone
    case camera
    case cameraAndMicrophone
}

public struct PermissionRequest: Sendable, Equatable {
    /// The frame that asks.
    public var origin: PageOrigin
    /// The page Dad sees in the tab. A frame inside another site's page is
    /// judged with that page: content never raises its own zone by being
    /// embedded, or by embedding.
    public var topOrigin: PageOrigin
    public var kind: PermissionKind

    public init(origin: PageOrigin, topOrigin: PageOrigin? = nil, kind: PermissionKind) {
        self.origin = origin
        self.topOrigin = topOrigin ?? origin
        self.kind = kind
    }
}

public enum PermissionDecision: Sendable, Equatable {
    case grant
    case deny
    /// The engine asks Dad itself, as Safari would.
    case ask
}

External links

Exercise

Pick one framework your app leans on hardest (a web view, a map, a payment SDK, a database driver) and write its root contract as a Swift file. List the questions your code asks it, and the decisions it must hand back to your code. Make the questions methods on a protocol the framework's wrapper implements, and the decisions methods on a delegate your code implements. Every type in the file must be your own value or a Foundation type.
Hint
If a signature mentions a framework type (a security origin, a map item, a driver's row), move that type below the line: define your own value and let the implementation convert. The test is simple: the contract file compiles with Foundation as its only import.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.