Skip to content
Logo

Composing Handlers

run takes one handler per request kind, but an application usually has several independent concerns per kind: a wallet that claims portal deposits, the application's own logic, an indexer that watches everything go by. chain and broadcast fold a list of handlers into the single one run expects.

import { , ,  } from "@cartesi/rollup";
import {  } from "./wallet.js";
import {  } from "./app.js";
import {  } from "./indexer.js";
 
const  = new ();
 
await .({
    // the wallet claims deposits; everything else falls through to the app
    : (.handler, .handler),
    // both answer every query, with a report each
    : (.query, .query),
});

Competing or observing

The two composers differ in one thing: whether an accepted request is still offered to the handlers that come after.

ComposerHandlers
chain(...handlers)compete — offered the request in turn; the first to accept wins and the rest never run
broadcast(...handlers)observe — every one is offered the request, in order, whatever the others answered

Reach for chain when at most one handler owns a given request (routing by msgSender, by a command in the payload, by a portal address), and for broadcast when several concerns react to the same request independently.

Both are ordinary functions returning a handler, so they nest: chain(wallet.handler, broadcast(app.handler, metrics.handler)) gives the wallet first refusal and lets the other two share whatever it declined.

The handlers always run one after another, never concurrently — they share one device and one application state, and the order in which they emit outputs is part of what the machine proves.

Accepting, declining, rejecting

Inside a composition, a handler's boolean is a claim, not a verdict: true accepts the request, false declines it and leaves it to the others. Only the composed handler's answer reaches run, and that is what decides the input's fate — both composers return true if any handler accepted and false if none did, so a request nobody claimed is rejected.

Exceptions are not caught. The first one aborts the remaining handlers and propagates to run, which rejects the input and emits the error as a report — which is exactly what you want, and why neither composer needs a catch.

Handlers must answer

A composed handler must return a real boolean. This is stricter than run, which reads a handler that returns nothing as an accept:

(, );
Argument of type '(request: AdvanceRequest, rollup: Rollup) => void' is not assignable to parameter of type 'RequestHandler<AdvanceRequest>'. Type 'void' is not assignable to type 'boolean | Promise<boolean>'.

The strictness is deliberate, and it is not the same question being answered twice. A handler passed to run decides the input's fate by itself, so "no answer" can safely mean accept — there is nobody else to ask. A handler inside a composition only claims the request, and there is no sensible default for a missing claim: reading it as an accept would silently swallow every handler after it, reading it as a decline would silently drop the request and reject an input run would have accepted. So the composers refuse to guess — the type rules it out, and for JavaScript callers a TypeError at runtime does, which run reports like any other handler failure.

In practice this means a handler you want to compose declares its answer up front:

import type {  } from "@cartesi/rollup";
 
const  = "0x0000000000000000000000000000000000000001";
 
// a handler that only claims what it recognizes
const :  = (, ) => {
    if (. !== ) {
        return false; // not mine — the next handler gets it
    }
    .(.("deposit credited"));
    return true;
};

AdvanceRequestHandler and InspectRequestHandler (both RequestHandler specialized to a request kind — see Types) name that contract. They are assignable to the matching run handler, so a composed handler drops straight into run with no adapter.