@agentick/gateway 0.0.1
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/LICENSE +21 -0
- package/README.md +477 -0
- package/dist/agent-registry.d.ts +51 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +78 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/app-registry.d.ts +51 -0
- package/dist/app-registry.d.ts.map +1 -0
- package/dist/app-registry.js +78 -0
- package/dist/app-registry.js.map +1 -0
- package/dist/bin.d.ts +8 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +37 -0
- package/dist/bin.js.map +1 -0
- package/dist/gateway.d.ts +165 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +1339 -0
- package/dist/gateway.js.map +1 -0
- package/dist/http-transport.d.ts +65 -0
- package/dist/http-transport.d.ts.map +1 -0
- package/dist/http-transport.js +517 -0
- package/dist/http-transport.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/protocol.d.ts +162 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +16 -0
- package/dist/protocol.js.map +1 -0
- package/dist/session-manager.d.ts +101 -0
- package/dist/session-manager.d.ts.map +1 -0
- package/dist/session-manager.js +208 -0
- package/dist/session-manager.js.map +1 -0
- package/dist/testing.d.ts +92 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +129 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport-protocol.d.ts +162 -0
- package/dist/transport-protocol.d.ts.map +1 -0
- package/dist/transport-protocol.js +16 -0
- package/dist/transport-protocol.js.map +1 -0
- package/dist/transport.d.ts +115 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +56 -0
- package/dist/transport.js.map +1 -0
- package/dist/types.d.ts +314 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +37 -0
- package/dist/types.js.map +1 -0
- package/dist/websocket-server.d.ts +87 -0
- package/dist/websocket-server.d.ts.map +1 -0
- package/dist/websocket-server.js +245 -0
- package/dist/websocket-server.js.map +1 -0
- package/dist/ws-transport.d.ts +17 -0
- package/dist/ws-transport.d.ts.map +1 -0
- package/dist/ws-transport.js +174 -0
- package/dist/ws-transport.js.map +1 -0
- package/package.json +51 -0
- package/src/__tests__/custom-methods.spec.ts +220 -0
- package/src/__tests__/gateway-methods.spec.ts +262 -0
- package/src/__tests__/gateway.spec.ts +404 -0
- package/src/__tests__/guards.spec.ts +235 -0
- package/src/__tests__/protocol.spec.ts +58 -0
- package/src/__tests__/session-manager.spec.ts +220 -0
- package/src/__tests__/ws-transport.spec.ts +246 -0
- package/src/app-registry.ts +103 -0
- package/src/bin.ts +38 -0
- package/src/gateway.ts +1712 -0
- package/src/http-transport.ts +623 -0
- package/src/index.ts +94 -0
- package/src/session-manager.ts +272 -0
- package/src/testing.ts +236 -0
- package/src/transport-protocol.ts +249 -0
- package/src/transport.ts +191 -0
- package/src/types.ts +392 -0
- package/src/websocket-server.ts +303 -0
- package/src/ws-transport.ts +205 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages persistent sessions across apps.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Session } from "@agentick/core";
|
|
8
|
+
import { devToolsEmitter, type DTGatewaySessionEvent } from "@agentick/shared";
|
|
9
|
+
import type { AppRegistry, AppInfo } from "./app-registry.js";
|
|
10
|
+
import type { SessionState } from "./types.js";
|
|
11
|
+
import { parseSessionKey, formatSessionKey } from "./transport-protocol.js";
|
|
12
|
+
|
|
13
|
+
interface ManagedSession {
|
|
14
|
+
state: SessionState;
|
|
15
|
+
coreSession: Session | null;
|
|
16
|
+
appInfo: AppInfo;
|
|
17
|
+
/** The session name without app prefix - used when creating App sessions */
|
|
18
|
+
sessionName: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* SessionManager configuration
|
|
23
|
+
*/
|
|
24
|
+
export interface SessionManagerConfig {
|
|
25
|
+
/** Gateway ID for DevTools events */
|
|
26
|
+
gatewayId: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class SessionManager {
|
|
30
|
+
private sessions = new Map<string, ManagedSession>();
|
|
31
|
+
private registry: AppRegistry;
|
|
32
|
+
private gatewayId: string;
|
|
33
|
+
private devToolsSequence = 0;
|
|
34
|
+
|
|
35
|
+
constructor(registry: AppRegistry, config?: SessionManagerConfig) {
|
|
36
|
+
this.registry = registry;
|
|
37
|
+
this.gatewayId = config?.gatewayId ?? "gateway";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Normalize a session key to the canonical format (appId:sessionName).
|
|
42
|
+
* This ensures consistent lookups regardless of input format.
|
|
43
|
+
*/
|
|
44
|
+
private normalizeKey(sessionKey: string): string {
|
|
45
|
+
const { appId, sessionName } = parseSessionKey(sessionKey, this.registry.defaultId);
|
|
46
|
+
return formatSessionKey({ appId, sessionName });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Emit a DevTools session event
|
|
51
|
+
*/
|
|
52
|
+
private emitDevToolsEvent(
|
|
53
|
+
action: DTGatewaySessionEvent["action"],
|
|
54
|
+
sessionId: string,
|
|
55
|
+
appId: string,
|
|
56
|
+
messageCount?: number,
|
|
57
|
+
clientId?: string,
|
|
58
|
+
): void {
|
|
59
|
+
if (!devToolsEmitter.hasSubscribers()) return;
|
|
60
|
+
|
|
61
|
+
devToolsEmitter.emitEvent({
|
|
62
|
+
type: "gateway_session",
|
|
63
|
+
executionId: this.gatewayId,
|
|
64
|
+
action,
|
|
65
|
+
sessionId,
|
|
66
|
+
appId,
|
|
67
|
+
messageCount,
|
|
68
|
+
clientId,
|
|
69
|
+
sequence: this.devToolsSequence++,
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
} as DTGatewaySessionEvent);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get or create a session
|
|
76
|
+
*/
|
|
77
|
+
async getOrCreate(sessionKey: string, clientId?: string): Promise<ManagedSession> {
|
|
78
|
+
const normalizedKey = this.normalizeKey(sessionKey);
|
|
79
|
+
|
|
80
|
+
// Check if session exists
|
|
81
|
+
let session = this.sessions.get(normalizedKey);
|
|
82
|
+
if (session) {
|
|
83
|
+
session.state.lastActivityAt = new Date();
|
|
84
|
+
return session;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse session key to get app and name
|
|
88
|
+
const { appId, sessionName } = parseSessionKey(sessionKey, this.registry.defaultId);
|
|
89
|
+
|
|
90
|
+
// Get the app
|
|
91
|
+
const appInfo = this.registry.resolve(appId);
|
|
92
|
+
|
|
93
|
+
// Create session state
|
|
94
|
+
const state: SessionState = {
|
|
95
|
+
id: formatSessionKey({ appId, sessionName }),
|
|
96
|
+
appId,
|
|
97
|
+
createdAt: new Date(),
|
|
98
|
+
lastActivityAt: new Date(),
|
|
99
|
+
messageCount: 0,
|
|
100
|
+
isActive: false,
|
|
101
|
+
subscribers: new Set(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// Create the managed session
|
|
105
|
+
// Note: sessionName is the name WITHOUT app prefix - used when creating App sessions
|
|
106
|
+
session = {
|
|
107
|
+
state,
|
|
108
|
+
coreSession: null,
|
|
109
|
+
appInfo,
|
|
110
|
+
sessionName,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
this.sessions.set(state.id, session);
|
|
114
|
+
|
|
115
|
+
// Emit DevTools event for session creation
|
|
116
|
+
this.emitDevToolsEvent("created", state.id, appId, 0, clientId);
|
|
117
|
+
|
|
118
|
+
return session;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get an existing session
|
|
123
|
+
*/
|
|
124
|
+
get(sessionKey: string): ManagedSession | undefined {
|
|
125
|
+
return this.sessions.get(this.normalizeKey(sessionKey));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if a session exists
|
|
130
|
+
*/
|
|
131
|
+
has(sessionKey: string): boolean {
|
|
132
|
+
return this.sessions.has(this.normalizeKey(sessionKey));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Close a session
|
|
137
|
+
*/
|
|
138
|
+
async close(sessionKey: string): Promise<void> {
|
|
139
|
+
const normalizedKey = this.normalizeKey(sessionKey);
|
|
140
|
+
const session = this.sessions.get(normalizedKey);
|
|
141
|
+
if (!session) return;
|
|
142
|
+
|
|
143
|
+
const { id, appId, messageCount } = session.state;
|
|
144
|
+
|
|
145
|
+
// Clean up session if active
|
|
146
|
+
if (session.coreSession) {
|
|
147
|
+
session.coreSession.close();
|
|
148
|
+
session.coreSession = null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
this.sessions.delete(normalizedKey);
|
|
152
|
+
|
|
153
|
+
// Emit DevTools event for session closure
|
|
154
|
+
this.emitDevToolsEvent("closed", id, appId, messageCount);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Reset a session (clear history but keep session)
|
|
159
|
+
*/
|
|
160
|
+
async reset(sessionKey: string): Promise<void> {
|
|
161
|
+
const normalizedKey = this.normalizeKey(sessionKey);
|
|
162
|
+
const session = this.sessions.get(normalizedKey);
|
|
163
|
+
if (!session) return;
|
|
164
|
+
|
|
165
|
+
const { id, appId, messageCount } = session.state;
|
|
166
|
+
|
|
167
|
+
// Reset session state
|
|
168
|
+
session.state.messageCount = 0;
|
|
169
|
+
session.state.lastActivityAt = new Date();
|
|
170
|
+
if (session.coreSession) {
|
|
171
|
+
session.coreSession.close();
|
|
172
|
+
session.coreSession = null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Emit DevTools event for session reset (treated as closed + recreated)
|
|
176
|
+
this.emitDevToolsEvent("closed", id, appId, messageCount);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Get all session IDs
|
|
181
|
+
*/
|
|
182
|
+
ids(): string[] {
|
|
183
|
+
return Array.from(this.sessions.keys());
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Get all sessions
|
|
188
|
+
*/
|
|
189
|
+
all(): ManagedSession[] {
|
|
190
|
+
return Array.from(this.sessions.values());
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get sessions for a specific app
|
|
195
|
+
*/
|
|
196
|
+
forApp(appId: string): ManagedSession[] {
|
|
197
|
+
return this.all().filter((s) => s.state.appId === appId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get session count
|
|
202
|
+
*/
|
|
203
|
+
get size(): number {
|
|
204
|
+
return this.sessions.size;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Add a subscriber to a session.
|
|
209
|
+
* Creates the session if it doesn't exist (ensures subscription is never lost).
|
|
210
|
+
*/
|
|
211
|
+
async subscribe(sessionKey: string, clientId: string): Promise<void> {
|
|
212
|
+
const session = await this.getOrCreate(sessionKey, clientId);
|
|
213
|
+
session.state.subscribers.add(clientId);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Remove a subscriber from a session
|
|
218
|
+
*/
|
|
219
|
+
unsubscribe(sessionKey: string, clientId: string): void {
|
|
220
|
+
const session = this.sessions.get(this.normalizeKey(sessionKey));
|
|
221
|
+
if (session) {
|
|
222
|
+
session.state.subscribers.delete(clientId);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Remove a client from all subscriptions
|
|
228
|
+
*/
|
|
229
|
+
unsubscribeAll(clientId: string): void {
|
|
230
|
+
for (const session of this.sessions.values()) {
|
|
231
|
+
session.state.subscribers.delete(clientId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get subscribers for a session
|
|
237
|
+
*/
|
|
238
|
+
getSubscribers(sessionKey: string): Set<string> {
|
|
239
|
+
const session = this.sessions.get(this.normalizeKey(sessionKey));
|
|
240
|
+
return session?.state.subscribers ?? new Set();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Update message count for a session
|
|
245
|
+
*/
|
|
246
|
+
incrementMessageCount(sessionKey: string, clientId?: string): void {
|
|
247
|
+
const session = this.sessions.get(this.normalizeKey(sessionKey));
|
|
248
|
+
if (session) {
|
|
249
|
+
session.state.messageCount++;
|
|
250
|
+
session.state.lastActivityAt = new Date();
|
|
251
|
+
|
|
252
|
+
// Emit DevTools event for session message
|
|
253
|
+
this.emitDevToolsEvent(
|
|
254
|
+
"message",
|
|
255
|
+
session.state.id,
|
|
256
|
+
session.state.appId,
|
|
257
|
+
session.state.messageCount,
|
|
258
|
+
clientId,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Set session active state
|
|
265
|
+
*/
|
|
266
|
+
setActive(sessionKey: string, isActive: boolean): void {
|
|
267
|
+
const session = this.sessions.get(this.normalizeKey(sessionKey));
|
|
268
|
+
if (session) {
|
|
269
|
+
session.state.isActive = isActive;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
package/src/testing.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gateway Testing Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides helpers for testing gateway interactions.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { createTestGateway, createMockApp } from '@agentick/gateway/testing';
|
|
9
|
+
*
|
|
10
|
+
* test('gateway handles messages', async () => {
|
|
11
|
+
* const mockApp = createMockApp({
|
|
12
|
+
* response: 'Hello!',
|
|
13
|
+
* });
|
|
14
|
+
*
|
|
15
|
+
* const { gateway, client, cleanup } = await createTestGateway({
|
|
16
|
+
* agents: { chat: mockApp },
|
|
17
|
+
* defaultAgent: 'chat',
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* try {
|
|
21
|
+
* const response = await client.send('main', 'Hi there');
|
|
22
|
+
* expect(response.payload.messageId).toBeDefined();
|
|
23
|
+
* } finally {
|
|
24
|
+
* await cleanup();
|
|
25
|
+
* }
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module @agentick/gateway/testing
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import WebSocket from "ws";
|
|
33
|
+
import type { Gateway } from "./gateway.js";
|
|
34
|
+
import { createGateway } from "./gateway.js";
|
|
35
|
+
import type { GatewayConfig, GatewayEvents } from "./types.js";
|
|
36
|
+
|
|
37
|
+
// Re-export mock factories from core
|
|
38
|
+
export {
|
|
39
|
+
createMockApp,
|
|
40
|
+
createMockSession,
|
|
41
|
+
createMockExecutionHandle,
|
|
42
|
+
createTestProcedure,
|
|
43
|
+
type MockAppOptions,
|
|
44
|
+
type MockSessionOptions,
|
|
45
|
+
type MockSession,
|
|
46
|
+
type MockApp,
|
|
47
|
+
type MockSessionExecutionHandle,
|
|
48
|
+
type MockExecutionHandleOptions,
|
|
49
|
+
type TestProcedure,
|
|
50
|
+
type TestProcedureOptions,
|
|
51
|
+
} from "@agentick/core/testing";
|
|
52
|
+
|
|
53
|
+
// ============================================================================
|
|
54
|
+
// Test Gateway Factory
|
|
55
|
+
// ============================================================================
|
|
56
|
+
|
|
57
|
+
export interface TestGatewayOptions extends Omit<GatewayConfig, "port" | "host"> {
|
|
58
|
+
/** Custom port (default: random available port) */
|
|
59
|
+
port?: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TestGatewayClient {
|
|
63
|
+
/** Send a request to the gateway */
|
|
64
|
+
request<T = unknown>(
|
|
65
|
+
method: string,
|
|
66
|
+
params?: Record<string, unknown>,
|
|
67
|
+
): Promise<{
|
|
68
|
+
ok: boolean;
|
|
69
|
+
payload?: T;
|
|
70
|
+
error?: { code: string; message: string };
|
|
71
|
+
}>;
|
|
72
|
+
|
|
73
|
+
/** Send a message to a session */
|
|
74
|
+
send(
|
|
75
|
+
sessionId: string,
|
|
76
|
+
message: string,
|
|
77
|
+
): Promise<{
|
|
78
|
+
ok: boolean;
|
|
79
|
+
payload?: { messageId: string };
|
|
80
|
+
error?: { code: string; message: string };
|
|
81
|
+
}>;
|
|
82
|
+
|
|
83
|
+
/** Collect events for a session */
|
|
84
|
+
collectEvents(
|
|
85
|
+
sessionId: string,
|
|
86
|
+
timeout?: number,
|
|
87
|
+
): Promise<Array<{ type: string; data: unknown }>>;
|
|
88
|
+
|
|
89
|
+
/** Close the client connection */
|
|
90
|
+
close(): void;
|
|
91
|
+
|
|
92
|
+
/** The raw WebSocket */
|
|
93
|
+
ws: WebSocket;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface TestGatewayResult {
|
|
97
|
+
/** The gateway instance */
|
|
98
|
+
gateway: Gateway;
|
|
99
|
+
|
|
100
|
+
/** A connected test client */
|
|
101
|
+
client: TestGatewayClient;
|
|
102
|
+
|
|
103
|
+
/** Gateway URL */
|
|
104
|
+
url: string;
|
|
105
|
+
|
|
106
|
+
/** Port the gateway is running on */
|
|
107
|
+
port: number;
|
|
108
|
+
|
|
109
|
+
/** Clean up resources */
|
|
110
|
+
cleanup: () => Promise<void>;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Create a test gateway with a connected client.
|
|
115
|
+
*
|
|
116
|
+
* Automatically handles port allocation, client connection, and cleanup.
|
|
117
|
+
*/
|
|
118
|
+
export async function createTestGateway(options: TestGatewayOptions): Promise<TestGatewayResult> {
|
|
119
|
+
// Use random high port to avoid conflicts
|
|
120
|
+
const port = options.port ?? 19000 + Math.floor(Math.random() * 1000);
|
|
121
|
+
const host = "127.0.0.1";
|
|
122
|
+
const url = `ws://${host}:${port}`;
|
|
123
|
+
|
|
124
|
+
const gateway = createGateway({
|
|
125
|
+
...options,
|
|
126
|
+
port,
|
|
127
|
+
host,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await gateway.start();
|
|
131
|
+
|
|
132
|
+
// Create and connect client
|
|
133
|
+
const ws = new WebSocket(url);
|
|
134
|
+
await new Promise<void>((resolve, reject) => {
|
|
135
|
+
ws.on("open", () => resolve());
|
|
136
|
+
ws.on("error", reject);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// Authenticate
|
|
140
|
+
ws.send(JSON.stringify({ type: "connect", clientId: "test-client" }));
|
|
141
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
142
|
+
|
|
143
|
+
let requestId = 0;
|
|
144
|
+
const pendingRequests = new Map<
|
|
145
|
+
string,
|
|
146
|
+
{
|
|
147
|
+
resolve: (value: any) => void;
|
|
148
|
+
reject: (error: Error) => void;
|
|
149
|
+
}
|
|
150
|
+
>();
|
|
151
|
+
|
|
152
|
+
ws.on("message", (data) => {
|
|
153
|
+
const msg = JSON.parse(data.toString());
|
|
154
|
+
if (msg.type === "res" && pendingRequests.has(msg.id)) {
|
|
155
|
+
const { resolve } = pendingRequests.get(msg.id)!;
|
|
156
|
+
pendingRequests.delete(msg.id);
|
|
157
|
+
resolve({ ok: msg.ok, payload: msg.payload, error: msg.error });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const client: TestGatewayClient = {
|
|
162
|
+
ws,
|
|
163
|
+
|
|
164
|
+
async request(method, params = {}) {
|
|
165
|
+
const id = `req-${++requestId}`;
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
pendingRequests.set(id, { resolve, reject });
|
|
168
|
+
ws.send(JSON.stringify({ type: "req", id, method, params }));
|
|
169
|
+
|
|
170
|
+
// Timeout after 5s
|
|
171
|
+
setTimeout(() => {
|
|
172
|
+
if (pendingRequests.has(id)) {
|
|
173
|
+
pendingRequests.delete(id);
|
|
174
|
+
reject(new Error(`Request ${method} timed out`));
|
|
175
|
+
}
|
|
176
|
+
}, 5000);
|
|
177
|
+
});
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
async send(sessionId, message) {
|
|
181
|
+
return this.request("send", { sessionId, message });
|
|
182
|
+
},
|
|
183
|
+
|
|
184
|
+
async collectEvents(sessionId, timeout = 1000) {
|
|
185
|
+
const events: Array<{ type: string; data: unknown }> = [];
|
|
186
|
+
|
|
187
|
+
const handler = (data: WebSocket.Data) => {
|
|
188
|
+
const msg = JSON.parse(data.toString());
|
|
189
|
+
if (msg.type === "event" && msg.sessionId === sessionId) {
|
|
190
|
+
events.push({ type: msg.event, data: msg.data });
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
ws.on("message", handler);
|
|
195
|
+
await new Promise((r) => setTimeout(r, timeout));
|
|
196
|
+
ws.off("message", handler);
|
|
197
|
+
|
|
198
|
+
return events;
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
close() {
|
|
202
|
+
ws.close();
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const cleanup = async () => {
|
|
207
|
+
client.close();
|
|
208
|
+
await gateway.stop();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return { gateway, client, url, port, cleanup };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ============================================================================
|
|
215
|
+
// Event Helpers
|
|
216
|
+
// ============================================================================
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Wait for a specific gateway event.
|
|
220
|
+
*/
|
|
221
|
+
export function waitForGatewayEvent<K extends keyof GatewayEvents>(
|
|
222
|
+
gateway: Gateway,
|
|
223
|
+
event: K,
|
|
224
|
+
timeout = 5000,
|
|
225
|
+
): Promise<GatewayEvents[K]> {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
const timer = setTimeout(() => {
|
|
228
|
+
reject(new Error(`Timeout waiting for gateway event: ${event}`));
|
|
229
|
+
}, timeout);
|
|
230
|
+
|
|
231
|
+
gateway.on(event, (payload) => {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
resolve(payload);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
}
|