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"
|
"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
|
// 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).
|
// 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.
|
// 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.loadCurrentTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
q.commitMutation(false)
|
if q.shuffleMode {
|
||||||
|
q.generateShuffleOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
q.persistAddTrack(track)
|
||||||
|
q.persistState()
|
||||||
q.emitTracksModified(
|
q.emitTracksModified(
|
||||||
"add",
|
"add",
|
||||||
[]Track{track},
|
[]Track{track},
|
||||||
@@ -388,7 +393,12 @@ func (q *Queue) AddTracks(filePaths []string) {
|
|||||||
q.loadCurrentTrack()
|
q.loadCurrentTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
q.commitMutation(false)
|
if q.shuffleMode {
|
||||||
|
q.generateShuffleOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
q.persistAddTracks(newTracks)
|
||||||
|
q.persistState()
|
||||||
q.emitTracksModified(
|
q.emitTracksModified(
|
||||||
"add",
|
"add",
|
||||||
newTracks,
|
newTracks,
|
||||||
@@ -439,7 +449,14 @@ func (q *Queue) InsertNextTracks(filePaths []string) {
|
|||||||
q.loadCurrentTrack()
|
q.loadCurrentTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
q.commitMutation(true)
|
q.reindexPositions()
|
||||||
|
|
||||||
|
if q.shuffleMode {
|
||||||
|
q.generateShuffleOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
q.persistInsertTracks(newTracks, insertPos)
|
||||||
|
q.persistState()
|
||||||
q.emitTracksModified(
|
q.emitTracksModified(
|
||||||
"insert",
|
"insert",
|
||||||
newTracks,
|
newTracks,
|
||||||
@@ -482,7 +499,14 @@ func (q *Queue) InsertNext(filePath string) {
|
|||||||
q.loadCurrentTrack()
|
q.loadCurrentTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
q.commitMutation(true)
|
q.reindexPositions()
|
||||||
|
|
||||||
|
if q.shuffleMode {
|
||||||
|
q.generateShuffleOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
q.persistInsertTracks([]Track{track}, insertPos)
|
||||||
|
q.persistState()
|
||||||
q.emitTracksModified(
|
q.emitTracksModified(
|
||||||
"insert",
|
"insert",
|
||||||
[]Track{track},
|
[]Track{track},
|
||||||
@@ -542,7 +566,14 @@ func (q *Queue) InsertTracksAt(filePaths []string, index int) {
|
|||||||
q.loadCurrentTrack()
|
q.loadCurrentTrack()
|
||||||
}
|
}
|
||||||
|
|
||||||
q.commitMutation(true)
|
q.reindexPositions()
|
||||||
|
|
||||||
|
if q.shuffleMode {
|
||||||
|
q.generateShuffleOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
q.persistInsertTracks(newTracks, index)
|
||||||
|
q.persistState()
|
||||||
q.emitTracksModified(
|
q.emitTracksModified(
|
||||||
"insert",
|
"insert",
|
||||||
newTracks,
|
newTracks,
|
||||||
@@ -722,7 +753,14 @@ func (q *Queue) RemoveTrack(position int) {
|
|||||||
q.currentIndex = len(q.tracks) - 1
|
q.currentIndex = len(q.tracks) - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
q.commitMutation(true)
|
q.persistRemoveTrack(position)
|
||||||
|
q.reindexPositions()
|
||||||
|
|
||||||
|
if q.shuffleMode {
|
||||||
|
q.generateShuffleOrder()
|
||||||
|
}
|
||||||
|
|
||||||
|
q.persistState()
|
||||||
q.emitTracksModified(
|
q.emitTracksModified(
|
||||||
"remove",
|
"remove",
|
||||||
nil,
|
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(
|
q.emitTracksModified(
|
||||||
"remove",
|
"remove",
|
||||||
nil,
|
nil,
|
||||||
|
|||||||
Reference in New Issue
Block a user