mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-06 20:10:12 +02:00
rework queue persistence (#1862)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import merge from 'lodash/merge';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { createJSONStorage, persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { persist, subscribeWithSelector } from 'zustand/middleware';
|
||||
import { immer } from 'zustand/middleware/immer';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { createWithEqualityFn } from 'zustand/traditional';
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
setTimestamp as setTimestampStore,
|
||||
useTimestampStoreBase,
|
||||
} from '/@/renderer/store/timestamp.store';
|
||||
import { idbStateStorage } from '/@/renderer/store/utils';
|
||||
import { migratePlayerStorePersist, playerStoreStorage } from '/@/renderer/store/utils';
|
||||
import { shuffleInPlace } from '/@/renderer/utils/shuffle';
|
||||
import { PlayerData, QueueData, QueueSong, Song } from '/@/shared/types/domain-types';
|
||||
import {
|
||||
@@ -1543,12 +1543,17 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
||||
merge: (persistedState: any, currentState: any) => {
|
||||
return merge(currentState, persistedState);
|
||||
},
|
||||
migrate: (persistedState, version) => {
|
||||
if (version <= 3) {
|
||||
migrate: async (persistedState, oldVersion) => {
|
||||
if (oldVersion < 3) {
|
||||
return {} as PlayerState;
|
||||
}
|
||||
|
||||
return persistedState;
|
||||
if (oldVersion === 3) {
|
||||
await migratePlayerStorePersist('player-store');
|
||||
return persistedState as Partial<PlayerState>;
|
||||
}
|
||||
|
||||
return persistedState as Partial<PlayerState>;
|
||||
},
|
||||
name: 'player-store',
|
||||
partialize: (state) => {
|
||||
@@ -1564,53 +1569,22 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
||||
excludedPlayerKeys.push('index');
|
||||
}
|
||||
|
||||
// Filter top-level state entries
|
||||
const filteredStateEntries = Object.entries(state).filter(([key]) => {
|
||||
// Exclude queue if shouldRestorePlayQueue is false
|
||||
if (!shouldRestorePlayQueue && key === 'queue') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const player = Object.fromEntries(
|
||||
Object.entries(state.player).filter(
|
||||
([key]) => !excludedPlayerKeys.includes(key),
|
||||
),
|
||||
) as typeof state.player;
|
||||
|
||||
const filteredState = Object.fromEntries(
|
||||
filteredStateEntries,
|
||||
) as Partial<PlayerState>;
|
||||
|
||||
// Filter player object
|
||||
if (filteredState.player) {
|
||||
filteredState.player = Object.fromEntries(
|
||||
Object.entries(filteredState.player).filter(
|
||||
([key]) => !excludedPlayerKeys.includes(key),
|
||||
),
|
||||
) as typeof filteredState.player;
|
||||
if (!shouldRestorePlayQueue) {
|
||||
return { player };
|
||||
}
|
||||
|
||||
if (filteredState.queue) {
|
||||
const allQueueIds = new Set([
|
||||
...(filteredState.queue.default || []),
|
||||
// shuffled now contains indexes, not uniqueIds, so we don't include it here
|
||||
]);
|
||||
|
||||
const songs = filteredState.queue.songs || {};
|
||||
const cleanedSongs: Record<string, QueueSong> = {};
|
||||
|
||||
for (const [id, song] of Object.entries(songs)) {
|
||||
if (allQueueIds.has(id)) {
|
||||
cleanedSongs[id] = song;
|
||||
}
|
||||
}
|
||||
|
||||
filteredState.queue = {
|
||||
...filteredState.queue,
|
||||
songs: cleanedSongs,
|
||||
};
|
||||
}
|
||||
|
||||
return filteredState;
|
||||
// Queue pruning and IDB writes are handled in `playerStoreStorage` so we only
|
||||
// serialize the large queue when the queue slice reference actually changes.
|
||||
return { player, queue: state.queue };
|
||||
},
|
||||
storage: createJSONStorage(() => idbStateStorage),
|
||||
version: 3,
|
||||
storage: playerStoreStorage,
|
||||
version: 4,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
+141
-1
@@ -1,6 +1,146 @@
|
||||
import type { QueueData, QueueSong } from '/@/shared/types/domain-types';
|
||||
import type { PersistStorage, StateStorage } from 'zustand/middleware';
|
||||
|
||||
import { del, get, set } from 'idb-keyval';
|
||||
import mergeWith from 'lodash/mergeWith';
|
||||
import { StateStorage } from 'zustand/middleware';
|
||||
|
||||
type PlayerStorePersistedSlice = {
|
||||
player?: unknown;
|
||||
queue?: QueueData;
|
||||
};
|
||||
|
||||
export function cleanQueueForPersistence(queue: QueueData): QueueData {
|
||||
const allQueueIds = new Set(queue.default || []);
|
||||
const songs = queue.songs || {};
|
||||
const cleanedSongs: Record<string, QueueSong> = {};
|
||||
|
||||
for (const [id, song] of Object.entries(songs)) {
|
||||
if (allQueueIds.has(id)) {
|
||||
cleanedSongs[id] = song;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...queue,
|
||||
songs: cleanedSongs,
|
||||
};
|
||||
}
|
||||
|
||||
// Migrate from v3 to v4 to handle queue migration
|
||||
export async function migratePlayerStorePersist(storeName: string): Promise<void> {
|
||||
const mainRaw = await get(storeName);
|
||||
if (!mainRaw) {
|
||||
return;
|
||||
}
|
||||
|
||||
let parsed: { state?: { player?: unknown; queue?: QueueData }; version?: number };
|
||||
try {
|
||||
parsed = JSON.parse(mainRaw as string);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const embeddedQueue = parsed.state?.queue;
|
||||
if (embeddedQueue === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queueKey = `${storeName}-queue`;
|
||||
const queueSeparateRaw = await get(queueKey);
|
||||
|
||||
if (!queueSeparateRaw) {
|
||||
const cleaned = cleanQueueForPersistence(embeddedQueue);
|
||||
await set(queueKey, JSON.stringify(cleaned));
|
||||
}
|
||||
|
||||
await set(
|
||||
storeName,
|
||||
JSON.stringify({
|
||||
state: { player: parsed.state?.player },
|
||||
version: parsed.version,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function playerStoreQueueKey(storeName: string): string {
|
||||
return `${storeName}-queue`;
|
||||
}
|
||||
|
||||
let lastPersistedPlayerQueueRef: QueueData | undefined;
|
||||
|
||||
export const playerStoreStorage: PersistStorage<unknown> = {
|
||||
getItem: async (name) => {
|
||||
const mainRaw = await get(name);
|
||||
if (!mainRaw) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let parsed: { state?: { player?: unknown; queue?: QueueData }; version?: number };
|
||||
try {
|
||||
parsed = JSON.parse(mainRaw as string);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
const version = parsed.version;
|
||||
let queue: QueueData | undefined;
|
||||
const queueRaw = await get(playerStoreQueueKey(name));
|
||||
|
||||
if (queueRaw) {
|
||||
try {
|
||||
queue = JSON.parse(queueRaw as string) as QueueData;
|
||||
} catch {
|
||||
queue = undefined;
|
||||
}
|
||||
} else if (parsed.state?.queue) {
|
||||
// Fallback to legacy format if queue is not found
|
||||
queue = parsed.state.queue;
|
||||
}
|
||||
|
||||
return {
|
||||
state: {
|
||||
player: parsed.state?.player,
|
||||
queue,
|
||||
} satisfies PlayerStorePersistedSlice,
|
||||
version,
|
||||
};
|
||||
},
|
||||
|
||||
removeItem: async (name) => {
|
||||
lastPersistedPlayerQueueRef = undefined;
|
||||
await del(name);
|
||||
await del(playerStoreQueueKey(name));
|
||||
},
|
||||
|
||||
setItem: async (name, value) => {
|
||||
const { state: rawState, version } = value;
|
||||
const state = rawState as PlayerStorePersistedSlice;
|
||||
const player = state.player;
|
||||
|
||||
await set(
|
||||
name,
|
||||
JSON.stringify({
|
||||
state: { player },
|
||||
version,
|
||||
}),
|
||||
);
|
||||
|
||||
if (state.queue === undefined) {
|
||||
lastPersistedPlayerQueueRef = undefined;
|
||||
await del(playerStoreQueueKey(name));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.queue === lastPersistedPlayerQueueRef) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cleaned = cleanQueueForPersistence(state.queue);
|
||||
await set(playerStoreQueueKey(name), JSON.stringify(cleaned));
|
||||
lastPersistedPlayerQueueRef = state.queue;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom deep merger that will replace all 'columns' items with the persistent
|
||||
* state, instead of the default merge behavior. This is important to preserve the user's
|
||||
|
||||
Reference in New Issue
Block a user