Skip to content

Breakout Task keeps running and can report completion twice after it finishes #1641

Open

Description

@joshunrau

The Breakout Task keeps running after it reports completion. runTask never stops the requestAnimationFrame loop and never removes its keydown, keyup and mousemove listeners from document, so the game keeps redrawing and responding to input after it calls onComplete. Production mostly hides this, because ScalarInstrumentRenderer replaces the iframe with the summary once onSubmit resolves, and that destroys the iframe's document. Until then, or for good if the submit fails, the game is still live. After a loss, the next frame takes another life, so the canvas shows Lives: -1 and serves the ball again. After a win, the ball keeps bouncing among the empty brick rows. If it then gets past the paddle three times, onComplete fires a second time with livesRemaining: 0, and InteractiveContent submits each done event it receives.

Where

packages/instrument-library/src/interactive/DNP_BREAKOUT_TASK/index.ts:

  • :111-116 and :188-192: onComplete(...) is called, and draw carries on.
  • :211: requestAnimationFrame(draw) is scheduled unconditionally at the end of every frame.
  • :214-216: the listeners are added and never removed.

Reproduce

  1. Start the Breakout Task and let the ball past the paddle three times.
  2. Watch the canvas after done has fired, before the summary replaces it. If you run the instrument outside the renderer, keep watching indefinitely.

Actual: the ball is served again and the lives counter goes to Lives: -1. The arrow keys and mouse still move the paddle.
Expected: the game freezes on the final frame once it reports completion, and done is called exactly once.

Tests

packages/instrument-library/src/__tests__/DNP_BREAKOUT_TASK.test.ts. Its afterEach removes every listener recorded through addDocumentListener, which works around the leak. When fixing:

  • add a test asserting that no further frame is requested after done is called (nextFrame stays undefined);
  • add a test asserting that a key press after completion does not move the paddle;
  • once the source removes its own listeners, drop the cleanup loop from afterEach.

Suggested fix

Have a finished flag, or a single complete(data) helper, that removes the three listeners, stops scheduling requestAnimationFrame (or calls cancelAnimationFrame), and then calls onComplete once.

Found while raising unit test coverage in #1632.

Activity

  1. added
    BugType: existing behavior is wrong
    Priority: LowNice to have, cosmetic or speculative
    Difficulty: LowIsolated change, about 2 hours or less
    Area: InstrumentsInstrument authoring API, bundling, runtime, rendering, measures and the built-in library
    on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Area: InstrumentsInstrument authoring API, bundling, runtime, rendering, measures and the built-in libraryBugType: existing behavior is wrongDifficulty: LowIsolated change, about 2 hours or lessPriority: LowNice to have, cosmetic or speculative

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions