refactor!: replace @deroll/app with chain composition over Rollup.run - #205
Merged
Conversation
…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
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
Every twoslash snippet is typechecked by the vocs build, so the docs are migrated in full rather than left behind the code. The API reference for the App wrapper is gone, since the methods it documented now belong to @cartesi/rollup: - createApp, addAdvanceHandler and addInspectHandler are deleted; the loop and its handlers are covered by "The Rollup Loop" and the handler pages. - the four create*Voucher/Notice/Report pages are replaced by a single Outputs page, which keeps the conceptual content (when to use each output, what a delegate-call voucher is for, why reports survive a rejection) and documents them as rollup.emit* calls. - chain and broadcast get reference pages, being what deroll now actually owns. The migration guide gains sections for registering several handlers, for createRouter losing its app, and for registerException becoming emitException. CLAUDE.md records the new architecture, corrects the create-app entry (templates come from apps/examples in this repo, not from cartesi/application-templates) and notes the two upstream rough edges: Rollup.run never resolving on the host, and the libcmt mock being unable to revert a rejected request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ud9jF1bYpLMhJaJU8q1KDX
🦋 Changeset detectedLatest commit: 0396248 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
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 latest updates on your projects. Learn more about Vercel for GitHub.
|
@cartesi/rollup 1.0.0-alpha.1 adds `chain` and `broadcast`, the handler types, and a `run` that resolves when the host mock's inputs are exhausted. That was the entire content of @deroll/core, so the package is removed rather than left re-exporting someone else's functions. Upstream chose the strict-boolean contract for composed handlers and kept `boolean | void` for the handlers `run` takes directly, which is the split this repo argued for: a `run` handler decides an input's fate alone, so no answer can mean accept, while a composed handler only makes a claim and a missing answer has no sensible default. - wallet and router take their types from @cartesi/rollup, as a peer dependency. - Router.handler returns whether a route matched, so it composes with `chain` and an unmatched query falls through. Its rollup parameter is narrowed to Pick<Rollup, "emitReport">, which keeps it satisfiable by a plain object in tests since Rollup carries a #private brand. - create-app no longer scaffolds @deroll/core; @cartesi/rollup was already a direct dependency of generated apps for esbuild bundling. - the chain/broadcast/Core doc pages are deleted, being reference material for another package now; composing handlers is covered in the handler pages. All seven dev:* examples exit 0 against the mock again, including router and walletRouter, which previously died on the mock's -ENOSYS when a rejected input was also the last one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ud9jF1bYpLMhJaJU8q1KDX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Alternative to #203 — open both and pick one. This branch contains #203's commits plus three more, so the two PRs are the same base and the same starting point, differing only in how far they go. Merging this closes #203.
The question
#203 kept
@deroll/appon the argument that multi-handler composition is real value the binding doesn't provide. That's true, but it does not follow that composition needs a class with a loop and five pass-through methods.Reading the binding's
run()implementation settles it. Deminified:That catch block is character-for-character the error policy #203 implements in
NativeApp— reject, emit the stack as a report, sameString(e?.stack ?? e)formatting. Andhandlers.advance ? … : falseis the same reject-by-default. #203 didn't just align deroll's types with the binding; it converged deroll's loop semantics ontorun()'s. What remained inNativeAppwas duplicated logic.So: delete the wrapper, keep the composition.
The API
What replaced what
NativeApp(159 lines) +createAppRollup.runin the bindingAppinterfaceRollupContext, aPickofRollupaddAdvanceHandler/broadcastAdvanceRequestschain()/broadcast()— 47 lines totalapp.createNotice(…)etc.rollup.emitNotice(…)etc.@deroll/appis deleted.@deroll/coreis now 47 lines of composition plus 75 of type re-exports, with no runtime dependency on the binding — it imports only types.Three things that came out better than expected
The router's wiring dependency disappears.
createRouter({ app })existed only so the router could callapp.createReport, forcing a create-app → create-router-with-app → register-on-app ordering.runpasses the rollup as the handler's second argument, socreateRouter()now takes nothing.RollupContextis a better contract, not just a testing workaround. It exposes the emit methods plusprogress/giowhile omittingfinish,run,closeand the merkle helpers — so a handler can no longer callfinish()mid-request and desynchronize the loop, which passing the rawRollupwould allow. Being a structuralPickrather than the class (which carries a#privatenominal brand) also means tests pass a plain object:@deroll/routerdroppedvitest-mock-extendedentirely.Composition is testable without a device.
packages/app/core/__tests__/compose.test.tsadds 8 tests that load no native addon at all — the file imports only types, so nothing pulls the binding in. That includes a test asserting a handler exception propagates and that later handlers don't run, which is the property that makeschainneed no try/catch.Docs
Migrated in full — Vocs typechecks every snippet, so they could not be left behind.
createApp,addAdvanceHandler,addInspectHandler,createNotice,createReport,createVoucher,createDelegateCallVoucher.chainandbroadcastreference pages, and a single Outputs page replacing the fourcreate*pages. It keeps the conceptual content — when each output is available, why reports survive a rejection, what a delegate-call voucher is for — since those are Cartesi concepts rather than binding API, and deferring them to@cartesi/rollupwould have lost real material.application.mdx→ "The Rollup Loop",overview.mdx→ "Core", both handler pages, quick-start, and the migration guide (new sections for composing handlers,createRouterlosing itsapp, andregisterException→emitException). Sidebar updated.CLAUDE.mdrecords the new architecture and corrects thecreate-appentry: templates come fromapps/examples/src/in this repo onprerelease/v2(src/index.ts:76), not fromcartesi/application-templates. So the examples are the scaffolded templates and move together —create-appneeded only a one-line dependency change, since@cartesi/rollupwas already a direct dependency of scaffolded apps for esbuild bundling.Known regression, deliberate
Rollup.runreturnsPromise<never>: itsfinishcall sits outside the try, so exhaustingCMT_INPUTSon the host rejects rather than resolving.app.start()used to convert that into a clean exit. All sevendev:*scripts now exit 1 on a successful run.This was an explicit call — host-mode behavior belongs in
@cartesi/rollup, not in a deroll shim. The upstream fix is small: movefinish()inside thetry, or resolve on-ENODATA, plus a decision about whetherrun()should ever terminate.Related pre-existing bug, unchanged by this PR and also upstream: the libcmt mock cannot revert, so any rejected request fails with
-ENOSYS(io-mock.c:200). This is why therouterandwalletRouterexamples exit 1 under the mock —0.binis an advance input and neither registers an advance handler. Verified identical on #203's branch, so it is not a regression here.Verification
bun run build— 9/9 packages including@deroll/docs, with every twoslash snippet typechecking. Lint clean.tsc --noEmiton examples clean.minimalandechorun end-to-end against the real binding and mock;echoemits a correctNotice(bytes)output (c258d6e5selector).prerelease/v2, same base as refactor(core,app)!: align with @cartesi/rollup, drop the HTTP-era surface #203.Open question
@deroll/coreis now ~25 lines of runtime code. It is worth asking whetherchain/broadcastshould live in@cartesi/rollupas a convenience alongsiderunitself — at which point@deroll/coredisappears and the app pillar becomes just@deroll/walletand@deroll/router.One thing would need resolving upstream first:
runtreatsvoidas accept (!== false), whilechainrequires a strictbooleanand treats falsy as declined. Movingchainupstream unchanged would mean the same handler means opposite things depending on whether it is passed torundirectly or throughchain— an inconsistency inside one package, which is worse than the current split across two.Generated by Claude Code