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,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Types
|
|
2
|
+
* Relay Types - Two-way Streaming Communication
|
|
3
3
|
*/
|
|
4
4
|
export interface RelayConfig {
|
|
5
5
|
/** Relay server URL */
|
|
@@ -27,6 +27,142 @@ export interface RelayTunnel {
|
|
|
27
27
|
/** Error message if any */
|
|
28
28
|
error?: string;
|
|
29
29
|
}
|
|
30
|
+
export type ConnectionState = {
|
|
31
|
+
state: 'disconnected';
|
|
32
|
+
} | {
|
|
33
|
+
state: 'connecting';
|
|
34
|
+
} | {
|
|
35
|
+
state: 'authenticating';
|
|
36
|
+
} | {
|
|
37
|
+
state: 'connected';
|
|
38
|
+
sessionId: string;
|
|
39
|
+
} | {
|
|
40
|
+
state: 'reconnecting';
|
|
41
|
+
attempt: number;
|
|
42
|
+
} | {
|
|
43
|
+
state: 'error';
|
|
44
|
+
error: string;
|
|
45
|
+
};
|
|
46
|
+
export interface ConnectionStateChange {
|
|
47
|
+
previous: ConnectionState;
|
|
48
|
+
current: ConnectionState;
|
|
49
|
+
timestamp: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Protocol version for typed streaming
|
|
53
|
+
*/
|
|
54
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
55
|
+
/**
|
|
56
|
+
* Generate a unique message ID
|
|
57
|
+
*/
|
|
58
|
+
export declare function generateMessageId(): string;
|
|
59
|
+
/**
|
|
60
|
+
* Create a timestamp
|
|
61
|
+
*/
|
|
62
|
+
export declare function createTimestamp(): string;
|
|
63
|
+
/**
|
|
64
|
+
* Base message interface
|
|
65
|
+
*/
|
|
66
|
+
export interface BaseMessage<T extends string, P = unknown> {
|
|
67
|
+
version: typeof PROTOCOL_VERSION;
|
|
68
|
+
type: T;
|
|
69
|
+
payload: P;
|
|
70
|
+
timestamp: string;
|
|
71
|
+
messageId?: string;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Server -> Client: Initial state snapshot sent on connect
|
|
75
|
+
*/
|
|
76
|
+
export interface SnapshotMessage extends BaseMessage<'snapshot'> {
|
|
77
|
+
payload: {
|
|
78
|
+
sessionId: string;
|
|
79
|
+
serverVersion: string;
|
|
80
|
+
connectedClients: number;
|
|
81
|
+
recentCallbacks: CallbackPayload[];
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Server -> Client: Synchronization marker (indicates all initial state sent)
|
|
86
|
+
*/
|
|
87
|
+
export interface SynchronizedMessage extends BaseMessage<'synchronized'> {
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Callback types from Telegram/LINE
|
|
91
|
+
*/
|
|
92
|
+
export interface CallbackPayload {
|
|
93
|
+
id: string;
|
|
94
|
+
type: 'telegram_callback' | 'line_callback';
|
|
95
|
+
action: string;
|
|
96
|
+
targetId: string;
|
|
97
|
+
userId: number;
|
|
98
|
+
username?: string;
|
|
99
|
+
messageId?: number;
|
|
100
|
+
timestamp: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Server -> Client: Live callback event (after synchronized)
|
|
104
|
+
*/
|
|
105
|
+
export interface CallbackMessage extends BaseMessage<'callback'> {
|
|
106
|
+
payload: CallbackPayload;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Server -> Client: Status update
|
|
110
|
+
*/
|
|
111
|
+
export type StatusKind = 'info' | 'warning' | 'error';
|
|
112
|
+
export interface StatusMessage extends BaseMessage<'status'> {
|
|
113
|
+
payload: {
|
|
114
|
+
kind: StatusKind;
|
|
115
|
+
code: string;
|
|
116
|
+
message: string;
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Heartbeat messages
|
|
121
|
+
*/
|
|
122
|
+
export interface PingMessage extends BaseMessage<'ping'> {
|
|
123
|
+
}
|
|
124
|
+
export interface PongMessage extends BaseMessage<'pong'> {
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Authentication messages
|
|
128
|
+
*/
|
|
129
|
+
export interface AuthRequestMessage extends BaseMessage<'auth'> {
|
|
130
|
+
payload: {
|
|
131
|
+
token: string;
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export interface AuthResponseMessage extends BaseMessage<'auth'> {
|
|
135
|
+
payload: {
|
|
136
|
+
success: boolean;
|
|
137
|
+
sessionId?: string;
|
|
138
|
+
error?: string;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Client -> Server: Subscribe to specific event types
|
|
143
|
+
*/
|
|
144
|
+
export interface SubscribeMessage extends BaseMessage<'subscribe'> {
|
|
145
|
+
payload: {
|
|
146
|
+
events: ('callback' | 'status' | 'sync')[];
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Connection acknowledged by server
|
|
151
|
+
*/
|
|
152
|
+
export interface ConnectedMessage extends BaseMessage<'connected'> {
|
|
153
|
+
payload: {
|
|
154
|
+
clientId: string;
|
|
155
|
+
serverTime: string;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Union type of all server -> client messages
|
|
160
|
+
*/
|
|
161
|
+
export type ServerMessage = SnapshotMessage | SynchronizedMessage | CallbackMessage | StatusMessage | PongMessage | ConnectedMessage | AuthResponseMessage;
|
|
162
|
+
/**
|
|
163
|
+
* Union type of all client -> server messages
|
|
164
|
+
*/
|
|
165
|
+
export type ClientMessage = AuthRequestMessage | PingMessage | SubscribeMessage;
|
|
30
166
|
export interface RelayMessage {
|
|
31
167
|
type: 'connect' | 'disconnect' | 'data' | 'ping' | 'pong' | 'error';
|
|
32
168
|
tunnelId?: string;
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Relay Types
|
|
2
|
+
* Relay Types - Two-way Streaming Communication
|
|
3
3
|
*/
|
|
4
|
-
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
// ============================================================================
|
|
6
|
+
// Typed Streaming Protocol
|
|
7
|
+
// ============================================================================
|
|
8
|
+
/**
|
|
9
|
+
* Protocol version for typed streaming
|
|
10
|
+
*/
|
|
11
|
+
export const PROTOCOL_VERSION = 1;
|
|
12
|
+
/**
|
|
13
|
+
* Generate a unique message ID
|
|
14
|
+
*/
|
|
15
|
+
export function generateMessageId() {
|
|
16
|
+
return randomUUID();
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Create a timestamp
|
|
20
|
+
*/
|
|
21
|
+
export function createTimestamp() {
|
|
22
|
+
return new Date().toISOString();
|
|
23
|
+
}
|
|
@@ -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
|
}
|