Skip to content

Commit 3a7b402

Browse files
committed
docs(client): document connection & auth error handling
Add a 'Handling connection and auth errors' section to the client guide covering rpc.status, rpc.connectionError, the connection:status / connection:error / rpc:error events, DevframeConnectionError (and its kind), and the callTimeout option, with one framework-neutral recipe that gates the UI on status and branches a failing call on error kind. Refresh the now-stale Events section and options table, and note that a hub viewer can read the same status centrally via context.connection.
1 parent b41d16a commit 3a7b402

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

docs/guide/client-context.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ Boot the host once per page: a second boot replaces the published context and lo
5353
| `panel` | Dock panel state: position, size, drag/resize flags. |
5454
| `commands` | The command palette: `register()`, `execute()`, `getKeybindings()`. |
5555
| `when` | The [when-clause](./when-clauses) evaluation context. |
56+
| `connection` | The client's live [connection status](./client#handling-connection-and-auth-errors)`status`, `error`, and `events` — so a viewer can render one central connection indicator for every docked plugin. |
5657

5758
### Accessing the context
5859

docs/guide/client.md

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ await connectDevframe({
4949
| `baseURL` | Mount path to probe for `__connection.json`. Accepts an array for fallback. Default: `'./'` — resolved relative to `document.baseURI` so the SPA finds its meta wherever it was deployed. Pass an explicit absolute path (e.g. `'/__devframe/'`) when calling from outside the SPA — say, an embedded webcomponent injected into a host app. |
5050
| `authToken` | Override the auth token. Defaults to a locally-persisted human-readable id. |
5151
| `cacheOptions` | `true` to enable caching with defaults, or an options object. |
52-
| `wsOptions` | Forwarded to the WebSocket transport (reconnect, heartbeat, etc.). |
52+
| `callTimeout` | Milliseconds after which a pending `rpc.call` rejects with a `DevframeConnectionError` of kind `'timeout'`. Omit (or `0`) to wait indefinitely. See [Handling connection and auth errors](#handling-connection-and-auth-errors). |
53+
| `wsOptions` | Low-level WebSocket transport overrides — `onConnected` / `onError` / `onDisconnected` lifecycle hooks and the socket URL. |
5354
| `rpcOptions` | Forwarded to `birpc`. |
5455
| `connectionMeta` | Pre-known descriptor that skips the `__connection.json` fetch. |
5556

@@ -229,6 +230,15 @@ The descriptor carries a session-only, pre-approved auth token, so `ensureTruste
229230

230231
## Events
231232

233+
The client emits over `rpc.events`:
234+
235+
| Event | Fires when |
236+
|-------|------------|
237+
| `rpc:is-trusted:updated` | Trust is granted, denied, or revoked. Carries the new `isTrusted` boolean. |
238+
| `connection:status` | The [connection status](#handling-connection-and-auth-errors) changes. Carries `(status, previous)`. |
239+
| `connection:error` | A connection-level failure occurs — the socket errors, or trust is refused. Carries the `Error`. |
240+
| `rpc:error` | An `rpc.call` rejects, from the server or a down connection. Carries `(error, method)`. |
241+
232242
```ts
233243
rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
234244
if (isTrusted)
@@ -239,3 +249,85 @@ rpc.events.on('rpc:is-trusted:updated', (isTrusted) => {
239249
```
240250

241251
`rpc.isTrusted` is the synchronous read. Subscribe to `rpc:is-trusted:updated` to drive reauth flows or gate rendering until the client is trusted.
252+
253+
## Handling connection and auth errors
254+
255+
A dev-mode client rides a live WebSocket, so it can lose the server mid-session or be refused authentication. Surface those states in your UI — a devtool that keeps spinning with no feedback leaves the user guessing whether it's loading or broken. The client gives you a single status to render from, events to react to, and calls that fail fast instead of hanging.
256+
257+
### Connection status
258+
259+
`rpc.status` collapses the transport and the trust handshake into one value, and `rpc.connectionError` holds the last connection-level `Error` (or `null` when healthy):
260+
261+
| Status | Meaning |
262+
|--------|---------|
263+
| `connecting` | Establishing the socket / running the initial handshake. Calls issued now queue until it opens. |
264+
| `connected` | Socket open and trusted; calls are served. |
265+
| `unauthorized` | Socket open, but the server refused trust. Prompt for [authentication](#authenticating-with-a-one-time-code). |
266+
| `disconnected` | The socket closed — dropped mid-session, or never opened. |
267+
| `error` | A fatal connection error, e.g. the socket errored or the connection meta couldn't load. |
268+
269+
A `static` backend has no live socket, so `rpc.status` is `connected` for its whole life — gating on it is a no-op there, and a build-time SPA never shows a connection state.
270+
271+
### Calls fail fast
272+
273+
Once the socket closes or trust is refused, in-flight and new `rpc.call` promises reject with a `DevframeConnectionError` rather than hanging forever. Its `kind` tells you why, so a `catch` can branch without string-matching:
274+
275+
- `'connection'` — the transport is down (`disconnected` / `error`).
276+
- `'auth'` — the client is `unauthorized`.
277+
- `'timeout'` — the call outlived the `callTimeout` option.
278+
279+
Set `callTimeout` when constructing the client to also cap a live-but-unresponsive server:
280+
281+
```ts
282+
const rpc = await connectDevframe({ callTimeout: 10_000 })
283+
```
284+
285+
### Putting it together
286+
287+
Gate the UI on `connection:status`, and wrap calls to branch on failure:
288+
289+
```ts
290+
import { connectDevframe, DevframeConnectionError } from 'devframe/client'
291+
292+
const rpc = await connectDevframe()
293+
294+
// 1. Render from the live status.
295+
function render() {
296+
switch (rpc.status) {
297+
case 'connected': return renderApp()
298+
case 'connecting': return renderSpinner('Connecting…')
299+
case 'unauthorized': return renderMessage('Not authorized — reopen the link from your dev server.')
300+
case 'disconnected': return renderMessage('Disconnected.', { onRetry: reconnect })
301+
case 'error': return renderMessage(rpc.connectionError?.message ?? 'Connection failed.', { onRetry: reconnect })
302+
}
303+
}
304+
rpc.events.on('connection:status', render)
305+
render()
306+
307+
// 2. Handle a failing call.
308+
async function loadModules() {
309+
try {
310+
return await rpc.call('my-devframe:get-modules', { limit: 10 })
311+
}
312+
catch (error) {
313+
if (error instanceof DevframeConnectionError) {
314+
// 'connection' | 'auth' | 'timeout' — the UI already reflects rpc.status.
315+
return null
316+
}
317+
throw error // a real server-side error — surface it.
318+
}
319+
}
320+
```
321+
322+
### Recovering
323+
324+
Recovery is explicit — the client doesn't reconnect on its own. The simplest path is a full page reload, which re-runs `connectDevframe` and the trust handshake; that's what the built-in plugins do behind their **Reload** button. An app that wants to reconnect without a reload can own it by re-running its connect routine to build a fresh client:
325+
326+
```ts
327+
async function reconnect() {
328+
rpc = await connectDevframe() // a new client; re-subscribe your listeners
329+
render()
330+
}
331+
```
332+
333+
The five built-in plugins are worked references — each gates its surface on `rpc.status` and offers a reload. In a hub, a viewer can read the same status centrally from [`context.connection`](./client-context#the-client-context) instead of every plugin surfacing its own.

0 commit comments

Comments
 (0)