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