diff --git a/src/inMemory.test.ts b/src/inMemory.test.ts new file mode 100644 index 00000000..f7e9e979 --- /dev/null +++ b/src/inMemory.test.ts @@ -0,0 +1,94 @@ +import { InMemoryTransport } from "./inMemory.js"; +import { JSONRPCMessage } from "./types.js"; + +describe("InMemoryTransport", () => { + let clientTransport: InMemoryTransport; + let serverTransport: InMemoryTransport; + + beforeEach(() => { + [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); + }); + + test("should create linked pair", () => { + expect(clientTransport).toBeDefined(); + expect(serverTransport).toBeDefined(); + }); + + test("should start without error", async () => { + await expect(clientTransport.start()).resolves.not.toThrow(); + await expect(serverTransport.start()).resolves.not.toThrow(); + }); + + test("should send message from client to server", async () => { + const message: JSONRPCMessage = { + jsonrpc: "2.0", + method: "test", + id: 1, + }; + + let receivedMessage: JSONRPCMessage | undefined; + serverTransport.onmessage = (msg) => { + receivedMessage = msg; + }; + + await clientTransport.send(message); + expect(receivedMessage).toEqual(message); + }); + + test("should send message from server to client", async () => { + const message: JSONRPCMessage = { + jsonrpc: "2.0", + method: "test", + id: 1, + }; + + let receivedMessage: JSONRPCMessage | undefined; + clientTransport.onmessage = (msg) => { + receivedMessage = msg; + }; + + await serverTransport.send(message); + expect(receivedMessage).toEqual(message); + }); + + test("should handle close", async () => { + let clientClosed = false; + let serverClosed = false; + + clientTransport.onclose = () => { + clientClosed = true; + }; + + serverTransport.onclose = () => { + serverClosed = true; + }; + + await clientTransport.close(); + expect(clientClosed).toBe(true); + expect(serverClosed).toBe(true); + }); + + test("should throw error when sending after close", async () => { + await clientTransport.close(); + await expect( + clientTransport.send({ jsonrpc: "2.0", method: "test", id: 1 }), + ).rejects.toThrow("Not connected"); + }); + + test("should queue messages sent before start", async () => { + const message: JSONRPCMessage = { + jsonrpc: "2.0", + method: "test", + id: 1, + }; + + let receivedMessage: JSONRPCMessage | undefined; + serverTransport.onmessage = (msg) => { + receivedMessage = msg; + }; + + await clientTransport.send(message); + await serverTransport.start(); + expect(receivedMessage).toEqual(message); + }); +}); diff --git a/src/inMemory.ts b/src/inMemory.ts new file mode 100644 index 00000000..2763f38c --- /dev/null +++ b/src/inMemory.ts @@ -0,0 +1,54 @@ +import { Transport } from "./shared/transport.js"; +import { JSONRPCMessage } from "./types.js"; + +/** + * In-memory transport for creating clients and servers that talk to each other within the same process. + */ +export class InMemoryTransport implements Transport { + private _otherTransport?: InMemoryTransport; + private _messageQueue: JSONRPCMessage[] = []; + + onclose?: () => void; + onerror?: (error: Error) => void; + onmessage?: (message: JSONRPCMessage) => void; + + /** + * Creates a pair of linked in-memory transports that can communicate with each other. One should be passed to a Client and one to a Server. + */ + static createLinkedPair(): [InMemoryTransport, InMemoryTransport] { + const clientTransport = new InMemoryTransport(); + const serverTransport = new InMemoryTransport(); + clientTransport._otherTransport = serverTransport; + serverTransport._otherTransport = clientTransport; + return [clientTransport, serverTransport]; + } + + async start(): Promise { + // Process any messages that were queued before start was called + while (this._messageQueue.length > 0) { + const message = this._messageQueue.shift(); + if (message) { + this.onmessage?.(message); + } + } + } + + async close(): Promise { + const other = this._otherTransport; + this._otherTransport = undefined; + await other?.close(); + this.onclose?.(); + } + + async send(message: JSONRPCMessage): Promise { + if (!this._otherTransport) { + throw new Error("Not connected"); + } + + if (this._otherTransport.onmessage) { + this._otherTransport.onmessage(message); + } else { + this._otherTransport._messageQueue.push(message); + } + } +}