Fix mpv protocol/transport typing and test regressions

This commit is contained in:
2026-02-15 17:35:43 -08:00
parent 396fde3011
commit 42b5b6ef89
6 changed files with 1076 additions and 1370 deletions

View File

@@ -172,19 +172,19 @@ test("dispatchMpvProtocolMessage pauses on sub-end when pendingPauseAtSubEnd is
test("splitMpvMessagesFromBuffer parses complete lines and preserves partial buffer", () => {
const parsed = splitMpvMessagesFromBuffer(
"{\"event\":\"shutdown\"}\\n{\"event\":\"property-change\",\"name\":\"media-title\",\"data\":\"x\"}\\n{\"partial\"",
'{"event":"shutdown"}\n{"event":"property-change","name":"media-title","data":"x"}\n{"partial"',
);
assert.equal(parsed.messages.length, 2);
assert.equal(parsed.nextBuffer, "{\"partial\"");
assert.equal(parsed.messages[0].event, "shutdown");
assert.equal(parsed.messages[1].name, "property-change");
assert.equal(parsed.messages[1].name, "media-title");
});
test("splitMpvMessagesFromBuffer reports invalid JSON lines", () => {
const errors: Array<{ line: string; error?: string }> = [];
splitMpvMessagesFromBuffer("{\"event\":\"x\"}\\n{invalid}\\n", undefined, (line, error) => {
splitMpvMessagesFromBuffer('{"event":"x"}\n{invalid}\n', undefined, (line, error) => {
errors.push({ line, error: String(error) });
});

View File

@@ -138,9 +138,10 @@ export async function dispatchMpvProtocolMessage(
end: deps.getCurrentSubEnd(),
});
} else if (msg.name === "sub-end") {
deps.setCurrentSubEnd((msg.data as number) || 0);
if (deps.getPendingPauseAtSubEnd() && deps.getCurrentSubEnd() > 0) {
deps.setPauseAtTime(deps.getCurrentSubEnd());
const subEnd = (msg.data as number) || 0;
deps.setCurrentSubEnd(subEnd);
if (deps.getPendingPauseAtSubEnd() && subEnd > 0) {
deps.setPauseAtTime(subEnd);
deps.setPendingPauseAtSubEnd(false);
deps.sendCommand({ command: ["set_property", "pause", false] });
}

View File

@@ -32,16 +32,6 @@ class FakeSocket extends EventEmitter {
}
}
function withSocketMock<T>(fn: () => T): T {
const OriginalSocket = net.Socket;
(net as any).Socket = FakeSocket as any;
try {
return fn();
} finally {
(net as any).Socket = OriginalSocket;
}
}
const wait = () => new Promise((resolve) => setTimeout(resolve, 0));
test("getMpvReconnectDelay follows existing reconnect ramp", () => {
@@ -104,140 +94,134 @@ test("scheduleMpvReconnect clears existing timer and increments attempt", () =>
test("MpvSocketTransport connects and sends payloads over a live socket", async () => {
const events: string[] = [];
await withSocketMock(async () => {
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
events.push("connect");
},
onData: () => {
events.push("data");
},
onError: () => {
events.push("error");
},
onClose: () => {
events.push("close");
},
});
const payload: MpvSocketMessagePayload = {
command: ["sub-seek", 1],
request_id: 1,
};
assert.equal(transport.send(payload), false);
transport.connect();
await wait();
assert.equal(events.includes("connect"), true);
assert.equal(transport.send(payload), true);
const fakeSocket = transport.getSocket() as unknown as FakeSocket;
assert.equal(fakeSocket.connectedPaths.at(0), "/tmp/mpv.sock");
assert.equal(fakeSocket.writePayloads.length, 1);
assert.equal(fakeSocket.writePayloads.at(0), `${JSON.stringify(payload)}\n`);
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
events.push("connect");
},
onData: () => {
events.push("data");
},
onError: () => {
events.push("error");
},
onClose: () => {
events.push("close");
},
socketFactory: () => new FakeSocket() as unknown as net.Socket,
});
const payload: MpvSocketMessagePayload = {
command: ["sub-seek", 1],
request_id: 1,
};
assert.equal(transport.send(payload), false);
transport.connect();
await wait();
assert.equal(events.includes("connect"), true);
assert.equal(transport.send(payload), true);
const fakeSocket = transport.getSocket() as unknown as FakeSocket;
assert.equal(fakeSocket.connectedPaths.at(0), "/tmp/mpv.sock");
assert.equal(fakeSocket.writePayloads.length, 1);
assert.equal(fakeSocket.writePayloads.at(0), `${JSON.stringify(payload)}\n`);
});
test("MpvSocketTransport reports lifecycle transitions and callback order", async () => {
const events: string[] = [];
const fakeError = new Error("boom");
await withSocketMock(async () => {
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
events.push("connect");
},
onData: () => {
events.push("data");
},
onError: () => {
events.push("error");
},
onClose: () => {
events.push("close");
},
});
transport.connect();
await wait();
const socket = transport.getSocket() as unknown as FakeSocket;
socket.emit("error", fakeError);
socket.emit("data", Buffer.from("{}"));
socket.destroy();
await wait();
assert.equal(events.includes("connect"), true);
assert.equal(events.includes("data"), true);
assert.equal(events.includes("error"), true);
assert.equal(events.includes("close"), true);
assert.equal(transport.isConnected, false);
assert.equal(transport.isConnecting, false);
assert.equal(socket.destroyed, true);
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
events.push("connect");
},
onData: () => {
events.push("data");
},
onError: () => {
events.push("error");
},
onClose: () => {
events.push("close");
},
socketFactory: () => new FakeSocket() as unknown as net.Socket,
});
transport.connect();
await wait();
const socket = transport.getSocket() as unknown as FakeSocket;
socket.emit("error", fakeError);
socket.emit("data", Buffer.from("{}"));
socket.destroy();
await wait();
assert.equal(events.includes("connect"), true);
assert.equal(events.includes("data"), true);
assert.equal(events.includes("error"), true);
assert.equal(events.includes("close"), true);
assert.equal(transport.isConnected, false);
assert.equal(transport.isConnecting, false);
assert.equal(socket.destroyed, true);
});
test("MpvSocketTransport ignores connect requests while already connecting or connected", async () => {
const events: string[] = [];
await withSocketMock(async () => {
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
events.push("connect");
},
onData: () => {
events.push("data");
},
onError: () => {
events.push("error");
},
onClose: () => {
events.push("close");
},
});
transport.connect();
transport.connect();
await wait();
assert.equal(events.includes("connect"), true);
const socket = transport.getSocket() as unknown as FakeSocket;
socket.emit("close");
await wait();
transport.connect();
await wait();
assert.equal(events.filter((entry) => entry === "connect").length, 2);
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
events.push("connect");
},
onData: () => {
events.push("data");
},
onError: () => {
events.push("error");
},
onClose: () => {
events.push("close");
},
socketFactory: () => new FakeSocket() as unknown as net.Socket,
});
transport.connect();
transport.connect();
await wait();
assert.equal(events.includes("connect"), true);
const socket = transport.getSocket() as unknown as FakeSocket;
socket.emit("close");
await wait();
transport.connect();
await wait();
assert.equal(events.filter((entry) => entry === "connect").length, 2);
});
test("MpvSocketTransport.shutdown clears socket and lifecycle flags", async () => {
await withSocketMock(async () => {
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
},
onData: () => {
},
onError: () => {
},
onClose: () => {
},
});
transport.connect();
await wait();
assert.equal(transport.isConnected, true);
transport.shutdown();
assert.equal(transport.isConnected, false);
assert.equal(transport.isConnecting, false);
assert.equal(transport.getSocket(), null);
const transport = new MpvSocketTransport({
socketPath: "/tmp/mpv.sock",
onConnect: () => {
},
onData: () => {
},
onError: () => {
},
onClose: () => {
},
socketFactory: () => new FakeSocket() as unknown as net.Socket,
});
transport.connect();
await wait();
assert.equal(transport.isConnected, true);
transport.shutdown();
assert.equal(transport.isConnected, false);
assert.equal(transport.isConnecting, false);
assert.equal(transport.getSocket(), null);
});

View File

@@ -73,11 +73,13 @@ export interface MpvSocketTransportOptions {
onData: (data: Buffer) => void;
onError: (error: Error) => void;
onClose: () => void;
socketFactory?: () => net.Socket;
}
export class MpvSocketTransport {
private socketPath: string;
private readonly callbacks: MpvSocketTransportEvents;
private readonly socketFactory: () => net.Socket;
private socketRef: net.Socket | null = null;
public socket: net.Socket | null = null;
public connected = false;
@@ -85,6 +87,7 @@ export class MpvSocketTransport {
constructor(options: MpvSocketTransportOptions) {
this.socketPath = options.socketPath;
this.socketFactory = options.socketFactory ?? (() => new net.Socket());
this.callbacks = {
onConnect: options.onConnect,
onData: options.onData,
@@ -107,7 +110,7 @@ export class MpvSocketTransport {
}
this.connecting = true;
this.socketRef = new net.Socket();
this.socketRef = this.socketFactory();
this.socket = this.socketRef;
this.socketRef.on("connect", () => {