pi-harness-runtime 1.1.376 → 1.1.378

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.
Files changed (123) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/mcp-adapter/package.json +1 -1
  51. package/packages/memory-engine/package.json +1 -1
  52. package/packages/milestone-manager/package.json +1 -1
  53. package/packages/model-registry/package.json +1 -1
  54. package/packages/nextjs-plugin/package.json +1 -1
  55. package/packages/notification/package.json +1 -1
  56. package/packages/observability/package.json +1 -1
  57. package/packages/okf-indexer/package.json +1 -1
  58. package/packages/paperclip-integration/package.json +1 -1
  59. package/packages/performance-optimizer/package.json +1 -1
  60. package/packages/permission-mode/package.json +1 -1
  61. package/packages/privilege-broker/package.json +1 -1
  62. package/packages/project-analyzer/package.json +1 -1
  63. package/packages/project-bootstrap/package.json +1 -1
  64. package/packages/projection-engine/package.json +1 -1
  65. package/packages/prompt-compiler/package.json +1 -1
  66. package/packages/prompt-versioning/package.json +1 -1
  67. package/packages/provider-adapter-sdk/package.json +1 -1
  68. package/packages/provider-router/package.json +1 -1
  69. package/packages/provider-selector/package.json +1 -1
  70. package/packages/providers/package.json +1 -1
  71. package/packages/qdrant-skills/package.json +1 -1
  72. package/packages/quota-manager/package.json +1 -1
  73. package/packages/rate-limiter/package.json +1 -1
  74. package/packages/react-vite-plugin/package.json +1 -1
  75. package/packages/relay/dist/client.d.ts +57 -35
  76. package/packages/relay/dist/client.js +284 -71
  77. package/packages/relay/dist/index.d.ts +12 -5
  78. package/packages/relay/dist/index.js +12 -4
  79. package/packages/relay/dist/server.d.ts +58 -20
  80. package/packages/relay/dist/server.js +344 -49
  81. package/packages/relay/dist/types.d.ts +137 -1
  82. package/packages/relay/dist/types.js +21 -2
  83. package/packages/relay/package.json +1 -1
  84. package/packages/relay/src/client.ts +380 -81
  85. package/packages/relay/src/index.ts +42 -5
  86. package/packages/relay/src/server.ts +452 -54
  87. package/packages/relay/src/types.ts +174 -1
  88. package/packages/release-manager/package.json +1 -1
  89. package/packages/requirement-compiler/package.json +1 -1
  90. package/packages/runtime/package.json +1 -1
  91. package/packages/scheduler/package.json +1 -1
  92. package/packages/scheduler-adapter/package.json +1 -1
  93. package/packages/session/package.json +1 -1
  94. package/packages/session-api/package.json +1 -1
  95. package/packages/session-export/package.json +1 -1
  96. package/packages/shared-context/package.json +1 -1
  97. package/packages/skill-mcp-client/package.json +1 -1
  98. package/packages/skill-registry/package.json +1 -1
  99. package/packages/skills/package.json +1 -1
  100. package/packages/sprint-planner/package.json +1 -1
  101. package/packages/subscription-engine/package.json +1 -1
  102. package/packages/task-compiler/package.json +1 -1
  103. package/packages/telegram/package.json +1 -1
  104. package/packages/tencentdb-memory/package.json +1 -1
  105. package/packages/tencentdb-sync/package.json +1 -1
  106. package/packages/test-data-generator/package.json +1 -1
  107. package/packages/test-generator/package.json +1 -1
  108. package/packages/todo-bd-sync/package.json +1 -1
  109. package/packages/token-estimation/package.json +1 -1
  110. package/packages/token-optimizer/package.json +1 -1
  111. package/packages/trajectory/package.json +1 -1
  112. package/packages/tui/package.json +1 -1
  113. package/packages/types/package.json +1 -1
  114. package/packages/voice-api/package.json +1 -1
  115. package/packages/webmcp-tester/package.json +1 -1
  116. package/packages/wiki/package.json +1 -1
  117. package/packages/workflow-events/package.json +1 -1
  118. package/packages/workspace-scanner/package.json +1 -1
  119. package/packages/worktree/package.json +1 -1
  120. package/packages/write-review/package.json +1 -1
  121. package/scripts/relay-client.ts +105 -96
  122. package/scripts/unified-webhook-server.ts +145 -143
  123. package/scripts/ws-server.ts +81 -0
@@ -1,10 +1,47 @@
1
1
  /**
2
- * Cloud Relay - T3 Connect Style
2
+ * Cloud Relay - Two-way Streaming Communication
3
3
  *
4
- * Enables remote access to pi-harness without router/port forwarding.
5
- * Based on T3Code's T3 Connect architecture.
4
+ * Enables real-time two-way communication with pi-harness without router/port forwarding.
5
+ * Based on T3Code's streaming architecture:
6
+ * - Snapshot + Live event pattern
7
+ * - Typed message protocol
8
+ * - Connection state machine
9
+ * - Stream coalescing
6
10
  */
7
11
 
8
- export { RelayServer } from './server.js';
12
+ // Server
13
+ export { RelayServer, createRelayServer } from './server.js';
14
+ export type { RelayServerOptions } from './server.js';
15
+
16
+ // Client
9
17
  export { RelayClient } from './client.js';
10
- export { RelayConfig, RelayTunnel } from './types.js';
18
+ export type { RelayClientCallbacks } from './client.js';
19
+
20
+ // Types
21
+ export {
22
+ PROTOCOL_VERSION,
23
+ generateMessageId,
24
+ createTimestamp,
25
+ } from './types.js';
26
+ export type {
27
+ RelayConfig,
28
+ RelayTunnel,
29
+ ConnectionState,
30
+ ConnectionStateChange,
31
+ BaseMessage,
32
+ SnapshotMessage,
33
+ SynchronizedMessage,
34
+ CallbackPayload,
35
+ CallbackMessage,
36
+ StatusKind,
37
+ StatusMessage,
38
+ PingMessage,
39
+ PongMessage,
40
+ AuthRequestMessage,
41
+ AuthResponseMessage,
42
+ SubscribeMessage,
43
+ ConnectedMessage,
44
+ ServerMessage,
45
+ ClientMessage,
46
+ RelayMessage,
47
+ } from './types.js';
@@ -1,120 +1,518 @@
1
1
  /**
2
- * Relay Server - Cloud relay server for tunnels
2
+ * Relay Server - Two-way streaming WebSocket relay
3
+ *
4
+ * Implements t3code-style streaming with:
5
+ * - Snapshot + Live event pattern
6
+ * - Typed message protocol
7
+ * - Connection state machine
8
+ * - Stream coalescing
3
9
  */
4
10
 
5
- import type { RelayConfig, RelayTunnel, RelayMessage } from './types.js';
11
+ import { createServer, IncomingMessage, ServerResponse, Server } from "node:http";
12
+ import { WebSocketServer, WebSocket } from "ws";
13
+ import { randomUUID } from "node:crypto";
14
+ import {
15
+ type RelayConfig,
16
+ type RelayTunnel,
17
+ type RelayMessage,
18
+ type ConnectionState,
19
+ type ConnectionStateChange,
20
+ type CallbackPayload,
21
+ type ServerMessage,
22
+ type ClientMessage,
23
+ type SnapshotMessage,
24
+ type SynchronizedMessage,
25
+ type CallbackMessage,
26
+ type StatusMessage,
27
+ type AuthResponseMessage,
28
+ type ConnectedMessage,
29
+ type SubscribeMessage,
30
+ PROTOCOL_VERSION,
31
+ generateMessageId,
32
+ createTimestamp,
33
+ } from './types.js';
6
34
 
7
- /**
8
- * Generate a UUID v4
9
- */
10
- function generateUUID(): string {
11
- // Use crypto.randomUUID if available
12
- const crypto = globalThis as { crypto?: { randomUUID?: () => string } };
13
- if (crypto.crypto?.randomUUID) {
14
- return crypto.crypto.randomUUID();
15
- }
16
- // Fallback
17
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c: string) => {
18
- const r = (Math.random() * 16) | 0;
19
- const v = c === 'x' ? r : (r & 0x3) | 0x8;
20
- return v.toString(16);
21
- });
22
- }
35
+ // ============================================================================
36
+ // Constants
37
+ // ============================================================================
38
+
39
+ const DEFAULT_PORT = 8444;
40
+ const DEFAULT_HOST = "0.0.0.0";
41
+ const MAX_RECENT_CALLBACKS = 100;
42
+ const STREAM_BATCH_SIZE = 50; // Batch events for coalescing
43
+ const STREAM_BATCH_TIMEOUT_MS = 100; // Flush batch after this delay
44
+
45
+ // ============================================================================
46
+ // Server Options
47
+ // ============================================================================
23
48
 
24
- /**
25
- * Relay Server options
26
- */
27
49
  export interface RelayServerOptions {
28
50
  /** Port to listen on */
29
51
  port?: number;
30
52
  /** Host to bind */
31
53
  host?: string;
54
+ /** Authentication token */
55
+ authToken?: string;
56
+ /** Server version string */
57
+ serverVersion?: string;
32
58
  /** Called when a tunnel connects */
33
59
  onTunnelConnect?: (tunnel: RelayTunnel) => void;
34
60
  /** Called when a tunnel disconnects */
35
61
  onTunnelDisconnect?: (tunnelId: string) => void;
36
62
  /** Called when a message is received */
37
63
  onMessage?: (tunnelId: string, message: RelayMessage) => void;
64
+ /** Called on state change */
65
+ onStateChange?: (change: ConnectionStateChange) => void;
38
66
  }
39
67
 
40
- /**
41
- * Relay Server
42
- *
43
- * Manages tunnels and relays messages between clients.
44
- */
68
+ // ============================================================================
69
+ // Client Session
70
+ // ============================================================================
71
+
72
+ interface ClientSession {
73
+ id: string;
74
+ ws: WebSocket;
75
+ state: ConnectionState;
76
+ subscriptions: Set<'callback' | 'status' | 'sync'>;
77
+ connectedAt: string;
78
+ lastPing: string;
79
+ pendingSnapshot: boolean; // True if snapshot not yet sent
80
+ }
81
+
82
+ // ============================================================================
83
+ // Stream Event Buffer (for coalescing)
84
+ // ============================================================================
85
+
86
+ interface EventBuffer {
87
+ callbacks: CallbackPayload[];
88
+ timer: ReturnType<typeof setTimeout> | null;
89
+ }
90
+
91
+ function createEventBuffer(): EventBuffer {
92
+ return {
93
+ callbacks: [],
94
+ timer: null,
95
+ };
96
+ }
97
+
98
+ // ============================================================================
99
+ // Relay Server - Two-way Streaming Implementation
100
+ // ============================================================================
101
+
45
102
  export class RelayServer {
103
+ private server: Server | null = null;
104
+ private wss: WebSocketServer | null = null;
105
+ private clients = new Map<string, ClientSession>();
46
106
  private tunnels = new Map<string, RelayTunnel>();
47
107
  private options: Required<RelayServerOptions>;
48
108
  private running = false;
109
+ private serverSessionId: string;
110
+
111
+ // Event stream buffering for coalescing
112
+ private eventBuffer: EventBuffer = createEventBuffer();
113
+
114
+ // Recent callbacks for snapshot
115
+ private recentCallbacks: CallbackPayload[] = [];
49
116
 
50
117
  constructor(options: RelayServerOptions = {}) {
118
+ this.serverSessionId = randomUUID();
51
119
  this.options = {
52
- port: options.port ?? 3001,
53
- host: options.host ?? '0.0.0.0',
120
+ port: options.port ?? DEFAULT_PORT,
121
+ host: options.host ?? DEFAULT_HOST,
122
+ authToken: options.authToken ?? "change-me-in-production",
123
+ serverVersion: options.serverVersion ?? "1.0.0",
54
124
  onTunnelConnect: options.onTunnelConnect ?? (() => {}),
55
125
  onTunnelDisconnect: options.onTunnelDisconnect ?? (() => {}),
56
126
  onMessage: options.onMessage ?? (() => {}),
127
+ onStateChange: options.onStateChange ?? (() => {}),
57
128
  };
58
129
  }
59
130
 
131
+ // ========================================================================
132
+ // Lifecycle
133
+ // ========================================================================
134
+
60
135
  /**
61
- * Start the relay server
136
+ * Start the WebSocket server
62
137
  */
63
138
  async start(): Promise<void> {
64
139
  if (this.running) return;
65
- this.running = true;
66
- // Note: Actual HTTP/WebSocket server would be implemented here
67
- // For now, this is a stub that can be expanded with actual server code
140
+
141
+ this.server = createServer();
142
+ this.wss = new WebSocketServer({ server: this.server });
143
+
144
+ this.wss.on("connection", (ws: WebSocket, req: IncomingMessage) => {
145
+ const clientId = randomUUID();
146
+ const clientIp = req.socket.remoteAddress;
147
+
148
+ console.log(`[RelayServer] Client connecting: ${clientId} from ${clientIp}`);
149
+
150
+ // Create client session
151
+ const session: ClientSession = {
152
+ id: clientId,
153
+ ws,
154
+ state: { state: 'connecting' },
155
+ subscriptions: new Set(['callback', 'status', 'sync']),
156
+ connectedAt: createTimestamp(),
157
+ lastPing: createTimestamp(),
158
+ pendingSnapshot: true,
159
+ };
160
+
161
+ this.clients.set(clientId, session);
162
+ this.updateState(session, { state: 'connecting' });
163
+
164
+ // Handle incoming messages
165
+ ws.on("message", (data: Buffer) => {
166
+ this.handleMessage(clientId, data);
167
+ });
168
+
169
+ ws.on("close", () => {
170
+ this.clients.delete(clientId);
171
+ console.log(`[RelayServer] Client disconnected: ${clientId}`);
172
+ });
173
+
174
+ ws.on("error", (err) => {
175
+ console.error(`[RelayServer] Client error: ${clientId}`, err.message);
176
+ this.clients.delete(clientId);
177
+ });
178
+
179
+ // Send initial connected message
180
+ this.sendMessage(session, {
181
+ version: PROTOCOL_VERSION,
182
+ type: 'connected',
183
+ payload: {
184
+ clientId,
185
+ serverTime: createTimestamp(),
186
+ },
187
+ timestamp: createTimestamp(),
188
+ } as ConnectedMessage);
189
+
190
+ // Auto-auth for now (can be changed to require explicit auth)
191
+ this.updateState(session, { state: 'authenticating' });
192
+ this.handleAuth(clientId, this.options.authToken);
193
+ });
194
+
195
+ return new Promise((resolve, reject) => {
196
+ this.server!.listen(this.options.port, this.options.host, () => {
197
+ this.running = true;
198
+ console.log(`[RelayServer] Server started on ${this.options.host}:${this.options.port}`);
199
+ resolve();
200
+ });
201
+
202
+ this.server!.on("error", (err) => {
203
+ console.error("[RelayServer] Server error:", err);
204
+ reject(err);
205
+ });
206
+ });
68
207
  }
69
208
 
70
209
  /**
71
210
  * Stop the relay server
72
211
  */
73
212
  async stop(): Promise<void> {
74
- this.running = false;
75
- this.tunnels.clear();
213
+ if (!this.running) return;
214
+
215
+ // Close all client connections
216
+ for (const session of this.clients.values()) {
217
+ session.ws.close();
218
+ }
219
+ this.clients.clear();
220
+
221
+ // Close event buffer timer
222
+ if (this.eventBuffer.timer) {
223
+ clearTimeout(this.eventBuffer.timer);
224
+ }
225
+
226
+ return new Promise((resolve) => {
227
+ this.server!.close(() => {
228
+ this.running = false;
229
+ console.log("[RelayServer] Server stopped");
230
+ resolve();
231
+ });
232
+ });
233
+ }
234
+
235
+ // ========================================================================
236
+ // State Management
237
+ // ========================================================================
238
+
239
+ private updateState(session: ClientSession, newState: ConnectionState): void {
240
+ const previous = session.state;
241
+ session.state = newState;
242
+
243
+ this.options.onStateChange({
244
+ previous,
245
+ current: newState,
246
+ timestamp: createTimestamp(),
247
+ });
248
+ }
249
+
250
+ // ========================================================================
251
+ // Message Handling
252
+ // ========================================================================
253
+
254
+ private handleMessage(clientId: string, data: Buffer): void {
255
+ const session = this.clients.get(clientId);
256
+ if (!session) return;
257
+
258
+ try {
259
+ const msg = JSON.parse(data.toString()) as ClientMessage & { type: string };
260
+
261
+ switch (msg.type) {
262
+ case 'auth':
263
+ this.handleAuth(clientId, (msg as { payload: { token: string } }).payload.token);
264
+ break;
265
+
266
+ case 'ping':
267
+ this.handlePing(session);
268
+ break;
269
+
270
+ case 'subscribe':
271
+ this.handleSubscribe(session, msg as SubscribeMessage);
272
+ break;
273
+
274
+ default:
275
+ const _msg = msg as { type: string };
276
+ console.warn(`[RelayServer] Unknown message type: ${_msg?.type}`);
277
+ }
278
+ } catch (err) {
279
+ console.error("[RelayServer] Invalid message:", err);
280
+ this.sendStatus(session, 'error', 'INVALID_MESSAGE', 'Failed to parse message');
281
+ }
282
+ }
283
+
284
+ private handleAuth(clientId: string, token: string): void {
285
+ const session = this.clients.get(clientId);
286
+ if (!session) return;
287
+
288
+ const success = token === this.options.authToken;
289
+ const response: AuthResponseMessage = {
290
+ version: PROTOCOL_VERSION,
291
+ type: 'auth',
292
+ payload: {
293
+ success,
294
+ sessionId: success ? this.serverSessionId : undefined,
295
+ error: success ? undefined : 'Invalid token',
296
+ },
297
+ timestamp: createTimestamp(),
298
+ messageId: generateMessageId(),
299
+ };
300
+
301
+ this.sendMessage(session, response);
302
+
303
+ if (success) {
304
+ this.updateState(session, { state: 'connected', sessionId: this.serverSessionId });
305
+ console.log(`[RelayServer] Client authenticated: ${clientId}`);
306
+
307
+ // Send snapshot + synchronized (t3code pattern)
308
+ this.sendSnapshot(session);
309
+ } else {
310
+ this.updateState(session, { state: 'error', error: 'Authentication failed' });
311
+ session.ws.close();
312
+ }
313
+ }
314
+
315
+ private handlePing(session: ClientSession): void {
316
+ session.lastPing = createTimestamp();
317
+ this.sendMessage(session, {
318
+ version: PROTOCOL_VERSION,
319
+ type: 'pong',
320
+ payload: {},
321
+ timestamp: createTimestamp(),
322
+ });
323
+ }
324
+
325
+ private handleSubscribe(session: ClientSession, msg: SubscribeMessage): void {
326
+ session.subscriptions = new Set(msg.payload.events);
327
+ console.log(`[RelayServer] Client ${session.id} subscribed to: ${Array.from(session.subscriptions).join(', ')}`);
76
328
  }
77
329
 
330
+ // ========================================================================
331
+ // Snapshot + Live Streaming (t3code pattern)
332
+ // ========================================================================
333
+
78
334
  /**
79
- * Get all active tunnels
335
+ * Send initial snapshot followed by synchronized marker
336
+ * This is the t3code pattern: snapshot -> synchronized -> live events
80
337
  */
81
- getTunnels(): RelayTunnel[] {
82
- return Array.from(this.tunnels.values());
338
+ private async sendSnapshot(session: ClientSession): Promise<void> {
339
+ // Build snapshot message
340
+ const snapshot: SnapshotMessage = {
341
+ version: PROTOCOL_VERSION,
342
+ type: 'snapshot',
343
+ payload: {
344
+ sessionId: this.serverSessionId,
345
+ serverVersion: this.options.serverVersion,
346
+ connectedClients: this.clients.size,
347
+ recentCallbacks: this.recentCallbacks.slice(-20), // Last 20 callbacks
348
+ },
349
+ timestamp: createTimestamp(),
350
+ messageId: generateMessageId(),
351
+ };
352
+
353
+ // Send snapshot
354
+ this.sendMessage(session, snapshot);
355
+
356
+ // Send synchronized marker (t3code pattern: indicates snapshot complete)
357
+ const synchronized: SynchronizedMessage = {
358
+ version: PROTOCOL_VERSION,
359
+ type: 'synchronized',
360
+ payload: {},
361
+ timestamp: createTimestamp(),
362
+ messageId: generateMessageId(),
363
+ };
364
+
365
+ this.sendMessage(session, synchronized);
366
+ session.pendingSnapshot = false;
367
+
368
+ console.log(`[RelayServer] Snapshot sent to ${session.id}`);
83
369
  }
84
370
 
85
371
  /**
86
- * Get tunnel by ID
372
+ * Flush event buffer and send batched callbacks
87
373
  */
88
- getTunnel(id: string): RelayTunnel | undefined {
89
- return this.tunnels.get(id);
374
+ private flushEventBuffer(): void {
375
+ if (this.eventBuffer.callbacks.length === 0) return;
376
+
377
+ const callbacks = this.eventBuffer.callbacks;
378
+ this.eventBuffer.callbacks = [];
379
+ this.eventBuffer.timer = null;
380
+
381
+ // Send each callback as a separate live event
382
+ for (const callback of callbacks) {
383
+ this.broadcastCallback(callback, false); // false = don't add to recent
384
+ }
90
385
  }
91
386
 
387
+ // ========================================================================
388
+ // Broadcasting
389
+ // ========================================================================
390
+
92
391
  /**
93
- * Create a tunnel
392
+ * Broadcast callback to all connected clients
393
+ * Uses stream batching for high-frequency events
94
394
  */
95
- createTunnel(localPort: number): RelayTunnel {
96
- const tunnel: RelayTunnel = {
97
- id: generateUUID(),
98
- url: `https://tunnel-${Date.now()}.relay.example.com`,
99
- localPort,
100
- status: 'connecting',
101
- createdAt: new Date().toISOString(),
395
+ broadcastCallback(callback: CallbackPayload, batch = true): void {
396
+ // Add to recent callbacks (circular buffer)
397
+ this.recentCallbacks.push(callback);
398
+ if (this.recentCallbacks.length > MAX_RECENT_CALLBACKS) {
399
+ this.recentCallbacks.shift();
400
+ }
401
+
402
+ const message: CallbackMessage = {
403
+ version: PROTOCOL_VERSION,
404
+ type: 'callback',
405
+ payload: callback,
406
+ timestamp: createTimestamp(),
407
+ messageId: generateMessageId(),
102
408
  };
103
- this.tunnels.set(tunnel.id, tunnel);
104
- return tunnel;
409
+
410
+ if (batch) {
411
+ // Add to buffer for coalescing
412
+ this.eventBuffer.callbacks.push(callback);
413
+
414
+ if (!this.eventBuffer.timer) {
415
+ this.eventBuffer.timer = setTimeout(() => {
416
+ this.flushEventBuffer();
417
+ }, STREAM_BATCH_TIMEOUT_MS);
418
+ }
419
+ } else {
420
+ // Send immediately
421
+ this.broadcast(message);
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Send status to all clients
427
+ */
428
+ broadcastStatus(kind: 'info' | 'warning' | 'error', code: string, message: string): void {
429
+ const msg: StatusMessage = {
430
+ version: PROTOCOL_VERSION,
431
+ type: 'status',
432
+ payload: { kind, code, message },
433
+ timestamp: createTimestamp(),
434
+ messageId: generateMessageId(),
435
+ };
436
+
437
+ this.broadcast(msg);
105
438
  }
106
439
 
107
440
  /**
108
- * Remove a tunnel
441
+ * Broadcast to all subscribed clients
109
442
  */
110
- removeTunnel(id: string): boolean {
111
- return this.tunnels.delete(id);
443
+ private broadcast(message: ServerMessage): void {
444
+ let count = 0;
445
+
446
+ for (const session of this.clients.values()) {
447
+ if (this.isConnected(session)) {
448
+ this.sendMessage(session, message);
449
+ count++;
450
+ }
451
+ }
452
+
453
+ console.log(`[RelayServer] Broadcast ${message.type} to ${count} client(s)`);
454
+ }
455
+
456
+ /**
457
+ * Check if client is connected and authenticated
458
+ */
459
+ private isConnected(session: ClientSession): boolean {
460
+ return session.ws.readyState === WebSocket.OPEN &&
461
+ session.state.state === 'connected';
112
462
  }
113
463
 
114
464
  /**
115
- * Check if server is running
465
+ * Send a typed message to a client
116
466
  */
467
+ private sendMessage(session: ClientSession, message: ServerMessage): void {
468
+ if (session.ws.readyState === WebSocket.OPEN) {
469
+ session.ws.send(JSON.stringify(message));
470
+ }
471
+ }
472
+
473
+ /**
474
+ * Send status to specific client
475
+ */
476
+ private sendStatus(session: ClientSession, kind: 'info' | 'warning' | 'error', code: string, message: string): void {
477
+ const msg: StatusMessage = {
478
+ version: PROTOCOL_VERSION,
479
+ type: 'status',
480
+ payload: { kind, code, message },
481
+ timestamp: createTimestamp(),
482
+ messageId: generateMessageId(),
483
+ };
484
+ this.sendMessage(session, msg);
485
+ }
486
+
487
+ // ========================================================================
488
+ // Public API
489
+ // ========================================================================
490
+
117
491
  isRunning(): boolean {
118
492
  return this.running;
119
493
  }
494
+
495
+ getClientCount(): number {
496
+ return this.clients.size;
497
+ }
498
+
499
+ getClients(): Array<{ id: string; state: ConnectionState; connectedAt: string }> {
500
+ return Array.from(this.clients.values()).map(s => ({
501
+ id: s.id,
502
+ state: s.state,
503
+ connectedAt: s.connectedAt,
504
+ }));
505
+ }
506
+
507
+ getRecentCallbacks(limit = 20): CallbackPayload[] {
508
+ return this.recentCallbacks.slice(-limit);
509
+ }
510
+ }
511
+
512
+ // ============================================================================
513
+ // Export for use in webhook server
514
+ // ============================================================================
515
+
516
+ export function createRelayServer(options?: RelayServerOptions): RelayServer {
517
+ return new RelayServer(options);
120
518
  }