Host
The host runs in the app's backend, under Bun or Node. It sits between the channel the app already has and the helper process, and it is the boundary a page cannot get around. It imports nothing from React.
import { createDatabaseHost, helperPath, parseRequest, spawnHelper } from '@adecore/database/host';
import type { DatabaseHost, DatabaseHostOptions, HelperPathOptions, HelperProcess, ParsedRequest, SpawnHelperOptions } from '@adecore/database/host';createDatabaseHost
const host = createDatabaseHost({
start: () => spawnHelper(path),
authorize: (connection, owner) => isAllowed(connection, owner),
authorizeFile: (path, access, owner) => isChosen(path, access, owner),
authorizeDiscovery: (kind, owner) => isTrusted(owner),
readyTimeoutMs: 10000
});DatabaseHostOptions:
| Option | |
|---|---|
start | () => HelperProcess. Starts a helper. Called on the first request, and again on the first request after one exited. |
authorize | (connection, owner) => boolean | Promise<boolean>. Asked on every open and test. Optional; every connection is allowed without it. |
authorizeFile | (path, access, owner) => boolean | Promise<boolean>. Asked on every export ('write'), and every import and sample ('read'). Optional, but every file is refused without it. |
authorizeDiscovery | (kind, owner) => boolean | Promise<boolean>. Asked on every discover. Optional, but discovery is refused without it, since it hands container credentials to the page. |
readyTimeoutMs | How long to wait for the helper's ready line. 10000 when left out. |
A check that returns false, throws or rejects turns the request down with forbidden, before the helper starts or sees it. Which checks to write, and what each one protects, is the subject of Security.
The host registers no channel and listens to no event. The app checks the sender, then calls handle.
DatabaseHost
| Method | |
|---|---|
handle(request, owner) | Takes what came over the channel, as it is, and resolves with a DatabaseResponse. It never rejects. |
release(owner) | Cancels the requests of an owner that went away and closes its sessions. |
dispose() | Releases every owner and stops the helper. A host that is disposed answers every request with helper-unavailable. |
handle takes unknown on purpose. It validates the request with parseRequest before anything else, so a page can send whatever it likes and the helper only sees well-formed requests.
owner is a string the app picks: a window id, a socket id. The host keeps these rules:
- A session belongs to the owner that opened it. Another owner using its id gets
unknown-session. - A
cancelonly reaches the requests of the owner that sent it. - Two requests of one owner that are running at once cannot share an id. The second fails with
invalid-request. - Request ids are the owner's. The host sends the helper ids of its own and writes the owner's id back on the response.
release and dispose wait for the helper to answer its close requests for up to two seconds, so a helper that hangs cannot hold up an app that quits.
Failures of the helper
The helper starts lazily and is expected to stay up. When it does not:
- If it exits while requests are running, they fail with
helper-exited. Its sessions are gone, and the host forgets them. The next request starts a new helper. - If
startthrows, if the helper does not write its ready line withinreadyTimeoutMs, if the line is not a ready line or if it names anotherPROTOCOL_VERSION, the request that started it fails withhelper-unavailable. - If the helper writes something that is not a response of the protocol, that request fails with
internal.
The client opens a session again after unknown-session where that is safe.
HelperProcess
HelperProcess is a running helper as the host sees it: lines in, lines out. spawnHelper makes one from a path. You can write your own, such as a fake in a test of the host:
| Member | |
|---|---|
write(line) | Sends one message, without its line break. |
onLine(listener) | Hears each line the helper writes to stdout. |
onExit(listener) | Hears the exit code, or null. |
kill() | Stops the process. |
spawnHelper
const helper = spawnHelper(path, { args, env, onLog });Runs the helper binary at path and speaks one JSON message per line over its stdin and stdout. It returns a HelperProcess, which is what start must return.
SpawnHelperOptions:
| Option | |
|---|---|
args | Arguments for the binary. None by default. |
env | Added over the environment of this process. |
onLog | Receives each line the helper writes to stderr. |
Lines are cut at \n, and a trailing \r is dropped. A line can be megabytes, and cutting one stays linear. A path that cannot be run ends in onExit with null, after onLog hears the reason, so the host answers with helper-unavailable. The helper is a separate build for each platform, so the path comes from helperPath or from a binary the app ships itself.
helperPath
const path = helperPath();Finds the prebuilt helper that the package manager installed for this machine. @adecore/database lists one package per platform as an optional dependency (@adecore/database-darwin-arm64 for Apple silicon, -linux-x64, -linux-arm64 and -win32-x64), and the manager installs the one that fits. helperPath returns the path of its binary, or null when the platform has no package or the install left optional dependencies out.
const path = helperPath();
if (path === null) {
throw new Error('No prebuilt helper for this platform.');
}
const host = createDatabaseHost({ start: () => spawnHelper(path) });A binary cannot run from inside an Electron app.asar archive. helperPath maps app.asar to app.asar.unpacked, so the app has to unpack the platform packages; see Getting started.
HelperPathOptions exists for tests, and each field has a default:
| Option | |
|---|---|
platform | process.platform. |
arch | process.arch. |
resolve | Resolves a package specifier to a file. The module resolution of the package when left out. |
exists | Whether a path exists. fs.existsSync when left out. |
parseRequest
const parsed = parseRequest(input);
if (parsed.ok) {
parsed.request; // a DatabaseRequest
} else {
parsed.id; // the request's id, or '' when it had none
parsed.error; // { code: 'invalid-request', message }
}parseRequest(input: unknown) checks the whole envelope and the params of its method and returns a ParsedRequest. handle calls it for you. Use it directly when you route requests yourself, for example to log or rate limit by method before they reach the host. It returns { ok: true, request } or { ok: false, id, error }. The rules are listed under Security.