pi-harness-runtime 1.1.375 → 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/README.md +68 -39
- package/package.json +2 -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/dist/ws-server.d.ts +48 -0
- package/packages/relay/dist/ws-server.js +130 -0
- 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/relay/src/ws-server.ts +165 -0
- 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 +184 -0
- package/scripts/sync-callback-queue.ts +57 -0
- package/scripts/telegram-callback-processor.ts +16 -0
- package/scripts/unified-webhook-server.ts +160 -145
- package/scripts/ws-server.ts +81 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Relay Server - Pushes callbacks to connected clients in real-time
|
|
3
|
+
*
|
|
4
|
+
* Architecture:
|
|
5
|
+
* Telegram/LINE → Webhook → This Server → WebSocket → Local Client
|
|
6
|
+
*
|
|
7
|
+
* No file queue needed - real-time streaming!
|
|
8
|
+
*/
|
|
9
|
+
import { createServer } from "node:http";
|
|
10
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
const AUTH_TOKEN = process.env.RELAY_AUTH_TOKEN || "change-me-in-production";
|
|
13
|
+
/**
|
|
14
|
+
* WebSocket Relay Server
|
|
15
|
+
*/
|
|
16
|
+
export class WsRelayServer {
|
|
17
|
+
server = null;
|
|
18
|
+
wss = null;
|
|
19
|
+
clients = new Map();
|
|
20
|
+
port;
|
|
21
|
+
running = false;
|
|
22
|
+
constructor(port = 8444) {
|
|
23
|
+
this.port = port;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Start the WebSocket server
|
|
27
|
+
*/
|
|
28
|
+
async start() {
|
|
29
|
+
if (this.running)
|
|
30
|
+
return;
|
|
31
|
+
this.server = createServer();
|
|
32
|
+
this.wss = new WebSocketServer({ server: this.server });
|
|
33
|
+
this.wss.on("connection", (ws, req) => {
|
|
34
|
+
const clientId = randomUUID();
|
|
35
|
+
console.log(`[WsRelay] Client connected: ${clientId} from ${req.socket.remoteAddress}`);
|
|
36
|
+
// Don't auth yet - just accept connection
|
|
37
|
+
// Client will send auth message
|
|
38
|
+
ws.on("message", (data) => {
|
|
39
|
+
try {
|
|
40
|
+
const msg = JSON.parse(data.toString());
|
|
41
|
+
this.handleMessage(ws, clientId, msg);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
console.error("[WsRelay] Invalid message:", err);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
ws.on("close", () => {
|
|
48
|
+
this.clients.delete(clientId);
|
|
49
|
+
console.log(`[WsRelay] Client disconnected: ${clientId}`);
|
|
50
|
+
});
|
|
51
|
+
ws.on("error", (err) => {
|
|
52
|
+
console.error(`[WsRelay] Client error: ${clientId}`, err);
|
|
53
|
+
this.clients.delete(clientId);
|
|
54
|
+
});
|
|
55
|
+
// Send welcome
|
|
56
|
+
ws.send(JSON.stringify({ type: "connected", payload: { clientId } }));
|
|
57
|
+
});
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
this.server.listen(this.port, () => {
|
|
60
|
+
this.running = true;
|
|
61
|
+
console.log(`[WsRelay] Server started on port ${this.port}`);
|
|
62
|
+
resolve();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
handleMessage(ws, clientId, msg) {
|
|
67
|
+
switch (msg.type) {
|
|
68
|
+
case "auth":
|
|
69
|
+
if (msg.payload === AUTH_TOKEN) {
|
|
70
|
+
this.clients.set(clientId, ws);
|
|
71
|
+
ws.send(JSON.stringify({ type: "auth", payload: { success: true } }));
|
|
72
|
+
console.log(`[WsRelay] Client authenticated: ${clientId}`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
ws.send(JSON.stringify({ type: "auth", payload: { success: false, error: "Invalid token" } }));
|
|
76
|
+
ws.close();
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
case "ping":
|
|
80
|
+
ws.send(JSON.stringify({ type: "pong", payload: {} }));
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Broadcast callback to all connected clients
|
|
86
|
+
*/
|
|
87
|
+
broadcastCallback(callback) {
|
|
88
|
+
const msg = JSON.stringify({ type: "callback", payload: callback });
|
|
89
|
+
let count = 0;
|
|
90
|
+
for (const [clientId, ws] of this.clients) {
|
|
91
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
92
|
+
ws.send(msg);
|
|
93
|
+
count++;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
console.log(`[WsRelay] Broadcast callback to ${count} client(s): ${callback.action} ${callback.targetId}`);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Stop the server
|
|
100
|
+
*/
|
|
101
|
+
async stop() {
|
|
102
|
+
if (!this.running)
|
|
103
|
+
return;
|
|
104
|
+
for (const ws of this.clients.values()) {
|
|
105
|
+
ws.close();
|
|
106
|
+
}
|
|
107
|
+
this.clients.clear();
|
|
108
|
+
return new Promise((resolve) => {
|
|
109
|
+
this.server.close(() => {
|
|
110
|
+
this.running = false;
|
|
111
|
+
console.log("[WsRelay] Server stopped");
|
|
112
|
+
resolve();
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
isRunning() {
|
|
117
|
+
return this.running;
|
|
118
|
+
}
|
|
119
|
+
getClientCount() {
|
|
120
|
+
return this.clients.size;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Export singleton for use in unified webhook server
|
|
124
|
+
let singleton = null;
|
|
125
|
+
export function getRelayServer() {
|
|
126
|
+
if (!singleton) {
|
|
127
|
+
singleton = new WsRelayServer();
|
|
128
|
+
}
|
|
129
|
+
return singleton;
|
|
130
|
+
}
|
|
@@ -1,119 +1,413 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Client -
|
|
2
|
+
* Relay Client - Two-way streaming WebSocket client
|
|
3
|
+
*
|
|
4
|
+
* Implements t3code-style streaming with:
|
|
5
|
+
* - Connection state machine
|
|
6
|
+
* - Snapshot + live event handling
|
|
7
|
+
* - Typed message protocol
|
|
8
|
+
* - Automatic reconnection with backoff
|
|
3
9
|
*/
|
|
4
10
|
|
|
5
|
-
import
|
|
11
|
+
import WebSocket from "ws";
|
|
12
|
+
import {
|
|
13
|
+
type RelayConfig,
|
|
14
|
+
type RelayTunnel,
|
|
15
|
+
type RelayMessage,
|
|
16
|
+
type ConnectionState,
|
|
17
|
+
type ConnectionStateChange,
|
|
18
|
+
type CallbackPayload,
|
|
19
|
+
type ServerMessage,
|
|
20
|
+
type ClientMessage,
|
|
21
|
+
type SnapshotMessage,
|
|
22
|
+
type SynchronizedMessage,
|
|
23
|
+
type CallbackMessage,
|
|
24
|
+
type StatusMessage,
|
|
25
|
+
type AuthResponseMessage,
|
|
26
|
+
type ConnectedMessage,
|
|
27
|
+
PROTOCOL_VERSION,
|
|
28
|
+
generateMessageId,
|
|
29
|
+
createTimestamp,
|
|
30
|
+
} from './types.js';
|
|
6
31
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
function generateUUID(): string {
|
|
11
|
-
// Use crypto.randomUUID if available (Node.js 14.17+)
|
|
12
|
-
const crypto = globalThis as { crypto?: { randomUUID?: () => string } };
|
|
13
|
-
if (crypto.crypto?.randomUUID) {
|
|
14
|
-
return crypto.crypto.randomUUID();
|
|
15
|
-
}
|
|
16
|
-
// Fallback: simple UUID generation
|
|
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
|
-
}
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Constants
|
|
34
|
+
// ============================================================================
|
|
23
35
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
const DEFAULT_RECONNECT_DELAY_MS = 1000;
|
|
37
|
+
const MAX_RECONNECT_DELAY_MS = 30000;
|
|
38
|
+
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
39
|
+
const CONNECT_TIMEOUT_MS = 10000;
|
|
40
|
+
|
|
41
|
+
// ============================================================================
|
|
42
|
+
// Client Callbacks
|
|
43
|
+
// ============================================================================
|
|
44
|
+
|
|
45
|
+
export interface RelayClientCallbacks {
|
|
46
|
+
/** Called when connected and authenticated */
|
|
47
|
+
onConnected?: (sessionId: string) => void;
|
|
30
48
|
/** Called when disconnected */
|
|
31
49
|
onDisconnected?: () => void;
|
|
32
|
-
/** Called when
|
|
33
|
-
|
|
50
|
+
/** Called when snapshot is received (initial state) */
|
|
51
|
+
onSnapshot?: (snapshot: SnapshotMessage['payload']) => void;
|
|
52
|
+
/** Called when synchronized marker received (live events start) */
|
|
53
|
+
onSynchronized?: () => void;
|
|
54
|
+
/** Called when a callback event is received */
|
|
55
|
+
onCallback?: (callback: CallbackPayload) => void;
|
|
56
|
+
/** Called when a status message is received */
|
|
57
|
+
onStatus?: (status: StatusMessage['payload']) => void;
|
|
34
58
|
/** Called on error */
|
|
35
59
|
onError?: (error: Error) => void;
|
|
60
|
+
/** Called when connection state changes */
|
|
61
|
+
onStateChange?: (change: ConnectionStateChange) => void;
|
|
36
62
|
}
|
|
37
63
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
*/
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Relay Client - Two-way Streaming Implementation
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
43
68
|
export class RelayClient {
|
|
69
|
+
private ws: WebSocket | null = null;
|
|
44
70
|
private config: RelayConfig;
|
|
45
|
-
private
|
|
46
|
-
|
|
47
|
-
|
|
71
|
+
private callbacks: RelayClientCallbacks;
|
|
72
|
+
|
|
73
|
+
// State machine
|
|
74
|
+
private state: ConnectionState = { state: 'disconnected' };
|
|
75
|
+
private sessionId: string | null = null;
|
|
76
|
+
private clientId: string | null = null;
|
|
77
|
+
|
|
78
|
+
// Reconnection
|
|
79
|
+
private reconnectAttempt = 0;
|
|
80
|
+
private reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
|
|
81
|
+
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
82
|
+
private connectTimeoutTimer: ReturnType<typeof setTimeout> | null = null;
|
|
83
|
+
|
|
84
|
+
// Heartbeat
|
|
85
|
+
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
86
|
+
|
|
87
|
+
// Snapshot tracking (t3code pattern)
|
|
88
|
+
private snapshotReceived = false;
|
|
89
|
+
private synchronizedReceived = false;
|
|
48
90
|
|
|
49
|
-
constructor(config: RelayConfig,
|
|
91
|
+
constructor(config: RelayConfig, callbacks: RelayClientCallbacks = {}) {
|
|
50
92
|
this.config = config;
|
|
51
|
-
this.
|
|
93
|
+
this.callbacks = callbacks;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ========================================================================
|
|
97
|
+
// Connection State Machine
|
|
98
|
+
// ========================================================================
|
|
99
|
+
|
|
100
|
+
private setState(newState: ConnectionState): void {
|
|
101
|
+
const previous = this.state;
|
|
102
|
+
this.state = newState;
|
|
103
|
+
|
|
104
|
+
this.callbacks.onStateChange?.({
|
|
105
|
+
previous,
|
|
106
|
+
current: newState,
|
|
107
|
+
timestamp: createTimestamp(),
|
|
108
|
+
});
|
|
52
109
|
}
|
|
53
110
|
|
|
111
|
+
private isConnected(): boolean {
|
|
112
|
+
return this.ws?.readyState === WebSocket.OPEN &&
|
|
113
|
+
this.state.state === 'connected';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ========================================================================
|
|
117
|
+
// Lifecycle
|
|
118
|
+
// ========================================================================
|
|
119
|
+
|
|
54
120
|
/**
|
|
55
|
-
* Connect to relay server
|
|
121
|
+
* Connect to relay server with automatic authentication
|
|
56
122
|
*/
|
|
57
123
|
async connect(): Promise<void> {
|
|
58
|
-
if (this.
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
124
|
+
if (this.isConnected()) return;
|
|
125
|
+
|
|
126
|
+
// Clear any existing connection
|
|
127
|
+
this.cleanup();
|
|
128
|
+
|
|
129
|
+
this.setState({ state: 'connecting' });
|
|
130
|
+
|
|
131
|
+
const url = this.config.serverUrl;
|
|
132
|
+
console.log(`[RelayClient] Connecting to ${url}...`);
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
// Set connect timeout
|
|
136
|
+
this.connectTimeoutTimer = setTimeout(() => {
|
|
137
|
+
if (this.state.state === 'connecting') {
|
|
138
|
+
this.ws?.close();
|
|
139
|
+
reject(new Error('Connection timeout'));
|
|
140
|
+
}
|
|
141
|
+
}, CONNECT_TIMEOUT_MS);
|
|
142
|
+
|
|
143
|
+
this.ws = new WebSocket(url);
|
|
144
|
+
|
|
145
|
+
this.ws.on("open", () => {
|
|
146
|
+
console.log("[RelayClient] WebSocket opened");
|
|
147
|
+
this.setState({ state: 'authenticating' });
|
|
148
|
+
|
|
149
|
+
// Send authentication
|
|
150
|
+
this.send({
|
|
151
|
+
version: PROTOCOL_VERSION,
|
|
152
|
+
type: 'auth',
|
|
153
|
+
payload: { token: this.config.token || '' },
|
|
154
|
+
timestamp: createTimestamp(),
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.ws.on("message", (data) => {
|
|
159
|
+
this.handleMessage(data.toString());
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
this.ws.on("close", () => {
|
|
163
|
+
console.log("[RelayClient] WebSocket closed");
|
|
164
|
+
this.handleDisconnect();
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
this.ws.on("error", (err) => {
|
|
168
|
+
console.error("[RelayClient] WebSocket error:", err.message);
|
|
169
|
+
this.callbacks.onError?.(err);
|
|
170
|
+
|
|
171
|
+
if (this.state.state === 'connecting' || this.state.state === 'authenticating') {
|
|
172
|
+
clearTimeout(this.connectTimeoutTimer!);
|
|
173
|
+
reject(err);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
this.handleDisconnect();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
69
179
|
}
|
|
70
180
|
|
|
71
181
|
/**
|
|
72
182
|
* Disconnect from relay server
|
|
73
183
|
*/
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
184
|
+
disconnect(): void {
|
|
185
|
+
console.log("[RelayClient] Disconnecting...");
|
|
186
|
+
this.cleanup();
|
|
187
|
+
this.setState({ state: 'disconnected' });
|
|
188
|
+
this.callbacks.onDisconnected?.();
|
|
78
189
|
}
|
|
79
190
|
|
|
80
191
|
/**
|
|
81
|
-
*
|
|
192
|
+
* Send a message through the WebSocket
|
|
82
193
|
*/
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
localPort,
|
|
88
|
-
status: this.connected ? 'connected' : 'disconnected',
|
|
89
|
-
createdAt: new Date().toISOString(),
|
|
90
|
-
};
|
|
91
|
-
this.tunnel = tunnel;
|
|
92
|
-
this.options.onTunnel?.(tunnel);
|
|
93
|
-
return tunnel;
|
|
194
|
+
send(message: ClientMessage): void {
|
|
195
|
+
if (this.isConnected()) {
|
|
196
|
+
this.ws!.send(JSON.stringify(message));
|
|
197
|
+
}
|
|
94
198
|
}
|
|
95
199
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
200
|
+
// ========================================================================
|
|
201
|
+
// Message Handling
|
|
202
|
+
// ========================================================================
|
|
203
|
+
|
|
204
|
+
private handleMessage(data: string): void {
|
|
205
|
+
try {
|
|
206
|
+
const msg = JSON.parse(data) as ServerMessage & { type: string };
|
|
207
|
+
|
|
208
|
+
switch (msg.type) {
|
|
209
|
+
case 'connected':
|
|
210
|
+
this.handleConnected(msg as ConnectedMessage);
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case 'auth':
|
|
214
|
+
this.handleAuth(msg as AuthResponseMessage);
|
|
215
|
+
break;
|
|
216
|
+
|
|
217
|
+
case 'snapshot':
|
|
218
|
+
this.handleSnapshot(msg as SnapshotMessage);
|
|
219
|
+
break;
|
|
220
|
+
|
|
221
|
+
case 'synchronized':
|
|
222
|
+
this.handleSynchronized(msg as SynchronizedMessage);
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case 'callback':
|
|
226
|
+
this.handleCallback(msg as CallbackMessage);
|
|
227
|
+
break;
|
|
228
|
+
|
|
229
|
+
case 'status':
|
|
230
|
+
this.handleStatus(msg as StatusMessage);
|
|
231
|
+
break;
|
|
232
|
+
|
|
233
|
+
case 'pong':
|
|
234
|
+
// Heartbeat response - connection is alive
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
default: {
|
|
238
|
+
const unknownMsg = msg as { type: string };
|
|
239
|
+
console.warn(`[RelayClient] Unknown message type: ${unknownMsg.type}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch (err) {
|
|
243
|
+
console.error("[RelayClient] Failed to parse message:", err);
|
|
244
|
+
this.callbacks.onError?.(err as Error);
|
|
102
245
|
}
|
|
103
|
-
// In a real implementation, this would send the message over WebSocket
|
|
104
246
|
}
|
|
105
247
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
isConnected(): boolean {
|
|
110
|
-
return this.connected;
|
|
248
|
+
private handleConnected(msg: ConnectedMessage): void {
|
|
249
|
+
this.clientId = msg.payload.clientId;
|
|
250
|
+
console.log(`[RelayClient] Connected, clientId: ${this.clientId}`);
|
|
111
251
|
}
|
|
112
252
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
253
|
+
private handleAuth(msg: AuthResponseMessage): void {
|
|
254
|
+
if (msg.payload.success) {
|
|
255
|
+
this.sessionId = msg.payload.sessionId || null;
|
|
256
|
+
this.setState({ state: 'connected', sessionId: this.sessionId || '' });
|
|
257
|
+
|
|
258
|
+
// Reset reconnection state
|
|
259
|
+
this.reconnectAttempt = 0;
|
|
260
|
+
this.reconnectDelay = DEFAULT_RECONNECT_DELAY_MS;
|
|
261
|
+
|
|
262
|
+
// Start heartbeat
|
|
263
|
+
this.startHeartbeat();
|
|
264
|
+
|
|
265
|
+
// Notify callback
|
|
266
|
+
this.callbacks.onConnected?.(this.sessionId || '');
|
|
267
|
+
console.log(`[RelayClient] Authenticated, sessionId: ${this.sessionId}`);
|
|
268
|
+
} else {
|
|
269
|
+
console.error(`[RelayClient] Auth failed: ${msg.payload.error}`);
|
|
270
|
+
this.callbacks.onError?.(new Error(msg.payload.error || 'Authentication failed'));
|
|
271
|
+
this.disconnect();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private handleSnapshot(msg: SnapshotMessage): void {
|
|
276
|
+
console.log(`[RelayClient] Snapshot received: ${msg.payload.recentCallbacks.length} recent callbacks`);
|
|
277
|
+
this.snapshotReceived = true;
|
|
278
|
+
|
|
279
|
+
// Notify callback
|
|
280
|
+
this.callbacks.onSnapshot?.(msg.payload);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private handleSynchronized(_msg: SynchronizedMessage): void {
|
|
284
|
+
console.log("[RelayClient] Synchronized - live events starting");
|
|
285
|
+
this.synchronizedReceived = true;
|
|
286
|
+
|
|
287
|
+
// Notify callback
|
|
288
|
+
this.callbacks.onSynchronized?.();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private handleCallback(msg: CallbackMessage): void {
|
|
292
|
+
// Only process callbacks after synchronized
|
|
293
|
+
if (!this.synchronizedReceived) {
|
|
294
|
+
console.warn("[RelayClient] Ignoring callback before synchronized");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
this.callbacks.onCallback?.(msg.payload);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private handleStatus(msg: StatusMessage): void {
|
|
302
|
+
console.log(`[RelayClient] Status [${msg.payload.kind}]: ${msg.payload.code} - ${msg.payload.message}`);
|
|
303
|
+
this.callbacks.onStatus?.(msg.payload);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ========================================================================
|
|
307
|
+
// Disconnect & Reconnect
|
|
308
|
+
// ========================================================================
|
|
309
|
+
|
|
310
|
+
private handleDisconnect(): void {
|
|
311
|
+
this.stopHeartbeat();
|
|
312
|
+
|
|
313
|
+
const wasConnected = this.state.state === 'connected';
|
|
314
|
+
this.setState({ state: 'disconnected' });
|
|
315
|
+
|
|
316
|
+
if (wasConnected) {
|
|
317
|
+
this.callbacks.onDisconnected?.();
|
|
318
|
+
this.scheduleReconnect();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
private scheduleReconnect(): void {
|
|
323
|
+
if (this.reconnectTimer) return;
|
|
324
|
+
|
|
325
|
+
this.reconnectAttempt++;
|
|
326
|
+
this.setState({ state: 'reconnecting', attempt: this.reconnectAttempt });
|
|
327
|
+
|
|
328
|
+
console.log(`[RelayClient] Scheduling reconnect attempt ${this.reconnectAttempt} in ${this.reconnectDelay}ms`);
|
|
329
|
+
|
|
330
|
+
this.reconnectTimer = setTimeout(() => {
|
|
331
|
+
this.reconnectTimer = null;
|
|
332
|
+
this.connect().catch((err) => {
|
|
333
|
+
console.error(`[RelayClient] Reconnect failed: ${err.message}`);
|
|
334
|
+
});
|
|
335
|
+
}, this.reconnectDelay);
|
|
336
|
+
|
|
337
|
+
// Exponential backoff
|
|
338
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, MAX_RECONNECT_DELAY_MS);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// ========================================================================
|
|
342
|
+
// Heartbeat
|
|
343
|
+
// ========================================================================
|
|
344
|
+
|
|
345
|
+
private startHeartbeat(): void {
|
|
346
|
+
this.stopHeartbeat();
|
|
347
|
+
|
|
348
|
+
this.heartbeatTimer = setInterval(() => {
|
|
349
|
+
if (this.isConnected()) {
|
|
350
|
+
this.send({
|
|
351
|
+
version: PROTOCOL_VERSION,
|
|
352
|
+
type: 'ping',
|
|
353
|
+
payload: {},
|
|
354
|
+
timestamp: createTimestamp(),
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private stopHeartbeat(): void {
|
|
361
|
+
if (this.heartbeatTimer) {
|
|
362
|
+
clearInterval(this.heartbeatTimer);
|
|
363
|
+
this.heartbeatTimer = null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ========================================================================
|
|
368
|
+
// Cleanup
|
|
369
|
+
// ========================================================================
|
|
370
|
+
|
|
371
|
+
private cleanup(): void {
|
|
372
|
+
// Clear timers
|
|
373
|
+
if (this.reconnectTimer) {
|
|
374
|
+
clearTimeout(this.reconnectTimer);
|
|
375
|
+
this.reconnectTimer = null;
|
|
376
|
+
}
|
|
377
|
+
if (this.connectTimeoutTimer) {
|
|
378
|
+
clearTimeout(this.connectTimeoutTimer);
|
|
379
|
+
this.connectTimeoutTimer = null;
|
|
380
|
+
}
|
|
381
|
+
this.stopHeartbeat();
|
|
382
|
+
|
|
383
|
+
// Reset snapshot state
|
|
384
|
+
this.snapshotReceived = false;
|
|
385
|
+
this.synchronizedReceived = false;
|
|
386
|
+
|
|
387
|
+
// Close WebSocket
|
|
388
|
+
if (this.ws) {
|
|
389
|
+
this.ws.close();
|
|
390
|
+
this.ws = null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ========================================================================
|
|
395
|
+
// Public API
|
|
396
|
+
// ========================================================================
|
|
397
|
+
|
|
398
|
+
getState(): ConnectionState {
|
|
399
|
+
return this.state;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
isReady(): boolean {
|
|
403
|
+
return this.isConnected() && this.synchronizedReceived;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
getSessionId(): string | null {
|
|
407
|
+
return this.sessionId;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
getClientId(): string | null {
|
|
411
|
+
return this.clientId;
|
|
118
412
|
}
|
|
119
413
|
}
|
|
@@ -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';
|