rastrillo / native Public
| .amadan/ | ||
| Sources/ | ||
| Tests/ | ||
| docs/ | ||
| examples/ | ||
| server/ | ||
| tools/ | ||
| .gitignore | ||
| ACCOUNTS-SIGNIN.md | ||
| AGENTS.md | ||
| APPLE.md | ||
| LICENSE | ||
| Makefile | ||
| Package.resolved | ||
| Package.swift | ||
| README.md | ||
| SKILL.md |
Rastrillo Native
Optional native components and app starting points in the Rastrillo family. The first package supports iOS 17+ and macOS 14+. Android and Go Mobile integration are documented extension points, not tested shipped adapters.
Use the package
Add https://amadan.net/rastrillo/native to your Swift package dependencies, pinned to a reviewed commit, then depend on its RastrilloNative product. Read SKILL.md before integrating it.
import RastrilloNative
@MainActor
final class Inbox {
private let refresh = CoalescedRunner()
func reload() async {
await refresh.run {
// Fetch and publish this inbox's current state here.
}
}
}Use a runner for one operation and account. Calls during a refresh queue one trailing refresh; every caller waits until shared work completes. Calls during the trailing refresh can queue another pass. The first caller's closure supplies every pass, so do not mix different operations in one runner. Handle errors within that closure. Cancelling a caller does not cancel the shared refresh. Calling the same runner from its own pass deadlocks.
Start a companion app
examples/companion is a small SwiftUI app with iOS and macOS targets. It connects to a Rastrillo server's public /api/version endpoint. It has no account linking, credentials, offline data or push enrolment.
Copy that directory into your app. Replace the local package path in project.yml with the repository URL and a reviewed revision, change the bundle identifiers, and add your app's screens. Run xcodegen generate in the copied directory and open Companion.xcodeproj. Sign with your own team for physical devices or distribution.
Prefer fully native UI so navigation, menus, contextual actions, links and accessibility meet the platform's expectations. For a complex app, keep a native shell around selected webview screens. A shared app manifest can describe destinations and commands for separate web and native renderers; that is a possible architecture, not a compiler supplied by this kit. See docs/app-architecture.md. Bind shared Go logic only when it removes duplication: see docs/go-mobile.md.
Validate
Apple editor clients use RastrilloApple.NativeHTTP for ephemeral, origin-bound requests. responseDetails returns body, status and case-insensitive headers for protocol cursors. response and request retain the 4 MiB default body bound; an explicit maxBytes allows larger snapshots/exports up to 256 MiB. Oversized responses fail rather than returning truncated content.
stream(path:onOpen:onLine:) requires a 200 text/event-stream response and supplies UTF-8 lines, including empty event delimiters. It accepts CR, LF and CRLF, limits each line to 256 KiB and each event to 1 MiB, and honors task cancellation. The optional onOpen callback runs after response validation and completes before any lines are delivered. A server that subscribes before flushing response headers allows this callback to fetch an initial snapshot without missing intervening edits. The consumer parses events, revalidates permissions through its server, and refetches/reconnects after EOF. An unfinished event at EOF must not be applied. Streams use the same no-redirect session and origin-bound credentials as ordinary requests. Responses and streams are not persisted.
download(_:to:) streams an explicit user export into a new file, with the same origin and redirect restrictions. It requires HTTP 200 and a file URL, applies 0600 permissions, and refuses to overwrite an existing destination. The caller owns that temporary export and must remove it after the save panel completes or is cancelled. Unlike in-memory responses, downloads have no body-size cap.
Typed errors and NativeTransport (in-progress primitive)
This is the first slice of the offline kit (spec: docs/superpowers/specs/2026-09-23-offline-kit.md on spec/offline-kit). It stays marked in progress until Sheets has adopted it.
NativeClientError tells an offline failure apart from an ended session: .offline(URLError.Code) covers notConnectedToInternet, networkConnectionLost, cannotFindHost, cannotConnectToHost, dnsLookupFailed, timedOut, dataNotAllowed and internationalRoamingOff. Other transport failures, TLS among them, are .transport(code), retried as transient and never shown as offline. Statuses map to .unauthorized (401 and 303), .forbidden, .notFound, .conflict(body), .tooLarge, .rateLimited(retryAfter:) (429, or 503 with Retry-After), .server(status:) and .rejected(status:body:). retryClass sorts every case into offline, transient, auth, permanent or conflict. localizedDescription keeps the sentences callers showed before. A cancelled request throws CancellationError, never URLSession's -999.
responseDetails and response still return any status; NativeResponse.requireSuccess() throws its typed case. request and stream throw typed cases, so a 401 on a stream is .unauthorized. NativeHTTP(origin:credential:observer:) reports every response and every offline-class failure to the optional observer, so connectivity can be learned from real traffic.
NativeTransport is the protocol NativeHTTP conforms to. Code that can outlive a session should take a supplier, @MainActor () -> NativeTransport?, rather than capture one client, because signing in again replaces it. The RastrilloTestSupport product ships ScriptedTransport, which answers each call from a script: a status and body, offline, a hang until cancelled, or "commit, then lose the response". Scripted streams are framed by the same byte-level parser as NativeHTTP. Use it in test targets in place of fixture servers.
Connectivity and backoff (in-progress primitive)
NativeConnectivity.shared (one per process) combines NWPathMonitor's path with what real traffic proves: every client NativeConnection makes reports its responses and offline-class failures, so serverReachable[origin] is learned without a probe. isOffline(origin) is true while the path is down, or while it is up and that server just failed to answer. changes fires on a path change, on a server becoming reachable and on return to the foreground.
A stream loop uses NativeReconnect(connectivity:): call opened() from the stream's onOpen, failed(error) when an attempt ends in an error, and await pause() before the next attempt. A server's Retry-After (up to 5 minutes) is a deadline nothing crosses, not even a connectivity change or foreground. Otherwise the pause waits out Backoff (in RastrilloNative: 0.5 s doubling to 10 s with equal jitter), cut short and reset by a connectivity change, with no timer at all while the path is down. The backoff resets only after a stream that stayed open longer than 5 s, timed from opened(). Lower-level loops can call connectivity.wait(upTo:) and Backoff.opened(at:)/ended(at:) directly. NativeOfflineBanner(origin:) is a thin bar shown while that origin is offline; pass durable wording only once edits survive a relaunch. The kit has no string catalog yet: apps pass their own localised copy. The catalog, reviewed in English and then translated into the package's 12 languages, is a requirement of K3, together with SyncStatus.
tools/bundle-mac.py includes SwiftPM resource bundles in Contents/Resources and reads the minimum macOS version from the package manifest. A SwiftPM consumer with resources should first locate its resource bundle with Bundle.main.url(forResource: "Package_Target", withExtension: "bundle"), then lazily fall back to Bundle.module for command-line development builds. SwiftPM's generated accessor alone looks in the app root and the original build directory; neither is the signed Mac app's resource directory. The packaging test relocates a signed app, deletes its build tree, and verifies resource access.
make ci runs the Swift tests and compiles both companion targets. It requires macOS, Xcode and XcodeGen. Builds are unsigned; this gate does not claim physical-device, signing or store-distribution validation.
Provenance
CoalescedRunner comes from Eleven/Ocho's ios/LChatCore at commit 000b882c, also copied into Keymail's apple/KeymailCore. The extraction preserves its implementation and tests the failure it prevented: concurrent refresh bursts and callers resuming before their new data has been fetched. Consumer branches replace the copies with public type aliases so existing call sites keep their API. See the adoption status on amadan before treating a consumer branch as landed.
MPL-2.0; see LICENSE. The extracted source retains that licence.