From 1b02aaaff7122f6922ee1fc815b296ecfea86e9b Mon Sep 17 00:00:00 2001 From: whalemare Date: Sat, 15 Oct 2022 15:08:05 +0700 Subject: [PATCH 1/6] Add initial implementation of async send --- package.json | 1 + src/websocket.ts | 41 +++++++++++++++++++ test/async-send/async-send.test.ts | 33 +++++++++++++++ yarn.lock | 65 ++++++++++++++++++++++++++++++ 4 files changed, 140 insertions(+) create mode 100644 test/async-send/async-send.test.ts diff --git a/package.json b/package.json index a6760c1..491a8b4 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "@types/ws": "^7.4.4", "coveralls": "^3.1.0", "jest": "^27.0.4", + "jest-websocket-mock": "2.4.0", "ts-jest": "^27.0.2", "typescript": "^4.3.2", "ws": "^7.4.6" diff --git a/src/websocket.ts b/src/websocket.ts index 261b945..6629d60 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -68,6 +68,47 @@ export class Websocket { this.websocket.send(data); } + public asyncSend( + data: string | ArrayBufferLike | Blob | ArrayBufferView, + isAnswer: (event: MessageEvent) => boolean + ): Promise { + return new Promise((resolve, reject) => { + try { + if (this.closedByUser) { + // TODO: need throw error instance + return reject("Closed by user") + } + + const listener = (_: Websocket, event: MessageEvent) => { + try { + if (isAnswer(event)) { + this.removeEventListener(WebsocketEvents.message, listener) + resolve(event) + } + } catch(e) { + this.removeEventListener(WebsocketEvents.message, listener) + reject(e) + } + } + + if (this.websocket === undefined || this.websocket.readyState !== this.websocket.OPEN) { + if (this.buffer) { + this.addEventListener(WebsocketEvents.message, listener) + this.buffer.write([data]); + } + + // TODO: need throw error instance + return reject(`WebSocket is in state ${this.websocket?.readyState}, and unable send message to buffer`) + } else { + this.addEventListener(WebsocketEvents.message, listener) + this.websocket.send(data); + } + } catch(e) { + return reject(e) + } + }) + } + public close(code?: number, reason?: string): void { this.closedByUser = true; this.websocket?.close(code, reason); diff --git a/test/async-send/async-send.test.ts b/test/async-send/async-send.test.ts new file mode 100644 index 0000000..2075ff7 --- /dev/null +++ b/test/async-send/async-send.test.ts @@ -0,0 +1,33 @@ +import { Websocket, WebsocketBuilder, WebsocketEvents } from "../../src"; +import WS from "jest-websocket-mock"; + +describe('async-send', () => { + const url = `ws://localhost:1234`; + + const server = new WS(url); + const client = new Websocket(url); + + beforeEach(async () => { + await server.connected + }); + + afterEach(async () => { + WS.clean() + }); + + test('when server send needed request, promise will be released with this event', async () => { + const promise = client.asyncSend("ping", (event) => { + if (event.data === "pong") { + return true + } + + return false + }) + + server.send("pong") + + const event = await promise + expect(event.data).toStrictEqual('pong') + }) +}) + diff --git a/yarn.lock b/yarn.lock index 9135cb4..78641dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -597,6 +597,13 @@ terminal-link "^2.0.0" v8-to-istanbul "^7.0.0" +"@jest/schemas@^28.1.3": + version "28.1.3" + resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-28.1.3.tgz#ad8b86a66f11f33619e3d7e1dcddd7f2d40ff905" + integrity sha512-/l/VWsdt/aBXgjshLWOFyFt3IVdYypu5y2Wn2rOO1un6nkqIn8SLXzgIMYXFyYsRWDyF5EthmKJMIdJvk08grg== + dependencies: + "@sinclair/typebox" "^0.24.1" + "@jest/source-map@^27.0.1": version "27.0.1" resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-27.0.1.tgz#2afbf73ddbaddcb920a8e62d0238a0a9e0a8d3e4" @@ -669,6 +676,11 @@ "@types/yargs" "^16.0.0" chalk "^4.0.0" +"@sinclair/typebox@^0.24.1": + version "0.24.46" + resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.24.46.tgz#57501b58023776dbbae9e25619146286440be34c" + integrity sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw== + "@sinonjs/commons@^1.7.0": version "1.8.1" resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.1.tgz#e7df00f98a203324f6dc7cc606cad9d4a8ab2217" @@ -864,6 +876,11 @@ ansi-regex@^5.0.0: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.0.tgz#388539f55179bf39339c81af30a654d69f87cb75" integrity sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg== +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -1277,6 +1294,11 @@ diff-sequences@^27.0.1: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.0.1.tgz#9c9801d52ed5f576ff0a20e3022a13ee6e297e7c" integrity sha512-XPLijkfJUh/PIBnfkcSHgvD6tlYixmcMAn3osTk6jt+H0v/mgURto1XUiD9DKuGX5NDoVS6dSlA23gd9FUaCFg== +diff-sequences@^28.1.1: + version "28.1.1" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6" + integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw== + domexception@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domexception/-/domexception-2.0.1.tgz#fb44aefba793e1574b0af6aed2801d057529f304" @@ -1820,6 +1842,16 @@ jest-diff@^27.0.2: jest-get-type "^27.0.1" pretty-format "^27.0.2" +jest-diff@^28.0.2: + version "28.1.3" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.3.tgz#948a192d86f4e7a64c5264ad4da4877133d8792f" + integrity sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw== + dependencies: + chalk "^4.0.0" + diff-sequences "^28.1.1" + jest-get-type "^28.0.2" + pretty-format "^28.1.3" + jest-docblock@^27.0.1: version "27.0.1" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-27.0.1.tgz#bd9752819b49fa4fab1a50b73eb58c653b962e8b" @@ -1873,6 +1905,11 @@ jest-get-type@^27.0.1: resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.0.1.tgz#34951e2b08c8801eb28559d7eb732b04bbcf7815" integrity sha512-9Tggo9zZbu0sHKebiAijyt1NM77Z0uO4tuWOxUCujAiSeXv30Vb5D4xVF4UR4YWNapcftj+PbByU54lKD7/xMg== +jest-get-type@^28.0.2: + version "28.0.2" + resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" + integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== + jest-haste-map@^27.0.2: version "27.0.2" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-27.0.2.tgz#3f1819400c671237e48b4d4b76a80a0dbed7577f" @@ -2127,6 +2164,14 @@ jest-watcher@^27.0.2: jest-util "^27.0.2" string-length "^4.0.1" +jest-websocket-mock@2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/jest-websocket-mock/-/jest-websocket-mock-2.4.0.tgz#95ab1f89f809e57d2714427736ab7b1094fb1c3c" + integrity sha512-AOwyuRw6fgROXHxMOiTDl1/T4dh3fV4jDquha5N0csS/PNp742HeTZWPAuKppVRSQ8s3fUGgJHoyZT9JDO0hMA== + dependencies: + jest-diff "^28.0.2" + mock-socket "^9.1.0" + jest-worker@^27.0.2: version "27.0.2" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.2.tgz#4ebeb56cef48b3e7514552f80d0d80c0129f0b05" @@ -2356,6 +2401,11 @@ mkdirp@1.x: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +mock-socket@^9.1.0: + version "9.1.5" + resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.1.5.tgz#2c4e44922ad556843b6dfe09d14ed8041fa2cdeb" + integrity sha512-3DeNIcsQixWHHKk6NdoBhWI4t1VMj5/HzfnI1rE/pLl5qKx7+gd4DNA07ehTaZ6MoUU053si6Hd+YtiM/tQZfg== + ms@2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" @@ -2532,6 +2582,16 @@ pretty-format@^27.0.2: ansi-styles "^5.0.0" react-is "^17.0.1" +pretty-format@^28.1.3: + version "28.1.3" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5" + integrity sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q== + dependencies: + "@jest/schemas" "^28.1.3" + ansi-regex "^5.0.1" + ansi-styles "^5.0.0" + react-is "^18.0.0" + prompts@^2.0.1: version "2.4.0" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.0.tgz#4aa5de0723a231d1ee9121c40fdf663df73f61d7" @@ -2560,6 +2620,11 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react-is@^18.0.0: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" From 3e6574d24b2b96dc764b6b1e6924f2ef532ca085 Mon Sep 17 00:00:00 2001 From: whalemare Date: Sat, 15 Oct 2022 15:33:40 +0700 Subject: [PATCH 2/6] Add test for clear callbacks --- test/async-send/async-send.test.ts | 86 +++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 13 deletions(-) diff --git a/test/async-send/async-send.test.ts b/test/async-send/async-send.test.ts index 2075ff7..eb26c72 100644 --- a/test/async-send/async-send.test.ts +++ b/test/async-send/async-send.test.ts @@ -1,33 +1,93 @@ import { Websocket, WebsocketBuilder, WebsocketEvents } from "../../src"; import WS from "jest-websocket-mock"; +type ClientServerPair = { + server: WS + client: Websocket +} + describe('async-send', () => { const url = `ws://localhost:1234`; - const server = new WS(url); - const client = new Websocket(url); - - beforeEach(async () => { + const withClientServer = async (action: (pair: ClientServerPair) => Promise) => { + const server = new WS(url); + const client = new Websocket(url); await server.connected - }); + + await action({client, server}) - afterEach(async () => { + client.close() + server.close() WS.clean() - }); + } - test('when server send needed request, promise will be released with this event', async () => { - const promise = client.asyncSend("ping", (event) => { + + describe('when client send "ping" and wait for "pong"', () => { + const waitForPong = (event: MessageEvent) => { if (event.data === "pong") { return true } return false + } + + test('promise will be released when server send "pong"', async () => { + await withClientServer(async ({ client, server }) => { + const promise = client.asyncSend("ping", waitForPong) + server.send("pong") + const event = await promise + expect(event.data).toStrictEqual('pong') + }) }) - - server.send("pong") - const event = await promise - expect(event.data).toStrictEqual('pong') + test('promise will be released only after server send "pong"', async () => { + await withClientServer(async ({ client, server }) => { + const seenEvents: MessageEvent[] = [] + const promise = client.asyncSend("ping", (event) => { + seenEvents.push(event) + + return waitForPong(event) + }) + + server.send("just") + server.send("trying") + server.send("to send") + + server.send("pong") + const event = await promise + + expect(event.data).toStrictEqual('pong') + expect(seenEvents.length).toEqual(4) + }) + }) + + test('callback will be cleared after success event', async () => { + await withClientServer(async ({client, server}) => { + const promise = client.asyncSend("ping", waitForPong) + expect(getMessageListeners(client).length).toBe(1) // because we in waiting for "pong" response + + server.send('incorrect response') + expect(getMessageListeners(client).length).toBe(1) // still waitinig + + server.send('pong') + await promise + + // no more listeners + expect(getMessageListeners(client).length).toBe(0) + }) + }) }) + + const getMessageListeners = (client: Websocket) => { + // @ts-ignore abstraction leaked here, but only for tests + return client.eventListeners['message'] + } }) +function delay(ms: number): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve(); + }, ms); + }) +} \ No newline at end of file From 7af0cce4617bc791503fe8f2a3d65b3681311460 Mon Sep 17 00:00:00 2001 From: whalemare Date: Sat, 15 Oct 2022 15:38:11 +0700 Subject: [PATCH 3/6] Check rejects --- src/websocket.ts | 2 +- test/async-send/async-send.test.ts | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/websocket.ts b/src/websocket.ts index 6629d60..432874c 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -68,7 +68,7 @@ export class Websocket { this.websocket.send(data); } - public asyncSend( + public sendAsync( data: string | ArrayBufferLike | Blob | ArrayBufferView, isAnswer: (event: MessageEvent) => boolean ): Promise { diff --git a/test/async-send/async-send.test.ts b/test/async-send/async-send.test.ts index eb26c72..14c1612 100644 --- a/test/async-send/async-send.test.ts +++ b/test/async-send/async-send.test.ts @@ -31,19 +31,19 @@ describe('async-send', () => { return false } - test('promise will be released when server send "pong"', async () => { + test('promise should be released when server send "pong"', async () => { await withClientServer(async ({ client, server }) => { - const promise = client.asyncSend("ping", waitForPong) + const promise = client.sendAsync("ping", waitForPong) server.send("pong") const event = await promise expect(event.data).toStrictEqual('pong') }) }) - test('promise will be released only after server send "pong"', async () => { + test('promise should be released only after server send "pong"', async () => { await withClientServer(async ({ client, server }) => { const seenEvents: MessageEvent[] = [] - const promise = client.asyncSend("ping", (event) => { + const promise = client.sendAsync("ping", (event) => { seenEvents.push(event) return waitForPong(event) @@ -61,9 +61,9 @@ describe('async-send', () => { }) }) - test('callback will be cleared after success event', async () => { + test('callback should be cleared after success event', async () => { await withClientServer(async ({client, server}) => { - const promise = client.asyncSend("ping", waitForPong) + const promise = client.sendAsync("ping", waitForPong) expect(getMessageListeners(client).length).toBe(1) // because we in waiting for "pong" response server.send('incorrect response') @@ -76,6 +76,20 @@ describe('async-send', () => { expect(getMessageListeners(client).length).toBe(0) }) }) + + test('promise should be rejected, when checking on answer is failed with error', async () => { + const error = new Error("Some error") + + await withClientServer(async ({client, server}) => { + const promise = client.sendAsync("hello", () => { + throw error + }) + + server.send("world") + + expect(promise).rejects.toStrictEqual(error) + }) + }) }) const getMessageListeners = (client: Websocket) => { From 5e7dc494c2ed1984d0b6366a3382fc044837adfb Mon Sep 17 00:00:00 2001 From: whalemare Date: Sat, 15 Oct 2022 15:42:46 +0700 Subject: [PATCH 4/6] Check that listeners cleared on rejects --- test/async-send/async-send.test.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/test/async-send/async-send.test.ts b/test/async-send/async-send.test.ts index 14c1612..e06c95d 100644 --- a/test/async-send/async-send.test.ts +++ b/test/async-send/async-send.test.ts @@ -90,6 +90,22 @@ describe('async-send', () => { expect(promise).rejects.toStrictEqual(error) }) }) + + test('when promise rejected, listeners should be cleared', async () => { + const error = new Error("Some error") + + await withClientServer(async ({client, server}) => { + expect(getMessageListeners(client).length).toStrictEqual(0) + + const promise = client.sendAsync("hello", () => { + throw error + }) + server.send("world") + expect(promise).rejects.toStrictEqual(error) + + expect(getMessageListeners(client).length).toStrictEqual(0) + }) + }) }) const getMessageListeners = (client: Websocket) => { From d8df43cd10ec9035be2ea8618ede63c3eecfcad0 Mon Sep 17 00:00:00 2001 From: whalemare Date: Sat, 15 Oct 2022 15:43:28 +0700 Subject: [PATCH 5/6] Remove delay function --- test/async-send/async-send.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/async-send/async-send.test.ts b/test/async-send/async-send.test.ts index e06c95d..c57ecdd 100644 --- a/test/async-send/async-send.test.ts +++ b/test/async-send/async-send.test.ts @@ -113,11 +113,3 @@ describe('async-send', () => { return client.eventListeners['message'] } }) - -function delay(ms: number): Promise { - return new Promise(resolve => { - setTimeout(() => { - resolve(); - }, ms); - }) -} \ No newline at end of file From 05bc0c7b1c463b3bb8b8612a98b39185bff0ea72 Mon Sep 17 00:00:00 2001 From: whalemare Date: Sat, 15 Oct 2022 15:54:04 +0700 Subject: [PATCH 6/6] Rename asyncSend to sendAsync --- .../{async-send/async-send.test.ts => send-async/send-async.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename test/{async-send/async-send.test.ts => send-async/send-async.ts} (99%) diff --git a/test/async-send/async-send.test.ts b/test/send-async/send-async.ts similarity index 99% rename from test/async-send/async-send.test.ts rename to test/send-async/send-async.ts index c57ecdd..b91e8d9 100644 --- a/test/async-send/async-send.test.ts +++ b/test/send-async/send-async.ts @@ -6,7 +6,7 @@ type ClientServerPair = { client: Websocket } -describe('async-send', () => { +describe('send-async', () => { const url = `ws://localhost:1234`; const withClientServer = async (action: (pair: ClientServerPair) => Promise) => {