Skip to content

Wire protocol & errors

Every operation in either direction uses the same two envelopes. There is one wire format, used symmetrically.

Sent for every op, both directions:

{
"op": "invoke | invokeSync | streamOpen | streamClose | stateRead | stateWrite | ...",
"contractId": "connect.host",
"member": "installEsim",
"scope": { "kind": "global | feature | instance", "feature": "...", "instance": "..." },
"payload": { /* encoded params, declared fields only */ },
"correlationId": "…",
"epoch": 7
}
// success
{ "ok": true, "value": /* encoded result */ }
// failure
{
"ok": false,
"code": "TIMEOUT",
"message": "dispatcher connected, contract 'connect.host' not provided in scope feature(Connect)",
"contractId": "connect.host",
"member": "installEsim",
"scope": { "kind": "feature", "feature": "Connect" },
"readiness": "connected",
"details": { /* optional */ }
}

Every error embeds enough context to state why it happened — readiness and provision context are always present.

CodeMeaning
CONTRACT_NOT_PROVIDEDNo live binding for (contract, scope) after the grace window.
METHOD_NOT_FOUNDMember absent — carries the member-set diff for skew context.
INCOMPATIBLE_CONTRACTDescriptor versions cannot be reconciled.
NOT_PROVIDERA non-owning side tried to write state (single-writer enforcement).
TIMEOUTCall exceeded its timeout; message states the readiness reason.
CANCELLEDCaller aborted (AbortSignal) or the binding closed mid-call.
PROVIDER_ERRORThe provider implementation threw.
VALIDATION_FAILEDDev-only: payload did not match the schema.
BRIDGE_NOT_READYDispatcher not connected, or a prior-epoch call after teardown.

Because BridgeKit is a singleton that can momentarily exist in two copies (dual-bundle edge cases), errors are matched by a stable code, not by class:

import { isBridgeError } from '@axion/bridgekit';
try {
await connect.installEsim({ url, iccId });
} catch (e) {
if (isBridgeError(e, 'TIMEOUT')) retryLater();
else if (isBridgeError(e, 'CONTRACT_NOT_PROVIDED')) showFallback();
else throw e;
}

The JS dispatcher never rejects at the transport level — it catches everything and resolves { ok: false, ... }. The Kotlin router wraps every native → JS dispatch in withTimeout + try/catch, mapping synchronous throws (dead runtime), never-settling promises (teardown race), and JS rejections all to envelope codes.

The AnyMap payloads accept no binary blobs and no nested functions. (Top-level ArrayBuffer params are a future extension.) Encoding always runs and walks declared fields only, so undefined and functions in a user object are skipped rather than crashing the converter.