-
-
Notifications
You must be signed in to change notification settings - Fork 209
fix: emit maxAttemptsFailed instead of unhandled rejection #1149
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
BetterAndBetterII
wants to merge
1
commit into
ueberdosis:main
Choose a base branch
from
BetterAndBetterII:fix/uncaught-max-attempts
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { HocuspocusProviderWebsocket } from "@hocuspocus/provider"; | ||
| import test from "ava"; | ||
| import { sleep } from "../utils/index.ts"; | ||
|
|
||
| const exhaustedRetryOptions = { | ||
| url: "ws://127.0.0.1:1", | ||
| maxAttempts: 1, | ||
| delay: 1, | ||
| minDelay: 1, | ||
| initialDelay: 0, | ||
| jitter: false, | ||
| }; | ||
|
|
||
| test("does not produce an unhandled rejection when maxAttempts are exhausted", async (t) => { | ||
| const unhandled: unknown[] = []; | ||
| const onUnhandledRejection = (reason: unknown) => { | ||
| unhandled.push(reason); | ||
| }; | ||
| process.on("unhandledRejection", onUnhandledRejection); | ||
| t.teardown(() => { | ||
| process.off("unhandledRejection", onUnhandledRejection); | ||
| }); | ||
|
|
||
| const ws = new HocuspocusProviderWebsocket(exhaustedRetryOptions); | ||
| t.teardown(() => ws.destroy()); | ||
|
|
||
| await sleep(300); | ||
| await new Promise((resolve) => setImmediate(resolve)); | ||
| await new Promise((resolve) => setImmediate(resolve)); | ||
|
|
||
| t.is(unhandled.length, 0); | ||
| }); | ||
|
|
||
| test("onMaxAttemptsFailed is executed when maxAttempts are exhausted", async (t) => { | ||
| await new Promise((resolve, reject) => { | ||
| const timeout = setTimeout(() => { | ||
| reject(new Error("onMaxAttemptsFailed was not called")); | ||
| }, 2000); | ||
|
|
||
| const ws = new HocuspocusProviderWebsocket({ | ||
| ...exhaustedRetryOptions, | ||
| onMaxAttemptsFailed({ error }) { | ||
| clearTimeout(timeout); | ||
| t.truthy(error); | ||
| ws.destroy(); | ||
| resolve("done"); | ||
| }, | ||
| }); | ||
| t.teardown(() => ws.destroy()); | ||
| }); | ||
| }); | ||
|
|
||
| test("on('maxAttemptsFailed') is executed when maxAttempts are exhausted", async (t) => { | ||
| await new Promise((resolve, reject) => { | ||
| const timeout = setTimeout(() => { | ||
| reject(new Error("maxAttemptsFailed was not emitted")); | ||
| }, 2000); | ||
|
|
||
| const ws = new HocuspocusProviderWebsocket({ | ||
| ...exhaustedRetryOptions, | ||
| autoConnect: false, | ||
| }); | ||
| t.teardown(() => ws.destroy()); | ||
|
|
||
| ws.on("maxAttemptsFailed", ({ error }) => { | ||
| clearTimeout(timeout); | ||
| t.truthy(error); | ||
| ws.destroy(); | ||
| resolve("done"); | ||
| }); | ||
|
|
||
| ws.connect(); | ||
| }); | ||
| }); |
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
Repository: ueberdosis/hocuspocus
Length of output: 15421
🏁 Script executed:
Repository: ueberdosis/hocuspocus
Length of output: 16302
🌐 Web query:
@lifeomic/attempt 3.1.0 retry implementation final callback rejection undefined error exhaustion source💡 Result:
In
@lifeomic/attempt3.1.0, the issue of "final callback rejection undefined error" often relates to how errors or timeouts are handled during the final retry attempt [1][2][3]. 1. Library Behavior: When the maximum number of attempts is reached, the library throws the last error encountered [1][4]. If a timeout occurs, the library throws an error with the code ATTEMPT_TIMEOUT [1][2]. 2. Synchronous/Asynchronous Handling: Historically, issues were reported where synchronous errors thrown within custom handlers (like handleTimeout) could result in unhandled promise rejections or unexpected behavior [5]. These issues were largely addressed in versions prior to 3.1.0 (e.g., version 3.0.1 fixed synchronous errors in handleTimeout, and 3.0.3 addressed error handling issues with the timeout option) [6][7][5]. 3. Potential Cause: If you are seeing an undefined error at the final stage, ensure that: - Your handleError or handleTimeout functions are returning a Promise or handling errors internally if they are designed to suppress them [1][4]. If these functions finish without returning a value or throwing, the library might propagate the original error [1][4]. - If you are manually triggering rejections, verify the error object is correctly populated. An undefined error may occur if a custom handler resolves with undefined instead of throwing a specific error or re-throwing the original one [1][8]. For debugging, review your handleError and handleTimeout implementations to ensure they explicitly return a Promise (if asynchronous) and correctly manage the control flow, as these are the primary injection points for custom retry logic [1][4]. Users have also noted that documentation regarding whether throwing in these handlers automatically triggers an abort can be ambiguous, suggesting that explicit calls to context.abort are safer if you intend to stop retries immediately [9][10].Citations:
🌐 Web query:
site:github.com/lifeomic/attempt/blob@lifeomic/attempt3.1.0 src/index.ts maxAttempts lastError reject undefined💡 Result:
In the
@lifeomic/attemptlibrary, the retry function is designed to execute an operation multiple times until it either succeeds or the maximum number of attempts (defined by maxAttempts, which defaults to 3) is reached [1][2]. If the maximum number of attempts is exceeded, the retry function will reject the promise with the last error that was encountered [1][2]. If the error was caused by a timeout, the rejected error will contain a code property set to the string ATTEMPT_TIMEOUT [1][2]. Regarding your query about "lastError" and "reject undefined": 1. The library tracks the state of the operation internally. If all attempts fail, the final error thrown to the caller is indeed the last error recorded [1][2]. 2. While the documentation does not explicitly detail an "undefined" rejection scenario for the last error, if an operation resolves to undefined (or any other value) instead of rejecting, the retry logic treats this as a successful completion [1][2]. 3. If you encounter a situation where the library appears to reject with undefined, it is likely due to the implementation of the operation you are passing to retry, as the library itself is structured to propagate the error returned by the failed operation [1]. If you need to prevent further attempts based on a specific error, you should use the abort function available on the context object within the handleError callback [1][2]. This allows you to stop the retry process immediately if the error indicates that retrying would be futile [1][2].Citations:
🏁 Script executed:
Repository: ueberdosis/hocuspocus
Length of output: 6187
Emit
maxAttemptsFailedwhen a socket closes without an error.When
onClose()rejects the active connection promise without a reason,@lifeomic/attemptpropagatesundefinedafter finite retries are exhausted. Theerror &&guard then skips the event. Useerror?.code !== "ATTEMPT_ABORTED"and add a close-without-error test.🤖 Prompt for AI Agents