fix: wire playlist-browser IPC deps through bootstrap surfaces

- Thread `openPlaylistBrowser` action into `IpcRuntimeBootstrapInput`
- Pass `playlistBrowserMainDeps` through bootstrap into `createIpcRuntime`
- Add playlist-browser mock deps to ipc-runtime tests
- Bump subminer-yomitan submodule
This commit is contained in:
2026-04-01 00:16:39 -07:00
parent 983f3b38ee
commit ec64eebb80
6 changed files with 58 additions and 2 deletions

View File

@@ -1,9 +1,10 @@
--- ---
id: TASK-238.8 id: TASK-238.8
title: Refactor src/main.ts composition root into domain runtimes title: Refactor src/main.ts composition root into domain runtimes
status: To Do status: In Progress
assignee: [] assignee: []
created_date: '2026-03-31 06:28' created_date: '2026-03-31 06:28'
updated_date: '2026-04-01 07:07'
labels: labels:
- tech-debt - tech-debt
- runtime - runtime
@@ -34,3 +35,9 @@ Refactor `src/main.ts` so it becomes a thin composition root and the domain-spec
- [ ] #5 Cross-domain coordination stays in `main.ts`; wrapper modules stay acyclic and communicate via injected callbacks. - [ ] #5 Cross-domain coordination stays in `main.ts`; wrapper modules stay acyclic and communicate via injected callbacks.
- [ ] #6 No user-facing behavior, config fields, or IPC channel names change. - [ ] #6 No user-facing behavior, config fields, or IPC channel names change.
<!-- AC:END --> <!-- AC:END -->
## Implementation Notes
<!-- SECTION:NOTES:BEGIN -->
CI follow-up: typecheck failed after the runtime split because playlist-browser IPC deps were not threaded through the new bootstrap/composer surfaces. Wiring the missing open action and registration deps now.
<!-- SECTION:NOTES:END -->

View File

@@ -131,6 +131,8 @@ import { shouldAutoOpenFirstRunSetup } from './main/first-run-runtime';
import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch'; import { createWindowsMpvLaunchDeps, launchWindowsMpv } from './main/runtime/windows-mpv-launch';
import { shouldEnsureTrayOnStartupForInitialArgs } from './main/runtime/startup-tray-policy'; import { shouldEnsureTrayOnStartupForInitialArgs } from './main/runtime/startup-tray-policy';
import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup'; import { createImmersionTrackerStartupHandler } from './main/runtime/immersion-startup';
import { createPlaylistBrowserIpcRuntime } from './main/runtime/playlist-browser-ipc';
import { openPlaylistBrowser as openPlaylistBrowserRuntime } from './main/runtime/playlist-browser-open';
import { import {
createRunStatsCliCommandHandler, createRunStatsCliCommandHandler,
writeStatsCliCommandResponse, writeStatsCliCommandResponse,
@@ -931,6 +933,7 @@ const { mpvRuntime, mining } = createMainPlaybackRuntime({
}, },
}, },
}); });
const playlistBrowserIpcRuntime = createPlaylistBrowserIpcRuntime(() => appState.mpvClient);
function createMpvClientRuntimeService(): MpvIpcClient { function createMpvClientRuntimeService(): MpvIpcClient {
return mpvRuntime.createMpvClientRuntimeService(); return mpvRuntime.createMpvClientRuntimeService();
@@ -1101,6 +1104,15 @@ const { registerIpcRuntimeHandlers } = createIpcRuntimeBootstrap({
actions: { actions: {
requestAppQuit, requestAppQuit,
openYomitanSettings: () => yomitan.openYomitanSettings(), openYomitanSettings: () => yomitan.openYomitanSettings(),
openPlaylistBrowser: () => {
void openPlaylistBrowserRuntime({
ensureOverlayStartupPrereqs: () => startupRuntime.appReady.ensureOverlayStartupPrereqs(),
ensureOverlayWindowsReadyForVisibilityActions: () =>
overlayUi?.ensureOverlayWindowsReadyForVisibilityActions(),
sendToActiveOverlayWindow: (channel, payload, runtimeOptions) =>
overlayUi?.sendToActiveOverlayWindow(channel, payload, runtimeOptions) ?? false,
});
},
showDesktopNotification, showDesktopNotification,
setAnkiIntegration: (integration: AnkiIntegration | null) => { setAnkiIntegration: (integration: AnkiIntegration | null) => {
appState.ankiIntegration = integration; appState.ankiIntegration = integration;
@@ -1112,6 +1124,9 @@ const { registerIpcRuntimeHandlers } = createIpcRuntimeBootstrap({
anilist, anilist,
mining, mining,
dictionarySupport, dictionarySupport,
playlistBrowser: {
playlistBrowserMainDeps: playlistBrowserIpcRuntime.playlistBrowserMainDeps,
},
configDerived: configDerivedRuntime, configDerived: configDerivedRuntime,
subsync: subsyncRuntime, subsync: subsyncRuntime,
}, },

View File

@@ -29,6 +29,7 @@ import type { OverlayModalRuntime } from './overlay-runtime';
import type { OverlayUiRuntime } from './overlay-ui-runtime'; import type { OverlayUiRuntime } from './overlay-ui-runtime';
import type { AppState } from './state'; import type { AppState } from './state';
import type { SubtitleRuntime } from './subtitle-runtime'; import type { SubtitleRuntime } from './subtitle-runtime';
import type { PlaylistBrowserIpcRuntime } from './runtime/playlist-browser-ipc';
import type { YoutubeRuntime } from './youtube-runtime'; import type { YoutubeRuntime } from './youtube-runtime';
import { resolveSubtitleStyleForRenderer } from './runtime/domains/overlay'; import { resolveSubtitleStyleForRenderer } from './runtime/domains/overlay';
import type { ShortcutsRuntime } from './shortcuts-runtime'; import type { ShortcutsRuntime } from './shortcuts-runtime';
@@ -82,6 +83,7 @@ export interface IpcRuntimeBootstrapInput {
actions: { actions: {
requestAppQuit: () => void; requestAppQuit: () => void;
openYomitanSettings: () => boolean; openYomitanSettings: () => boolean;
openPlaylistBrowser: () => void | Promise<void>;
showDesktopNotification: (title: string, options: { body?: string }) => void; showDesktopNotification: (title: string, options: { body?: string }) => void;
setAnkiIntegration: (integration: AnkiIntegration | null) => void; setAnkiIntegration: (integration: AnkiIntegration | null) => void;
}; };
@@ -103,6 +105,7 @@ export interface IpcRuntimeBootstrapInput {
| 'setFieldGroupingResolver' | 'setFieldGroupingResolver'
| 'resolveMediaPathForJimaku' | 'resolveMediaPathForJimaku'
>; >;
playlistBrowser: Pick<PlaylistBrowserIpcRuntime, 'playlistBrowserMainDeps'>;
configDerived: ConfigDerivedRuntimeLike; configDerived: ConfigDerivedRuntimeLike;
subsync: SubsyncRuntimeLike; subsync: SubsyncRuntimeLike;
}; };
@@ -115,6 +118,7 @@ export function createIpcRuntimeBootstrap(input: IpcRuntimeBootstrapInput): IpcR
triggerSubsyncFromConfig: () => input.runtimes.subsync.triggerFromConfig(), triggerSubsyncFromConfig: () => input.runtimes.subsync.triggerFromConfig(),
openRuntimeOptionsPalette: () => input.overlay.getOverlayUi()?.openRuntimeOptionsPalette(), openRuntimeOptionsPalette: () => input.overlay.getOverlayUi()?.openRuntimeOptionsPalette(),
openYoutubeTrackPicker: () => input.runtimes.youtube.openYoutubeTrackPickerFromPlayback(), openYoutubeTrackPicker: () => input.runtimes.youtube.openYoutubeTrackPickerFromPlayback(),
openPlaylistBrowser: () => input.actions.openPlaylistBrowser(),
cycleRuntimeOption: (id, direction) => { cycleRuntimeOption: (id, direction) => {
if (!input.appState.runtimeOptionsManager) { if (!input.appState.runtimeOptionsManager) {
return { ok: false, error: 'Runtime options manager unavailable' }; return { ok: false, error: 'Runtime options manager unavailable' };
@@ -204,6 +208,7 @@ export function createIpcRuntimeBootstrap(input: IpcRuntimeBootstrapInput): IpcR
}, },
getImmersionTracker: () => input.appState.immersionTracker, getImmersionTracker: () => input.appState.immersionTracker,
}, },
playlistBrowser: input.runtimes.playlistBrowser.playlistBrowserMainDeps,
anilist: { anilist: {
getStatus: () => input.runtimes.anilist.getStatusSnapshot(), getStatus: () => input.runtimes.anilist.getStatusSnapshot(),
clearToken: () => input.runtimes.anilist.clearTokenState(), clearToken: () => input.runtimes.anilist.clearTokenState(),

View File

@@ -17,6 +17,21 @@ function createBaseRuntimeInput(capturedRegistration: { value: unknown | null })
quitApp: () => {}, quitApp: () => {},
toggleVisibleOverlay: () => {}, toggleVisibleOverlay: () => {},
}, },
playlistBrowser: {
getPlaylistBrowserSnapshot: async () => ({
directoryPath: null,
directoryAvailable: false,
directoryStatus: '',
directoryItems: [],
playlistItems: [],
playingIndex: null,
currentFilePath: null,
}),
appendPlaylistBrowserFile: async () => ({ ok: true, message: 'ok' }),
playPlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
removePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
movePlaylistBrowserIndex: async () => ({ ok: true, message: 'ok' }),
},
subtitle: { subtitle: {
tokenizeCurrentSubtitle: async () => null, tokenizeCurrentSubtitle: async () => null,
getCurrentSubtitleRaw: () => '', getCurrentSubtitleRaw: () => '',
@@ -86,6 +101,7 @@ function createBaseRuntimeInput(capturedRegistration: { value: unknown | null })
triggerSubsyncFromConfig: () => {}, triggerSubsyncFromConfig: () => {},
openRuntimeOptionsPalette: () => {}, openRuntimeOptionsPalette: () => {},
openYoutubeTrackPicker: () => {}, openYoutubeTrackPicker: () => {},
openPlaylistBrowser: () => {},
cycleRuntimeOption: () => ({ ok: true }), cycleRuntimeOption: () => ({ ok: true }),
showMpvOsd: () => {}, showMpvOsd: () => {},
replayCurrentSubtitle: () => {}, replayCurrentSubtitle: () => {},
@@ -134,11 +150,13 @@ test('ipc runtime registers composed IPC handlers from explicit registration inp
const registration = capturedRegistration.value as { const registration = capturedRegistration.value as {
runtimeOptions: { showMpvOsd: unknown }; runtimeOptions: { showMpvOsd: unknown };
mainDeps: { mainDeps: {
getPlaylistBrowserSnapshot: unknown;
handleMpvCommand: unknown; handleMpvCommand: unknown;
runSubsyncManual: (payload: unknown) => Promise<unknown>; runSubsyncManual: (payload: unknown) => Promise<unknown>;
}; };
}; };
assert.equal(registration.runtimeOptions.showMpvOsd !== undefined, true); assert.equal(registration.runtimeOptions.showMpvOsd !== undefined, true);
assert.equal(registration.mainDeps.getPlaylistBrowserSnapshot instanceof Function, true);
assert.equal(registration.mainDeps.handleMpvCommand instanceof Function, true); assert.equal(registration.mainDeps.handleMpvCommand instanceof Function, true);
assert.deepEqual( assert.deepEqual(
await registration.mainDeps.runSubsyncManual({ payload: null } as never), await registration.mainDeps.runSubsyncManual({ payload: null } as never),
@@ -163,11 +181,13 @@ test('ipc runtime builds grouped registration input from main state', async () =
const registration = capturedRegistration.value as { const registration = capturedRegistration.value as {
runtimeOptions: { showMpvOsd: unknown }; runtimeOptions: { showMpvOsd: unknown };
mainDeps: { mainDeps: {
getPlaylistBrowserSnapshot: unknown;
handleMpvCommand: unknown; handleMpvCommand: unknown;
runSubsyncManual: (payload: unknown) => Promise<unknown>; runSubsyncManual: (payload: unknown) => Promise<unknown>;
}; };
}; };
assert.equal(registration.runtimeOptions.showMpvOsd !== undefined, true); assert.equal(registration.runtimeOptions.showMpvOsd !== undefined, true);
assert.equal(registration.mainDeps.getPlaylistBrowserSnapshot instanceof Function, true);
assert.equal(registration.mainDeps.handleMpvCommand instanceof Function, true); assert.equal(registration.mainDeps.handleMpvCommand instanceof Function, true);
assert.deepEqual( assert.deepEqual(
await registration.mainDeps.runSubsyncManual({ payload: null } as never), await registration.mainDeps.runSubsyncManual({ payload: null } as never),

View File

@@ -30,6 +30,14 @@ export interface IpcRuntimeMainInput {
| 'quitApp' | 'quitApp'
| 'toggleVisibleOverlay' | 'toggleVisibleOverlay'
>; >;
playlistBrowser: Pick<
RegisterIpcRuntimeServicesParams['mainDeps'],
| 'getPlaylistBrowserSnapshot'
| 'appendPlaylistBrowserFile'
| 'playPlaylistBrowserIndex'
| 'removePlaylistBrowserIndex'
| 'movePlaylistBrowserIndex'
>;
subtitle: Pick< subtitle: Pick<
RegisterIpcRuntimeServicesParams['mainDeps'], RegisterIpcRuntimeServicesParams['mainDeps'],
| 'tokenizeCurrentSubtitle' | 'tokenizeCurrentSubtitle'
@@ -110,6 +118,7 @@ export function createIpcRuntime(input: IpcRuntimeInput): IpcRuntime {
runtimeOptions: input.registration.runtimeOptions, runtimeOptions: input.registration.runtimeOptions,
mainDeps: { mainDeps: {
...input.registration.main.window, ...input.registration.main.window,
...input.registration.main.playlistBrowser,
...input.registration.main.subtitle, ...input.registration.main.subtitle,
...input.registration.main.controller, ...input.registration.main.controller,
...input.registration.main.runtime, ...input.registration.main.runtime,