Repository navigation
feat(sdk): verify and measure per-operation entry points - #171
Merged
Merged
Conversation
The `./api/<family>/<op>` and `./actions/<op>` entry points shipped with the package; this closes the remaining acceptance criteria of #161 around them. - Import graph: add budgets for a single Exchange (`order`) and Subscription (`allMids`) operation, forbid family-crossing modules in each operation's closure (no signing or Exchange/Subscription code behind Info `allMids`), and assert every representative operation loads fewer modules than its client and barrel. - Export sync: require a `./actions/<op>` entry for every public builder, the same contract `_methods` files already had. - Build: externalize `transport/runtime.ts` and the bounded dispatch lanes to the shared core so narrow bundles never inline stateful modules; guard the bare-chunk-import stripping by failing the build when a deferred chunk has a top-level side effect beyond chunk-local writes; walk the published closures of single Subscription and Explorer operations too. - Published consumer: type-check under `nodenext`, `node16` and `bundler` resolution, with operation-specific inference and negatives for Info, Explorer and Subscription operations, and check `systemRuntime` identity. - `.dev/perf/entry_points.ts`: client vs barrel vs operation for Info, Exchange and Subscription — files and bytes evaluated, minified/gzip bundle size, and cold import medians on Node and Bun. - Docs: a "Choosing an import style" section, per-op paths in every example. Closes #161 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
The 193
./api/<family>/<op>entries and 66./actions/<op>builder entries already shipped with the package (#166), along with the compact published export patterns and the shared-core build. This PR closes the remaining #161 acceptance criteria around them: regression budgets for every family, stronger published-package checks, a guard on the riskiest build step, measurements, and docs on which import style to use..dev/import_graph_check.ts): new budgets for a single Exchange operation (api/exchange/order, 29/35) and Subscription operation (api/subscription/allMids, 2/8). Each operation budget also lists forbidden modules: InfoallMidsfails if its closure reachessigning/,api/exchange,api/subscriptionortransport/websocket. A newNARROWER_THANgate checks that each representative operation (Info, Exchange, Subscription, Explorer,actions/order) loads strictly fewer modules than its client and its barrel..dev/export_sync_check.ts): every publicactions/<name>.tsbuilder must have its./actions/<name>entry, matching the existing rule for_methodsfiles..dev/build/build.ts):transport/runtime.tsand the bounded-dispatch lanes (_dispatch.ts, which holds module-level state) move into the externalized shared core, so narrow bundles never inline a stateful module.prepareRequest/submitPreparedpreviously inlined parts of_dispatch.ts.import "./_chunks/…"was dropped. It fails if any top-level statement in that chunk is something other than a declaration or a write/method call on a binding the chunk itself declares. That forbids a bare call, a write to an imported or global binding, or a class static block. Today 22 chunks are stripped; their only top-level expression statements areAGENT_DIGEST[0]=25-style table setup andresolved.add(systemRuntime). To test the guard, I addedObject.freeze(Object.prototype.constructor)to a stripped chunk's source, and the build failed withhas a top-level side effect, so its bare import cannot be dropped.signL1Action/createL1ActionHash/ExchangeClient/SubscriptionClient) now also coversapi/subscription/_methods/allMids.jsandapi/explorer/_methods/explorerBlock.js.consumer.ts,consumer.mjs):tsc --strictnow runs under--module nodenext,--module node16and--module esnext --moduleResolution bundler. New operation-specific inference checks:L2BookResponsefromapi/info/l2Book,BlockDetailsResponsefromapi/explorer/blockDetails, and theapi/subscription/allMidsevent type. New@ts-expect-errornegatives check that a response does not widen and that operation parameters are type-checked. Also assertsroot.systemRuntime === systemRuntime..dev/perf/entry_points.ts): client vs barrel vs one-operation consumers for Info, Exchange and Subscription, run against the publisheddist/.guides/tree-shaking.md): new "Choosing an import style" table (clients, API barrels, one-operation paths, builders +actions/execution). Explorer and Subscription examples now use per-op paths. Theactionsbarrel note now mentions the order batcher.Acceptance criteria (#161)
nodenext,node16andbundlerresolution.NARROWER_THANgate). Representative closures have explicit budgets: InfoallMids12, Exchangeorder35, SubscriptionallMids8, ExplorerexplorerBlock12,actions/order35._*paths arenullin the published map, and export sync requires an entry for every operation and builder.Measurements
bun run build && bun .dev/perf/entry_points.ts --samples 15. Node v24.10.0, Bun 1.4.0, Apple Silicon (macOS). Each consumer imports the entry point plus its transport and performs one call. Cold import times are medians of 15 fresh processes, timed from inside the process. Bundle sizes are esbuild--minifyoutput with dependencies external.api/info/clientapi/infoapi/info/allMidsapi/exchange/clientapi/exchangeapi/exchange/orderapi/subscription/clientapi/subscriptionapi/subscription/allMidsInfo
allMidsvsInfoClient: cold import is 21% faster on Node and 24% faster on Bun, the bundle is 59% smaller (gzip −45%), and 62% fewer bytes are evaluated.Exchange
ordervsExchangeClient: cold import is 25% faster on both runtimes, the bundle is 56% smaller, and 52% fewer bytes are evaluated. Bundled,orderis about the same size as the tree-shaken barrel, which is expected because bundlers already drop the barrel's siblings. The per-op path pays off for unbundled Node/Bun.Subscription
allMids: Bun cold import is 18% faster than the client. On Node, loading the WebSocket transport dominates (~16 ms), so the per-op gain there is about 1 ms.Source module closures (
check:imports):allMidsorderallMidsexplorerBlockactions/orderactionsbarrel)Published file count for Exchange: in
dist/,orderevaluates 19 files against the barrel's 18. The narrow bundle imports the shared_core/*files separately, while the barrel reaches the same code through larger split chunks. It still evaluates about half the bytes and imports 22% faster. The module-count gates run on source closures, which is what Bun loads directly.Test plan
bun run check(format, lint, docs, types, ts7, jsdoc, export sync, import budgets)bun run test:offline: 2072 pass, 0 failbun run build: bundle verification, published consumer under node + bun, and tsc under nodenext / node16 / bundlercheck:imports; a top-level side effect in a stripped chunk failsbuildbun .dev/perf/entry_points.ts --samples 15Closes #161
🤖 Generated with Claude Code