Skip to content

Methods, streams & state

A contract has three buckets: methods, streams, and state. Each maps to a marker and a t.* form.

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 params
trackEvent: t.fire(t.object({ name: t.string() })), // with params

t.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,
  • nullnever 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.

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
}),
OptionDefaultEffect
paramsnoneSchema for a parameterized stream (the params become part of the sharing key).
latestOnlyfalseSwitch backpressure from a lossless bounded buffer to conflation.
stickyfalseOpt 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()
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>>