Skip to content

Fix finished-promise recreation on animation completion - #82

Open
JonathonRP wants to merge 1 commit into
jonathonrp-fix-animation-replay-and-speedfrom
jonathonrp-fix-finished-promise-recreation
Open

Fix finished-promise recreation on animation completion#82
JonathonRP wants to merge 1 commit into
jonathonrp-fix-animation-replay-and-speedfrom
jonathonrp-fix-finished-promise-recreation

Conversation

@JonathonRP

Copy link
Copy Markdown
Owner

Stacked PR — base is jonathonrp-fix-animation-replay-and-speed (#76), not main. This depends on #76 and should be merged after it.

Ports the behavioural fix for upstream motiondivision/motion#3145 and motiondivision/motion#3144. No architectural refactor was attempted: upstream's #3148 rewrite (the WithPromise mixin, GroupAnimation/GroupAnimationWithThen split) is deliberately not included — the equivalent semantics are implemented inside the existing class structure.

The bug

MainThreadAnimation.teardown() resolved the finished promise and then immediately replaced it with a fresh, unresolved one. Anything that awaited or chained the controls after the animation had completed waited forever, because BaseAnimation.then read currentFinishedPromise — by then the new, never-resolved instance. GroupPlaybackControls.then compounded it by passing the member controls to Promise.all as raw one-shot thenables.

Changes

  • BaseAnimation — adds a finished getter returning the current finished promise; then() now reads it fresh (this.finished.then(...)).
  • MainThreadAnimationteardown() resolves the finished promise but no longer recreates it; cancel() no longer recreates it either, so a cancelled animation still resolves its awaiters. The promise is recreated only in play() when replaying a finished animation (added in Fix animation replay and playback speed correctness #76).
  • AcceleratedAnimationstop() resolves without recreating, since isStopped prevents the animation from ever being replayed. play() still recreates the promise for a finished WAAPI animation.
  • NativeAnimation — adds the same finished getter and routes then() through it. Its existing resolve/recreate semantics were already correct and were left alone.
  • GroupPlaybackControls — adds a finished getter that awaits each member's own finished promise, falling back to the member itself (a thenable) when it doesn't expose one, so third-party/legacy AnimationPlaybackControls implementations keep working. then() routes through it. An empty group (e.g. new GroupPlaybackControls([]) from animation/interfaces/motion-value.ts) still resolves immediately.
  • animation/types.tsAnimationPlaybackControls.finished added as optional so existing implementors don't break.

Regression tests

All new tests use a withTimeout helper (added to animators/__tests__/utils.ts) so a hang fails fast instead of stalling the suite.

MainThreadAnimation.spec.tsdescribe('finished promise'):

  1. await resolves once the animation completes
  2. a second await after natural completion still resolves (core bug)
  3. .then() fires after natural completion
  4. replaying a finished animation creates a new finished promise
  5. a cancelled animation resolves subsequent awaits
  6. a stopped animation resolves subsequent awaits

GroupPlaybackControls.spec.tsdescribe('finished promise'):
7. await resolves once all animations complete
8. a second await after natural completion still resolves
9. .then() fires after natural completion
10. an empty group resolves immediately
11. falls back to members that do not expose finished

Verified by git stash-ing only the source edits: 8 of these fail without the fix (the rest exercise paths that were already correct or the new API surface), and all pass with it.

Test results

  • bun run test:run (full suite): 129 files passed, 1 skipped — 644 tests passed, 1 skipped, no type errors
  • bun x vitest --run src/animation: 24 files, 218 tests passed, no type errors
  • No new biome lint violations in the touched files (remaining diagnostics are pre-existing: noThenProperty, useOptionalChain, CRLF format noise)
  • bun.lock is not included in this PR

teardown() replaced the just-resolved finished promise with a fresh
unresolved one, so awaiting or chaining an animation after it completed
would hang forever. The finished promise is now only recreated when a
finished animation is replayed via play().

- BaseAnimation/NativeAnimation: add a `finished` getter and read it
  fresh in then().
- MainThreadAnimation: teardown() resolves without recreating; cancel()
  no longer recreates either.
- AcceleratedAnimation: stop() resolves without recreating, since a
  stopped animation can never be replayed.
- GroupPlaybackControls: add a `finished` getter that awaits members'
  own finished promises, falling back to the member itself for controls
  that predate the getter. Empty groups still resolve immediately.

Fixes motiondivision/motion#3145 and motiondivision/motion#3144.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@changeset-bot

changeset-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fbc41ca

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
motion-start Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f161b4e0-1981-4d40-a077-17b95651301c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/motion-start@82

commit: fbc41ca

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