UNPKG

@baptistecdr/aria2

Version:

Library for aria2, "The next generation download utility."

281 lines (233 loc) 6.59 kB
import { EventEmitter } from 'events'; function Deferred() { this.promise = new Promise((resolve, reject) => { this.resolve = resolve; this.reject = reject; }); } class JSONRPCError extends Error { constructor({ message, code, data }) { super(message); this.code = code; if (data) this.data = data; this.name = this.constructor.name; } } function promiseEvent(target, event) { return new Promise((resolve, reject) => { function cleanup() { target.removeListener(event, onEvent); target.removeListener("error", onError); } function onEvent(data) { resolve(data); cleanup(); } function onError(err) { reject(err); cleanup(); } target.addListener(event, onEvent); target.addListener("error", onError); }); } // biome-ignore lint/style/useNodejsImportProtocol: nodePolyfills cannot polyfill if the module is imported with the node: protocol class JSONRPCClient extends EventEmitter { constructor(options) { super(); this.deferreds = Object.create(null); this.lastId = 0; Object.assign(this, this.constructor.defaultOptions, options); } id() { return this.lastId++; } url(protocol) { return `${protocol + (this.secure ? "s" : "")}://${this.host}:${this.port}${this.path}`; } websocket(message) { return new Promise((resolve, reject) => { const cb = (err) => { if (err) reject(err); else resolve(); }; this.socket.send(JSON.stringify(message), cb); if (globalThis.WebSocket && this.socket instanceof globalThis.WebSocket) cb(); }); } async http(message) { const response = await this.fetch(this.url("http"), { method: "POST", body: JSON.stringify(message), headers: { Accept: "application/json", "Content-Type": "application/json", }, }); response .json() .then((msg) => this._onmessage(msg)) .catch((err) => { this.emit("error", err); }); return response; } _buildMessage(method, params) { if (typeof method !== "string") { throw new TypeError(`${method} is not a string`); } const message = { method, "json-rpc": "2.0", id: this.id(), }; if (params) Object.assign(message, { params }); return message; } async batch(calls) { const message = calls.map(([method, params]) => { return this._buildMessage(method, params); }); await this._send(message); return message.map(({ id }) => { this.deferreds[id] = new Deferred(); const { promise } = this.deferreds[id]; return promise; }); } async call(method, parameters) { const message = this._buildMessage(method, parameters); await this._send(message); this.deferreds[message.id] = new Deferred(); const { promise } = this.deferreds[message.id]; return promise; } async _send(message) { this.emit("output", message); const { socket } = this; return socket && socket.readyState === 1 ? this.websocket(message) : this.http(message); } _onresponse({ id, error, result }) { const deferred = this.deferreds[id]; if (!deferred) return; if (error) deferred.reject(new JSONRPCError(error)); else deferred.resolve(result); delete this.deferreds[id]; } _onrequest({ method, params }) { return this.onrequest(method, params); } _onnotification({ method, params }) { this.emit(method, params); } _onmessage(message) { this.emit("input", message); if (Array.isArray(message)) { for (const object of message) { this._onobject(object); } } else { this._onobject(message); } } _onobject(message) { if (message.method === undefined) this._onresponse(message); else if (message.id === undefined) this._onnotification(message); else this._onrequest(message); } async open() { this.socket = new this.WebSocket(this.url("ws")); const socket = this.socket; socket.onclose = (...args) => { this.emit("close", ...args); }; socket.onmessage = (event) => { let message; try { message = JSON.parse(event.data); } catch (err) { this.emit("error", err); return; } this._onmessage(message); }; socket.onopen = (...args) => { this.emit("open", ...args); }; socket.onerror = (...args) => { this.emit("error", ...args); }; return promiseEvent(this, "open"); } async close() { const { socket } = this; socket.close(); return promiseEvent(this, "close"); } } JSONRPCClient.defaultOptions = { secure: false, host: "localhost", port: 80, secret: "", path: "/jsonrpc", WebSocket: globalThis.WebSocket, fetch: globalThis.fetch.bind(globalThis), }; function prefix(str) { let prefixedStr = str; if (!str.startsWith("system.") && !str.startsWith("aria2.")) { prefixedStr = `aria2.${str}`; } return prefixedStr; } function unprefix(str) { const suffix = str.split("aria2.")[1]; return suffix || str; } class Aria2 extends JSONRPCClient { addSecret(parameters) { let params = this.secret ? [`token:${this.secret}`] : []; if (Array.isArray(parameters)) { params = params.concat(parameters); } return params; } _onnotification(notification) { const { method, params } = notification; const event = unprefix(method); if (event !== method) this.emit(event, params); return super._onnotification(notification); } async call(method, ...params) { return super.call(prefix(method), this.addSecret(params)); } async multicall(calls) { const multi = [ calls.map(([method, ...params]) => { return { methodName: prefix(method), params: this.addSecret(params) }; }), ]; return super.call("system.multicall", multi); } async batch(calls) { return super.batch(calls.map(([method, ...params]) => [prefix(method), this.addSecret(params)])); } async listNotifications() { const events = await this.call("system.listNotifications"); return events.map((event) => unprefix(event)); } async listMethods() { const methods = await this.call("system.listMethods"); return methods.map((method) => unprefix(method)); } } Object.assign(Aria2, { prefix, unprefix }); Aria2.defaultOptions = Object.assign({}, JSONRPCClient.defaultOptions, { secure: false, host: "localhost", port: 6800, secret: "", path: "/jsonrpc", }); export { Aria2 as default };