Files
Psychotoxical-psysonic/src/features/playback/store/queueUndo.ts
T
Psychotoxical 9058abd340 refactor(lib): extract Track/QueueItemRef domain model to lib/media/trackTypes
Track is the app's normalized song model and QueueItemRef its thin queue
identity -- consumed app-wide (queue, cover, context menus, sharing, lucky-mix,
library browse), not a playback-internal detail. They lived in
features/playback/store/playerStoreTypes, forcing ~115 core/feature files to
reach into the playback feature for the central media type.

Move the two interfaces to lib/media/trackTypes; playerStoreTypes keeps
PlayerState and now imports them from lib. 115 importers repointed (mixed
'PlayerState, Track' lines split so PlayerState stays). Pure type move (erased at
runtime). tsc 0, lint 0.
2026-06-30 17:16:56 +02:00

107 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import type { QueueItemRef, Track } from '@/lib/media/trackTypes';
import type { PlayerState } from '@/features/playback/store/playerStoreTypes';
/** Hard cap on undo/redo depth — keeps memory bounded for very long sessions. */
export const QUEUE_UNDO_MAX = 32;
export type QueueUndoSnapshot = {
/** Thin queue refs (thin-state phase 4) — not hydrated `Track[]`, so 32
* snapshots of a 50k queue cost refs, not 32×50k full tracks. Rebuilt to a
* display `Track[]` through the resolver on restore. */
queueItems: QueueItemRef[];
queueIndex: number;
/** Kept full — one resolved playing track, restored to the engine on undo. */
currentTrack: Track | null;
/** Seconds — captured with the snapshot (older entries may omit). */
currentTime?: number;
progress?: number;
isPlaying?: boolean;
/** Main queue panel list `scrollTop` when the snapshot was taken. */
queueListScrollTop?: number;
/** Canonical playback-server identity at snapshot time. Restore uses this
* for any ref it has to prepend (e.g. a still-playing track absent from the
* snapshot's queue) so a mid-restore server switch can't bind the prepended
* ref to the wrong server (B1/H3). Older in-memory entries may omit it;
* callers fall back to the live `queueServerId` in that case. */
queueServerId?: string | null;
};
const queueUndoStack: QueueUndoSnapshot[] = [];
const queueRedoStack: QueueUndoSnapshot[] = [];
/** Test-only: clears module-scoped undo/redo stacks so each test starts clean. */
export function _resetQueueUndoStacksForTest(): void {
queueUndoStack.length = 0;
queueRedoStack.length = 0;
}
/** QueuePanel registers a reader so undo snapshots capture list scroll position. */
let queueListScrollTopReader: (() => number | undefined) | null = null;
export function registerQueueListScrollTopReader(reader: (() => number | undefined) | null): void {
queueListScrollTopReader = reader;
}
function readQueueListScrollTopForUndo(): number | undefined {
return queueListScrollTopReader?.() ?? undefined;
}
/** Set in applyQueueHistorySnapshot; QueuePanel consumes in useLayoutEffect after commit. */
let pendingQueueListScrollTop: number | undefined;
export function setPendingQueueListScrollTop(value: number): void {
pendingQueueListScrollTop = value;
}
export function consumePendingQueueListScrollTop(): number | undefined {
const v = pendingQueueListScrollTop;
pendingQueueListScrollTop = undefined;
return v;
}
export function queueUndoSnapshotFromState(s: PlayerState): QueueUndoSnapshot {
const scrollTop = readQueueListScrollTopForUndo();
return {
// Thin refs straight off the canonical list — 32 snapshots cost refs, not
// 32×50k full tracks (the undo "hidden multiplier" the thin-state plan kills).
queueItems: [...s.queueItems],
queueIndex: s.queueIndex,
currentTrack: s.currentTrack ? { ...s.currentTrack } : null,
currentTime: s.currentTime,
progress: s.progress,
isPlaying: s.isPlaying,
queueServerId: s.queueServerId,
...(scrollTop !== undefined ? { queueListScrollTop: scrollTop } : {}),
};
}
/**
* Snapshot the current state onto the undo stack and clear the redo stack —
* standard "new action invalidates redo history" behaviour. Called from every
* mutating queue action.
*/
export function pushQueueUndoFromGetter(get: () => PlayerState): void {
queueRedoStack.length = 0;
queueUndoStack.push(queueUndoSnapshotFromState(get()));
while (queueUndoStack.length > QUEUE_UNDO_MAX) queueUndoStack.shift();
}
export function popQueueUndoSnapshot(): QueueUndoSnapshot | undefined {
return queueUndoStack.pop();
}
export function popQueueRedoSnapshot(): QueueUndoSnapshot | undefined {
return queueRedoStack.pop();
}
/** Push a prebuilt snapshot onto the redo stack — used after an undo step. */
export function pushQueueRedoSnapshot(snap: QueueUndoSnapshot): void {
queueRedoStack.push(snap);
while (queueRedoStack.length > QUEUE_UNDO_MAX) queueRedoStack.shift();
}
/** Push a prebuilt snapshot onto the undo stack — used after a redo step. */
export function pushQueueUndoSnapshot(snap: QueueUndoSnapshot): void {
queueUndoStack.push(snap);
while (queueUndoStack.length > QUEUE_UNDO_MAX) queueUndoStack.shift();
}