Keep both craft-web-api and craft-web-host in the workspace list, the README and the lockfile. Point the host README at the command API that landed in #413, and the command API README at these host messages. Co-authored-by: Cursor <cursoragent@cursor.com>
craft-web-host
The host messages between a Craft app's web build and the page that hosts it, as one contract and one bridge for every app (storytold/craft-libs#358). A page frames the app with ?host=parent, or runs it directly with data-host="page" on its canvas; either way the same messages pass files in, saves out, and unsaved-changes state, so a page can host any of the apps the same way.
The crate has two parts:
- The contract (all targets): the message kinds and their
<app>:<kind>types, the contract version, how the hosting mode is read from the URL and the canvas, and the clean-up of file names a host passes in. Pure, tested natively. - The bridge (
web, wasm32 only):Host::connect(app, canvas)finds the host,listenhears its messages, andpost_ready,post_save,post_dirty,post_failedandpost_handoffanswer it. Only the same-origin parent window is heard and answered; with a parent on another origin the app runs as if unframed.
Messages
| Kind | Direction | Fields | Meaning |
|---|---|---|---|
ready |
app → host | version, appVersion?, file? |
the app can take a document; file: true means it reads a File/Blob in slices |
host |
host → app | version, handoff? |
what the host supports; handoff: true means it routes hand-offs |
open |
host → app | name, and bytes or file + size |
open a document as File › Open would |
save |
app → host | name, bytes, id?, saveAs? |
Save, Save As or Export: the host stores the file |
saved |
host → app | id |
the save with that id is stored; the app clears dirty |
dirty |
app → host | dirty |
unsaved changes appeared or went away |
failed |
app → host | error, id? |
a document or a save could not be taken |
handoff |
app → host | name, and bytes or file + size |
this file belongs to another app |
handoff-done |
host → app | name, app |
the host gave it to the app that edits it |
With ?host=parent a message is an object with type (pdfcraft:open) and the fields, sent with same-origin postMessage; bytes go as a transferred ArrayBuffer. With data-host="page" it is a CustomEvent of that type on the canvas, with the fields in detail.
Compatibility with the bridges shipped before this crate:
- Fields marked
?are optional. A host answerssavedonly for asavethat carries anid; an app that sends none treats its save as done once it is posted. - A host that never sends
hostgets the behaviour of the earlier bridges. sizebesidefileis for hosts and older bridges; this crate takes aBlob's length from theBlobitself.- A page host may dispatch an event while the app's handler runs (a
savedanswering thesaveit just posted). The bridge holds it and calls the handler again once the running call returns, so messages arrive one at a time and in order. versionis"1". The earlier bridges put the app's own release in it ("0.14.0"), so a host treats aversionit doesn't know as"1". The crate sends the release asappVersion.
Use in an app's web shell
#[cfg(target_arch = "wasm32")]
if let Some(host) = craft_web_host::web::Host::connect("pdfcraft", &canvas) {
host.listen(move |message| match message {
craft_web_host::web::Message::Open { name, data } => {
// Check data.size() against the app's limit before copying it in.
}
craft_web_host::web::Message::Saved { id } => { /* clear dirty for that save */ }
_ => {}
});
let _ = host.post_ready(env!("CARGO_PKG_VERSION"), false);
// Saves: host.post_save(name, &bytes, None, save_as); unsaved state: host.post_dirty(dirty).
}
The page command API is craft-web-api (storytold/craft-libs#413): window.<app>.request and the command functions. It sits beside these messages. Opening, saving, the document name and the dirty flag stay here.