From c998d23cd3574e9549f60f2df18b90756fbbabd6 Mon Sep 17 00:00:00 2001 From: Sonny Piers Date: Thu, 25 Feb 2021 11:27:10 +0100 Subject: [PATCH] Stop using EventEmitter --- README.md | 145 +++++++++++++++++++++--------------------- lib/Aria2.js | 59 +++++++++-------- lib/JSONRPCClient.js | 84 ++++++++++-------------- lib/Signaler.js | 27 ++++++++ lib/debug.js | 21 ------ lib/promiseEvent.js | 16 ++--- node.js | 9 +++ package-lock.json | 6 +- package.json | 17 ++--- test/Aria2.js | 23 +++++-- test/JSONRPCClient.js | 36 ++++++----- 11 files changed, 229 insertions(+), 214 deletions(-) create mode 100644 lib/Signaler.js delete mode 100644 lib/debug.js create mode 100644 node.js diff --git a/README.md b/README.md index 1330c10..43f2b9c 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,11 @@ JavaScript (Node.js and browsers) library for [aria2, "The next generation downl - [Introduction](#introduction) - [Getting started](#getting-started) - [Usage](#usage) - - [open](#open) - - [close](#close) - [call](#call) - [multicall](#multicall) - [batch](#batch) + - [open](#open) + - [close](#close) - [listNotifications](#listnotifications) - [listMethods](#listmethods) - [events](#events) @@ -19,19 +19,17 @@ JavaScript (Node.js and browsers) library for [aria2, "The next generation downl aria2.js controls aria2 via its [JSON-RPC interface](https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface) and features -- Node.js and browsers support -- multiple transports - - [HTTP](https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface) - - [WebSocket](https://aria2.github.io/manual/en/html/aria2c.html#json-rpc-over-websocket) -- promise API +- support for browsers, Node.js, React Native, deno and gjs environments +- support for [HTTP](https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface) and [WebSocket](https://aria2.github.io/manual/en/html/aria2c.html#json-rpc-over-websocket) transports +- modern ECMAScript API -See [aria2 methods](https://aria2.github.io/manual/en/html/aria2c.html#methods) and [aria2 notifications](https://aria2.github.io/manual/en/html/aria2c.html#notifications). +See [aria2 methods](https://aria2.github.io/manual/en/html/aria2c.html#methods) and [aria2 notifications](https://aria2.github.io/manual/en/html/aria2c.html#notifications) to get an idea of what aria2 and aria2.js are capable of. [↑](#aria2js) ## Getting started -Start aria2 with rpc, example: +Start aria2 in daemon mode with rpc, example: `aria2c --enable-rpc --rpc-listen-all=true --rpc-allow-origin-all` @@ -46,6 +44,8 @@ Install aria2.js ```javascript const Aria2 = require("aria2"); const aria2 = new Aria2([options]); +aria2.onerror = console.error; +aria2.onnotification = console.log; ``` default options match aria2c defaults and are @@ -64,34 +64,6 @@ default options match aria2c defaults and are If the WebSocket is open (via the [open method](#open)) aria2.js will use the WebSocket transport, otherwise the HTTP transport. -The `"aria2."` prefix can be omitted from both methods and notifications. - -[↑](#aria2js) - -### open - -`aria2.open()` opens the WebSocket connection. All subsequent requests will use the WebSocket transport instead of HTTP. - -```javascript -aria2 - .open() - .then(() => console.log("open")) - .catch((err) => console.log("error", err)); -``` - -[↑](#aria2js) - -### close - -`aria2.close()` closes the WebSocket connection. All subsequent requests will use the HTTP transport instead of WebSocket. - -```javascript -aria2 - .close() - .then(() => console.log("closed")) - .catch((err) => console.log("error", err)); -``` - [↑](#aria2js) ### call @@ -103,7 +75,7 @@ Example using [`addUri`](https://aria2.github.io/manual/en/html/aria2c.html#aria ```javascript const magnet = "magnet:?xt=urn:btih:88594AAACBDE40EF3E2510C47374EC0AA396C08E&dn=bbb_sunflower_1080p_30fps_normal.mp4&tr=udp%3a%2f%2ftracker.openbittorrent.com%3a80%2fannounce&tr=udp%3a%2f%2ftracker.publicbt.com%3a80%2fannounce&ws=http%3a%2f%2fdistribution.bbb3d.renderfarming.net%2fvideo%2fmp4%2fbbb_sunflower_1080p_30fps_normal.mp4"; -const [guid] = await aria2.call("addUri", [magnet], { dir: "/tmp" }); +const guid = await aria2.call("addUri", [magnet], { dir: "/tmp" }); ``` [↑](#aria2js) @@ -121,6 +93,8 @@ const multicall = [ const results = await aria2.multicall(multicall); ``` +[↑](#aria2js) + ### batch `aria2.batch()` is a helper for [batch](https://aria2.github.io/manual/en/html/aria2c.html#system.multicall). It behaves the same as [multicall](#multicall) except it returns an array of promises which gives more flexibility in handling errors. @@ -136,6 +110,32 @@ const promises = await aria2.batch(batch); [↑](#aria2js) +### open + +`aria2.open()` opens the WebSocket connection. All subsequent requests will use the WebSocket transport instead of HTTP. + +```javascript +aria2 + .open() + .then(() => console.log("open")) + .catch((err) => console.log("error", err)); +``` + +[↑](#aria2js) + +### close + +`aria2.close()` closes the WebSocket connection. All subsequent requests will use the HTTP transport instead of WebSocket. + +```javascript +aria2 + .close() + .then(() => console.log("closed")) + .catch((err) => console.log("error", err)); +``` + +[↑](#aria2js) + ### listNotifications `aria2.listNotifications()` is a helper for [system.listNotifications](https://aria2.github.io/manual/en/html/aria2c.html#system.listNotifications). The difference with `aria2.call('listNotifications')` is that it removes the `"aria2."` prefix from the results. @@ -152,20 +152,13 @@ const notifications = await aria2.listNotifications(); 'onBtDownloadComplete' ] */ - -// notifications logger example -notifications.forEach((notification) => { - aria2.on(notification, (params) => { - console.log("aria2", notification, params); - }); -}); ``` [↑](#aria2js) ### listMethods -`aria2.listMethods()` is a helper for [system.listMethods](https://aria2.github.io/manual/en/html/aria2c.html#system.listMethods). The difference with `aria2.call('listMethods')` is that it removes the `"aria2."` prefix for the results. +`aria2.listMethods()` is a helper for [system.listMethods](https://aria2.github.io/manual/en/html/aria2c.html#system.listMethods). The difference with `aria2.call('listMethods')` is that it removes the `"aria2."` prefix from the results. ```javascript const methods = await aria2.listMethods(); @@ -182,33 +175,39 @@ const methods = await aria2.listMethods(); ### events ```javascript -// emitted when the WebSocket is open. -aria2.on("open", () => { - console.log("aria2 OPEN"); -}); - -// emitted when the WebSocket is closed. -aria2.on("close", () => { - console.log("aria2 CLOSE"); -}); - -// emitted for every message sent. -aria2.on("output", (m) => { - console.log("aria2 OUT", m); -}); - -// emitted for every message received. -aria2.on("input", (m) => { - console.log("aria2 IN", m); -}); -``` - -Additionally every [aria2 notifications](https://aria2.github.io/manual/en/html/aria2c.html#notifications) received will be emitted as an event (with and without the `"aria2."` prefix). Only available when using WebSocket, see [open](#open). - -```javascript -aria2.on("onDownloadStart", ([guid]) => { - console.log("aria2 onDownloadStart", guid); -}); +// called when an error occurs +aria2.onerror = (error) => { + console.log("aria2", "ERROR"); + console.log(error); +}; + +// called when a notification is received on the WebSocket +aria2.onnotification = (name, params) => { + console.log("aria2", "notification", name); + console.log(params); +}; + +// called when the WebSocket is open. +aria2.onopen = () => { + console.log("aria2", "OPEN"); +}; + +// called when the WebSocket is closed. +aria2.onclose = () => { + console.log("aria2", "CLOSE"); +}; + +// called for every message received. +aria2.oninput = (message) => { + console.log("aria2", "IN"); + console.log(message); +}; + +// called for every message sent. +aria2.onoutput = (message) => { + console.log("aria2", "OUT"); + console.log(message); +}; ``` [↑](#aria2js) diff --git a/lib/Aria2.js b/lib/Aria2.js index 1e8034c..d923f13 100644 --- a/lib/Aria2.js +++ b/lib/Aria2.js @@ -1,19 +1,7 @@ import JSONRPCClient from "./JSONRPCClient.js"; -function prefix(str) { - if (!str.startsWith("system.") && !str.startsWith("aria2.")) { - str = "aria2." + str; - } - return str; -} - -function unprefix(str) { - const suffix = str.split("aria2.")[1]; - return suffix || str; -} - class Aria2 extends JSONRPCClient { - addSecret(parameters) { + _withSecret(parameters) { let params = this.secret ? ["token:" + this.secret] : []; if (Array.isArray(parameters)) { params = params.concat(parameters); @@ -21,21 +9,21 @@ class Aria2 extends JSONRPCClient { return params; } - _onnotification(notification) { + _handleNotification(notification) { const { method, params } = notification; const event = unprefix(method); - if (event !== method) this.emit(event, params); - return super._onnotification(notification); + this._signal([event, params]); + super._handleNotification(notification); } async call(method, ...params) { - return super.call(prefix(method), this.addSecret(params)); + return super.call(prefix(method), this._withSecret(params)); } async multicall(calls) { const multi = [ calls.map(([method, ...params]) => { - return { methodName: prefix(method), params: this.addSecret(params) }; + return { methodName: prefix(method), params: this._withSecret(params) }; }), ]; return super.call("system.multicall", multi); @@ -45,7 +33,7 @@ class Aria2 extends JSONRPCClient { return super.batch( calls.map(([method, ...params]) => [ prefix(method), - this.addSecret(params), + this._withSecret(params), ]) ); } @@ -61,14 +49,29 @@ class Aria2 extends JSONRPCClient { } } -Object.assign(Aria2, { prefix, unprefix }); - -Aria2.defaultOptions = Object.assign({}, JSONRPCClient.defaultOptions, { - secure: false, - host: "localhost", - port: 6800, - secret: "", - path: "/jsonrpc", -}); +Aria2.defaultOptions = { + ...JSONRPCClient.defaultOptions, + ...{ + secure: false, + host: "localhost", + port: 6800, + secret: "", + path: "/jsonrpc", + }, +}; export default Aria2; + +function prefix(str) { + if (!str.startsWith("system.") && !str.startsWith("aria2.")) { + str = "aria2." + str; + } + return str; +} + +function unprefix(str) { + const suffix = str.split("aria2.")[1]; + return suffix || str; +} + +Object.assign(Aria2, { prefix, unprefix }); diff --git a/lib/JSONRPCClient.js b/lib/JSONRPCClient.js index e087dca..db0043d 100644 --- a/lib/JSONRPCClient.js +++ b/lib/JSONRPCClient.js @@ -1,15 +1,9 @@ -import _WebSocket from "ws"; -import _fetch from "node-fetch"; -import EventEmitter from "events"; - import Deferred from "./Deferred.js"; import promiseEvent from "./promiseEvent.js"; import JSONRPCError from "./JSONRPCError.js"; +import Signaler from "./Signaler.js"; -const WebSocket = global.WebSocket || _WebSocket; -const fetch = global.fetch ? global.fetch.bind(global) : _fetch; - -class JSONRPCClient extends EventEmitter { +class JSONRPCClient extends Signaler { constructor(options) { super(); this.deferreds = Object.create(null); @@ -34,15 +28,8 @@ class JSONRPCClient extends EventEmitter { ); } - websocket(message) { - return new Promise((resolve, reject) => { - const cb = (err) => { - if (err) reject(err); - else resolve(); - }; - this.socket.send(JSON.stringify(message), cb); - if (global.WebSocket && this.socket instanceof global.WebSocket) cb(); - }); + async websocket(message) { + this.socket.send(JSON.stringify(message)); } async http(message) { @@ -57,9 +44,9 @@ class JSONRPCClient extends EventEmitter { response .json() - .then((msg) => this._onmessage(msg)) + .then((msg) => this._handleMessage(msg)) .catch((err) => { - this.emit("error", err); + this._signal(["error", err]); }); return response; @@ -81,8 +68,6 @@ class JSONRPCClient extends EventEmitter { } async batch(calls) { - const promises = []; - const message = calls.map(([method, params]) => { return this._buildMessage(method, params); }); @@ -105,7 +90,7 @@ class JSONRPCClient extends EventEmitter { } async _send(message) { - this.emit("output", message); + this._signal(["output", message]); const { socket } = this; return socket && socket.readyState === 1 @@ -113,7 +98,7 @@ class JSONRPCClient extends EventEmitter { : this.http(message); } - _onresponse({ id, error, result }) { + _handleResponse({ id, error, result }) { const deferred = this.deferreds[id]; if (!deferred) return; if (error) deferred.reject(new JSONRPCError(error)); @@ -121,62 +106,61 @@ class JSONRPCClient extends EventEmitter { delete this.deferreds[id]; } - _onrequest({ method, params }) { - return this.onrequest(method, params); + _handleNotification(message) { + this._signal(["notification", message]); } - _onnotification({ method, params }) { - this.emit(method, params); + _handleObject(message) { + if (message.method === undefined) this._handleResponse(message); + else if (message.id === undefined) this._handleNotification(message); + else this._handleRequest(message); + } + _signal(value) { + this.signal(value); } - _onmessage(message) { - this.emit("input", message); + _handleMessage(message) { + this._signal(["input", message]); if (Array.isArray(message)) { for (const object of message) { - this._onobject(object); + this._handleObject(object); } } else { - this._onobject(message); + this._handleObject(message); } } - _onobject(message) { - if (message.method === undefined) this._onresponse(message); - else if (message.id === undefined) this._onnotification(message); - else this._onrequest(message); - } - async open() { const socket = (this.socket = new this.WebSocket(this.url("ws"))); - socket.onclose = (...args) => { - this.emit("close", ...args); + socket.onclose = (event) => { + this._signal(["close", event]); }; socket.onmessage = (event) => { let message; try { message = JSON.parse(event.data); } catch (err) { - this.emit("error", err); - return; + return this._signal(["error", err]); } - this._onmessage(message); + this._handleMessage(message); }; - socket.onopen = (...args) => { - this.emit("open", ...args); + socket.onopen = (event) => { + this._signal(["open", event]); }; - socket.onerror = (...args) => { - this.emit("error", ...args); + socket.onerror = (event) => { + this._signal(["error", event]); }; - return promiseEvent(this, "open"); + await promiseEvent(socket, "open"); } async close() { + if (!this.socket) return; const { socket } = this; socket.close(); - return promiseEvent(this, "close"); + await promiseEvent(socket, "close"); } } @@ -186,8 +170,8 @@ JSONRPCClient.defaultOptions = { port: 80, secret: "", path: "/jsonrpc", - fetch, - WebSocket, + fetch: globalThis.fetch, + WebSocket: globalThis.WebSocket, }; export default JSONRPCClient; diff --git a/lib/Signaler.js b/lib/Signaler.js new file mode 100644 index 0000000..5d93c3b --- /dev/null +++ b/lib/Signaler.js @@ -0,0 +1,27 @@ +export default class Signaler { + constructor() { + this._queue = []; + this._pending = null; + } + + signal(value) { + if (this._pending) { + this._pending(value); + this._pending = null; + } else { + this._queue.push(value); + } + } + + async *[Symbol.asyncIterator]() { + while (true) { + yield new Promise((resolve) => { + if (this._queue.length > 0) { + resolve(this._queue.shift()); + } else { + this._pending = resolve; + } + }); + } + } +} diff --git a/lib/debug.js b/lib/debug.js deleted file mode 100644 index 09dcafd..0000000 --- a/lib/debug.js +++ /dev/null @@ -1,21 +0,0 @@ -import { inspect } from "util"; - -export default function (aria2) { - aria2.on("open", () => { - console.log("aria2", "OPEN"); - }); - - aria2.on("close", () => { - console.log("aria2", "CLOSE"); - }); - - aria2.on("input", (m) => { - console.log("aria2", "IN"); - console.log(inspect(m, { depth: null, colors: true })); - }); - - aria2.on("output", (m) => { - console.log("aria2", "OUT"); - console.log(inspect(m, { depth: null, colors: true })); - }); -} diff --git a/lib/promiseEvent.js b/lib/promiseEvent.js index 8ad7944..3dff01e 100644 --- a/lib/promiseEvent.js +++ b/lib/promiseEvent.js @@ -1,18 +1,18 @@ export default function promiseEvent(target, event) { return new Promise((resolve, reject) => { function cleanup() { - target.removeListener(event, onEvent); - target.removeListener("error", onError); + target.removeEventListener(event, onEvent); + target.removeEventListener("error", onError); } - function onEvent(data) { - resolve(data); + function onEvent(event) { + resolve(event); cleanup(); } - function onError(err) { - reject(err); + function onError(event) { + reject(event); cleanup(); } - target.addListener(event, onEvent); - target.addListener("error", onError); + target.addEventListener(event, onEvent); + target.addEventListener("error", onError); }); } diff --git a/node.js b/node.js new file mode 100644 index 0000000..3eebb64 --- /dev/null +++ b/node.js @@ -0,0 +1,9 @@ +import Aria2 from "./lib/Aria2.js"; + +import WebSocket from "ws"; +import fetch from "node-fetch"; + +Aria2.defaultOptions.WebSocket = WebSocket; +Aria2.defaultOptions.fetch = fetch; + +export default Aria2; diff --git a/package-lock.json b/package-lock.json index af3b838..4c767ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aria2", - "version": "4.1.2", + "version": "5.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "aria2", - "version": "4.1.2", + "version": "5.0.0", "license": "ISC", "dependencies": { "node-fetch": "^2.6.1", @@ -17,7 +17,7 @@ "prettier": "2.2.1" }, "engines": { - "node": ">= 10" + "node": ">= 12.4.0" } }, "node_modules/@babel/code-frame": { diff --git a/package.json b/package.json index 44743bc..85e8b3c 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "aria2", - "version": "4.1.2", + "version": "5.0.0", "description": "Library and for aria2, \"The next generation download utility.\"", "homepage": "https://github.com/sonnyp/aria2.js", "bugs": "https://github.com/sonnyp/aria2.js/issues", "license": "ISC", "type": "module", - "main": "./lib/Aria2.js", + "main": "./node.js", + "browser": "./lib/Aria2.js", "author": "Sonny Piers ", "keywords": [ "aria2", @@ -18,12 +19,16 @@ "SFTP", "MetaLink", "magnet", - "torrent" + "torrent", + "node", + "deno", + "react-native", + "gjs" ], "scripts": { "preversion": "npm test", "lint": "prettier -l **/*.js", - "unit": "ava", + "unit": "ava test/*.js", "test": "npm run unit && npm run lint" }, "repository": "github:sonnyp/aria2.js", @@ -37,9 +42,5 @@ }, "engines": { "node": ">= 12.4.0" - }, - "browser": { - "ws": false, - "node-fetch": false } } diff --git a/test/Aria2.js b/test/Aria2.js index 413c18b..7d0a7dd 100644 --- a/test/Aria2.js +++ b/test/Aria2.js @@ -77,7 +77,10 @@ test("#listNotifications", async (t) => { }; setTimeout(() => { - aria2._onresponse({ id: 0, result: ["aria2.foo", "bar", "system.foo"] }); + aria2._handleResponse({ + id: 0, + result: ["aria2.foo", "bar", "system.foo"], + }); }); const notifications = await aria2.listNotifications(); @@ -97,19 +100,27 @@ test("#listMethods", async (t) => { }; setTimeout(() => { - aria2._onresponse({ id: 0, result: ["aria2.foo", "bar", "system.foo"] }); + aria2._handleResponse({ + id: 0, + result: ["aria2.foo", "bar", "system.foo"], + }); }); const methods = await aria2.listMethods(); t.deepEqual(methods, ["foo", "bar", "system.foo"]); }); -test("#_onnotification", async (t) => { +test.cb("#_handleNotification", (t) => { + t.plan(2); + const aria2 = new Aria2({ secret: "foobar" }); const params = ["foo", "bar"]; - const promise = promiseEvent(aria2, "onDownloadStart"); - aria2._onnotification({ method: "aria2.onDownloadStart", params }); + aria2.onnotification = (method, p) => { + t.is(method, "onDownloadStart"); + t.is(p, params); - t.is(await promise, params); + t.end(); + }; + aria2._handleNotification({ method: "aria2.onDownloadStart", params }); }); diff --git a/test/JSONRPCClient.js b/test/JSONRPCClient.js index ec11eba..0fc535d 100644 --- a/test/JSONRPCClient.js +++ b/test/JSONRPCClient.js @@ -1,7 +1,13 @@ import test from "ava"; +import WebSocket from "ws"; +import fetch from "node-fetch"; + import JSONRPCClient from "../lib/JSONRPCClient.js"; +JSONRPCClient.defaultOptions.WebSocket = WebSocket; +JSONRPCClient.defaultOptions.fetch = fetch; + test("#id", (t) => { const client = new JSONRPCClient(); t.is(client.lastId, 0); @@ -32,9 +38,8 @@ test("#websocket", async (t) => { const message = { hello: "world" }; client.socket = { - send(str, cb) { + send(str) { t.is(str, JSON.stringify(message)); - cb(); }, }; @@ -49,9 +54,9 @@ test("#websocket error", async (t) => { const error = new Error(); client.socket = { - send(str, cb) { + send(str) { t.is(str, JSON.stringify(message)); - cb(error); + throw error; }, }; @@ -65,17 +70,14 @@ test("#websocket error", async (t) => { test.cb("#websocket json error", (t) => { const client = new JSONRPCClient(); - client.WebSocket = function () { - return {}; - }; client.open(); client.socket.onopen(); - client.on("error", (err) => { + client.onerror = (err) => { t.true(err instanceof SyntaxError); t.is(err.message, "Unexpected token o in JSON at position 1"); t.end(); - }); + }; client.socket.onmessage({ data: "foo" }); }); @@ -104,7 +106,7 @@ test.cb("#http", (t) => { }); }; - client._onmessage = (m) => { + client.oninput = (m) => { t.is(m, response); t.end(); }; @@ -136,10 +138,10 @@ test("#http json error", async (t) => { }); }; - client.on("error", (err) => { + client.onerror = (err) => { t.true(err instanceof SyntaxError); t.is(err.message, "Unexpected token o in JSON at position 1"); - }); + }; await client.http({}); }); @@ -190,8 +192,8 @@ test("#batch", async (t) => { ["bar", {}], ]); - client._onresponse({ id: 0 }); - client._onresponse({ id: 1 }); + client._handleResponse({ id: 0 }); + client._handleResponse({ id: 1 }); return Promise.all(batch); }); @@ -215,7 +217,7 @@ test("#call", async (t) => { Promise.resolve().then(async () => { await Promise.resolve(); - client._onresponse({ id: 0 }); + client._handleResponse({ id: 0 }); }); return promise; @@ -227,9 +229,9 @@ test("#send", async (t) => { const message = {}; - client.on("output", (m) => { + client.onoutput = (m) => { t.is(m, message); - }); + }; client._send(message); });