UNPKG

@trutoo/event-bus

Version:

Typesafe cross-platform pubsub event bus ensuring reliable communication between fragments and micro frontends.

381 lines (367 loc) 17.2 kB
'use strict'; var fastEquals = require('fast-equals'); var jsonschema = require('jsonschema'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var extendStatics = function(d, b) { extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; return extendStatics(d, b); }; function __extends(d, b) { if (typeof b !== "function" && b !== null) throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } function __generator(thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } } typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; var PayloadMismatchError = /** @class */ (function (_super) { __extends(PayloadMismatchError, _super); /** * Creates a new PayloadMismatchError error * @param channel - name of event channel * @param schema - registered schema on event channel * @param payload - payload detail sent */ function PayloadMismatchError(channel, schema, payload) { var _this = _super.call(this, "Payload does not match the specified schema for channel [".concat(channel, "]. Schema: ").concat(JSON.stringify(schema), ", Payload: ").concat(JSON.stringify(payload))) || this; _this.channel = channel; _this.schema = schema; _this.payload = payload; if (Error.captureStackTrace) { Error.captureStackTrace(_this, PayloadMismatchError); } _this.name = 'PayloadMismatchError'; return _this; } return PayloadMismatchError; }(Error)); var SchemaMismatchError = /** @class */ (function (_super) { __extends(SchemaMismatchError, _super); /** * Creates a new SchemaMismatchError error * @param channel - name of event channel * @param schema - registered schema on event channel * @param newSchema - new schema attempting to be registered on event channel */ function SchemaMismatchError(channel, schema, newSchema) { var _this = _super.call(this, "Schema registration for [".concat(channel, "] must match already registered schema.")) || this; _this.channel = channel; _this.schema = schema; _this.newSchema = newSchema; if (Error.captureStackTrace) { Error.captureStackTrace(_this, SchemaMismatchError); } _this.name = 'SchemaMismatchError'; return _this; } return SchemaMismatchError; }(Error)); var EventBus = /** @class */ (function () { function EventBus(options) { if (options === void 0) { options = {}; } this._lastId = 0; this._subscriptions = new Map(); this._options = __assign({ logLevel: 'error' }, options); this._validator = new jsonschema.Validator(); } /** * Generates and returns the next available sequential identifier. * Increments the internal counter after returning the current value. * @returns The next sequential identifier */ EventBus.prototype._getNextId = function () { return this._lastId++; }; /** * Logs messages for debugging and monitoring. * @param message - The message to log. * @param level - The log level (info, warn, error). */ EventBus.prototype._log = function (message, level) { if (level === void 0) { level = 'info'; } var logLevels = { none: 0, error: 1, warn: 2, info: 3, }; if (logLevels[this._options.logLevel] >= logLevels[level]) { console[level]("[EventBus] ".concat(message)); } }; /** * Safely executes a callback asynchronously with error handling. * @param callback - The callback to execute. * @param event - The event to pass to the callback. */ EventBus.prototype._asyncCallback = function (callback, event) { return __awaiter(this, void 0, void 0, function () { var error_1; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 3]); return [4 /*yield*/, callback(event)]; case 1: _a.sent(); return [3 /*break*/, 3]; case 2: error_1 = _a.sent(); this._log("Error in callback execution: ".concat(error_1 instanceof Error ? error_1.message : String(error_1)), 'error'); return [3 /*break*/, 3]; case 3: return [2 /*return*/]; } }); }); }; /** * Gets an existing channel subscription or creates a new one if it doesn't exist. * @param channel - The channel identifier to get or create a subscription for * @returns The channel subscription object containing registered callbacks */ EventBus.prototype._getOrCreateChannel = function (channel) { var sub = this._subscriptions.get(channel); if (!sub) { sub = { callbacks: {} }; this._subscriptions.set(channel, sub); } return sub; }; /** * Register a schema for the specified channel and equality checking on subsequent registers. * Subsequent registers must use an equal schema or an error will be thrown. * @param channel - name of event channel to register schema to * @param schema - all communication on channel must follow this schema * @returns returns true if event channel already existed of false if a new one was created * * @throws {SchemaMismatchError} * This exception is thrown if new schema does not match already registered schema. */ EventBus.prototype.register = function (channel, schema) { this._log("Registering schema for channel [".concat(channel, "]")); var sub = this._getOrCreateChannel(channel); var exists = !!sub.schema; if (exists && !fastEquals.strictDeepEqual(sub.schema, schema)) { throw new SchemaMismatchError(channel, sub.schema, schema); } sub.schema = schema; return exists; }; /** * Unregister the schema for the specified channel if channel exists. * @param channel - name of event channel to unregister schema from * @returns returns true if event channel existed and an existing schema was removed */ EventBus.prototype.unregister = function (channel) { var sub = this._subscriptions.get(channel); if (sub === null || sub === void 0 ? void 0 : sub.schema) { delete sub.schema; this._log("Unregistered schema for channel [".concat(channel, "]")); return true; } return false; }; EventBus.prototype.subscribe = function (channel, param2, param3) { return __awaiter(this, void 0, void 0, function () { var id, replay, callback, sub; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: id = this._getNextId().toString(); replay = typeof param2 === 'boolean' ? param2 : false; callback = typeof param2 === 'function' ? param2 : param3; if (typeof callback !== 'function') { throw new Error('Callback function must be supplied as either the second or third argument.'); } sub = this._getOrCreateChannel(channel); if (!(replay && sub.replay !== undefined)) return [3 /*break*/, 2]; return [4 /*yield*/, this._asyncCallback(callback, { channel: channel, payload: sub.replay, })]; case 1: _a.sent(); _a.label = 2; case 2: sub.callbacks[id] = callback; return [2 /*return*/, { unsubscribe: function () { delete sub.callbacks[id]; _this._log("Unsubscribed from channel [".concat(channel, "]")); }, }]; } }); }); }; /** * Helper method to publish an event to a specific channel. * @param channel - The channel to publish to. * @param payload - The payload to send. */ EventBus.prototype._publishToChannel = function (channel, origin, payload) { return __awaiter(this, void 0, void 0, function () { var sub, event; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: sub = this._subscriptions.get(channel); if (!sub) return [2 /*return*/]; event = { channel: origin, payload: payload }; return [4 /*yield*/, Promise.all(Object.values(sub.callbacks).map(function (callback) { return _this._asyncCallback(callback, event); }))]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; /** * Publishes a payload to the specified channel and triggers all subscription callbacks. * If a schema is registered for the channel, the payload will be validated against it. * @param channel - The name of the event channel to send the payload on. * @param payload - The payload to be sent. * @returns Promise that resolves when all callbacks have completed * @throws {PayloadMismatchError} If the payload does not match the registered schema. */ EventBus.prototype.publish = function (channel, payload) { return __awaiter(this, void 0, void 0, function () { var sub; return __generator(this, function (_a) { switch (_a.label) { case 0: sub = this._getOrCreateChannel(channel); if (typeof payload !== 'undefined' && sub.schema && !this._validator.validate(payload, sub.schema).valid) { throw new PayloadMismatchError(channel, sub.schema, payload); } sub.replay = payload; return [4 /*yield*/, Promise.all([ this._publishToChannel(channel, channel, payload), this._publishToChannel('*', channel, payload), ])]; case 1: _a.sent(); return [2 /*return*/]; } }); }); }; /** * Get the latest published payload on the specified event channel. * @param channel - name of the event channel to fetch the latest payload from * @returns the latest payload or `undefined` */ EventBus.prototype.getLatest = function (channel) { var _a; return (_a = this._subscriptions.get(channel)) === null || _a === void 0 ? void 0 : _a.replay; }; /** * Get the schema registered on the specified event channel. * @param channel - name of the event channel to fetch the schema from * @returns the schema or `undefined` */ EventBus.prototype.getSchema = function (channel) { var _a; return (_a = this._subscriptions.get(channel)) === null || _a === void 0 ? void 0 : _a.schema; }; /** * Clears the replay event for the specified channel. * @param channel - The name of the event channel to clear the replay event from. * @returns Returns true if the replay event was cleared, false otherwise. */ EventBus.prototype.clearReplay = function (channel) { var sub = this._subscriptions.get(channel); if ((sub === null || sub === void 0 ? void 0 : sub.replay) !== undefined) { delete sub.replay; this._log("Cleared replay event for channel [".concat(channel, "]")); return true; } return false; }; return EventBus; }()); var getGlobal = function () { /* istanbul ignore next */ if (typeof self !== 'undefined') { return self; } /* istanbul ignore next */ if (typeof window !== 'undefined') { return window; } /* istanbul ignore next */ if (typeof global !== 'undefined') { return global; } /* istanbul ignore next */ throw new Error('unable to locate global object'); }; getGlobal().eventBus = new EventBus(); exports.EventBus = EventBus; exports.getGlobal = getGlobal;