rastrillo / native Public

Clone
git clone https://amadan.net/rastrillo/native

Plain git — no account needed to clone.

Download

Download this file

1# Rastrillo Native
2
3Optional native components and app starting points in the Rastrillo family.
4The first package supports iOS 17+ and macOS 14+. Android and Go Mobile
5integration are documented extension points, not tested shipped adapters.
6
7## Use the package
8
9Add `https://amadan.net/rastrillo/native` to your Swift package dependencies,
10pinned to a reviewed commit, then depend on its `RastrilloNative` product.
11Read [SKILL.md](SKILL.md) before integrating it.
12
13```swift
14import RastrilloNative
15
16@MainActor
17final class Inbox {
18 private let refresh = CoalescedRunner()
19
20 func reload() async {
21 await refresh.run {
22 // Fetch and publish this inbox's current state here.
23 }
24 }
25}
26```
27
28Use a runner for one operation and account. Calls during a refresh queue one
29trailing refresh; every caller waits until shared work completes. Calls
30during the trailing refresh can queue another pass. The first caller's
31closure supplies every pass, so do not mix different operations in one runner.
32Handle errors within that closure. Cancelling a caller does not cancel the
33shared refresh. Calling the same runner from its own pass deadlocks.
34
35## Start a companion app
36
37`examples/companion` is a small SwiftUI app with iOS and macOS targets. It
38connects to a Rastrillo server's public `/api/version` endpoint. It has no
39account linking, credentials, offline data or push enrolment.
40
41Copy that directory into your app. Replace the local package path in
42`project.yml` with the repository URL and a reviewed revision, change the
43bundle identifiers, and add your app's screens. Run `xcodegen generate` in
44the copied directory and open `Companion.xcodeproj`. Sign with your own team
45for physical devices or distribution.
46
47Prefer fully native UI so navigation, menus, contextual actions, links and
48accessibility meet the platform's expectations. For a complex app, keep a
49native shell around selected webview screens. A shared app manifest can
50describe destinations and commands for separate web and native renderers;
51that is a possible architecture, not a compiler supplied by this kit. See
52[docs/app-architecture.md](docs/app-architecture.md). Bind shared Go logic only
53when it removes duplication: see [docs/go-mobile.md](docs/go-mobile.md).
54
55## Validate
56
57Apple editor clients use `RastrilloApple.NativeHTTP` for ephemeral, origin-bound
58requests. `responseDetails` returns body, status and case-insensitive headers for
59protocol cursors. `response` and `request` retain the 4 MiB default body bound;
60an explicit `maxBytes` allows larger snapshots/exports up to 256 MiB. Oversized
61responses fail rather than returning truncated content.
62
63`stream(path:onOpen:onLine:)` requires a 200 `text/event-stream` response and supplies
64UTF-8 lines, including empty event delimiters. It accepts CR, LF and CRLF,
65limits each line to 256 KiB and each event to 1 MiB, and honors task cancellation.
66The optional `onOpen` callback runs after response validation and completes before
67any lines are delivered. A server that subscribes before flushing response headers
68allows this callback to fetch an initial snapshot without missing intervening edits.
69The consumer parses events, revalidates permissions through its server, and
70refetches/reconnects after EOF. An unfinished event at EOF must not be applied.
71Streams use the same no-redirect session and origin-bound credentials as ordinary
72requests. Responses and streams are not persisted.
73
74`download(_:to:)` streams an explicit user export into a new file, with the same
75origin and redirect restrictions. It requires HTTP 200 and a file URL, applies
760600 permissions, and refuses to overwrite an existing destination. The caller
77owns that temporary export and must remove it after the save panel completes
78or is cancelled. Unlike in-memory responses, downloads have no body-size cap.
79
80### Typed errors and `NativeTransport` (in-progress primitive)
81
82This is the first slice of the offline kit (spec: `docs/superpowers/specs/2026-09-23-offline-kit.md`
83on `spec/offline-kit`). It stays marked in progress until Sheets has adopted it.
84
85`NativeClientError` tells an offline failure apart from an ended session:
86`.offline(URLError.Code)` covers `notConnectedToInternet`, `networkConnectionLost`,
87`cannotFindHost`, `cannotConnectToHost`, `dnsLookupFailed`, `timedOut`, `dataNotAllowed`
88and `internationalRoamingOff`. Other transport failures, TLS among them, are
89`.transport(code)`, retried as transient and never shown as offline. Statuses map
90to `.unauthorized` (401 and 303), `.forbidden`, `.notFound`, `.conflict(body)`,
91`.tooLarge`, `.rateLimited(retryAfter:)` (429, or 503 with Retry-After),
92`.server(status:)` and `.rejected(status:body:)`. `retryClass` sorts every case into
93offline, transient, auth, permanent or conflict. `localizedDescription` keeps the
94sentences callers showed before. A cancelled request throws `CancellationError`,
95never URLSession's -999.
96
97`responseDetails` and `response` still return any status; `NativeResponse.requireSuccess()`
98throws its typed case. `request` and `stream` throw typed cases, so a 401 on a
99stream is `.unauthorized`. `NativeHTTP(origin:credential:observer:)` reports every
100response and every offline-class failure to the optional observer, so connectivity
101can be learned from real traffic.
102
103`NativeTransport` is the protocol `NativeHTTP` conforms to. Code that can outlive
104a session should take a supplier, `@MainActor () -> NativeTransport?`, rather than
105capture one client, because signing in again replaces it. The
106`RastrilloTestSupport` product ships `ScriptedTransport`, which answers each call
107from a script: a status and body, offline, a hang until cancelled, or "commit,
108then lose the response". Scripted streams are framed by the same byte-level parser
109as `NativeHTTP`. Use it in test targets in place of fixture servers.
110
111### Connectivity and backoff (in-progress primitive)
112
113`NativeConnectivity.shared` (one per process) combines `NWPathMonitor`'s path
114with what real traffic proves: every client `NativeConnection` makes reports its
115responses and offline-class failures, so `serverReachable[origin]` is learned
116without a probe. `isOffline(origin)` is true while the path is down, or while it
117is up and that server just failed to answer. `changes` fires on a path change,
118on a server becoming reachable and on return to the foreground.
119
120A stream loop uses `NativeReconnect(connectivity:)`: call `opened()` from the
121stream's `onOpen`, `failed(error)` when an attempt ends in an error, and
122`await pause()` before the next attempt. A server's Retry-After (up to 5 minutes)
123is a deadline nothing crosses, not even a connectivity change or foreground.
124Otherwise the pause waits out `Backoff` (in `RastrilloNative`: 0.5 s doubling to
12510 s with equal jitter), cut short and reset by a connectivity change, with no
126timer at all while the path is down. The backoff resets only after a stream that
127stayed open longer than 5 s, timed from `opened()`. Lower-level loops can call
128`connectivity.wait(upTo:)` and `Backoff.opened(at:)`/`ended(at:)` directly.
129`NativeOfflineBanner(origin:)`
130is a thin bar shown while that origin is offline; pass durable wording only once
131edits survive a relaunch. The kit has no string catalog yet: apps pass their own
132localised copy. The catalog, reviewed in English and then translated into the
133package's 12 languages, is a requirement of K3, together with `SyncStatus`.
134
135`tools/bundle-mac.py` includes SwiftPM resource bundles in `Contents/Resources`
136and reads the minimum macOS version from the package manifest. A SwiftPM
137consumer with resources should first locate its resource bundle with
138`Bundle.main.url(forResource: "Package_Target", withExtension: "bundle")`,
139then lazily fall back to `Bundle.module` for command-line development builds.
140SwiftPM's generated accessor alone looks in the app root and the original build
141directory; neither is the signed Mac app's resource directory. The packaging
142test relocates a signed app, deletes its build tree, and verifies resource access.
143
144`make ci` runs the Swift tests and compiles both companion targets. It
145requires macOS, Xcode and XcodeGen. Builds are unsigned; this gate does not
146claim physical-device, signing or store-distribution validation.
147
148## Provenance
149
150`CoalescedRunner` comes from Eleven/Ocho's `ios/LChatCore` at commit
151`000b882c`, also copied into Keymail's `apple/KeymailCore`. The extraction
152preserves its implementation and tests the failure it prevented: concurrent
153refresh bursts and callers resuming before their new data has been fetched.
154Consumer branches replace the copies with public type aliases so existing
155call sites keep their API. See the adoption status on amadan before treating
156a consumer branch as landed.
157
158MPL-2.0; see [LICENSE](LICENSE). The extracted source retains that licence.
159