UNPKG

@cardos/service-bus-portal

Version:

A modular, composable cross-context communication system for Web Workers, iframes, Shared Workers, and Service Workers

414 lines (411 loc) 11.9 kB
//#region src/core.ts /** * Base portal implementation */ var BasePortal = class { messageHandlers = []; connected = false; messageIdCounter = 0; constructor(id, type, config) { this.id = id; this.type = type; this.config = config; } onMessage(handler) { this.messageHandlers.push(handler); } isConnected() { return this.connected; } generateMessageId() { return `${this.id}_${Date.now()}_${++this.messageIdCounter}`; } notifyHandlers(message) { this.messageHandlers.forEach((handler) => { try { handler(message); } catch (error) { console.error("Portal message handler error:", error); } }); } validateMessage(message) { return Boolean(message.id && message.type && message.timestamp > 0); } }; /** * PostMessage portal implementation */ var PostMessagePortal = class extends BasePortal { target; messageListener; constructor(id, direction, target, config) { super(id, direction, config); this.target = target; } async send(message) { if (!this.connected) throw new Error("Portal not connected"); const wrappedMessage = { __portal: true, portalId: this.id, data: message }; if (typeof Window !== "undefined" && this.target instanceof Window) this.target.postMessage(wrappedMessage, this.getTargetOrigin()); else if (typeof self !== "undefined" && this.target === self) self.postMessage(wrappedMessage); else this.target.postMessage(wrappedMessage); } async connect() { if (this.connected) return; this.messageListener = (event) => { const data = event.data; if ((data === null || data === void 0 ? void 0 : data.__portal) && data.portalId === this.id) { const message = data.data; if (this.validateMessage(message)) this.notifyHandlers(message); } }; const eventTarget = this.getEventTarget(); eventTarget.addEventListener("message", this.messageListener); this.connected = true; } async disconnect() { if (!this.connected) return; if (this.messageListener) { this.getEventTarget().removeEventListener("message", this.messageListener); this.messageListener = void 0; } this.connected = false; } getTargetInfo() { return { type: this.type, origin: this.getTargetOrigin(), capabilities: ["postmessage"], metadata: { target: this.target } }; } getEventTarget() { return this.target; } getTargetOrigin() { if (typeof Window !== "undefined" && this.target instanceof Window) return this.target.location.origin; return "*"; } }; /** * EventTarget portal implementation */ var EventTargetPortal = class extends BasePortal { eventTarget; channel; listener; constructor(id, direction, eventTarget, channel, config) { super(id, direction, config); this.eventTarget = eventTarget; this.channel = channel; } async send(message) { if (!this.connected) throw new Error("Portal not connected"); const event = new CustomEvent(`${this.channel}:portal`, { detail: { portalId: this.id, data: message } }); this.eventTarget.dispatchEvent(event); } async connect() { if (this.connected) return; this.listener = (event) => { const detail = event.detail; if ((detail === null || detail === void 0 ? void 0 : detail.portalId) === this.id) { const message = detail.data; if (this.validateMessage(message)) this.notifyHandlers(message); } }; this.eventTarget.addEventListener(`${this.channel}:portal`, this.listener); this.connected = true; } async disconnect() { if (!this.connected) return; if (this.listener) { this.eventTarget.removeEventListener(`${this.channel}:portal`, this.listener); this.listener = void 0; } this.connected = false; } getTargetInfo() { return { type: this.type, capabilities: ["event-target"], metadata: { channel: this.channel, eventTarget: this.eventTarget } }; } }; //#endregion //#region src/service-bus.ts /** * Service bus connector based on portal */ var PortalServiceBusConnector = class { portal; serviceBus; constructor(portal, serviceBus) { this.portal = portal; this.serviceBus = serviceBus; this.setupMessageHandling(); } async connect() { await this.portal.connect(); } async disconnect() { await this.portal.disconnect(); } setupMessageHandling() { this.portal.onMessage(async (message) => { if (message.type === "invoke") await this.handleInvoke(message); }); } async handleInvoke(message) { try { const { key, args = [] } = message.data; if (!key) throw new Error("Missing service key"); const result = await Promise.resolve(this.serviceBus.invoke(key, ...args)); const responseMessage = { id: this.portal.generateMessageId(), type: "result", timestamp: Date.now(), source: this.portal.id, target: message.source, data: { result }, metadata: { originalMessageId: message.id } }; await this.portal.send(responseMessage); } catch (error) { console.error(`[PortalServiceBusConnector] Error handling invoke:`, error); const errorMessage = { id: this.portal.generateMessageId(), type: "error", timestamp: Date.now(), source: this.portal.id, target: message.source, data: { error: error instanceof Error ? error.message : String(error) }, metadata: { originalMessageId: message.id } }; await this.portal.send(errorMessage); } } }; /** * Service bus proxy based on portal */ var PortalServiceBusProxy = class { portal; pending = /* @__PURE__ */ new Map(); constructor(portal) { this.portal = portal; this.setupMessageHandling(); } async connect() { await this.portal.connect(); } async disconnect() { for (const [id, pending] of this.pending) { clearTimeout(pending.timer); pending.reject(new Error("Portal disconnected")); } this.pending.clear(); await this.portal.disconnect(); } setupMessageHandling() { this.portal.onMessage((message) => { var _message$metadata, _message$metadata3; const pending = this.pending.get((_message$metadata = message.metadata) === null || _message$metadata === void 0 ? void 0 : _message$metadata.originalMessageId); if (!pending) { var _message$metadata2; console.warn(`[PortalServiceBusProxy] No pending request found for messageId: ${(_message$metadata2 = message.metadata) === null || _message$metadata2 === void 0 ? void 0 : _message$metadata2.originalMessageId}`); return; } clearTimeout(pending.timer); this.pending.delete((_message$metadata3 = message.metadata) === null || _message$metadata3 === void 0 ? void 0 : _message$metadata3.originalMessageId); if (message.type === "result") pending.resolve(message.data.result); else if (message.type === "error") { console.error(`[PortalServiceBusProxy] Rejecting with error:`, message.data.error); pending.reject(new Error(message.data.error)); } }); } async invoke(key, ...args) { if (!this.portal.isConnected()) throw new Error("Portal not connected"); const messageId = this.portal.generateMessageId(); return new Promise((resolve, reject) => { const timer = setTimeout(() => { console.error(`[PortalServiceBusProxy] Service invocation timeout for: ${key} (messageId: ${messageId})`); console.error(`[PortalServiceBusProxy] Pending requests:`, Array.from(this.pending.keys())); this.pending.delete(messageId); reject(new Error(`Service invocation timeout for: ${key}`)); }, 1e4); this.pending.set(messageId, { resolve, reject, timer }); const message = { id: messageId, type: "invoke", timestamp: Date.now(), source: this.portal.id, target: "*", data: { key, args }, metadata: { messageId } }; this.portal.send(message).catch((error) => { console.error(`[PortalServiceBusProxy] Failed to send message:`, error); this.pending.delete(messageId); clearTimeout(timer); reject(error); }); }); } createProxy() { return new Proxy({}, { get: (_, prop) => { return (...args) => this.invoke(prop, ...args); } }); } }; //#endregion //#region src/factory.ts /** * Portal factory for creating different types of portals */ var PortalFactory = class { /** * Create a PostMessage-based portal */ static createPostMessagePortal(id, direction, target, config = {}) { const fullConfig = { id, type: direction, timeoutMs: 1e4, retryAttempts: 3, ...config }; return new PostMessagePortal(id, direction, target, fullConfig); } /** * Create an EventTarget-based portal */ static createEventTargetPortal(id, direction, eventTarget, channel, config = {}) { const fullConfig = { id, type: direction, timeoutMs: 1e4, retryAttempts: 3, ...config }; return new EventTargetPortal(id, direction, eventTarget, channel, fullConfig); } /** * Create a portal for Web Worker communication * Automatically determines the correct configuration based on current context */ static createWorkerPortal(worker, config = {}, portalId) { const id = portalId || `worker-${Date.now()}`; if (typeof window !== "undefined" && window === self) return this.createPostMessagePortal(id, "worker-to-window", worker, config); else return this.createPostMessagePortal(id, "worker-to-window", worker, config); } /** * Create a portal for window communication from worker context */ static createWindowPortal(config = {}, portalId) { const id = portalId || `window-${Date.now()}`; return this.createPostMessagePortal(id, "worker-to-window", self, config); } /** * Create a portal for iframe communication * Automatically determines the correct configuration based on current context */ static createIframePortal(iframe, config = {}) { const id = `iframe-${Date.now()}`; if (typeof window !== "undefined" && window !== self) return this.createPostMessagePortal(id, "iframe-to-window", window, config); else return this.createPostMessagePortal(id, "iframe-to-window", self, config); } }; /** * Portal composer for managing multiple portals */ var PortalComposer = class { portals = /* @__PURE__ */ new Map(); connectors = /* @__PURE__ */ new Map(); proxies = /* @__PURE__ */ new Map(); /** * Add a portal to the composer */ addPortal(portal) { this.portals.set(portal.id, portal); } /** * Remove a portal from the composer */ removePortal(portalId) { const portal = this.portals.get(portalId); if (portal) { portal.disconnect(); this.portals.delete(portalId); this.connectors.delete(portalId); this.proxies.delete(portalId); } } /** * Create a service bus connector for a portal */ createConnector(portalId, serviceBus) { const portal = this.portals.get(portalId); if (!portal) throw new Error(`Portal not found: ${portalId}`); const connector = new PortalServiceBusConnector(portal, serviceBus); this.connectors.set(portalId, connector); return connector; } /** * Create a service bus proxy for a portal */ createProxy(portalId) { const portal = this.portals.get(portalId); if (!portal) throw new Error(`Portal not found: ${portalId}`); const proxy = new PortalServiceBusProxy(portal); this.proxies.set(portalId, proxy); return proxy; } /** * Connect all portals */ async connectAll() { const promises = Array.from(this.portals.values()).map((portal) => portal.connect()); await Promise.all(promises); } /** * Disconnect all portals */ async disconnectAll() { const promises = Array.from(this.portals.values()).map((portal) => portal.disconnect()); await Promise.all(promises); } /** * Get a portal by ID */ getPortal(portalId) { return this.portals.get(portalId); } /** * List all portals */ listPortals() { return Array.from(this.portals.values()); } }; //#endregion export { BasePortal, EventTargetPortal, PortalComposer, PortalFactory, PortalServiceBusConnector, PortalServiceBusProxy, PostMessagePortal }; //# sourceMappingURL=index.mjs.map