Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The callback

The problem a module has

Give a module an endpoint that transforms a document with a stylesheet. The caller passes stylesheet=urn:file:report.xsl.

The module cannot resolve that. It has no file space, no catalog, no cache, and no capability of its own. The name means something only in the host’s kernel.

So either the host resolves every argument eagerly before dispatching — which would defeat laziness, break Exists, and force it to know which arguments are resource references — or the module asks. It asks.

Where the seam already was

The good news is that nothing had to be invented. An endpoint never touches the kernel directly in the first place; it reaches it through its Invocation, which holds an Issuer. That indirection was already there so that sub-requests could inherit the caller’s capability and be traced.

So a module is simply handed the host as its issuer. From inside the endpoint, the call is the same one you would write in a linked-in endpoint:

/// `urn:greet:hello` — the module's one endpoint.
///
/// It takes a `name` argument that is *an IRI naming another resource*, and resolves it.
/// That resolution is the whole point of this book: the module does not own the resource,
/// cannot see it, and has no catalog of its own to find it in. `inv.source(…)` crosses
/// back into the **host's** kernel to get it.
pub fn hello() -> AsyncFnEndpoint {
    AsyncFnEndpoint::new("hello", |inv: &Invocation<'_>| -> InvokeFuture<'_> {
        Box::pin(async move {
            let target = inv.inline_str("name")?;
            let iri = Iri::parse(target).map_err(|e| Error::InvalidArgument {
                name: "name".into(),
                detail: format!("not a valid IRI: {e}"),
            })?;

            // ← THE CALLBACK. This is a resolution against the host, from inside the
            //   module, in the middle of the module's own invocation.
            let resolved = inv.source(&iri).await?;
            let who = String::from_utf8_lossy(&resolved.bytes).trim().to_string();

            let greeting = format!("Hello, {who}!");
            Ok(Representation::new(
                text_plain_utf8(),
                greeting.into_bytes(),
            ))
        })
    })
    .with_description(
        Description::new("hello")
            .title("Greet")
            .summary("Greets whoever the `name` resource resolves to.")
            .verb(Verb::Source)
            .verb(Verb::Meta),
    )
}

/// The module's space — everything it offers, independent of any host.
pub fn module_space() -> EndpointSpace {
    EndpointSpace::new().bind(Exact::new("urn:greet:hello"), hello())
}

inv.source(&iri) is the callback. The module is in the middle of its own invocation, and that line resolves a name in the host’s kernel — with the host’s spaces, the host’s cache, and the capability the invocation is already carrying.

That last clause matters: the module does not get authority by being a module. It borrows the caller’s, which can only narrow on the way down.

Why this is the interesting property

A remote peer is autonomous: it answers with its own resources. A module is parasitic — deliberately — it contributes endpoints while continuing to live in the host’s world.

That is what makes a module composable in a way a peer is not. The module’s endpoint can take any name the host can resolve, including endpoints from other modules, without knowing any of them exist.

It is also what makes a module harder to isolate than a peer, since the callback is a hole in whatever boundary you put around it. That tension is the subject of Where this actually stands.