From 3124599fe75ed1a1c38e74c074d949e398922315 Mon Sep 17 00:00:00 2001 From: mishraditi Date: Wed, 20 May 2026 16:20:12 +0530 Subject: [PATCH 1/3] feat(transport): add EmbeddedTransportBackend for direct in-page messaging --- transport/EmbeddedTransportBackend.ts | 80 +++++++++++++++++++++++++++ transport/index.ts | 1 + 2 files changed, 81 insertions(+) create mode 100644 transport/EmbeddedTransportBackend.ts diff --git a/transport/EmbeddedTransportBackend.ts b/transport/EmbeddedTransportBackend.ts new file mode 100644 index 0000000..a52d310 --- /dev/null +++ b/transport/EmbeddedTransportBackend.ts @@ -0,0 +1,80 @@ +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. + * @returns {void} + */ + send(message: any): 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 ]; + } +} diff --git a/transport/index.ts b/transport/index.ts index 1ee0df5..3a29e8f 100644 --- a/transport/index.ts +++ b/transport/index.ts @@ -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'; From 44ff6924cb2eb6b9439770cc3eb04c4e7b312932 Mon Sep 17 00:00:00 2001 From: mishraditi Date: Wed, 20 May 2026 16:21:10 +0530 Subject: [PATCH 2/3] test(transport): add unit tests for EmbeddedTransportBackend --- transport/EmbeddedTransportBackend.spec.ts | 199 +++++++++++++++++++++ 1 file changed, 199 insertions(+) create mode 100644 transport/EmbeddedTransportBackend.spec.ts diff --git a/transport/EmbeddedTransportBackend.spec.ts b/transport/EmbeddedTransportBackend.spec.ts new file mode 100644 index 0000000..dadb9fa --- /dev/null +++ b/transport/EmbeddedTransportBackend.spec.ts @@ -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(); + }); + }); +}); From fb76f254e2f86002e7dc5dab51820497aa3bfb0a Mon Sep 17 00:00:00 2001 From: mishraditi Date: Thu, 28 May 2026 15:20:54 +0530 Subject: [PATCH 3/3] fix(transport): update send() signature to match ITransportBackend interface --- transport/EmbeddedTransportBackend.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/transport/EmbeddedTransportBackend.ts b/transport/EmbeddedTransportBackend.ts index a52d310..e45eefe 100644 --- a/transport/EmbeddedTransportBackend.ts +++ b/transport/EmbeddedTransportBackend.ts @@ -46,9 +46,10 @@ export default class EmbeddedTransportBackend implements ITransportBackend { * 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): void { + send(message: any, _transfer?: Transferable[]): void { this._otherEnd?._receiveCallback(message); }