message-nexus 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs ADDED
@@ -0,0 +1,572 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ BaseDriver: () => BaseDriver,
34
+ BroadcastDriver: () => BroadcastDriver,
35
+ MittDriver: () => MittDriver,
36
+ PostMessageDriver: () => PostMessageDriver,
37
+ WebSocketDriver: () => WebSocketDriver,
38
+ createEmitter: () => createEmitter,
39
+ default: () => MessageBridge
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/drivers/BaseDriver.ts
44
+ var BaseDriver = class {
45
+ constructor() {
46
+ this.onMessage = null;
47
+ }
48
+ send(data) {
49
+ throw new Error("Not implemented");
50
+ }
51
+ destroy() {
52
+ }
53
+ };
54
+
55
+ // src/drivers/BroadcastDriver.ts
56
+ var MESSAGE_NEXUS_PROTOCOL = "message-nexus-v1";
57
+ function isBridgeMessage(data) {
58
+ return typeof data === "object" && data !== null && "__messageBridge" in data && data.__messageBridge === MESSAGE_NEXUS_PROTOCOL;
59
+ }
60
+ var BroadcastDriver = class extends BaseDriver {
61
+ constructor(options) {
62
+ super();
63
+ this.messageHandler = null;
64
+ if (!options.channel) {
65
+ throw new Error("BroadcastDriver requires a channel name");
66
+ }
67
+ this.channel = new BroadcastChannel(options.channel);
68
+ this.messageHandler = (event) => {
69
+ if (!isBridgeMessage(event.data)) {
70
+ return;
71
+ }
72
+ const { __messageBridge, ...message } = event.data;
73
+ this.onMessage?.(message);
74
+ };
75
+ this.channel.addEventListener("message", this.messageHandler);
76
+ }
77
+ send(data) {
78
+ const bridgeMessage = {
79
+ ...data,
80
+ __messageBridge: MESSAGE_NEXUS_PROTOCOL
81
+ };
82
+ this.channel.postMessage(bridgeMessage);
83
+ }
84
+ destroy() {
85
+ if (this.channel) {
86
+ this.channel.close();
87
+ }
88
+ if (this.messageHandler) {
89
+ this.channel.removeEventListener("message", this.messageHandler);
90
+ this.messageHandler = null;
91
+ }
92
+ this.onMessage = null;
93
+ }
94
+ };
95
+
96
+ // src/drivers/MittDriver.ts
97
+ var eventIndicator = "message_bridge_message_event";
98
+ var MittDriver = class extends BaseDriver {
99
+ constructor(emitter) {
100
+ super();
101
+ this.emitter = emitter;
102
+ this.listener = () => {
103
+ };
104
+ const handler = (data) => {
105
+ if (data) this.onMessage?.(data);
106
+ };
107
+ this.emitter.on(eventIndicator, handler);
108
+ this.listener = () => {
109
+ this.emitter.off(eventIndicator, handler);
110
+ };
111
+ }
112
+ send(data) {
113
+ this.emitter.emit(eventIndicator, data);
114
+ }
115
+ destroy() {
116
+ this.listener();
117
+ this.onMessage = null;
118
+ }
119
+ };
120
+
121
+ // src/drivers/PostMessageDriver.ts
122
+ var MESSAGE_NEXUS_PROTOCOL2 = "message-nexus-v1";
123
+ function isBridgeMessage2(data) {
124
+ return typeof data === "object" && data !== null && "__messageBridge" in data && data.__messageBridge === MESSAGE_NEXUS_PROTOCOL2;
125
+ }
126
+ var PostMessageDriver = class extends BaseDriver {
127
+ constructor(targetWindow, targetOrigin) {
128
+ super();
129
+ this.messageHandler = null;
130
+ if (!targetOrigin || targetOrigin === "*") {
131
+ throw new Error(
132
+ 'PostMessageDriver requires explicit targetOrigin for security. Do not use "*" as it allows any origin.'
133
+ );
134
+ }
135
+ this.targetWindow = targetWindow;
136
+ this.targetOrigin = targetOrigin;
137
+ this.messageHandler = (event) => {
138
+ if (event.origin !== this.targetOrigin) {
139
+ return;
140
+ }
141
+ if (!isBridgeMessage2(event.data)) {
142
+ return;
143
+ }
144
+ const { __messageBridge, ...message } = event.data;
145
+ this.onMessage?.(message);
146
+ };
147
+ window.addEventListener("message", this.messageHandler);
148
+ }
149
+ send(data) {
150
+ const bridgeMessage = {
151
+ ...data,
152
+ __messageBridge: MESSAGE_NEXUS_PROTOCOL2
153
+ };
154
+ this.targetWindow.postMessage(bridgeMessage, this.targetOrigin);
155
+ }
156
+ destroy() {
157
+ if (this.messageHandler) {
158
+ window.removeEventListener("message", this.messageHandler);
159
+ this.messageHandler = null;
160
+ }
161
+ this.onMessage = null;
162
+ }
163
+ };
164
+
165
+ // src/utils/logger.ts
166
+ var Logger = class {
167
+ constructor(context, minLevel = "info" /* INFO */) {
168
+ this.handlers = [];
169
+ this.context = context;
170
+ this.minLevel = minLevel;
171
+ }
172
+ addHandler(handler) {
173
+ this.handlers.push(handler);
174
+ }
175
+ setMinLevel(level) {
176
+ this.minLevel = level;
177
+ }
178
+ shouldLog(level) {
179
+ const levels = ["debug" /* DEBUG */, "info" /* INFO */, "warn" /* WARN */, "error" /* ERROR */];
180
+ return levels.indexOf(level) >= levels.indexOf(this.minLevel);
181
+ }
182
+ log(level, message, metadata) {
183
+ if (!this.shouldLog(level)) return;
184
+ const entry = {
185
+ level,
186
+ timestamp: Date.now(),
187
+ message,
188
+ metadata,
189
+ context: this.context
190
+ };
191
+ this.handlers.forEach((handler) => handler(entry));
192
+ }
193
+ debug(message, metadata) {
194
+ this.log("debug" /* DEBUG */, message, metadata);
195
+ }
196
+ info(message, metadata) {
197
+ this.log("info" /* INFO */, message, metadata);
198
+ }
199
+ warn(message, metadata) {
200
+ this.log("warn" /* WARN */, message, metadata);
201
+ }
202
+ error(message, metadata) {
203
+ this.log("error" /* ERROR */, message, metadata);
204
+ }
205
+ };
206
+ var createConsoleHandler = () => {
207
+ return (entry) => {
208
+ const timestamp = new Date(entry.timestamp).toISOString();
209
+ const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.context || "app"}]`;
210
+ const logFn = entry.level === "debug" /* DEBUG */ ? console.debug : entry.level === "info" /* INFO */ ? console.info : entry.level === "warn" /* WARN */ ? console.warn : console.error;
211
+ if (entry.metadata) {
212
+ logFn(prefix, entry.message, entry.metadata);
213
+ } else {
214
+ logFn(prefix, entry.message);
215
+ }
216
+ };
217
+ };
218
+
219
+ // src/drivers/WebSocktDriver.ts
220
+ var MESSAGE_NEXUS_PROTOCOL3 = "message-nexus-v1";
221
+ var WebSocketDriver = class extends BaseDriver {
222
+ constructor(options) {
223
+ super();
224
+ this.ws = null;
225
+ this.retryCount = 0;
226
+ this.reconnectTimer = null;
227
+ this.isManuallyClosed = false;
228
+ this.url = options.url;
229
+ this.reconnectEnabled = options.reconnect !== false;
230
+ this.maxRetries = (typeof options.reconnect === "object" ? options.reconnect.maxRetries : void 0) ?? Infinity;
231
+ this.retryInterval = (typeof options.reconnect === "object" ? options.reconnect.retryInterval : void 0) ?? 5e3;
232
+ this.logger = options.logger || new Logger("WebSocketDriver");
233
+ this.logger.addHandler(createConsoleHandler());
234
+ this.connect();
235
+ }
236
+ connect() {
237
+ this.ws = new WebSocket(this.url);
238
+ this.ws.addEventListener("open", () => {
239
+ this.logger.info("WebSocket connected", { url: this.url });
240
+ this.retryCount = 0;
241
+ });
242
+ this.ws.addEventListener("message", (event) => {
243
+ try {
244
+ const rawData = JSON.parse(event.data);
245
+ if (typeof rawData === "object" && rawData !== null && "__messageBridge" in rawData && rawData.__messageBridge === MESSAGE_NEXUS_PROTOCOL3) {
246
+ const { __messageBridge, ...data } = rawData;
247
+ this.logger.debug("Message received", { data });
248
+ this.onMessage?.(data);
249
+ } else {
250
+ this.logger.debug("Ignored non-bridge message", { data: rawData });
251
+ }
252
+ } catch (error) {
253
+ this.logger.error("Failed to parse WebSocket message", { error, data: event.data });
254
+ }
255
+ });
256
+ this.ws.addEventListener("error", (event) => {
257
+ this.logger.error("WebSocket error", { event });
258
+ });
259
+ this.ws.addEventListener("close", () => {
260
+ this.logger.info("WebSocket connection closed", {
261
+ manuallyClosed: this.isManuallyClosed,
262
+ retryCount: this.retryCount,
263
+ maxRetries: this.maxRetries
264
+ });
265
+ if (!this.isManuallyClosed && this.reconnectEnabled && this.retryCount < this.maxRetries) {
266
+ this.scheduleReconnect();
267
+ }
268
+ });
269
+ }
270
+ scheduleReconnect() {
271
+ this.retryCount++;
272
+ const delay = this.retryInterval * this.retryCount;
273
+ this.logger.info("Reconnecting scheduled", {
274
+ delay,
275
+ attempt: this.retryCount,
276
+ maxRetries: this.maxRetries,
277
+ url: this.url
278
+ });
279
+ this.reconnectTimer = window.setTimeout(() => {
280
+ this.connect();
281
+ }, delay);
282
+ }
283
+ send(data) {
284
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
285
+ this.logger.error("WebSocket is not open", {
286
+ state: this.ws?.readyState,
287
+ url: this.url
288
+ });
289
+ throw new Error("WebSocket is not open");
290
+ }
291
+ this.logger.debug("Sending message", { data });
292
+ const bridgeMessage = {
293
+ ...data,
294
+ __messageBridge: MESSAGE_NEXUS_PROTOCOL3
295
+ };
296
+ this.ws.send(JSON.stringify(bridgeMessage));
297
+ }
298
+ close() {
299
+ this.logger.info("Closing WebSocket connection", { url: this.url });
300
+ this.isManuallyClosed = true;
301
+ if (this.reconnectTimer) {
302
+ clearTimeout(this.reconnectTimer);
303
+ this.reconnectTimer = null;
304
+ }
305
+ if (this.ws) {
306
+ this.ws.close();
307
+ this.ws = null;
308
+ }
309
+ }
310
+ destroy() {
311
+ this.close();
312
+ this.onMessage = null;
313
+ }
314
+ };
315
+
316
+ // src/utils/emitter.ts
317
+ var import_mitt = __toESM(require("mitt"), 1);
318
+ function createEmitter() {
319
+ return (0, import_mitt.default)();
320
+ }
321
+
322
+ // src/index.ts
323
+ var MessageBridge = class {
324
+ constructor(driver, options) {
325
+ this.cleanupInterval = null;
326
+ this.messageQueue = [];
327
+ this.maxQueueSize = 100;
328
+ this.errorHandler = null;
329
+ this.metrics = {
330
+ messagesSent: 0,
331
+ messagesReceived: 0,
332
+ messagesFailed: 0,
333
+ pendingMessages: 0,
334
+ queuedMessages: 0,
335
+ totalLatency: 0,
336
+ averageLatency: 0
337
+ };
338
+ this.metricsCallbacks = /* @__PURE__ */ new Set();
339
+ this.driver = driver;
340
+ this.instanceId = options?.instanceId || crypto.randomUUID();
341
+ this.timeout = options?.timeout ?? 1e4;
342
+ this.logger = options?.logger || new Logger("MessageBridge");
343
+ this.logger.addHandler(createConsoleHandler());
344
+ this.pendingTasks = /* @__PURE__ */ new Map();
345
+ this.incomingMessages = /* @__PURE__ */ new Map();
346
+ this.messageHandlers = /* @__PURE__ */ new Set();
347
+ this.cleanupInterval = null;
348
+ this.driver.onMessage = (data) => this._handleIncoming(data);
349
+ this.logger.info("MessageBridge initialized", {
350
+ instanceId: this.instanceId,
351
+ timeout: this.timeout
352
+ });
353
+ this.cleanupInterval = window.setInterval(() => {
354
+ const now = Date.now();
355
+ for (const [id, msg] of this.incomingMessages.entries()) {
356
+ if (now - msg.timestamp > this.timeout * 2) {
357
+ this.incomingMessages.delete(id);
358
+ }
359
+ }
360
+ }, 6e4);
361
+ }
362
+ async request(typeOrOptions) {
363
+ const id = crypto.randomUUID();
364
+ let type;
365
+ let payload;
366
+ let to;
367
+ let metadata;
368
+ let timeout;
369
+ let retryCount = 0;
370
+ let retryDelay = 1e3;
371
+ if (typeof typeOrOptions === "string") {
372
+ type = typeOrOptions;
373
+ payload = void 0;
374
+ to = void 0;
375
+ metadata = {};
376
+ timeout = this.timeout;
377
+ } else {
378
+ const opts = typeOrOptions;
379
+ type = opts.type;
380
+ payload = opts.payload;
381
+ to = opts.to;
382
+ metadata = opts.metadata || {};
383
+ timeout = opts.timeout ?? this.timeout;
384
+ retryCount = opts.retryCount ?? 0;
385
+ retryDelay = opts.retryDelay ?? 1e3;
386
+ }
387
+ const attempt = async (attemptNumber) => {
388
+ return new Promise((resolve, reject) => {
389
+ const timer = setTimeout(() => {
390
+ this.pendingTasks.delete(id);
391
+ this.metrics.messagesFailed++;
392
+ this.metrics.pendingMessages--;
393
+ reject(new Error(`Message timeout: ${type} (${id})`));
394
+ }, timeout);
395
+ this.pendingTasks.set(id, { resolve, reject, timer, timestamp: Date.now() });
396
+ const message = {
397
+ id,
398
+ type,
399
+ payload,
400
+ from: this.instanceId,
401
+ to,
402
+ metadata: { ...metadata, timestamp: Date.now() }
403
+ };
404
+ this._sendMessage(message);
405
+ }).catch((error) => {
406
+ if (attemptNumber < retryCount) {
407
+ return new Promise(
408
+ (resolve) => setTimeout(() => resolve(attempt(attemptNumber + 1)), retryDelay * (attemptNumber + 1))
409
+ );
410
+ }
411
+ this.metrics.messagesFailed++;
412
+ this.metrics.pendingMessages--;
413
+ throw error;
414
+ });
415
+ };
416
+ return attempt(0);
417
+ }
418
+ _sendMessage(message) {
419
+ try {
420
+ this.driver.send(message);
421
+ this.metrics.messagesSent++;
422
+ this.metrics.pendingMessages++;
423
+ this.logger.debug("Message sent", { messageId: message.id, type: message.type });
424
+ } catch (error) {
425
+ const err = error instanceof Error ? error : new Error(String(error));
426
+ this.metrics.messagesFailed++;
427
+ this.logger.error("Failed to send message", { error: err.message, messageId: message.id });
428
+ this.errorHandler?.(err, { message });
429
+ if (this.messageQueue.length < this.maxQueueSize) {
430
+ this.messageQueue.push(message);
431
+ this.logger.debug("Message queued", {
432
+ messageId: message.id,
433
+ queueSize: this.messageQueue.length + 1
434
+ });
435
+ } else {
436
+ this.logger.warn("Message queue full, dropping oldest message", {
437
+ queueSize: this.messageQueue.length
438
+ });
439
+ this.messageQueue.shift();
440
+ this.messageQueue.push(message);
441
+ }
442
+ }
443
+ this.metrics.queuedMessages = this.messageQueue.length;
444
+ this._notifyMetrics();
445
+ }
446
+ onError(handler) {
447
+ this.errorHandler = handler;
448
+ return () => {
449
+ this.errorHandler = null;
450
+ };
451
+ }
452
+ flushQueue() {
453
+ while (this.messageQueue.length > 0) {
454
+ const message = this.messageQueue.shift();
455
+ if (message) {
456
+ try {
457
+ this.driver.send(message);
458
+ } catch (error) {
459
+ this.messageQueue.unshift(message);
460
+ break;
461
+ }
462
+ }
463
+ }
464
+ }
465
+ _validateMessage(data) {
466
+ if (!data || typeof data !== "object") return false;
467
+ const msg = data;
468
+ if (typeof msg.id !== "string") return false;
469
+ if (typeof msg.type !== "string") return false;
470
+ if (msg.from && typeof msg.from !== "string") return false;
471
+ if (msg.to && typeof msg.to !== "string") return false;
472
+ if (msg.metadata && typeof msg.metadata !== "object") return false;
473
+ if (msg.isResponse !== void 0 && typeof msg.isResponse !== "boolean") return false;
474
+ return true;
475
+ }
476
+ _handleIncoming(data) {
477
+ if (!this._validateMessage(data)) {
478
+ this.logger.error("Invalid message format received", { data });
479
+ this.errorHandler?.(new Error("Invalid message format received"), { data });
480
+ this.metrics.messagesFailed++;
481
+ return;
482
+ }
483
+ const { id, to, type, payload, isResponse, error, from } = data;
484
+ if (to && to !== this.instanceId) {
485
+ this.logger.debug("Message filtered: not for this instance", {
486
+ messageId: id,
487
+ to,
488
+ instanceId: this.instanceId
489
+ });
490
+ return;
491
+ }
492
+ if (isResponse && this.pendingTasks.has(id)) {
493
+ const { resolve, reject, timer, timestamp } = this.pendingTasks.get(id);
494
+ clearTimeout(timer);
495
+ this.pendingTasks.delete(id);
496
+ const latency = Date.now() - timestamp;
497
+ this.metrics.messagesReceived++;
498
+ this.metrics.pendingMessages--;
499
+ this.metrics.totalLatency += latency;
500
+ this.metrics.averageLatency = this.metrics.totalLatency / this.metrics.messagesReceived;
501
+ this.logger.debug("Response received", { messageId: id, latency });
502
+ if (error) reject(error);
503
+ else resolve(payload);
504
+ this._notifyMetrics();
505
+ return;
506
+ }
507
+ if (isResponse) {
508
+ this.logger.warn("Orphaned response received", { messageId: id });
509
+ return;
510
+ }
511
+ this.logger.debug("Command message received", { messageId: id, type, from });
512
+ this.incomingMessages.set(id, { from, type, timestamp: Date.now() });
513
+ this.messageHandlers.forEach((handler) => handler(data));
514
+ }
515
+ getMetrics() {
516
+ return { ...this.metrics, pendingMessages: this.pendingTasks.size };
517
+ }
518
+ onMetrics(callback) {
519
+ this.metricsCallbacks.add(callback);
520
+ return () => this.metricsCallbacks.delete(callback);
521
+ }
522
+ _notifyMetrics() {
523
+ const metrics = this.getMetrics();
524
+ this.metricsCallbacks.forEach((callback) => callback(metrics));
525
+ }
526
+ onCommand(handler) {
527
+ this.messageHandlers.add(handler);
528
+ return () => this.messageHandlers.delete(handler);
529
+ }
530
+ reply(messageId, payload, error) {
531
+ const incoming = this.incomingMessages.get(messageId);
532
+ if (!incoming) {
533
+ throw new Error(`Message not found: ${messageId}`);
534
+ }
535
+ const responsePayload = payload;
536
+ const responseError = error;
537
+ this.driver.send({
538
+ id: messageId,
539
+ type: `${incoming.type}_RESPONSE`,
540
+ payload: responsePayload,
541
+ error: responseError,
542
+ isResponse: true,
543
+ from: this.instanceId,
544
+ to: incoming.from
545
+ });
546
+ this.incomingMessages.delete(messageId);
547
+ }
548
+ destroy() {
549
+ this.logger.info("MessageBridge destroying", {
550
+ instanceId: this.instanceId,
551
+ pendingMessages: this.pendingTasks.size,
552
+ queuedMessages: this.messageQueue.length,
553
+ metrics: this.getMetrics()
554
+ });
555
+ this.driver.destroy?.();
556
+ if (this.cleanupInterval) {
557
+ clearInterval(this.cleanupInterval);
558
+ this.cleanupInterval = null;
559
+ }
560
+ this.messageHandlers.clear();
561
+ this.metricsCallbacks.clear();
562
+ }
563
+ };
564
+ // Annotate the CommonJS export names for ESM import in node:
565
+ 0 && (module.exports = {
566
+ BaseDriver,
567
+ BroadcastDriver,
568
+ MittDriver,
569
+ PostMessageDriver,
570
+ WebSocketDriver,
571
+ createEmitter
572
+ });