fix: swallow async opifex MQTT socket close rejection (node crash loop) - #10
Merged
Merged
Conversation
After the witness fixes (#8, #9) the node still crash-looped — but on a different, silent exit(1) during live Midnight event processing. Traced via a process.exit/unhandledRejection probe to: TypeError: Cannot close a writable stream that is closed or errored at close (@seriousme/opifex/dist/socket/socket.js:33) <- this.writer.close() at close (.../mqttConn/mqttConn.js:171) at #receive (.../mqttConn/mqttConn.js:131) <- MQTT client teardown -> unhandledRejection -> @effectstream/log process-handlers.ts:48 -> process.exit(1) opifex's socket close() wraps `this.writer.close()` in a synchronous try/catch ("swallow any errors on close"), but `WritableStreamDefaultWriter.close()` returns a Promise that rejects *asynchronously* when the stream is already closed. The sync try/catch can't catch it; under bun the floating rejection is fatal (deno tolerated it, which is why this only bites post-bun-migration). Patch opifex in patch.sh (same mechanism we already use for hardhat/fetch-blob) to add `.catch(() => {})` so the benign async close rejection is swallowed too. Co-Authored-By: Claude Opus 4.8 <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.
The real cause of the node crash loop after #8/#9 — and it's not game logic, witnesses, or the contract. It's the in-process MQTT engine.
Diagnosis
With both witnesses objects fixed, the node initialized cleanly but still
exit(1)'d ~12s in, silently (no stack). I instrumented it with aprocess.exit/unhandledRejectionprobe (bun--preload) and captured:opifex's
close()already tries to swallow close errors:…but
WritableStreamDefaultWriter.close()returns a Promise that rejects asynchronously when the stream is already closed/errored. A synchronoustry/catchcan't catch that, so the rejection floats up. Under bun, effectstream'sunhandledRejectionhandler turns it intoprocess.exit(1). (Under deno this was tolerated — which is why it only became fatal after the bun migration. The skip-ahead to realtime made MQTT teardowns frequent → constant crash loop.)Fix
In
patch.sh(the existing post-install node_modules patcher, already used for hardhat + fetch-blob), rewritethis.writer.close();→this.writer.close().catch(() => {});in opifex's socket. Completes the swallow the code already intended, for the async case. Idempotent; covers both bun-hoisted and npm-hoisted layouts.Validation
Root-caused from the live stack trace + source. I was not able to runtime-validate on the box (direct node_modules edits there are blocked by policy). After merge I'll pull on the box, re-run, and confirm the node stays up past the crash point and processes live events — and report back before declaring go-fish healthy.
🤖 Generated with Claude Code