perf(07-01): add incremental persistence helpers for queue mutations
- Add persistAddTrack/persistAddTracks for O(1) append operations - Add persistInsertTracks with variable-N position shift for insert-at operations - Add persistRemoveTrack with single DELETE + position shift - Wire AddTrack/AddTracks to use incremental INSERT (no full table rewrite) - Wire InsertNext/InsertNextTracks/InsertTracksAt to use position shift + INSERT - Wire RemoveTrack to use single DELETE + shift (no full table rewrite) - RemoveTracks keeps full persistTracks rewrite (bulk operation per design) - Preserve shuffle order regeneration in all mutation paths
This commit is contained in:
@@ -10,6 +10,187 @@ import (
|
||||
"yellowjacket/backend/profiling"
|
||||
)
|
||||
|
||||
// persistAddTrack inserts a single track at the end of the queue.
|
||||
// No position shifting is needed because this is always an append.
|
||||
// The caller must hold q.mu.
|
||||
func (q *Queue) persistAddTrack(track Track) {
|
||||
_, err := q.db.Queries.InsertQueueTrack(q.db.Ctx, sqlcgen.InsertQueueTrackParams{
|
||||
AudioFileID: track.AudioFileID,
|
||||
Position: track.Position,
|
||||
})
|
||||
if err != nil {
|
||||
q.logger.Error("Failed to persist added track", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// persistAddTracks inserts multiple tracks at the end of the queue
|
||||
// atomically in a transaction. No position shifting is needed because
|
||||
// these are always appends.
|
||||
// The caller must hold q.mu.
|
||||
func (q *Queue) persistAddTracks(tracks []Track) {
|
||||
if len(tracks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := q.db.BeginTx()
|
||||
if err != nil {
|
||||
q.logger.Error("Failed to begin transaction", "err", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
committed := false
|
||||
|
||||
defer func() {
|
||||
if !committed {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
q.logger.Error(
|
||||
"Failed to rollback transaction",
|
||||
"err", rbErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
txQueries := q.db.Queries.WithTx(tx)
|
||||
|
||||
for _, track := range tracks {
|
||||
_, insertErr := txQueries.InsertQueueTrack(q.db.Ctx, sqlcgen.InsertQueueTrackParams{
|
||||
AudioFileID: track.AudioFileID,
|
||||
Position: track.Position,
|
||||
})
|
||||
if insertErr != nil {
|
||||
q.logger.Error("Failed to insert track", "err", insertErr)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
q.logger.Error("Failed to commit transaction", "err", commitErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
committed = true
|
||||
}
|
||||
|
||||
// persistInsertTracks inserts multiple tracks at a given position,
|
||||
// shifting existing tracks to make room. Uses a transaction for atomicity.
|
||||
// The caller must hold q.mu.
|
||||
func (q *Queue) persistInsertTracks(tracks []Track, insertPos int) {
|
||||
if len(tracks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
tx, err := q.db.BeginTx()
|
||||
if err != nil {
|
||||
q.logger.Error("Failed to begin transaction", "err", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
committed := false
|
||||
|
||||
defer func() {
|
||||
if !committed {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
q.logger.Error(
|
||||
"Failed to rollback transaction",
|
||||
"err", rbErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// SAFETY: Multi-row position shift by variable N unsupported by sqlc
|
||||
// (ShiftQueuePositionsUp only shifts by 1). Bind variables match args;
|
||||
// no string interpolation.
|
||||
_, err = tx.ExecContext(
|
||||
q.db.Ctx,
|
||||
"UPDATE queue_tracks SET position = position + ? WHERE position >= ?",
|
||||
len(tracks), insertPos,
|
||||
)
|
||||
if err != nil {
|
||||
q.logger.Error("Failed to shift positions up", "err", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
txQueries := q.db.Queries.WithTx(tx)
|
||||
|
||||
for i, track := range tracks {
|
||||
_, insertErr := txQueries.InsertQueueTrack(q.db.Ctx, sqlcgen.InsertQueueTrackParams{
|
||||
AudioFileID: track.AudioFileID,
|
||||
Position: int64(insertPos + i),
|
||||
})
|
||||
if insertErr != nil {
|
||||
q.logger.Error("Failed to insert track", "err", insertErr)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
q.logger.Error("Failed to commit transaction", "err", commitErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
committed = true
|
||||
}
|
||||
|
||||
// persistRemoveTrack deletes a single track at the given position and
|
||||
// shifts subsequent positions down to close the gap.
|
||||
// The caller must hold q.mu.
|
||||
func (q *Queue) persistRemoveTrack(position int) {
|
||||
tx, err := q.db.BeginTx()
|
||||
if err != nil {
|
||||
q.logger.Error("Failed to begin transaction", "err", err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
committed := false
|
||||
|
||||
defer func() {
|
||||
if !committed {
|
||||
if rbErr := tx.Rollback(); rbErr != nil {
|
||||
q.logger.Error(
|
||||
"Failed to rollback transaction",
|
||||
"err", rbErr,
|
||||
)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
txQueries := q.db.Queries.WithTx(tx)
|
||||
|
||||
if removeErr := txQueries.RemoveQueueTrackByPosition(
|
||||
q.db.Ctx, int64(position),
|
||||
); removeErr != nil {
|
||||
q.logger.Error("Failed to remove track by position", "err", removeErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if shiftErr := txQueries.ShiftQueuePositionsDown(
|
||||
q.db.Ctx, int64(position),
|
||||
); shiftErr != nil {
|
||||
q.logger.Error("Failed to shift positions down", "err", shiftErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if commitErr := tx.Commit(); commitErr != nil {
|
||||
q.logger.Error("Failed to commit transaction", "err", commitErr)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
committed = true
|
||||
}
|
||||
|
||||
// lookupTrackMetaBatch fetches audio file IDs and metadata for a batch of
|
||||
// file paths using a single query per chunk (instead of 2 queries per track).
|
||||
// Returns a map keyed by file path. This is safe to call without holding q.mu.
|
||||
|
||||
+52
-7
@@ -343,7 +343,12 @@ func (q *Queue) AddTrack(filePath string) {
|
||||
q.loadCurrentTrack()
|
||||
}
|
||||
|
||||
q.commitMutation(false)
|
||||
if q.shuffleMode {
|
||||
q.generateShuffleOrder()
|
||||
}
|
||||
|
||||
q.persistAddTrack(track)
|
||||
q.persistState()
|
||||
q.emitTracksModified(
|
||||
"add",
|
||||
[]Track{track},
|
||||
@@ -388,7 +393,12 @@ func (q *Queue) AddTracks(filePaths []string) {
|
||||
q.loadCurrentTrack()
|
||||
}
|
||||
|
||||
q.commitMutation(false)
|
||||
if q.shuffleMode {
|
||||
q.generateShuffleOrder()
|
||||
}
|
||||
|
||||
q.persistAddTracks(newTracks)
|
||||
q.persistState()
|
||||
q.emitTracksModified(
|
||||
"add",
|
||||
newTracks,
|
||||
@@ -439,7 +449,14 @@ func (q *Queue) InsertNextTracks(filePaths []string) {
|
||||
q.loadCurrentTrack()
|
||||
}
|
||||
|
||||
q.commitMutation(true)
|
||||
q.reindexPositions()
|
||||
|
||||
if q.shuffleMode {
|
||||
q.generateShuffleOrder()
|
||||
}
|
||||
|
||||
q.persistInsertTracks(newTracks, insertPos)
|
||||
q.persistState()
|
||||
q.emitTracksModified(
|
||||
"insert",
|
||||
newTracks,
|
||||
@@ -482,7 +499,14 @@ func (q *Queue) InsertNext(filePath string) {
|
||||
q.loadCurrentTrack()
|
||||
}
|
||||
|
||||
q.commitMutation(true)
|
||||
q.reindexPositions()
|
||||
|
||||
if q.shuffleMode {
|
||||
q.generateShuffleOrder()
|
||||
}
|
||||
|
||||
q.persistInsertTracks([]Track{track}, insertPos)
|
||||
q.persistState()
|
||||
q.emitTracksModified(
|
||||
"insert",
|
||||
[]Track{track},
|
||||
@@ -542,7 +566,14 @@ func (q *Queue) InsertTracksAt(filePaths []string, index int) {
|
||||
q.loadCurrentTrack()
|
||||
}
|
||||
|
||||
q.commitMutation(true)
|
||||
q.reindexPositions()
|
||||
|
||||
if q.shuffleMode {
|
||||
q.generateShuffleOrder()
|
||||
}
|
||||
|
||||
q.persistInsertTracks(newTracks, index)
|
||||
q.persistState()
|
||||
q.emitTracksModified(
|
||||
"insert",
|
||||
newTracks,
|
||||
@@ -722,7 +753,14 @@ func (q *Queue) RemoveTrack(position int) {
|
||||
q.currentIndex = len(q.tracks) - 1
|
||||
}
|
||||
|
||||
q.commitMutation(true)
|
||||
q.persistRemoveTrack(position)
|
||||
q.reindexPositions()
|
||||
|
||||
if q.shuffleMode {
|
||||
q.generateShuffleOrder()
|
||||
}
|
||||
|
||||
q.persistState()
|
||||
q.emitTracksModified(
|
||||
"remove",
|
||||
nil,
|
||||
@@ -784,7 +822,14 @@ func (q *Queue) RemoveTracks(positions []int) {
|
||||
}
|
||||
}
|
||||
|
||||
q.commitMutation(true)
|
||||
q.reindexPositions()
|
||||
|
||||
if q.shuffleMode {
|
||||
q.generateShuffleOrder()
|
||||
}
|
||||
|
||||
q.persistTracks()
|
||||
q.persistState()
|
||||
q.emitTracksModified(
|
||||
"remove",
|
||||
nil,
|
||||
|
||||
Reference in New Issue
Block a user