An interior · Part 2 · SafariWebExtensionHandler.swift

Inside SafariWebExtensionHandler.swift

The one place Swift actually runs — read line by line, for someone who reads ClojureScript and is new to Swift.

This is the principal class Safari hands every generated web-extension project: the native entry point Safari instantiates when the web extension sends a native message. By the end of this page you can read its Swift unaided — every construct named, every line with a JS or ClojureScript analogue. One honest caveat up front: organon does not use this bridge on its data path. The captured facts flush over HTTP to 127.0.0.1 (Part 3’s subject), never through sendNativeMessage, so this handler is never actually called. Read it as what Safari hands you, not what organon runs — the canonical shape of the native bridge, worth being able to read even when it sits inert. This interior is reached from Part 2 — the native wall, which frames the Swift as “the Swift you barely write.” Here we step inside that one file.

Act 1 — the shape

The round trip, before any Swift: what happens when the web extension calls sendNativeMessage.

Forget the Swift for a moment and watch the gestalt. The web-extension side — JavaScript, or in organon’s case compiled ClojureScript — calls browser.runtime.sendNativeMessage. Safari packages that call into an NSExtensionContext and instantiates this class, calling its one method, beginRequest(with:). The method reads the message out of the context, builds a reply dictionary, and signals it is done by calling completeRequest — at which point the value travels back to the JavaScript caller’s promise. SafariWebExtensionHandler.swift:13–39 · the whole round trip lives in one method. There is no event loop, no listener registration: Safari calls you once per message and you complete the request once. Play it through:

Each stage maps to a point in the file — beginRequest reads, builds, and completes; the first and last stages are the JavaScript caller on the other side of the bridge.

Act 2 — Swift construct by construct

Rebuild the handler from the simplest thing up, naming each Swift construct with a JS / ClojureScript analogue.

The promise of this act: rebuild the handler from the simplest thing that could work, in three stages, naming each Swift construct as it first appears and pairing it with something a ClojureScript reader already knows. These drafts are simplified — each .srcann below is a teaching scaffold, not the real file (the verbatim source is Act 3). Click any underlined construct token in the code, or any chip in the “Swift you just met” strip, to read what it is and its analogue.

Stage A — the skeleton teaching scaffold

Start with nothing but the frame: bring in the two frameworks, declare the class Safari will instantiate, and stub the one method it calls. No logic yet — just the shape the platform requires.

1import SafariServices
2import os.log
Two frameworks pulled into scope: SafariServices gives the extension types, os.log the system logger.
3class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
A class that subclasses NSObject and conforms to the NSExtensionRequestHandling protocol — the contract Safari calls into.
4 func beginRequest(with context: NSExtensionContext) { }
The one method the protocol demands. with is the external argument label; context is the internal name — Swift has both.

Swift you just met — click a chip:

Stage B — reading the message safely teaching scaffold

Now pull the message out of the context. Every link in that chain can be absent — there may be no input item, no userInfo, no message key — so Swift makes the absence explicit in the types, and short-circuits it safely.

1let request = context.inputItems.first as? NSExtensionItem
A safe downcast: as? yields the value typed as NSExtensionItem, or nil if it is some other type.
2let message = request?.userInfo?[SFExtensionMessageKey]
Optional chaining: each ? short-circuits the whole expression to nil if that link is nil.
3if #available(macOS 11.0, *) { … }
A compile-checked OS-version gate: the body runs only where the API exists. The result type is an optional — value or nil.

Swift you just met — click a chip:

Stage C — replying teaching scaffold

Finally, build the reply and hand it back. Construct a fresh extension item, stuff a nested dictionary into its userInfo, log what arrived, and complete the request — passing nil for the completion callback we do not need.

1let response = NSExtensionItem()
A plain initializer call — NSExtensionItem() is Swift’s new NSExtensionItem().
2response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
A dictionary literal, nested: a map whose value is itself a map. The echo handler simply wraps the message under "echo".
3os_log(.default, "…", String(describing: message))
String(describing:) renders any value as a readable string for the log line.
4context.completeRequest(returningItems: [ response ], completionHandler: nil)
The last argument is a closure; here nil means “no callback when the system finishes.”

Swift you just met — click a chip:

Pick a construct above
Click an underlined token in the code or a chip in a strip to read what the Swift construct is, and its JavaScript / ClojureScript analogue.

Act 3 — the verbatim source

Now the real file, unsimplified. The scaffolds dropped two availability branches and a profile read; here is every line.

Acts 1 and 2 simplified. The real handler does the same three things — read, reply, complete — but guards each modern API behind an availability branch with an older-OS fallback, and reads a profile UUID alongside the message. Below is the file as it ships, line for line. The annotations are the same teaching aids from Act 2; when you think you can read it unaided, hit Read it cold to strip them and test yourself.

8import SafariServices
9import os.log
Same two frameworks as the skeleton: the extension types and the system logger.
10 
11class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
The principal class: subclasses NSObject, conforms to the NSExtensionRequestHandling protocol Safari calls into.
12 
13 func beginRequest(with context: NSExtensionContext) {
The protocol’s one method — the whole round trip happens here.
14 let request = context.inputItems.first as? NSExtensionItem
Safely downcast the first input item to NSExtensionItemnil if there is none.
15 
16 let profile: UUID?
An optional UUID: the Safari profile the message came from, which may be absent.
17 if #available(iOS 17.0, macOS 14.0, *) {
On new-enough OSes, read the profile from the modern key…
18 profile = request?.userInfo?[SFExtensionProfileKey] as? UUID
19 } else {
20 profile = request?.userInfo?["profile"] as? UUID
…and on older ones, fall back to the literal "profile" string key.
21 }
22 
23 let message: Any?
The message itself: Any? — could be any type, or absent.
24 if #available(iOS 15.0, macOS 11.0, *) {
25 message = request?.userInfo?[SFExtensionMessageKey]
Same availability shape as the profile: modern key when available…
26 } else {
27 message = request?.userInfo?["message"]
…literal "message" key as the fallback.
28 }
29 
30 os_log(.default, "Received message from browser.runtime.sendNativeMessage: %@ (profile: %@)", String(describing: message), profile?.uuidString ?? "none")
Log what arrived. String(describing:) renders the message; profile?.uuidString ?? "none" is optional-chaining + the ?? nil-coalescing default.
31 
32 let response = NSExtensionItem()
Build the reply container.
33 if #available(iOS 15.0, macOS 11.0, *) {
34 response.userInfo = [ SFExtensionMessageKey: [ "echo": message ] ]
The echo: a nested dictionary literal wrapping the message under "echo", keyed by the modern message key…
35 } else {
36 response.userInfo = [ "message": [ "echo": message ] ]
…or the literal "message" key on older OSes.
37 }
38 
39 context.completeRequest(returningItems: [ response ], completionHandler: nil)
Hand the reply back to the caller; completionHandler: nil — the trailing closure we do not need.
40 }
41 
42}

SafariWebExtensionHandler.swift:8–42

That is the whole bridge Safari offers: one class, one method, one round trip. organon does not ride it — its facts flush to the localhost receiver, and this handler stays inert — but now you can read it, construct by construct, and recognise the same shape in any other Safari web-extension project you open.

Grounded in the organon Safari-capture extension, June 2026. Citations are file:line into the real source.

Citation colour marks the source — native the generated Swift / native side.

SafariWebExtensionHandler.swift:8–42