Repository navigation
feat: swift client - #1
Merged
Merged
Conversation
Record the scoping decisions for a native Swift Zero client on iOS 26 and macOS 26: architecture, API, storage, lifecycle, conformance suite, drift harness, distribution, and milestones M0-M8. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Add the swift/ package with Zero, ZeroUI, and ZeroTesting targets, the private zero-conformance package that emits vectors from the JS implementation, and workflows for Linux, macOS, and the SwiftPM mirror. The orb setup installs the Swift 6.2.4 toolchain. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Add the Swift JSONValue model with a JSON.parse-compatible parser and a JSON.stringify-compatible serializer, JS number formatting and parsing, UTF-8 and UTF-16 string orders, xxHash32 (h32/h64/h128), the AST with normalization and query hashing, value comparison, primary-key storage keys, LIKE/ILIKE, and TTL. Each is checked against vectors that the zero-conformance package generates from the JS implementation; seeded fuzz corpora are generated in CI and shared with the Swift job. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…uilder Port the client IVM engine 1:1 from packages/zql: MemorySource with its B+tree indexes and push overlays, Filter/Exists/FanOut/FanIn, Join, FlippedJoin, UnionFanOut/In, Take, Skip, Cap, TakeGate, the pipeline builder with correlated predicate pushdown, ArrayView change application, and the untyped AnyQuery builder. The zero-conformance package gains IVM vectors generated by running the JS builder and engine: 34 curated programs and a seeded fuzz corpus, each replaying a push sequence and recording the view and the changes it receives. Swift replays the same programs through its builder and engine and must match the AST, format, view, and changes at every step. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Add Swift types for every upstream and downstream message of
packages/zero-protocol, with their bodies, the error kind, origin and
reason enums, binary poke chunk decoding, the Sec-WebSocket-Protocol
encoding, and normalizeClientSchema. Decoding accepts and rejects what
valita does in passthrough mode. The types drop unknown keys, as strip
mode does.
Valita behaviors that the types reproduce:
- Upstream is an open tagged tuple union. A message with an unknown
string tag decodes to .unrecognized, kept as sent. Downstream is
closed.
- ASTs sent by clients use strictASTSchema: no unknown keys, and at most
50 levels deep. ASTs from zero-cache are lenient.
- Mutation results follow the JS client: it parses in passthrough mode
and then checks 'error' in result. Valita returns just {data} once
data is present, so a result with data is OK even when it also has an
error key.
- pushResponse tries the OK variant first. jsonObjectSchema (rows patch
merge) also accepts arrays.
- Inspect bodies are checked by a small valita interpreter and kept as
stripped JSON.
The new zero-conformance generator emits vectors for downstream and
upstream messages (hand-written, seeded random, and randomly edited),
poke chunks split at seeded byte boundaries, sec protocols, and client
schema normalization. Larger fuzz corpora go to generated/.
Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859
Co-authored-by: tmm <tmm@tmm.dev>
Node 24.22's V8 misreads the escaped key in `{"\n":2}` as a backslash once it
has parsed an object keyed by a backslash, so the poke-chunk fuzz corpus CI
generated on Node 24 held wrong expectations. Generate vectors with the Node
in .nvmrc, and refuse to emit vectors on a runtime whose JSON.parse fails the
probe.
Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859
Co-authored-by: tmm <tmm@tmm.dev>
Add the transport protocol the engine talks through, the sync connect URL, client-to-server name mapping, and the client schema the handshake sends. A Linux end-to-end suite connects to zero-cache over a test-only SwiftNIO WebSocket transport, asks for a named query, and checks the rows it receives through binary poke chunks. packages/zero-conformance/src/e2e runs the server side: a Postgres database loaded from a fixture, an API server for the app's named queries and mutators, and zero-cache from source. CI runs it on the Ubuntu runner's Postgres. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Add the typed layer over AnyQuery: generated row structs conform to ZeroRow and name their columns and relationships by key path, Query<Row, Result> builds with key paths and a `$0.column >= value` expression DSL, and views decode into the row structs. Column values go through ZeroValue, with string enums, integer columns, and typed JSON objects. packages/zero-swift-cli is the `zero-swift codegen` command. It loads the TS schema, reads enumerations and json<T>() types with the TypeScript checker, and emits the row structs, their enum and JSON types, the ZeroSchema, and the `zql` builder. The Swift tests compile the Swift it generates for the zql test schema and a fixture that covers keywords, reserved names, shared enums, nested JSON, integer overrides, compound keys, and two-hop relationships; CI checks the checked-in output is current. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Runs 127 scenarios (connection, errors, pokes, queries, mutations) against TestZero over a MockSocket with fake timers and memory storage, and records what the app and the server observe after every step. The Swift client replays them in M4. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Port the zero-client engine: the connection run loop and state machine, the replica with its pending mutations and rebase, the pusher, the query manager with TTLs and got queries, the poke handler, the mutation tracker, auth and recovery. It runs synchronously on the main actor: an event loop reproduces JS microtask order, timers come from an injected Scheduler, and sockets are callback-based. ZeroTesting gains ManualScheduler (fake timers) and MockSocket. The public API is Zero<Context> with defineQuery/defineMutator; mutator bodies are plain throws. A view is destroyed when its last reference goes. All 127 client scenario vectors from the JS client pass. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
The client harness gains `restart`: close the client and start a new one on the same in-memory storage, optionally later or with other options or mutators. New scenarios cover what a relaunched client starts from (rows, cookie, got queries, pending mutations of earlier clients), client group selection by mutator names, storage names, client GC with deleteClients, and the server errors that disable a client group or drop the store. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
The replica is stored per JS database name in SQLite (or in process memory with ZeroStorage.memory): client groups, clients and their heartbeats, deleted clients, and each group's snapshot and pending mutations. A new client joins the group with its mutator names and replays the pending mutations; writes go through on a background queue in order. Heartbeats, client and client group GC, deleteClients, collecting unused databases, disabling a group or dropping the store on server errors, and the memory fallback with onStorageFailure follow Replicache. All 144 client scenarios pass on memory and on SQLite. Definition overloads now let a context-free app write `(tx, args)`; NoArgs marks a no-argument definition that reads the context. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
The harness stubs document with one whose visibility a step sets, and passes hiddenTabDisconnectDelay through, so the client's background and foreground behavior is recorded from the JS client like the rest. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
- Logging: LogSink and logLevel (default warn) at the JS client's log points; os.Logger on Apple platforms, stderr elsewhere. - Views: snapshots with row diffs (listener and AsyncStream); LiveQuery, an @observable typed query; Connection is @observable with updateNeeded. - Recovery: without onClientStateNotFound, Zero replaces its client in place and moves views, live queries, and preloads to it. - Visibility: disconnect in the background, reconnect in the foreground (hiddenTabDisconnectDelay semantics); reconnect without the backoff when the network is back; wait for pending mutations before suspension. - Apple: URLSessionTransport (the default socket factory), the UIKit/AppKit lifecycle and NWPathMonitor hooks, a SwiftUI @ZeroQuery and environment, and a diffable data source adapter. - Transport frames are written by one task, so they keep their order. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
DynamicProperty is not main-actor isolated, so ZeroQuery conforms with @preconcurrency; AppKit snapshots reload changed rows, having no reconfigure. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Syncs a named query, runs mutations optimistically until the API server confirms them, sees them from a second client, and relaunches on the same SQLite store. The test schema is generated for the E2E target too. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
zero-swift drift record runs an app's TS queries and mutators on samples in offline clients and records their ASTs, outcomes, and row changes. Drift.failures (ZeroTesting) replays the record against the Swift port. Query gains a partial-row start, a where that takes nil as JS takes undefined, and and/or over optional conditions; escapeLike is ported. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
apps/zbugs-swift ports apps/zbugs/shared to the Swift client and checks the port against the TS definitions on 37 query and 38 mutator samples. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
The drift recorder runs each mutator sample again with the clock a day later and fails when the runs differ. DESIGN.md records the M6 and M7 decisions. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…port zbugs' Postgres allows nulls in columns its schema says can't be null, and JS shows those rows. Swift failed to decode them, which emptied the whole view. A null in a non-optional String, Double, Int, or Bool column now reads as "", 0, or false; enums and JSON types still fail. NIOTransport moves from the E2E tests into a ZeroNIOTesting product, so apps can run their own end-to-end tests on Linux, where corelibs' URLSessionWebSocketTask never completes a connection. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
The GitHub login sets the JWT as a cookie, which the Swift app's web authentication session can't read. With redirect=zbugs-swift://auth the API redirects there with the JWT in the query instead. `pnpm dev-token <userID> [crew|user]` signs a JWT with the dev key, for clients that can't log in through GitHub in development and tests. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
ZBugsUI has the app's screens: the issue list with zbugs' filters and sorts, issue detail with comments, and creating, editing, closing, and commenting, all through the ported queries and mutators. The list also comes in UIKit and AppKit through DiffableAdapter, picked in the sidebar. Login takes a zbugs JWT, from the GitHub login through a web authentication session or from `pnpm dev-token`, and keeps it in the keychain. App/ is an XcodeGen project with one multiplatform target, which macOS CI builds for the iOS simulator and macOS. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
server/server.ts runs zbugs' backend from this repository's sources: a fresh database with zbugs' migrations and seed data, the API server, and zero-cache. BackendTests lists, creates, and comments through it, so the API server runs the TS mutators and queries next to the Swift ones, and checks that a second client sees the result and that both sides report zbugs' errors alike. The Linux end-to-end job runs it. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
- The mutation tracker found a mutation by scanning the outstanding ones, so mutations slowed down as they piled up (229 µs each over a second of them, now 21 µs). It keeps them by ID, as the JS Map does. - A live query read every row again on every change. It now reuses the rows of unchanged entries, matched in order (92 µs instead of about 640 µs per update of a 1000-row list). - B-tree nodes skip dynamic exclusivity checks (the tree copies shared nodes before writing them), and the forward iterator reads a leaf's keys once rather than per element: a 1000-element scan went from 23 µs to 2.7 µs, and views hydrate faster. The tree is inlinable, so other modules get it specialized. - A view's hydration takes its row list out once, not once per row. - UTF-8 string comparison and code unit equality compare bytes. - Reading a column is inlinable and specialized into the row structs. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
ZeroBenchmarks ports zero.bench.ts and the shared B-tree and hash benchmarks under their names, and adds the Swift client's own: memory for 100k rows, hydration of up to 100k rows (as structs too), pokes, and mutations including the client's side of a round trip. `swift run -c release ZeroBenchmarks [filter…]`. packages/zero-conformance has the JS side of those that have one (`pnpm --filter zero-conformance bench`). BENCHMARKS.md has both sets of numbers from one machine: Swift is faster on all the client benchmarks and B-tree scans, and slower on B-tree lookups. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
The IVM operators, wire-protocol bodies, B-tree, hashes, and JSON machinery were public, so apps (and the docs) saw ~1,700 symbols. They are package now: the package's tests, ZeroTesting, and the benchmarks still see them; apps see what they and the generated code use (~720). ErroredQuery and escapeLike stay public (views report the one; zbugs uses the other). zbugs reads JWT claims with JSONDecoder instead of the engine's parser. A package type with @inlinable members has to be @usableFromInline, or other modules quietly get unspecialized calls: BTreeSet<Int> scans from the benchmarks were 5x slower until it was. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Each row is hosted in its own hosting view, cut off from the list's environment, so a @ZeroQuery in a row had no client and stayed empty without an error. Rows now get the list's .zero(_:) client. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…'s frame timing
On the iOS simulator, real-fling tests failed the momentum check in their
first frames ("speed 4590 between 10413.9 and 4290"), while no row was out
of place: the scroll offset and the rows moved together in every frame.
The simulator's display link fires on time, but its callbacks come late
by varying amounts, so a frame's sample can be one frame ahead of its
timestamp, and the next pair's speed spikes. Under a fast fling,
deceleration alone also moves a pair's speed by more than the fixed 300
pt/s band.
A pair's speed is now compared with the mean of its neighbors', within
300 pt/s or 15 % of it, and frames within two of one whose callback came
more than half a frame later than the last are skipped, as late frames
already were. Replayed on the iPhone traces, it still finds bf41176's
jump under a real fling.
…hey are Rows' hosting views already inherit the list's environment, Zero client and custom values alike (checked on macOS and the iOS 26.5 simulator), so 0ff3f12's explicit .environment(\.zero, zero) was redundant. It goes, and so does ZeroVirtual's dependency on ZeroUI; the doc comment now says rows inherit. The test stays, guarding the inheritance. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…lGeometryHost goes VirtualList lays its rows out with LayoutHost since a018d98, and nothing used ScrollGeometryHost. Its scenario tests (paging, taller rows, stick to bottom, a far jump, flings, a fling to the top from a permalink) now drive LayoutHost through a fake view that runs its layout passes, and check every pass, not only where the list settles: a row the test holds is never drawn moved, the core never scrolls under a fling away from the start, and no gap stands above the first row at the top. A new case checks LayoutHost takes leftover space at the start out at once. The tests of ScrollGeometryHost's own reporting go with it. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
93af68b committed the renamed ScrollGeometryHost tests, not their port to LayoutHost, so nothing built. This is the port it describes. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
VirtualListView is VirtualList for UIKit and AppKit apps, with rows that are the app's own views. It takes the same ZeroVirtualizer and a row closure, (item, reusing) -> UIView/NSView, which makes a row's view or shows the row in one another row left. Views of rows that leave are kept hidden in one pool. A row whose data changes is shown again in its own view. Heights come from Auto Layout at the list's width (systemLayoutSizeFitting; on macOS, fittingSize with a width constraint, after laying out at that width so wrapping labels wrap there), and rowHeightDidChange(_:) is for rows that change height on their own. It's the same native scroll view as VirtualList's, exposed as scrollView for insets and delegates, so the rows' frames, the content size and the offset still change in one layout pass. It follows the virtualizer while it's in a window. AppleTests open it in a window and scroll to a row far below, and check that a reused view shows its new row; each fails when its part is undone (not attaching; reusing a view without showing the new row in it). Underneath, RowsLayout no longer hosts SwiftUI itself: how a row's view is made, shown again when its row changes, and measured is a RowViews (HostedRowViews for VirtualList, unchanged in behavior; AppRowViews here), and the rest of the layout is the same for both.
… too With -native 1, TestHost's chat and issues lists are VirtualListViews of UIKit or AppKit rows (NativeRowView: two labels and the probe's marker, one accessibility element like the SwiftUI rows). NativeVirtualListUITests and NativeFrameProbeUITests run the same tests on them, and CI runs each in its own job.
ubuntu-latest moved to Ubuntu 26.04, and the cache, keyed by Playwright's version alone, restored a WebKit built for 24.04 that can't load libicudata.so.74 there: Public Test failed on main at b39e85c. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
Git merged pnpm-lock.yaml without a conflict, but upstream's move to esbuild 0.28.2 and vite 8.3.4 left the zero-swift-cli and zero-conformance importers on the old versions, so a frozen install failed. Those importers now take the new versions; nothing else in the lockfile changes. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…buntu 24.04 Public JS runs on pull requests into main, so PR #1 was the first to check zero-swift's CLI: - syncpack: its esbuild follows the workspace's 0.28.2. It stays on TypeScript 6, as it uses the compiler API at runtime, which TypeScript 7 doesn't have, and its @rocicorp/zero peer range is the app's, not the workspace's version; both are recorded in .syncpackrc. - oxfmt: zbugs-screenshots.yml formatted. Public Test moves to ubuntu-24.04: ubuntu-latest is now 26.04, which Playwright 1.60 has no browsers for. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
VirtualList and VirtualListView lay out a header and a footer in the same pass as the rows: they're part of the content, before the space before the rows and after the space after them, so scrollToItem and the anchoring account for them. A selection of one row by key (a Binding for VirtualList; selection and onSelect for VirtualListView): a tap selects a row, even the selected one, and while the list has focus the arrow keys and j and k move the selection and scroll it into view. The list highlights the selected row in the platform's colors, passes isSelected to the row, and marks it selected for accessibility.
…ection The UI tests check the header pushing the rows at the top and leaving them in place above the screen, the footer after the last row, and selecting by a tap and (on macOS) by the arrow keys and j and k. The frame probe's Issues script grows the header at the top and above the screen and, on macOS, moves the selection by the arrow keys past the screen's bottom.
… virtualizer over Switching between two lists on one virtualizer (zbugs' SwiftUI and native issue lists) can attach the new list before the old one leaves; the old one then detached the virtualizer from the new list.
…they scroll The issue list is a VirtualList of issueListV2 pages, set up as zbugs' list page sets up useZeroVirtualizer, in place of its first 200 issues; the UIKit and AppKit list is a VirtualListView in place of the DiffableAdapter table. Both use the virtual list's selection for the split view's highlight, and open an issue on a tap, even the selected one. An issue's page is one VirtualList: the header and the description above its comments, which page in as zbugs' comments do, and the composer below. The selected issue and the list's scroll state are kept in SceneStorage: a relaunch opens the list at the issue, as the web app's list permalink does, or else where it was. A UI test scrolls the list of all issues past 200 of them.
…its hosting controller
…ng title, and a new filter opens at its top The cell's nested stack views squeezed the icon out beside a long title, and measured some titles at one line: plain constraints instead. A filter change cleared the selected issue's permalink, so the new list opened at its top, not at an issue it may not have.
…ts rows offline The list held off drawing its rows until it had landed at its target, and it lands once the rows' queries complete. Offline, those queries never complete, so the list stayed blank. It now draws once the viewport is placed at the target, whether or not the queries have completed. The offline zbugs screenshot runs skip the paging test, since the list doesn't page past its first page offline.
Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…o npm swift-mirror.yml becomes swift-release.yml. After the mirror job, it packs packages/zero-swift-cli without credentials and publishes the tarball with npm trusted publishing, in the swift-release environment. The npm jobs run once the ZERO_SWIFT_NPM_PUBLISH variable is 'true', and the package version must match swift/VERSION. The package is no longer private, and the mirror is public. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…ery row When no row is on screen in both frames, the probe flagged a jump. A stalled main thread (a slow CI simulator's) lets a fling run on by a screenful or more between two frames, and UIScrollView catches up on the frame after it, so neither frame shares a row with the one before. The probe now compares the rows laid out in both frames by their content positions: a scroll leaves them where they were, less what the list compensated in that pass; a jump moves them. Up to a quarter of the offset's change, or 50 pt, counts as a scroll. The flag reports dt, the callback latency and the offset.
On iOS the arrow keys and j and k needed a hardware keyboard, and the list took first responder on a tap only for them. The iOS list now has no key commands and doesn't take first responder; a tap still selects a row, highlights it, and calls onSelect, and the selection binding and the accessibility traits are as they were. macOS keeps its keys.
…provenance checks Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
…lish to npm by default A changeset for zero-swift versions the CLI and the Swift package together: pnpm changeset:version writes the CLI's version to swift/VERSION. On main, swift-release.yml keeps a version PR open. With no changesets pending, it releases the version when its mirror tag or npm package is missing, after approval in swift-release. Each step skips what's already released, so a failed run can be rerun. The ZERO_SWIFT_NPM_PUBLISH and mirror-token gates are gone; a missing token or trusted publisher now fails the job. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
tmm
marked this pull request as ready for review
October 21, 2026 18:50
…prettierignore as upstream has them .prettierignore goes back to upstream's: appending to it had joined its last line to ours. Changesets runs through pnpm dlx, pinned at 3.0.3, from zero-swift's version-packages script, so it's no longer a root dependency. Amp-Thread-ID: https://ampcode.com/threads/T-c513a037-a257-45e0-b9a9-15c15edfe859 Co-authored-by: tmm <tmm@tmm.dev>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A Swift client for Zero on iOS and macOS, with parity to the JS client, checked against conformance vectors generated from the JS implementation.
What's here
swift/: theZeroSwift package. It has the client (connection, mutations, queries and IVM),ZeroUI(@ZeroQueryfor SwiftUI),ZeroVirtual(virtual lists:VirtualListfor SwiftUI andVirtualListViewfor UIKit and AppKit, a counterpart of@rocicorp/zero-virtual),ZeroTesting, andTestHost, a test app for the virtual lists.packages/zero-conformance: vectors generated from the JS client and ZQL, which the Swift tests replay.packages/zero-swift-cli: generates Swift schema, query and mutator types from a Zero schema.apps/zbugs-swift: zbugs as a SwiftUI app for iPhone, iPad and macOS, with its own backend for CI.Changes outside those directories
swift.yml(Linux build and tests, the conformance vectors, end-to-end tests),swift-macos.yml(macOS, the iOS simulator and the zbugs build),zbugs-screenshots.yml(zbugs and virtual-list UI tests in simulators and on macOS), andswift-release.yml(Changesets version the Swift client; a release tagsswift/on thewevm/zero-swiftmirror and publishes thezero-swiftCLI to npm, after approval in theswift-releaseenvironment). See.changeset/README.md..github/actions/playwright-install: the browser cache is now keyed by the OS release too.ubuntu-latestmoved to Ubuntu 26.04, and a WebKit cached on 24.04 failed Public Test onmainat b39e85c.apps/zbugs/api/index.ts: the GitHub login callback now redirects tozbugs-swift://auth?jwt=…when that is the requested redirect, because the app's web authentication session can't read the cookie. This is safe becauseASWebAuthenticationSessionhands the callback only to the app that started it, even if another app registers the scheme, and the app doesn't register the scheme with the system. A comment in the code says so.apps/zbugs/scripts/dev-token.ts(pnpm dev-token <userID> [crew|user]): prints a development JWT for clients that can't log in through GitHub. Thepackage.jsonandtsconfig.scripts.jsonchanges register it..agents/setupfor orbs, andpnpm-lock.yaml.Testing
All of this runs in CI. The virtual list's per-frame probe reports 0 wrong frames on a physical iPhone 15 Pro, the iOS simulator and macOS.