Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
196 changes: 196 additions & 0 deletions transport/EmbeddedTransportBackend.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import { expect } from '@esm-bundle/chai';
import EmbeddedTransportBackend, {
createEmbeddedTransportPair
} from './EmbeddedTransportBackend';

describe('EmbeddedTransportBackend', () => {
let backendA: EmbeddedTransportBackend;
let backendB: EmbeddedTransportBackend;

beforeEach(() => {
[ backendA, backendB ] = createEmbeddedTransportPair();
});

afterEach(() => {
backendA.dispose();
backendB.dispose();
});

describe('createEmbeddedTransportPair', () => {
it('should return two EmbeddedTransportBackend instances', () => {
expect(backendA).to.be.instanceOf(EmbeddedTransportBackend);
expect(backendB).to.be.instanceOf(EmbeddedTransportBackend);
});

it('should return two distinct instances', () => {
expect(backendA).to.not.equal(backendB);
});
});

describe('send', () => {
it('should deliver message from A to B', () => {
const received: any[] = [];

backendB.setReceiveCallback(message => received.push(message));

backendA.send({ type: 'event', data: 'hello' });

expect(received).to.have.lengthOf(1);
expect(received[0]).to.deep.equal({ type: 'event', data: 'hello' });
});

it('should deliver message from B to A', () => {
const received: any[] = [];

backendA.setReceiveCallback(message => received.push(message));

backendB.send({ type: 'event', data: 'world' });

expect(received).to.have.lengthOf(1);
expect(received[0]).to.deep.equal({ type: 'event', data: 'world' });
});

it('should deliver multiple messages in order', () => {
const received: any[] = [];

backendB.setReceiveCallback(message => received.push(message));

backendA.send({ id: 1 });
backendA.send({ id: 2 });
backendA.send({ id: 3 });

expect(received).to.have.lengthOf(3);
expect(received[0]).to.deep.equal({ id: 1 });
expect(received[1]).to.deep.equal({ id: 2 });
expect(received[2]).to.deep.equal({ id: 3 });
});

it('should not throw when no receive callback is set', () => {
expect(() => backendA.send({ data: 'test' })).to.not.throw();
});

it('should support bidirectional communication', () => {
const receivedByA: any[] = [];
const receivedByB: any[] = [];

backendA.setReceiveCallback(message => receivedByA.push(message));
backendB.setReceiveCallback(message => receivedByB.push(message));

backendA.send({ from: 'A' });
backendB.send({ from: 'B' });

expect(receivedByB).to.have.lengthOf(1);
expect(receivedByB[0]).to.deep.equal({ from: 'A' });
expect(receivedByA).to.have.lengthOf(1);
expect(receivedByA[0]).to.deep.equal({ from: 'B' });
});
});

describe('setReceiveCallback', () => {
it('should replace the previous callback', () => {
const first: any[] = [];
const second: any[] = [];

backendB.setReceiveCallback(message => first.push(message));
backendA.send({ id: 1 });

backendB.setReceiveCallback(message => second.push(message));
backendA.send({ id: 2 });

expect(first).to.have.lengthOf(1);
expect(second).to.have.lengthOf(1);
});
});

describe('dispose', () => {
it('should stop delivering messages after dispose', () => {
const received: any[] = [];

backendB.setReceiveCallback(message => received.push(message));

backendA.dispose();
backendA.send({ data: 'after dispose' });

expect(received).to.have.lengthOf(0);
});

it('should stop the other end from delivering messages', () => {
const received: any[] = [];

backendA.setReceiveCallback(message => received.push(message));

backendB.dispose();
backendB.send({ data: 'after dispose' });

expect(received).to.have.lengthOf(0);
});

it('should be safe to call dispose twice', () => {
expect(() => {
backendA.dispose();
backendA.dispose();
}).to.not.throw();
});

it('should sever the connection in both directions', () => {
const receivedByA: any[] = [];
const receivedByB: any[] = [];

backendA.setReceiveCallback(message => receivedByA.push(message));
backendB.setReceiveCallback(message => receivedByB.push(message));

backendA.dispose();

backendA.send({ from: 'A' });
backendB.send({ from: 'B' });

expect(receivedByA).to.have.lengthOf(0);
expect(receivedByB).to.have.lengthOf(0);
});
});

describe('integration with Transport', () => {
it('should work as a Transport backend', async () => {
const { default: Transport } = await import('./Transport');

const transportA = new Transport({ backend: backendA });
const transportB = new Transport({ backend: backendB });

const received: any[] = [];

transportB.on('event', (data: any) => {
received.push(data);

return true;
});

transportA.sendEvent({ name: 'test-event', value: 42 });

expect(received).to.have.lengthOf(1);
expect(received[0]).to.deep.equal({ name: 'test-event', value: 42 });

transportA.dispose();
transportB.dispose();
});

it('should support request/response through Transport', async () => {
const { default: Transport } = await import('./Transport');

const transportA = new Transport({ backend: backendA });
const transportB = new Transport({ backend: backendB });

transportB.on('request', (data: any, callback: any) => {
callback(data.x + data.y);

return true;
});

const result = await transportA.sendRequest({ x: 3, y: 4 });

expect(result).to.equal(7);

transportA.dispose();
transportB.dispose();
});
});
});
70 changes: 70 additions & 0 deletions transport/EmbeddedTransportBackend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { ITransportBackend } from './types';

/**
* Implements message transport using direct function calls
* instead of the postMessage API. Designed for embedding Jitsi Meet
* directly into a host page without an iframe.
*/
export default class EmbeddedTransportBackend implements ITransportBackend {
/**
* The other end of this transport pair.
*/
_otherEnd: EmbeddedTransportBackend | null;

/**
* Callback function for receiving messages.
*/
private _receiveCallback: (message: any) => void;

constructor() {
this._otherEnd = null;
this._receiveCallback = () => {
// Do nothing until a callback is set by the consumer
// via setReceiveCallback.
};
}

/**
* Disposes the backend and severs the connection to the other end.
*
* @returns {void}
*/
dispose(): void {
if (this._otherEnd) {
this._otherEnd._otherEnd = null;
this._otherEnd = null;
}

this._receiveCallback = () => {
// no-op
};
}

/**
* Sends a message to the other end of the pair by directly
* invoking its receiver callback.
*/
send(message: any): void {
this._otherEnd?._receiveCallback(message);
}

/**
* Sets the callback for receiving messages from the other end.
*
* @param {Function} callback - The new callback.
* @returns {void}
*/
setReceiveCallback(callback: (message: any) => void): void {
this._receiveCallback = callback;
}
}

export function createEmbeddedTransportPair(): [EmbeddedTransportBackend, EmbeddedTransportBackend] {
const backendA = new EmbeddedTransportBackend();
const backendB = new EmbeddedTransportBackend();

backendA._otherEnd = backendB;
backendB._otherEnd = backendA;

return [ backendA, backendB ];
}
1 change: 1 addition & 0 deletions transport/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { MessageType } from './constants';
export { default as EmbeddedTransportBackend, createEmbeddedTransportPair } from './EmbeddedTransportBackend';
export { default as PostMessageTransportBackend } from './PostMessageTransportBackend';
export { default as Transport } from './Transport';
export type {
Expand Down