Eximia docs
Bridge contract
The exact wire format that travels between JavaScript and native code in both directions, on both platforms. Anything that doesn't match this doc is a bug.
Audience: anyone implementing a side of the bridge (
SLMEximiaBridge.kt,SLMEximiaBridge.swift,js/src/index.ts), or a plugin author who needs to reason about errors and progress.
Transports
| Direction | Android | iOS |
|---|---|---|
| JS → native | __eximia_bridge__.exec(payload) (where __eximia_bridge__ is a @JavascriptInterface on the WebView) | window.webkit.messageHandlers.eximia.postMessage(payload) |
| native → JS | webView.evaluateJavascript("__slmeximia_reply__(...)", null) on the UI thread | webView.evaluateJavaScript("__slmeximia_reply__(...)") on the main queue |
Both transports carry the JSON envelopes defined below.
payload is always a string (stringified JSON) on Android — the
@JavascriptInterface boundary doesn't pass structured types. On iOS it's
delivered as Any? and we cast to [String: Any] immediately.
Envelope: exec request (JS → native)
{
"v": 1,
"id": "c5e6a4f3-...",
"plugin": "Camera",
"action": "take",
"args": { "quality": 80, "type": "rear" }
}
| Field | Type | Required | Meaning |
|---|---|---|---|
v | int | yes | Protocol version. Always 1 for v1.0. Future breaking changes bump this. |
id | string | yes | UUIDv4 generated by JS. Used to correlate response. |
plugin | string | yes | Plugin name (matches plugin.json → name field, or its globalAs). |
action | string | yes | Method to invoke. Plugin defines this set. |
args | object | no | Method arguments. Plain JSON, no Date/RegExp/Map etc. |
If v is unknown, the native side responds with error.code = "eximia/unsupported-version".
Envelope: exec response (native → JS, success)
{
"v": 1,
"id": "c5e6a4f3-...",
"kind": "ok",
"value": { "imageData": "iVBORw0K...", "width": 1024, "height": 768 }
}
| Field | Type | Meaning |
|---|---|---|
v | int | Same as request. |
id | string | Matches the request's id. |
kind | "ok" | Discriminator. |
value | any | Plugin-defined result. Plain JSON only. Binary data is base64-encoded. |
Envelope: exec response (native → JS, error)
{
"v": 1,
"id": "c5e6a4f3-...",
"kind": "err",
"error": {
"code": "camera/permission-denied",
"message": "User denied camera permission",
"retryable": false,
"debug": { "platform": "android", "api": 33 }
}
}
error.code is a stable string identifier of the form
<plugin-or-eximia>/<kebab-case-reason>. See error-model.md
for the full taxonomy.
error.retryable is a hint: true means "calling again may succeed"
(e.g. transient network), false means "calling again won't help"
(e.g. denied permission).
error.debug is optional and only populated in debug builds. Never
parsed by code; for human consumption only.
Envelope: progress (native → JS, optional, multiple per exec)
For long-running operations (file download, image processing), a plugin may emit progress events between request and final response:
{
"v": 1,
"id": "c5e6a4f3-...",
"kind": "progress",
"progress": { "loaded": 102400, "total": 524288 }
}
JS dispatches these to the optional onProgress callback registered when
the caller used SLMEximia.exec(plugin, action, args, { onProgress: fn }).
A progress envelope is never the final reply — every exec must end
with kind=ok or kind=err.
Envelope: lifecycle event (native → JS, broadcast)
The native side broadcasts OS events to JS without an id (no request
correlates):
{
"v": 1,
"kind": "event",
"event": "pause",
"data": null
}
Supported events: resume, pause, online, offline, keyboardShow,
keyboardHide, backbutton (Android only), deeplink, notification.
See lifecycle.md for triggers and payloads.
JS subscribes via:
SLMEximia.on("pause", () => { ... });
Concurrency
The bridge supports unlimited concurrent in-flight exec calls, each
keyed by its id. The native side MUST NOT serialise plugin calls
unless the plugin itself opts in by declaring serial: true in its
plugin manifest.
A plugin's execute() runs on:
- Android: the IO dispatcher of the runtime's coroutine scope, unless
the plugin overrides via
runOnin its manifest. - iOS: a dedicated
DispatchQueueper plugin, orTask { ... }detached, based on the same manifest hint.
The runtime guarantees the response envelope arrives on the JS thread (main thread of the WebView) so handlers can touch the DOM safely.
Encoding gotchas
| Type | How to carry |
|---|---|
| Binary | base64 string. Mark with a sibling field *Encoding: "base64". |
| Dates | ISO 8601 string. Plugins MUST NOT use Unix timestamps unless the field is named *Timestamp or *EpochMs. |
null vs missing | Always be explicit; document the difference in each plugin. |
Numbers > 2^53 | Send as strings; JS Number loses precision past 2^53. |
| Maps with non-string keys | Forbidden. Convert to array of {key, value}. |
Versioning
v: 1 covers everything in this document. We bump v only when we make
breaking changes to envelopes (rename fields, change the kind tags,
restructure errors). New optional fields don't bump v.
Plugins MUST tolerate unknown fields in any envelope (forwards compat).
Validation in the runtime
The SLMEximia bridge SHOULD validate incoming envelopes against this contract in debug builds and emit a warning when something is off. In release builds, validation is skipped and ill-formed envelopes silently drop with a single bridge-level error logged.