From c24726dafa459dd1a5e55bf1eafb76c506f17352 Mon Sep 17 00:00:00 2001 From: wxyz-abcd Date: Wed, 20 Nov 2024 16:31:50 +0300 Subject: [PATCH 1/2] solves #303 --- src/lib/index.ts | 2 +- src/lib/types.ts | 2 +- src/polyfill/RTCDataChannel.ts | 11 +- test/jest-tests/polyfill.test.ts | 171 +++++++++++++++++++++++++++++-- 4 files changed, 174 insertions(+), 12 deletions(-) diff --git a/src/lib/index.ts b/src/lib/index.ts index 958d9546..09d36390 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -105,7 +105,7 @@ export interface DataChannel extends Channel { onClosed(cb: () => void): void; onError(cb: (err: string) => void): void; onBufferedAmountLow(cb: () => void): void; - onMessage(cb: (msg: string | Buffer) => void): void; + onMessage(cb: (msg: string | Buffer | ArrayBuffer) => void): void; } export const DataChannel: { // DataChannel implementation diff --git a/src/lib/types.ts b/src/lib/types.ts index 12c16061..74f3317c 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -10,7 +10,7 @@ export interface Channel { onClosed(cb: () => void): void; onError(cb: (err: string) => void): void; onBufferedAmountLow(cb: () => void): void; - onMessage(cb: (msg: string | Buffer) => void): void; + onMessage(cb: (msg: string | Buffer | ArrayBuffer) => void): void; } export interface WebSocketServerConfiguration { diff --git a/src/polyfill/RTCDataChannel.ts b/src/polyfill/RTCDataChannel.ts index 7515b2ac..6092b13e 100644 --- a/src/polyfill/RTCDataChannel.ts +++ b/src/polyfill/RTCDataChannel.ts @@ -27,7 +27,7 @@ export default class RTCDataChannel extends EventTarget implements globalThis.RT super(); this.#dataChannel = dataChannel; - this.#binaryType = 'arraybuffer'; + this.#binaryType = 'blob'; this.#readyState = this.#dataChannel.isOpen() ? 'open' : 'connecting'; this.#bufferedAmountLowThreshold = 0; this.#maxPacketLifeTime = opts.maxPacketLifeTime || null; @@ -72,9 +72,12 @@ export default class RTCDataChannel extends EventTarget implements globalThis.RT }); this.#dataChannel.onMessage((data) => { - if (ArrayBuffer.isView(data)) { - data = Buffer.from(data.buffer); - } + if (ArrayBuffer.isView(data)) { + if (this.binaryType == 'arraybuffer') + data = data.buffer; + else + data = Buffer.from(data.buffer); + } this.dispatchEvent(new MessageEvent('message', { data })); }); diff --git a/test/jest-tests/polyfill.test.ts b/test/jest-tests/polyfill.test.ts index 9dd7646d..c7597448 100644 --- a/test/jest-tests/polyfill.test.ts +++ b/test/jest-tests/polyfill.test.ts @@ -1,10 +1,169 @@ import { expect } from '@jest/globals'; -import polyfill from '../../src/polyfill/index'; +import { RTCPeerConnection, RTCDataChannel } from '../../src/polyfill/index'; describe('polyfill', () => { - test('generateCertificate should throw', async () => { - await expect(async () => { - await polyfill.RTCPeerConnection.generateCertificate(); - }).rejects.toEqual(new DOMException('Not implemented')); - }); + test('generateCertificate should throw', async () => { + await expect(async () => { + await RTCPeerConnection.generateCertificate(); + }).rejects.toEqual(new DOMException('Not implemented')); + }); + test('P2P Test', () => { + return new Promise((done) => { + // Mocks + const p1ConnectionStateMock = jest.fn(); + const p1IceConnectionStateMock = jest.fn(); + const p1IceGatheringStateMock = jest.fn(); + const p1IceCandidateMock = jest.fn(); + const p1SDPMock = jest.fn(); + const p1DCMock = jest.fn(); + const p1MessageMock = jest.fn(); + const p2ConnectionStateMock = jest.fn(); + const p2IceConnectionStateMock = jest.fn(); + const p2IceGatheringStateMock = jest.fn(); + const p2IceCandidateMock = jest.fn(); + const p2SDPMock = jest.fn(); + const p2DCMock = jest.fn(); + const p2MessageMock = jest.fn(); + + let dc1: RTCDataChannel = null; + let dc2: RTCDataChannel = null; + + function createBinaryTestData(){ + let binaryData = new Uint8Array(17); + let dv = new DataView(binaryData.buffer); + dv.setInt8(0, 123); + dv.setFloat32(1, 123.456); + dv.setUint32(5, 987654321); + dv.setFloat64(9, 789.012); + return binaryData; + } + function analyzeBinaryTestData(binaryData){ + let dv = new DataView(binaryData); + return (dv.getInt8(0)==123 && dv.getFloat32(1)==Math.fround(123.456) && dv.getUint32(5)==987654321 && dv.getFloat64(9)==789.012); + } + const testMessages = [ + { binaryType: 'arraybuffer', data: 'Hello' }, + { binaryType: 'arraybuffer', data: createBinaryTestData() }, + { binaryType: 'blob', data: createBinaryTestData() } + ]; + var currentIndex = -1; + function analyzeData(idx, data){ + switch(idx){ + case 0: + return data==testMessages[idx].data; + case 1: + return analyzeBinaryTestData(data); + case 2: + return analyzeBinaryTestData(data.buffer); + } + return false; + } + function nextSendTest(){ + var current = testMessages[++currentIndex]; + if (!current) + return false; + dc1.binaryType = current.binaryType as BinaryType; + dc2.binaryType = current.binaryType as BinaryType; + dc1.send(current.data); + return true; + } + + const peer1 = new RTCPeerConnection({ + peerIdentity: 'peer1', + iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], + }); + + // Set Callbacks + peer1.onconnectionstatechange = (): void => { + p1ConnectionStateMock(); + }; + peer1.oniceconnectionstatechange = (): void => { + p1IceConnectionStateMock(); + }; + peer1.onicegatheringstatechange = (): void => { + p1IceGatheringStateMock(); + }; + peer1.onicecandidate = (e): void => { + p1IceCandidateMock(); + peer2.addIceCandidate(e.candidate); + }; + + const peer2 = new RTCPeerConnection({ + peerIdentity: 'peer2', + iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], + }); + + // Set Callbacks + peer2.onconnectionstatechange = (): void => { + p2ConnectionStateMock(); + }; + peer2.oniceconnectionstatechange = (): void => { + p2IceConnectionStateMock(); + }; + peer2.onicegatheringstatechange = (): void => { + p2IceGatheringStateMock(); + }; + peer2.onicecandidate = (e): void => { + p2IceCandidateMock(); + peer1.addIceCandidate(e.candidate); + }; + peer2.ondatachannel = (dce): void => { + p2DCMock(); + dc2 = dce.channel; + dc2.onmessage = (msg): void => { + p2MessageMock(msg.data); + dc2.send(msg.data); + }; + }; + + dc1 = peer1.createDataChannel('test-p2p'); + dc1.onopen = (): void => { + p1DCMock(); + nextSendTest(); + }; + dc1.onmessage = (msg): void => { + p1MessageMock(msg.data); + if (!nextSendTest()){ + peer1.close(); + peer2.close(); + expect(p1ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1SDPMock.mock.calls.length).toBe(1); + expect(p2SDPMock.mock.calls.length).toBe(1); + expect(p1DCMock.mock.calls.length).toBe(1); + expect(p2DCMock.mock.calls.length).toBe(1); + expect(p1MessageMock.mock.calls.length).toBe(3); + expect(p2MessageMock.mock.calls.length).toBe(3); + expect(analyzeData(0, p1MessageMock.mock.calls[0][0])).toEqual(true); + expect(analyzeData(0, p2MessageMock.mock.calls[0][0])).toEqual(true); + expect(analyzeData(1, p1MessageMock.mock.calls[1][0])).toEqual(true); + expect(analyzeData(1, p2MessageMock.mock.calls[1][0])).toEqual(true); + expect(analyzeData(2, p1MessageMock.mock.calls[2][0])).toEqual(true); + expect(analyzeData(2, p2MessageMock.mock.calls[2][0])).toEqual(true); + done(); + } + }; + peer1 + .createOffer() + .then((desc) => { + p1SDPMock(); + peer2.setRemoteDescription(desc); + }) + //.catch((err) => console.error(err)); + + peer2 + .createAnswer() + .then((answerDesc) => { + p2SDPMock(); + peer1.setRemoteDescription(answerDesc); + }) + //.catch((err) => console.error('Couldn't create answer', err)); + }); + }); }); From 29ce0ca4e569688c96c4f7d5195df22efcebd1aa Mon Sep 17 00:00:00 2001 From: wxyz-abcd Date: Thu, 21 Nov 2024 03:57:43 +0300 Subject: [PATCH 2/2] fixed lint errors & added comments --- test/jest-tests/polyfill.test.ts | 187 +++++++++++++++++++------------ 1 file changed, 113 insertions(+), 74 deletions(-) diff --git a/test/jest-tests/polyfill.test.ts b/test/jest-tests/polyfill.test.ts index c7597448..0cb78778 100644 --- a/test/jest-tests/polyfill.test.ts +++ b/test/jest-tests/polyfill.test.ts @@ -10,69 +10,133 @@ describe('polyfill', () => { test('P2P Test', () => { return new Promise((done) => { // Mocks - const p1ConnectionStateMock = jest.fn(); - const p1IceConnectionStateMock = jest.fn(); - const p1IceGatheringStateMock = jest.fn(); - const p1IceCandidateMock = jest.fn(); + const p1ConnectionStateMock = jest.fn(); + const p1IceConnectionStateMock = jest.fn(); + const p1IceGatheringStateMock = jest.fn(); + const p1IceCandidateMock = jest.fn(); const p1SDPMock = jest.fn(); - const p1DCMock = jest.fn(); - const p1MessageMock = jest.fn(); - const p2ConnectionStateMock = jest.fn(); - const p2IceConnectionStateMock = jest.fn(); - const p2IceGatheringStateMock = jest.fn(); - const p2IceCandidateMock = jest.fn(); + const p1DCMock = jest.fn(); + const p1MessageMock = jest.fn(); + const p2ConnectionStateMock = jest.fn(); + const p2IceConnectionStateMock = jest.fn(); + const p2IceGatheringStateMock = jest.fn(); + const p2IceCandidateMock = jest.fn(); const p2SDPMock = jest.fn(); - const p2DCMock = jest.fn(); - const p2MessageMock = jest.fn(); + const p2DCMock = jest.fn(); + const p2MessageMock = jest.fn(); + + const peer1 = new RTCPeerConnection({ + peerIdentity: 'peer1', + iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], + }); + + const peer2 = new RTCPeerConnection({ + peerIdentity: 'peer2', + iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], + }); let dc1: RTCDataChannel = null; let dc2: RTCDataChannel = null; - function createBinaryTestData(){ - let binaryData = new Uint8Array(17); - let dv = new DataView(binaryData.buffer); + // Creates a fixed binary data for testing + function createBinaryTestData(): Uint8Array { + const binaryData = new Uint8Array(17); + const dv = new DataView(binaryData.buffer); dv.setInt8(0, 123); dv.setFloat32(1, 123.456); dv.setUint32(5, 987654321); dv.setFloat64(9, 789.012); return binaryData; } - function analyzeBinaryTestData(binaryData){ - let dv = new DataView(binaryData); + + // Compares the received binary data to the expected value of the fixed binary data + function analyzeBinaryTestData(binaryData: ArrayBufferLike): boolean { + const dv = new DataView(binaryData); return (dv.getInt8(0)==123 && dv.getFloat32(1)==Math.fround(123.456) && dv.getUint32(5)==987654321 && dv.getFloat64(9)==789.012); } + + // We will set the "binaryType" and then send/receive the "data" from the datachannel in each test, and then compare them. + // For example, the first line will send a "Hello" string after setting binaryType to "arraybuffer". const testMessages = [ { binaryType: 'arraybuffer', data: 'Hello' }, { binaryType: 'arraybuffer', data: createBinaryTestData() }, { binaryType: 'blob', data: createBinaryTestData() } ]; - var currentIndex = -1; - function analyzeData(idx, data){ + + // Index of the message in testMessages that we are currently testing. + let currentIndex: number = -1; + + // We run this function to analyze the data just after receiving it from the datachannel. + function analyzeData(idx: number, data: string|Buffer|ArrayBuffer): boolean { switch(idx){ - case 0: - return data==testMessages[idx].data; - case 1: - return analyzeBinaryTestData(data); - case 2: - return analyzeBinaryTestData(data.buffer); + case 0: // binaryType is not used here because data is a string ("Hello"). + return (data as string)==testMessages[idx].data; + case 1: // binaryType is "arraybuffer" and data is expected to be an ArrayBuffer. + return analyzeBinaryTestData(data as ArrayBufferLike); + case 2: // binaryType is "blob" and data is expected to be a Buffer. + return analyzeBinaryTestData((data as Buffer).buffer); } return false; } - function nextSendTest(){ - var current = testMessages[++currentIndex]; - if (!current) - return false; + + function finalizeTest(): void { + peer1.close(); + peer2.close(); + + // State Callbacks + expect(p1ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p1IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + + // SDP + expect(p1SDPMock.mock.calls.length).toBe(1); + expect(p2SDPMock.mock.calls.length).toBe(1); + + // Candidates + expect(p1IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + expect(p2IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); + + // DataChannel + expect(p1DCMock.mock.calls.length).toBe(1); + expect(p2DCMock.mock.calls.length).toBe(1); + + expect(p1MessageMock.mock.calls.length).toBe(3); + expect(p2MessageMock.mock.calls.length).toBe(3); + + // Analyze and compare received messages + expect(analyzeData(0, p1MessageMock.mock.calls[0][0])).toEqual(true); + expect(analyzeData(1, p1MessageMock.mock.calls[1][0])).toEqual(true); + expect(analyzeData(2, p1MessageMock.mock.calls[2][0])).toEqual(true); + + expect(analyzeData(0, p2MessageMock.mock.calls[0][0])).toEqual(true); + expect(analyzeData(1, p2MessageMock.mock.calls[1][0])).toEqual(true); + expect(analyzeData(2, p2MessageMock.mock.calls[2][0])).toEqual(true); + + done(); + } + + // starts the next message-sending test + function nextSendTest(): void { + // Get the next test data + const current = testMessages[++currentIndex]; + + // If finished, quit + if (!current){ + finalizeTest(); + return; + } + + // Assign the binaryType value dc1.binaryType = current.binaryType as BinaryType; dc2.binaryType = current.binaryType as BinaryType; + + // Send the test message dc1.send(current.data); - return true; } - const peer1 = new RTCPeerConnection({ - peerIdentity: 'peer1', - iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], - }); - // Set Callbacks peer1.onconnectionstatechange = (): void => { p1ConnectionStateMock(); @@ -88,11 +152,6 @@ describe('polyfill', () => { peer2.addIceCandidate(e.candidate); }; - const peer2 = new RTCPeerConnection({ - peerIdentity: 'peer2', - iceServers: [{ urls: ['stun:stun.l.google.com:19302'] }], - }); - // Set Callbacks peer2.onconnectionstatechange = (): void => { p2ConnectionStateMock(); @@ -112,43 +171,13 @@ describe('polyfill', () => { dc2 = dce.channel; dc2.onmessage = (msg): void => { p2MessageMock(msg.data); + + // send the received message from peer2 back to peer1 dc2.send(msg.data); }; }; - - dc1 = peer1.createDataChannel('test-p2p'); - dc1.onopen = (): void => { - p1DCMock(); - nextSendTest(); - }; - dc1.onmessage = (msg): void => { - p1MessageMock(msg.data); - if (!nextSendTest()){ - peer1.close(); - peer2.close(); - expect(p1ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2ConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p1IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2IceConnectionStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p1IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2IceGatheringStateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p1IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p2IceCandidateMock.mock.calls.length).toBeGreaterThanOrEqual(1); - expect(p1SDPMock.mock.calls.length).toBe(1); - expect(p2SDPMock.mock.calls.length).toBe(1); - expect(p1DCMock.mock.calls.length).toBe(1); - expect(p2DCMock.mock.calls.length).toBe(1); - expect(p1MessageMock.mock.calls.length).toBe(3); - expect(p2MessageMock.mock.calls.length).toBe(3); - expect(analyzeData(0, p1MessageMock.mock.calls[0][0])).toEqual(true); - expect(analyzeData(0, p2MessageMock.mock.calls[0][0])).toEqual(true); - expect(analyzeData(1, p1MessageMock.mock.calls[1][0])).toEqual(true); - expect(analyzeData(1, p2MessageMock.mock.calls[1][0])).toEqual(true); - expect(analyzeData(2, p1MessageMock.mock.calls[2][0])).toEqual(true); - expect(analyzeData(2, p2MessageMock.mock.calls[2][0])).toEqual(true); - done(); - } - }; + + // Actions peer1 .createOffer() .then((desc) => { @@ -164,6 +193,16 @@ describe('polyfill', () => { peer1.setRemoteDescription(answerDesc); }) //.catch((err) => console.error('Couldn't create answer', err)); + + dc1 = peer1.createDataChannel('test-p2p'); + dc1.onopen = (): void => { + p1DCMock(); + nextSendTest(); + }; + dc1.onmessage = (msg): void => { // peer2 sends all messages back to peer1 + p1MessageMock(msg.data); + nextSendTest(); + }; }); }); });