Methods, streams & state
A contract has three buckets: methods, streams, and state. Each maps to a marker
and a t.* form.
Methods
Section titled “Methods”t.fire(params?) · Void<P>()
Section titled “t.fire(params?) · Void<P>()”Fire-and-forget. No result. Failures (no provider, no method) produce an internal envelope: logged loudly in dev, dropped and counted in diagnostics in prod.
showLogin: t.fire(), // no paramstrackEvent: t.fire(t.object({ name: t.string() })), // with paramst.query(result) · t.query(params, result, opts?) · Async<P, R>()
Section titled “t.query(result) · t.query(params, result, opts?) · Async<P, R>()”Async request/response. The opts.timeoutMs controls the per-method timeout:
- a number → default-per-method timeout,
null→ never times out (interactive actions:pickMedia,showDatePicker,installEsim),- unset → no default; the caller may set a timeout per call.
isLoggedIn: t.query(t.boolean()),installEsim: t.query(InstallParams, InstallResult, { timeoutMs: null }),t.querySync(params, result) · Sync<P, R>()
Section titled “t.querySync(params, result) · Sync<P, R>()”A constrained synchronous read over the sync transport channel. The documented contract:
in-memory lookup only on the provider side — dev builds assert the duration (< 2ms)
and warn. It exists for parameterized render-time reads.
Streams
Section titled “Streams”otpCodes: t.stream(t.string()),positions: t.stream(t.object({ lat: t.number(), lng: t.number() }), { params: t.object({ highAccuracy: t.boolean() }), // parameterized stream latestOnly: true, // conflation instead of buffering sticky: true, // auto-resubscribe across rebinds}),| Option | Default | Effect |
|---|---|---|
params | none | Schema for a parameterized stream (the params become part of the sharing key). |
latestOnly | false | Switch backpressure from a lossless bounded buffer to conflation. |
sticky | false | Opt into auto-resubscribe across provider rebinds. |
The default is lossless — a bounded buffer (default 64, drop-oldest with a logged
diagnostic). Streams replace events; conflation silently dropping discrete values is the
wrong default, so you opt into it explicitly with latestOnly.
The native provider returns a platform stream type:
override fun otpCodes(): Flow<String> = smsRetriever.codes()func otpCodes() -> AsyncStream<String> { AsyncStream { continuation in let task = Task { for await code in smsRetriever.codes() { continuation.yield(code) } } continuation.onTermination = { _ in task.cancel() } }}connectivity: t.state(t.object({ online: t.boolean() }), { online: false }),counter: State<number>(0),The initial value is required — this is the keystone decision. It seeds the native store
and the JS mirror before any provider binds, so reads are total (get(): T, never
undefined). The render-gating dance dies here.
State is provider-owned and single-writer, enforced by the router (NOT_PROVIDER if the
non-owning side writes). Availability travels with the value:
BridgeValue<T> = Available(value) | Initial(value) | Unprovided(lastKnown)Binding close transitions observers to Unprovided; a re-provide resets to the new
provider’s current value with a normal change notification.
The native provider exposes state as a platform-native reactive type:
// Provider interface (generated)override val connectivity: MutableStateFlow<Connectivity>
// Consumer side (generated client interface)val connectivity: StateFlow<BridgeValue<Connectivity>>// Provider protocol (generated)var connectivity: AsyncStream<Connectivity> { get }
// Consumer side (generated client protocol)var connectivity: AsyncStream<BridgeValue<Connectivity>> { get }The provider yields values via the AsyncStream continuation; the runtime subscribes in a
background Task and feeds StateStore. BridgeValue.remap(_:) re-types the carried value
element-wise on the outbound path.