pi-harness-runtime 1.1.376 → 1.1.377
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 +1 -1
- package/packages/a2a-adapter/package.json +1 -1
- package/packages/agent-policy/package.json +1 -1
- package/packages/architecture-generator/package.json +1 -1
- package/packages/auth/package.json +1 -1
- package/packages/autonomous-refactor/package.json +1 -1
- package/packages/autonomous-runtime/package.json +1 -1
- package/packages/cache-strategy/package.json +1 -1
- package/packages/capability-registry/package.json +1 -1
- package/packages/checkpoint/package.json +1 -1
- package/packages/cli-plugin-sdk/package.json +1 -1
- package/packages/clipboard/package.json +1 -1
- package/packages/clipboard-plugin/package.json +1 -1
- package/packages/code-generation/package.json +1 -1
- package/packages/code-review/package.json +1 -1
- package/packages/codex-adapter/package.json +1 -1
- package/packages/codex-watcher/package.json +1 -1
- package/packages/config-capture/package.json +1 -1
- package/packages/connection-runtime/package.json +1 -1
- package/packages/context-compiler/package.json +1 -1
- package/packages/context-discovery/package.json +1 -1
- package/packages/context-manager/package.json +1 -1
- package/packages/contracts/package.json +1 -1
- package/packages/cookie-sanitizer/package.json +1 -1
- package/packages/cost-optimizer/package.json +1 -1
- package/packages/dependency-analyzer/package.json +1 -1
- package/packages/django-plugin/package.json +1 -1
- package/packages/doc-generator/package.json +1 -1
- package/packages/durable-review/package.json +1 -1
- package/packages/e2e-tester/package.json +1 -1
- package/packages/evaluation-engine/package.json +1 -1
- package/packages/evaluation-runner/package.json +1 -1
- package/packages/event-bus/package.json +1 -1
- package/packages/event-sink/package.json +1 -1
- package/packages/event-store/package.json +1 -1
- package/packages/experience-replay/package.json +1 -1
- package/packages/feedback-collector/package.json +1 -1
- package/packages/file-copy-helper/package.json +1 -1
- package/packages/framework-detector/package.json +1 -1
- package/packages/framework-plugin-sdk/package.json +1 -1
- package/packages/frappe-plugin/package.json +1 -1
- package/packages/generic-web-plugin/package.json +1 -1
- package/packages/health-monitor/package.json +1 -1
- package/packages/intent-analyzer/package.json +1 -1
- package/packages/jev-judge/package.json +1 -1
- package/packages/knowledge-graph/package.json +1 -1
- package/packages/knowledge-retrieval/package.json +1 -1
- package/packages/laravel-plugin/package.json +1 -1
- package/packages/learning-engine/package.json +1 -1
- package/packages/mcp-adapter/package.json +1 -1
- package/packages/memory-engine/package.json +1 -1
- package/packages/milestone-manager/package.json +1 -1
- package/packages/model-registry/package.json +1 -1
- package/packages/nextjs-plugin/package.json +1 -1
- package/packages/notification/package.json +1 -1
- package/packages/observability/package.json +1 -1
- package/packages/okf-indexer/package.json +1 -1
- package/packages/paperclip-integration/package.json +1 -1
- package/packages/performance-optimizer/package.json +1 -1
- package/packages/permission-mode/package.json +1 -1
- package/packages/privilege-broker/package.json +1 -1
- package/packages/project-analyzer/package.json +1 -1
- package/packages/project-bootstrap/package.json +1 -1
- package/packages/projection-engine/package.json +1 -1
- package/packages/prompt-compiler/package.json +1 -1
- package/packages/prompt-versioning/package.json +1 -1
- package/packages/provider-adapter-sdk/package.json +1 -1
- package/packages/provider-router/package.json +1 -1
- package/packages/provider-selector/package.json +1 -1
- package/packages/providers/package.json +1 -1
- package/packages/qdrant-skills/package.json +1 -1
- package/packages/quota-manager/package.json +1 -1
- package/packages/rate-limiter/package.json +1 -1
- package/packages/react-vite-plugin/package.json +1 -1
- package/packages/relay/dist/client.d.ts +56 -35
- package/packages/relay/dist/client.js +278 -71
- package/packages/relay/dist/index.d.ts +12 -5
- package/packages/relay/dist/index.js +12 -4
- package/packages/relay/dist/server.d.ts +58 -20
- package/packages/relay/dist/server.js +344 -49
- package/packages/relay/dist/types.d.ts +137 -1
- package/packages/relay/dist/types.js +21 -2
- package/packages/relay/package.json +1 -1
- package/packages/relay/src/client.ts +375 -81
- package/packages/relay/src/index.ts +42 -5
- package/packages/relay/src/server.ts +452 -54
- package/packages/relay/src/types.ts +174 -1
- package/packages/release-manager/package.json +1 -1
- package/packages/requirement-compiler/package.json +1 -1
- package/packages/runtime/package.json +1 -1
- package/packages/scheduler/package.json +1 -1
- package/packages/scheduler-adapter/package.json +1 -1
- package/packages/session/package.json +1 -1
- package/packages/session-api/package.json +1 -1
- package/packages/session-export/package.json +1 -1
- package/packages/shared-context/package.json +1 -1
- package/packages/skill-mcp-client/package.json +1 -1
- package/packages/skill-registry/package.json +1 -1
- package/packages/skills/package.json +1 -1
- package/packages/sprint-planner/package.json +1 -1
- package/packages/subscription-engine/package.json +1 -1
- package/packages/task-compiler/package.json +1 -1
- package/packages/telegram/package.json +1 -1
- package/packages/tencentdb-memory/package.json +1 -1
- package/packages/tencentdb-sync/package.json +1 -1
- package/packages/test-data-generator/package.json +1 -1
- package/packages/test-generator/package.json +1 -1
- package/packages/todo-bd-sync/package.json +1 -1
- package/packages/token-estimation/package.json +1 -1
- package/packages/token-optimizer/package.json +1 -1
- package/packages/trajectory/package.json +1 -1
- package/packages/tui/package.json +1 -1
- package/packages/types/package.json +1 -1
- package/packages/voice-api/package.json +1 -1
- package/packages/webmcp-tester/package.json +1 -1
- package/packages/wiki/package.json +1 -1
- package/packages/workflow-events/package.json +1 -1
- package/packages/workspace-scanner/package.json +1 -1
- package/packages/worktree/package.json +1 -1
- package/packages/write-review/package.json +1 -1
- package/scripts/relay-client.ts +105 -96
- package/scripts/unified-webhook-server.ts +145 -143
- package/scripts/ws-server.ts +81 -0
|
@@ -1,10 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Cloud Relay -
|
|
2
|
+
* Cloud Relay - Two-way Streaming Communication
|
|
3
3
|
*
|
|
4
|
-
* Enables
|
|
5
|
-
* Based on T3Code's
|
|
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
|
-
|
|
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 {
|
|
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 -
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 ??
|
|
53
|
-
host: options.host ??
|
|
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
|
|
136
|
+
* Start the WebSocket server
|
|
62
137
|
*/
|
|
63
138
|
async start(): Promise<void> {
|
|
64
139
|
if (this.running) return;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
75
|
-
|
|
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
|
-
*
|
|
335
|
+
* Send initial snapshot followed by synchronized marker
|
|
336
|
+
* This is the t3code pattern: snapshot -> synchronized -> live events
|
|
80
337
|
*/
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
*
|
|
372
|
+
* Flush event buffer and send batched callbacks
|
|
87
373
|
*/
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
*
|
|
392
|
+
* Broadcast callback to all connected clients
|
|
393
|
+
* Uses stream batching for high-frequency events
|
|
94
394
|
*/
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
*
|
|
441
|
+
* Broadcast to all subscribed clients
|
|
109
442
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
*
|
|
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
|
}
|