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.
- 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 +57 -35
- package/packages/relay/dist/client.js +284 -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 +380 -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,58 +1,96 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Server -
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*
|
|
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
|
|
7
9
|
*/
|
|
10
|
+
import { type RelayTunnel, type RelayMessage, type ConnectionState, type ConnectionStateChange, type CallbackPayload } from './types.js';
|
|
8
11
|
export interface RelayServerOptions {
|
|
9
12
|
/** Port to listen on */
|
|
10
13
|
port?: number;
|
|
11
14
|
/** Host to bind */
|
|
12
15
|
host?: string;
|
|
16
|
+
/** Authentication token */
|
|
17
|
+
authToken?: string;
|
|
18
|
+
/** Server version string */
|
|
19
|
+
serverVersion?: string;
|
|
13
20
|
/** Called when a tunnel connects */
|
|
14
21
|
onTunnelConnect?: (tunnel: RelayTunnel) => void;
|
|
15
22
|
/** Called when a tunnel disconnects */
|
|
16
23
|
onTunnelDisconnect?: (tunnelId: string) => void;
|
|
17
24
|
/** Called when a message is received */
|
|
18
25
|
onMessage?: (tunnelId: string, message: RelayMessage) => void;
|
|
26
|
+
/** Called on state change */
|
|
27
|
+
onStateChange?: (change: ConnectionStateChange) => void;
|
|
19
28
|
}
|
|
20
|
-
/**
|
|
21
|
-
* Relay Server
|
|
22
|
-
*
|
|
23
|
-
* Manages tunnels and relays messages between clients.
|
|
24
|
-
*/
|
|
25
29
|
export declare class RelayServer {
|
|
30
|
+
private server;
|
|
31
|
+
private wss;
|
|
32
|
+
private clients;
|
|
26
33
|
private tunnels;
|
|
27
34
|
private options;
|
|
28
35
|
private running;
|
|
36
|
+
private serverSessionId;
|
|
37
|
+
private eventBuffer;
|
|
38
|
+
private recentCallbacks;
|
|
29
39
|
constructor(options?: RelayServerOptions);
|
|
30
40
|
/**
|
|
31
|
-
* Start the
|
|
41
|
+
* Start the WebSocket server
|
|
32
42
|
*/
|
|
33
43
|
start(): Promise<void>;
|
|
34
44
|
/**
|
|
35
45
|
* Stop the relay server
|
|
36
46
|
*/
|
|
37
47
|
stop(): Promise<void>;
|
|
48
|
+
private updateState;
|
|
49
|
+
private handleMessage;
|
|
50
|
+
private handleAuth;
|
|
51
|
+
private handlePing;
|
|
52
|
+
private handleSubscribe;
|
|
53
|
+
/**
|
|
54
|
+
* Send initial snapshot followed by synchronized marker
|
|
55
|
+
* This is the t3code pattern: snapshot -> synchronized -> live events
|
|
56
|
+
*/
|
|
57
|
+
private sendSnapshot;
|
|
58
|
+
/**
|
|
59
|
+
* Flush event buffer and send batched callbacks
|
|
60
|
+
*/
|
|
61
|
+
private flushEventBuffer;
|
|
62
|
+
/**
|
|
63
|
+
* Broadcast callback to all connected clients
|
|
64
|
+
* Uses stream batching for high-frequency events
|
|
65
|
+
*/
|
|
66
|
+
broadcastCallback(callback: CallbackPayload, batch?: boolean): void;
|
|
38
67
|
/**
|
|
39
|
-
*
|
|
68
|
+
* Send status to all clients
|
|
40
69
|
*/
|
|
41
|
-
|
|
70
|
+
broadcastStatus(kind: 'info' | 'warning' | 'error', code: string, message: string): void;
|
|
42
71
|
/**
|
|
43
|
-
*
|
|
72
|
+
* Broadcast to all subscribed clients
|
|
44
73
|
*/
|
|
45
|
-
|
|
74
|
+
private broadcast;
|
|
46
75
|
/**
|
|
47
|
-
*
|
|
76
|
+
* Check if client is connected and authenticated
|
|
48
77
|
*/
|
|
49
|
-
|
|
78
|
+
private isConnected;
|
|
50
79
|
/**
|
|
51
|
-
*
|
|
80
|
+
* Send a typed message to a client
|
|
52
81
|
*/
|
|
53
|
-
|
|
82
|
+
private sendMessage;
|
|
54
83
|
/**
|
|
55
|
-
*
|
|
84
|
+
* Send status to specific client
|
|
56
85
|
*/
|
|
86
|
+
private sendStatus;
|
|
57
87
|
isRunning(): boolean;
|
|
88
|
+
getClientCount(): number;
|
|
89
|
+
getClients(): Array<{
|
|
90
|
+
id: string;
|
|
91
|
+
state: ConnectionState;
|
|
92
|
+
connectedAt: string;
|
|
93
|
+
}>;
|
|
94
|
+
getRecentCallbacks(limit?: number): CallbackPayload[];
|
|
58
95
|
}
|
|
96
|
+
export declare function createRelayServer(options?: RelayServerOptions): RelayServer;
|
|
@@ -1,93 +1,388 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Server -
|
|
3
|
-
*/
|
|
4
|
-
/**
|
|
5
|
-
* Generate a UUID v4
|
|
6
|
-
*/
|
|
7
|
-
function generateUUID() {
|
|
8
|
-
// Use crypto.randomUUID if available
|
|
9
|
-
const crypto = globalThis;
|
|
10
|
-
if (crypto.crypto?.randomUUID) {
|
|
11
|
-
return crypto.crypto.randomUUID();
|
|
12
|
-
}
|
|
13
|
-
// Fallback
|
|
14
|
-
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
15
|
-
const r = (Math.random() * 16) | 0;
|
|
16
|
-
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
17
|
-
return v.toString(16);
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Relay Server
|
|
2
|
+
* Relay Server - Two-way streaming WebSocket relay
|
|
22
3
|
*
|
|
23
|
-
*
|
|
4
|
+
* Implements t3code-style streaming with:
|
|
5
|
+
* - Snapshot + Live event pattern
|
|
6
|
+
* - Typed message protocol
|
|
7
|
+
* - Connection state machine
|
|
8
|
+
* - Stream coalescing
|
|
24
9
|
*/
|
|
10
|
+
import { createServer } from "node:http";
|
|
11
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
12
|
+
import { randomUUID } from "node:crypto";
|
|
13
|
+
import { PROTOCOL_VERSION, generateMessageId, createTimestamp, } from './types.js';
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Constants
|
|
16
|
+
// ============================================================================
|
|
17
|
+
const DEFAULT_PORT = 8444;
|
|
18
|
+
const DEFAULT_HOST = "0.0.0.0";
|
|
19
|
+
const MAX_RECENT_CALLBACKS = 100;
|
|
20
|
+
const STREAM_BATCH_SIZE = 50; // Batch events for coalescing
|
|
21
|
+
const STREAM_BATCH_TIMEOUT_MS = 100; // Flush batch after this delay
|
|
22
|
+
function createEventBuffer() {
|
|
23
|
+
return {
|
|
24
|
+
callbacks: [],
|
|
25
|
+
timer: null,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
// ============================================================================
|
|
29
|
+
// Relay Server - Two-way Streaming Implementation
|
|
30
|
+
// ============================================================================
|
|
25
31
|
export class RelayServer {
|
|
32
|
+
server = null;
|
|
33
|
+
wss = null;
|
|
34
|
+
clients = new Map();
|
|
26
35
|
tunnels = new Map();
|
|
27
36
|
options;
|
|
28
37
|
running = false;
|
|
38
|
+
serverSessionId;
|
|
39
|
+
// Event stream buffering for coalescing
|
|
40
|
+
eventBuffer = createEventBuffer();
|
|
41
|
+
// Recent callbacks for snapshot
|
|
42
|
+
recentCallbacks = [];
|
|
29
43
|
constructor(options = {}) {
|
|
44
|
+
this.serverSessionId = randomUUID();
|
|
30
45
|
this.options = {
|
|
31
|
-
port: options.port ??
|
|
32
|
-
host: options.host ??
|
|
46
|
+
port: options.port ?? DEFAULT_PORT,
|
|
47
|
+
host: options.host ?? DEFAULT_HOST,
|
|
48
|
+
authToken: options.authToken ?? "change-me-in-production",
|
|
49
|
+
serverVersion: options.serverVersion ?? "1.0.0",
|
|
33
50
|
onTunnelConnect: options.onTunnelConnect ?? (() => { }),
|
|
34
51
|
onTunnelDisconnect: options.onTunnelDisconnect ?? (() => { }),
|
|
35
52
|
onMessage: options.onMessage ?? (() => { }),
|
|
53
|
+
onStateChange: options.onStateChange ?? (() => { }),
|
|
36
54
|
};
|
|
37
55
|
}
|
|
56
|
+
// ========================================================================
|
|
57
|
+
// Lifecycle
|
|
58
|
+
// ========================================================================
|
|
38
59
|
/**
|
|
39
|
-
* Start the
|
|
60
|
+
* Start the WebSocket server
|
|
40
61
|
*/
|
|
41
62
|
async start() {
|
|
42
63
|
if (this.running)
|
|
43
64
|
return;
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
65
|
+
this.server = createServer();
|
|
66
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
67
|
+
this.wss.on("connection", (ws, req) => {
|
|
68
|
+
const clientId = randomUUID();
|
|
69
|
+
const clientIp = req.socket.remoteAddress;
|
|
70
|
+
console.log(`[RelayServer] Client connecting: ${clientId} from ${clientIp}`);
|
|
71
|
+
// Create client session
|
|
72
|
+
const session = {
|
|
73
|
+
id: clientId,
|
|
74
|
+
ws,
|
|
75
|
+
state: { state: 'connecting' },
|
|
76
|
+
subscriptions: new Set(['callback', 'status', 'sync']),
|
|
77
|
+
connectedAt: createTimestamp(),
|
|
78
|
+
lastPing: createTimestamp(),
|
|
79
|
+
pendingSnapshot: true,
|
|
80
|
+
};
|
|
81
|
+
this.clients.set(clientId, session);
|
|
82
|
+
this.updateState(session, { state: 'connecting' });
|
|
83
|
+
// Handle incoming messages
|
|
84
|
+
ws.on("message", (data) => {
|
|
85
|
+
this.handleMessage(clientId, data);
|
|
86
|
+
});
|
|
87
|
+
ws.on("close", () => {
|
|
88
|
+
this.clients.delete(clientId);
|
|
89
|
+
console.log(`[RelayServer] Client disconnected: ${clientId}`);
|
|
90
|
+
});
|
|
91
|
+
ws.on("error", (err) => {
|
|
92
|
+
console.error(`[RelayServer] Client error: ${clientId}`, err.message);
|
|
93
|
+
this.clients.delete(clientId);
|
|
94
|
+
});
|
|
95
|
+
// Send initial connected message
|
|
96
|
+
this.sendMessage(session, {
|
|
97
|
+
version: PROTOCOL_VERSION,
|
|
98
|
+
type: 'connected',
|
|
99
|
+
payload: {
|
|
100
|
+
clientId,
|
|
101
|
+
serverTime: createTimestamp(),
|
|
102
|
+
},
|
|
103
|
+
timestamp: createTimestamp(),
|
|
104
|
+
});
|
|
105
|
+
// Auto-auth for now (can be changed to require explicit auth)
|
|
106
|
+
this.updateState(session, { state: 'authenticating' });
|
|
107
|
+
this.handleAuth(clientId, this.options.authToken);
|
|
108
|
+
});
|
|
109
|
+
return new Promise((resolve, reject) => {
|
|
110
|
+
this.server.listen(this.options.port, this.options.host, () => {
|
|
111
|
+
this.running = true;
|
|
112
|
+
console.log(`[RelayServer] Server started on ${this.options.host}:${this.options.port}`);
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
this.server.on("error", (err) => {
|
|
116
|
+
console.error("[RelayServer] Server error:", err);
|
|
117
|
+
reject(err);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
47
120
|
}
|
|
48
121
|
/**
|
|
49
122
|
* Stop the relay server
|
|
50
123
|
*/
|
|
51
124
|
async stop() {
|
|
52
|
-
this.running
|
|
53
|
-
|
|
125
|
+
if (!this.running)
|
|
126
|
+
return;
|
|
127
|
+
// Close all client connections
|
|
128
|
+
for (const session of this.clients.values()) {
|
|
129
|
+
session.ws.close();
|
|
130
|
+
}
|
|
131
|
+
this.clients.clear();
|
|
132
|
+
// Close event buffer timer
|
|
133
|
+
if (this.eventBuffer.timer) {
|
|
134
|
+
clearTimeout(this.eventBuffer.timer);
|
|
135
|
+
}
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
this.server.close(() => {
|
|
138
|
+
this.running = false;
|
|
139
|
+
console.log("[RelayServer] Server stopped");
|
|
140
|
+
resolve();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
// ========================================================================
|
|
145
|
+
// State Management
|
|
146
|
+
// ========================================================================
|
|
147
|
+
updateState(session, newState) {
|
|
148
|
+
const previous = session.state;
|
|
149
|
+
session.state = newState;
|
|
150
|
+
this.options.onStateChange({
|
|
151
|
+
previous,
|
|
152
|
+
current: newState,
|
|
153
|
+
timestamp: createTimestamp(),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
// ========================================================================
|
|
157
|
+
// Message Handling
|
|
158
|
+
// ========================================================================
|
|
159
|
+
handleMessage(clientId, data) {
|
|
160
|
+
const session = this.clients.get(clientId);
|
|
161
|
+
if (!session)
|
|
162
|
+
return;
|
|
163
|
+
try {
|
|
164
|
+
const msg = JSON.parse(data.toString());
|
|
165
|
+
switch (msg.type) {
|
|
166
|
+
case 'auth':
|
|
167
|
+
this.handleAuth(clientId, msg.payload.token);
|
|
168
|
+
break;
|
|
169
|
+
case 'ping':
|
|
170
|
+
this.handlePing(session);
|
|
171
|
+
break;
|
|
172
|
+
case 'subscribe':
|
|
173
|
+
this.handleSubscribe(session, msg);
|
|
174
|
+
break;
|
|
175
|
+
default:
|
|
176
|
+
const _msg = msg;
|
|
177
|
+
console.warn(`[RelayServer] Unknown message type: ${_msg?.type}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error("[RelayServer] Invalid message:", err);
|
|
182
|
+
this.sendStatus(session, 'error', 'INVALID_MESSAGE', 'Failed to parse message');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
handleAuth(clientId, token) {
|
|
186
|
+
const session = this.clients.get(clientId);
|
|
187
|
+
if (!session)
|
|
188
|
+
return;
|
|
189
|
+
const success = token === this.options.authToken;
|
|
190
|
+
const response = {
|
|
191
|
+
version: PROTOCOL_VERSION,
|
|
192
|
+
type: 'auth',
|
|
193
|
+
payload: {
|
|
194
|
+
success,
|
|
195
|
+
sessionId: success ? this.serverSessionId : undefined,
|
|
196
|
+
error: success ? undefined : 'Invalid token',
|
|
197
|
+
},
|
|
198
|
+
timestamp: createTimestamp(),
|
|
199
|
+
messageId: generateMessageId(),
|
|
200
|
+
};
|
|
201
|
+
this.sendMessage(session, response);
|
|
202
|
+
if (success) {
|
|
203
|
+
this.updateState(session, { state: 'connected', sessionId: this.serverSessionId });
|
|
204
|
+
console.log(`[RelayServer] Client authenticated: ${clientId}`);
|
|
205
|
+
// Send snapshot + synchronized (t3code pattern)
|
|
206
|
+
this.sendSnapshot(session);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
this.updateState(session, { state: 'error', error: 'Authentication failed' });
|
|
210
|
+
session.ws.close();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
handlePing(session) {
|
|
214
|
+
session.lastPing = createTimestamp();
|
|
215
|
+
this.sendMessage(session, {
|
|
216
|
+
version: PROTOCOL_VERSION,
|
|
217
|
+
type: 'pong',
|
|
218
|
+
payload: {},
|
|
219
|
+
timestamp: createTimestamp(),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
handleSubscribe(session, msg) {
|
|
223
|
+
session.subscriptions = new Set(msg.payload.events);
|
|
224
|
+
console.log(`[RelayServer] Client ${session.id} subscribed to: ${Array.from(session.subscriptions).join(', ')}`);
|
|
225
|
+
}
|
|
226
|
+
// ========================================================================
|
|
227
|
+
// Snapshot + Live Streaming (t3code pattern)
|
|
228
|
+
// ========================================================================
|
|
229
|
+
/**
|
|
230
|
+
* Send initial snapshot followed by synchronized marker
|
|
231
|
+
* This is the t3code pattern: snapshot -> synchronized -> live events
|
|
232
|
+
*/
|
|
233
|
+
async sendSnapshot(session) {
|
|
234
|
+
// Build snapshot message
|
|
235
|
+
const snapshot = {
|
|
236
|
+
version: PROTOCOL_VERSION,
|
|
237
|
+
type: 'snapshot',
|
|
238
|
+
payload: {
|
|
239
|
+
sessionId: this.serverSessionId,
|
|
240
|
+
serverVersion: this.options.serverVersion,
|
|
241
|
+
connectedClients: this.clients.size,
|
|
242
|
+
recentCallbacks: this.recentCallbacks.slice(-20), // Last 20 callbacks
|
|
243
|
+
},
|
|
244
|
+
timestamp: createTimestamp(),
|
|
245
|
+
messageId: generateMessageId(),
|
|
246
|
+
};
|
|
247
|
+
// Send snapshot
|
|
248
|
+
this.sendMessage(session, snapshot);
|
|
249
|
+
// Send synchronized marker (t3code pattern: indicates snapshot complete)
|
|
250
|
+
const synchronized = {
|
|
251
|
+
version: PROTOCOL_VERSION,
|
|
252
|
+
type: 'synchronized',
|
|
253
|
+
payload: {},
|
|
254
|
+
timestamp: createTimestamp(),
|
|
255
|
+
messageId: generateMessageId(),
|
|
256
|
+
};
|
|
257
|
+
this.sendMessage(session, synchronized);
|
|
258
|
+
session.pendingSnapshot = false;
|
|
259
|
+
console.log(`[RelayServer] Snapshot sent to ${session.id}`);
|
|
54
260
|
}
|
|
55
261
|
/**
|
|
56
|
-
*
|
|
262
|
+
* Flush event buffer and send batched callbacks
|
|
57
263
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
264
|
+
flushEventBuffer() {
|
|
265
|
+
if (this.eventBuffer.callbacks.length === 0)
|
|
266
|
+
return;
|
|
267
|
+
const callbacks = this.eventBuffer.callbacks;
|
|
268
|
+
this.eventBuffer.callbacks = [];
|
|
269
|
+
this.eventBuffer.timer = null;
|
|
270
|
+
// Send each callback as a separate live event
|
|
271
|
+
for (const callback of callbacks) {
|
|
272
|
+
this.broadcastCallback(callback, false); // false = don't add to recent
|
|
273
|
+
}
|
|
60
274
|
}
|
|
275
|
+
// ========================================================================
|
|
276
|
+
// Broadcasting
|
|
277
|
+
// ========================================================================
|
|
61
278
|
/**
|
|
62
|
-
*
|
|
279
|
+
* Broadcast callback to all connected clients
|
|
280
|
+
* Uses stream batching for high-frequency events
|
|
63
281
|
*/
|
|
64
|
-
|
|
65
|
-
|
|
282
|
+
broadcastCallback(callback, batch = true) {
|
|
283
|
+
// Add to recent callbacks (circular buffer)
|
|
284
|
+
this.recentCallbacks.push(callback);
|
|
285
|
+
if (this.recentCallbacks.length > MAX_RECENT_CALLBACKS) {
|
|
286
|
+
this.recentCallbacks.shift();
|
|
287
|
+
}
|
|
288
|
+
const message = {
|
|
289
|
+
version: PROTOCOL_VERSION,
|
|
290
|
+
type: 'callback',
|
|
291
|
+
payload: callback,
|
|
292
|
+
timestamp: createTimestamp(),
|
|
293
|
+
messageId: generateMessageId(),
|
|
294
|
+
};
|
|
295
|
+
if (batch) {
|
|
296
|
+
// Add to buffer for coalescing
|
|
297
|
+
this.eventBuffer.callbacks.push(callback);
|
|
298
|
+
if (!this.eventBuffer.timer) {
|
|
299
|
+
this.eventBuffer.timer = setTimeout(() => {
|
|
300
|
+
this.flushEventBuffer();
|
|
301
|
+
}, STREAM_BATCH_TIMEOUT_MS);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
// Send immediately
|
|
306
|
+
this.broadcast(message);
|
|
307
|
+
}
|
|
66
308
|
}
|
|
67
309
|
/**
|
|
68
|
-
*
|
|
310
|
+
* Send status to all clients
|
|
69
311
|
*/
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
312
|
+
broadcastStatus(kind, code, message) {
|
|
313
|
+
const msg = {
|
|
314
|
+
version: PROTOCOL_VERSION,
|
|
315
|
+
type: 'status',
|
|
316
|
+
payload: { kind, code, message },
|
|
317
|
+
timestamp: createTimestamp(),
|
|
318
|
+
messageId: generateMessageId(),
|
|
77
319
|
};
|
|
78
|
-
this.
|
|
79
|
-
return tunnel;
|
|
320
|
+
this.broadcast(msg);
|
|
80
321
|
}
|
|
81
322
|
/**
|
|
82
|
-
*
|
|
323
|
+
* Broadcast to all subscribed clients
|
|
83
324
|
*/
|
|
84
|
-
|
|
85
|
-
|
|
325
|
+
broadcast(message) {
|
|
326
|
+
let count = 0;
|
|
327
|
+
for (const session of this.clients.values()) {
|
|
328
|
+
if (this.isConnected(session)) {
|
|
329
|
+
this.sendMessage(session, message);
|
|
330
|
+
count++;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
console.log(`[RelayServer] Broadcast ${message.type} to ${count} client(s)`);
|
|
86
334
|
}
|
|
87
335
|
/**
|
|
88
|
-
* Check if
|
|
336
|
+
* Check if client is connected and authenticated
|
|
89
337
|
*/
|
|
338
|
+
isConnected(session) {
|
|
339
|
+
return session.ws.readyState === WebSocket.OPEN &&
|
|
340
|
+
session.state.state === 'connected';
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Send a typed message to a client
|
|
344
|
+
*/
|
|
345
|
+
sendMessage(session, message) {
|
|
346
|
+
if (session.ws.readyState === WebSocket.OPEN) {
|
|
347
|
+
session.ws.send(JSON.stringify(message));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Send status to specific client
|
|
352
|
+
*/
|
|
353
|
+
sendStatus(session, kind, code, message) {
|
|
354
|
+
const msg = {
|
|
355
|
+
version: PROTOCOL_VERSION,
|
|
356
|
+
type: 'status',
|
|
357
|
+
payload: { kind, code, message },
|
|
358
|
+
timestamp: createTimestamp(),
|
|
359
|
+
messageId: generateMessageId(),
|
|
360
|
+
};
|
|
361
|
+
this.sendMessage(session, msg);
|
|
362
|
+
}
|
|
363
|
+
// ========================================================================
|
|
364
|
+
// Public API
|
|
365
|
+
// ========================================================================
|
|
90
366
|
isRunning() {
|
|
91
367
|
return this.running;
|
|
92
368
|
}
|
|
369
|
+
getClientCount() {
|
|
370
|
+
return this.clients.size;
|
|
371
|
+
}
|
|
372
|
+
getClients() {
|
|
373
|
+
return Array.from(this.clients.values()).map(s => ({
|
|
374
|
+
id: s.id,
|
|
375
|
+
state: s.state,
|
|
376
|
+
connectedAt: s.connectedAt,
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
getRecentCallbacks(limit = 20) {
|
|
380
|
+
return this.recentCallbacks.slice(-limit);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
// ============================================================================
|
|
384
|
+
// Export for use in webhook server
|
|
385
|
+
// ============================================================================
|
|
386
|
+
export function createRelayServer(options) {
|
|
387
|
+
return new RelayServer(options);
|
|
93
388
|
}
|