Skip to content

feat(truapi): serve the Pocket card collection - #706

Open
valentinfernandez1 wants to merge 14 commits into
rfc/pocket-modalityfrom
pocket-core-impl
Open

feat(truapi): serve the Pocket card collection#706
valentinfernandez1 wants to merge 14 commits into
rfc/pocket-modalityfrom
pocket-core-impl

Conversation

@valentinfernandez1

Copy link
Copy Markdown
Collaborator

Follow-up to #609

The RFC and protocol spec live in #609. This stays a draft until that merges, then it goes up against main on 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_subscribe and remove_card.

Hosts opt in through PocketPlatform, a new optional trait on OptionalPlatform. A host that leaves it out answers Pocket calls Unsupported. 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 one navigate_to call, 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 call parse_navigate to 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.pocket manifest parsing are host and manifest work. Neither is here, and neither is in #632 or #633.

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
valentinfernandez1 changed the base branch from main to rfc/pocket-modality September 9, 2026 17:25
@valentinfernandez1
valentinfernandez1 added this pull request to stack #707 September 9, 2026 17:25
@valentinfernandez1
valentinfernandez1 marked this pull request as ready for review September 9, 2026 17:27
@valentinfernandez1
valentinfernandez1 requested a review from a team September 9, 2026 17:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants