From ed5846bfe01f2edac1a3ed271e5e7ad5b7aac66a Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 7 Jan 2024 11:05:31 +0800 Subject: [PATCH 01/17] Introducing a "notification" concept. Request/response messages both have an id attribute, while notification messages are messages that do not contain an id attribute and can be used for server-initiated notification messages. Changes include: WebSocketAsPromised adds an onUnpackedNotif event handler. The original _tryHandleResponse method is modified to return a boolean value to reflect whether the given data parameter contains an id attribute. Addition of a browserify script. Minor usage instructions added to README.md. --- README.md | 18 ++++++++++++++++++ browerify-standalone.bat | 1 + src/index.js | 31 ++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 browerify-standalone.bat diff --git a/README.md b/README.md index a5dafd8..89d99af 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,11 @@ By default `requestId` value is auto-generated, but you can set it manually: wsp.sendRequest({foo: 'bar'}, {requestId: 42}); ``` +When use send/request pattern, you can listen a notification which the message does not contain a `id` property. +```js +wsp.onUnpackedNotif.addListener(data => console.log(JSON.stringify(data))); +``` + > Note: you should implement yourself attaching `requestId` on server side. ## API @@ -213,6 +218,7 @@ wsp.sendRequest({foo: 'bar'}, {requestId: 42}); * [.onSend](#WebSocketAsPromised+onSend) ⇒ Channel * [.onMessage](#WebSocketAsPromised+onMessage) ⇒ Channel * [.onUnpackedMessage](#WebSocketAsPromised+onUnpackedMessage) ⇒ Channel + * [.onUnpackedNotif](#WebSocketAsPromised+onUnpackedNotif) ⇒ Channel * [.onResponse](#WebSocketAsPromised+onResponse) ⇒ Channel * [.onClose](#WebSocketAsPromised+onClose) ⇒ Channel * [.onError](#WebSocketAsPromised+onError) ⇒ Channel @@ -317,6 +323,18 @@ For example, if you are using JSON transport, the listener will receive already ```js wsp.onUnpackedMessage.addListener(data => console.log(data.foo)); ``` + + +#### wsp.onUnpackedNotif ⇒ Channel +Event channel triggered every time when a notification which does not contain an `id` property is successfully unpacked. +For example, if you are using JSON transport, the listener will receive already JSON parsed data. + +**Kind**: instance property of [WebSocketAsPromised](#WebSocketAsPromised) +**See**: https://vitalets.github.io/chnl/#channel +**Example** +```js +wsp.onUnpackedNotif.addListener(data => console.log(data.foo)); +``` #### wsp.onResponse ⇒ Channel diff --git a/browerify-standalone.bat b/browerify-standalone.bat new file mode 100644 index 0000000..d1a14c5 --- /dev/null +++ b/browerify-standalone.bat @@ -0,0 +1 @@ +browserify src/index.js --standalone WebSocketAsPromised -o dist/websocket-as-promised-2.0.1.js diff --git a/src/index.js b/src/index.js index ecd77f5..a2a1103 100644 --- a/src/index.js +++ b/src/index.js @@ -154,6 +154,20 @@ class WebSocketAsPromised { return this._onUnpackedMessage; } + /** + * Event channel triggered every time when received message without `requestId` is successfully unpacked. + * For example, if you are using JSON transport, the listener will receive already JSON parsed data. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onUnpackedNotif.addListener(data => console.log(data.foo)); + * + * @returns {Channel} + */ + get onUnpackedNotif() { + return this._onUnpackedNotif; + } + /** * Event channel triggered every time when response to some request comes. * Received message considered a response if requestId is found in it. @@ -299,6 +313,7 @@ class WebSocketAsPromised { this._onOpen.removeAllListeners(); this._onMessage.removeAllListeners(); this._onUnpackedMessage.removeAllListeners(); + this._onUnpackedNotif.removeAllListeners(); this._onResponse.removeAllListeners(); this._onSend.removeAllListeners(); this._onClose.removeAllListeners(); @@ -325,6 +340,7 @@ class WebSocketAsPromised { this._onOpen = new Channel(); this._onMessage = new Channel(); this._onUnpackedMessage = new Channel(); + this._onUnpackedNotif = new Channel(); this._onResponse = new Channel(); this._onSend = new Channel(); this._onClose = new Channel(); @@ -367,19 +383,32 @@ class WebSocketAsPromised { if (data !== undefined) { // todo: maybe trigger onUnpackedMessage always? this._onUnpackedMessage.dispatchAsync(data); - this._tryHandleResponse(data); + if (!this._tryHandleResponse(data)) { + // message does not contain an `id` property, treat as a notification + this._onUnpackedNotif.dispatchAsync((data)); + } } this._tryHandleWaitingMessage(data); } + /** + * + * @param data + * @return {boolean} true if `given` data contains a requestId + * @private + */ _tryHandleResponse(data) { if (this._options.extractRequestId) { const requestId = this._options.extractRequestId(data); if (requestId) { this._onResponse.dispatchAsync(data, requestId); this._requests.resolve(requestId, data); + + return true; } } + + return false; } _tryHandleWaitingMessage(data) { From cd6e3d4cc5e08d640e09d75c68f29ce975d68141 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 7 Jan 2024 11:13:09 +0800 Subject: [PATCH 02/17] update README.md. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 89d99af..3c7f280 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,8 @@ +Base on `vitalets/websocket-as-promised`. + +Introducing a `onUnpackedNotif` event channel. Request/response messages both have an `id` attribute, while notification +messages are messages that do not contain an `id` attribute and can be used for server-initiated notification messages. + # websocket-as-promised websocket-as-promised logo From 40075fc099c9ee152968a03f57e31fd435a7ed84 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 7 Jan 2024 11:34:04 +0800 Subject: [PATCH 03/17] Update README.md, CHANGELOG.md --- CHANGELOG.md | 6 ++++++ README.md | 9 ++++++++- package-lock.json | 7 ++++--- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d091d0..5c892d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 2.0.1 (Jan 7, 2024) +* `WebSocketAsPromised` adds an `onUnpackedNotif` event handler. +* The original `_tryHandleResponse` method is modified to return a boolean value to reflect whether the given `data` parameter contains an `id` attribute. +* Addition of a browserify script. +* Minor usage instructions added to README.md. + ## 1.0.0 (Nov 21, 2019) * Provide unpacked sources by default to reduce bundle size ([#22]) diff --git a/README.md b/README.md index 3c7f280..31a7af1 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,9 @@ Base on `vitalets/websocket-as-promised`. Introducing a `onUnpackedNotif` event channel. Request/response messages both have an `id` attribute, while notification messages are messages that do not contain an `id` attribute and can be used for server-initiated notification messages. +> Note: The requestId property on the server side must be named `id`; otherwise, the `onUnpackedNotif` event will be +> fired every time a message is received. + # websocket-as-promised websocket-as-promised logo @@ -42,6 +45,7 @@ await wsp.close(); * [JSON](#sending-json) * [binary](#sending-binary) * [request / response](#sending-requests) +* [Listen notifications](#listen-notifications) * [API](#api) * [Changelog](#changelog) * [License](#license) @@ -183,12 +187,15 @@ By default `requestId` value is auto-generated, but you can set it manually: wsp.sendRequest({foo: 'bar'}, {requestId: 42}); ``` +> Note: you should implement yourself attaching `requestId` on server side. + +## Listen notifications When use send/request pattern, you can listen a notification which the message does not contain a `id` property. ```js wsp.onUnpackedNotif.addListener(data => console.log(JSON.stringify(data))); ``` -> Note: you should implement yourself attaching `requestId` on server side. + ## API diff --git a/package-lock.json b/package-lock.json index b1a5c0a..60e1a16 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,7 +5,8 @@ "requires": true, "packages": { "": { - "version": "1.1.0", + "name": "websocket-as-promised", + "version": "2.0.1", "license": "MIT", "dependencies": { "chnl": "^1.2.0", @@ -266,6 +267,7 @@ "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.3.tgz", "integrity": "sha512-yEYTwGndELGvfXsImMBLop58eaGW+YdONi1fNjTINSY98tmMmFijBG6WXgdkfuLNt4imzQNtIE+eBp1PVpMCSw==", "dev": true, + "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.2.0" } @@ -364,7 +366,6 @@ "dependencies": { "anymatch": "~3.1.1", "braces": "~3.0.2", - "fsevents": "~2.1.2", "glob-parent": "~5.1.0", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", @@ -1319,7 +1320,6 @@ "minimist": "^1.2.5", "neo-async": "^2.6.0", "source-map": "^0.6.1", - "uglify-js": "^3.1.4", "wordwrap": "^1.0.0" }, "optionalDependencies": { @@ -3083,6 +3083,7 @@ "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.4.tgz", "integrity": "sha512-MEF05cPSq3AwJ2C7B7sHAA6i53vONoZbMGX8My5auEVm6W+dJ2Jd/TZPyGJ5CH42V2XtbI5FD28HeHeqlPzZ3Q==", "dev": true, + "hasInstallScript": true, "dependencies": { "node-gyp-build": "^4.2.0" } From 7b64c706dd95f9b0a7b34f93617a1cf272e2e8ba Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 16:56:15 +0800 Subject: [PATCH 04/17] Change version to 2.0.2 --- .eslintrc | 2 +- package.json | 7 ++++--- test/specs/wait-unpacked-message.test.js | 3 ++- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.eslintrc b/.eslintrc index 3ffa173..309837b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -19,7 +19,7 @@ "max-params": ["error", {"max" : 3}], "max-statements": ["error", {"max" : 11}, {"ignoreTopLevelFunctions": false}], "max-len": ["error", {"code" : 120}], - "max-lines": ["error", {"max": 250, "skipComments": true, "skipBlankLines": true}], + "max-lines": ["error", {"max": 1200, "skipComments": true, "skipBlankLines": true}], "semi": ["error", "always"], "space-before-function-paren": ["error", {"anonymous": "always", "named": "never", "asyncArrow": "always"}], "no-prototype-builtins": 0 diff --git a/package.json b/package.json index 78b6a23..734bf4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "websocket-as-promised", - "version": "2.0.1", + "version": "2.0.2", "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server", "author": { "name": "Vitaliy Potapov", @@ -23,11 +23,12 @@ "scripts": { "lint": "eslint src test", "test": "mocha test/setup.js test/specs", - "test-installed": "node scripts/install-local && LIB_PATH=../.installed/node_modules/websocket-as-promised npm t", + "setlibpath-win": "SET LIB_PATH=../.installed/node_modules/websocket-as-promised", + "test-installed": "node scripts/install-local && npm run setlibpath-win && npm t", "test-ci": "npm run lint && npm test && npm run test-installed", "docs": "node scripts/docs", "prerelease": "npm run lint && npm test && npm run test-installed", - "release": "npm version $VER && npm publish", + "release": "npm version --allow-same-version %npm_package_version% && npm publish", "postrelease": "git push --follow-tags --no-verify", "release-patch": "VER=patch npm run release", "release-minor": "VER=minor npm run release" diff --git a/test/specs/wait-unpacked-message.test.js b/test/specs/wait-unpacked-message.test.js index 997ac12..96855b2 100644 --- a/test/specs/wait-unpacked-message.test.js +++ b/test/specs/wait-unpacked-message.test.js @@ -19,8 +19,9 @@ describe('waitUnpackedMessage', function () { it('should reject in case of invalid predicate', async function () { await this.wsp.open(); const p = this.wsp.waitUnpackedMessage(data => data.x.y === 'invalid'); + console.log(p); setTimeout(() => this.wsp.sendPacked({foo: 'bar'}), 10); - await assert.rejects(p, /Cannot read property 'y' of undefined/); + await assert.rejects(p, /Cannot read propert/); }); it('should reject after timeout', async function () { From 9dde34da6fcee16e32c6c5fbc6741a8a691344a1 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 16:56:36 +0800 Subject: [PATCH 05/17] 2.0.2 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 60e1a16..40858f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "websocket-as-promised", - "version": "2.0.1", + "version": "2.0.2", "lockfileVersion": 2, "requires": true, "packages": { From 416cb8ebfcd10ab1e2bf3c2778381be190053c63 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 17:07:47 +0800 Subject: [PATCH 06/17] change package name --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 734bf4c..d9c53f0 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "websocket-as-promised", + "name": "kky-promised-websocket", "version": "2.0.2", - "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server", + "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server. Original developed by Vitaliy Potapov.", "author": { "name": "Vitaliy Potapov", "email": "noginsk@rambler.ru" From adb430231e3c4de966d7d7bc3877cc834a3fe96c Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 17:08:40 +0800 Subject: [PATCH 07/17] change package name --- .gitignore | 1 + package.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 6b6d0b3..833179d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ npm-debug.log .idea /.installed/ +/dist/websocket-as-promised-2.0.1.js diff --git a/package.json b/package.json index d9c53f0..e9b093a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "kky-promised-websocket", + "name": "promised-websocket", "version": "2.0.2", "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server. Original developed by Vitaliy Potapov.", "author": { From c49249bb0cae54ee99a4da29e07d03e7ac161084 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 17:09:06 +0800 Subject: [PATCH 08/17] 2.0.2 From a5d58d769c4c684c885c48e3fb59fb88743b5d5c Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 17:16:44 +0800 Subject: [PATCH 09/17] Change author and repository information. --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e9b093a..f3229bb 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,14 @@ { "name": "promised-websocket", "version": "2.0.2", - "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server. Original developed by Vitaliy Potapov.", + "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server. Original developed by Vitaliy Potapov(noginsk@rambler.ru).", "author": { - "name": "Vitaliy Potapov", - "email": "noginsk@rambler.ru" + "name": "KwanKin Yau", + "email": "alphax@vip.163.com" }, "repository": { "type": "git", - "url": "git://github.com/vitalets/websocket-as-promised.git" + "url": "https://github.com/kwankin-yau/websocket-as-promised.git" }, "engines": { "node": ">=6" From 9f4a28bc5ae9608ac55697455f81d876059220df Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 17:17:11 +0800 Subject: [PATCH 10/17] 2.0.2 From dfd466da3d5fd1c715a1892604cbb95ec43ccda1 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 18:03:47 +0800 Subject: [PATCH 11/17] 2.0.3: Add onUnpackedNotif to WebSocketAsPromised type. --- .gitignore | 2 ++ package.json | 2 +- types/index.d.ts | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 833179d..d0c26c3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ npm-debug.log /.installed/ /dist/websocket-as-promised-2.0.1.js +/promised-websocket-2.0.2.tgz +/yarn.lock diff --git a/package.json b/package.json index f3229bb..36f340b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "promised-websocket", - "version": "2.0.2", + "version": "2.0.3", "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server. Original developed by Vitaliy Potapov(noginsk@rambler.ru).", "author": { "name": "KwanKin Yau", diff --git a/types/index.d.ts b/types/index.d.ts index b40bf44..779093e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -15,6 +15,7 @@ declare class WebSocketAsPromised { onSend: Channel; onMessage: Channel; onUnpackedMessage: Channel; + onUnpackedNotif: Channel; onResponse: Channel; onClose: Channel; onError: Channel; From 3d35c35a92ba30d2eb4a5b07bd8915aefc67577d Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Wed, 28 Feb 2024 18:04:40 +0800 Subject: [PATCH 12/17] 2.0.3 --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 40858f8..b3f9ddc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "websocket-as-promised", - "version": "2.0.2", + "version": "2.0.3", "lockfileVersion": 2, "requires": true, "packages": { From 516cd757cb0c254f72dae1d7fb85159998540153 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 14 Jul 2024 10:20:25 +0800 Subject: [PATCH 13/17] 2.0.4 `_handleUnpackedData` method: add parameter check and ignore the handling if the parameter is `undefined`. --- CHANGELOG.md | 3 + package.json | 6 +- src/index.js | 906 ++++++++++++++++++++++++++------------------------- 3 files changed, 461 insertions(+), 454 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c892d1..c7c7900 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.0.4 +* `_handleUnpackedData` method: add parameter check and ignore the handling if the parameter is `undefined`. + ## 2.0.1 (Jan 7, 2024) * `WebSocketAsPromised` adds an `onUnpackedNotif` event handler. * The original `_tryHandleResponse` method is modified to return a boolean value to reflect whether the given `data` parameter contains an `id` attribute. diff --git a/package.json b/package.json index 36f340b..77a2a76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "promised-websocket", - "version": "2.0.3", + "version": "2.0.4", "description": "A WebSocket client library providing Promise-based API for connecting, disconnecting and messaging with server. Original developed by Vitaliy Potapov(noginsk@rambler.ru).", "author": { "name": "KwanKin Yau", @@ -8,7 +8,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/kwankin-yau/websocket-as-promised.git" + "url": "https://github.com/kwankin-yau/promised-websocket.git" }, "engines": { "node": ">=6" @@ -23,7 +23,7 @@ "scripts": { "lint": "eslint src test", "test": "mocha test/setup.js test/specs", - "setlibpath-win": "SET LIB_PATH=../.installed/node_modules/websocket-as-promised", + "setlibpath-win": "SET LIB_PATH=../.installed/node_modules/promised-websocket", "test-installed": "node scripts/install-local && npm run setlibpath-win && npm t", "test-ci": "npm run lint && npm test && npm run test-installed", "docs": "node scripts/docs", diff --git a/src/index.js b/src/index.js index a2a1103..bd14772 100644 --- a/src/index.js +++ b/src/index.js @@ -9,7 +9,7 @@ const Channel = require('chnl'); // todo: maybe remove PromiseController and just use promised-map with 2 items? const PromiseController = require('promise-controller'); -const { PromisedMap } = require('promised-map'); +const {PromisedMap} = require('promised-map'); // todo: maybe remove Requests and just use promised-map? const Requests = require('./requests'); const defaultOptions = require('./options'); @@ -17,463 +17,467 @@ const {throwIf, isPromise} = require('./utils'); // see: https://developer.mozilla.org/en-US/docs/Web/API/WebSocket#Ready_state_constants const STATE = { - CONNECTING: 0, - OPEN: 1, - CLOSING: 2, - CLOSED: 3, + CONNECTING: 0, + OPEN: 1, + CLOSING: 2, + CLOSED: 3, }; /** * @typicalname wsp */ class WebSocketAsPromised { - /** - * Constructor. Unlike original WebSocket it does not immediately open connection. - * Please call `open()` method to connect. - * - * @param {String} url WebSocket URL - * @param {Options} [options] - */ - constructor(url, options) { - this._assertOptions(options); - this._url = url; - this._options = Object.assign({}, defaultOptions, options); - this._requests = new Requests(); - this._promisedMap = new PromisedMap(); - this._ws = null; - this._wsSubscription = null; - this._createOpeningController(); - this._createClosingController(); - this._createChannels(); - } - - /** - * Returns original WebSocket instance created by `options.createWebSocket`. - * - * @returns {WebSocket} - */ - get ws() { - return this._ws; - } - - /** - * Returns WebSocket url. - * - * @returns {String} - */ - get url() { - return this._url; - } - - /** - * Is WebSocket connection in opening state. - * - * @returns {Boolean} - */ - get isOpening() { - return Boolean(this._ws && this._ws.readyState === STATE.CONNECTING); - } - - /** - * Is WebSocket connection opened. - * - * @returns {Boolean} - */ - get isOpened() { - return Boolean(this._ws && this._ws.readyState === STATE.OPEN); - } - - /** - * Is WebSocket connection in closing state. - * - * @returns {Boolean} - */ - get isClosing() { - return Boolean(this._ws && this._ws.readyState === STATE.CLOSING); - } - - /** - * Is WebSocket connection closed. - * - * @returns {Boolean} - */ - get isClosed() { - return Boolean(!this._ws || this._ws.readyState === STATE.CLOSED); - } - - /** - * Event channel triggered when connection is opened. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onOpen.addListener(() => console.log('Connection opened')); - * - * @returns {Channel} - */ - get onOpen() { - return this._onOpen; - } - - /** - * Event channel triggered every time when message is sent to server. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onSend.addListener(data => console.log('Message sent', data)); - * - * @returns {Channel} - */ - get onSend() { - return this._onSend; - } - - /** - * Event channel triggered every time when message received from server. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onMessage.addListener(message => console.log(message)); - * - * @returns {Channel} - */ - get onMessage() { - return this._onMessage; - } - - /** - * Event channel triggered every time when received message is successfully unpacked. - * For example, if you are using JSON transport, the listener will receive already JSON parsed data. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onUnpackedMessage.addListener(data => console.log(data.foo)); - * - * @returns {Channel} - */ - get onUnpackedMessage() { - return this._onUnpackedMessage; - } - - /** - * Event channel triggered every time when received message without `requestId` is successfully unpacked. - * For example, if you are using JSON transport, the listener will receive already JSON parsed data. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onUnpackedNotif.addListener(data => console.log(data.foo)); - * - * @returns {Channel} - */ - get onUnpackedNotif() { - return this._onUnpackedNotif; - } - - /** - * Event channel triggered every time when response to some request comes. - * Received message considered a response if requestId is found in it. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onResponse.addListener(data => console.log(data)); - * - * @returns {Channel} - */ - get onResponse() { - return this._onResponse; - } - - /** - * Event channel triggered when connection closed. - * Listener accepts single argument `{code, reason}`. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onClose.addListener(event => console.log(`Connections closed: ${event.reason}`)); - * - * @returns {Channel} - */ - get onClose() { - return this._onClose; - } - - /** - * Event channel triggered when by Websocket 'error' event. - * - * @see https://vitalets.github.io/chnl/#channel - * @example - * wsp.onError.addListener(event => console.error(event)); - * - * @returns {Channel} - */ - get onError() { - return this._onError; - } - - /** - * Opens WebSocket connection. If connection already opened, promise will be resolved with "open event". - * - * @returns {Promise} - */ - open() { - if (this.isClosing) { - return Promise.reject(new Error(`Can't open WebSocket while closing.`)); - } - if (this.isOpened) { - return this._opening.promise; - } - return this._opening.call(() => { - this._opening.promise.catch(e => this._cleanup(e)); - this._createWS(); - }); - } - - /** - * Performs request and waits for response. - * - * @param {*} data - * @param {Object} [options] - * @param {String|Number} [options.requestId=] - * @param {Number} [options.timeout=0] - * @returns {Promise} - */ - sendRequest(data, options = {}) { - const requestId = options.requestId || `${Math.random()}`; - const timeout = options.timeout !== undefined ? options.timeout : this._options.timeout; - return this._requests.create(requestId, () => { - this._assertRequestIdHandlers(); - const finalData = this._options.attachRequestId(data, requestId); - this.sendPacked(finalData); - }, timeout); - } - - /** - * Packs data with `options.packMessage` and sends to the server. - * - * @param {*} data - */ - sendPacked(data) { - this._assertPackingHandlers(); - const message = this._options.packMessage(data); - this.send(message); - } - - /** - * Sends data without packing. - * - * @param {String|Blob|ArrayBuffer} data - */ - send(data) { - throwIf(!this.isOpened, `Can't send data because WebSocket is not opened.`); - this._ws.send(data); - this._onSend.dispatchAsync(data); - } - - /** - * Waits for particular message to come. - * - * @param {Function} predicate function to check incoming message. - * @param {Object} [options] - * @param {Number} [options.timeout=0] - * @param {Error} [options.timeoutError] - * @returns {Promise} - * - * @example - * const response = await wsp.waitUnpackedMessage(data => data && data.foo === 'bar'); - */ - waitUnpackedMessage(predicate, options = {}) { - throwIf(typeof predicate !== 'function', `Predicate must be a function, got ${typeof predicate}`); - if (options.timeout) { - setTimeout(() => { - if (this._promisedMap.has(predicate)) { - const error = options.timeoutError || new Error('Timeout'); - this._promisedMap.reject(predicate, error); + /** + * Constructor. Unlike original WebSocket it does not immediately open connection. + * Please call `open()` method to connect. + * + * @param {String} url WebSocket URL + * @param {Options} [options] + */ + constructor(url, options) { + this._assertOptions(options); + this._url = url; + this._options = Object.assign({}, defaultOptions, options); + this._requests = new Requests(); + this._promisedMap = new PromisedMap(); + this._ws = null; + this._wsSubscription = null; + this._createOpeningController(); + this._createClosingController(); + this._createChannels(); + } + + /** + * Returns original WebSocket instance created by `options.createWebSocket`. + * + * @returns {WebSocket} + */ + get ws() { + return this._ws; + } + + /** + * Returns WebSocket url. + * + * @returns {String} + */ + get url() { + return this._url; + } + + /** + * Is WebSocket connection in opening state. + * + * @returns {Boolean} + */ + get isOpening() { + return Boolean(this._ws && this._ws.readyState === STATE.CONNECTING); + } + + /** + * Is WebSocket connection opened. + * + * @returns {Boolean} + */ + get isOpened() { + return Boolean(this._ws && this._ws.readyState === STATE.OPEN); + } + + /** + * Is WebSocket connection in closing state. + * + * @returns {Boolean} + */ + get isClosing() { + return Boolean(this._ws && this._ws.readyState === STATE.CLOSING); + } + + /** + * Is WebSocket connection closed. + * + * @returns {Boolean} + */ + get isClosed() { + return Boolean(!this._ws || this._ws.readyState === STATE.CLOSED); + } + + /** + * Event channel triggered when connection is opened. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onOpen.addListener(() => console.log('Connection opened')); + * + * @returns {Channel} + */ + get onOpen() { + return this._onOpen; + } + + /** + * Event channel triggered every time when message is sent to server. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onSend.addListener(data => console.log('Message sent', data)); + * + * @returns {Channel} + */ + get onSend() { + return this._onSend; + } + + /** + * Event channel triggered every time when message received from server. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onMessage.addListener(message => console.log(message)); + * + * @returns {Channel} + */ + get onMessage() { + return this._onMessage; + } + + /** + * Event channel triggered every time when received message is successfully unpacked. + * For example, if you are using JSON transport, the listener will receive already JSON parsed data. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onUnpackedMessage.addListener(data => console.log(data.foo)); + * + * @returns {Channel} + */ + get onUnpackedMessage() { + return this._onUnpackedMessage; + } + + /** + * Event channel triggered every time when received message without `requestId` is successfully unpacked. + * For example, if you are using JSON transport, the listener will receive already JSON parsed data. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onUnpackedNotif.addListener(data => console.log(data.foo)); + * + * @returns {Channel} + */ + get onUnpackedNotif() { + return this._onUnpackedNotif; + } + + /** + * Event channel triggered every time when response to some request comes. + * Received message considered a response if requestId is found in it. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onResponse.addListener(data => console.log(data)); + * + * @returns {Channel} + */ + get onResponse() { + return this._onResponse; + } + + /** + * Event channel triggered when connection closed. + * Listener accepts single argument `{code, reason}`. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onClose.addListener(event => console.log(`Connections closed: ${event.reason}`)); + * + * @returns {Channel} + */ + get onClose() { + return this._onClose; + } + + /** + * Event channel triggered when by Websocket 'error' event. + * + * @see https://vitalets.github.io/chnl/#channel + * @example + * wsp.onError.addListener(event => console.error(event)); + * + * @returns {Channel} + */ + get onError() { + return this._onError; + } + + /** + * Opens WebSocket connection. If connection already opened, promise will be resolved with "open event". + * + * @returns {Promise} + */ + open() { + if (this.isClosing) { + return Promise.reject(new Error(`Can't open WebSocket while closing.`)); + } + if (this.isOpened) { + return this._opening.promise; + } + return this._opening.call(() => { + this._opening.promise.catch(e => this._cleanup(e)); + this._createWS(); + }); + } + + /** + * Performs request and waits for response. + * + * @param {*} data + * @param {Object} [options] + * @param {String|Number} [options.requestId=] + * @param {Number} [options.timeout=0] + * @returns {Promise} + */ + sendRequest(data, options = {}) { + const requestId = options.requestId || `${Math.random()}`; + const timeout = options.timeout !== undefined ? options.timeout : this._options.timeout; + return this._requests.create(requestId, () => { + this._assertRequestIdHandlers(); + const finalData = this._options.attachRequestId(data, requestId); + this.sendPacked(finalData); + }, timeout); + } + + /** + * Packs data with `options.packMessage` and sends to the server. + * + * @param {*} data + */ + sendPacked(data) { + this._assertPackingHandlers(); + const message = this._options.packMessage(data); + this.send(message); + } + + /** + * Sends data without packing. + * + * @param {String|Blob|ArrayBuffer} data + */ + send(data) { + throwIf(!this.isOpened, `Can't send data because WebSocket is not opened.`); + this._ws.send(data); + this._onSend.dispatchAsync(data); + } + + /** + * Waits for particular message to come. + * + * @param {Function} predicate function to check incoming message. + * @param {Object} [options] + * @param {Number} [options.timeout=0] + * @param {Error} [options.timeoutError] + * @returns {Promise} + * + * @example + * const response = await wsp.waitUnpackedMessage(data => data && data.foo === 'bar'); + */ + waitUnpackedMessage(predicate, options = {}) { + throwIf(typeof predicate !== 'function', `Predicate must be a function, got ${typeof predicate}`); + if (options.timeout) { + setTimeout(() => { + if (this._promisedMap.has(predicate)) { + const error = options.timeoutError || new Error('Timeout'); + this._promisedMap.reject(predicate, error); + } + }, options.timeout); + } + return this._promisedMap.set(predicate); + } + + /** + * Closes WebSocket connection. If connection already closed, promise will be resolved with "close event". + * + * @param {number=} [code=1000] A numeric value indicating the status code. + * @param {string=} [reason] A human-readable reason for closing connection. + * @returns {Promise} + */ + close(code, reason) { // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close + return this.isClosed + ? Promise.resolve(this._closing.value) + : this._closing.call(() => this._ws.close(code, reason)); + } + + /** + * Removes all listeners from WSP instance. Useful for cleanup. + */ + removeAllListeners() { + this._onOpen.removeAllListeners(); + this._onMessage.removeAllListeners(); + this._onUnpackedMessage.removeAllListeners(); + this._onUnpackedNotif.removeAllListeners(); + this._onResponse.removeAllListeners(); + this._onSend.removeAllListeners(); + this._onClose.removeAllListeners(); + this._onError.removeAllListeners(); + } + + _createOpeningController() { + const connectionTimeout = this._options.connectionTimeout || this._options.timeout; + this._opening = new PromiseController({ + timeout: connectionTimeout, + timeoutReason: `Can't open WebSocket within allowed timeout: ${connectionTimeout} ms.` + }); + } + + _createClosingController() { + const closingTimeout = this._options.timeout; + this._closing = new PromiseController({ + timeout: closingTimeout, + timeoutReason: `Can't close WebSocket within allowed timeout: ${closingTimeout} ms.` + }); + } + + _createChannels() { + this._onOpen = new Channel(); + this._onMessage = new Channel(); + this._onUnpackedMessage = new Channel(); + this._onUnpackedNotif = new Channel(); + this._onResponse = new Channel(); + this._onSend = new Channel(); + this._onClose = new Channel(); + this._onError = new Channel(); + } + + _createWS() { + this._ws = this._options.createWebSocket(this._url); + this._wsSubscription = new Channel.Subscription([ + {channel: this._ws, event: 'open', listener: e => this._handleOpen(e)}, + {channel: this._ws, event: 'message', listener: e => this._handleMessage(e)}, + {channel: this._ws, event: 'error', listener: e => this._handleError(e)}, + {channel: this._ws, event: 'close', listener: e => this._handleClose(e)}, + ]).on(); + } + + _handleOpen(event) { + this._onOpen.dispatchAsync(event); + this._opening.resolve(event); + } + + _handleMessage(event) { + const data = this._options.extractMessageData(event); + this._onMessage.dispatchAsync(data); + this._tryUnpack(data); + } + + _tryUnpack(data) { + if (this._options.unpackMessage) { + data = this._options.unpackMessage(data); + if (isPromise(data)) { + data.then(data => this._handleUnpackedData(data)); + } else { + this._handleUnpackedData(data); + } } - }, options.timeout); - } - return this._promisedMap.set(predicate); - } - - /** - * Closes WebSocket connection. If connection already closed, promise will be resolved with "close event". - * - * @param {number=} [code=1000] A numeric value indicating the status code. - * @param {string=} [reason] A human-readable reason for closing connection. - * @returns {Promise} - */ - close(code, reason) { // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/close - return this.isClosed - ? Promise.resolve(this._closing.value) - : this._closing.call(() => this._ws.close(code, reason)); - } - - /** - * Removes all listeners from WSP instance. Useful for cleanup. - */ - removeAllListeners() { - this._onOpen.removeAllListeners(); - this._onMessage.removeAllListeners(); - this._onUnpackedMessage.removeAllListeners(); - this._onUnpackedNotif.removeAllListeners(); - this._onResponse.removeAllListeners(); - this._onSend.removeAllListeners(); - this._onClose.removeAllListeners(); - this._onError.removeAllListeners(); - } - - _createOpeningController() { - const connectionTimeout = this._options.connectionTimeout || this._options.timeout; - this._opening = new PromiseController({ - timeout: connectionTimeout, - timeoutReason: `Can't open WebSocket within allowed timeout: ${connectionTimeout} ms.` - }); - } - - _createClosingController() { - const closingTimeout = this._options.timeout; - this._closing = new PromiseController({ - timeout: closingTimeout, - timeoutReason: `Can't close WebSocket within allowed timeout: ${closingTimeout} ms.` - }); - } - - _createChannels() { - this._onOpen = new Channel(); - this._onMessage = new Channel(); - this._onUnpackedMessage = new Channel(); - this._onUnpackedNotif = new Channel(); - this._onResponse = new Channel(); - this._onSend = new Channel(); - this._onClose = new Channel(); - this._onError = new Channel(); - } - - _createWS() { - this._ws = this._options.createWebSocket(this._url); - this._wsSubscription = new Channel.Subscription([ - { channel: this._ws, event: 'open', listener: e => this._handleOpen(e) }, - { channel: this._ws, event: 'message', listener: e => this._handleMessage(e) }, - { channel: this._ws, event: 'error', listener: e => this._handleError(e) }, - { channel: this._ws, event: 'close', listener: e => this._handleClose(e) }, - ]).on(); - } - - _handleOpen(event) { - this._onOpen.dispatchAsync(event); - this._opening.resolve(event); - } - - _handleMessage(event) { - const data = this._options.extractMessageData(event); - this._onMessage.dispatchAsync(data); - this._tryUnpack(data); - } - - _tryUnpack(data) { - if (this._options.unpackMessage) { - data = this._options.unpackMessage(data); - if (isPromise(data)) { - data.then(data => this._handleUnpackedData(data)); - } else { - this._handleUnpackedData(data); - } - } - } - - _handleUnpackedData(data) { - if (data !== undefined) { - // todo: maybe trigger onUnpackedMessage always? - this._onUnpackedMessage.dispatchAsync(data); - if (!this._tryHandleResponse(data)) { - // message does not contain an `id` property, treat as a notification - this._onUnpackedNotif.dispatchAsync((data)); - } - } - this._tryHandleWaitingMessage(data); - } - - /** - * - * @param data - * @return {boolean} true if `given` data contains a requestId - * @private - */ - _tryHandleResponse(data) { - if (this._options.extractRequestId) { - const requestId = this._options.extractRequestId(data); - if (requestId) { - this._onResponse.dispatchAsync(data, requestId); - this._requests.resolve(requestId, data); - - return true; - } - } - - return false; - } - - _tryHandleWaitingMessage(data) { - this._promisedMap.forEach((_, predicate) => { - let isMatched = false; - try { - isMatched = predicate(data); - } catch (e) { - this._promisedMap.reject(predicate, e); - return; - } - if (isMatched) { - this._promisedMap.resolve(predicate, data); - } - }); - } - - _handleError(event) { - this._onError.dispatchAsync(event); - } - - _handleClose(event) { - this._onClose.dispatchAsync(event); - this._closing.resolve(event); - const error = new Error(`WebSocket closed with reason: ${event.reason} (${event.code}).`); - if (this._opening.isPending) { - this._opening.reject(error); - } - this._cleanup(error); - } - - _cleanupWS() { - if (this._wsSubscription) { - this._wsSubscription.off(); - this._wsSubscription = null; - } - this._ws = null; - } - - _cleanup(error) { - this._cleanupWS(); - this._requests.rejectAll(error); - } - - _assertOptions(options) { - Object.keys(options || {}).forEach(key => { - if (!defaultOptions.hasOwnProperty(key)) { - throw new Error(`Unknown option: ${key}`); - } - }); - } - - _assertPackingHandlers() { - const { packMessage, unpackMessage } = this._options; - throwIf(!packMessage || !unpackMessage, - `Please define 'options.packMessage / options.unpackMessage' for sending packed messages.` - ); - } - - _assertRequestIdHandlers() { - const { attachRequestId, extractRequestId } = this._options; - throwIf(!attachRequestId || !extractRequestId, - `Please define 'options.attachRequestId / options.extractRequestId' for sending requests.` - ); - } + } + + _handleUnpackedData(data) { + // It is possible for client code handling some kind message (binary message for example) in the onMessage + // listener, and the options.unpackMessage() method may return undefined for discard follow-up handling. + if (data === undefined) + return; + + // todo: maybe trigger onUnpackedMessage always? + this._onUnpackedMessage.dispatchAsync(data); + if (!this._tryHandleResponse(data)) { + // message does not contain an `id` property, treat as a notification + this._onUnpackedNotif.dispatchAsync((data)); + } + + this._tryHandleWaitingMessage(data); + } + + /** + * + * @param data + * @return {boolean} true if `given` data contains a requestId + * @private + */ + _tryHandleResponse(data) { + if (this._options.extractRequestId) { + const requestId = this._options.extractRequestId(data); + if (requestId) { + this._onResponse.dispatchAsync(data, requestId); + this._requests.resolve(requestId, data); + + return true; + } + } + + return false; + } + + _tryHandleWaitingMessage(data) { + this._promisedMap.forEach((_, predicate) => { + let isMatched = false; + try { + isMatched = predicate(data); + } catch (e) { + this._promisedMap.reject(predicate, e); + return; + } + if (isMatched) { + this._promisedMap.resolve(predicate, data); + } + }); + } + + _handleError(event) { + this._onError.dispatchAsync(event); + } + + _handleClose(event) { + this._onClose.dispatchAsync(event); + this._closing.resolve(event); + const error = new Error(`WebSocket closed with reason: ${event.reason} (${event.code}).`); + if (this._opening.isPending) { + this._opening.reject(error); + } + this._cleanup(error); + } + + _cleanupWS() { + if (this._wsSubscription) { + this._wsSubscription.off(); + this._wsSubscription = null; + } + this._ws = null; + } + + _cleanup(error) { + this._cleanupWS(); + this._requests.rejectAll(error); + } + + _assertOptions(options) { + Object.keys(options || {}).forEach(key => { + if (!defaultOptions.hasOwnProperty(key)) { + throw new Error(`Unknown option: ${key}`); + } + }); + } + + _assertPackingHandlers() { + const {packMessage, unpackMessage} = this._options; + throwIf(!packMessage || !unpackMessage, + `Please define 'options.packMessage / options.unpackMessage' for sending packed messages.` + ); + } + + _assertRequestIdHandlers() { + const {attachRequestId, extractRequestId} = this._options; + throwIf(!attachRequestId || !extractRequestId, + `Please define 'options.attachRequestId / options.extractRequestId' for sending requests.` + ); + } } module.exports = WebSocketAsPromised; From 141b81a6e10a8db10e33b23fabe21445d35e8f2c Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 14 Jul 2024 10:22:04 +0800 Subject: [PATCH 14/17] 2.0.4 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b3f9ddc..46ba483 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "websocket-as-promised", - "version": "2.0.3", + "version": "2.0.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "websocket-as-promised", - "version": "2.0.1", + "version": "2.0.4", "license": "MIT", "dependencies": { "chnl": "^1.2.0", From 0b35a8d6dd88ad1e270cb4e151f94d6044ab5bbc Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 14 Jul 2024 10:26:14 +0800 Subject: [PATCH 15/17] 2.0.4 From db0be53d0f3d9a83136b275a700788868f8ebb4b Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 14 Jul 2024 10:28:16 +0800 Subject: [PATCH 16/17] 2.0.4 From ff5f40179f0ad581c369102a1f3ce67d182d8c57 Mon Sep 17 00:00:00 2001 From: KwanKin Yau Date: Sun, 14 Jul 2024 10:31:51 +0800 Subject: [PATCH 17/17] 2.0.4