Skip to content

refactor(core,app)!: align with @cartesi/rollup, drop the HTTP-era surface - #203

Closed
tuler wants to merge 2 commits into
prerelease/v2from
claude/deroll-core-app-evaluation-d0nha0
Closed

refactor(core,app)!: align with @cartesi/rollup, drop the HTTP-era surface#203
tuler wants to merge 2 commits into
prerelease/v2from
claude/deroll-core-app-evaluation-d0nha0

Conversation

@tuler

@tuler tuler commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Why

This started as an evaluation of whether @deroll/core and @deroll/app still earn their place now that @cartesi/rollup exists — it already ships a Rollup.run({ advance, inspect }) loop, and the two deroll packages are only ~230 lines combined.

The conclusion was keep both, but stop them restating the protocol:

  • @deroll/app earns its place on multi-handler composition. Rollup.run() takes exactly one advance handler and one inspect handler; addAdvanceHandler/addInspectHandler are what let wallet.handler and router.handler be independently authored libraries sharing one loop. It also inverts the accept default (reject unless a handler opts in, which is the safer default for a rollup) and converts host-mode ENODATA into a clean exit.
  • @deroll/core earns its place as the composition seam — wallet and router depend on the App interface, not on the concrete app.
  • What did not earn its place was the vocabulary. Nine of ~22 exported types had zero consumers outside core/srcrequest_type: "advance_state", nested data, NoticeResponse = { index }, ReportResponse = Record<string, never> (still carrying its // XXX: should probably be 204 comment) — all leftovers of the removed Rollup HTTP Server transport. The surviving types were near-duplicates of the binding's, but narrower and drifting.

What changed

Protocol types come from @cartesi/rollup. @deroll/core re-exports AdvanceRequest, InspectRequest, RollupRequest, Voucher, DelegateCallVoucher, BytesLike, AddressLike, U256Like and Hex instead of declaring its own copies, so the two cannot drift. viem is no longer a dependency of core at all. core/src/types.ts is now mostly doc comments.

Dead types deleted: RollupAdvanceRequest, RollupInspectRequest, RequestType, RequestData, RequestMetadata, NoticeResponse, ReportResponse, VoucherResponse.

The advance request is flat. AdvanceRequestData/AdvanceRequestMetadataAdvanceRequest, which also removes the destructuring conversion in the request loop.

-app.addAdvanceHandler(async ({ metadata, payload }) => {
-    console.log(metadata.msgSender);
+app.addAdvanceHandler(({ msgSender, payload }) => {
+    console.log(msgSender);
     return "accept";
 });

Outputs are synchronous, and notice/report payloads are no longer wrapped. Emitting an output is a device write, not I/O the event loop can interleave with — finish pauses the entire guest. Vouchers keep their object argument, since they carry a destination and optional value besides the payload.

-const id = await app.createNotice({ payload: stringToHex("hello") });
-await app.createReport({ payload: stringToHex("hello") });
+const id = app.createNotice(stringToHex("hello"));
+app.createReport(stringToHex("hello"));

Handlers may still be async — they are awaited — but no longer have to be. The wallet and router handlers are now synchronous.

A handler exception rejects the request and is reported. Previously it went to stderr and the next handler ran anyway, which could let a later handler write state on top of a partially applied one, and left the failure invisible from outside the machine. Now the input is rejected immediately, remaining handlers are skipped, and the error is emitted as a report — which survives the rejection.

registerException is now on the App interface. NativeApp always implemented it, but it was missing from the interface createApp returns, so it was unreachable through the public API.

Bonus fix: Router.handler was doing bytesToString(toBytes(buffer)), which only round-tripped correctly because TextEncoder coerces a Buffer via toString(). It now reads the Buffer directly.

Two judgment calls worth reviewing

@cartesi/rollup is a non-optional peerDependency of @deroll/core. This weakens the "no native addon in the pure packages" property — wallet and router now get the tarball in their tree. I chose it anyway because the binding is a singleton native resource (new Rollup() returns -EBUSY if one is already open), so two copies at different versions in one tree is a real failure mode; a non-optional peer forces exactly one hoisted copy. What survives is "the addon is never loaded by the pure packages," which is what matters for their tests. An optional peer would have preserved the stronger claim but broken type resolution when absent.

The Notice/Report/Exception wrapper types are gone. This is a larger API break than just dropping async. Easy to revert to createNotice({ payload }) if preferred.

Verification

  • bun run build — 9/9 packages, including @deroll/docs, whose Vocs/twoslash snippets are genuinely typechecked. All 33 affected .mdx pages updated, plus the migration guide.
  • bun run lint clean; tsc --noEmit on apps/examples clean.
  • 35 tests pass (30 wallet, 5 router).
  • End-to-end against the real native binding + libcmt mock: minimal prints the flat request and exits 0; echo emits a correct Notice(bytes) output (c258d6e5 selector, advance-0\n payload); a throwing handler produced a report file with the stack, no output files, and the second handler never ran — the new error policy confirmed at runtime, not just in types.

bun run test at the repo root still fails on @deroll/genext2fs, whose addon needs a C toolchain the dev container lacks. Pre-existing and unrelated to this change; it aborts the turbo run, which is why wallet/router were run directly.

A changeset (minor for core/app/router/wallet) documents each break with diffs, and CLAUDE.md is updated.


Generated by Claude Code

…rface

@deroll/core restated the rollup protocol in types that had drifted from the
binding, and nine of them were leftovers of the removed Rollup HTTP Server
transport with no consumers at all. @deroll/app wrapped a deliberately
synchronous binding in an asynchronous facade.

Keep both packages — the multi-handler loop and the interface that lets wallet
and router plug into it are what deroll actually adds over Rollup.run(), which
takes exactly one handler of each kind — but stop them restating the protocol.

- core re-exports AdvanceRequest, InspectRequest, RollupRequest, Voucher,
  DelegateCallVoucher, BytesLike, AddressLike, U256Like and Hex from
  @cartesi/rollup instead of declaring its own copies, so they cannot drift.
  @cartesi/rollup becomes a peer dependency: the rollup device allows one open
  handle per process, so the tree must resolve to a single copy. viem is no
  longer a dependency of core.
- delete RollupAdvanceRequest, RollupInspectRequest, RequestType, RequestData,
  RequestMetadata, NoticeResponse, ReportResponse and VoucherResponse.
- flatten the advance request: AdvanceRequestData/AdvanceRequestMetadata become
  AdvanceRequest, carrying the metadata fields directly, which also drops the
  destructuring conversion in the request loop.
- add registerException to App. NativeApp always implemented it, but it was
  missing from the interface createApp returns, so it was unreachable.
- a handler exception now rejects the request and is emitted as a report,
  skipping the remaining handlers, instead of going to stderr and letting the
  next handler write state on top of a partially applied one. Reports survive a
  rejection, so the failure is visible from outside the machine.
- outputs are synchronous, and notice/report payloads are passed directly
  rather than wrapped in an object. Handlers may still be async but need not be;
  the wallet and router handlers are now synchronous.

Router.handler also reads its query straight out of the request Buffer instead
of round-tripping through viem's toBytes, which only decoded correctly by
accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ud9jF1bYpLMhJaJU8q1KDX
@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
deroll Ready Ready Preview Aug 13, 2026 5:40am
deroll-explorer Ready Ready Preview Aug 13, 2026 5:40am

@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3a9efd3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@deroll/core Minor
@deroll/app Minor
@deroll/router Minor
@deroll/wallet Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

The string union was the last of the Rollup HTTP Server's vocabulary left in
the API. deroll v1 did not merely mirror it — it passed the handler's return
value straight into the transport:

    const { data } = await this.POST("/finish", { body: { status }, ... });
    status = await this.handleAdvance(...);

`RequestHandlerResult` was the `/finish` request body's `status` enum. With the
HTTP transport gone, handlers return a boolean, matching `finish({ accept })`
in the binding.

The words were misleading anyway. A handler returning "reject" never rejected
the input, it only declined it and passed it to the next handler; the input is
rejected only when no handler accepted. `true`/`false` describes that chain
honestly, where "accept"/"reject" claimed an authority a single handler in a
chain does not have.

The type is a strict boolean with no `void`: `Rollup.run` in the binding
accepts a request unless a handler returns false, whereas deroll rejects unless
a handler opts in, so a handler that falls off its end must be a type error
rather than a silent accept.

Also tightens the wallet deposit assertions from `toBeTruthy()` to
`toBe(true)`. They were vacuous before — both "accept" and "reject" are truthy
strings, so they passed whatever the handler returned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ud9jF1bYpLMhJaJU8q1KDX

tuler commented Aug 13, 2026

Copy link
Copy Markdown
Owner Author

Closing in favor of #205.

This PR kept @deroll/app on the argument that multi-handler composition is real value the binding doesn't provide. That was true when it was opened, and is no longer: @cartesi/rollup@1.0.0-alpha.1 now ships chain and broadcast, the handler types, and a run that resolves when the host mock's inputs are exhausted.

That covered everything @deroll/app and @deroll/core contained, so #205 removes both packages rather than leave them re-exporting someone else's functions. It also pins @cartesi/rollup@1.0.0-alpha.1, which this branch does not.

The work here is not lost — #205 contains these commits and builds on them. Left as a record of the alternative.


Generated by Claude Code

@tuler tuler closed this Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants