Skip to content
Closed
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
199 changes: 199 additions & 0 deletions transport/EmbeddedTransportBackend.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
/**
* Unit tests for EmbeddedTransportBackend.
* Tests message delivery, disposal, and Transport integration.
*/

import { expect } from '@esm-bundle/chai';
import EmbeddedTransportBackend from './EmbeddedTransportBackend';

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

beforeEach(() => {
[ backendA, backendB ] = EmbeddedTransportBackend.createPair();
});

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

describe('createPair', () => {
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();
});
});
});
81 changes: 81 additions & 0 deletions transport/EmbeddedTransportBackend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { ITransportBackend } from './types';

/**
* Implements message transport using direct function calls instead of the
* postMessage API.
*/
export default class EmbeddedTransportBackend implements ITransportBackend {
/**
* The other end of this transport pair.
*/
private _otherEnd: EmbeddedTransportBackend | null;

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

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

/**
* Disposes the allocated resources.
*
* @returns {void}
*/
dispose(): void {
if (this._otherEnd) {
this._otherEnd._otherEnd = null;
this._otherEnd = null;
}

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

/**
* Sends the passed message.
*
* @param {Object} message - The message to be sent.
* @param {Transferable[]} [_transfer] - Ignored; included to satisfy ITransportBackend.
* @returns {void}
*/
send(message: any, _transfer?: Transferable[]): void {
this._otherEnd?._receiveCallback(message);
}

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

/**
* Creates a linked pair of {@link EmbeddedTransportBackend} instances wired
* to each other.
*
* @returns {[EmbeddedTransportBackend, EmbeddedTransportBackend]}
*/
static createPair(): [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 } from './EmbeddedTransportBackend';
export { default as MessageChannelTransportBackend } from './MessageChannelTransportBackend';
export type { IMessageChannelTransportBackendOptions } from './MessageChannelTransportBackend';
export { default as PostMessageTransportBackend } from './PostMessageTransportBackend';
Expand Down