Skip to content

feat(sdk): verify and measure per-operation entry points - #171

Merged
joeblau merged 1 commit into
mainfrom
feat/161-operation-entry-points
Oct 7, 2026
Merged

joeblau merged 1 commit into
mainfrom
feat/161-operation-entry-points

Conversation

@joeblau

@joeblau joeblau commented Oct 7, 2026

Copy link
Copy Markdown

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.

  • Import graph (.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: Info allMids fails if its closure reaches signing/, api/exchange, api/subscription or transport/websocket. A new NARROWER_THAN gate checks that each representative operation (Info, Exchange, Subscription, Explorer, actions/order) loads strictly fewer modules than its client and its barrel.
  • Export sync (.dev/export_sync_check.ts): every public actions/<name>.ts builder must have its ./actions/<name> entry, matching the existing rule for _methods files.
  • Build (.dev/build/build.ts):
    • transport/runtime.ts and 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/submitPrepared previously inlined parts of _dispatch.ts.
    • Guard for the bare-chunk-import stripping: the build records every chunk whose 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 are AGENT_DIGEST[0]=25-style table setup and resolved.add(systemRuntime). To test the guard, I added Object.freeze(Object.prototype.constructor) to a stripped chunk's source, and the build failed with has a top-level side effect, so its bare import cannot be dropped.
    • The published-closure walk (no signL1Action/createL1ActionHash/ExchangeClient/SubscriptionClient) now also covers api/subscription/_methods/allMids.js and api/explorer/_methods/explorerBlock.js.
  • Published consumer (consumer.ts, consumer.mjs): tsc --strict now runs under --module nodenext, --module node16 and --module esnext --moduleResolution bundler. New operation-specific inference checks: L2BookResponse from api/info/l2Book, BlockDetailsResponse from api/explorer/blockDetails, and the api/subscription/allMids event type. New @ts-expect-error negatives check that a response does not widen and that operation parameters are type-checked. Also asserts root.systemRuntime === systemRuntime.
  • Measurement (.dev/perf/entry_points.ts): client vs barrel vs one-operation consumers for Info, Exchange and Subscription, run against the published dist/.
  • Docs (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. The actions barrel note now mentions the order batcher.

Acceptance criteria (#161)

  • Source imports work in Bun (tests import through the per-op paths). Published JS and declaration imports were verified under Node and Bun runtime, and under TypeScript nodenext, node16 and bundler resolution.
  • Existing package entry points remain compatible. All explicit exports are kept, and the consumer checks every subpath's export names against source.
  • Small Info operations do not load Exchange, Subscription or signing code. This is checked at source level (forbidden-module budgets) and in the published closure walk.
  • Individual operations load fewer modules than their client and barrel (NARROWER_THAN gate). Representative closures have explicit budgets: Info allMids 12, Exchange order 35, Subscription allMids 8, Explorer explorerBlock 12, actions/order 35.
  • Published-package smoke checks verify runtime resolution (Node + Bun), identity sharing, that private paths are blocked, and operation-specific type inference.
  • Cold-process import time was measured on Node and Bun, and bundle size compared, for representative one-operation consumers (below).
  • Export and build checks pass without consumers needing internal paths. _* paths are null in 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 --minify output with dependencies external.

Family Import Files evaluated Bytes evaluated Bundle min Bundle gzip Node cold Bun cold
Info api/info/client 13 71,261 39,135 10,795 7.71 ms 5.39 ms
Info api/info 12 68,277 24,909 8,410 7.64 ms 5.28 ms
Info api/info/allMids 11 27,178 15,949 5,983 6.08 ms 4.09 ms
Exchange api/exchange/client 22 199,084 111,146 30,893 15.50 ms 15.29 ms
Exchange api/exchange 18 177,548 47,316 15,539 14.79 ms 14.97 ms
Exchange api/exchange/order 19 95,710 48,593 15,740 11.62 ms 11.40 ms
Subscription api/subscription/client 13 81,459 52,359 14,803 17.14 ms 5.31 ms
Subscription api/subscription 12 79,434 42,510 12,422 17.11 ms 5.27 ms
Subscription api/subscription/allMids 11 56,760 38,307 11,183 16.15 ms 4.38 ms
  • Info allMids vs InfoClient: 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 order vs ExchangeClient: cold import is 25% faster on both runtimes, the bundle is 56% smaller, and 52% fewer bytes are evaluated. Bundled, order is 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):

    Operation Operation Client Barrel
    Info allMids 6 98 98
    Exchange order 29 111 97
    Subscription allMids 2 33 33
    Explorer explorerBlock 2 12 12
    actions/order 30 — 161 (actions barrel)
  • Published file count for Exchange: in dist/, order evaluates 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 fail
  • bun run build: bundle verification, published consumer under node + bun, and tsc under nodenext / node16 / bundler
  • Guard negatives: a forbidden module in a budget fails check:imports; a top-level side effect in a stripped chunk fails build
  • bun .dev/perf/entry_points.ts --samples 15

Closes #161

🤖 Generated with Claude Code

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>
@joeblau
joeblau merged commit c98169f into main Oct 7, 2026
6 checks passed
@joeblau
joeblau deleted the feat/161-operation-entry-points branch October 7, 2026 00:48
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.

Expose individual API operations through narrow package entry points

1 participant