Files
craft-libs/crates/web-api/README.md
T
Helen Kwok 2bfc45c493 web-api: refuse calls beyond an in-flight limit, and say who can call window.<app>
* web-api: refuse calls beyond an in-flight limit, and say who can call window.<app>

At most 64 calls (PageApi::with_max_in_flight) may wait for the app at once. A call made while that many wait rejects at once with a busy error and never reaches the app, so a page script that calls in a loop without awaiting cannot grow the app queue without bound, as the desktop control server already answers busy when its queue is full. The README also says the API is a property of the page global with no key: any script in the page origin has the same command control as the desktop control server.

* web-api: turn a busy call away before converting its params, and say a same-origin parent or opener can call window.<app>

A call over the in-flight limit used to stringify and convert its params before the busy check, so a large payload still cost the work it was being refused for. The params are now built by a closure the call runs only once it holds a slot.

The README said a parent, a child or an opener cannot reach window.<app>. That holds across origins only: a same-origin parent, child or opener with a window reference calls it directly, and the craft-web-host origin and source checks do not apply to that call.

---------

Co-authored-by: Helen Kwok <107752876+helenkwok@users.noreply.github.com>
2026-11-12 10:06:13 -05:00

71 lines
4.5 KiB
Markdown

# craft-web-api
The page command API for every Craft web build: one `window.<app>` shape over the app's JSON
control channel.
## The JS API
`PageApi::install` puts this on `window.<app>` (`"vectorcraft"` → `window.vectorcraft`), without
replacing an existing `window.<app>`: missing functions are added, an app that already has its own
keeps them. Each function returns a Promise that resolves with the method's `result` or rejects with
an `Error` carrying `error`. Every call has a timeout (30 s by default, `PageApi::with_timeout`);
when the app does not answer in time, the error says so and the app's request future is dropped, so an app that releases a request when its future goes away (the desktop `ControlRequest` has `abandon`) never runs a call the page was told had failed.
At most 64 calls may wait for the app at once (`PageApi::with_max_in_flight`); a call made while that many are waiting rejects at once with a `busy` error and never reaches the app, so a script that calls in a loop without awaiting cannot grow the app's queue without bound. The size of a call's params is not capped here: opening a file passes the file's bytes in them, and each app already limits the files it opens.
## Who can call it
`window.<app>` is a plain property of the page's own global object: it is not a network port and it has no key. Any script that runs in the page's origin can call every function and so has the same command control as the app's desktop control server; a page from another origin cannot reach it, but a same-origin parent, child or opener that holds a reference to the window can call it directly, and frame or opener separation alone does not stop that (the host messages in `craft-web-host` check `event.origin` and `event.source`, and those checks do not apply to a direct call). Treat it as you treat the page's own scripts.
`screenshot()` resolves with the reply the app's control method sends, plus `data` when the reply carries the PNG inline: the base64 PNG, whatever field the app names it (`pngBase64`, `png_base64`, `png` or `base64`). A reply with no inline PNG, such as one the app wrote to a `path`, has no `data` and is unchanged.
Which functions are installed follows the app's `Methods` table: only the functions whose control
method the table provides are installed; `request` is always there.
| Function | Call | Table entry | Default method |
|---|---|---|---|
| `request` | `request(method, params)` | — | — (as given) |
| `execute` | `execute(command, params)` | `execute` | `engine.execute {command, params}` |
| `commands` | `commands()` | `commands` | `engine.commands {}` |
| `docInspect` | `docInspect(params)` | `doc_inspect` | `document.inspect` |
| `uiInspect` | `uiInspect(params)` | `ui_inspect` | `ui.inspect` |
| `screenshot` | `screenshot(params)` | `screenshot` | `ui.screenshot` (with `data: true`); an inline PNG is always also in `data` |
`Methods::default()` is the desktop control-server method names; an app whose control channel
names a method differently, or lacks one, says so in its table (`Methods::NONE` is the empty table).
## How an app wires it
The app hands the crate its control channel as a `Request`; the crate never knows the app's
internals. Using VectorCraft's channel (the same one the desktop control server uses) as the example:
```rust
use std::rc::Rc;
use craft_web_api::methods::Methods;
use craft_web_api::page::{PageApi, Request};
// Send a ControlRequest to the app and await its reply object ({ok, result} / {ok: false, error}).
let request: Request = Rc::new(move |method: String, params: serde_json::Value| {
let (req, rx) = ControlRequest::new(method, params);
control_tx.send(req);
Box::pin(poll_reply(rx)) // resolves when the app answers the request
});
let page = Rc::new(PageApi::new("vectorcraft", Methods::default(), request));
page.install()?; // window.vectorcraft
```
## Beside the host messages
Opening a document, handing a saved or exported file to the host, the document name and the dirty
flag stay with the host messages in [`craft-web-host`](../web-host/README.md) (`<app>:open`,
`<app>:save`, `<app>:dirty`, …) and the app's own control methods. A page reaches any control
method through `request(method, params)`.
## Tests
`src/methods.rs` carries the native unit tests (the default and empty tables). `tests/page.rs` runs
under `wasm-bindgen-test` in Node with a canned control channel: install and keeping an app's own
functions, the table deciding which functions exist, the method and params each function sends,
error replies as rejections, the timeout, and the in-flight limit.