Skip to content

fix(runner): check the polling deadline after the attempt, not instead of it - #45

Open
monikon22 wants to merge 2 commits into
Drownek:masterfrom
monikon22:pr/0-poll-deadline
Open

fix(runner): check the polling deadline after the attempt, not instead of it#45
monikon22 wants to merge 2 commits into
Drownek:masterfrom
monikon22:pr/0-poll-deadline

Conversation

@monikon22

@monikon22 monikon22 commented Aug 21, 2026

Copy link
Copy Markdown

This one stands alone. It fixes a bug that is already on master, it does not depend on
anything else, and it can be merged on its own.

It came out of a larger piece of work, proposed separately in #46 — that issue needs a
decision from you, this PR does not.

The bug

Every polling helper is shaped like this:

while (Date.now() < deadline) {
    const result = await fn();
    if (result !== undefined) return result;
    await sleep(interval, signal);
}
throw new Error(`Timeout: ...`);

The loop can exit on the clock without ever looking at what arrived during the last sleep. The
value is sitting in the buffer, the wait reports a timeout, and the log reads as though the
confirmation arrived and the wait ignored it.

That final window is not the 50 ms it looks like. A bot loading chunks blocks the event loop for
seconds; the socket data flushes in one batch when it unblocks, and the sleep timer that was
already pending resolves past the deadline. Under that stall an AuthMe login test failed with
did not confirm registration in time on a run whose own log shows Successfully registered!
five messages above the failure.

The change

The deadline is now checked after the attempt, so the last thing each helper does before
giving up is look one more time. Same one-line omission in five places:

  • runner-package/lib/utils.tspoll, waitForAssertion, waitUntil
  • runner-package/lib/matchers.tspollAssertion, PollMatchers.pollUntilPass

waitForStable already had the right order and is untouched. Its second loop deliberately checks
the clock first, since "stay true for the whole duration" should stop at the end of the window.

One deliberate consequence: a helper called with a timeout of zero now makes one attempt rather
than none. That is what "poll until the deadline" should have meant.

The second commit is unrelated but tiny, and in the same corner. _registerPersistentListeners
captured username into a local before the handshake finished, so bot chat lines could print
[Bot undefined] — including every line a login wall produces, which are the ones worth reading
when authentication misbehaves. _captureSpawnPromise already reads the name lazily for exactly
this reason; the three log calls now do the same.

On evidence

I want to be straight about this: the failure is a race, and I could not make it fail on demand.
The argument is the log ordering — at the instant poll threw, the matching message was already
in the buffer, and the path from that throw to the printed failure crosses only microtasks — plus
an isolated repro of the old and new poll over the same input, where the old one throws and the
new one resolves. The suite passes before and after.

…d of it

Every polling helper was shaped `while (Date.now() < deadline) { attempt; sleep }`,
so the loop could exit on the clock without ever looking at what arrived during
the last sleep. The value is in the buffer, the wait reports a timeout, and the
log reads "the confirmation arrived and it timed out anyway".

The window is not the 50ms it looks like. A bot loading chunks blocks the event
loop for seconds; the socket data flushes in one batch when it unblocks, and the
sleep timer that was already pending resolves past the deadline. Under that stall
the AuthMe preflight failed with `did not confirm registration in time` on a run
whose log shows `Successfully registered!` five messages before the failure was
printed.

The loops now check the deadline after the attempt, so the last thing each does
before giving up is look one more time. `poll`, `waitForAssertion` and `waitUntil`
in utils, `pollAssertion` and `pollUntilPass` in matchers — the same omission in
all five. `waitForStable` already had the right order and is untouched.

One deliberate consequence: a helper called with a timeout of zero now makes one
attempt rather than none, which is what "poll until the deadline" should have
meant.
`_registerPersistentListeners` captured `username` into a local and printed that
local on every chat line. `username` is `bot.username`, which mineflayer leaves
undefined until the client is through the handshake, so the listeners could go up
holding nothing and every message afterwards read "[Bot undefined]".

Those are the lines worth reading when a login wall misbehaves, so it cost
exactly where it hurt. `_captureSpawnPromise` already reads the name lazily for
this reason; these three log calls now do the same.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant