♻️ Extract util.callbackOrEmit() for synchronous error paths - #729
Open
alecgibson wants to merge 1 commit into
Open
♻️ Extract util.callbackOrEmit() for synchronous error paths#729alecgibson wants to merge 1 commit into
util.callbackOrEmit() for synchronous error paths#729alecgibson wants to merge 1 commit into
Conversation
At the moment, `Doc` branches on whether it was handed a callback in
seven separate places, and `Query` and `Backend` do it once each:
```js
if (callback) return callback(err);
return this.emit('error', err);
```
The idiom is already recognised elsewhere, but only as a private method
copy-pasted onto three classes: `Presence` and `LocalPresence` carry
byte-identical `_callbackOrEmit()` methods, and `MilestoneDB` has
`_callBackOrEmitError()`, which differs in capitalisation and drops the
falsy-error guard.
None of them was reusable here, because all three call back
asynchronously via `util.nextTick()`, and every site in `doc.js` calls
back synchronously. Adopting one verbatim would change when the error
callbacks on `doc.create()`, `doc.del()` and `doc.submitOp()` fire,
which is a breaking change for consumers rather than a refactor.
This change adds the synchronous helper to `util`, next to
`callEach()`, and uses it at the nine sites whose behaviour it leaves
untouched. It takes the emitter explicitly rather than relying on
`this`, so it also works inside the nested callbacks in `Doc#destroy()`
and `Backend#close()`.
Three sites in `doc.js` deliberately keep their branch.
`_emitResponseError()` interleaves `_emitNothingPending()` differently
between its branches, and calls back with no error on a silent
rejection. `_hardRollback()` and `_clearInflightOp()` use the array
form — `util.callEach()` plus a `called` boolean — which is the shape
of `Presence._callEachOrEmit()`, not this helper.
Note the shared helper is synchronous while the three private ones
aren't, despite the near-identical name. The tests pin that down, but
it's worth watching when the presence helpers are eventually folded in:
mechanically rewriting `this._callbackOrEmit(error, callback)` to
`util.callbackOrEmit(this, error, callback)` would quietly make every
presence callback synchronous.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
util.callbackOrEmit() currently drops the emit() return value in the no-callback error path, subtly changing observable return semantics vs the previous inlined pattern.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR introduces a new synchronous util.callbackOrEmit() helper to centralize the common “callback if provided, otherwise emit error” branching, and then refactors Doc, Query, and Backend to use it in synchronous error paths without changing callback timing.
Changes:
- Added
util.callbackOrEmit(emitter, error, callback)as a shared synchronous helper. - Replaced repeated callback-vs-emit branching in
lib/client/doc.js,lib/client/query.js, andlib/backend.js. - Added unit tests covering the helper’s sync callback and emit behavior.
File summaries
| File | Description |
|---|---|
| test/lib-util.js | Adds tests for util.callbackOrEmit() behavior (sync callback + emit/no-op cases). |
| lib/util.js | Adds the new callbackOrEmit helper near other util helpers. |
| lib/client/query.js | Refactors _finishResponse error path to use util.callbackOrEmit. |
| lib/client/doc.js | Refactors several synchronous error branches to use util.callbackOrEmit. |
| lib/backend.js | Refactors Backend#close() error handling to use util.callbackOrEmit. |
Review details
- Files reviewed: 5/5 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
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.
At the moment,
Docbranches on whether it was handed a callback in seven separate places, andQueryandBackenddo it once each:The idiom is already recognised elsewhere, but only as a private method copy-pasted onto three classes:
PresenceandLocalPresencecarry byte-identical_callbackOrEmit()methods, andMilestoneDBhas_callBackOrEmitError(), which differs in capitalisation and drops the falsy-error guard.None of them was reusable here, because all three call back asynchronously via
util.nextTick(), and every site indoc.jscalls back synchronously. Adopting one verbatim would change when the error callbacks ondoc.create(),doc.del()anddoc.submitOp()fire, which is a breaking change for consumers rather than a refactor.This change adds the synchronous helper to
util, next tocallEach(), and uses it at the nine sites whose behaviour it leaves untouched. It takes the emitter explicitly rather than relying onthis, so it also works inside the nested callbacks inDoc#destroy()andBackend#close().Three sites in
doc.jsdeliberately keep their branch._emitResponseError()interleaves_emitNothingPending()differently between its branches, and calls back with no error on a silent rejection._hardRollback()and_clearInflightOp()use the array form —util.callEach()plus acalledboolean — which is the shape ofPresence._callEachOrEmit(), not this helper.Note the shared helper is synchronous while the three private ones aren't, despite the near-identical name. The tests pin that down, but it's worth watching when the presence helpers are eventually folded in: mechanically rewriting
this._callbackOrEmit(error, callback)toutil.callbackOrEmit(this, error, callback)would quietly make every presence callback synchronous.🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com