@cardos/service-bus-portal
Version:
A modular, composable cross-context communication system for Web Workers, iframes, Shared Workers, and Service Workers
421 lines (417 loc) • 12 kB
JavaScript
//#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
exports.BasePortal = BasePortal;
exports.EventTargetPortal = EventTargetPortal;
exports.PortalComposer = PortalComposer;
exports.PortalFactory = PortalFactory;
exports.PortalServiceBusConnector = PortalServiceBusConnector;
exports.PortalServiceBusProxy = PortalServiceBusProxy;
exports.PostMessagePortal = PostMessagePortal;
//# sourceMappingURL=index.js.map