mikroevent
Version:
Ultra-lightweight, Node-native way to handle events, both in-process (as EventEmitter events) or across systems via HTTP(S).
286 lines (283 loc) • 9.42 kB
JavaScript
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/index.ts
var index_exports = {};
__export(index_exports, {
MikroEvent: () => MikroEvent
});
module.exports = __toCommonJS(index_exports);
// src/MikroEvent.ts
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
});
;