fix(anime): harden stream proxy and session lifecycle

- Reject non-origin-form proxy targets
- Release stale anime browser sessions when senders move
This commit is contained in:
2026-08-16 01:46:16 -07:00
parent 4300517da7
commit f6993ae507
9 changed files with 495 additions and 370 deletions
@@ -9,6 +9,7 @@ import {
type AnimeBrowserIpcSender,
} from './anime-browser-ipc-handlers';
import { createAnimeBrowserRuntime, type AnimeBrowserRuntimeDeps } from './anime-browser-runtime';
import { createAnimeBrowserSessionRegistry } from './anime-browser-sessions';
import { createOpenConfigSettingsWindowHandler } from './config-settings-window';
import { createCreateAnimeBrowserWindowHandler } from './setup-window-factory';
import {
@@ -68,7 +69,6 @@ export interface AnimeBrowserApplicationRuntime {
export function createAnimeBrowserApplicationRuntime(
deps: AnimeBrowserApplicationRuntimeDeps,
): AnimeBrowserApplicationRuntime {
const sessions = new Map<string, AnimeBrowserIpcSender>();
let playbackState: AnimeBrowserPlaybackState | null = toAnimeBrowserPlaybackState(
deps.getInitialPlaybackMetadata(),
);
@@ -114,20 +114,15 @@ export function createAnimeBrowserApplicationRuntime(
},
onQueueState: (state) => broadcast(IPC_CHANNELS.event.animeBrowserQueueState, state),
});
const sessions = createAnimeBrowserSessionRegistry((sessionId) =>
runtime.releaseSession(sessionId),
);
registerAnimeBrowserIpcHandlers({
ipcMain: deps.ipcMain,
runtime,
getPlaybackState: () => playbackState,
registerSession: (sessionId, sender) => {
if (sessions.get(sessionId) === sender) return;
sessions.set(sessionId, sender);
sender.once('destroyed', () => {
if (sessions.get(sessionId) !== sender) return;
sessions.delete(sessionId);
runtime.releaseSession(sessionId);
});
},
registerSession: sessions.register,
});
let dockIconRetained = false;
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { AnimeBrowserIpcSender } from './anime-browser-ipc-handlers';
import { createAnimeBrowserSessionRegistry } from './anime-browser-sessions';
test('moving a live sender to a new anime browser session releases its stale session', () => {
const destroyedListeners: Array<() => void> = [];
const sender: AnimeBrowserIpcSender = {
send: () => {},
isDestroyed: () => false,
once: (_event, listener) => destroyedListeners.push(listener),
};
const released: string[] = [];
const sessions = createAnimeBrowserSessionRegistry((sessionId) => released.push(sessionId));
sessions.register('old', sender);
sessions.register('old', sender);
assert.equal(destroyedListeners.length, 1, 'duplicate registration keeps its existing handler');
sessions.register('new', sender);
assert.deepEqual(released, ['old']);
assert.equal(sessions.get('old'), undefined);
assert.equal(sessions.get('new'), sender);
for (const listener of destroyedListeners) listener();
assert.deepEqual(released, ['old', 'new']);
assert.equal(sessions.get('new'), undefined);
});
@@ -0,0 +1,34 @@
import type { AnimeBrowserIpcSender } from './anime-browser-ipc-handlers';
export interface AnimeBrowserSessionRegistry {
get: (sessionId: string) => AnimeBrowserIpcSender | undefined;
register: (sessionId: string, sender: AnimeBrowserIpcSender) => void;
values: () => IterableIterator<AnimeBrowserIpcSender>;
}
export function createAnimeBrowserSessionRegistry(
releaseSession: (sessionId: string) => void,
): AnimeBrowserSessionRegistry {
const sessions = new Map<string, AnimeBrowserIpcSender>();
const register = (sessionId: string, sender: AnimeBrowserIpcSender): void => {
if (sessions.get(sessionId) === sender) return;
for (const [previousSessionId, registeredSender] of sessions) {
if (previousSessionId === sessionId || registeredSender !== sender) continue;
sessions.delete(previousSessionId);
releaseSession(previousSessionId);
}
sessions.set(sessionId, sender);
sender.once('destroyed', () => {
if (sessions.get(sessionId) !== sender) return;
sessions.delete(sessionId);
releaseSession(sessionId);
});
};
return {
get: (sessionId) => sessions.get(sessionId),
register,
values: () => sessions.values(),
};
}