feat(truapi): serve the Pocket card collection - #706
Open
valentinfernandez1 wants to merge 14 commits into
Open
Conversation
A host installs its card collection once, before any product runtime, and both hosts expose the same setter. The collection is only reachable from a Worker execution with an active session, so an App is told `Denied` even where a host serves Pocket, and a card id is screened before it reaches the host.
A host with a Pocket surface passes `NativePocketCallbacks` when it opens a product execution, and the core refuses a privileged removal from the list that adapter reports rather than asking the host to. The Swift and Kotlin packages wrap it as `PocketHostBridge`. The Swift package is compiled by CI (ios-swift) and the Kotlin adapter by ci-android; this machine has no Xcode, JDK, Gradle or Android SDK, so both were verified against freshly generated bindings rather than compiled.
`TRUAPI_POCKET_CARDS` seeds the collection and `TRUAPI_POCKET_LOG` records every removal the host was asked for, so a battery can tell a refusal apart from a removal that never reached the host.
The battery seeds the CLI Pocket host with one removable and one privileged card and asserts both sides agree: the product observes the seeded list with its privileged flags, the live list shrinking on its own removal, and the three removal rules, while the host's transcript says which removals actually reached it. The explorer aggregates a `pocket/` report directory into its own compatibility matrix, which stays hidden until a run lands one there.
Removal is deliberately not wired: a card leaves Pocket because the user asked or the product gave it up, and a demo product that dropped one on startup would take a card off the user's screen.
…llbacks `OptionalPlatform` gained `PocketPlatform`, so codegen adds `pocket?` to the package's `HostCallbacks` interface: an additive surface change the release has to carry, the same way the host locale subscription did.
Two defects in the reserved-namespace classifier. `card` arrives percent-decoded from `query_pairs`, and the canonical `polkadot://` form interpolated it raw, so a card id carrying URL syntax re-parsed as a different deeplink: `?card=a%23b` became `card=a` plus a fragment, and `%26open%3Dx` injected a second parameter. It is encoded again on the way out, and the new test asserts the canonical form round trips to the same card. Unrecognized targets under `/-/` returned `Reject`, which the RFC contradicts: "a host without the named modality, or one that does not know the action, opens the App instead". They now fall through to the App path, so a deeplink minted for a newer host degrades instead of failing. A pocket action whose `card` argument is missing stays refused, since the action is known and the link is simply malformed.
… kind Both Chat and Pocket declare `required_execution = Worker`, so filtering the section's rows on the execution kind matched every Worker-gated service in both directions: the Pocket table listed Chat's methods and never showed its own, and the Chat table would gain Pocket's rows as soon as a released version carried them. The section now names the service it measures, which is what the playground's diagnosis tests already do (`servicesForExecution(...).filter((s) => s.name === "Chat")`). The execution-kind sets go away with it: a service name is stable across the `Chat` to `Worker` rename those sets existed to paper over.
Codegen already emitted every piece of the web path: the `pocket?` callbacks on `HostCallbacks`, the `impl PocketPlatform for WasmPlatform` and `has_pocket()` on the JS bridge, the worker-side proxies and the `pocket` flag on `OptionalCapabilities`. Only the two hand-written joins between them were missing, so a host that supplied `pocket` callbacks was silently answered `Unsupported`. `wasm_platform` now builds the adapter when the bridge reports the callbacks and installs it on both host runtimes, next to the permission status host. The Web Worker handshake reports `pocket` alongside `chat`, without which the worker never builds the proxies and the core sees a different capability set on each side of the boundary. The test fixture grows a `pocket` group so a fixture can ask for a Pocket-serving host, and the capability gate is covered on both sides: the handshake reports it, and the worker's raw callbacks appear only when it is reported.
The adapter read `list_cards()` and only then pushed its sender onto the subscriber list, so a `notifyPocketCardsChanged` landing between the two reached no subscriber and was dropped. The product kept a stale card list with no later correction, since the host only reports changes. The event bus now takes the snapshot through a closure it runs after registering the sender, and delivers it on the same channel. A change that interleaves is queued ahead of the snapshot instead of lost, and the snapshot still reflects it, so the last item is always the current list. Running the closure outside the mutex also keeps a host that notifies from the calling thread from deadlocking on it.
`OptionalPlatform` has carried a third member since the Pocket adapter landed, and both READMEs still described it as combining two. The host package's list now names the `pocket` group the web host serves.
valentinfernandez1
added this pull request to stack #707
September 9, 2026 17:25
valentinfernandez1
marked this pull request as ready for review
September 9, 2026 17:27
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.
Follow-up to #609
The RFC and protocol spec live in #609. This stays a draft until that merges, then it goes up against
mainon its own.Summary
Implements the Pocket collection that spec describes. The host owns the collection and is its only writer. A product observes the cards it owns and can give one up, through two methods,
list_subscribeandremove_card.Hosts opt in through
PocketPlatform, a new optional trait onOptionalPlatform. A host that leaves it out answers Pocket callsUnsupported. Adding the trait there is what makes codegen emit the dispatcher and each host's callback group, so per host the only work left is installing the adapter and reporting the capability. Every host in the repo now serves it: the browser and Web Worker host, iOS and Android through UniFFI, and the headless CLI.The access gate lives in the core, so a call from anything other than a Worker execution with an active session is denied before a host is consulted, and a card id a product sends is screened with the same rules chat applies to its own identifiers.
There is no add method, on purpose. A card enters when the user follows a
/-/pocket/add?card=<id>deeplink and approves a host dialog. A product opens that deeplink with onenavigate_tocall, so an Add to Pocket button is easy to build, but the user is the one who consents. The core classifies the deeplink so every host reads it the same way, and a native host can callparse_navigateto get the action and the card id back instead of parsing a string itself.The CLI host keeps a small in-memory collection, so the battery's Pocket phase drives three cases over the real wire and writes the committed diagnosis report.
Not in this change
Drawing a card face belongs to the Unified Renderer (#633), and starting the worker behind one belongs to Worker Lifecycle (#632). Nothing here references a renderer type. The only value that crosses between them is a card id, so a host reads its collection through this change and opens the render stream on
PocketCard { card_id }from that one. That is what lets this land before either of them rather than after.The approval dialog and
includes.pocketmanifest parsing are host and manifest work. Neither is here, and neither is in #632 or #633.