Five faults found while auditing the play/pause and position path for a desktop report of the pause icon showing over a seek bar that was not moving. They are one commit because they are one file's worth of tangled state, and two of them do not compile apart. The finished callback did not know which chain it came from. It is dispatched as a goroutine from the beep callback and then queues for p.mu, so a user pressing Next in the last second of a track had it wake up holding the lock for a player that had loaded something else -- and rewind it, stop it, and hand a stale finish to the queue's auto-advance. updateStreamers now stamps a chainID and the callback carries the one it was registered with. (#123) It also emitted PlaybackFinished and PlaybackStateChanged(stopped) *after* releasing p.mu, alone in this file, so a Play() taking the lock in that gap emitted `playing` first and the stale `stopped` landed last -- the button showing play over a track that was audibly running. Both emits are back under the lock. (#123) A source that failed mid-track was reported to the queue as a natural end, so a broken file auto-advanced in silence and was counted as played. The handler takes the reason now: the player cannot name the track, because the metadata is the queue's, so the queue emits PlaybackFailed and skips recording the play. (#123) p.format was assigned once, in the constructor, to the *speaker's* rate, and never again -- so it claimed 44.1 kHz for every file. The replay-after-finish path resamples from it, meaning a finished track played a second time was resampled from a rate the decoder never produced: audibly wrong speed and pitch, and the length and position fallbacks wrong with it. The fixtures are 22050 Hz, which is what lets a test see this at all. (#124) p.trackLengthMs was written only when the database had a row and cleared only by UnloadTrack, so a file with no row inherited the previous track's duration -- and every position report is scaled by it, so the bar reported one track's progress on another's scale. (#125) Queue.OnPlaybackFinished indexed q.tracks[currentIndex] having checked only that the queue was non-empty. currentIndex is -1 whenever the queue has been exhausted, and onQueueExhausted deliberately leaves the finished track loaded -- so playing it from there and letting it end panicked, on a goroutine with no caller to recover it. (#126) The position readers guarded the decoder with the speaker lock, which the read-ahead goroutine has no reason to hold and never takes -- so Position() raced readAhead's Stream() on every position emit, once a second for the whole of playback. srcMu is the lock that excludes that goroutine, and taking it naively deadlocks, because seekLocked already holds it and then emits the landing position from inside that region. seekSourceLocked is that region extracted, so the lock is released before anything is emitted. Found by the race detector, via the test added here for the chain guard: the existing suite never loads a file outside the integration guard, so make test was green over it. (#127) OnPlaybackFinished picks up //wails:ignore along with its error parameter: v3's generator segfaults on a bound method taking an error, and this was never IPC. That removes a binding the frontend could have called to force an auto-advance. Closes #123 Closes #124 Closes #125 Closes #126 Closes #127
YellowJacket Frontend
This directory contains the frontend for YellowJacket.
Dependencies
There are a couple of tools that are required to build and use the frontend.
vitefor- transpiling typescript
- bundling the final "package" that is useb by the webview
- running a dev server with hot-reloading
- configured with the
vite.config.tsfile in this directory
pnpmfor managing frontend dependency packages
There are a handful of dependency packages that we use directly in the frontend.
picocssfor basic CSS while developinglitfor a simple but powerful wrapper around Web Components
Lastly, there are some dependencies that only benefit development
tsservercomes bundled with vscode and can be used as an LSP with other editors usingtypescript-language-server- Used for autocomplete, syntax highlighting, etc.
- This can be configured with the
tsconfig.jsonfile in the root of the project- NOTE: your configuration should align with the
viteconfig so you get in-editor feedback that aligns with how the build will be done.
- NOTE: your configuration should align with the
Development
Ideally, you would use the frontend in the wails app as the frontend depends on the Go bindings generated by Wails.
You can do this by running wails dev in the root of the YellowJacket repo.
If you want to run the frontend standalone, make sure the Wails go bindings have been generated with wails generate modules.
Then, you can run pnpm dev to run the vite dev server.
For more information on what commands are available, refer to package.json.
Code
Web Components
Our frontend is based off of Web Components, a standard that provides native browser encapsulation of components that can include HTML, CSS and Javascript all together.
Instead of writing these components manually in Javascript, we utilize lit as a wrapper library.
Typescript
To better integrate with our tooling and to provide a better developer experience with strong typing, we have written all functional frontend code in Typescript. Vite serves as our Typescript transpiler.
Important Files and Directories
NOTE: most of these directories have aliases defined in tsconfig.json and vite.config.ts so that we may refer to them by shorthand when importing.
index.htmlandindex.jsis the entrypoint for the frontend. The first page that loads.wailsjs/goGo code bindings generated by wails reside herewailsjs/runtimethe Wails runtime code needed to use Wails featuressrcall app code resides heresrc/assetsstatic assets like fonts, images and iconssrc/componentslit components that are used to compose the applicationsrc/pagespages that serve as other entrypoints for the application that can be navigated to