diff --git a/packages/timeline-state-resolver/src/__mocks__/net.ts b/packages/timeline-state-resolver/src/__mocks__/net.ts index cd31e5f89..00f94f8ea 100644 --- a/packages/timeline-state-resolver/src/__mocks__/net.ts +++ b/packages/timeline-state-resolver/src/__mocks__/net.ts @@ -67,7 +67,7 @@ export class Socket extends EventEmitter { public mockClose() { this.setClosed() } - public mockData(data: Buffer) { + public mockData(data: string) { this.emit('data', data) } diff --git a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts index 9e919c271..a071ae971 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vMixResponseStreamReader.spec.ts @@ -1,13 +1,20 @@ import { VMixResponseStreamReader } from '../vMixResponseStreamReader' describe('VMixResponseStreamReader', () => { + test('the helper uses byte length of strings', () => { + // this is a meta-test for the helper used in unit tests below, to assert that the data length is in bytes (utf-8), not characters + expect(makeXmlMessage('abc')).toBe('XML 18\r\nabc\r\n') + expect(makeXmlMessage('abc¾')).toBe('XML 20\r\nabc¾\r\n') + expect(makeXmlMessage('abc🚀🚀')).toBe('XML 26\r\nabc🚀🚀\r\n') + }) + it('processes a complete message', async () => { const reader = new VMixResponseStreamReader() const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\n') expect(onMessage).toHaveBeenCalledTimes(1) expect(onMessage).toHaveBeenCalledWith( @@ -24,8 +31,8 @@ describe('VMixResponseStreamReader', () => { const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n')) - reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\n') + reader.processIncomingData('FUNCTION OK Take\r\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -51,8 +58,8 @@ describe('VMixResponseStreamReader', () => { const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION O')) - reader.processIncomingData(Buffer.from('K 27.0.0.49\r\n')) + reader.processIncomingData('VERSION O') + reader.processIncomingData('K 27.0.0.49\r\n') expect(onMessage).toHaveBeenCalledTimes(1) expect(onMessage).toHaveBeenCalledWith( @@ -69,9 +76,9 @@ describe('VMixResponseStreamReader', () => { const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n')) - reader.processIncomingData(Buffer.from('FUNCTION')) - reader.processIncomingData(Buffer.from(' ER Error message\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\n') + reader.processIncomingData('FUNCTION') + reader.processIncomingData(' ER Error message\r\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -97,11 +104,11 @@ describe('VMixResponseStreamReader', () => { const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49')) - reader.processIncomingData(Buffer.from('\r\n')) - reader.processIncomingData(Buffer.from('FUNCTION')) - reader.processIncomingData(Buffer.from(' ER Error message\r')) - reader.processIncomingData(Buffer.from('\n')) + reader.processIncomingData('VERSION OK 27.0.0.49') + reader.processIncomingData('\r\n') + reader.processIncomingData('FUNCTION') + reader.processIncomingData(' ER Error message\r') + reader.processIncomingData('\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -127,7 +134,7 @@ describe('VMixResponseStreamReader', () => { const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\nFUNCTION ER Error message\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\nFUNCTION ER Error message\r\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -153,8 +160,8 @@ describe('VMixResponseStreamReader', () => { const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\nFUNCTION E')) - reader.processIncomingData(Buffer.from('R Error message\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\nFUNCTION E') + reader.processIncomingData('R Error message\r\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -180,9 +187,9 @@ describe('VMixResponseStreamReader', () => { const onMessage = jest.fn() reader.on('response', onMessage) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\nFUNCTION E')) - reader.processIncomingData(Buffer.from('R Error message\r\nFUNCTION OK T')) - reader.processIncomingData(Buffer.from('ake\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\nFUNCTION E') + reader.processIncomingData('R Error message\r\nFUNCTION OK T') + reader.processIncomingData('ake\r\n') expect(onMessage).toHaveBeenCalledTimes(3) expect(onMessage).toHaveBeenNthCalledWith( @@ -218,7 +225,28 @@ describe('VMixResponseStreamReader', () => { const xmlString = '27.0.0.49HDC:\\preset.vmix' - reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString))) + reader.processIncomingData(makeXmlMessage(xmlString)) + + expect(onMessage).toHaveBeenCalledTimes(1) + expect(onMessage).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + command: 'XML', + response: 'OK', + body: xmlString, + }) + ) + }) + + it('processes a message with data containing multi-byte characters', async () => { + const reader = new VMixResponseStreamReader() + + const onMessage = jest.fn() + reader.on('response', onMessage) + + const xmlString = + '27.0.0.49HDC:\\🚀\\preset3¾.vmix' + reader.processIncomingData(makeXmlMessage(xmlString)) expect(onMessage).toHaveBeenCalledTimes(1) expect(onMessage).toHaveBeenNthCalledWith( @@ -241,7 +269,7 @@ describe('VMixResponseStreamReader', () => { const xmlString = '\r\n27.0.0.49\r\nHD\r\nC:\\preset.vmix\r\n\r\n' - reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString))) + reader.processIncomingData(makeXmlMessage(xmlString)) expect(onMessage).toHaveBeenCalledTimes(1) expect(onMessage).toHaveBeenNthCalledWith( @@ -265,7 +293,7 @@ describe('VMixResponseStreamReader', () => { const xmlMessage = makeXmlMessage(xmlString) splitAtIndices(xmlMessage, [2, 10, 25, 40]).forEach((fragment) => { expect(fragment.length).toBeGreaterThan(0) - reader.processIncomingData(Buffer.from(fragment)) + reader.processIncomingData(fragment) }) expect(onMessage).toHaveBeenCalledTimes(1) @@ -289,7 +317,7 @@ describe('VMixResponseStreamReader', () => { '27.0.0.49HDC:\\preset.vmix' const xmlString2 = '25.0.0.14K' - reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString) + makeXmlMessage(xmlString2))) + reader.processIncomingData(makeXmlMessage(xmlString) + makeXmlMessage(xmlString2)) expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -320,8 +348,8 @@ describe('VMixResponseStreamReader', () => { '27.0.0.49HDC:\\preset.vmix' const xmlString2 = '25.0.0.14K' - reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString))) - reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString2))) + reader.processIncomingData(makeXmlMessage(xmlString)) + reader.processIncomingData(makeXmlMessage(xmlString2)) expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -352,9 +380,9 @@ describe('VMixResponseStreamReader', () => { '27.0.0.49HDC:\\preset.vmix' const xmlString2 = '25.0.0.14K' - reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString).substring(0, 44))) + reader.processIncomingData(makeXmlMessage(xmlString).substring(0, 44)) reader.reset() - reader.processIncomingData(Buffer.from(makeXmlMessage(xmlString2))) + reader.processIncomingData(makeXmlMessage(xmlString2)) expect(onMessage).toHaveBeenCalledTimes(1) expect(onMessage).toHaveBeenNthCalledWith( @@ -377,8 +405,8 @@ describe('VMixResponseStreamReader', () => { const onError = jest.fn() reader.on('error', onError) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n')) - reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\n') + reader.processIncomingData('FUNCTION OK Take\r\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -408,9 +436,9 @@ describe('VMixResponseStreamReader', () => { const onError = jest.fn() reader.on('error', onError) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n')) - reader.processIncomingData(Buffer.from('\r\n')) - reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\n') + reader.processIncomingData('\r\n') + reader.processIncomingData('FUNCTION OK Take\r\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -440,9 +468,9 @@ describe('VMixResponseStreamReader', () => { const onError = jest.fn() reader.on('error', onError) - reader.processIncomingData(Buffer.from('VERSION OK 27.0.0.49\r\n')) - reader.processIncomingData(Buffer.from('WASSUP\r\n')) - reader.processIncomingData(Buffer.from('FUNCTION OK Take\r\n')) + reader.processIncomingData('VERSION OK 27.0.0.49\r\n') + reader.processIncomingData('WASSUP\r\n') + reader.processIncomingData('FUNCTION OK Take\r\n') expect(onMessage).toHaveBeenCalledTimes(2) expect(onMessage).toHaveBeenNthCalledWith( @@ -466,7 +494,8 @@ describe('VMixResponseStreamReader', () => { }) function makeXmlMessage(xmlString: string): string { - return `XML ${xmlString.length + 2}\r\n${xmlString}\r\n` + // the length of the data is in bytes, not characters! + return `XML ${Buffer.byteLength(xmlString, 'utf-8') + 2}\r\n${xmlString}\r\n` } function splitAtIndices(text: string, indices: number[]) { diff --git a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts index f298246f6..1779c685b 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/__tests__/vmixMock.ts @@ -157,9 +157,8 @@ function buildResponse(command: string, state?: 'OK' | 'ER', dataOrMessage?: str // send every item in the array in a separate `data` event/packet function sendData(socket: net.Socket, response: string[]) { for (const packet of response) { - const dataBuf = Buffer.from(packet, 'utf-8') orgSetImmediate(() => { - socket.mockData(dataBuf) + socket.mockData(packet) }) } } diff --git a/packages/timeline-state-resolver/src/integrations/vmix/connection.ts b/packages/timeline-state-resolver/src/integrations/vmix/connection.ts index 4efae0d50..aaa3e8fbf 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/connection.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/connection.ts @@ -114,6 +114,12 @@ export class BaseConnection extends EventEmitter { this._socket.setEncoding('utf-8') this._socket.on('data', (data) => { + if (typeof data !== 'string') { + // this is against the types, but according to the docs the data will be a string + // the problem of a character split into chunks in transit should be taken care of + // (https://nodejs.org/docs/latest-v12.x/api/stream.html#stream_readable_setencoding_encoding) + throw new Error('Received a non-string even though encoding should have been set to utf-8') + } this._responseStreamReader.processIncomingData(data) }) this._socket.on('connect', () => { diff --git a/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts b/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts index 7657f82b0..0f3fbfbaa 100644 --- a/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts +++ b/packages/timeline-state-resolver/src/integrations/vmix/vMixResponseStreamReader.ts @@ -26,17 +26,18 @@ export class VMixResponseStreamReader extends EventEmitter