@sparkleideas/swarm 3.0.0-alpha.7
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/MIGRATION.md +472 -0
- package/README.md +634 -0
- package/__tests__/consensus.test.ts +577 -0
- package/__tests__/coordinator.test.ts +501 -0
- package/__tests__/queen-coordinator.test.ts +1335 -0
- package/__tests__/topology.test.ts +621 -0
- package/package.json +32 -0
- package/src/agent-pool.ts +476 -0
- package/src/application/commands/create-task.command.ts +124 -0
- package/src/application/commands/spawn-agent.command.ts +122 -0
- package/src/application/index.ts +30 -0
- package/src/application/services/swarm-application-service.ts +200 -0
- package/src/attention-coordinator.ts +1000 -0
- package/src/consensus/byzantine.ts +431 -0
- package/src/consensus/gossip.ts +513 -0
- package/src/consensus/index.ts +267 -0
- package/src/consensus/raft.ts +443 -0
- package/src/coordination/agent-registry.ts +544 -0
- package/src/coordination/index.ts +23 -0
- package/src/coordination/swarm-hub.ts +776 -0
- package/src/coordination/task-orchestrator.ts +605 -0
- package/src/domain/entities/agent.d.ts +151 -0
- package/src/domain/entities/agent.d.ts.map +1 -0
- package/src/domain/entities/agent.js +280 -0
- package/src/domain/entities/agent.js.map +1 -0
- package/src/domain/entities/agent.ts +370 -0
- package/src/domain/entities/task.d.ts +133 -0
- package/src/domain/entities/task.d.ts.map +1 -0
- package/src/domain/entities/task.js +261 -0
- package/src/domain/entities/task.js.map +1 -0
- package/src/domain/entities/task.ts +319 -0
- package/src/domain/index.ts +41 -0
- package/src/domain/repositories/agent-repository.interface.d.ts +57 -0
- package/src/domain/repositories/agent-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/agent-repository.interface.js +9 -0
- package/src/domain/repositories/agent-repository.interface.js.map +1 -0
- package/src/domain/repositories/agent-repository.interface.ts +69 -0
- package/src/domain/repositories/task-repository.interface.d.ts +61 -0
- package/src/domain/repositories/task-repository.interface.d.ts.map +1 -0
- package/src/domain/repositories/task-repository.interface.js +9 -0
- package/src/domain/repositories/task-repository.interface.js.map +1 -0
- package/src/domain/repositories/task-repository.interface.ts +75 -0
- package/src/domain/services/coordination-service.ts +320 -0
- package/src/federation-hub.d.ts +284 -0
- package/src/federation-hub.d.ts.map +1 -0
- package/src/federation-hub.js +692 -0
- package/src/federation-hub.js.map +1 -0
- package/src/federation-hub.ts +979 -0
- package/src/index.ts +348 -0
- package/src/message-bus.ts +607 -0
- package/src/queen-coordinator.ts +2025 -0
- package/src/shared/events.ts +285 -0
- package/src/shared/types.ts +389 -0
- package/src/topology-manager.ts +656 -0
- package/src/types.ts +545 -0
- package/src/unified-coordinator.ts +1844 -0
- package/src/workers/index.ts +65 -0
- package/src/workers/worker-dispatch.d.ts +234 -0
- package/src/workers/worker-dispatch.d.ts.map +1 -0
- package/src/workers/worker-dispatch.js +842 -0
- package/src/workers/worker-dispatch.js.map +1 -0
- package/src/workers/worker-dispatch.ts +1076 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
- package/vitest.config.ts +20 -0
|
@@ -0,0 +1,607 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 Message Bus
|
|
3
|
+
* High-performance inter-agent communication system
|
|
4
|
+
* Target: 1000+ messages/second throughput
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from 'events';
|
|
8
|
+
import {
|
|
9
|
+
Message,
|
|
10
|
+
MessageAck,
|
|
11
|
+
MessageBusConfig,
|
|
12
|
+
MessageBusStats,
|
|
13
|
+
MessageType,
|
|
14
|
+
IMessageBus,
|
|
15
|
+
SWARM_CONSTANTS,
|
|
16
|
+
} from './types.js';
|
|
17
|
+
|
|
18
|
+
interface MessageQueueEntry {
|
|
19
|
+
message: Message;
|
|
20
|
+
attempts: number;
|
|
21
|
+
enqueuedAt: Date;
|
|
22
|
+
lastAttemptAt?: Date;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// High-Performance Deque Implementation
|
|
27
|
+
// O(1) push/pop from both ends using circular buffer
|
|
28
|
+
// ============================================================================
|
|
29
|
+
|
|
30
|
+
class Deque<T> {
|
|
31
|
+
private buffer: (T | undefined)[];
|
|
32
|
+
private head: number = 0;
|
|
33
|
+
private tail: number = 0;
|
|
34
|
+
private count: number = 0;
|
|
35
|
+
private capacity: number;
|
|
36
|
+
|
|
37
|
+
constructor(initialCapacity: number = 16) {
|
|
38
|
+
this.capacity = initialCapacity;
|
|
39
|
+
this.buffer = new Array(this.capacity);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get length(): number {
|
|
43
|
+
return this.count;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private grow(): void {
|
|
47
|
+
const newCapacity = this.capacity * 2;
|
|
48
|
+
const newBuffer = new Array(newCapacity);
|
|
49
|
+
|
|
50
|
+
// Copy elements in order
|
|
51
|
+
for (let i = 0; i < this.count; i++) {
|
|
52
|
+
newBuffer[i] = this.buffer[(this.head + i) % this.capacity];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.buffer = newBuffer;
|
|
56
|
+
this.head = 0;
|
|
57
|
+
this.tail = this.count;
|
|
58
|
+
this.capacity = newCapacity;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
pushBack(item: T): void {
|
|
62
|
+
if (this.count === this.capacity) {
|
|
63
|
+
this.grow();
|
|
64
|
+
}
|
|
65
|
+
this.buffer[this.tail] = item;
|
|
66
|
+
this.tail = (this.tail + 1) % this.capacity;
|
|
67
|
+
this.count++;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
popFront(): T | undefined {
|
|
71
|
+
if (this.count === 0) {
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
const item = this.buffer[this.head];
|
|
75
|
+
this.buffer[this.head] = undefined; // Help GC
|
|
76
|
+
this.head = (this.head + 1) % this.capacity;
|
|
77
|
+
this.count--;
|
|
78
|
+
return item;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
peekFront(): T | undefined {
|
|
82
|
+
if (this.count === 0) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return this.buffer[this.head];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
clear(): void {
|
|
89
|
+
this.buffer = new Array(this.capacity);
|
|
90
|
+
this.head = 0;
|
|
91
|
+
this.tail = 0;
|
|
92
|
+
this.count = 0;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Find and remove first matching element - O(n) but rarely used
|
|
96
|
+
findAndRemove(predicate: (item: T) => boolean): T | undefined {
|
|
97
|
+
for (let i = 0; i < this.count; i++) {
|
|
98
|
+
const idx = (this.head + i) % this.capacity;
|
|
99
|
+
const item = this.buffer[idx];
|
|
100
|
+
if (item !== undefined && predicate(item)) {
|
|
101
|
+
// Shift remaining elements (O(n) but acceptable for rare operations)
|
|
102
|
+
for (let j = i; j < this.count - 1; j++) {
|
|
103
|
+
const currentIdx = (this.head + j) % this.capacity;
|
|
104
|
+
const nextIdx = (this.head + j + 1) % this.capacity;
|
|
105
|
+
this.buffer[currentIdx] = this.buffer[nextIdx];
|
|
106
|
+
}
|
|
107
|
+
this.tail = (this.tail - 1 + this.capacity) % this.capacity;
|
|
108
|
+
this.buffer[this.tail] = undefined;
|
|
109
|
+
this.count--;
|
|
110
|
+
return item;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
find(predicate: (item: T) => boolean): T | undefined {
|
|
117
|
+
for (let i = 0; i < this.count; i++) {
|
|
118
|
+
const idx = (this.head + i) % this.capacity;
|
|
119
|
+
const item = this.buffer[idx];
|
|
120
|
+
if (item !== undefined && predicate(item)) {
|
|
121
|
+
return item;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
*[Symbol.iterator](): Iterator<T> {
|
|
128
|
+
for (let i = 0; i < this.count; i++) {
|
|
129
|
+
const item = this.buffer[(this.head + i) % this.capacity];
|
|
130
|
+
if (item !== undefined) {
|
|
131
|
+
yield item;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ============================================================================
|
|
138
|
+
// Priority Queue using 4-Level Deques
|
|
139
|
+
// O(1) insert, O(1) dequeue (vs O(n) for sorted array)
|
|
140
|
+
// ============================================================================
|
|
141
|
+
|
|
142
|
+
type Priority = 'urgent' | 'high' | 'normal' | 'low';
|
|
143
|
+
const PRIORITY_ORDER: Priority[] = ['urgent', 'high', 'normal', 'low'];
|
|
144
|
+
|
|
145
|
+
class PriorityMessageQueue {
|
|
146
|
+
private queues: Map<Priority, Deque<MessageQueueEntry>> = new Map();
|
|
147
|
+
private totalCount: number = 0;
|
|
148
|
+
|
|
149
|
+
constructor() {
|
|
150
|
+
for (const priority of PRIORITY_ORDER) {
|
|
151
|
+
this.queues.set(priority, new Deque<MessageQueueEntry>());
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
get length(): number {
|
|
156
|
+
return this.totalCount;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
enqueue(entry: MessageQueueEntry): void {
|
|
160
|
+
const priority = entry.message.priority;
|
|
161
|
+
const queue = this.queues.get(priority)!;
|
|
162
|
+
queue.pushBack(entry);
|
|
163
|
+
this.totalCount++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
dequeue(): MessageQueueEntry | undefined {
|
|
167
|
+
// Dequeue from highest priority non-empty queue
|
|
168
|
+
for (const priority of PRIORITY_ORDER) {
|
|
169
|
+
const queue = this.queues.get(priority)!;
|
|
170
|
+
if (queue.length > 0) {
|
|
171
|
+
this.totalCount--;
|
|
172
|
+
return queue.popFront();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Find and remove first low/normal priority entry for overflow handling
|
|
179
|
+
removeLowestPriority(): MessageQueueEntry | undefined {
|
|
180
|
+
// Check low priority first, then normal
|
|
181
|
+
for (const priority of ['low', 'normal'] as Priority[]) {
|
|
182
|
+
const queue = this.queues.get(priority)!;
|
|
183
|
+
if (queue.length > 0) {
|
|
184
|
+
this.totalCount--;
|
|
185
|
+
return queue.popFront();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Fall back to any queue
|
|
189
|
+
for (const priority of PRIORITY_ORDER) {
|
|
190
|
+
const queue = this.queues.get(priority)!;
|
|
191
|
+
if (queue.length > 0) {
|
|
192
|
+
this.totalCount--;
|
|
193
|
+
return queue.popFront();
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
clear(): void {
|
|
200
|
+
for (const queue of this.queues.values()) {
|
|
201
|
+
queue.clear();
|
|
202
|
+
}
|
|
203
|
+
this.totalCount = 0;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
find(predicate: (entry: MessageQueueEntry) => boolean): MessageQueueEntry | undefined {
|
|
207
|
+
for (const queue of this.queues.values()) {
|
|
208
|
+
const found = queue.find(predicate);
|
|
209
|
+
if (found) {
|
|
210
|
+
return found;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
interface Subscription {
|
|
218
|
+
agentId: string;
|
|
219
|
+
callback: (message: Message) => void;
|
|
220
|
+
filter?: MessageType[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export class MessageBus extends EventEmitter implements IMessageBus {
|
|
224
|
+
private config: MessageBusConfig;
|
|
225
|
+
private queues: Map<string, PriorityMessageQueue> = new Map();
|
|
226
|
+
private subscriptions: Map<string, Subscription> = new Map();
|
|
227
|
+
private pendingAcks: Map<string, { message: Message; timeout: NodeJS.Timeout }> = new Map();
|
|
228
|
+
private processingInterval?: NodeJS.Timeout;
|
|
229
|
+
private statsInterval?: NodeJS.Timeout;
|
|
230
|
+
private messageCounter: number = 0;
|
|
231
|
+
private stats: MessageBusStats;
|
|
232
|
+
private startTime: Date = new Date();
|
|
233
|
+
// Circular buffer for message history (max 60 entries for 60 seconds)
|
|
234
|
+
private messageHistory: { timestamp: number; count: number }[] = [];
|
|
235
|
+
private messageHistoryIndex: number = 0;
|
|
236
|
+
private static readonly MAX_HISTORY_SIZE = 60;
|
|
237
|
+
|
|
238
|
+
constructor(config: Partial<MessageBusConfig> = {}) {
|
|
239
|
+
super();
|
|
240
|
+
this.config = {
|
|
241
|
+
maxQueueSize: config.maxQueueSize ?? SWARM_CONSTANTS.MAX_QUEUE_SIZE,
|
|
242
|
+
processingIntervalMs: config.processingIntervalMs ?? 10,
|
|
243
|
+
ackTimeoutMs: config.ackTimeoutMs ?? 5000,
|
|
244
|
+
retryAttempts: config.retryAttempts ?? SWARM_CONSTANTS.MAX_RETRIES,
|
|
245
|
+
enablePersistence: config.enablePersistence ?? false,
|
|
246
|
+
compressionEnabled: config.compressionEnabled ?? false,
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
this.stats = {
|
|
250
|
+
totalMessages: 0,
|
|
251
|
+
messagesPerSecond: 0,
|
|
252
|
+
avgLatencyMs: 0,
|
|
253
|
+
queueDepth: 0,
|
|
254
|
+
ackRate: 1.0,
|
|
255
|
+
errorRate: 0,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async initialize(config?: MessageBusConfig): Promise<void> {
|
|
260
|
+
if (config) {
|
|
261
|
+
this.config = { ...this.config, ...config };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.startProcessing();
|
|
265
|
+
this.startStatsCollection();
|
|
266
|
+
this.emit('initialized');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async shutdown(): Promise<void> {
|
|
270
|
+
if (this.processingInterval) {
|
|
271
|
+
clearInterval(this.processingInterval);
|
|
272
|
+
this.processingInterval = undefined;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (this.statsInterval) {
|
|
276
|
+
clearInterval(this.statsInterval);
|
|
277
|
+
this.statsInterval = undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Clear all pending acks
|
|
281
|
+
for (const [, pending] of this.pendingAcks) {
|
|
282
|
+
clearTimeout(pending.timeout);
|
|
283
|
+
}
|
|
284
|
+
this.pendingAcks.clear();
|
|
285
|
+
|
|
286
|
+
// Clear all queues
|
|
287
|
+
this.queues.clear();
|
|
288
|
+
this.subscriptions.clear();
|
|
289
|
+
this.messageHistory = [];
|
|
290
|
+
|
|
291
|
+
this.emit('shutdown');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private generateMessageId(): string {
|
|
295
|
+
this.messageCounter++;
|
|
296
|
+
return `msg_${Date.now()}_${this.messageCounter.toString(36)}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async send(message: Omit<Message, 'id' | 'timestamp'>): Promise<string> {
|
|
300
|
+
const fullMessage: Message = {
|
|
301
|
+
...message,
|
|
302
|
+
id: this.generateMessageId(),
|
|
303
|
+
timestamp: new Date(),
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
return this.enqueue(fullMessage);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async broadcast(message: Omit<Message, 'id' | 'timestamp' | 'to'>): Promise<string> {
|
|
310
|
+
const fullMessage: Message = {
|
|
311
|
+
...message,
|
|
312
|
+
id: this.generateMessageId(),
|
|
313
|
+
timestamp: new Date(),
|
|
314
|
+
to: 'broadcast',
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
return this.enqueue(fullMessage);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private async enqueue(message: Message): Promise<string> {
|
|
321
|
+
const startTime = performance.now();
|
|
322
|
+
|
|
323
|
+
if (message.to === 'broadcast') {
|
|
324
|
+
// Broadcast to all subscribed agents
|
|
325
|
+
for (const [agentId, subscription] of this.subscriptions) {
|
|
326
|
+
if (agentId !== message.from) {
|
|
327
|
+
this.addToQueue(agentId, message);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
this.addToQueue(message.to, message);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
this.stats.totalMessages++;
|
|
335
|
+
const latency = performance.now() - startTime;
|
|
336
|
+
this.updateLatencyStats(latency);
|
|
337
|
+
|
|
338
|
+
this.emit('message.enqueued', { messageId: message.id, to: message.to });
|
|
339
|
+
|
|
340
|
+
return message.id;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private addToQueue(agentId: string, message: Message): void {
|
|
344
|
+
if (!this.queues.has(agentId)) {
|
|
345
|
+
this.queues.set(agentId, new PriorityMessageQueue());
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const queue = this.queues.get(agentId)!;
|
|
349
|
+
|
|
350
|
+
// Check queue size limit - O(1) removal of lowest priority
|
|
351
|
+
if (queue.length >= this.config.maxQueueSize) {
|
|
352
|
+
queue.removeLowestPriority();
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// O(1) priority-aware insertion
|
|
356
|
+
const entry: MessageQueueEntry = {
|
|
357
|
+
message,
|
|
358
|
+
attempts: 0,
|
|
359
|
+
enqueuedAt: new Date(),
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
queue.enqueue(entry);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
subscribe(agentId: string, callback: (message: Message) => void, filter?: MessageType[]): void {
|
|
366
|
+
this.subscriptions.set(agentId, {
|
|
367
|
+
agentId,
|
|
368
|
+
callback,
|
|
369
|
+
filter,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// Initialize queue for this agent
|
|
373
|
+
if (!this.queues.has(agentId)) {
|
|
374
|
+
this.queues.set(agentId, new PriorityMessageQueue());
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
this.emit('subscription.added', { agentId });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
unsubscribe(agentId: string): void {
|
|
381
|
+
this.subscriptions.delete(agentId);
|
|
382
|
+
this.queues.delete(agentId);
|
|
383
|
+
this.emit('subscription.removed', { agentId });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async acknowledge(ack: MessageAck): Promise<void> {
|
|
387
|
+
const pending = this.pendingAcks.get(ack.messageId);
|
|
388
|
+
if (!pending) {
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
clearTimeout(pending.timeout);
|
|
393
|
+
this.pendingAcks.delete(ack.messageId);
|
|
394
|
+
|
|
395
|
+
if (!ack.received && ack.error) {
|
|
396
|
+
this.handleAckFailure(pending.message, ack.error);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
this.emit('message.acknowledged', {
|
|
400
|
+
messageId: ack.messageId,
|
|
401
|
+
success: ack.received
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private startProcessing(): void {
|
|
406
|
+
this.processingInterval = setInterval(() => {
|
|
407
|
+
this.processQueues();
|
|
408
|
+
}, this.config.processingIntervalMs);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
private processQueues(): void {
|
|
412
|
+
const now = Date.now();
|
|
413
|
+
|
|
414
|
+
for (const [agentId, queue] of this.queues) {
|
|
415
|
+
const subscription = this.subscriptions.get(agentId);
|
|
416
|
+
if (!subscription) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Process messages in batch for better throughput
|
|
421
|
+
const batchSize = Math.min(10, queue.length);
|
|
422
|
+
const batch: MessageQueueEntry[] = [];
|
|
423
|
+
|
|
424
|
+
for (let i = 0; i < batchSize && queue.length > 0; i++) {
|
|
425
|
+
// O(1) dequeue from highest priority queue
|
|
426
|
+
const entry = queue.dequeue();
|
|
427
|
+
if (!entry) break;
|
|
428
|
+
|
|
429
|
+
// Check TTL
|
|
430
|
+
if (now - entry.message.timestamp.getTime() > entry.message.ttlMs) {
|
|
431
|
+
this.emit('message.expired', { messageId: entry.message.id });
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Check filter
|
|
436
|
+
if (subscription.filter && !subscription.filter.includes(entry.message.type)) {
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
batch.push(entry);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Deliver batch
|
|
444
|
+
for (const entry of batch) {
|
|
445
|
+
this.deliverMessage(subscription, entry);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
private deliverMessage(subscription: Subscription, entry: MessageQueueEntry): void {
|
|
451
|
+
const message = entry.message;
|
|
452
|
+
|
|
453
|
+
try {
|
|
454
|
+
// Set up ack timeout if required
|
|
455
|
+
if (message.requiresAck) {
|
|
456
|
+
const timeout = setTimeout(() => {
|
|
457
|
+
this.handleAckTimeout(message);
|
|
458
|
+
}, this.config.ackTimeoutMs);
|
|
459
|
+
|
|
460
|
+
this.pendingAcks.set(message.id, { message, timeout });
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Deliver asynchronously
|
|
464
|
+
setImmediate(() => {
|
|
465
|
+
try {
|
|
466
|
+
subscription.callback(message);
|
|
467
|
+
this.emit('message.delivered', {
|
|
468
|
+
messageId: message.id,
|
|
469
|
+
to: subscription.agentId
|
|
470
|
+
});
|
|
471
|
+
} catch (error) {
|
|
472
|
+
this.handleDeliveryError(message, entry, error as Error);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
} catch (error) {
|
|
476
|
+
this.handleDeliveryError(message, entry, error as Error);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private handleAckTimeout(message: Message): void {
|
|
481
|
+
this.pendingAcks.delete(message.id);
|
|
482
|
+
this.stats.ackRate = Math.max(0, this.stats.ackRate - 0.01);
|
|
483
|
+
this.emit('message.ack_timeout', { messageId: message.id });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private handleAckFailure(message: Message, error: string): void {
|
|
487
|
+
this.stats.errorRate += 0.01;
|
|
488
|
+
this.emit('message.ack_failed', { messageId: message.id, error });
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
private handleDeliveryError(message: Message, entry: MessageQueueEntry, error: Error): void {
|
|
492
|
+
entry.attempts++;
|
|
493
|
+
entry.lastAttemptAt = new Date();
|
|
494
|
+
|
|
495
|
+
if (entry.attempts < this.config.retryAttempts) {
|
|
496
|
+
// Re-queue for retry
|
|
497
|
+
this.addToQueue(message.to, message);
|
|
498
|
+
this.emit('message.retry', {
|
|
499
|
+
messageId: message.id,
|
|
500
|
+
attempt: entry.attempts
|
|
501
|
+
});
|
|
502
|
+
} else {
|
|
503
|
+
this.stats.errorRate += 0.01;
|
|
504
|
+
this.emit('message.failed', {
|
|
505
|
+
messageId: message.id,
|
|
506
|
+
error: error.message
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private startStatsCollection(): void {
|
|
512
|
+
this.statsInterval = setInterval(() => {
|
|
513
|
+
this.calculateMessagesPerSecond();
|
|
514
|
+
}, 1000);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
private calculateMessagesPerSecond(): void {
|
|
518
|
+
const now = Date.now();
|
|
519
|
+
const entry = { timestamp: now, count: this.stats.totalMessages };
|
|
520
|
+
|
|
521
|
+
// Use circular buffer pattern - O(1) instead of O(n) filter
|
|
522
|
+
if (this.messageHistory.length < MessageBus.MAX_HISTORY_SIZE) {
|
|
523
|
+
this.messageHistory.push(entry);
|
|
524
|
+
} else {
|
|
525
|
+
this.messageHistory[this.messageHistoryIndex] = entry;
|
|
526
|
+
this.messageHistoryIndex = (this.messageHistoryIndex + 1) % MessageBus.MAX_HISTORY_SIZE;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Calculate messages per second from history
|
|
530
|
+
if (this.messageHistory.length >= 2) {
|
|
531
|
+
// Find oldest valid entry (within last 60 seconds)
|
|
532
|
+
let oldest = entry;
|
|
533
|
+
for (const h of this.messageHistory) {
|
|
534
|
+
if (h.timestamp < oldest.timestamp && now - h.timestamp < 60000) {
|
|
535
|
+
oldest = h;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const seconds = (now - oldest.timestamp) / 1000;
|
|
539
|
+
const messages = entry.count - oldest.count;
|
|
540
|
+
this.stats.messagesPerSecond = seconds > 0 ? messages / seconds : 0;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Update queue depth
|
|
544
|
+
this.stats.queueDepth = this.getQueueDepth();
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
private updateLatencyStats(latencyMs: number): void {
|
|
548
|
+
// Exponential moving average
|
|
549
|
+
const alpha = 0.1;
|
|
550
|
+
this.stats.avgLatencyMs = alpha * latencyMs + (1 - alpha) * this.stats.avgLatencyMs;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
getStats(): MessageBusStats {
|
|
554
|
+
return { ...this.stats };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
getQueueDepth(): number {
|
|
558
|
+
let total = 0;
|
|
559
|
+
for (const queue of this.queues.values()) {
|
|
560
|
+
total += queue.length;
|
|
561
|
+
}
|
|
562
|
+
return total;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Direct message retrieval for agents (pull mode)
|
|
566
|
+
getMessages(agentId: string): Message[] {
|
|
567
|
+
const queue = this.queues.get(agentId);
|
|
568
|
+
if (!queue) {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const now = Date.now();
|
|
573
|
+
const messages: Message[] = [];
|
|
574
|
+
|
|
575
|
+
// O(1) per dequeue operation
|
|
576
|
+
while (queue.length > 0) {
|
|
577
|
+
const entry = queue.dequeue();
|
|
578
|
+
if (!entry) break;
|
|
579
|
+
if (now - entry.message.timestamp.getTime() <= entry.message.ttlMs) {
|
|
580
|
+
messages.push(entry.message);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return messages;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// Query pending messages for an agent
|
|
588
|
+
hasPendingMessages(agentId: string): boolean {
|
|
589
|
+
const queue = this.queues.get(agentId);
|
|
590
|
+
return queue !== undefined && queue.length > 0;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Get message by ID
|
|
594
|
+
getMessage(messageId: string): Message | undefined {
|
|
595
|
+
for (const queue of this.queues.values()) {
|
|
596
|
+
const entry = queue.find(e => e.message.id === messageId);
|
|
597
|
+
if (entry) {
|
|
598
|
+
return entry.message;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export function createMessageBus(config?: Partial<MessageBusConfig>): MessageBus {
|
|
606
|
+
return new MessageBus(config);
|
|
607
|
+
}
|