# Backend and Island Interfaces

Two classes of objects, defined in `core/net/` and `core/islands/`:

* **Backend** -- the *transport*: a named server you call with `exec`/`exec2`/`getJson`, over HTTP or WebSocket.
* **Island** -- a backend *mounted* into the site at `/<id>`: routes, files (`.vue`, `.md`), sidebar tree, identity.

A static website needs neither (see [Architecture](./Architecture.md#site-backends-and-islands)).

## Result contract

Every transport call returns this envelope and **never throws** (`readFile` is the one exception, because vue3-sfc-loader expects its resolver to throw):

```js
{ isOk: true,  result: <payload>,      status?: 200 }
{ isOk: false, result: "<message>",    status?: 404, stack?: [...] }
```

`status` is the HTTP status (`0` when the request never completed). `ok()` and `nok()` in `JsUtils.mjs` build these objects.

## Backend

```js
backend.id          // "admin"
backend.transport   // "http" | "ws"
backend.url         // base URL of the server
backend.capabilities// {stream: true, push: false}   (ws: push true)
backend.state       // reactive {isReady, isConnected}
await backend.ready(timeoutMs = 0)      // replaces `if (state.isReady) ... else setTimeout(...)` polling

// RPC: identical on both transports
await backend.exec(endpoint, args = [], bytes = null)      // {isOk, result}
backend.exec2(endpoint, args = [], bytes = null)           // Observable<Uint8Array> raw chunks
backend.exec2Result(endpoint, args, bytes)                 // Observable<{type, ...}> stdout as text
backend.exec2ResultBinary(endpoint, args, bytes)           // Observable<{type, ...}> stdout as Uint8Array

// Plain HTTP against backend.url (relative paths)
await backend.getText(path) / getJson(path) / getBinary(path) / postJson(path, body)

// Files
await backend.readFile(path)     // Response, throws (SFC loader)
await backend.readBinary(path)   // {isOk, result: Uint8Array}

// Lifecycle
await backend.init()  connect()  disconnect()  destroy()
backend.toConfig()               // config to create it again (no password)
```

`HttpBackend` implements all of it with `fetch`. `WsBackend extends HttpBackend` and replaces `exec`, `exec2`, `readFile`, `readBinary` and the lifecycle; the plain HTTP calls still go to the same server (cookie login, `/c0ckp1t/...` files, `getJson`).

### HTTP wire format

The server must implement these for an HTTP backend (paths can be changed with `rpcPath` / `rpc2Path`). The reference implementation is `WsServer.enableHttpRpc()` in `c0ckp1t-server/src/WsServer.mjs`.

**exec** -- `POST {url}/exec`

```json
// request  (bytes is base64 or null)
{"endpoint": "/user", "args": ["userContext"], "bytes": null}
// response 200
{"isOk": true, "result": "..."}
```

`exec2` needs rxjs, which an HTTP backend loads in `init()`. Set `stream: false` in its config to skip that (the backend then only does `exec`, `getJson`, `readFile`; `exec2` answers `[STREAM_DISABLED]`). Inline backends of `kind: "static"` islands default to `stream: false`.

A non-2xx status becomes `{isOk: false, result: "HTTP error: 404 ...", status: 404}`.

**exec2** -- `POST {url}/exec2` with the same request body. The response is `application/x-ndjson`, one JSON object per line:

```
{"bytes": "<base64>"}                              raw chunk (what the WebSocket exec2 emits)
{"type": "START", "id": 1, "currentTimeMs": 0}     typed events (what exec2Result emits)
{"type": "STDOUT", "bytes": "<base64>"}            also STDERR, STDIELD
{"type": "END", "exitCode": 0, "runtimeMs": 12}
{"type": "ERROR", "result": "message"}             error, ends the stream
```

The stream ends when the response body ends. Unsubscribing from the Observable aborts the request. `exec2` yields the `bytes` of raw chunks; `exec2Result` requires typed events.

## Island

```js
island.instanceId     // "admin"  (the URL segment: /admin/...)
island.type           // "Island" | "ServerIsland"
island.backend        // the backend it uses
island.connection     // WebSocket Connection or null (Connections page)
island.state          // reactive {isLoading, isReady}
island.store          // reactive {root, selectedNode, context, ...}
await island.ready(timeoutMs = 0)

// navigation
island.routeByEndpoint(endpoint)     // router.push(`/${id}${endpoint}`)
island.selectDefaultDashboard()
island.selectNode(node, route = true)

// RPC and files: delegate to the backend
island.exec / exec2 / exec2Result / exec2ResultBinary
island.getText(path) / getBinary(path)      // read from the island's files, {isOk, result}
island.getJson(path) / postJson(path, body) // plain HTTP against backend.url
island.resolver(path)                       // Response for vue3-sfc-loader

island.toConfig()    // config to register it again (no password)
```

### ServerIsland (`kind: "server"`)

Adds the C0ckp1t server protocol on top of `Island`:

| Member | Description |
|--------|-------------|
| `userContext()` | `exec('/user', ['userContext'])` -> `store.context` (`accessLevel`, `uniqueId`, ...) |
| `rootNode()` | `exec('/', ['infoNode'])` -> `store.root`, and mounts the routes of every node |
| `state.workflowTable`, `findNodeByWfId()`, `findNodeByEndpoint()` | node tree helpers |
| `readFile()` | via `/sys/resolver`, replaces `##instanceId##` with the island id |

For every node the island loads `node.config.path` (a `config.json`) and adds its routes under `/<id><node.endpoint>`. Without a config it falls back to `_admin.vue` (access level < 500) or `core/nodes/_api.vue`.

### Static island (`kind: "static"`)

Routes (`config.routes`) and sidebar tree (`config.nav`) come from the config; files are read through the backend. See `examples/c0ckp1t-demo/Config.mjs`.

## Global API

```js
import {api, store} from 'GlobalStore'

api.http                       // HttpClient for the page origin
api.site                       // the page origin with the island read API (getText, getBinary, resolver)
api.source(instanceId?)        // island, or the site when no id: used by Documentation, xmd, xcode
await api.registerBackend(id, config, {init = true})
api.backend(id)                // throws [BACKEND_NOT_FOUND]
await api.registerIsland(config)
api.getIsland(id)              // throws [ISLAND_NOT_FOUND]
await api.unregisterIsland(id) // also removes its routes
api.frameworkPath('/core/x.vue')   // prefixes config.frameworkBase

store.r                        // islands by id (stable contract for node applications)
store.backends                 // backends by id
store.selectedInstId           // island matching the current URL
```

## Debugging

* Every backend has its own logger named `backend:<id>`; every island logs as `<id>`. Set levels in `config.defaultLoggerLevels`.
* `getIsland` / `backend` errors list the registered ids.
* WebSocket packets appear on the Traffic page (`/_c0ckp1t/traffic`, needs `devRoutes()`).
* A failing island or backend in `config.islands` / `config.backends` is logged and does not stop the rest of the app.
