message-nexus 1.0.0 → 1.1.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 CHANGED
@@ -52,7 +52,7 @@ var BroadcastDriver = class extends BaseDriver {
52
52
  };
53
53
 
54
54
  // src/drivers/MittDriver.ts
55
- var eventIndicator = "message_bridge_message_event";
55
+ var eventIndicator = "message_nexus_message_event";
56
56
  var MittDriver = class extends BaseDriver {
57
57
  constructor(emitter) {
58
58
  super();
@@ -121,11 +121,26 @@ var PostMessageDriver = class extends BaseDriver {
121
121
  };
122
122
 
123
123
  // src/utils/logger.ts
124
+ function isLogger(value) {
125
+ if (value == null || typeof value !== "object") return false;
126
+ const logger = value;
127
+ return typeof logger.addHandler === "function" && typeof logger.setMinLevel === "function" && typeof logger.enable === "function" && typeof logger.disable === "function" && typeof logger.isEnabled === "function" && typeof logger.debug === "function" && typeof logger.info === "function" && typeof logger.warn === "function" && typeof logger.error === "function";
128
+ }
124
129
  var Logger = class {
125
- constructor(context, minLevel = "info" /* INFO */) {
130
+ constructor(context, minLevel = "info" /* INFO */, enabled = false) {
126
131
  this.handlers = [];
127
132
  this.context = context;
128
133
  this.minLevel = minLevel;
134
+ this.enabled = enabled;
135
+ }
136
+ enable() {
137
+ this.enabled = true;
138
+ }
139
+ disable() {
140
+ this.enabled = false;
141
+ }
142
+ isEnabled() {
143
+ return this.enabled;
129
144
  }
130
145
  addHandler(handler) {
131
146
  this.handlers.push(handler);
@@ -138,7 +153,7 @@ var Logger = class {
138
153
  return levels.indexOf(level) >= levels.indexOf(this.minLevel);
139
154
  }
140
155
  log(level, message, metadata) {
141
- if (!this.shouldLog(level)) return;
156
+ if (!this.enabled || !this.shouldLog(level)) return;
142
157
  const entry = {
143
158
  level,
144
159
  timestamp: Date.now(),
@@ -161,9 +176,19 @@ var Logger = class {
161
176
  this.log("error" /* ERROR */, message, metadata);
162
177
  }
163
178
  };
179
+ function genTimestamp() {
180
+ const now = /* @__PURE__ */ new Date();
181
+ return now.toLocaleTimeString("zh-CN", {
182
+ hour12: false,
183
+ hour: "2-digit",
184
+ minute: "2-digit",
185
+ second: "2-digit",
186
+ fractionalSecondDigits: 3
187
+ });
188
+ }
164
189
  var createConsoleHandler = () => {
165
190
  return (entry) => {
166
- const timestamp = new Date(entry.timestamp).toISOString();
191
+ const timestamp = genTimestamp();
167
192
  const prefix = `[${timestamp}] [${entry.level.toUpperCase()}] [${entry.context || "app"}]`;
168
193
  const logFn = entry.level === "debug" /* DEBUG */ ? console.debug : entry.level === "info" /* INFO */ ? console.info : entry.level === "warn" /* WARN */ ? console.warn : console.error;
169
194
  if (entry.metadata) {
@@ -189,13 +214,16 @@ var WebSocketDriver = class extends BaseDriver {
189
214
  this.retryInterval = (typeof options.reconnect === "object" ? options.reconnect.retryInterval : void 0) ?? 5e3;
190
215
  this.logger = options.logger || new Logger("WebSocketDriver");
191
216
  this.logger.addHandler(createConsoleHandler());
217
+ this.onStatusChange = options.onStatusChange;
192
218
  this.connect();
193
219
  }
194
220
  connect() {
221
+ this.onStatusChange?.("connecting");
195
222
  this.ws = new WebSocket(this.url);
196
223
  this.ws.addEventListener("open", () => {
197
224
  this.logger.info("WebSocket connected", { url: this.url });
198
225
  this.retryCount = 0;
226
+ this.onStatusChange?.("connected");
199
227
  });
200
228
  this.ws.addEventListener("message", (event) => {
201
229
  try {
@@ -213,6 +241,7 @@ var WebSocketDriver = class extends BaseDriver {
213
241
  });
214
242
  this.ws.addEventListener("error", (event) => {
215
243
  this.logger.error("WebSocket error", { event });
244
+ this.onStatusChange?.("error");
216
245
  });
217
246
  this.ws.addEventListener("close", () => {
218
247
  this.logger.info("WebSocket connection closed", {
@@ -222,6 +251,8 @@ var WebSocketDriver = class extends BaseDriver {
222
251
  });
223
252
  if (!this.isManuallyClosed && this.reconnectEnabled && this.retryCount < this.maxRetries) {
224
253
  this.scheduleReconnect();
254
+ } else {
255
+ this.onStatusChange?.("disconnected");
225
256
  }
226
257
  });
227
258
  }
@@ -234,6 +265,7 @@ var WebSocketDriver = class extends BaseDriver {
234
265
  maxRetries: this.maxRetries,
235
266
  url: this.url
236
267
  });
268
+ this.onStatusChange?.("connecting");
237
269
  this.reconnectTimer = window.setTimeout(() => {
238
270
  this.connect();
239
271
  }, delay);
@@ -264,6 +296,7 @@ var WebSocketDriver = class extends BaseDriver {
264
296
  this.ws.close();
265
297
  this.ws = null;
266
298
  }
299
+ this.onStatusChange?.("disconnected");
267
300
  }
268
301
  destroy() {
269
302
  this.close();
@@ -278,7 +311,7 @@ function createEmitter() {
278
311
  }
279
312
 
280
313
  // src/index.ts
281
- var MessageBridge = class {
314
+ var MessageNexus = class {
282
315
  constructor(driver, options) {
283
316
  this.cleanupInterval = null;
284
317
  this.messageQueue = [];
@@ -297,45 +330,45 @@ var MessageBridge = class {
297
330
  this.driver = driver;
298
331
  this.instanceId = options?.instanceId || crypto.randomUUID();
299
332
  this.timeout = options?.timeout ?? 1e4;
300
- this.logger = options?.logger || new Logger("MessageBridge");
301
- this.logger.addHandler(createConsoleHandler());
333
+ if (options?.logger && isLogger(options.logger)) {
334
+ this.logger = options.logger;
335
+ } else {
336
+ this.logger = new Logger("MessageNexus");
337
+ }
338
+ const loggerEnabled = options?.loggerEnabled ?? false;
339
+ if (loggerEnabled) {
340
+ this.logger.enable();
341
+ this.logger.addHandler(createConsoleHandler());
342
+ this.logger.info("MessageNexus initialized", {
343
+ instanceId: this.instanceId,
344
+ timeout: this.timeout
345
+ });
346
+ }
302
347
  this.pendingTasks = /* @__PURE__ */ new Map();
303
- this.incomingMessages = /* @__PURE__ */ new Map();
304
- this.messageHandlers = /* @__PURE__ */ new Set();
348
+ this.invokeHandlers = /* @__PURE__ */ new Map();
349
+ this.notificationHandlers = /* @__PURE__ */ new Map();
305
350
  this.cleanupInterval = null;
306
351
  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
352
  }
320
- async request(typeOrOptions) {
353
+ async invoke(methodOrOptions) {
321
354
  const id = crypto.randomUUID();
322
- let type;
323
- let payload;
355
+ let method;
356
+ let params;
324
357
  let to;
325
358
  let metadata;
326
359
  let timeout;
327
360
  let retryCount = 0;
328
361
  let retryDelay = 1e3;
329
- if (typeof typeOrOptions === "string") {
330
- type = typeOrOptions;
331
- payload = void 0;
362
+ if (typeof methodOrOptions === "string") {
363
+ method = methodOrOptions;
364
+ params = void 0;
332
365
  to = void 0;
333
366
  metadata = {};
334
367
  timeout = this.timeout;
335
368
  } else {
336
- const opts = typeOrOptions;
337
- type = opts.type;
338
- payload = opts.payload;
369
+ const opts = methodOrOptions;
370
+ method = opts.method;
371
+ params = opts.params;
339
372
  to = opts.to;
340
373
  metadata = opts.metadata || {};
341
374
  timeout = opts.timeout ?? this.timeout;
@@ -348,16 +381,20 @@ var MessageBridge = class {
348
381
  this.pendingTasks.delete(id);
349
382
  this.metrics.messagesFailed++;
350
383
  this.metrics.pendingMessages--;
351
- reject(new Error(`Message timeout: ${type} (${id})`));
384
+ reject(new Error(`Message timeout: ${method} (${id})`));
352
385
  }, timeout);
353
386
  this.pendingTasks.set(id, { resolve, reject, timer, timestamp: Date.now() });
387
+ const rpcRequest = {
388
+ jsonrpc: "2.0",
389
+ method,
390
+ params,
391
+ id
392
+ };
354
393
  const message = {
355
- id,
356
- type,
357
- payload,
358
394
  from: this.instanceId,
359
395
  to,
360
- metadata: { ...metadata, timestamp: Date.now() }
396
+ metadata: { ...metadata, timestamp: Date.now() },
397
+ payload: rpcRequest
361
398
  };
362
399
  this._sendMessage(message);
363
400
  }).catch((error) => {
@@ -374,20 +411,26 @@ var MessageBridge = class {
374
411
  return attempt(0);
375
412
  }
376
413
  _sendMessage(message) {
414
+ const payload = message.payload;
415
+ const isRequest = "method" in payload;
416
+ const messageId = "id" in payload ? String(payload.id) : void 0;
417
+ const typeOrMethod = isRequest ? payload.method : "RESPONSE";
377
418
  try {
378
419
  this.driver.send(message);
379
420
  this.metrics.messagesSent++;
380
- this.metrics.pendingMessages++;
381
- this.logger.debug("Message sent", { messageId: message.id, type: message.type });
421
+ if (isRequest && messageId !== void 0) {
422
+ this.metrics.pendingMessages++;
423
+ }
424
+ this.logger.debug("Message sent", { messageId, type: typeOrMethod });
382
425
  } catch (error) {
383
426
  const err = error instanceof Error ? error : new Error(String(error));
384
427
  this.metrics.messagesFailed++;
385
- this.logger.error("Failed to send message", { error: err.message, messageId: message.id });
428
+ this.logger.error("Failed to send message", { error: err.message, messageId });
386
429
  this.errorHandler?.(err, { message });
387
430
  if (this.messageQueue.length < this.maxQueueSize) {
388
431
  this.messageQueue.push(message);
389
432
  this.logger.debug("Message queued", {
390
- messageId: message.id,
433
+ messageId,
391
434
  queueSize: this.messageQueue.length + 1
392
435
  });
393
436
  } else {
@@ -420,55 +463,145 @@ var MessageBridge = class {
420
463
  }
421
464
  }
422
465
  }
466
+ notify(methodOrOptions) {
467
+ let method;
468
+ let params;
469
+ let to;
470
+ let metadata;
471
+ if (typeof methodOrOptions === "string") {
472
+ method = methodOrOptions;
473
+ params = void 0;
474
+ to = void 0;
475
+ metadata = {};
476
+ } else {
477
+ const opts = methodOrOptions;
478
+ method = opts.method;
479
+ params = opts.params;
480
+ to = opts.to;
481
+ metadata = opts.metadata || {};
482
+ }
483
+ const rpcNotification = {
484
+ jsonrpc: "2.0",
485
+ method,
486
+ params
487
+ };
488
+ const message = {
489
+ from: this.instanceId,
490
+ to,
491
+ metadata: { ...metadata, timestamp: Date.now() },
492
+ payload: rpcNotification
493
+ };
494
+ this._sendMessage(message);
495
+ }
423
496
  _validateMessage(data) {
424
497
  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;
498
+ const env = data;
499
+ if (typeof env.from !== "string") return false;
500
+ if (env.to !== void 0 && typeof env.to !== "string") return false;
501
+ if (env.metadata !== void 0 && typeof env.metadata !== "object") return false;
502
+ const payload = env.payload;
503
+ if (!payload || typeof payload !== "object") return false;
504
+ if (payload.jsonrpc !== "2.0") return false;
505
+ const isRequest = "method" in payload;
506
+ const isResponse = "result" in payload || "error" in payload;
507
+ if (!isRequest && !isResponse) return false;
432
508
  return true;
433
509
  }
434
- _handleIncoming(data) {
510
+ async _handleIncoming(data) {
435
511
  if (!this._validateMessage(data)) {
436
512
  this.logger.error("Invalid message format received", { data });
437
513
  this.errorHandler?.(new Error("Invalid message format received"), { data });
438
514
  this.metrics.messagesFailed++;
439
515
  return;
440
516
  }
441
- const { id, to, type, payload, isResponse, error, from } = data;
442
- if (to && to !== this.instanceId) {
517
+ const envelope = data;
518
+ const payload = envelope.payload;
519
+ if (envelope.to && envelope.to !== this.instanceId) {
443
520
  this.logger.debug("Message filtered: not for this instance", {
444
- messageId: id,
445
- to,
521
+ messageId: "id" in payload ? payload.id : void 0,
522
+ to: envelope.to,
446
523
  instanceId: this.instanceId
447
524
  });
448
525
  return;
449
526
  }
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();
527
+ if ("result" in payload || "error" in payload) {
528
+ const response = payload;
529
+ const id = String(response.id);
530
+ if (this.pendingTasks.has(id)) {
531
+ const { resolve, reject, timer, timestamp } = this.pendingTasks.get(id);
532
+ clearTimeout(timer);
533
+ this.pendingTasks.delete(id);
534
+ const latency = Date.now() - timestamp;
535
+ this.metrics.messagesReceived++;
536
+ this.metrics.pendingMessages--;
537
+ this.metrics.totalLatency += latency;
538
+ this.metrics.averageLatency = this.metrics.totalLatency / this.metrics.messagesReceived;
539
+ this.logger.debug("Response received", { messageId: id, latency });
540
+ if (response.error) {
541
+ const err = new Error(response.error.message);
542
+ err.code = response.error.code;
543
+ err.data = response.error.data;
544
+ reject(err);
545
+ } else {
546
+ resolve(response.result);
547
+ }
548
+ this._notifyMetrics();
549
+ } else {
550
+ this.logger.warn("Orphaned response received", { messageId: id });
551
+ }
463
552
  return;
464
553
  }
465
- if (isResponse) {
466
- this.logger.warn("Orphaned response received", { messageId: id });
467
- return;
554
+ if ("method" in payload) {
555
+ if ("id" in payload) {
556
+ const request = payload;
557
+ const id = String(request.id);
558
+ this.logger.debug("Invoke message received", {
559
+ messageId: id,
560
+ type: request.method,
561
+ from: envelope.from
562
+ });
563
+ const context = {
564
+ messageId: id,
565
+ from: envelope.from,
566
+ to: envelope.to,
567
+ metadata: envelope.metadata
568
+ };
569
+ const handler = this.invokeHandlers.get(request.method);
570
+ if (handler) {
571
+ try {
572
+ const result = await handler(request.params, context);
573
+ this._reply(id, envelope.from, result);
574
+ } catch (error) {
575
+ this._replyError(id, envelope.from, error);
576
+ }
577
+ } else {
578
+ const err = new Error(`Method not found: ${request.method}`);
579
+ err.code = -32601;
580
+ this._replyError(id, envelope.from, err);
581
+ }
582
+ } else {
583
+ const notification = payload;
584
+ this.logger.debug("Notification message received", {
585
+ type: notification.method,
586
+ from: envelope.from
587
+ });
588
+ const context = {
589
+ from: envelope.from,
590
+ to: envelope.to,
591
+ metadata: envelope.metadata
592
+ };
593
+ const handlers = this.notificationHandlers.get(notification.method);
594
+ if (handlers) {
595
+ handlers.forEach((handler) => {
596
+ try {
597
+ handler(notification.params, context);
598
+ } catch (error) {
599
+ this.logger.error("Error in notification handler", { error: String(error) });
600
+ }
601
+ });
602
+ }
603
+ }
468
604
  }
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
605
  }
473
606
  getMetrics() {
474
607
  return { ...this.metrics, pendingMessages: this.pendingTasks.size };
@@ -481,30 +614,65 @@ var MessageBridge = class {
481
614
  const metrics = this.getMetrics();
482
615
  this.metricsCallbacks.forEach((callback) => callback(metrics));
483
616
  }
484
- onCommand(handler) {
485
- this.messageHandlers.add(handler);
486
- return () => this.messageHandlers.delete(handler);
617
+ handle(method, handler) {
618
+ if (this.invokeHandlers.has(method)) {
619
+ this.logger.warn(`Overriding existing handler for method: ${method}`);
620
+ }
621
+ this.invokeHandlers.set(method, handler);
622
+ return () => this.invokeHandlers.delete(method);
623
+ }
624
+ removeHandler(method) {
625
+ this.invokeHandlers.delete(method);
487
626
  }
488
- reply(messageId, payload, error) {
489
- const incoming = this.incomingMessages.get(messageId);
490
- if (!incoming) {
491
- throw new Error(`Message not found: ${messageId}`);
627
+ onNotification(method, handler) {
628
+ if (!this.notificationHandlers.has(method)) {
629
+ this.notificationHandlers.set(method, /* @__PURE__ */ new Set());
630
+ }
631
+ this.notificationHandlers.get(method).add(handler);
632
+ return () => this.offNotification(method, handler);
633
+ }
634
+ offNotification(method, handler) {
635
+ const handlers = this.notificationHandlers.get(method);
636
+ if (handlers) {
637
+ handlers.delete(handler);
638
+ if (handlers.size === 0) {
639
+ this.notificationHandlers.delete(method);
640
+ }
492
641
  }
493
- const responsePayload = payload;
494
- const responseError = error;
495
- this.driver.send({
642
+ }
643
+ _reply(messageId, to, payload) {
644
+ const rpcResponse = {
645
+ jsonrpc: "2.0",
496
646
  id: messageId,
497
- type: `${incoming.type}_RESPONSE`,
498
- payload: responsePayload,
499
- error: responseError,
500
- isResponse: true,
647
+ result: payload
648
+ };
649
+ const message = {
501
650
  from: this.instanceId,
502
- to: incoming.from
503
- });
504
- this.incomingMessages.delete(messageId);
651
+ to,
652
+ payload: rpcResponse
653
+ };
654
+ this.driver.send(message);
655
+ }
656
+ _replyError(messageId, to, error) {
657
+ const err = error instanceof Error ? error : new Error(String(error));
658
+ const rpcResponse = {
659
+ jsonrpc: "2.0",
660
+ id: messageId,
661
+ error: {
662
+ code: err.code || -32e3,
663
+ message: err.message,
664
+ data: err.data
665
+ }
666
+ };
667
+ const message = {
668
+ from: this.instanceId,
669
+ to,
670
+ payload: rpcResponse
671
+ };
672
+ this.driver.send(message);
505
673
  }
506
674
  destroy() {
507
- this.logger.info("MessageBridge destroying", {
675
+ this.logger.info("MessageNexus destroying", {
508
676
  instanceId: this.instanceId,
509
677
  pendingMessages: this.pendingTasks.size,
510
678
  queuedMessages: this.messageQueue.length,
@@ -515,7 +683,8 @@ var MessageBridge = class {
515
683
  clearInterval(this.cleanupInterval);
516
684
  this.cleanupInterval = null;
517
685
  }
518
- this.messageHandlers.clear();
686
+ this.invokeHandlers.clear();
687
+ this.notificationHandlers.clear();
519
688
  this.metricsCallbacks.clear();
520
689
  }
521
690
  };
@@ -526,5 +695,5 @@ export {
526
695
  PostMessageDriver,
527
696
  WebSocketDriver,
528
697
  createEmitter,
529
- MessageBridge as default
698
+ MessageNexus as default
530
699
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "message-nexus",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "description": "A unified, type-safe, multi-protocol cross-context message communication library",
6
6
  "main": "./dist/index.js",
@@ -16,6 +16,13 @@
16
16
  "files": [
17
17
  "dist"
18
18
  ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "dev": "tsup --watch",
22
+ "test": "vitest",
23
+ "test:run": "vitest run",
24
+ "type-check": "tsc --noEmit"
25
+ },
19
26
  "keywords": [
20
27
  "message",
21
28
  "nexus",
@@ -43,12 +50,5 @@
43
50
  },
44
51
  "engines": {
45
52
  "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
53
  }
54
- }
54
+ }