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.
Files changed (123) hide show
  1. package/package.json +1 -1
  2. package/packages/a2a-adapter/package.json +1 -1
  3. package/packages/agent-policy/package.json +1 -1
  4. package/packages/architecture-generator/package.json +1 -1
  5. package/packages/auth/package.json +1 -1
  6. package/packages/autonomous-refactor/package.json +1 -1
  7. package/packages/autonomous-runtime/package.json +1 -1
  8. package/packages/cache-strategy/package.json +1 -1
  9. package/packages/capability-registry/package.json +1 -1
  10. package/packages/checkpoint/package.json +1 -1
  11. package/packages/cli-plugin-sdk/package.json +1 -1
  12. package/packages/clipboard/package.json +1 -1
  13. package/packages/clipboard-plugin/package.json +1 -1
  14. package/packages/code-generation/package.json +1 -1
  15. package/packages/code-review/package.json +1 -1
  16. package/packages/codex-adapter/package.json +1 -1
  17. package/packages/codex-watcher/package.json +1 -1
  18. package/packages/config-capture/package.json +1 -1
  19. package/packages/connection-runtime/package.json +1 -1
  20. package/packages/context-compiler/package.json +1 -1
  21. package/packages/context-discovery/package.json +1 -1
  22. package/packages/context-manager/package.json +1 -1
  23. package/packages/contracts/package.json +1 -1
  24. package/packages/cookie-sanitizer/package.json +1 -1
  25. package/packages/cost-optimizer/package.json +1 -1
  26. package/packages/dependency-analyzer/package.json +1 -1
  27. package/packages/django-plugin/package.json +1 -1
  28. package/packages/doc-generator/package.json +1 -1
  29. package/packages/durable-review/package.json +1 -1
  30. package/packages/e2e-tester/package.json +1 -1
  31. package/packages/evaluation-engine/package.json +1 -1
  32. package/packages/evaluation-runner/package.json +1 -1
  33. package/packages/event-bus/package.json +1 -1
  34. package/packages/event-sink/package.json +1 -1
  35. package/packages/event-store/package.json +1 -1
  36. package/packages/experience-replay/package.json +1 -1
  37. package/packages/feedback-collector/package.json +1 -1
  38. package/packages/file-copy-helper/package.json +1 -1
  39. package/packages/framework-detector/package.json +1 -1
  40. package/packages/framework-plugin-sdk/package.json +1 -1
  41. package/packages/frappe-plugin/package.json +1 -1
  42. package/packages/generic-web-plugin/package.json +1 -1
  43. package/packages/health-monitor/package.json +1 -1
  44. package/packages/intent-analyzer/package.json +1 -1
  45. package/packages/jev-judge/package.json +1 -1
  46. package/packages/knowledge-graph/package.json +1 -1
  47. package/packages/knowledge-retrieval/package.json +1 -1
  48. package/packages/laravel-plugin/package.json +1 -1
  49. package/packages/learning-engine/package.json +1 -1
  50. package/packages/mcp-adapter/package.json +1 -1
  51. package/packages/memory-engine/package.json +1 -1
  52. package/packages/milestone-manager/package.json +1 -1
  53. package/packages/model-registry/package.json +1 -1
  54. package/packages/nextjs-plugin/package.json +1 -1
  55. package/packages/notification/package.json +1 -1
  56. package/packages/observability/package.json +1 -1
  57. package/packages/okf-indexer/package.json +1 -1
  58. package/packages/paperclip-integration/package.json +1 -1
  59. package/packages/performance-optimizer/package.json +1 -1
  60. package/packages/permission-mode/package.json +1 -1
  61. package/packages/privilege-broker/package.json +1 -1
  62. package/packages/project-analyzer/package.json +1 -1
  63. package/packages/project-bootstrap/package.json +1 -1
  64. package/packages/projection-engine/package.json +1 -1
  65. package/packages/prompt-compiler/package.json +1 -1
  66. package/packages/prompt-versioning/package.json +1 -1
  67. package/packages/provider-adapter-sdk/package.json +1 -1
  68. package/packages/provider-router/package.json +1 -1
  69. package/packages/provider-selector/package.json +1 -1
  70. package/packages/providers/package.json +1 -1
  71. package/packages/qdrant-skills/package.json +1 -1
  72. package/packages/quota-manager/package.json +1 -1
  73. package/packages/rate-limiter/package.json +1 -1
  74. package/packages/react-vite-plugin/package.json +1 -1
  75. package/packages/relay/dist/client.d.ts +56 -35
  76. package/packages/relay/dist/client.js +278 -71
  77. package/packages/relay/dist/index.d.ts +12 -5
  78. package/packages/relay/dist/index.js +12 -4
  79. package/packages/relay/dist/server.d.ts +58 -20
  80. package/packages/relay/dist/server.js +344 -49
  81. package/packages/relay/dist/types.d.ts +137 -1
  82. package/packages/relay/dist/types.js +21 -2
  83. package/packages/relay/package.json +1 -1
  84. package/packages/relay/src/client.ts +375 -81
  85. package/packages/relay/src/index.ts +42 -5
  86. package/packages/relay/src/server.ts +452 -54
  87. package/packages/relay/src/types.ts +174 -1
  88. package/packages/release-manager/package.json +1 -1
  89. package/packages/requirement-compiler/package.json +1 -1
  90. package/packages/runtime/package.json +1 -1
  91. package/packages/scheduler/package.json +1 -1
  92. package/packages/scheduler-adapter/package.json +1 -1
  93. package/packages/session/package.json +1 -1
  94. package/packages/session-api/package.json +1 -1
  95. package/packages/session-export/package.json +1 -1
  96. package/packages/shared-context/package.json +1 -1
  97. package/packages/skill-mcp-client/package.json +1 -1
  98. package/packages/skill-registry/package.json +1 -1
  99. package/packages/skills/package.json +1 -1
  100. package/packages/sprint-planner/package.json +1 -1
  101. package/packages/subscription-engine/package.json +1 -1
  102. package/packages/task-compiler/package.json +1 -1
  103. package/packages/telegram/package.json +1 -1
  104. package/packages/tencentdb-memory/package.json +1 -1
  105. package/packages/tencentdb-sync/package.json +1 -1
  106. package/packages/test-data-generator/package.json +1 -1
  107. package/packages/test-generator/package.json +1 -1
  108. package/packages/todo-bd-sync/package.json +1 -1
  109. package/packages/token-estimation/package.json +1 -1
  110. package/packages/token-optimizer/package.json +1 -1
  111. package/packages/trajectory/package.json +1 -1
  112. package/packages/tui/package.json +1 -1
  113. package/packages/types/package.json +1 -1
  114. package/packages/voice-api/package.json +1 -1
  115. package/packages/webmcp-tester/package.json +1 -1
  116. package/packages/wiki/package.json +1 -1
  117. package/packages/workflow-events/package.json +1 -1
  118. package/packages/workspace-scanner/package.json +1 -1
  119. package/packages/worktree/package.json +1 -1
  120. package/packages/write-review/package.json +1 -1
  121. package/scripts/relay-client.ts +105 -96
  122. package/scripts/unified-webhook-server.ts +145 -143
  123. package/scripts/ws-server.ts +81 -0
@@ -1,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
- export {};
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-harness/relay",
3
- "version": "1.1.376",
3
+ "version": "1.1.377",
4
4
  "type": "module",
5
5
  "description": "Cloud relay for remote access - T3 Connect style for pi-harness",
6
6
  "main": "./dist/index.js",
@@ -1,119 +1,413 @@
1
1
  /**
2
- * Relay Client - Connect to relay server for remote access
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 type { RelayConfig, RelayTunnel, RelayMessage } from './types.js';
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
- * Generate a UUID v4
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
- * Relay Client options
26
- */
27
- export interface RelayClientOptions {
28
- /** Called when connected */
29
- onConnected?: () => void;
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 tunnel is established */
33
- onTunnel?: (tunnel: RelayTunnel) => void;
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
- * Relay Client
40
- *
41
- * Connects to a relay server and establishes tunnels for remote access.
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 options: RelayClientOptions;
46
- private connected = false;
47
- private tunnel?: RelayTunnel;
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, options: RelayClientOptions) {
91
+ constructor(config: RelayConfig, callbacks: RelayClientCallbacks = {}) {
50
92
  this.config = config;
51
- this.options = options;
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.connected) return;
59
-
60
- try {
61
- // In a real implementation, this would establish WebSocket connection
62
- // to the relay server and authenticate
63
- this.connected = true;
64
- this.options.onConnected?.();
65
- } catch (error) {
66
- this.options.onError?.(error as Error);
67
- throw error;
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
- async disconnect(): Promise<void> {
75
- this.connected = false;
76
- this.tunnel = undefined;
77
- this.options.onDisconnected?.();
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
- * Create a tunnel
192
+ * Send a message through the WebSocket
82
193
  */
83
- async createTunnel(localPort: number): Promise<RelayTunnel> {
84
- const tunnel: RelayTunnel = {
85
- id: generateUUID(),
86
- url: `${this.config.serverUrl}/tunnel/${localPort}`,
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
- * Send a message through the tunnel
98
- */
99
- async send(_message: RelayMessage): Promise<void> {
100
- if (!this.connected) {
101
- throw new Error('Not connected to relay server');
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
- * Check if connected
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
- * Get current tunnel
115
- */
116
- getTunnel(): RelayTunnel | undefined {
117
- return this.tunnel;
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
  }