feat(services): server kind for the playlist storage backend

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-10-12 00:16:55 +02:00
1 parent 872d8edc68
commit ab2eef26d2
2 files changed
+59 -2

No files matched your search

@@ -41,4 +41,30 @@ describe('PlaylistStorageBackend', () => {
expect(backend.kind()).toBe('indexeddb');
expect(backend.documentStore()).toBeNull();
});
it('prefers the server store once it is set', () => {
testWindow.electron = undefined;
const backend = create(false);
const store = { dbGetAppPlaylists: jest.fn() } as never;
backend.useServerStore(store);
expect(backend.kind()).toBe('server');
expect(backend.documentStore()).toBe(store);
});
it('falls back to browser storage while an override task runs', async () => {
testWindow.electron = undefined;
const backend = create(false);
backend.useServerStore({ dbGetAppPlaylists: jest.fn() } as never);
const seen = await backend.runWithBrowserStorage(async () => [
backend.kind(),
backend.usesBrowserStorageOverride(),
]);
expect(seen).toEqual(['indexeddb', true]);
expect(backend.kind()).toBe('server');
expect(backend.usesBrowserStorageOverride()).toBe(false);
});
});
@@ -28,20 +28,51 @@ export type PlaylistDocumentStore = Pick<
>
>;
export type PlaylistStorageKind = 'electron' | 'indexeddb';
export type PlaylistStorageKind = 'electron' | 'server' | 'indexeddb';
/** Single place that decides where `PlaylistsService` persists playlists. */
@Injectable({ providedIn: 'root' })
export class PlaylistStorageBackend {
private readonly runtime = inject(RuntimeCapabilitiesService);
private serverStore: PlaylistDocumentStore | null = null;
private browserOverrideDepth = 0;
/** Server mode: set once by the server storage session at startup. */
useServerStore(store: PlaylistDocumentStore): void {
this.serverStore = store;
}
usesBrowserStorageOverride(): boolean {
return this.browserOverrideDepth > 0;
}
/**
* Runs `task` against browser storage although server mode is active.
* Only the migration export uses it, inside a modal flow.
*/
async runWithBrowserStorage<T>(task: () => Promise<T>): Promise<T> {
this.browserOverrideDepth += 1;
try {
return await task();
} finally {
this.browserOverrideDepth -= 1;
}
}
kind(): PlaylistStorageKind {
if (this.serverStore && this.browserOverrideDepth === 0) {
return 'server';
}
return this.runtime.supportsSqlite ? 'electron' : 'indexeddb';
}
/** The document store for `kind()`, or `null` for IndexedDB. */
documentStore(): PlaylistDocumentStore | null {
if (this.kind() !== 'electron' || typeof window === 'undefined') {
const kind = this.kind();
if (kind === 'server') {
return this.serverStore;
}
if (kind !== 'electron' || typeof window === 'undefined') {
return null;
}
return (window.electron as PlaylistDocumentStore | undefined) ?? null;