diff --git a/lib/client.js b/lib/client.js index 1c19623..abdcf35 100644 --- a/lib/client.js +++ b/lib/client.js @@ -159,6 +159,9 @@ class Client extends EventEmitter { } } + // Adding a unique request id to the request + data.$req_id = crypto.randomUUID(); + // Capture stacktrace const error = new Error(); diff --git a/lib/client.test.js b/lib/client.test.js index 73c62ae..5e18c52 100644 --- a/lib/client.test.js +++ b/lib/client.test.js @@ -228,7 +228,7 @@ describe('Client', () => { expect(serverRequestStub).toHaveBeenCalledWith( 'get', 'url', - { $data: 'data' }, + { $data: 'data', $req_id: expect.any(String) }, expect.any(Function), ); }); @@ -252,6 +252,7 @@ describe('Client', () => { client: 'id2', }, $session: 'session-id', + $req_id: expect.any(String), }, expect.any(Function), ); @@ -264,7 +265,7 @@ describe('Client', () => { expect(serverRequestStub).toHaveBeenCalledWith( 'get', 'url', - { $data: null }, + { $data: null, $req_id: expect.any(String) }, expect.any(Function), ); }); @@ -319,7 +320,7 @@ describe('Client', () => { expect(respondSpy).toHaveBeenCalledWith( 'get', 'url', - { $client: 'id', $data: 'data', $key: 'key' }, + { $client: 'id', $data: 'data', $key: 'key', $req_id: expect.any(String) }, { $status: 200, $data: 'success' }, expect.any(Function), ); diff --git a/lib/connection.js b/lib/connection.js index 9fc52b9..f564268 100644 --- a/lib/connection.js +++ b/lib/connection.js @@ -20,6 +20,8 @@ class Connection extends EventEmitter { this.buffer = []; this.requestBuffer = []; this.requested = 0; + // Map to register callbacks if responses can be received in any order + this.requestCallbacks = new Map(); this.host = host; this.port = port; @@ -112,7 +114,14 @@ class Connection extends EventEmitter { } const args = this.requestBuffer[this.requested]; if (args) { - const request = JSON.stringify(args.slice(0, -1)); + const requestArr = args.slice(0, -1); + // last argument is the callback + const req_id = requestArr[requestArr.length - 1]?.$req_id; + if (req_id) { + // Register callback in map, if $req_id is specified + this.requestCallbacks.set(req_id, args[args.length - 1]); + } + const request = JSON.stringify(requestArr); this.stream.write(request + '\n'); this.requested++; } @@ -158,7 +167,17 @@ class Connection extends EventEmitter { } const request = this.requestBuffer.shift(); - const responder = request && request.pop(); + + let responder; + const req_id = response?.$req_id; + if (req_id) { + // If $req_id is specified using callback from requestCallbacks map + responder = this.requestCallbacks.get(req_id); + this.requestCallbacks.delete(req_id); + } else { + // Otherwise use callback from last argument of request + responder = request && request.pop(); + } this.requested--; @@ -223,6 +242,7 @@ class Connection extends EventEmitter { this.connected = false; this.stream = null; + this.requestCallbacks.clear(); if (this.requestBuffer.length > 0) { this.connect(); diff --git a/lib/connection.test.js b/lib/connection.test.js index 5dff3c6..625e75e 100644 --- a/lib/connection.test.js +++ b/lib/connection.test.js @@ -49,5 +49,56 @@ describe('Connection', () => { const response = { $push: true }; conn.receiveResponse(JSON.stringify(response)); }); + + it('should resolve requests in any ordrer, if $req_id is defined', () => { + const conn = new Connection('host', 'port'); + conn.connected = true; + conn.stream = { + write: () => {}, + }; + + const productCallback = jest.fn(); + const productResult = { + $data: { id: 'prod_1', name: 'Product 1' }, + $time: 1, + $status: 200, + $req_id: 'r1', + }; + const categoriesCallback = jest.fn(); + const categoriesResult = { + $data: { + count: 0, + page_count: 1, + page: 1, + results: [], + }, + $time: 1, + $status: 200, + $req_id: 'r2', + }; + + conn.request( + 'get', + '/products/prod_1', + { + $data: {}, + $req_id: 'r1', + }, + productCallback, + ); + conn.request( + 'get', + '/categories', + { + $data: {}, + $req_id: 'r2', + }, + categoriesCallback, + ); + conn.receiveResponse(JSON.stringify(categoriesResult)); + conn.receiveResponse(JSON.stringify(productResult)); + expect(categoriesCallback).toHaveBeenCalledWith(categoriesResult); + expect(productCallback).toHaveBeenCalledWith(productResult); + }); }); });