UNPKG

@uppy/companion

Version:

OAuth helper and remote fetcher for Uppy's (https://uppy.io) extensible file upload widget with support for drag&drop, resumable uploads, previews, restrictions, file processing/encoding, remote providers like Dropbox and Google Drive, S3 and more :dog:

211 lines (184 loc) 5.73 kB
import { EventEmitter } from 'node:events' import safeStringify from 'fast-safe-stringify' import * as logger from '../logger.js' function replacer(key, value) { // Remove the circular structure and internal ones return key[0] === '_' || value === '[Circular]' ? undefined : value } /** * This module simulates the builtin events.EventEmitter but with the use of redis. * This is useful for when companion is running on multiple instances and events need * to be distributed across. * * @param {import('ioredis').Redis} redisClient * @param {string} redisPubSubScope * @returns */ export default function redisEmitter(redisClient, redisPubSubScope) { const prefix = redisPubSubScope ? `${redisPubSubScope}:` : '' const getPrefixedEventName = (eventName) => `${prefix}${eventName}` const errorEmitter = new EventEmitter() const handleError = (err) => errorEmitter.emit('error', err) async function makeRedis() { const publisher = redisClient.duplicate({ lazyConnect: true }) publisher.on('error', (err) => logger.error('publisher redis error', err.toString()), ) const subscriber = publisher.duplicate() subscriber.on('error', (err) => logger.error('subscriber redis error', err.toString()), ) await publisher.connect() await subscriber.connect() return { subscriber, publisher } } const redisPromise = makeRedis() redisPromise.catch((err) => handleError(err)) /** * * @param {(a: Awaited<typeof redisPromise>) => void} fn */ async function runWhenConnected(fn) { try { await fn(await redisPromise) } catch (err) { handleError(err) } } // because each event can have multiple listeners, we need to keep track of them /** @type {Map<string, Map<() => unknown, () => unknown>>} */ const handlersByEventName = new Map() /** * Remove an event listener * * @param {string} eventName name of the event * @param {any} handler the handler of the event to remove */ async function removeListener(eventName, handler) { if (eventName === 'error') { errorEmitter.removeListener('error', handler) return } const actualHandlerByHandler = handlersByEventName.get(eventName) if (actualHandlerByHandler == null) return const actualHandler = actualHandlerByHandler.get(handler) if (actualHandler == null) return actualHandlerByHandler.delete(handler) const didRemoveLastListener = actualHandlerByHandler.size === 0 if (didRemoveLastListener) { handlersByEventName.delete(eventName) } await runWhenConnected(async ({ subscriber }) => { subscriber.off('pmessage', actualHandler) if (didRemoveLastListener) { await subscriber.punsubscribe(getPrefixedEventName(eventName)) } }) } /** * * @param {string} eventName * @param {*} handler * @param {*} _once */ async function addListener(eventName, handler, _once = false) { if (eventName === 'error') { if (_once) errorEmitter.once('error', handler) else errorEmitter.addListener('error', handler) return } function actualHandler(pattern, channel, message) { if (pattern !== getPrefixedEventName(eventName)) { return } if (_once) removeListener(eventName, handler) let args try { args = JSON.parse(message) } catch (_ex) { handleError( new Error( `Invalid JSON received! Channel: ${eventName} Message: ${message}`, ), ) return } handler(...args) } let actualHandlerByHandler = handlersByEventName.get(eventName) if (actualHandlerByHandler == null) { actualHandlerByHandler = new Map() handlersByEventName.set(eventName, actualHandlerByHandler) } actualHandlerByHandler.set(handler, actualHandler) await runWhenConnected(async ({ subscriber }) => { subscriber.on('pmessage', actualHandler) await subscriber.psubscribe(getPrefixedEventName(eventName)) }) } /** * Add an event listener * * @param {string} eventName name of the event * @param {any} handler the handler of the event */ async function on(eventName, handler) { await addListener(eventName, handler) } /** * Remove an event listener * * @param {string} eventName name of the event * @param {any} handler the handler of the event */ async function off(eventName, handler) { await removeListener(eventName, handler) } /** * Add an event listener (will be triggered at most once) * * @param {string} eventName name of the event * @param {any} handler the handler of the event */ async function once(eventName, handler) { await addListener(eventName, handler, true) } /** * Announce the occurrence of an event * * @param {string} eventName name of the event */ async function emit(eventName, ...args) { await runWhenConnected(async ({ publisher }) => publisher.publish( getPrefixedEventName(eventName), safeStringify.default(args, replacer), ), ) } /** * Remove all listeners of an event * * @param {string} eventName name of the event */ async function removeAllListeners(eventName) { if (eventName === 'error') { errorEmitter.removeAllListeners(eventName) return } const actualHandlerByHandler = handlersByEventName.get(eventName) if (actualHandlerByHandler != null) { for (const handler of actualHandlerByHandler.keys()) { await removeListener(eventName, handler) } } } return { on, off, once, emit, removeListener, removeAllListeners, } }