@agentxjs/core 1.9.1-dev
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/package.json +31 -0
- package/src/agent/AgentStateMachine.ts +151 -0
- package/src/agent/README.md +296 -0
- package/src/agent/__tests__/AgentStateMachine.test.ts +346 -0
- package/src/agent/__tests__/createAgent.test.ts +728 -0
- package/src/agent/__tests__/engine/internal/messageAssemblerProcessor.test.ts +567 -0
- package/src/agent/__tests__/engine/internal/stateEventProcessor.test.ts +315 -0
- package/src/agent/__tests__/engine/internal/turnTrackerProcessor.test.ts +340 -0
- package/src/agent/__tests__/engine/mealy/Mealy.test.ts +370 -0
- package/src/agent/__tests__/engine/mealy/Store.test.ts +123 -0
- package/src/agent/__tests__/engine/mealy/combinators.test.ts +322 -0
- package/src/agent/createAgent.ts +467 -0
- package/src/agent/engine/AgentProcessor.ts +106 -0
- package/src/agent/engine/MealyMachine.ts +184 -0
- package/src/agent/engine/internal/index.ts +35 -0
- package/src/agent/engine/internal/messageAssemblerProcessor.ts +550 -0
- package/src/agent/engine/internal/stateEventProcessor.ts +313 -0
- package/src/agent/engine/internal/turnTrackerProcessor.ts +239 -0
- package/src/agent/engine/mealy/Mealy.ts +308 -0
- package/src/agent/engine/mealy/Processor.ts +70 -0
- package/src/agent/engine/mealy/Sink.ts +56 -0
- package/src/agent/engine/mealy/Source.ts +51 -0
- package/src/agent/engine/mealy/Store.ts +98 -0
- package/src/agent/engine/mealy/combinators.ts +176 -0
- package/src/agent/engine/mealy/index.ts +45 -0
- package/src/agent/index.ts +106 -0
- package/src/agent/types/engine.ts +395 -0
- package/src/agent/types/event.ts +478 -0
- package/src/agent/types/index.ts +197 -0
- package/src/agent/types/message.ts +387 -0
- package/src/common/index.ts +8 -0
- package/src/common/logger/ConsoleLogger.ts +137 -0
- package/src/common/logger/LoggerFactoryImpl.ts +123 -0
- package/src/common/logger/index.ts +26 -0
- package/src/common/logger/types.ts +98 -0
- package/src/container/Container.ts +185 -0
- package/src/container/index.ts +44 -0
- package/src/container/types.ts +71 -0
- package/src/driver/index.ts +42 -0
- package/src/driver/types.ts +363 -0
- package/src/event/EventBus.ts +260 -0
- package/src/event/README.md +237 -0
- package/src/event/__tests__/EventBus.test.ts +251 -0
- package/src/event/index.ts +46 -0
- package/src/event/types/agent.ts +512 -0
- package/src/event/types/base.ts +241 -0
- package/src/event/types/bus.ts +429 -0
- package/src/event/types/command.ts +749 -0
- package/src/event/types/container.ts +471 -0
- package/src/event/types/driver.ts +452 -0
- package/src/event/types/index.ts +26 -0
- package/src/event/types/session.ts +314 -0
- package/src/image/Image.ts +203 -0
- package/src/image/index.ts +36 -0
- package/src/image/types.ts +77 -0
- package/src/index.ts +20 -0
- package/src/mq/OffsetGenerator.ts +48 -0
- package/src/mq/README.md +166 -0
- package/src/mq/__tests__/OffsetGenerator.test.ts +121 -0
- package/src/mq/index.ts +18 -0
- package/src/mq/types.ts +172 -0
- package/src/network/RpcClient.ts +455 -0
- package/src/network/index.ts +76 -0
- package/src/network/jsonrpc.ts +336 -0
- package/src/network/protocol.ts +90 -0
- package/src/network/types.ts +284 -0
- package/src/persistence/index.ts +27 -0
- package/src/persistence/types.ts +226 -0
- package/src/runtime/AgentXRuntime.ts +501 -0
- package/src/runtime/index.ts +56 -0
- package/src/runtime/types.ts +236 -0
- package/src/session/Session.ts +71 -0
- package/src/session/index.ts +25 -0
- package/src/session/types.ts +77 -0
- package/src/workspace/index.ts +27 -0
- package/src/workspace/types.ts +131 -0
- package/tsconfig.json +10 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver Types - LLM Communication Layer
|
|
3
|
+
*
|
|
4
|
+
* Driver is the bridge between AgentX and external LLM (Claude, OpenAI, etc.)
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* AgentX
|
|
8
|
+
* │
|
|
9
|
+
* receive() │ AsyncIterable<StreamEvent>
|
|
10
|
+
* ─────────► │ ◄─────────────────────────
|
|
11
|
+
* │
|
|
12
|
+
* ┌───────────────┐
|
|
13
|
+
* │ Driver │
|
|
14
|
+
* │ │
|
|
15
|
+
* │ UserMessage │
|
|
16
|
+
* │ ↓ │
|
|
17
|
+
* │ [SDK call] │
|
|
18
|
+
* │ ↓ │
|
|
19
|
+
* │ StreamEvent │
|
|
20
|
+
* └───────────────┘
|
|
21
|
+
* │
|
|
22
|
+
* ▼
|
|
23
|
+
* External LLM
|
|
24
|
+
* (Claude SDK)
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Key Design:
|
|
28
|
+
* - Driver = single session communication (like Kimi SDK's Session)
|
|
29
|
+
* - Clear input/output boundary (for recording/playback)
|
|
30
|
+
* - Configuration defined by us (capability boundary)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import type { UserMessage } from "../agent/types/message";
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// MCP Server Configuration
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* MCP Server configuration
|
|
41
|
+
*
|
|
42
|
+
* Defines how to launch an MCP server process.
|
|
43
|
+
*/
|
|
44
|
+
export interface McpServerConfig {
|
|
45
|
+
/**
|
|
46
|
+
* Command to run the MCP server
|
|
47
|
+
*/
|
|
48
|
+
command: string;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Command arguments
|
|
52
|
+
*/
|
|
53
|
+
args?: string[];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Environment variables for the process
|
|
57
|
+
*/
|
|
58
|
+
env?: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ============================================================================
|
|
62
|
+
// Stream Event (Lightweight)
|
|
63
|
+
// ============================================================================
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* StopReason - Why the LLM stopped generating
|
|
67
|
+
*/
|
|
68
|
+
export type StopReason =
|
|
69
|
+
| "end_turn"
|
|
70
|
+
| "max_tokens"
|
|
71
|
+
| "tool_use"
|
|
72
|
+
| "stop_sequence"
|
|
73
|
+
| "content_filter"
|
|
74
|
+
| "error"
|
|
75
|
+
| "other";
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* StreamEvent - Lightweight event from Driver
|
|
79
|
+
*
|
|
80
|
+
* Only contains essential fields: type, timestamp, data
|
|
81
|
+
* No source, category, intent, context (those are added by upper layers)
|
|
82
|
+
*/
|
|
83
|
+
export interface StreamEvent<T extends string = string, D = unknown> {
|
|
84
|
+
readonly type: T;
|
|
85
|
+
readonly timestamp: number;
|
|
86
|
+
readonly data: D;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Stream Event Types
|
|
90
|
+
export interface MessageStartEvent extends StreamEvent<
|
|
91
|
+
"message_start",
|
|
92
|
+
{
|
|
93
|
+
messageId: string;
|
|
94
|
+
model: string;
|
|
95
|
+
}
|
|
96
|
+
> {}
|
|
97
|
+
|
|
98
|
+
export interface MessageStopEvent extends StreamEvent<
|
|
99
|
+
"message_stop",
|
|
100
|
+
{
|
|
101
|
+
stopReason: StopReason;
|
|
102
|
+
}
|
|
103
|
+
> {}
|
|
104
|
+
|
|
105
|
+
export interface TextDeltaEvent extends StreamEvent<
|
|
106
|
+
"text_delta",
|
|
107
|
+
{
|
|
108
|
+
text: string;
|
|
109
|
+
}
|
|
110
|
+
> {}
|
|
111
|
+
|
|
112
|
+
export interface ToolUseStartEvent extends StreamEvent<
|
|
113
|
+
"tool_use_start",
|
|
114
|
+
{
|
|
115
|
+
toolCallId: string;
|
|
116
|
+
toolName: string;
|
|
117
|
+
}
|
|
118
|
+
> {}
|
|
119
|
+
|
|
120
|
+
export interface InputJsonDeltaEvent extends StreamEvent<
|
|
121
|
+
"input_json_delta",
|
|
122
|
+
{
|
|
123
|
+
partialJson: string;
|
|
124
|
+
}
|
|
125
|
+
> {}
|
|
126
|
+
|
|
127
|
+
export interface ToolUseStopEvent extends StreamEvent<
|
|
128
|
+
"tool_use_stop",
|
|
129
|
+
{
|
|
130
|
+
toolCallId: string;
|
|
131
|
+
toolName: string;
|
|
132
|
+
input: Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
> {}
|
|
135
|
+
|
|
136
|
+
export interface ToolResultEvent extends StreamEvent<
|
|
137
|
+
"tool_result",
|
|
138
|
+
{
|
|
139
|
+
toolCallId: string;
|
|
140
|
+
result: unknown;
|
|
141
|
+
isError?: boolean;
|
|
142
|
+
}
|
|
143
|
+
> {}
|
|
144
|
+
|
|
145
|
+
export interface ErrorEvent extends StreamEvent<
|
|
146
|
+
"error",
|
|
147
|
+
{
|
|
148
|
+
message: string;
|
|
149
|
+
errorCode?: string;
|
|
150
|
+
}
|
|
151
|
+
> {}
|
|
152
|
+
|
|
153
|
+
export interface InterruptedEvent extends StreamEvent<
|
|
154
|
+
"interrupted",
|
|
155
|
+
{
|
|
156
|
+
reason: "user" | "timeout" | "error";
|
|
157
|
+
}
|
|
158
|
+
> {}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* DriverStreamEvent - Union of all stream events from Driver
|
|
162
|
+
*/
|
|
163
|
+
export type DriverStreamEvent =
|
|
164
|
+
| MessageStartEvent
|
|
165
|
+
| MessageStopEvent
|
|
166
|
+
| TextDeltaEvent
|
|
167
|
+
| ToolUseStartEvent
|
|
168
|
+
| InputJsonDeltaEvent
|
|
169
|
+
| ToolUseStopEvent
|
|
170
|
+
| ToolResultEvent
|
|
171
|
+
| ErrorEvent
|
|
172
|
+
| InterruptedEvent;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* DriverStreamEventType - String literal union of event types
|
|
176
|
+
*/
|
|
177
|
+
export type DriverStreamEventType = DriverStreamEvent["type"];
|
|
178
|
+
|
|
179
|
+
// ============================================================================
|
|
180
|
+
// Driver Configuration
|
|
181
|
+
// ============================================================================
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* DriverConfig - All configuration for creating a Driver
|
|
185
|
+
*
|
|
186
|
+
* This is our capability boundary - we define what we support.
|
|
187
|
+
* Specific implementations (Claude, OpenAI) must work within this.
|
|
188
|
+
*/
|
|
189
|
+
export interface DriverConfig {
|
|
190
|
+
// === Provider Configuration ===
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* API key for authentication
|
|
194
|
+
*/
|
|
195
|
+
apiKey: string;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Base URL for API endpoint (optional, for custom deployments)
|
|
199
|
+
*/
|
|
200
|
+
baseUrl?: string;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Model identifier (e.g., "claude-sonnet-4-20250514")
|
|
204
|
+
*/
|
|
205
|
+
model?: string;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Request timeout in milliseconds (default: 600000 = 10 minutes)
|
|
209
|
+
*/
|
|
210
|
+
timeout?: number;
|
|
211
|
+
|
|
212
|
+
// === Agent Configuration ===
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Agent ID (for identification and logging)
|
|
216
|
+
*/
|
|
217
|
+
agentId: string;
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* System prompt for the agent
|
|
221
|
+
*/
|
|
222
|
+
systemPrompt?: string;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Current working directory for tool execution
|
|
226
|
+
*/
|
|
227
|
+
cwd?: string;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* MCP servers configuration
|
|
231
|
+
*/
|
|
232
|
+
mcpServers?: Record<string, McpServerConfig>;
|
|
233
|
+
|
|
234
|
+
// === Session Configuration ===
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Session ID to resume (for conversation continuity)
|
|
238
|
+
*
|
|
239
|
+
* If provided, Driver will attempt to resume this session.
|
|
240
|
+
* If not provided, a new session is created.
|
|
241
|
+
*/
|
|
242
|
+
resumeSessionId?: string;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Callback when SDK session ID is captured
|
|
246
|
+
*
|
|
247
|
+
* Called once when the session ID becomes available.
|
|
248
|
+
* Save this ID to enable session resume later.
|
|
249
|
+
*/
|
|
250
|
+
onSessionIdCaptured?: (sessionId: string) => void;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// Driver State
|
|
255
|
+
// ============================================================================
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* DriverState - Current state of the Driver
|
|
259
|
+
*
|
|
260
|
+
* - idle: Ready to receive messages
|
|
261
|
+
* - active: Currently processing a message
|
|
262
|
+
* - disposed: Driver has been disposed, cannot be used
|
|
263
|
+
*/
|
|
264
|
+
export type DriverState = "idle" | "active" | "disposed";
|
|
265
|
+
|
|
266
|
+
// ============================================================================
|
|
267
|
+
// Driver Interface
|
|
268
|
+
// ============================================================================
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Driver - LLM Communication Interface
|
|
272
|
+
*
|
|
273
|
+
* Responsible for a single session's communication with LLM.
|
|
274
|
+
* Similar to Kimi SDK's Session concept.
|
|
275
|
+
*
|
|
276
|
+
* Lifecycle:
|
|
277
|
+
* 1. createDriver(config) → Driver instance
|
|
278
|
+
* 2. driver.initialize() → Start SDK, MCP servers
|
|
279
|
+
* 3. driver.receive(message) → Send message, get events
|
|
280
|
+
* 4. driver.dispose() → Cleanup
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* ```typescript
|
|
284
|
+
* const driver = createDriver(config);
|
|
285
|
+
* await driver.initialize();
|
|
286
|
+
*
|
|
287
|
+
* const events = driver.receive(message);
|
|
288
|
+
* for await (const event of events) {
|
|
289
|
+
* if (event.type === "text_delta") {
|
|
290
|
+
* console.log(event.data.text);
|
|
291
|
+
* }
|
|
292
|
+
* }
|
|
293
|
+
*
|
|
294
|
+
* await driver.dispose();
|
|
295
|
+
* ```
|
|
296
|
+
*/
|
|
297
|
+
export interface Driver {
|
|
298
|
+
/**
|
|
299
|
+
* Driver name (for identification and logging)
|
|
300
|
+
*/
|
|
301
|
+
readonly name: string;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* SDK Session ID (available after first message)
|
|
305
|
+
*/
|
|
306
|
+
readonly sessionId: string | null;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Current state
|
|
310
|
+
*/
|
|
311
|
+
readonly state: DriverState;
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Receive a user message and return stream of events
|
|
315
|
+
*
|
|
316
|
+
* @param message - User message to send
|
|
317
|
+
* @returns AsyncIterable of stream events
|
|
318
|
+
*/
|
|
319
|
+
receive(message: UserMessage): AsyncIterable<DriverStreamEvent>;
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Interrupt current operation
|
|
323
|
+
*
|
|
324
|
+
* Stops the current receive() operation gracefully.
|
|
325
|
+
* The AsyncIterable will emit an "interrupted" event and complete.
|
|
326
|
+
*/
|
|
327
|
+
interrupt(): void;
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Initialize the Driver
|
|
331
|
+
*
|
|
332
|
+
* Starts SDK subprocess, MCP servers, etc.
|
|
333
|
+
* Must be called before receive().
|
|
334
|
+
*/
|
|
335
|
+
initialize(): Promise<void>;
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Dispose and cleanup resources
|
|
339
|
+
*
|
|
340
|
+
* Stops SDK subprocess, MCP servers, etc.
|
|
341
|
+
* Driver cannot be used after dispose().
|
|
342
|
+
*/
|
|
343
|
+
dispose(): Promise<void>;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ============================================================================
|
|
347
|
+
// CreateDriver Function Type
|
|
348
|
+
// ============================================================================
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* CreateDriver - Factory function type for creating Driver instances
|
|
352
|
+
*
|
|
353
|
+
* Each implementation package exports a function of this type.
|
|
354
|
+
*
|
|
355
|
+
* @example
|
|
356
|
+
* ```typescript
|
|
357
|
+
* // @agentxjs/claude-driver
|
|
358
|
+
* export const createDriver: CreateDriver = (config) => {
|
|
359
|
+
* return new ClaudeDriverImpl(config);
|
|
360
|
+
* };
|
|
361
|
+
* ```
|
|
362
|
+
*/
|
|
363
|
+
export type CreateDriver = (config: DriverConfig) => Driver;
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EventBus - Central event bus implementation
|
|
3
|
+
*
|
|
4
|
+
* Pub/Sub event bus for runtime communication.
|
|
5
|
+
* Uses RxJS Subject for reactive event distribution.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
EventBus,
|
|
10
|
+
EventProducer,
|
|
11
|
+
EventConsumer,
|
|
12
|
+
Unsubscribe,
|
|
13
|
+
BusEventHandler,
|
|
14
|
+
SubscribeOptions,
|
|
15
|
+
BusEvent,
|
|
16
|
+
CommandEventMap,
|
|
17
|
+
CommandRequestType,
|
|
18
|
+
ResponseEventFor,
|
|
19
|
+
RequestDataFor,
|
|
20
|
+
CommandRequestResponseMap,
|
|
21
|
+
} from "./types";
|
|
22
|
+
import { Subject } from "rxjs";
|
|
23
|
+
import { createLogger } from "commonxjs/logger";
|
|
24
|
+
|
|
25
|
+
const logger = createLogger("event/EventBus");
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generate a unique request ID
|
|
29
|
+
*
|
|
30
|
+
* Uses crypto.randomUUID when available (modern browsers and Node.js 19+),
|
|
31
|
+
* falls back to a timestamp-based ID for older environments.
|
|
32
|
+
*/
|
|
33
|
+
function generateRequestId(): string {
|
|
34
|
+
// Use crypto.randomUUID if available (browsers and modern Node.js)
|
|
35
|
+
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
36
|
+
return `req_${crypto.randomUUID()}`;
|
|
37
|
+
}
|
|
38
|
+
// Fallback for older environments
|
|
39
|
+
return `req_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Internal subscription record
|
|
44
|
+
*/
|
|
45
|
+
interface Subscription {
|
|
46
|
+
id: number;
|
|
47
|
+
type: string | string[] | "*";
|
|
48
|
+
handler: BusEventHandler;
|
|
49
|
+
filter?: (event: BusEvent) => boolean;
|
|
50
|
+
priority: number;
|
|
51
|
+
once: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* EventBusImpl - EventBus implementation using RxJS Subject
|
|
56
|
+
*/
|
|
57
|
+
export class EventBusImpl implements EventBus {
|
|
58
|
+
private readonly subject = new Subject<BusEvent>();
|
|
59
|
+
private subscriptions: Subscription[] = [];
|
|
60
|
+
private nextId = 0;
|
|
61
|
+
private isDestroyed = false;
|
|
62
|
+
|
|
63
|
+
// Cached restricted views
|
|
64
|
+
private producerView: EventProducer | null = null;
|
|
65
|
+
private consumerView: EventConsumer | null = null;
|
|
66
|
+
|
|
67
|
+
constructor() {
|
|
68
|
+
this.subject.subscribe((event) => {
|
|
69
|
+
this.dispatch(event);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
emit(event: BusEvent): void {
|
|
74
|
+
if (this.isDestroyed) return;
|
|
75
|
+
this.subject.next(event);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
emitBatch(events: BusEvent[]): void {
|
|
79
|
+
for (const event of events) {
|
|
80
|
+
this.emit(event);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
on<T extends string>(
|
|
85
|
+
typeOrTypes: T | string[],
|
|
86
|
+
handler: BusEventHandler<BusEvent & { type: T }>,
|
|
87
|
+
options?: SubscribeOptions<BusEvent & { type: T }>
|
|
88
|
+
): Unsubscribe {
|
|
89
|
+
if (this.isDestroyed) return () => {};
|
|
90
|
+
|
|
91
|
+
const subscription: Subscription = {
|
|
92
|
+
id: this.nextId++,
|
|
93
|
+
type: typeOrTypes,
|
|
94
|
+
handler: handler as BusEventHandler,
|
|
95
|
+
filter: options?.filter as ((event: BusEvent) => boolean) | undefined,
|
|
96
|
+
priority: options?.priority ?? 0,
|
|
97
|
+
once: options?.once ?? false,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.subscriptions.push(subscription);
|
|
101
|
+
this.sortByPriority();
|
|
102
|
+
|
|
103
|
+
return () => this.removeSubscription(subscription.id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onAny(handler: BusEventHandler, options?: SubscribeOptions): Unsubscribe {
|
|
107
|
+
if (this.isDestroyed) return () => {};
|
|
108
|
+
|
|
109
|
+
const subscription: Subscription = {
|
|
110
|
+
id: this.nextId++,
|
|
111
|
+
type: "*",
|
|
112
|
+
handler,
|
|
113
|
+
filter: options?.filter as ((event: BusEvent) => boolean) | undefined,
|
|
114
|
+
priority: options?.priority ?? 0,
|
|
115
|
+
once: options?.once ?? false,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
this.subscriptions.push(subscription);
|
|
119
|
+
this.sortByPriority();
|
|
120
|
+
|
|
121
|
+
return () => this.removeSubscription(subscription.id);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
once<T extends string>(type: T, handler: BusEventHandler<BusEvent & { type: T }>): Unsubscribe {
|
|
125
|
+
return this.on(type, handler, { once: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
onCommand<T extends keyof CommandEventMap>(
|
|
129
|
+
type: T,
|
|
130
|
+
handler: (event: CommandEventMap[T]) => void
|
|
131
|
+
): Unsubscribe {
|
|
132
|
+
// Reuse the existing on() implementation with type casting
|
|
133
|
+
return this.on(type as string, handler as BusEventHandler);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
emitCommand<T extends keyof CommandEventMap>(type: T, data: CommandEventMap[T]["data"]): void {
|
|
137
|
+
this.emit({
|
|
138
|
+
type,
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
data,
|
|
141
|
+
source: "command",
|
|
142
|
+
category: (type as string).endsWith("_response") ? "response" : "request",
|
|
143
|
+
intent: (type as string).endsWith("_response") ? "result" : "request",
|
|
144
|
+
} as BusEvent);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
request<T extends CommandRequestType>(
|
|
148
|
+
type: T,
|
|
149
|
+
data: RequestDataFor<T>,
|
|
150
|
+
timeout: number = 30000
|
|
151
|
+
): Promise<ResponseEventFor<T>> {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const requestId = generateRequestId();
|
|
154
|
+
|
|
155
|
+
// Get response type from request type
|
|
156
|
+
const responseType = (type as string).replace(
|
|
157
|
+
"_request",
|
|
158
|
+
"_response"
|
|
159
|
+
) as CommandRequestResponseMap[T];
|
|
160
|
+
|
|
161
|
+
// Set up timeout
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
unsubscribe();
|
|
164
|
+
reject(new Error(`Request timeout: ${type}`));
|
|
165
|
+
}, timeout);
|
|
166
|
+
|
|
167
|
+
// Listen for response
|
|
168
|
+
const unsubscribe = this.onCommand(responseType, (event) => {
|
|
169
|
+
// Match by requestId
|
|
170
|
+
if ((event.data as { requestId: string }).requestId === requestId) {
|
|
171
|
+
clearTimeout(timer);
|
|
172
|
+
unsubscribe();
|
|
173
|
+
resolve(event as ResponseEventFor<T>);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Emit request with generated requestId
|
|
178
|
+
this.emitCommand(type, { ...data, requestId } as CommandEventMap[T]["data"]);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
destroy(): void {
|
|
183
|
+
if (this.isDestroyed) return;
|
|
184
|
+
this.isDestroyed = true;
|
|
185
|
+
this.subscriptions = [];
|
|
186
|
+
this.subject.complete();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private dispatch(event: BusEvent): void {
|
|
190
|
+
const toRemove: number[] = [];
|
|
191
|
+
|
|
192
|
+
for (const sub of this.subscriptions) {
|
|
193
|
+
if (!this.matchesType(sub.type, event.type)) continue;
|
|
194
|
+
if (sub.filter && !sub.filter(event)) continue;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
sub.handler(event);
|
|
198
|
+
} catch (err) {
|
|
199
|
+
logger.error("Event handler error", {
|
|
200
|
+
eventType: event.type,
|
|
201
|
+
subscriptionType: sub.type,
|
|
202
|
+
error: err instanceof Error ? err.message : String(err),
|
|
203
|
+
stack: err instanceof Error ? err.stack : undefined,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (sub.once) {
|
|
208
|
+
toRemove.push(sub.id);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const id of toRemove) {
|
|
213
|
+
this.removeSubscription(id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private matchesType(subscriptionType: string | string[] | "*", eventType: string): boolean {
|
|
218
|
+
if (subscriptionType === "*") return true;
|
|
219
|
+
if (Array.isArray(subscriptionType)) return subscriptionType.includes(eventType);
|
|
220
|
+
return subscriptionType === eventType;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private sortByPriority(): void {
|
|
224
|
+
this.subscriptions.sort((a, b) => b.priority - a.priority);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private removeSubscription(id: number): void {
|
|
228
|
+
this.subscriptions = this.subscriptions.filter((s) => s.id !== id);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get a read-only consumer view (only subscribe methods)
|
|
233
|
+
*/
|
|
234
|
+
asConsumer(): EventConsumer {
|
|
235
|
+
if (!this.consumerView) {
|
|
236
|
+
this.consumerView = {
|
|
237
|
+
on: this.on.bind(this),
|
|
238
|
+
onAny: this.onAny.bind(this),
|
|
239
|
+
once: this.once.bind(this),
|
|
240
|
+
onCommand: this.onCommand.bind(this),
|
|
241
|
+
request: this.request.bind(this),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return this.consumerView;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get a write-only producer view (only emit methods)
|
|
249
|
+
*/
|
|
250
|
+
asProducer(): EventProducer {
|
|
251
|
+
if (!this.producerView) {
|
|
252
|
+
this.producerView = {
|
|
253
|
+
emit: this.emit.bind(this),
|
|
254
|
+
emitBatch: this.emitBatch.bind(this),
|
|
255
|
+
emitCommand: this.emitCommand.bind(this),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return this.producerView;
|
|
259
|
+
}
|
|
260
|
+
}
|