├── .coveragebadgesrc
├── .editorconfig
├── .eslintrc.json
├── .github
└── workflows
│ └── test.yml
├── .gitignore
├── .nycrc
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── package.json
├── src
├── codec.js
├── errors.js
├── id-generator.js
├── index.js
└── request.js
└── tests
├── benchmark.js
├── create.js
└── index.test.js
/.coveragebadgesrc:
--------------------------------------------------------------------------------
1 | {
2 | "source": "./coverage/coverage-summary.json",
3 | "attribute": "total.statements.pct",
4 | "outputDir": "./coverage/badges"
5 | }
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "standard",
3 | "env": {
4 | "node": true,
5 | "browser": true
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on:
3 | push:
4 | branches: [ main ]
5 | pull_request:
6 | branches: [ main ]
7 | jobs:
8 | test:
9 | timeout-minutes: 60
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: 16
16 | - name: Install dependencies
17 | run: npm install
18 | - name: Run tests
19 | run: npm test
20 | coverage:
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v3
24 | - uses: actions/setup-node@v3
25 | with:
26 | node-version: 16
27 | - run: npm install
28 | - run: npm run coverage
29 | - run: npm run make-badge
30 | - name: Publish coverage report to GitHub Pages
31 | uses: JamesIves/github-pages-deploy-action@v4
32 | with:
33 | folder: coverage
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (https://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # TypeScript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # Yarn Integrity file
55 | .yarn-integrity
56 |
57 | # dotenv environment variables file
58 | .env
59 |
60 | # next.js build output
61 | .next
62 |
63 | jsconfig.json
64 | package-lock.json
65 | dist
66 |
--------------------------------------------------------------------------------
/.nycrc:
--------------------------------------------------------------------------------
1 | {
2 | "reporter": [
3 | "html",
4 | "json-summary",
5 | "text"
6 | ],
7 | "lines": 95,
8 | "branches": "82",
9 | "statements": "95"
10 | }
11 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to nanomessage
2 |
3 | ## Issue Contributions
4 |
5 | When opening new issues or commenting on existing issues on this repository
6 | please make sure discussions are related to concrete technical issues.
7 |
8 | Try to be *friendly* (we are not animals :monkey: or bad people :rage4:) and explain correctly how we can reproduce your issue.
9 |
10 | ## Code Contributions
11 |
12 | This document will guide you through the contribution process.
13 |
14 | ### Step 1: Fork
15 |
16 | Fork the project [on GitHub](https://github.com/geut/nanomessage) and check out your copy locally.
17 |
18 | ```bash
19 | $ git clone git@github.com:username/nanomessage.git
20 | $ cd nanomessage
21 | $ npm install
22 | $ git remote add upstream git://github.com/geut/nanomessage.git
23 | ```
24 |
25 | ### Step 2: Branch
26 |
27 | Create a feature branch and start hacking:
28 |
29 | ```bash
30 | $ git checkout -b my-feature-branch -t origin/main
31 | ```
32 |
33 | ### Step 3: Test
34 |
35 | Bug fixes and features **should come with tests**. We use [uvu](https://github.com/lukeed/uvu) to do that.
36 |
37 | ```bash
38 | $ npm test
39 | ```
40 |
41 | ### Step 4: Lint
42 |
43 | Make sure the linter is happy and that all tests pass. Please, do not submit
44 | patches that fail either check.
45 |
46 | We use [standard](https://standardjs.com/)
47 |
48 | ### Step 5: Commit
49 |
50 | Make sure git knows your name and email address:
51 |
52 | ```bash
53 | $ git config --global user.name "Bruce Wayne"
54 | $ git config --global user.email "bruce@batman.com"
55 | ```
56 |
57 | Writing good commit logs is important. A commit log should describe what
58 | changed and why.
59 |
60 | ### Step 6: Changelog
61 |
62 | If your changes are really important for the project probably the users want to know about it.
63 |
64 | We use [chan](https://github.com/geut/chan/) to maintain a well readable changelog for our users.
65 |
66 | ### Step 7: Push
67 |
68 | ```bash
69 | $ git push origin my-feature-branch
70 | ```
71 |
72 | ### Step 8: Make a pull request ;)
73 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 GEUT
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # nanomessage (aka nm)
2 |
3 | 
4 | [](https://geut.github.io/nanomessage/)
5 | [](https://standardjs.com)
6 | [](https://github.com/RichardLitt/standard-readme)
7 |
8 | > Simple module that helps you to build a `request-response` abstraction on top of any other solution (e.g. streams).
9 |
10 | ## Install
11 |
12 | ```
13 | $ npm install nanomessage
14 | ```
15 |
16 | ## Usage
17 |
18 | ```javascript
19 | import WebSocket from 'ws'
20 |
21 | import { Nanomessage } from 'nanomessage'
22 |
23 | // server.js
24 | const server = new WebSocket.Server({ port: 3000 })
25 | server.on('connection', function connection (ws) {
26 | const nm = new Nanomessage({
27 | subscribe (ondata) {
28 | // Define how to read data
29 | ws.on('message', ondata)
30 | },
31 | send (msg) {
32 | // Define how to send data
33 | ws.send(msg)
34 | },
35 | onMessage (msg, opts) {
36 | // Process the new request and return a response
37 | console.log(msg)
38 | return 'pong from Alice'
39 | }
40 | })
41 |
42 | nm.open().catch(err => console.error(err))
43 | })
44 |
45 | // client.js
46 | const ws = new WebSocket('ws://127.0.0.1:3000')
47 | const Bob = new Nanomessage({
48 | async open() {
49 | if (ws.readyState === 0) {
50 | await new Promise(resolve => ws.once('open', resolve))
51 | }
52 | },
53 | subscribe (ondata) {
54 | ws.on('message', ondata)
55 | },
56 | send (msg) {
57 | ws.send(msg)
58 | }
59 | })
60 |
61 | ;(async () => {
62 | await Bob.open()
63 | console.log(await Bob.request('ping from Bob'))
64 | })()
65 | ```
66 |
67 | ## API
68 |
69 | #### `const nm = new Nanomessage(options)`
70 |
71 | Create a new nanomessage.
72 |
73 | Options include:
74 |
75 | - `send: (chunk: Buffer, info: Object) => (Promise|undefined)`: Defines how to send the messages provide it by nanomessage to the low level solution.
76 | - `subscribe: (onData: buf => Promise) => UnsubscribeFunction`: Defines how to read data from the low level solution.
77 | - `onMessage: (msg: *, info: Object) => Promise`: Async handler to process the incoming requests.
78 | - `open: () => Promise`: Defines a function to run before the nanomessage instance is opened.
79 | - `close: () => Promise`: Defines a function to run after the nanomessage instance was close.
80 | - `timeout: null`: Time to wait for the response of a request. Disabled by default.
81 | - `concurrency: { incoming: 256, outgoing: 256 }`: Defines how many requests do you want to run (outgoing) and process (incoming) in concurrent.
82 | - `valueEncoding: msgpackr`: Defines a [compatible codec](https://github.com/mafintosh/codecs) to encode/decode messages in nanomessage. By default use: [msgpackr](https://github.com/kriszyp/msgpackr)
83 |
84 | `info` is an object with:
85 |
86 | - `info.id: Number`: Incremental ID request.
87 | - `info.data: *`: Plain data to send.
88 | - `info.ephemeral: boolean`: It's true if the message is ephemeral.
89 | - `info.response: boolean`: It's true if the message is a response.
90 | - `info.responseData: *`: Plain data to response.
91 |
92 | You can also extend from this prototype if you prefer:
93 |
94 | ```javascript
95 | const { Nanomessage } = require('nanomessage')
96 |
97 | class CustomNanomessage exports Nanomessage {
98 | constructor (...args) {
99 | super(...args)
100 | }
101 |
102 | _subscribe (onData) {}
103 |
104 | async _send (chunk, info) {}
105 |
106 | async _onMessage (msg, info) {}
107 |
108 | async _open() {
109 | await super._open()
110 | }
111 |
112 | async _close () {
113 | await super._close()
114 | }
115 | }
116 | ```
117 |
118 | #### `nm.requests: Array`
119 |
120 | Get the current list of requests (inflight and pending).
121 |
122 | #### `nm.inflightRequests: Number`
123 |
124 | Number of requests processing in the queue.
125 |
126 | #### `nm.requestTimeout: Number`
127 |
128 | Get the current request timeout.
129 |
130 | #### `nm.concurrency: { incoming: Number, outgoing: Number }`
131 |
132 | Get the current concurrency.
133 |
134 | #### `nm.setRequestsTimeout(Number)`
135 |
136 | Change the timeout for the future requests.
137 |
138 | #### `nm.setConcurrency(Number | { incoming: Number, outgoing: Number })`
139 |
140 | Update the concurrency number of operations for incoming and outgoing requests.
141 |
142 | #### `nm.open() => Promise`
143 |
144 | Opens nanomessage and start listening for incoming data.
145 |
146 | #### `nm.close() => Promise`
147 |
148 | Closes nanomessage and unsubscribe from incoming data.
149 |
150 | #### `nm.request(data, [opts]) => Promise`
151 |
152 | Send a request and wait for a response. `data` can be any serializable type supported by your codec.
153 |
154 | - `opts.timeout: number`: Define a custom timeout for the current request.
155 | - `opts.signal: AbortSignal`: Set an abort signal object to cancel the request.
156 |
157 | #### `nm.send(data) => Promise`
158 |
159 | Send a `ephemeral` message. `data` can be any serializable type supported by your codec.
160 |
161 | #### `nm.processIncomingMessage(buf: Buffer) => Promise`
162 |
163 | Access directly to the handler of incoming messages. It's recommended to use the subscription model instead.
164 |
165 | #### `nm.setMessageHandler(handler) => Nanomessage`
166 |
167 | Defines a request handler. It will override the old handler.
168 |
169 | ## Issues
170 |
171 | :bug: If you found an issue we encourage you to report it on [github](https://github.com/geut/nanomessage/issues). Please specify your OS and the actions to reproduce it.
172 |
173 | ## Contributing
174 |
175 | :busts_in_silhouette: Ideas and contributions to the project are welcome. You must follow this [guideline](https://github.com/geut/nanomessage/blob/main/CONTRIBUTING.md).
176 |
177 | ## License
178 |
179 | MIT © A [**GEUT**](http://geutstudio.com/) project
180 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nanomessage",
3 | "version": "11.1.1",
4 | "description": "Simple module that helps you to build a `request-response` abstraction on top of any other solution (e.g. streams).",
5 | "type": "module",
6 | "main": "./dist/index.cjs",
7 | "exports": {
8 | ".": {
9 | "require": "./dist/index.cjs",
10 | "import": "./src/index.js"
11 | },
12 | "./package.json": "./package.json"
13 | },
14 | "files": [
15 | "dist",
16 | "src"
17 | ],
18 | "scripts": {
19 | "benchmark": "node tests/benchmark.js",
20 | "build": "tsup",
21 | "test": "uvu -i create -i benchmark",
22 | "posttest": "npm run lint",
23 | "lint": "standard",
24 | "prepublishOnly": "npm test && npm run build",
25 | "coverage": "c8 uvu -i create -i benchmark",
26 | "make-badge": "coverage-badges"
27 | },
28 | "dependencies": {
29 | "fastq": "^1.8.0",
30 | "msgpackr": "^1.6.3",
31 | "nanocustomassert": "^1.0.0",
32 | "nanoerror": "^1.3.0",
33 | "nanoresource-promise": "^3.1.0"
34 | },
35 | "devDependencies": {
36 | "abortcontroller-polyfill": "^1.7.3",
37 | "c8": "^7.12.0",
38 | "coverage-badges": "^1.0.7",
39 | "standard": "^17.0.0",
40 | "streamx": "^2.6.4",
41 | "tinybench": "^2.3.1",
42 | "tinyspy": "^1.0.2",
43 | "tsup": "^6.3.0",
44 | "uvu": "^0.5.6"
45 | },
46 | "standard": {
47 | "env": [
48 | "node",
49 | "browser"
50 | ]
51 | },
52 | "tsup": {
53 | "entry": [
54 | "src/index.js"
55 | ],
56 | "format": [
57 | "cjs",
58 | "iife"
59 | ],
60 | "globalName": "Nanomessage",
61 | "splitting": false,
62 | "sourcemap": true,
63 | "clean": true
64 | },
65 | "repository": {
66 | "type": "git",
67 | "url": "git+https://github.com/geut/nanomessage.git"
68 | },
69 | "keywords": [
70 | "nano",
71 | "geut",
72 | "request",
73 | "request-response",
74 | "websocket",
75 | "socket",
76 | "stream"
77 | ],
78 | "author": {
79 | "name": "GEUT",
80 | "email": "contact@geutstudio.com"
81 | },
82 | "license": "MIT",
83 | "bugs": {
84 | "url": "https://github.com/geut/nanomessage/issues"
85 | },
86 | "homepage": "https://github.com/geut/nanomessage#readme",
87 | "publishConfig": {
88 | "access": "public"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/codec.js:
--------------------------------------------------------------------------------
1 | import { Packr } from 'msgpackr'
2 |
3 | import { RequestInfo } from './request.js'
4 | import {
5 | encodeError,
6 | decodeError,
7 | NM_ERR_ENCODE,
8 | NM_ERR_DECODE
9 | } from './errors.js'
10 |
11 | const ATTR_RESPONSE = 1
12 | const ATTR_ERROR = 1 << 1
13 |
14 | export const createPackr = (opts = {}) => {
15 | return new Packr({
16 | useRecords: true,
17 | ...opts
18 | })
19 | }
20 |
21 | const staticPackr = createPackr({
22 | structures: [
23 | ['id', 'header', 'data'],
24 | ['code', 'unformatMessage', 'args', 'metadata']
25 | ]
26 | })
27 |
28 | const dynamicPackr = createPackr()
29 | const defaultValueEncoding = {
30 | encode: (data) => dynamicPackr.pack(data),
31 | decode: (data) => dynamicPackr.unpack(data)
32 | }
33 |
34 | export function createCodec (valueEncoding = defaultValueEncoding) {
35 | return {
36 | encode (info) {
37 | try {
38 | let header = 0
39 | if (info.response) header = header | ATTR_RESPONSE
40 | if (info.error) header = header | ATTR_ERROR
41 |
42 | let data = info.response ? info.responseData : info.data
43 | data = info.error ? encodeError(data) : data
44 | const buf = staticPackr.pack({
45 | id: info.id,
46 | header,
47 | data: info.error ? staticPackr.pack(data) : valueEncoding.encode(data)
48 | })
49 | return buf
50 | } catch (err) {
51 | throw NM_ERR_ENCODE.from(err)
52 | }
53 | },
54 |
55 | decode (buf, context) {
56 | try {
57 | const { id, header, data } = staticPackr.unpack(buf)
58 |
59 | const response = !!(header & ATTR_RESPONSE)
60 | const error = !!(header & ATTR_ERROR)
61 |
62 | return new RequestInfo(
63 | id,
64 | response,
65 | error,
66 | context,
67 | null,
68 | () => {
69 | if (error) return decodeError(staticPackr.unpack(data))
70 | return valueEncoding.decode(data)
71 | }
72 | )
73 | } catch (err) {
74 | throw NM_ERR_DECODE.from(err)
75 | }
76 | }
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/errors.js:
--------------------------------------------------------------------------------
1 | import nanoerror from 'nanoerror'
2 |
3 | const errors = new Map()
4 |
5 | export function createError (code, message, persist = false) {
6 | const err = nanoerror(code, message)
7 | if (persist) errors.set(code, err)
8 | return err
9 | }
10 |
11 | export function encodeError (err) {
12 | return {
13 | code: err.code,
14 | unformatMessage: err.unformatMessage,
15 | args: err.args,
16 | metadata: err.metadata
17 | }
18 | }
19 |
20 | export function decodeError ({ code, unformatMessage, args = [], metadata = {} }) {
21 | const ErrorDecoded = errors.get(code) || nanoerror(code, unformatMessage)
22 | const err = new ErrorDecoded(...args)
23 | err.metadata = metadata
24 | return err
25 | }
26 |
27 | export const NM_ERR_TIMEOUT = createError('NM_ERR_TIMEOUT', 'timeout on request: %s', true)
28 | export const NM_ERR_ENCODE = createError('NM_ERR_ENCODE', 'error encoding the request: %s', true)
29 | export const NM_ERR_DECODE = createError('NM_ERR_DECODE', 'error decoding the request: %s', true)
30 | export const NM_ERR_MESSAGE = createError('NM_ERR_MESSAGE', 'on message error: %s', true)
31 | export const NM_ERR_REMOTE_RESPONSE = createError('NM_ERR_REMOTE_RESPONSE', 'remote response error: %s', true)
32 | export const NM_ERR_CLOSE = createError('NM_ERR_CLOSE', 'nanomessage was closed', true)
33 | export const NM_ERR_NOT_OPEN = createError('NM_ERR_NOT_OPEN', 'nanomessage is not open', true)
34 | export const NM_ERR_CANCEL = createError('NM_ERR_CANCEL', '%o', true)
35 |
--------------------------------------------------------------------------------
/src/id-generator.js:
--------------------------------------------------------------------------------
1 | export default class IdGenerator {
2 | constructor (generate) {
3 | this._generate = generate
4 | this._free = []
5 | }
6 |
7 | get () {
8 | if (!this._free.length) {
9 | return this._generate()
10 | }
11 |
12 | return this._free.pop()
13 | }
14 |
15 | release (id) {
16 | this._free.push(id)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { NanoresourcePromise } from 'nanoresource-promise/emitter2'
2 | import fastq from 'fastq'
3 |
4 | import Request from './request.js'
5 | import { createCodec, createPackr } from './codec.js'
6 | import {
7 | NM_ERR_CLOSE,
8 | NM_ERR_NOT_OPEN,
9 | NM_ERR_MESSAGE,
10 | NM_ERR_REMOTE_RESPONSE
11 | } from './errors.js'
12 | import IdGenerator from './id-generator.js'
13 |
14 | const VOID_RESPONSE = Symbol('VOID_RESPONSE')
15 | const kRequests = Symbol('nanomessage.requests')
16 | const kInQueue = Symbol('nanomessage.inqueue')
17 | const kOutQueue = Symbol('nanomessage.outqueue')
18 | const kUnsubscribe = Symbol('nanomessage.unsubscribe')
19 | const kOpen = Symbol('nanomessage.open')
20 | const kClose = Symbol('nanomessage.close')
21 | const kFastCheckOpen = Symbol('nanomessage.fastcheckopen')
22 | const kTimeout = Symbol('nanomessage.timeout')
23 | const kIdGenerator = Symbol('nanomessage.idgenerator')
24 | const kCodec = Symbol('nanomessage.codec')
25 |
26 | function inWorker ({ info, onMessage }, done) {
27 | this[kFastCheckOpen]()
28 | .then(() => onMessage(info.data, info))
29 | .catch(_err => {
30 | if (_err.isNanoerror) return _err
31 | const err = NM_ERR_REMOTE_RESPONSE.from(_err)
32 | err.metadata = _err.metadata
33 | return err
34 | })
35 | .then(async data => {
36 | if (VOID_RESPONSE === data || this.closed || this.closing) return
37 |
38 | info.response = true
39 | info.responseData = data
40 | info.error = !!(data?.isNanoerror)
41 |
42 | await this._send(this[kCodec].encode(info), info)
43 |
44 | if (info.error) throw data
45 | })
46 | .then(() => done())
47 | .catch(err => {
48 | if (err.isNanoerror) return done(err)
49 | done(NM_ERR_MESSAGE.from(err))
50 | })
51 | }
52 |
53 | function outWorker (request, done) {
54 | const info = request.info()
55 | this[kFastCheckOpen]()
56 | .then(() => {
57 | if (request.finished) return
58 | request.start()
59 | return this._send(this[kCodec].encode(info), info)
60 | })
61 | .then(() => {
62 | if (request.finished) return
63 | return request.promise
64 | })
65 | .then(data => done(null, data))
66 | .catch(err => done(err))
67 | }
68 |
69 | export * from './errors.js'
70 |
71 | export { createPackr, VOID_RESPONSE }
72 | export class Nanomessage extends NanoresourcePromise {
73 | /**
74 | * Creates an instance of Nanomessage.
75 | * @param {Object} [opts={}]
76 | * @param {(buf: Buffer, info: Object) => Promise|undefined} [opts.send]
77 | * @param {function} [opts.subscribe]
78 | * @param {(data: Object, info: Object) => Promise<*>} [opts.onMessage]
79 | * @param {function} [opts.open]
80 | * @param {function} [opts.close]
81 | * @param {number} [opts.timeout]
82 | * @param {Object} [opts.valueEncoding]
83 | * @param {({ incoming: number, outgoing: number }|number)} [opts.concurrency]
84 | * @memberof Nanomessage
85 | */
86 | constructor (opts = {}) {
87 | super()
88 |
89 | const { send, subscribe, onMessage, open, close, timeout, valueEncoding, concurrency = 256 } = opts
90 |
91 | if (send) this._send = send
92 | if (subscribe) this._subscribe = subscribe
93 | if (onMessage) this.setMessageHandler(onMessage)
94 | if (open) this[kOpen] = open
95 | if (close) this[kClose] = close
96 | this.setRequestTimeout(timeout)
97 |
98 | this[kCodec] = createCodec(valueEncoding)
99 |
100 | this[kInQueue] = fastq(this, inWorker, 1)
101 | this[kOutQueue] = fastq(this, outWorker, 1)
102 | this.setConcurrency(concurrency)
103 |
104 | this[kRequests] = new Map()
105 | this[kIdGenerator] = new IdGenerator(() => this[kRequests].size + 1)
106 | }
107 |
108 | /**
109 | * @readonly
110 | * @type {Object}
111 | */
112 | get codec () {
113 | return this[kCodec]
114 | }
115 |
116 | /**
117 | * @readonly
118 | * @type {Array}
119 | */
120 | get requests () {
121 | return Array.from(this[kRequests].values())
122 | }
123 |
124 | /**
125 | * @readonly
126 | * @type {number}
127 | */
128 | get inflightRequests () {
129 | return this[kOutQueue].running()
130 | }
131 |
132 | /**
133 | * @readonly
134 | * @type {number}
135 | */
136 | get requestTimeout () {
137 | return this[kTimeout]
138 | }
139 |
140 | /**
141 | * @readonly
142 | * @type {Object}
143 | */
144 | get concurrency () {
145 | return {
146 | incoming: this[kInQueue].concurrency,
147 | outgoing: this[kOutQueue].concurrency
148 | }
149 | }
150 |
151 | /**
152 | * @param {number} timeout
153 | * @returns {Nanomessage}
154 | */
155 | setRequestTimeout (timeout) {
156 | this[kTimeout] = timeout
157 | return this
158 | }
159 |
160 | /**
161 | * @param {({ incoming: number, outgoing: number }|number)} value
162 | * @returns {Nanomessage}
163 | */
164 | setConcurrency (value) {
165 | if (typeof value === 'number') {
166 | this[kInQueue].concurrency = value
167 | this[kOutQueue].concurrency = value
168 | } else {
169 | this[kInQueue].concurrency = value.incoming || this[kInQueue].concurrency
170 | this[kOutQueue].concurrency = value.outgoing || this[kOutQueue].concurrency
171 | }
172 | return this
173 | }
174 |
175 | /**
176 | * Send a request and wait for the response.
177 | *
178 | * @param {*} data
179 | * @param {Object} [opts]
180 | * @param {number} [opts.timeout]
181 | * @param {AbortSignal} [opts.signal]
182 | * @param {function} [opts.onCancel]
183 | * @param {*} [opts.context]
184 | * @returns {Promise<*>}
185 | */
186 | async request (data, opts = {}) {
187 | if (this.closed || this.closing) throw new NM_ERR_CLOSE()
188 |
189 | const request = new Request({ id: this[kIdGenerator].get(), data, timeout: opts.timeout || this[kTimeout], signal: opts.signal, context: opts.context, onCancel: opts.onCancel })
190 | const info = request.info()
191 |
192 | this[kRequests].set(request.id, request)
193 | request.onFinish(() => {
194 | this[kRequests].delete(request.id)
195 | this[kIdGenerator].release(request.id)
196 | })
197 |
198 | this.emit('request-created', info)
199 |
200 | this[kOutQueue].push(request, (err, data) => {
201 | if (err) request.reject(err)
202 | info.response = true
203 | info.responseData = data
204 | this.emit('request-ended', err, info)
205 | })
206 |
207 | return request.promise
208 | }
209 |
210 | /**
211 | * Send a ephemeral message.
212 | *
213 | * @param {*} data
214 | * @param {Object} [opts]
215 | * @param {Object} [opts.context]
216 | * @returns {Promise}
217 | */
218 | send (data, opts = {}) {
219 | return this[kFastCheckOpen]()
220 | .then(() => {
221 | const info = Request.info({ id: 0, data, context: opts.context })
222 | return this._send(this[kCodec].encode(info), info)
223 | })
224 | }
225 |
226 | /**
227 | * @param {(data: Object, info: Object) => Promise<*>} onMessage
228 | * @returns {Nanomessage}
229 | */
230 | setMessageHandler (onMessage) {
231 | this._onMessage = onMessage
232 | return this
233 | }
234 |
235 | /**
236 | * @param {Buffer} buf
237 | * @param {Object} [opts]
238 | * @param {function} [opts.onMessage]
239 | * @param {*} [opts.context]
240 | * @returns {Promise}
241 | */
242 | async processIncomingMessage (buf, opts = {}) {
243 | if (this.closed || this.closing) return
244 |
245 | const { onMessage = this._onMessage, context } = opts
246 |
247 | const info = this[kCodec].decode(buf, context)
248 |
249 | // resolve response
250 | if (info.response) {
251 | const request = this[kRequests].get(info.id)
252 | if (request) {
253 | if (info.error) {
254 | request.reject(info.data)
255 | } else {
256 | request.resolve(info.data)
257 | }
258 | }
259 | return new Promise(resolve => resolve(info))
260 | }
261 |
262 | this.emit('message', info)
263 |
264 | if (info.ephemeral) {
265 | return this[kFastCheckOpen]()
266 | .then(() => onMessage(info.data, info))
267 | .then(() => info)
268 | .catch(err => {
269 | if (!err.isNanoerror) {
270 | err = NM_ERR_MESSAGE.from(err)
271 | }
272 | this.emit('error-message', err, info)
273 | throw err
274 | })
275 | }
276 |
277 | return new Promise((resolve, reject) => this[kInQueue].push({ info, onMessage }, err => {
278 | if (err) {
279 | this.emit('error-message', err, info)
280 | reject(err)
281 | } else {
282 | resolve(info)
283 | }
284 | }))
285 | }
286 |
287 | /**
288 | * @abstract
289 | * @param {Buffer} buf
290 | * @param {Object} info
291 | * @returns {Promise|undefined}
292 | */
293 | async _send (buf, info) {
294 | throw new Error('_send not implemented')
295 | }
296 |
297 | /**
298 | * @abstract
299 | * @param {Object} data
300 | * @param {Object} info
301 | * @returns {Promise<*>}
302 | */
303 | async _onMessage (data, info) {}
304 |
305 | async _open () {
306 | await (this[kOpen] && this[kOpen]())
307 | const opts = {
308 | onMessage: this._onMessage.bind(this)
309 | }
310 | const processIncomingMessage = buf => this.processIncomingMessage(buf, opts)
311 | this[kUnsubscribe] = this._subscribe && this._subscribe(processIncomingMessage)
312 | }
313 |
314 | async _close () {
315 | if (this[kUnsubscribe]) this[kUnsubscribe]()
316 |
317 | const requestsToClose = []
318 | this[kRequests].forEach(request => request.reject(new NM_ERR_CLOSE()))
319 | this[kRequests].clear()
320 |
321 | this[kInQueue] && this[kInQueue].kill()
322 | this[kOutQueue] && this[kOutQueue].kill()
323 |
324 | await (this[kClose] && this[kClose]())
325 | await Promise.all(requestsToClose)
326 | }
327 |
328 | async [kFastCheckOpen] () {
329 | if (this.closed || this.closing) throw new NM_ERR_CLOSE()
330 | if (this.opening) return this.open()
331 | if (!this.opened) throw new NM_ERR_NOT_OPEN()
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/src/request.js:
--------------------------------------------------------------------------------
1 | import { NM_ERR_CANCEL, NM_ERR_TIMEOUT } from './errors.js'
2 |
3 | const kEmpty = Symbol('empty')
4 | export class RequestInfo {
5 | constructor (id, response = false, error = false, context = {}, data, getData) {
6 | this.id = id
7 | this.response = response
8 | this.ephemeral = id === 0
9 | this.error = error
10 | this.context = context
11 | this.responseData = undefined
12 | this._data = getData ? kEmpty : data
13 | this._getData = getData
14 | }
15 |
16 | get data () {
17 | if (this._data !== kEmpty) return this._data
18 | this._data = this._getData()
19 | return this._data
20 | }
21 |
22 | toJSON () {
23 | return {
24 | id: this.id,
25 | data: this.data,
26 | response: this.response,
27 | ephemeral: this.ephemeral,
28 | error: this.error,
29 | context: this.context,
30 | responseData: this.responseData
31 | }
32 | }
33 | }
34 | export default class Request {
35 | static info (obj = {}) {
36 | return new RequestInfo(obj.id, obj.response, obj.error, obj.context, obj.data)
37 | }
38 |
39 | constructor (opts = {}) {
40 | const {
41 | id,
42 | data,
43 | response = false,
44 | timeout,
45 | signal,
46 | context = {},
47 | onCancel = (aborted = false) => new NM_ERR_CANCEL({ id, timeout, aborted, context })
48 | } = opts
49 |
50 | this.id = id
51 | this.data = data
52 | this.response = response
53 | this.finished = false
54 | this.timeout = timeout
55 | this.context = context
56 | this.timer = null
57 |
58 | let _resolve, _reject
59 | this.promise = new Promise((resolve, reject) => {
60 | _resolve = resolve
61 | _reject = reject
62 | })
63 |
64 | const onAbort = () => {
65 | this.reject(onCancel(true) || new NM_ERR_CANCEL({ id, timeout, aborted: true }))
66 | }
67 |
68 | if (signal) {
69 | if (signal.aborted) {
70 | queueMicrotask(onAbort)
71 | } else {
72 | signal.addEventListener('abort', onAbort)
73 | }
74 | }
75 |
76 | this.resolve = (data) => {
77 | if (!this.finished) {
78 | this.timer && clearTimeout(this.timer)
79 | signal && signal.removeEventListener('abort', onAbort)
80 | this.finished = true
81 | this._onFinish()
82 | _resolve(data)
83 | }
84 | }
85 |
86 | this.reject = (err) => {
87 | if (!this.finished) {
88 | this.timer && clearTimeout(this.timer)
89 | signal && signal.removeEventListener('abort', onAbort)
90 | this.finished = true
91 | this._onFinish(err)
92 | _reject(err)
93 | }
94 | }
95 | }
96 |
97 | start () {
98 | if (this.timeout) {
99 | this.timer = setTimeout(() => {
100 | this.reject(new NM_ERR_TIMEOUT(this.id))
101 | }, this.timeout)
102 | }
103 | }
104 |
105 | onFinish (cb) {
106 | this._onFinish = cb
107 | }
108 |
109 | info () {
110 | return Request.info(this)
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/tests/benchmark.js:
--------------------------------------------------------------------------------
1 | import { Bench } from 'tinybench'
2 |
3 | import create from './create.js'
4 | import { Nanomessage, createPackr } from '../src/index.js'
5 |
6 | const context = {
7 | basic: async () => {
8 | const [alice, bob] = create({
9 | onMessage: () => ({ value: 'pong' })
10 | }, {
11 | onMessage: () => ({ value: 'pong' })
12 | })
13 | await alice.open()
14 | await bob.open()
15 | return { alice, bob }
16 | },
17 | sharedStructures: async () => {
18 | const packr = createPackr({
19 | structures: []
20 | })
21 |
22 | const valueEncoding = {
23 | encode: (data) => packr.pack(data),
24 | decode: (data) => packr.unpack(data)
25 | }
26 |
27 | const [alice, bob] = create({
28 | valueEncoding,
29 | onMessage: () => ({ value: 'pong' })
30 | }, {
31 | valueEncoding,
32 | onMessage: () => ({ value: 'pong' })
33 | })
34 | await alice.open()
35 | await bob.open()
36 | return { alice, bob }
37 | }
38 | }
39 |
40 | const bench = new Bench({
41 | time: 0,
42 | iterations: 10_000,
43 | setup: async (task) => {
44 | if (task.name.includes('sharedStructures')) {
45 | task.context = await context.sharedStructures()
46 | } else {
47 | task.context = await context.basic()
48 | }
49 | }
50 | })
51 |
52 | bench
53 | .add('execute 10000 requests x 2 peers', async function () {
54 | const { alice } = this.context
55 | const res = await alice.request({ value: 'ping' })
56 | if (res.value !== 'pong') throw new Error('wrong')
57 | })
58 | .add('execute 10000 ephemeral messages x 2 peers', async function () {
59 | const { alice, bob } = this.context
60 | await alice.send('test')
61 | await Nanomessage.once(bob, 'message')
62 | })
63 | .add('execute 10000 requests x 2 peers using sharedStructures', async function () {
64 | const { alice } = this.context
65 | const res = await alice.request({ value: 'ping' })
66 | if (res.value !== 'pong') throw new Error('wrong')
67 | })
68 |
69 | await bench.run()
70 |
71 | console.table(
72 | bench.tasks.map(({ name, result }) => (result
73 | ? ({
74 | 'Task Name': name,
75 | 'ops/sec': parseInt(result.hz, 10),
76 | 'Total Time (ms)': Math.round(result.totalTime),
77 | 'Average Time (ns)': Math.round(result.mean * 1000 * 1000),
78 | Margin: `\xb1${result.rme.toFixed(2)}%`,
79 | Samples: result.samples.length
80 | })
81 | : null))
82 | )
83 |
--------------------------------------------------------------------------------
/tests/create.js:
--------------------------------------------------------------------------------
1 | import { Duplex } from 'streamx'
2 |
3 | import { Nanomessage } from '../src/index.js'
4 |
5 | function createFromStream (stream, options = {}) {
6 | const { onSend = () => {}, onClose = () => {}, ...nmOptions } = options
7 |
8 | const nm = new Nanomessage(Object.assign({
9 | subscribe (ondata) {
10 | stream.on('data', (data) => {
11 | ondata(data).catch(err => {
12 | nm.emit('subscribe-error', err)
13 | })
14 | })
15 |
16 | return () => {
17 | nm.emit('unsubscribe')
18 | }
19 | },
20 | send (chunk, info) {
21 | onSend(chunk, info)
22 | if (stream.destroyed) return
23 | stream.write(chunk)
24 | },
25 | close () {
26 | onClose()
27 | if (stream.destroyed) return
28 | return new Promise(resolve => {
29 | stream.once('close', () => resolve())
30 | stream.destroy()
31 | })
32 | }
33 | }, nmOptions))
34 |
35 | nm.open().catch((err) => {
36 | console.log(err)
37 | })
38 |
39 | stream.on('close', () => {
40 | nm.close()
41 | })
42 |
43 | nm.stream = stream
44 |
45 | return nm
46 | }
47 |
48 | export default function create (aliceOpts = { onMessage () {} }, bobOpts = { onMessage () {} }) {
49 | const stream1 = new Duplex({
50 | write (data, cb) {
51 | stream2.push(data)
52 | cb()
53 | }
54 | })
55 | const stream2 = new Duplex({
56 | write (data, cb) {
57 | stream1.push(data)
58 | cb()
59 | }
60 | })
61 |
62 | const alice = createFromStream(stream1, aliceOpts)
63 | const bob = createFromStream(stream2, bobOpts)
64 |
65 | return [alice, bob]
66 | }
67 |
--------------------------------------------------------------------------------
/tests/index.test.js:
--------------------------------------------------------------------------------
1 | import { test } from 'uvu'
2 | import * as assert from 'uvu/assert'
3 | import { spy } from 'tinyspy'
4 |
5 | import { AbortController } from 'abortcontroller-polyfill/dist/abortcontroller.js'
6 |
7 | import create from './create.js'
8 |
9 | import {
10 | Nanomessage,
11 | createError,
12 | VOID_RESPONSE,
13 | NM_ERR_TIMEOUT,
14 | NM_ERR_CANCEL,
15 | NM_ERR_CLOSE,
16 | NM_ERR_MESSAGE,
17 | NM_ERR_NOT_OPEN,
18 | NM_ERR_REMOTE_RESPONSE,
19 | NM_ERR_ENCODE
20 | } from '../src/index.js'
21 |
22 | const assertThrow = async (p, ErrorClass) => {
23 | try {
24 | await p
25 | assert.unreachable('should have thrown')
26 | } catch (err) {
27 | if (ErrorClass.isNanoerror) {
28 | assert.ok(ErrorClass.equals(err), `expect code: ${ErrorClass.code} resolve: ${err}`)
29 | } else {
30 | assert.instance(err, ErrorClass)
31 | }
32 | }
33 | }
34 |
35 | test('basic', async () => {
36 | const onSend = spy((data, info) => {
37 | assert.ok(Buffer.isBuffer(data))
38 | assert.is.not(info.id, undefined)
39 | if (!info.response) {
40 | assert.ok(info.context.optionalInformation)
41 | }
42 |
43 | return { ...info.toJSON(), id: undefined }
44 | })
45 |
46 | const onError = spy()
47 |
48 | const [alice, bob] = create(
49 | {
50 | onMessage: spy((data, info) => {
51 | assert.equal(data, Buffer.from('ping from bob'))
52 | assert.is.not(info.id, undefined)
53 | assert.is(info.ephemeral, false)
54 | assert.is(info.response, false)
55 | assert.equal(info.data, data)
56 | return Buffer.from('pong from alice')
57 | }),
58 | onSend
59 | },
60 | {
61 | onMessage: spy((data) => {
62 | assert.equal(data, 'ping from alice')
63 | return 'pong from bob'
64 | })
65 | }
66 | )
67 |
68 | alice.on('error-message', onError)
69 | bob.on('error-message', onError)
70 |
71 | assert.equal(await alice.request('ping from alice', { context: { optionalInformation: true } }), 'pong from bob')
72 | assert.equal(await bob.request(Buffer.from('ping from bob')), Buffer.from('pong from alice'))
73 | assert.ok(!onError.called, 'should not get a message error')
74 |
75 | assert.is(alice._onMessage.callCount, 1)
76 | assert.is(bob._onMessage.callCount, 1)
77 |
78 | assert.is(onSend.callCount, 2)
79 | assert.equal(onSend.results[0][1], { id: undefined, responseData: undefined, data: 'ping from alice', ephemeral: false, response: false, error: false, context: { optionalInformation: true } })
80 | assert.equal(onSend.results[1][1], { id: undefined, data: Buffer.from('ping from bob'), responseData: Buffer.from('pong from alice'), ephemeral: false, response: true, error: false, context: {} })
81 | })
82 |
83 | test('options', () => {
84 | const [alice] = create({ timeout: 1000 })
85 |
86 | assert.not.type(alice.codec, undefined)
87 |
88 | assert.is(alice.requestTimeout, 1000)
89 |
90 | assert.equal(alice.concurrency, {
91 | incoming: 256,
92 | outgoing: 256
93 | })
94 |
95 | alice.setConcurrency({
96 | incoming: 1,
97 | outgoing: 2
98 | })
99 | assert.equal(alice.concurrency, {
100 | incoming: 1,
101 | outgoing: 2
102 | })
103 |
104 | alice.setConcurrency({
105 | incoming: 10
106 | })
107 | assert.equal(alice.concurrency, {
108 | incoming: 10,
109 | outgoing: 2
110 | })
111 |
112 | alice.setConcurrency({
113 | outgoing: 15
114 | })
115 | assert.equal(alice.concurrency, {
116 | incoming: 10,
117 | outgoing: 15
118 | })
119 | })
120 |
121 | test('timeout', async () => {
122 | const [alice] = create(
123 | {
124 | timeout: 100
125 | },
126 | {
127 | onMessage: async () => {
128 | await new Promise(resolve => setTimeout(resolve, 200))
129 | }
130 | }
131 | )
132 |
133 | await assertThrow(alice.request('ping'), NM_ERR_TIMEOUT)
134 | })
135 |
136 | test('automatic cleanup requests', async () => {
137 | const [alice, bob] = create({
138 | onMessage () {}
139 | }, {
140 | onMessage () {}
141 | })
142 |
143 | assert.is(alice.requests.length, 0)
144 | assert.is(bob.requests.length, 0)
145 |
146 | const aliceTen = Array.from(Array(10).keys()).map(() => alice.request('message'))
147 | const bobTen = Array.from(Array(10).keys()).map(() => bob.request('message'))
148 |
149 | assert.is(bob.requests.length, 10)
150 | assert.is(alice.requests.length, 10)
151 |
152 | await Promise.all([...aliceTen, ...bobTen])
153 |
154 | assert.is(alice.requests.length, 0)
155 | assert.is(bob.requests.length, 0)
156 | })
157 |
158 | test('close', async () => {
159 | const [alice, bob] = create()
160 |
161 | const request = bob.request('message')
162 |
163 | assert.is(bob.requests.length, 1)
164 |
165 | await Promise.all([
166 | Nanomessage.once(alice, 'unsubscribe'),
167 | Nanomessage.once(bob, 'unsubscribe'),
168 | assertThrow(request, NM_ERR_CLOSE),
169 | alice.close(),
170 | bob.close()
171 | ])
172 |
173 | assert.is(bob.requests.length, 0)
174 | await assertThrow(alice.send('test'), NM_ERR_CLOSE)
175 | })
176 |
177 | test('detect invalid request', async () => {
178 | const [alice, bob] = create()
179 |
180 | queueMicrotask(() => bob.stream.write('not valid'))
181 | const [error] = await Nanomessage.once(alice, 'subscribe-error')
182 | assert.is(error.code, 'NM_ERR_DECODE')
183 | })
184 |
185 | test('custom valueEncoding', async () => {
186 | const valueEncoding = {
187 | encode (data) {
188 | if (typeof data === 'number') throw new Error('test')
189 | return Buffer.from(data)
190 | },
191 | decode (buf) {
192 | return buf.toString('utf8')
193 | }
194 | }
195 |
196 | const [alice] = create({ valueEncoding }, { valueEncoding, onMessage: () => 'hi' })
197 |
198 | const bobMessage = await alice.request('hello')
199 | assert.is(bobMessage, 'hi')
200 | await assertThrow(alice.request(2), NM_ERR_ENCODE)
201 | })
202 |
203 | test('void response', async () => {
204 | const [alice] = create({ onMessage: () => {} }, { onMessage: () => VOID_RESPONSE })
205 | await assertThrow(alice.request('hello', { timeout: 100 }), NM_ERR_TIMEOUT)
206 | })
207 |
208 | test('send ephemeral message', async () => {
209 | const onError = spy()
210 |
211 | const [alice, bob] = create(
212 | {
213 | onMessage: spy((data, { ephemeral }) => {
214 | assert.is(ephemeral, true)
215 | assert.equal(data, Buffer.from('ping from bob'))
216 | })
217 | },
218 | {
219 | onMessage: spy((data, { ephemeral }) => {
220 | assert.is(ephemeral, true)
221 | assert.is(data, 'ping from alice')
222 | })
223 | }
224 | )
225 |
226 | alice.on('error-message', onError)
227 | bob.on('error-message', onError)
228 |
229 | await Promise.all([
230 | alice.send('ping from alice'),
231 | bob.send(Buffer.from('ping from bob')),
232 | Nanomessage.once(alice, 'message'),
233 | Nanomessage.once(bob, 'message')
234 | ])
235 |
236 | await new Promise(resolve => setTimeout(resolve, 100))
237 |
238 | assert.ok(!onError.called, 'should not get a message error')
239 | })
240 |
241 | test('concurrency', async () => {
242 | const [alice, bob] = create(
243 | {
244 | concurrency: 2
245 | },
246 | {
247 | concurrency: 2
248 | }
249 | )
250 |
251 | alice.request('ping from alice').catch(() => {})
252 | alice.request('ping from alice').catch(() => {})
253 | alice.request('ping from alice').catch(() => {}) // this request will wait, only 2 inflightRequests
254 |
255 | assert.is(alice.inflightRequests, 2)
256 |
257 | await Promise.all([alice.close(), bob.close()])
258 | })
259 |
260 | test('abort signal', async () => {
261 | const [alice] = create(
262 | {},
263 | {
264 | onMessage: async () => {
265 | await new Promise(resolve => setTimeout(resolve, 100))
266 | return 'pong'
267 | }
268 | }
269 | )
270 |
271 | const ErrorCustom = createError('CUSTOM')
272 |
273 | {
274 | const controller = new AbortController()
275 | const signal = controller.signal
276 | controller.abort()
277 | const request = alice.request('ping', { signal })
278 | await assertThrow(request, NM_ERR_CANCEL)
279 | }
280 |
281 | {
282 | const controller = new AbortController()
283 | const signal = controller.signal
284 | controller.abort()
285 | const request = alice.request('ping', { signal, onCancel: () => new ErrorCustom() })
286 | await assertThrow(request, ErrorCustom)
287 | }
288 |
289 | {
290 | const onCancelFn = spy()
291 | const controller = new AbortController()
292 | const signal = controller.signal
293 | controller.abort()
294 | const request = alice.request('ping', { signal, onCancel: onCancelFn })
295 | await assertThrow(request, NM_ERR_CANCEL)
296 | assert.ok(onCancelFn.called)
297 | }
298 |
299 | {
300 | const controller = new AbortController()
301 | const signal = controller.signal
302 | const request = alice.request('ping', { signal })
303 | setTimeout(() => controller.abort(), 50)
304 | await assertThrow(request, NM_ERR_CANCEL)
305 | }
306 |
307 | {
308 | const controller = new AbortController()
309 | const signal = controller.signal
310 | const request = alice.request('ping', { signal, timeout: 150 })
311 | await new Promise(resolve => setTimeout(resolve, 110))
312 | controller.abort()
313 | assert.is(await request, 'pong')
314 | }
315 | })
316 |
317 | test('processIncomingMessage', async () => {
318 | const messageContext = []
319 |
320 | const alice = new Nanomessage({
321 | send: (data, info) => {
322 | messageContext.push(info.context)
323 | bob.processIncomingMessage(data, {
324 | context: info.context
325 | })
326 | },
327 | onMessage: (_, info) => {
328 | messageContext.push(info.context)
329 | }
330 | })
331 |
332 | const bob = new Nanomessage({
333 | send: (data) => {
334 | alice.processIncomingMessage(data)
335 | }
336 | })
337 |
338 | await alice.open()
339 | await bob.open()
340 |
341 | await alice.request('ping', { context: 'randomData' })
342 | assert.equal(messageContext, ['randomData'])
343 | })
344 |
345 | test('valueEncoding', async () => {
346 | const onEncode = spy()
347 | const onDecode = spy()
348 |
349 | const valueEncoding = {
350 | encode (obj) {
351 | onEncode()
352 | return JSON.stringify(obj)
353 | },
354 | decode (str) {
355 | onDecode()
356 | return JSON.parse(str)
357 | }
358 | }
359 |
360 | const alice = new Nanomessage({
361 | valueEncoding,
362 | send: (data, info) => {
363 | bob.processIncomingMessage(data)
364 | },
365 | onMessage: (data, info) => {
366 | assert.is(data, 'ping')
367 | return 'pong'
368 | }
369 | })
370 |
371 | const bob = new Nanomessage({
372 | valueEncoding,
373 | send: (data) => {
374 | alice.processIncomingMessage(data)
375 | }
376 | })
377 |
378 | await alice.open()
379 | await bob.open()
380 |
381 | const res = await bob.request('ping')
382 | assert.is(res, 'pong')
383 |
384 | assert.is(onEncode.callCount, 2)
385 | assert.is(onDecode.callCount, 2)
386 | })
387 |
388 | test('create a request over a closed resource', async () => {
389 | const [alice] = create()
390 | await alice.close()
391 | // do nothing with incoming messages
392 | await alice.processIncomingMessage()
393 | await assertThrow(alice.request('test'), NM_ERR_CLOSE)
394 | })
395 |
396 | test('error-message', async () => {
397 | const [alice, bob] = create({ onMessage: () => { throw new Error('oh no') } })
398 |
399 | const result = Nanomessage.once(alice, 'error-message')
400 | await bob.send('test')
401 | const [error] = await result
402 | assert.is(error.code, NM_ERR_MESSAGE.code)
403 | await assertThrow(bob.request('test'), NM_ERR_REMOTE_RESPONSE)
404 | })
405 |
406 | test('nanoerror error-message', async () => {
407 | const Err = createError('ERR', 'oh no')
408 | const [alice, bob] = create({ onMessage: () => { throw new Err() } })
409 |
410 | const result = Nanomessage.once(alice, 'error-message')
411 | await bob.send('test')
412 | const [error] = await result
413 | assert.instance(error, Err)
414 |
415 | await assertThrow(bob.request('test'), Err)
416 | })
417 |
418 | test('send-error', async () => {
419 | const [alice, bob] = create({
420 | send: () => {
421 | throw new Error('send error')
422 | }
423 | })
424 |
425 | const result = Nanomessage.once(alice, 'error-message')
426 | await assertThrow(bob.request('test', { timeout: 100 }), NM_ERR_TIMEOUT)
427 | const [error] = await result
428 | assert.is(error.code, NM_ERR_MESSAGE.code)
429 | })
430 |
431 | test('send not implemented', async () => {
432 | const [alice] = create({
433 | send: undefined
434 | })
435 |
436 | await assertThrow(alice.request('test'), Error)
437 | })
438 |
439 | test('open option', async () => {
440 | const openFn = spy()
441 |
442 | const [alice] = create({
443 | open: openFn
444 | })
445 |
446 | await alice.open()
447 | assert.ok(openFn.called)
448 |
449 | const bob = new Nanomessage()
450 | await assertThrow(bob.send('data'), NM_ERR_NOT_OPEN)
451 | })
452 |
453 | test.run()
454 |
--------------------------------------------------------------------------------