UNPKG

@baptistecdr/aria2

Version:

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

751 lines (639 loc) 19.7 kB
var domain; // This constructor is used to store event handlers. Instantiating this is // faster than explicitly calling `Object.create(null)` to get a "clean" empty // object (tested with v8 v4.9). function EventHandlers() {} EventHandlers.prototype = Object.create(null); function EventEmitter() { EventEmitter.init.call(this); } // nodejs oddity // require('events') === require('events').EventEmitter EventEmitter.EventEmitter = EventEmitter; EventEmitter.usingDomains = false; EventEmitter.prototype.domain = undefined; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; EventEmitter.init = function() { this.domain = null; if (EventEmitter.usingDomains) { // if there is an active domain, then attach to it. if (domain.active) ; } if (!this._events || this._events === Object.getPrototypeOf(this)._events) { this._events = new EventHandlers(); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || isNaN(n)) throw new TypeError('"n" argument must be a positive number'); this._maxListeners = n; return this; }; function $getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return $getMaxListeners(this); }; // These standalone emit* functions are used to optimize calling of event // handlers for fast cases because emit() itself often has a variable number of // arguments and can be deoptimized because of that. These functions always have // the same number of arguments and thus do not get deoptimized, so the code // inside them can execute faster. function emitNone(handler, isFn, self) { if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } function emitOne(handler, isFn, self, arg1) { if (isFn) handler.call(self, arg1); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1); } } function emitTwo(handler, isFn, self, arg1, arg2) { if (isFn) handler.call(self, arg1, arg2); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2); } } function emitThree(handler, isFn, self, arg1, arg2, arg3) { if (isFn) handler.call(self, arg1, arg2, arg3); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2, arg3); } } function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } EventEmitter.prototype.emit = function emit(type) { var er, handler, len, args, i, events, domain; var doError = (type === 'error'); events = this._events; if (events) doError = (doError && events.error == null); else if (!doError) return false; domain = this.domain; // If there is no 'error' event listener then throw. if (doError) { er = arguments[1]; if (domain) { if (!er) er = new Error('Uncaught, unspecified "error" event'); er.domainEmitter = this; er.domain = domain; er.domainThrown = false; domain.emit('error', er); } else if (er instanceof Error) { throw er; // Unhandled 'error' event } else { // At least give some kind of context to the user var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); err.context = er; throw err; } return false; } handler = events[type]; if (!handler) return false; var isFn = typeof handler === 'function'; len = arguments.length; switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: emitOne(handler, isFn, this, arguments[1]); break; case 3: emitTwo(handler, isFn, this, arguments[1], arguments[2]); break; case 4: emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); break; // slower default: args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; emitMany(handler, isFn, this, args); } return true; }; function _addListener(target, type, listener, prepend) { var m; var events; var existing; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = target._events; if (!events) { events = target._events = new EventHandlers(); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } existing = events[type]; } if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { // If we've already got an array, just append. if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } // Check for listener leak if (!existing.warned) { m = $getMaxListeners(target); if (m && m > 0 && existing.length > m) { existing.warned = true; var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + type + ' listeners added. ' + 'Use emitter.setMaxListeners() to increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; emitWarning(w); } } } return target; } function emitWarning(e) { typeof console.warn === 'function' ? console.warn(e) : console.log(e); } EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); }; function _onceWrap(target, type, listener) { var fired = false; function g() { target.removeListener(type, g); if (!fired) { fired = true; listener.apply(target, arguments); } } g.listener = listener; return g; } EventEmitter.prototype.once = function once(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.prependListener(type, _onceWrap(this, type, listener)); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = this._events; if (!events) return this; list = events[type]; if (!list) return this; if (list === listener || (list.listener && list.listener === listener)) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else { delete events[type]; if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { position = -1; for (i = list.length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { originalListener = list[i].listener; position = i; break; } } if (position < 0) return this; if (list.length === 1) { list[0] = undefined; if (--this._eventsCount === 0) { this._events = new EventHandlers(); return this; } else { delete events[type]; } } else { spliceOne(list, position); } if (events.removeListener) this.emit('removeListener', type, originalListener || listener); } return this; }; // Alias for removeListener added in NodeJS 10.0 // https://nodejs.org/api/events.html#events_emitter_off_eventname_listener EventEmitter.prototype.off = function(type, listener){ return this.removeListener(type, listener); }; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events; events = this._events; if (!events) return this; // not listening for removeListener, no need to emit if (!events.removeListener) { if (arguments.length === 0) { this._events = new EventHandlers(); this._eventsCount = 0; } else if (events[type]) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else delete events[type]; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { var keys = Object.keys(events); for (var i = 0, key; i < keys.length; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = new EventHandlers(); this._eventsCount = 0; return this; } listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners) { // LIFO order do { this.removeListener(type, listeners[listeners.length - 1]); } while (listeners[0]); } return this; }; EventEmitter.prototype.listeners = function listeners(type) { var evlistener; var ret; var events = this._events; if (!events) ret = []; else { evlistener = events[type]; if (!evlistener) ret = []; else if (typeof evlistener === 'function') ret = [evlistener.listener || evlistener]; else ret = unwrapListeners(evlistener); } return ret; }; EventEmitter.listenerCount = function(emitter, type) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(type); } else { return listenerCount.call(emitter, type); } }; EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; if (events) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener) { return evlistener.length; } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; }; // About 1.5x faster than the two-arg version of Array#splice(). function spliceOne(list, index) { for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); } function arrayClone(arr, i) { var copy = new Array(i); while (i--) copy[i] = arr[i]; return copy; } function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { ret[i] = arr[i].listener || arr[i]; } return ret; } 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 };