Make npm run build exit 0 by declaring the types the code already depends on - #818
Draft
kriszyp wants to merge 7 commits into
Draft
Make npm run build exit 0 by declaring the types the code already depends on#818kriszyp wants to merge 7 commits into
npm run build exit 0 by declaring the types the code already depends on#818kriszyp wants to merge 7 commits into
Conversation
`tsc --project tsconfig.json` exited 2 on a clean checkout of main with 31 errors while
still emitting a complete dist/, so the build's exit code was not a usable success signal.
Three families, all declaration gaps rather than code defects:
- 17 TS2339 on the `ws` library's private `_socket`, which replication's keep-alive
watchdog and blob-send backpressure both read. Declared once as a replication-scoped
`ReplicationWebSocket = WebSocket & { _socket: Socket | null }` applied at the four
transport ingress boundaries, so all 17 reads type without per-site casts. (A
`declare module 'ws'` augmentation does not work: @types/ws exports the class via
`export =`, so the imported type is the class instance type, which an interface
augmentation cannot merge into.)
- 4 TS2339 on `error.code` / `error.isHandled` in the socket 'error' handler, fixed by
annotating that one listener parameter.
- 9 TS2345/TS2365 in analytics/profile.ts, where pprof-format declares sample fields as
`number | bigint`; asserted `as number`, the idiom the same function already uses.
- 1 TS2550 for `Promise.withResolvers` in pinned core code; harper-pro's tsconfig had no
`lib`, so it defaulted to ES2022. Mirrors core/tsconfig.json's list, which already
carries ES2024.Promise for this exact call. `target` is unchanged.
Also adds a unit test pinning the `ws` internals the new type declares, and declares
@types/ws directly (production code imports `ws`, but its types arrived transitively
through the dev-only `mqtt`).
Zero runtime change: all 1033 emitted dist/ files are byte-identical to the pre-change
build except the two touched sources' .js.map, whose mappings shift because erased
`as`/annotation syntax moves source columns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CwEcEqeKKoP4WcxFXWPaB
- record the @types/ws root edge in package-lock.json (npm install --package-lock-only); it resolved to the hoisted transitive copy, so package.json claimed a dependency the lockfile's root entry did not - bound the ws contract test's socket waits: .mocharc.json sets timeout: 0, so a bind or connect failure would have hung the whole unit suite instead of failing this file - correct the tsconfig comment (the lib list is TypeScript's default for target ES2022 plus ES2024.Promise, which is the reviewable fact; "mirrors core" is not) and the test's header and one case title Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013CwEcEqeKKoP4WcxFXWPaB
onceOrFail left its 'error' listener attached after a successful wait, so a later socket failure resolved into a no-op reject instead of surfacing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013CwEcEqeKKoP4WcxFXWPaB
Nothing outside replicationConnection.ts names the type, and an exported type under
TypeStrip is a footgun: a value-shaped `import { ReplicationWebSocket }` typechecks but
survives stripping and fails at runtime.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013CwEcEqeKKoP4WcxFXWPaB
…est header Head-side line numbers in replicationConnection.ts rot on its next edit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013CwEcEqeKKoP4WcxFXWPaB
`sendAuditRecord`'s backpressure wait is not the only non-optional `_socket` read; the open handlers' `unref()` are too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013CwEcEqeKKoP4WcxFXWPaB
There was a problem hiding this comment.
Code Review
This pull request introduces TypeScript type safety improvements and compatibility updates. It defines a custom ReplicationWebSocket type to access the private _socket field of ws for keep-alive and backpressure handling, adds @types/ws as a dependency, updates tsconfig.json to support Promise.withResolvers(), and introduces a new unit test suite to pin the ws internals. The review feedback highlights a potential synchronous TypeError in the test cleanup hook if the server fails to initialize, recommending proper guards and try-catch blocks for robust resource cleanup.
If `before` fails before the server is constructed, an unguarded close() throws a TypeError over the real failure. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013CwEcEqeKKoP4WcxFXWPaB
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.
npm run build(tsc --project tsconfig.json) exited 2 on a clean checkout ofmainwhile still emitting a completedist/, so the build's exit code was not a usable success signal for humans or agents. All 31 diagnostics were declaration gaps rather than code defects: the repo depended on a privatewsfield, onerrno-shaped socket errors, onpprof-format's narrower runtime reality, and on a Node 22 lib, without declaring any of them. This declares each one at its own layer, and changes no runtime statement — every emitteddist/**/*.jsfile is byte-identical to the pre-change build._socketws's private_socket;@types/wsdeclares only the public surfaceReplicationWebSocketapplied at the four transport ingress boundaries —createWebSocket's return, the stored socket field,replicateOverWS's parameter, and the two test fault-injection helpers — so all 17 reads sit downstream of itcode/isHandledwstypes its'error'listener as(err: Error); the handler reads anerrnocode and sets Harper'sisHandledflagpprof-formatdeclares sample fields asnumber | bigint; the profiler does number arithmetic on themas number, the idiom the same function already uses on the neighbouring readsPromise.withResolverslib, so it got TypeScript's ES2022 default while pinned core code calls a Node 22 APIlib= that same default set plusES2024.Promise;targetis untouchedA unit test pins the
wsinternals the new type declares, and@types/wsbecomes a direct devDependency — production code importsws, but its types were arriving transitively through the dev-onlymqtt.For the human reviewer
The planning gate changed the design, and that is the thing most worth a second opinion. The obvious shape — a single
declare module 'ws'augmentation adding_socket— does not compile.@types/ws@8.18.1exports the class withexport =, soimport { WebSocket } from 'ws'resolves to the class instance type, which an interface augmentation cannot merge into; a minimal probe still reported TS2339 on every site. The planning review returnedFraming-Verdict: better-alternative-existsand I adopted its alternative: a replication-scoped structural refinement. It has a second benefit the augmentation lacked — Harper's strongernet.Socketassumption stays inside replication, where upstreamwsonly promises aDuplex.Open decisions, none of them forced by this change:
lib= default-equivalent, not minimal. Withtarget: ES2022and nolib, TypeScript loadslib.es2022.full.d.ts— exactly the six libs now listed. So this adds onlyES2024.Promiseto the surfacemainalready compiles against. A reviewer suggested trimming toES2022+ES2024.Promiseto drop the DOM globals; that is a project-wide type-surface change and should not ride on a build-exit-code PR._socket: Socket | null.wsinitialises it tonullpre-handshake, so the union is accurate, butstrict: falseerases it today — it guarantees nothing until someone enablesstrictNullChecks, at which point it surfaces the three pre-existing non-optional reads as the real questions they are.as numberrather thanNumber(...)in the profiler, because a coercion would change emitted JavaScript. The assertion keeps the (pre-existing) bigint path latent if a sample ever exceeds safe-integer range.wscontract test proves the library, not replication's use of it, and it is the only unit test in the suite that binds a socket. It exists because an exact version pin is still bumped by routine dependency PRs, and a reviewer of such a PR has no signal today that replication reads a private field.Three review findings I overruled rather than fixed, each on a fact rather than a preference:
DOMinlibpollutes the global namespace, so a forgottenimport { WebSocket } from 'ws'silently resolves to the browser global and drops TLS options." Withtarget: ES2022and nolib, TypeScript loadslib.es2022.full.d.ts, which is exactlyES2022+DOM+DOM.Iterable+DOM.AsyncIterable+WebWorker.ImportScripts+ScriptHost— the six now listed.maincompiles against those globals today; this diff adds onlyES2024.Promise.asassertions are incompatible with Node type-stripping and will throwSyntaxError."node v26.2.0runs a.tsfile containingas number[]andas numberwithout complaint,erasableSyntaxOnlyexists precisely to permit them, andanalytics/profile.ts:100,103already ships two.rocksdbBackup.ts:899TS2554 means the newlibdoes not deliver exit 0." Compiling withmain's owntsconfig.json— nolibat all — reproduces that diagnostic identically against the same installed tree. It is dependency-tree drift in the reviewer's checkout, not this change.No CI type-check gate is added, deliberately. Whether
mainshould be gated ontscexiting 0 is a separate team decision, and bundling it would make this change unreviewable. Note also thatbuild-tools/build-pro.shand the unit-test CI job still callnpm run build || true; only direct callers gain a reliable exit signal from this change.Verification
Not observable end-to-end by design — the change emits no different JavaScript, so the end-to-end route is the build itself plus a byte-comparison proving that.
npm run build→ exit 0 (was exit 2 with 31 errors onmain);npx tsc --noEmit→ 0 errors.dist/snapshot built frommain: 1033 files, identical file list, every non-map file byte-identical. Only the two touched sources'.js.mapdiffer, becausesourceMapis on and erasedas/annotation syntax shifts source columns.npm run test:unit→ 886 passing (882 onmain, plus the 4 newwscontract tests).npm run lint:requiredclean,prettier --checkclean on the changed files.Pre-existing and left alone:
getUserHitCountinanalytics/profile.tsaccumulatestotalHarperCountonce per harper stack frame (that branch has noreturn, unlike the user branch above it) whiletotalUserCountaccumulates once per sample, so thecpu-usage/harpermetric is inflated by stack depth. Found during this review, unrelated to the type change, and fixing it would be a runtime change.Complexity: easy
Review-Coverage: authored=claude; ran=codex,gemini; declined=cursor-grok,cursor-composer,domain; rounds=7 @ 1e4302c
Human-Review-Need: 4 @ 1e4302c