Skip to content

Move seekTarget into machine context (single source of truth) - #29

Merged
switz merged 5 commits into
masterfrom
refactor/seek-target-in-context
Sep 20, 2026
Merged

switz merged 5 commits into
masterfrom
refactor/seek-target-in-context

Conversation

@switz

@switz switz commented Sep 19, 2026

Copy link
Copy Markdown
Member

Summary

Eliminates the dual-authority problem identified by Fable's architectural review: the track machine owned lifecycle but Track owned timing truth (pausedAtTrackTime), causing a class of bugs where seek state could be dropped, erased, or reported stale.

  • seekTarget in TrackContext replaces pausedAtTrackTime as the single source of truth for "where should playback resume"
  • SEEK in idle/loading is now just assign({ seekTarget }) — no HTML5 side effects, no isWebAudioOnly guard, no stale loadedmetadata listeners
  • ACTIVATE/DEACTIVATE atomically reset seekTarget alongside other timing state
  • activateAndPlayCurrent split into activateCurrent + playCurrent in the queue machine so GOTO does activate → seek → play in correct order
  • freezePausedTime is now a provided assign that writes directly into context
  • Net -58 lines — the refactor is simpler than what it replaced

Supersedes #28 (closed). Addresses all findings from both Codex and Fable reviews.

Test plan

  • gotoTrack(n, true, 142.6) starts playback at 142.6s (the original bug)
  • Seek while paused on an idle track, then play — resumes from seeked position
  • Normal play/pause/seek/resume cycle unaffected (no backward jumps)
  • WEBAUDIO_ONLY mode: seek before decode completes, then play — starts at correct position
  • Gapless transitions still work correctly
  • HTML5 → WebAudio crossover preserves position
  • All 349 existing tests pass

🤖 Generated with Claude Code

The track machine and Track class had a dual-authority problem:
the machine owned lifecycle (states, transitions) but Track owned
timing truth (pausedAtTrackTime). Neither was the single source of
truth, causing bugs where SEEK events were dropped in idle/loading,
activation erased pending seeks, and the currentTime getter reported
stale values.

This refactor:

- Adds seekTarget to TrackContext as the single owner of "where
  should playback resume"
- SEEK in idle/loading is now just assign({ seekTarget }) — no HTML5
  side effects, no playback-method guards needed
- ACTIVATE/DEACTIVATE atomically reset seekTarget alongside other
  timing state via clearSeekTarget
- freezePausedTime (webaudio pause) is now an assign that writes
  seekTarget directly into context
- Splits activateAndPlayCurrent in the queue machine into
  activateCurrent + playCurrent so GOTO does activate → seek → play
  in the correct order (activate no longer erases the seek)
- _playHtml5 reconciles seekTarget with audio.currentTime at play
  time, handling seek-before-load without needing idle-state side
  effects
- Mid-stream crossover writes to _crossoverComputedOffset then
  syncs via syncSeekTargetFromCrossover assign action

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Deploying gapless with  Cloudflare Pages  Cloudflare Pages

Latest commit: a946a30
Status: ✅  Deploy successful!
Preview URL: https://6ffd7f2b.gapless-e1l.pages.dev
Branch Preview URL: https://refactor-seek-target-in-cont.gapless-e1l.pages.dev

View logs

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-20T05:36:31.975554Z a946a30 Manual request
🔒 Security Review ✅ Completed 2026-09-19T19:42:16.238683Z d158d08 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d158d0870f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Track.ts Outdated
Comment on lines +543 to +546
const target = seekTarget;
this.audio.addEventListener('loadedmetadata', () => {
this.audio.currentTime = target;
}, { once: true });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Invalidate delayed seeks when resetting a track

When metadata is still loading, this listener closes over the current seekTarget but survives DEACTIVATE/ACTIVATE. If a user starts a track at a nonzero offset, skips away, and returns before loadedmetadata fires, activation resets the track to zero and starts it normally, but the stale listener subsequently jumps the active playback back to the old offset. Remove or invalidate pending metadata handlers when resetting the track, or have the handler verify the current machine context before applying its captured target.

Useful? React with 👍 / 👎.

When a track is reset (ACTIVATE/DEACTIVATE), abort the seek signal so
any pending loadedmetadata listener from a prior seek is removed rather
than firing later and jumping playback to a stale position.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@switz

switz commented Sep 19, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 08665af7a1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Track.ts Outdated
private _playHtml5(seekTarget: number): void {
if (this.audio.preload !== 'auto') this.audio.preload = 'auto';
this.audio.playbackRate = this.queueRef.playbackRate;
if (isFinite(seekTarget) && seekTarget > this.audio.currentTime + 0.5) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply sub-half-second HTML5 start offsets

When gotoTrack(index, true, startTime) selects an unloaded track with a valid startTime of 0.5 seconds or less, activation resets audio.currentTime to zero and this tolerance check prevents the requested offset from being applied, so HTML5 playback starts at the beginning instead. Apply a newly assigned seek target regardless of this 0.5-second difference, or separately track whether the target is pending.

Useful? React with 👍 / 👎.

seekTarget is only non-zero when explicitly set, so we can use a tight
threshold (0.01s) instead of 0.5s. This ensures small start offsets
like gotoTrack(n, true, 0.3) are applied correctly.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@switz

switz commented Sep 19, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c57c9d0723

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Track.ts
Comment on lines +543 to +545
if (seekTarget > 0 && Math.abs(this.audio.currentTime - seekTarget) > 0.01) {
if (this.audio.readyState >= HTMLMediaElement.HAVE_METADATA) {
this.audio.currentTime = seekTarget;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid reapplying stale HTML5 seek targets on resume

When playback remains on HTML5 (especially in HTML5_ONLY, or while hybrid decoding is pending), seekTarget retains the last requested nonzero seek while audio.currentTime continues advancing. After a user seeks to 60 seconds, listens until 90 seconds, pauses, and resumes, this condition resets currentTime to 60 because the HTML5 PAUSE handler never refreshes seekTarget. Track the paused HTML5 position or only apply the target while an initial seek is pending so resume continues from the actual pause point.

Useful? React with 👍 / 👎.

Without this, seekTarget retained the last explicit seek position
while audio.currentTime advanced during playback. On resume,
_playHtml5 would jump back to the stale seekTarget.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@switz

switz commented Sep 19, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: becb48795e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

on: {
PAUSE: {
actions: ['clearIsPlaying', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],
actions: ['clearIsPlaying', 'freezePausedTime', 'pauseHtml5', 'stopProgressLoop', 'reportProgress'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve pending seeks when pausing before metadata

When HTML5 playback is paused before loadedmetadata applies a requested nonzero seek, audio.currentTime is still typically 0, so freezePausedTime overwrites the pending seekTarget with 0. In HYBRID mode, if Web Audio decoding then completes before the metadata listener fires, the BUFFER_READY crossover uses that erased target and starts the decoded source from the beginning. Preserve the existing target while an HTML5 seek is still pending, and only freeze audio.currentTime after the seek has taken effect.

Useful? React with 👍 / 👎.

freezePausedTime now takes the max of the live position and the
existing seekTarget. This prevents a pause before loadedmetadata
from erasing a pending seek — the target is preserved until the
audio element can actually apply it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@switz

switz commented Sep 20, 2026

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Bravo.

Reviewed commit: a946a3033a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@switz
switz merged commit 87490e5 into master Sep 20, 2026
2 checks passed
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