Repository navigation
feat(transport): inject runtime dependencies for isolated, deterministic tests - #164
Merged
Merged
Conversation
joeblau
force-pushed
the
feat/158-runtime-injection
branch
5 times, most recently
from
October 6, 2026 23:24
57e4375 to
2a520d7
Compare
…tic tests Add an optional `Runtime` (wall clock, monotonic clock, timers, random source) plus an injectable `fetch` and `webSocketFactory`, resolved once per component at construction and defaulting to the platform. Elapsed-time logic (timeout wheel, rate limiter, info cache, WebSocket attempt windows) now reads monotonic time; nonces and HTTP-date Retry-After stay on wall time. Retry and reconnect jitter read the injected random source. Timer-driven tests move from global FakeTime / fetch / WebSocket patching to local FakeRuntime, injected fetch, and FakeSocket helpers, with cleanup assertions for timers and abort listeners. Closes #158 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Browsers' window.fetch throws "Illegal invocation" when invoked with a
receiver other than the global, so `this._fetch(url, init)` broke
`new HttpTransport({ fetch: window.fetch })` on every request. Call it
through a local instead and cover it with a receiver-sensitive fetch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
joeblau
force-pushed
the
feat/158-runtime-injection
branch
from
October 6, 2026 23:33
1ca3e0b to
7635a27
Compare
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
Transports and their schedulers no longer have to reach for globals. A new
Runtimecontract (now,monotonicNow,setTimeout/clearTimeout,setInterval/clearInterval,random) plus an injectablefetchandwebSocketFactorycan be passed per transport. When omitted, everything falls back to the platform.src/transport/runtime.ts, exported as@bloxwap/hyperliquid/transport/runtimeand from thetransportbarrel:Runtime,TimerHandle,systemRuntime,resolveRuntime,delay.resolveRuntime()runs in each constructor. It returnssystemRuntimeitself when nothing is overridden, and an already-resolved runtime unchanged, soWebSocketTransportresolves once and shares one object across its socket, dispatcher and keep-alive. Overriding methods keep their receiver, so a stateful fake clock can be passed directly.now) drives only protocol timestamps: nonces and HTTP-dateRetry-After.monotonicNow(performance.now) drives theTimeoutWheel, the token-bucket refill,InfoCacheTransportexpiry andWebSocketQuotaattempt windows. A wall-clock correction neither fires nor stalls them.retryDelayMsanddefaultReconnectionDelayuseruntime.random.HttpTransportOptions.fetchand.runtimeWebSocketTransportOptions.runtimeand.webSocketFactoryReconnectingWebSocketOptions.runtimeand.webSocketFactoryWebSocketQuotaOptions.runtimeandInfoCacheOptions.runtimeruntimeargument onTokenBucketRateLimiter,TimeoutWheel,WebSocketDispatcherandWebSocketKeepAlivecreateNonceManager(maxEntries, runtime)delay(), which releases its timer and abort listener on every path. Like thesleep()it replaces, itunrefs the platform timer.quota, aWebSocketTransportkeeps the per-network sharedWebSocketQuotaon the platform clock, even when a runtime or socket factory is injected.globalNonceManageris unchanged: one process-wide manager keyed by signer and network, on the platform wall clock. It is intentionally not injectable per transport, so HTTP and WebSocket clients for the same signer can never fork nonce state. Tests that need control can pass the existingnonceManageroption onExchangeClient.createNonceManager(…, runtime)stays internal.Tests
tests/_fakeRuntime.ts(FakeRuntime, a virtual wall and monotonic clock with timers,pendingTimers, and an optionalrandom) andtests/_fakeSocket.ts.tests/transport/_runtime.test.tscovers:random = 0.5Retry-Afteragainst the injected wall clockresolveRuntimeidentitydelayunreftests/transport/_runtimeIntegration.test.tscovers:ExchangeClients on separate fake runtimes still get unique per-signer noncespendingTimers == 0after close@stdFakeTimeand global patching toFakeRuntimewith an injected fetch or socket factory:_rateLimiter,_infoCache,_infoCacheExpiryand_keepAliveTimeoutWheelblock in_abortretryOnRateLimitandrateLimitblocks inhttp/mod.test.ts_connectionQuota_reconnectingSocket. These used realsleep/polling before.api/exchange/_noncehttp/mod.test.tsand_reconnectingSocket.test.tscases still patch the globalfetch/WebSocketon purpose. They are the coverage for the default (omitted-dependency) path, which reads the globals at call time.Docs
New Injectable runtimes section in
apps/docs/content/docs/transports.md. It covers the options, the wall vs. monotonic split, the shared quota and nonce caveats, and a small runnable fake-clock test that drives a 429 retry without sleeping. The example was run underbun testand type-checked before landing.Notes
maintoo (the scheduled Security run has been failing since 2026-10-05). The cause is pre-existingbun.lockadvisories: braces, katex, sharp, smol-toml and source-map-js. braces is fix(docs): lint Markdown without markdownlint-cli2 to drop vulnerable braces #163's territory, and the rest are docs-app transitive deps. This PR leaves the lockfile alone.apps/docs/app/layout.tsxgets a Biome format-only fix. It came in unformatted with 68cb6d2 (pushed directly to main) and would otherwise failbun run checkin Code Quality.Acceptance criteria
systemRuntimereads globals at call time; the existing global-patching tests still pass unchanged)._runtime.test.ts,_runtimeIntegration.test.tsand_connectionQuota.test.ts.pendingTimers === 0,getEventListeners(...) === 0).Retry-AfterHTTP-date).Measurements
These are perf scenarios with default dependencies:
main(68cb6d2) vs. this branch on the same machine (Apple M3 Max, Bun 1.4.0 canary). Runs were interleaved, and each figure is the median of the per-run medians.Transport, final commit (6 interleaved runs each):
Per-run spreads overlap: http_request was 1.02–1.14 µs on main and 1.06–1.17 µs here. An earlier 5-run set on a previous revision read +0.5% / −10.6% / −1.3%. The residual ~30 ns per HTTP request comes from
monotonicNow. It defaults toperformance.now(), which costs about 7 ns more thanDate.now()in Bun (measured: 30 ns vs. 23 ns), and theTimeoutWheelreads it once per request through the runtime object. That is the inherent cost of the monotonic deadlines the issue asks for. When nofetchis injected, the request path calls the globalfetchdirectly, with no wrapper.Transaction (3 runs each, earlier revision):
nonce_manager_over_capacity: after the first CI gate run showed +11.4%,createNonceManager()without a runtime (the process-wide manager) goes back to a directDate.now()call. Only an injected runtime pays the indirection. Local 8-run medians were 84 ns (main) vs. 89 ns (branch), within this scenario's run-to-run spread (80–125 ns on both sides). The CI gate then read −4.1%.CI perf gate (
bun run perf:gate, all 57 scenarios, final commit): 0 regressed, 57 unchanged:Earlier revisions hit two flaky gate failures, both in signing scenarios this PR doesn't touch: a
canonicalize_order_1band of −2.4% … +219%, and an "inconclusive"multisig_user_signed_3_signers_no_ecdsa..dev/perf/info_cache_capacity.ts(3 runs each) was within ±10% with mixed signs across all 15 mode × size cells. There is no consistent direction. On this branch the script injectsruntime: { now, monotonicNow }, so it is not a strict defaults comparison.Test plan
bun install && bun run check(format, lint, docs, sdk: tsc, ts7, jsdoc, export, imports)bun run test:offline: 1987 pass, 0 failbun run build: dist exports./transport/runtimebun run perf -- --filter transport/--filter transactionvs.main(above)bun testfile and type-checkedCloses #158
🤖 Generated with Claude Code