diff --git a/bun.lock b/bun.lock index 59f32f3f..fffea52b 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,7 @@ "app-builder-lib": "26.15.3", "brace-expansion": "5.0.9", "electron-builder-squirrel-windows": "26.15.3", - "fast-uri": "3.1.5", + "fast-uri": "3.1.6", "form-data": "4.0.6", "ip-address": "10.2.0", "js-yaml": "4.3.1", @@ -406,7 +406,7 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], - "fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="], + "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], diff --git a/package.json b/package.json index 4c53a95b..acd788eb 100644 --- a/package.json +++ b/package.json @@ -88,7 +88,7 @@ "app-builder-lib": "26.15.3", "brace-expansion": "5.0.9", "electron-builder-squirrel-windows": "26.15.3", - "fast-uri": "3.1.5", + "fast-uri": "3.1.6", "form-data": "4.0.6", "ip-address": "10.2.0", "js-yaml": "4.3.1", diff --git a/packaging/aur/subminer-bin/.SRCINFO b/packaging/aur/subminer-bin/.SRCINFO index d2e3e50c..298afa3d 100644 --- a/packaging/aur/subminer-bin/.SRCINFO +++ b/packaging/aur/subminer-bin/.SRCINFO @@ -1,6 +1,6 @@ pkgbase = subminer-bin pkgdesc = All-in-one sentence mining overlay with AnkiConnect and dictionary integration - pkgver = 0.6.2 + pkgver = 0.19.5 pkgrel = 2 url = https://github.com/ksyasuda/SubMiner arch = x86_64 @@ -22,16 +22,16 @@ pkgbase = subminer-bin optdepends = alass-git: preferred subtitle synchronization engine optdepends = python-ffsubsync: fallback subtitle synchronization engine optdepends = mangatan-extension-server: anime browser bridge shared with Mangatan; skips the bundle download and is updated by pacman - provides = subminer=0.6.2 + provides = subminer=0.19.5 conflicts = subminer - noextract = SubMiner-0.6.2.AppImage + noextract = SubMiner-0.19.5.AppImage options = !strip options = !debug - source = SubMiner-0.6.2.AppImage::https://github.com/ksyasuda/SubMiner/releases/download/v0.6.2/SubMiner-0.6.2.AppImage - source = subminer-0.6.2::https://github.com/ksyasuda/SubMiner/releases/download/v0.6.2/subminer - source = subminer-assets-0.6.2.tar.gz::https://github.com/ksyasuda/SubMiner/releases/download/v0.6.2/subminer-assets.tar.gz - sha256sums = c91667adbbc47a0fba34855358233454a9ea442ab57510546b2219abd1f2461e - sha256sums = 85050918e14cb2512fcd34be83387a2383fa5c206dc1bdc11e8d98f7d37817e5 - sha256sums = 210113be64a06840f4dfaebc22a8e6fc802392f1308413aa00d9348c804ab2a1 + source = SubMiner-0.19.5.AppImage::https://github.com/ksyasuda/SubMiner/releases/download/v0.19.5/SubMiner-0.19.5.AppImage + source = subminer-0.19.5::https://github.com/ksyasuda/SubMiner/releases/download/v0.19.5/subminer + source = subminer-assets-0.19.5.tar.gz::https://github.com/ksyasuda/SubMiner/releases/download/v0.19.5/subminer-assets.tar.gz + sha256sums = bd683d949956ff847927f23acfede49efad6320cf7ec4cdc3cbd586a5287884e + sha256sums = 2fa292f672df715461765aef8d5c6bf3612004c8b8070b1b1f0afa10d4b74c1e + sha256sums = 591468a1fc68a13de315acaf1154df9da41fe3bcbc0e5522a04ba21e939ed1ae pkgname = subminer-bin diff --git a/packaging/aur/subminer-bin/PKGBUILD b/packaging/aur/subminer-bin/PKGBUILD index 57b94e9d..bdbf4df4 100644 --- a/packaging/aur/subminer-bin/PKGBUILD +++ b/packaging/aur/subminer-bin/PKGBUILD @@ -1,7 +1,7 @@ # Maintainer: Kyle Yasuda pkgname=subminer-bin -pkgver=0.6.2 +pkgver=0.19.5 pkgrel=2 pkgdesc='All-in-one sentence mining overlay with AnkiConnect and dictionary integration' arch=('x86_64') @@ -37,9 +37,9 @@ source=( "subminer-assets-${pkgver}.tar.gz::https://github.com/ksyasuda/SubMiner/releases/download/v${pkgver}/subminer-assets.tar.gz" ) sha256sums=( - 'c91667adbbc47a0fba34855358233454a9ea442ab57510546b2219abd1f2461e' - '85050918e14cb2512fcd34be83387a2383fa5c206dc1bdc11e8d98f7d37817e5' - '210113be64a06840f4dfaebc22a8e6fc802392f1308413aa00d9348c804ab2a1' + 'bd683d949956ff847927f23acfede49efad6320cf7ec4cdc3cbd586a5287884e' + '2fa292f672df715461765aef8d5c6bf3612004c8b8070b1b1f0afa10d4b74c1e' + '591468a1fc68a13de315acaf1154df9da41fe3bcbc0e5522a04ba21e939ed1ae' ) noextract=("SubMiner-${pkgver}.AppImage") diff --git a/src/core/services/immersion-tracker-service.test.ts b/src/core/services/immersion-tracker-service.test.ts index e98b3f8c..6410648d 100644 --- a/src/core/services/immersion-tracker-service.test.ts +++ b/src/core/services/immersion-tracker-service.test.ts @@ -617,6 +617,72 @@ test('tracker starts the injected lexical rollup backfill when it is pending', a } }); +test('destroy waits for lexical backfill shutdown before finalizing and draining writes', async () => { + const dbPath = makeDbPath(); + let tracker: ImmersionTrackerService | null = null; + let releaseBackfill = (): void => {}; + let releaseTermination = (): void => {}; + const heldBackfill = new Promise((resolve) => { + releaseBackfill = resolve; + }); + const heldTermination = new Promise((resolve) => { + releaseTermination = resolve; + }); + const calls: string[] = []; + + try { + const setupDb = new Database(dbPath); + const { ensureSchema } = await import('./immersion-tracker/storage'); + ensureSchema(setupDb); + setupDb + .prepare( + `UPDATE imm_rollup_state SET state_value = '0' WHERE state_key = 'lexical_daily_rollups_version'`, + ) + .run(); + setupDb.close(); + + const Ctor = await loadTrackerCtor(); + tracker = new Ctor( + { dbPath }, + { + runLexicalRollupBackfillTask: async () => heldBackfill, + destroyLexicalRollupBackfillRunner: async () => { + calls.push('stop'); + releaseBackfill(); + await heldTermination; + calls.push('stopped'); + }, + }, + ); + tracker.handleMediaChange('/tmp/destroy-backfill.mkv', 'Destroy Backfill'); + tracker.recordCardsMined(1); + + const privateApi = tracker as unknown as { + queue: unknown[]; + finalizeActiveSession: () => void; + }; + const finalizeActiveSession = privateApi.finalizeActiveSession.bind(tracker); + privateApi.finalizeActiveSession = () => { + finalizeActiveSession(); + calls.push(`finalized:${privateApi.queue.length}`); + }; + + const destroyTask = tracker.destroy(); + assert.ok(destroyTask instanceof Promise); + assert.deepEqual(calls, ['stop']); + assert.ok(privateApi.queue.length > 0); + + releaseTermination(); + await destroyTask; + assert.deepEqual(calls, ['stop', 'stopped', 'finalized:0']); + } finally { + releaseBackfill(); + releaseTermination(); + await tracker?.destroy(); + cleanupDbPath(dbPath); + } +}); + test('tracker runs startup session-rollup maintenance before lexical backfill locks writes', async () => { const dbPath = makeDbPath(); let tracker: ImmersionTrackerService | null = null; diff --git a/src/core/services/immersion-tracker-service.ts b/src/core/services/immersion-tracker-service.ts index 11d3d722..617aaace 100644 --- a/src/core/services/immersion-tracker-service.ts +++ b/src/core/services/immersion-tracker-service.ts @@ -463,7 +463,7 @@ export class ImmersionTrackerService { private readonly vocabularySummariesInFlight = new Map>(); private readonly destroyVocabularySummaryRunner: () => void; private readonly runLexicalRollupBackfillTask: () => Promise; - private readonly destroyLexicalRollupBackfillRunner: () => void; + private readonly destroyLexicalRollupBackfillRunner: () => void | Promise; private readonly deleteMaintenanceScheduler: DeleteMaintenanceScheduler; private flushTimer: ReturnType | null = null; private maintenanceTimer: ReturnType | null = null; @@ -472,6 +472,9 @@ export class ImmersionTrackerService { private preserveWriteQueueUntilDrained = false; private lastVacuumMs = 0; private isDestroyed = false; + private isDestroying = false; + private lexicalRollupBackfillTask: Promise | null = null; + private destroyTask: Promise | null = null; private sessionState: SessionState | null = null; private currentVideoKey = ''; private currentMediaPathOrUrl = ''; @@ -495,7 +498,7 @@ export class ImmersionTrackerService { runVocabularySummaryTask?: RunVocabularySummaryTask; destroyVocabularySummaryRunner?: () => void; runLexicalRollupBackfillTask?: (dbPath: string) => Promise; - destroyLexicalRollupBackfillRunner?: () => void; + destroyLexicalRollupBackfillRunner?: () => void | Promise; } = {}, ) { this.dbPath = options.dbPath; @@ -635,8 +638,10 @@ export class ImmersionTrackerService { this.scheduleFlush(); } - destroy(): void { + destroy(): void | Promise { + if (this.destroyTask) return this.destroyTask; if (this.isDestroyed) return; + this.isDestroying = true; if (this.flushTimer) { clearTimeout(this.flushTimer); this.flushTimer = null; @@ -645,13 +650,28 @@ export class ImmersionTrackerService { clearInterval(this.maintenanceTimer); this.maintenanceTimer = null; } - this.finalizeActiveSession(); - this.isDestroyed = true; - this.deleteMaintenanceScheduler.destroy(); - this.destroyDeleteMaintenanceRunner(); - this.destroyVocabularySummaryRunner(); - this.destroyLexicalRollupBackfillRunner(); - this.db.close(); + const stopLexicalBackfill = this.destroyLexicalRollupBackfillRunner(); + const pendingLexicalBackfill = this.lexicalRollupBackfillTask; + const finish = (): void => { + this.finalizeActiveSession(); + this.isDestroyed = true; + this.deleteMaintenanceScheduler.destroy(); + this.destroyDeleteMaintenanceRunner(); + this.destroyVocabularySummaryRunner(); + this.db.close(); + }; + + if (!stopLexicalBackfill && !pendingLexicalBackfill) { + finish(); + return; + } + + this.destroyTask = (async () => { + await stopLexicalBackfill; + await pendingLexicalBackfill; + finish(); + })(); + return this.destroyTask; } async getSessionSummaries(limit = 50): Promise { @@ -914,14 +934,13 @@ export class ImmersionTrackerService { const statsPath = normalizeMediaPath(episode.statsPath); if (!statsPath) continue; if (watched) { - needsLifetimeRebuild = - this.recordStreamPlaybackMetadata(episode, { deferLifetimeRebuild: true }) || - needsLifetimeRebuild; + this.recordStreamPlaybackMetadata(episode, { deferLifetimeRebuild: true }); } const videoId = getVideoIdByVideoKey(this.db, buildVideoKey(statsPath, SOURCE_TYPE_REMOTE)); if (videoId === null) continue; markVideoWatched(this.db, videoId, watched); + needsLifetimeRebuild = true; changed += 1; if (!watched && this.sessionState?.videoId === videoId) clearedActiveVideo = true; @@ -1111,7 +1130,7 @@ export class ImmersionTrackerService { this.requireWriteQueueDrained('lexical rollup backfill'); this.preserveWriteQueueUntilDrained = true; this.setWriteLock('lexical-rollup-backfill', true); - void this.runLexicalRollupBackfillTask() + const task = this.runLexicalRollupBackfillTask() .catch((error: unknown) => { this.logger.warn( 'Lexical daily rollup backfill failed; it will retry on next startup', @@ -1121,8 +1140,10 @@ export class ImmersionTrackerService { .finally(() => { this.setWriteLock('lexical-rollup-backfill', false); if (this.queue.length === 0) this.preserveWriteQueueUntilDrained = false; - else if (!this.isDestroyed) this.scheduleFlush(0); + else if (!this.isDestroyed && !this.isDestroying) this.scheduleFlush(0); + if (this.lexicalRollupBackfillTask === task) this.lexicalRollupBackfillTask = null; }); + this.lexicalRollupBackfillTask = task; } async reassignAnimeAnilist( diff --git a/src/core/services/immersion-tracker-watch-marks.test.ts b/src/core/services/immersion-tracker-watch-marks.test.ts index 903264e0..d4a2a33a 100644 --- a/src/core/services/immersion-tracker-watch-marks.test.ts +++ b/src/core/services/immersion-tracker-watch-marks.test.ts @@ -116,6 +116,35 @@ test('a batch rebuilds the lifetime summaries once, not once per episode', async } }); +test('watch-state changes rebuild lifetime completion for an already-recorded episode', async () => { + const { tracker, dir } = await createTracker(); + try { + const recordedEpisode = episode(EP1, 1); + tracker.recordStreamPlaybackMetadata(recordedEpisode); + tracker.handleMediaChange(EP1, recordedEpisode.displayTitle); + tracker.handleMediaChange(EP2, 'Next Episode'); + + const privateApi = tracker as unknown as { + db: import('./immersion-tracker/sqlite').DatabaseSync; + }; + const getCompleted = (): number | null => + ( + privateApi.db + .prepare('SELECT completed FROM imm_lifetime_media ORDER BY video_id LIMIT 1') + .get() as { completed: number } | null + )?.completed ?? null; + + assert.equal(getCompleted(), 0); + await tracker.setStreamWatchState([recordedEpisode], true); + assert.equal(getCompleted(), 1); + await tracker.setStreamWatchState([recordedEpisode], false); + assert.equal(getCompleted(), 0); + } finally { + await tracker.destroy(); + fs.rmSync(dir, { recursive: true, force: true }); + } +}); + test('an episode with no stats path is skipped rather than recorded as unknown', async () => { const { tracker, dir } = await createTracker(); try { diff --git a/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.test.ts b/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.test.ts index 6ae22046..ec13dc2b 100644 --- a/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.test.ts +++ b/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.test.ts @@ -134,3 +134,43 @@ test('lexical rollup worker times out when it never responds', async () => { runtime.destroy(); } }); + +test('destroy waits for active worker termination', async () => { + let releaseTermination = (): void => {}; + let emitExit: ((code: number) => void) | null = null; + const terminationGate = new Promise((resolve) => { + releaseTermination = resolve; + }); + const runtime = new LexicalRollupWorkerRuntime({ + resolveWorkerPath: () => '/tmp/fake-worker.js', + createWorker: async () => ({ + once(event: string, listener: (value: never) => void) { + if (event === 'exit') emitExit = listener as (code: number) => void; + return this; + }, + terminate: async () => { + await terminationGate; + emitExit?.(1); + return 1; + }, + }), + warn: () => {}, + } as never); + + const runTask = runtime.run('/tmp/not-used.sqlite'); + await new Promise((resolve) => setImmediate(resolve)); + const destroyTask = runtime.destroy(); + assert.ok(destroyTask instanceof Promise); + + let destroyed = false; + void destroyTask.then(() => { + destroyed = true; + }); + await Promise.resolve(); + assert.equal(destroyed, false); + + releaseTermination(); + await destroyTask; + await assert.rejects(runTask, /exited with code 1/); + assert.equal(destroyed, true); +}); diff --git a/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.ts b/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.ts index 30c5c45f..9e9c3f60 100644 --- a/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.ts +++ b/src/core/services/immersion-tracker/lexical-rollup-worker-runtime.ts @@ -35,6 +35,7 @@ export function resolveLexicalRollupWorkerPath(): string | null { export class LexicalRollupWorkerRuntime { private readonly activeWorkers = new Set(); private destroyed = false; + private destroyTask: Promise | null = null; constructor(private readonly options: LexicalRollupWorkerRuntimeOptions = {}) {} @@ -106,12 +107,15 @@ export class LexicalRollupWorkerRuntime { }); } - destroy(): void { - if (this.destroyed) return; + destroy(): void | Promise { + if (this.destroyed) return this.destroyTask ?? undefined; this.destroyed = true; - for (const worker of this.activeWorkers) { - void worker.terminate().catch(() => undefined); - } + const activeWorkers = [...this.activeWorkers]; this.activeWorkers.clear(); + if (activeWorkers.length === 0) return; + this.destroyTask = Promise.all( + activeWorkers.map((worker) => worker.terminate().catch(() => undefined)), + ).then(() => undefined); + return this.destroyTask; } } diff --git a/src/main/runtime/app-lifecycle-actions.test.ts b/src/main/runtime/app-lifecycle-actions.test.ts index 5ab6227c..39acb3ac 100644 --- a/src/main/runtime/app-lifecycle-actions.test.ts +++ b/src/main/runtime/app-lifecycle-actions.test.ts @@ -6,7 +6,7 @@ import { createShouldRestoreWindowsOnActivateHandler, } from './app-lifecycle-actions'; -test('on will quit cleanup handler runs all cleanup steps', () => { +test('on will quit cleanup handler runs all cleanup steps', async () => { const calls: string[] = []; const cleanup = createOnWillQuitCleanupHandler({ destroyTray: () => calls.push('destroy-tray'), @@ -32,7 +32,9 @@ test('on will quit cleanup handler runs all cleanup steps', () => { destroyMpvSocket: () => calls.push('destroy-socket'), clearReconnectTimer: () => calls.push('clear-reconnect'), destroySubtitleTimingTracker: () => calls.push('destroy-subtitle-tracker'), - destroyImmersionTracker: () => calls.push('destroy-immersion'), + destroyImmersionTracker: () => { + calls.push('destroy-immersion'); + }, destroyAnkiIntegration: () => calls.push('destroy-anki'), destroyAnilistSetupWindow: () => calls.push('destroy-anilist-window'), clearAnilistSetupWindow: () => calls.push('clear-anilist-window'), @@ -50,7 +52,7 @@ test('on will quit cleanup handler runs all cleanup steps', () => { stopDiscordPresenceService: () => calls.push('stop-discord-presence'), }); - cleanup(); + await cleanup(); assert.equal(calls.length, 35); assert.equal(calls[0], 'destroy-tray'); assert.equal(calls[calls.length - 1], 'stop-discord-presence'); @@ -63,7 +65,7 @@ test('on will quit cleanup handler runs all cleanup steps', () => { assert.ok(calls.indexOf('flush-mpv-log') < calls.indexOf('destroy-socket')); }); -test('on will quit cleanup handler cleans jellyfin subtitle cache when stopping remote session fails', () => { +test('on will quit cleanup handler cleans jellyfin subtitle cache when stopping remote session fails', async () => { const calls: string[] = []; const cleanup = createOnWillQuitCleanupHandler({ destroyTray: () => {}, @@ -106,7 +108,7 @@ test('on will quit cleanup handler cleans jellyfin subtitle cache when stopping stopDiscordPresenceService: () => calls.push('stop-discord-presence'), }); - assert.throws(() => cleanup(), /stop failed/); + await assert.rejects(cleanup(), /stop failed/); assert.deepEqual(calls, [ 'stop-jellyfin-remote', 'cleanup-jellyfin-subtitles', diff --git a/src/main/runtime/app-lifecycle-actions.ts b/src/main/runtime/app-lifecycle-actions.ts index c0b77a0a..a8080ad6 100644 --- a/src/main/runtime/app-lifecycle-actions.ts +++ b/src/main/runtime/app-lifecycle-actions.ts @@ -18,7 +18,7 @@ export function createOnWillQuitCleanupHandler(deps: { destroyMpvSocket: () => void; clearReconnectTimer: () => void; destroySubtitleTimingTracker: () => void; - destroyImmersionTracker: () => void; + destroyImmersionTracker: () => void | Promise; destroyAnkiIntegration: () => void; destroyAnilistSetupWindow: () => void; clearAnilistSetupWindow: () => void; @@ -35,7 +35,7 @@ export function createOnWillQuitCleanupHandler(deps: { cleanupJellyfinSubtitleCache: () => void; stopDiscordPresenceService: () => void; }) { - return (): Promise => { + return async (): Promise => { deps.destroyTray(); deps.stopConfigHotReload(); deps.restorePreviousSecondarySubVisibility(); @@ -55,7 +55,7 @@ export function createOnWillQuitCleanupHandler(deps: { deps.destroyMpvSocket(); deps.clearReconnectTimer(); deps.destroySubtitleTimingTracker(); - deps.destroyImmersionTracker(); + await deps.destroyImmersionTracker(); deps.destroyAnkiIntegration(); deps.destroyAnilistSetupWindow(); deps.clearAnilistSetupWindow(); @@ -77,7 +77,7 @@ export function createOnWillQuitCleanupHandler(deps: { deps.cleanupYoutubeSubtitleTempDirs(); deps.cleanupYoutubeMediaCache(); deps.stopDiscordPresenceService(); - return Promise.resolve(stopSyncAutoScheduler); + await stopSyncAutoScheduler; }; } diff --git a/src/main/runtime/app-lifecycle-main-cleanup.test.ts b/src/main/runtime/app-lifecycle-main-cleanup.test.ts index f12df1e7..d926248d 100644 --- a/src/main/runtime/app-lifecycle-main-cleanup.test.ts +++ b/src/main/runtime/app-lifecycle-main-cleanup.test.ts @@ -3,7 +3,7 @@ import test from 'node:test'; import { createBuildOnWillQuitCleanupDepsHandler } from './app-lifecycle-main-cleanup'; import { createOnWillQuitCleanupHandler } from './app-lifecycle-actions'; -test('cleanup deps builder returns handlers that guard optional runtime objects', () => { +test('cleanup deps builder returns handlers that guard optional runtime objects', async () => { const calls: string[] = []; let reconnectTimer: ReturnType | null = setTimeout(() => {}, 60_000); let immersionTracker: { destroy: () => void } | null = { @@ -80,7 +80,7 @@ test('cleanup deps builder returns handlers that guard optional runtime objects' }); const cleanup = createOnWillQuitCleanupHandler(depsFactory()); - cleanup(); + await cleanup(); assert.ok(calls.includes('destroy-tray')); assert.ok(calls.includes('destroy-main-overlay-window')); diff --git a/src/main/runtime/app-lifecycle-main-cleanup.ts b/src/main/runtime/app-lifecycle-main-cleanup.ts index 249d1618..b539c6f2 100644 --- a/src/main/runtime/app-lifecycle-main-cleanup.ts +++ b/src/main/runtime/app-lifecycle-main-cleanup.ts @@ -119,10 +119,10 @@ export function createBuildOnWillQuitCleanupDepsHandler(deps: { destroySubtitleTimingTracker: () => { deps.getSubtitleTimingTracker()?.destroy(); }, - destroyImmersionTracker: () => { + destroyImmersionTracker: async () => { const tracker = deps.getImmersionTracker(); if (!tracker) return; - tracker.destroy(); + await tracker.destroy(); deps.clearImmersionTracker(); }, destroyAnkiIntegration: () => { diff --git a/src/main/runtime/composers/startup-lifecycle-composer.ts b/src/main/runtime/composers/startup-lifecycle-composer.ts index 07a15b8d..e8e95059 100644 --- a/src/main/runtime/composers/startup-lifecycle-composer.ts +++ b/src/main/runtime/composers/startup-lifecycle-composer.ts @@ -32,7 +32,7 @@ export type StartupLifecycleComposerOptions = ComposerInputs<{ export type StartupLifecycleComposerResult = ComposerOutputs<{ registerProtocolUrlHandlers: () => void; - onWillQuitCleanup: () => void; + onWillQuitCleanup: () => Promise; shouldRestoreWindowsOnActivate: () => boolean; restoreWindowsOnActivate: () => void; }>; diff --git a/src/types/config.ts b/src/types/config.ts index a8f2a111..adb8d036 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -324,6 +324,7 @@ export interface ResolvedConfig { extensionsDir: string; repos: string[]; preferredQuality: string; + bridgeDir: string; }; jimaku: JimakuConfig & { apiBaseUrl: string;