# @lia-labs/bridge

> The lowest layer. Typed calls and events over Lia's frame bridge, the timeouts and limits, and mockLia() for building outside Lia. From the Lia developer docs: https://docs.lialabs.org/packages/bridge

Most apps want [`@lia-labs/sdk`](/packages/sdk), which is built on this and re-exports all of it. Use the bridge on its own to build your own layer, or to call something the SDK doesn't wrap.

```sh
npm install @lia-labs/bridge
```

## The protocol

Inside Lia, your page runs in the Lia app's web view. A call is a JSON message the page posts with `window.ReactNativeWebView.postMessage`:

```json
{ "lia": 1, "id": 7, "method": "haptic", "params": { "kind": "success" } }
```

Lia answers each call once, through `window.__lia.receive(...)`, with `{ id, ok: true, result }` or `{ id, ok: false, error: { code, message? } }`, and sends events the same way as `{ event, data }`. The bridge defines `window.__lia` (frozen, once per page) and matches answers to calls by id.

## Calling

```ts
import { isLia, request, send } from '@lia-labs/bridge';

isLia(); // true inside the Lia app (or with mockLia)

const me = await request('init');
const { best } = await request('submitScore', { score: 1280 });
send('ready'); // without waiting for the answer; failures are dropped
```

`request(method, params?, options?)` is typed per method: the params it takes and the answer it resolves with come from the `Methods` type. It rejects with a `LiaError`:

| Code | When |
| --- | --- |
| `not_in_lia` | There's no bridge (not inside Lia, no mock). |
| `timeout` | Lia didn't answer within 20 seconds (or your `timeout`). Calls that wait on the person (`pay`, ads, dialogs, the scanner, Face ID…) have no limit unless you set one. |
| `aborted` | Your `signal` was aborted. Lia may still act on the call. |
| `too_large` | The message is over 16,000 characters, which Lia would ignore. |
| Lia's own codes | Whatever Lia answered: see [Error codes](/reference/errors). |

```ts
const controller = new AbortController();
const code = request('scanQr', { text: 'Scan a table' }, { signal: controller.signal, timeout: 60_000 });
```

## Listening

```ts
import { on, once, onAny, off } from '@lia-labs/bridge';

const stop = on('theme', ({ colorScheme, theme }) => repaint(colorScheme, theme));
once('resume', () => refresh());
onAny((event, data) => console.log(event, data));
stop();
```

Events are typed (`Events`): `pause`, `resume`, `theme`, `viewport`, `fullscreenChanged`, `languageChanged`, `startParam`, `notificationsChanged`, `soundChanged`, `charge`, `ad`, the four `*ButtonClicked` events, and `sensor` (`{ name, values }`; the SDK turns it into each sensor's `reading`). A listener that throws is logged and doesn't stop the others.

## Everything it exports

| Export | |
| --- | --- |
| `isLia()` | Whether a bridge is there. |
| `request(method, params?, options?)` | A typed call. |
| `send(method, params?)` | A call without waiting. |
| `on`, `off`, `once`, `onAny` | Events. |
| `mockLia(options?)`, `MOCK_INIT` | A pretend Lia: see [Local development](/packages/local-development). |
| `emit(message)` | Hands a message to the page as if Lia sent it (for mocks and tests). |
| `setDebug(on, logger?)` | Logs every call, answer and event. |
| `install()` | Starts listening now; says whether `window.__lia` is this package's. |
| `LiaError`, `isLiaError(error, code?)` | Errors. |
| `PROTOCOL`, `DEFAULT_TIMEOUT`, `MAX_MESSAGE`, `PATIENT` | The protocol's version and limits. |
| `resetForTests()` | Forgets every call and listener. |
| Types | `Methods`, `MethodName`, `ParamsOf`, `ResultOf`, `Events`, `EventName`, `InitResult`, `ThemeParams` and every shape Lia sends. |

## One per page

Two copies of the bridge in one bundle share one set of calls and listeners (kept on a global symbol), so nothing breaks. The script tag SDK can't share: if it's already on the page, the bridge warns and can't hear Lia's answers. Use one or the other.
