UNPKG

mikroevent

Version:

Ultra-lightweight, Node-native way to handle events, both in-process (as EventEmitter events) or across systems via HTTP(S).

284 lines (282 loc) 9.42 kB
"use strict"; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/MikroEvent.ts var MikroEvent_exports = {}; __export(MikroEvent_exports, { MikroEvent: () => MikroEvent }); module.exports = __toCommonJS(MikroEvent_exports); var import_node_events = require("events"); var MikroEvent = class { emitter; targets = {}; options; constructor(options) { this.emitter = new import_node_events.EventEmitter(); if (options?.maxListeners === 0) this.emitter.setMaxListeners(0); else this.emitter.setMaxListeners(options?.maxListeners || 10); this.options = { errorHandler: options?.errorHandler || ((error) => console.error(error)) }; } /** * @description Add one or more Targets for events. * @example * // Add single Target that is triggered on all events * events.addTarget({ name: 'my-internal-api', events: ['*'] }); * // Add single Target using HTTPS fetch * events.addTarget({ name: 'my-external-api', url: 'https://api.mydomain.com', events: ['*'] }); * // Add multiple Targets, responding to multiple events (single Target shown) * events.addTarget([{ name: 'my-interla-api', events: ['user.added', 'user.updated'] }]); * @returns Boolean that expresses if all Targets were successfully added. */ addTarget(target) { const targets = Array.isArray(target) ? target : [target]; const results = targets.map((target2) => { if (this.targets[target2.name]) { console.error(`Target with name '${target2.name}' already exists.`); return false; } this.targets[target2.name] = { name: target2.name, url: target2.url, headers: target2.headers || {}, events: target2.events || [] }; return true; }); return results.every((result) => result === true); } /** * @description Update an existing Target. * @example * events.updateTarget('system_a', { url: 'http://localhost:8000', events: ['user.updated'] }; * @returns Boolean that expresses if the Target was successfully added. */ updateTarget(name, update) { if (!this.targets[name]) { console.error(`Target with name '${name}' does not exist.`); return false; } const target = this.targets[name]; if (update.url !== void 0) target.url = update.url; if (update.headers) target.headers = { ...target.headers, ...update.headers }; if (update.events) target.events = update.events; return true; } /** * @description Remove a Target. * @example * events.removeTarget('system_a'); * @returns Boolean that expresses if the Target was successfully removed. */ removeTarget(name) { if (!this.targets[name]) { console.error(`Target with name '${name}' does not exist.`); return false; } delete this.targets[name]; return true; } /** * @description Add one or more events to an existing Target. * @example * events.addEventToTarget('system_a', ['user.updated', 'user.deleted']); * @returns Boolean that expresses if all events were successfully added. */ addEventToTarget(name, events) { if (!this.targets[name]) { console.error(`Target with name '${name}' does not exist.`); return false; } const eventsArray = Array.isArray(events) ? events : [events]; const target = this.targets[name]; eventsArray.forEach((event) => { if (!target.events.includes(event)) target.events.push(event); }); return true; } /** * @description Register an event handler for internal events. */ on(eventName, handler) { this.emitter.on(eventName, handler); return this; } /** * @description Remove an event handler. */ off(eventName, handler) { this.emitter.off(eventName, handler); return this; } /** * @description Register a one-time event handler. */ once(eventName, handler) { this.emitter.once(eventName, handler); return this; } /** * @description Emit an event locally and to its bound Targets. * @example * await events.emit('user.added', { id: 'abc123', name: 'Sam Person' }); * @return Returns a result object with success status and any errors. */ async emit(eventName, data) { const result = { success: true, errors: [] }; const makeError = (targetName, eventName2, error) => ({ target: targetName, event: eventName2, error }); const targets = Object.values(this.targets).filter( (target) => target.events.includes(eventName) || target.events.includes("*") ); targets.filter((target) => !target.url).forEach((target) => { try { this.emitter.emit(eventName, data); } catch (error) { const actualError = error instanceof Error ? error : new Error(String(error)); result.errors.push({ target: target.name, event: eventName, error: actualError }); this.options.errorHandler(actualError, eventName, data); result.success = false; } }); const externalTargets = targets.filter((target) => target.url); if (externalTargets.length > 0) { const promises = externalTargets.map(async (target) => { try { const response = await fetch(target.url, { method: "POST", headers: { "Content-Type": "application/json", ...target.headers }, body: JSON.stringify({ eventName, data }) }); if (!response.ok) { const errorMessage = `HTTP error! Status: ${response.status}: ${response.statusText}`; const httpError = new Error(errorMessage); result.errors.push(makeError(target.name, eventName, httpError)); this.options.errorHandler(httpError, eventName, data); result.success = false; } } catch (error) { const actualError = error instanceof Error ? error : new Error(String(error)); result.errors.push(makeError(target.name, eventName, actualError)); this.options.errorHandler(actualError, eventName, data); result.success = false; } }); await Promise.allSettled(promises); } return result; } /** * @description Handle an incoming event arriving over HTTP. * Used for server integrations, when you want to manually handle * the incoming event payload. * * The processing will be async using `process.nextTick()` * and running in a non-blocking fashion. * @example * await mikroEvent.handleIncomingEvent({ * eventName: 'user.created', * data: { id: '123', name: 'Test User' } * }); */ async handleIncomingEvent(body) { try { const { eventName, data } = typeof body === "string" ? JSON.parse(body) : body; process.nextTick(() => { try { this.emitter.emit(eventName, data); } catch (error) { this.options.errorHandler( error instanceof Error ? error : new Error(String(error)), eventName, data ); } }); } catch (error) { this.options.errorHandler( error instanceof Error ? error : new Error(String(error)), "parse_event" ); throw error; } } /** * @description Create middleware for Express-style servers, i.e. * using `req` and `res` objects. This is an approach that replaces * using `handleIncomingEvent()` manually. * @example * const middleware = mikroEvent.createMiddleware(); * await middleware(req, res, next); */ createMiddleware() { return async (req, res, next) => { if (req.method !== "POST") { if (next) next(); return; } if (req.body) { try { await this.handleIncomingEvent(req.body); res.statusCode = 202; res.end(); } catch (error) { res.statusCode = 400; res.end(JSON.stringify({ error: "Invalid event format" })); if (next) next(error); } } else { let body = ""; req.on("data", (chunk) => body += chunk.toString()); req.on("end", async () => { try { await this.handleIncomingEvent(body); res.statusCode = 202; res.end(); } catch (error) { res.statusCode = 400; res.end(JSON.stringify({ error: "Invalid event format" })); if (next) next(error); } }); } }; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { MikroEvent });