@love-moon/conductor-sdk 0.2.17 → 0.2.19
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/dist/backend/client.d.ts +1 -0
- package/dist/client.d.ts +3 -5
- package/dist/client.js +4 -1
- package/dist/message/router.d.ts +2 -0
- package/dist/message/router.js +18 -0
- package/dist/session/manager.d.ts +6 -0
- package/dist/session/manager.js +18 -0
- package/dist/ws/client.d.ts +41 -4
- package/dist/ws/client.js +189 -13
- package/package.json +2 -2
package/dist/backend/client.d.ts
CHANGED
package/dist/client.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { ConductorConfig } from './config/index.js';
|
|
|
3
3
|
import { MessageRouter } from './message/index.js';
|
|
4
4
|
import { DownstreamCursorStore, DurableUpstreamOutboxStore } from './outbox/index.js';
|
|
5
5
|
import { SessionDiskStore, SessionManager } from './session/index.js';
|
|
6
|
-
import { ConductorWebSocketClient } from './ws/index.js';
|
|
6
|
+
import { ConductorWebSocketClient, WebSocketConnectedEvent, WebSocketDisconnectEvent } from './ws/index.js';
|
|
7
7
|
type BackendApiLike = Pick<BackendApiClient, 'listProjects' | 'createProject' | 'listTasks' | 'createTask' | 'updateTask' | 'commitSdkMessage' | 'commitTaskStatusUpdate' | 'commitAgentCommandAck' | 'commitTaskStopAck' | 'matchProjectByPath' | 'getProject' | 'updateProject'>;
|
|
8
8
|
type RealtimeClientLike = Pick<ConductorWebSocketClient, 'registerHandler' | 'connect' | 'disconnect' | 'sendJson'>;
|
|
9
9
|
export interface ConductorClientConnectOptions {
|
|
@@ -21,10 +21,8 @@ export interface ConductorClientConnectOptions {
|
|
|
21
21
|
upstreamOutbox?: DurableUpstreamOutboxStore;
|
|
22
22
|
downstreamCursorStore?: DownstreamCursorStore;
|
|
23
23
|
agentHost?: string;
|
|
24
|
-
onConnected?: (event:
|
|
25
|
-
|
|
26
|
-
}) => void;
|
|
27
|
-
onDisconnected?: () => void;
|
|
24
|
+
onConnected?: (event: WebSocketConnectedEvent) => void;
|
|
25
|
+
onDisconnected?: (event: WebSocketDisconnectEvent) => void;
|
|
28
26
|
onStopTask?: (event: StopTaskEvent) => Promise<void> | void;
|
|
29
27
|
}
|
|
30
28
|
interface ConductorClientInit {
|
package/dist/client.js
CHANGED
|
@@ -5,7 +5,7 @@ import { getPlanLimitMessageFromError } from './limits/index.js';
|
|
|
5
5
|
import { MessageRouter } from './message/index.js';
|
|
6
6
|
import { DownstreamCursorStore, DurableUpstreamOutboxStore, normalizeDownstreamCommandCursor, } from './outbox/index.js';
|
|
7
7
|
import { SessionDiskStore, SessionManager, currentHostname } from './session/index.js';
|
|
8
|
-
import { ConductorWebSocketClient } from './ws/index.js';
|
|
8
|
+
import { ConductorWebSocketClient, } from './ws/index.js';
|
|
9
9
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
10
|
export class ConductorClient {
|
|
11
11
|
config;
|
|
@@ -131,6 +131,7 @@ export class ConductorClient {
|
|
|
131
131
|
id: taskId,
|
|
132
132
|
projectId,
|
|
133
133
|
title,
|
|
134
|
+
status: 'running',
|
|
134
135
|
backendType,
|
|
135
136
|
sessionId: logicalSessionId,
|
|
136
137
|
sessionFilePath: explicitSessionFilePath,
|
|
@@ -841,6 +842,8 @@ function formatMessagesResponse(messages) {
|
|
|
841
842
|
role: msg.role,
|
|
842
843
|
content: msg.content,
|
|
843
844
|
ack_token: msg.ackToken,
|
|
845
|
+
metadata: msg.metadata ?? undefined,
|
|
846
|
+
attachments: msg.attachments?.length ? msg.attachments : undefined,
|
|
844
847
|
created_at: msg.createdAt.toISOString(),
|
|
845
848
|
})),
|
|
846
849
|
next_ack_token: messages.length ? messages[messages.length - 1].ackToken ?? null : null,
|
package/dist/message/router.d.ts
CHANGED
package/dist/message/router.js
CHANGED
|
@@ -26,6 +26,8 @@ export class MessageRouter {
|
|
|
26
26
|
role: this.coerceRole(data.role, 'user'),
|
|
27
27
|
content: this.coerceContent(data.content),
|
|
28
28
|
ackToken: data.ack_token ? String(data.ack_token) : null,
|
|
29
|
+
metadata: this.coerceMetadata(data.metadata),
|
|
30
|
+
attachments: this.coerceAttachments(data.attachments),
|
|
29
31
|
});
|
|
30
32
|
await this.notify(taskId);
|
|
31
33
|
}
|
|
@@ -36,6 +38,8 @@ export class MessageRouter {
|
|
|
36
38
|
role: this.coerceRole(data.role, 'action'),
|
|
37
39
|
content: this.formatActionContent(data),
|
|
38
40
|
ackToken: data.ack_token ? String(data.ack_token) : null,
|
|
41
|
+
metadata: this.coerceMetadata(data.metadata),
|
|
42
|
+
attachments: this.coerceAttachments(data.attachments),
|
|
39
43
|
});
|
|
40
44
|
await this.notify(taskId);
|
|
41
45
|
}
|
|
@@ -92,6 +96,20 @@ export class MessageRouter {
|
|
|
92
96
|
return String(content);
|
|
93
97
|
}
|
|
94
98
|
}
|
|
99
|
+
coerceMetadata(metadata) {
|
|
100
|
+
if (!metadata || typeof metadata !== 'object' || Array.isArray(metadata)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return { ...metadata };
|
|
104
|
+
}
|
|
105
|
+
coerceAttachments(attachments) {
|
|
106
|
+
if (!Array.isArray(attachments)) {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
return attachments
|
|
110
|
+
.filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))
|
|
111
|
+
.map((entry) => ({ ...entry }));
|
|
112
|
+
}
|
|
95
113
|
formatActionContent(data) {
|
|
96
114
|
if (typeof data.content === 'string' && data.content.trim()) {
|
|
97
115
|
return data.content;
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
type JsonRecord = Record<string, unknown>;
|
|
1
2
|
export interface MessageInput {
|
|
2
3
|
messageId: string;
|
|
3
4
|
role: string;
|
|
4
5
|
content: string;
|
|
5
6
|
ackToken?: string | null;
|
|
7
|
+
metadata?: JsonRecord | null;
|
|
8
|
+
attachments?: JsonRecord[];
|
|
6
9
|
}
|
|
7
10
|
export declare class MessageRecord {
|
|
8
11
|
readonly messageId: string;
|
|
@@ -10,6 +13,8 @@ export declare class MessageRecord {
|
|
|
10
13
|
readonly content: string;
|
|
11
14
|
readonly createdAt: Date;
|
|
12
15
|
readonly ackToken?: string | null;
|
|
16
|
+
readonly metadata?: JsonRecord | null;
|
|
17
|
+
readonly attachments: JsonRecord[];
|
|
13
18
|
constructor(init: MessageInput);
|
|
14
19
|
}
|
|
15
20
|
export declare class SessionState {
|
|
@@ -37,3 +42,4 @@ export declare class SessionManager {
|
|
|
37
42
|
endSession(taskId: string): Promise<void>;
|
|
38
43
|
private ensureEvent;
|
|
39
44
|
}
|
|
45
|
+
export {};
|
package/dist/session/manager.js
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
function normalizeJsonRecord(value) {
|
|
3
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
return { ...value };
|
|
7
|
+
}
|
|
8
|
+
function normalizeJsonRecordArray(value) {
|
|
9
|
+
if (!Array.isArray(value)) {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
return value
|
|
13
|
+
.filter((entry) => Boolean(entry) && typeof entry === 'object' && !Array.isArray(entry))
|
|
14
|
+
.map((entry) => ({ ...entry }));
|
|
15
|
+
}
|
|
2
16
|
export class MessageRecord {
|
|
3
17
|
messageId;
|
|
4
18
|
role;
|
|
5
19
|
content;
|
|
6
20
|
createdAt;
|
|
7
21
|
ackToken;
|
|
22
|
+
metadata;
|
|
23
|
+
attachments;
|
|
8
24
|
constructor(init) {
|
|
9
25
|
this.messageId = init.messageId;
|
|
10
26
|
this.role = init.role;
|
|
11
27
|
this.content = init.content;
|
|
12
28
|
this.ackToken = init.ackToken;
|
|
29
|
+
this.metadata = normalizeJsonRecord(init.metadata);
|
|
30
|
+
this.attachments = normalizeJsonRecordArray(init.attachments);
|
|
13
31
|
this.createdAt = new Date();
|
|
14
32
|
}
|
|
15
33
|
}
|
package/dist/ws/client.d.ts
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { ConductorConfig } from '../config/index.js';
|
|
2
2
|
export type WebSocketHandler = (payload: Record<string, any>) => Promise<void> | void;
|
|
3
|
+
export interface WebSocketCloseInfo {
|
|
4
|
+
code?: number | null;
|
|
5
|
+
reason?: string | null;
|
|
6
|
+
}
|
|
3
7
|
export interface WebSocketLike {
|
|
4
8
|
send(data: string): Promise<void> | void;
|
|
5
9
|
ping(): Promise<void> | void;
|
|
6
10
|
close(): Promise<void> | void;
|
|
11
|
+
terminate?(): Promise<void> | void;
|
|
12
|
+
onPong?(handler: () => void): void;
|
|
13
|
+
onCloseInfo?(handler: (info: WebSocketCloseInfo) => void): void;
|
|
14
|
+
onErrorInfo?(handler: (error: unknown) => void): void;
|
|
7
15
|
closed?: boolean;
|
|
8
16
|
[Symbol.asyncIterator](): AsyncIterableIterator<string>;
|
|
9
17
|
}
|
|
@@ -11,16 +19,35 @@ export interface ConnectOptions {
|
|
|
11
19
|
headers: Record<string, string>;
|
|
12
20
|
}
|
|
13
21
|
export type ConnectImpl = (url: string, options: ConnectOptions) => Promise<WebSocketLike>;
|
|
22
|
+
export interface WebSocketConnectedEvent {
|
|
23
|
+
isReconnect: boolean;
|
|
24
|
+
connectedAt: number;
|
|
25
|
+
}
|
|
26
|
+
export interface WebSocketPongEvent {
|
|
27
|
+
at: number;
|
|
28
|
+
latencyMs: number | null;
|
|
29
|
+
}
|
|
30
|
+
export interface WebSocketDisconnectEvent {
|
|
31
|
+
reason: string;
|
|
32
|
+
disconnectedAt: number;
|
|
33
|
+
connectedAt: number | null;
|
|
34
|
+
closeCode: number | null;
|
|
35
|
+
closeReason: string | null;
|
|
36
|
+
socketError: string | null;
|
|
37
|
+
missedPongs: number;
|
|
38
|
+
lastPingAt: number | null;
|
|
39
|
+
lastPongAt: number | null;
|
|
40
|
+
lastMessageAt: number | null;
|
|
41
|
+
}
|
|
14
42
|
export interface WebSocketClientOptions {
|
|
15
43
|
reconnectDelay?: number;
|
|
16
44
|
heartbeatInterval?: number;
|
|
17
45
|
extraHeaders?: Record<string, string>;
|
|
18
46
|
connectImpl?: ConnectImpl;
|
|
19
47
|
hostName?: string;
|
|
20
|
-
onConnected?: (event:
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
onDisconnected?: () => void;
|
|
48
|
+
onConnected?: (event: WebSocketConnectedEvent) => void;
|
|
49
|
+
onDisconnected?: (event: WebSocketDisconnectEvent) => void;
|
|
50
|
+
onPong?: (event: WebSocketPongEvent) => void;
|
|
24
51
|
onReconnected?: () => void;
|
|
25
52
|
}
|
|
26
53
|
export declare class ConductorWebSocketClient {
|
|
@@ -31,10 +58,12 @@ export declare class ConductorWebSocketClient {
|
|
|
31
58
|
private readonly connectImpl;
|
|
32
59
|
private readonly onConnected?;
|
|
33
60
|
private readonly onDisconnected?;
|
|
61
|
+
private readonly onPong?;
|
|
34
62
|
private readonly onReconnected?;
|
|
35
63
|
private readonly handlers;
|
|
36
64
|
private readonly extraHeaders;
|
|
37
65
|
private conn;
|
|
66
|
+
private runtime;
|
|
38
67
|
private stop;
|
|
39
68
|
private listenTask;
|
|
40
69
|
private heartbeatTask;
|
|
@@ -45,6 +74,7 @@ export declare class ConductorWebSocketClient {
|
|
|
45
74
|
registerHandler(handler: WebSocketHandler): void;
|
|
46
75
|
connect(): Promise<void>;
|
|
47
76
|
disconnect(): Promise<void>;
|
|
77
|
+
forceReconnect(reason?: string): Promise<void>;
|
|
48
78
|
sendJson(payload: Record<string, any>): Promise<void>;
|
|
49
79
|
private ensureConnection;
|
|
50
80
|
private openConnection;
|
|
@@ -59,4 +89,11 @@ export declare class ConductorWebSocketClient {
|
|
|
59
89
|
private isConnectionClosed;
|
|
60
90
|
private sendWithReconnect;
|
|
61
91
|
private isNotOpenError;
|
|
92
|
+
private createRuntime;
|
|
93
|
+
private attachConnectionObservers;
|
|
94
|
+
private getRuntime;
|
|
95
|
+
private markDisconnectReason;
|
|
96
|
+
private buildDisconnectEvent;
|
|
97
|
+
private closeConnection;
|
|
98
|
+
private terminateConnection;
|
|
62
99
|
}
|
package/dist/ws/client.js
CHANGED
|
@@ -16,10 +16,12 @@ export class ConductorWebSocketClient {
|
|
|
16
16
|
connectImpl;
|
|
17
17
|
onConnected;
|
|
18
18
|
onDisconnected;
|
|
19
|
+
onPong;
|
|
19
20
|
onReconnected;
|
|
20
21
|
handlers = [];
|
|
21
22
|
extraHeaders;
|
|
22
23
|
conn = null;
|
|
24
|
+
runtime = null;
|
|
23
25
|
stop = false;
|
|
24
26
|
listenTask = null;
|
|
25
27
|
heartbeatTask = null;
|
|
@@ -38,6 +40,7 @@ export class ConductorWebSocketClient {
|
|
|
38
40
|
this.connectImpl = options.connectImpl ?? defaultConnectImpl;
|
|
39
41
|
this.onConnected = options.onConnected;
|
|
40
42
|
this.onDisconnected = options.onDisconnected;
|
|
43
|
+
this.onPong = options.onPong;
|
|
41
44
|
this.onReconnected = options.onReconnected;
|
|
42
45
|
}
|
|
43
46
|
registerHandler(handler) {
|
|
@@ -60,9 +63,20 @@ export class ConductorWebSocketClient {
|
|
|
60
63
|
this.heartbeatTask = null;
|
|
61
64
|
}
|
|
62
65
|
if (this.conn && !this.isConnectionClosed(this.conn)) {
|
|
63
|
-
|
|
66
|
+
this.markDisconnectReason(this.conn, 'manual_disconnect');
|
|
67
|
+
await this.closeConnection(this.conn);
|
|
64
68
|
}
|
|
65
69
|
this.conn = null;
|
|
70
|
+
this.runtime = null;
|
|
71
|
+
}
|
|
72
|
+
async forceReconnect(reason = 'manual_reconnect') {
|
|
73
|
+
const conn = this.conn;
|
|
74
|
+
if (!conn || this.isConnectionClosed(conn)) {
|
|
75
|
+
await this.openConnection(true);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.markDisconnectReason(conn, reason);
|
|
79
|
+
await this.terminateConnection(conn);
|
|
66
80
|
}
|
|
67
81
|
async sendJson(payload) {
|
|
68
82
|
await this.ensureConnection();
|
|
@@ -83,15 +97,19 @@ export class ConductorWebSocketClient {
|
|
|
83
97
|
while (!this.stop) {
|
|
84
98
|
try {
|
|
85
99
|
const headers = { Authorization: `Bearer ${this.token}`, ...this.extraHeaders };
|
|
86
|
-
|
|
100
|
+
const conn = await this.connectImpl(this.url, { headers });
|
|
101
|
+
const runtime = this.createRuntime(conn);
|
|
102
|
+
this.attachConnectionObservers(runtime);
|
|
103
|
+
this.conn = conn;
|
|
104
|
+
this.runtime = runtime;
|
|
87
105
|
const isReconnect = this.hasConnectedAtLeastOnce;
|
|
88
106
|
this.hasConnectedAtLeastOnce = true;
|
|
89
|
-
this.notifyConnected(isReconnect);
|
|
107
|
+
this.notifyConnected({ isReconnect, connectedAt: runtime.connectedAt });
|
|
90
108
|
if (isReconnect) {
|
|
91
109
|
this.notifyReconnected();
|
|
92
110
|
}
|
|
93
|
-
this.listenTask = this.listenLoop(
|
|
94
|
-
this.heartbeatTask = this.heartbeatLoop(
|
|
111
|
+
this.listenTask = this.listenLoop(conn);
|
|
112
|
+
this.heartbeatTask = this.heartbeatLoop(conn);
|
|
95
113
|
return;
|
|
96
114
|
}
|
|
97
115
|
catch (error) {
|
|
@@ -124,10 +142,26 @@ export class ConductorWebSocketClient {
|
|
|
124
142
|
try {
|
|
125
143
|
while (!this.stop && !this.isConnectionClosed(conn)) {
|
|
126
144
|
await wait(this.heartbeatInterval, this.waitController.signal);
|
|
145
|
+
if (this.stop || conn !== this.conn) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const runtime = this.getRuntime(conn);
|
|
149
|
+
if (!runtime) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (runtime.supportsPongTracking && runtime.waitingForPong) {
|
|
153
|
+
runtime.missedPongs += 1;
|
|
154
|
+
runtime.disconnectReason = 'pong_timeout';
|
|
155
|
+
await this.terminateConnection(conn);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
runtime.lastPingAt = Date.now();
|
|
159
|
+
runtime.waitingForPong = runtime.supportsPongTracking;
|
|
127
160
|
try {
|
|
128
161
|
await conn.ping();
|
|
129
162
|
}
|
|
130
163
|
catch {
|
|
164
|
+
this.markDisconnectReason(conn, 'ping_failed');
|
|
131
165
|
break;
|
|
132
166
|
}
|
|
133
167
|
}
|
|
@@ -140,11 +174,17 @@ export class ConductorWebSocketClient {
|
|
|
140
174
|
if (this.stop || conn !== this.conn) {
|
|
141
175
|
return;
|
|
142
176
|
}
|
|
177
|
+
const runtime = this.getRuntime(conn);
|
|
143
178
|
this.conn = null;
|
|
144
|
-
this.
|
|
179
|
+
this.runtime = null;
|
|
180
|
+
this.notifyDisconnected(this.buildDisconnectEvent(runtime));
|
|
145
181
|
await this.openConnection(true);
|
|
146
182
|
}
|
|
147
183
|
async dispatch(message) {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
if (this.runtime) {
|
|
186
|
+
this.runtime.lastMessageAt = now;
|
|
187
|
+
}
|
|
148
188
|
let payload;
|
|
149
189
|
try {
|
|
150
190
|
payload = JSON.parse(message);
|
|
@@ -159,12 +199,12 @@ export class ConductorWebSocketClient {
|
|
|
159
199
|
}
|
|
160
200
|
}
|
|
161
201
|
}
|
|
162
|
-
notifyConnected(
|
|
202
|
+
notifyConnected(event) {
|
|
163
203
|
if (!this.onConnected) {
|
|
164
204
|
return;
|
|
165
205
|
}
|
|
166
206
|
try {
|
|
167
|
-
this.onConnected(
|
|
207
|
+
this.onConnected(event);
|
|
168
208
|
}
|
|
169
209
|
catch {
|
|
170
210
|
// Swallow callback errors to avoid impacting reconnect behavior.
|
|
@@ -181,12 +221,12 @@ export class ConductorWebSocketClient {
|
|
|
181
221
|
// Swallow callback errors to avoid impacting reconnect behavior.
|
|
182
222
|
}
|
|
183
223
|
}
|
|
184
|
-
notifyDisconnected() {
|
|
224
|
+
notifyDisconnected(event) {
|
|
185
225
|
if (!this.onDisconnected) {
|
|
186
226
|
return;
|
|
187
227
|
}
|
|
188
228
|
try {
|
|
189
|
-
this.onDisconnected();
|
|
229
|
+
this.onDisconnected(event);
|
|
190
230
|
}
|
|
191
231
|
catch {
|
|
192
232
|
// Swallow callback errors to avoid impacting reconnect behavior.
|
|
@@ -222,8 +262,10 @@ export class ConductorWebSocketClient {
|
|
|
222
262
|
}
|
|
223
263
|
attemptedReconnect = true;
|
|
224
264
|
if (this.conn === conn) {
|
|
265
|
+
this.markDisconnectReason(conn, 'send_reconnect');
|
|
225
266
|
this.conn = null;
|
|
226
|
-
this.notifyDisconnected();
|
|
267
|
+
this.notifyDisconnected(this.buildDisconnectEvent(this.getRuntime(conn)));
|
|
268
|
+
this.runtime = null;
|
|
227
269
|
}
|
|
228
270
|
await this.openConnection(true);
|
|
229
271
|
}
|
|
@@ -235,6 +277,100 @@ export class ConductorWebSocketClient {
|
|
|
235
277
|
const message = error instanceof Error ? error.message : String(error);
|
|
236
278
|
return message.toLowerCase().includes('websocket is not open');
|
|
237
279
|
}
|
|
280
|
+
createRuntime(conn) {
|
|
281
|
+
const connectedAt = Date.now();
|
|
282
|
+
return {
|
|
283
|
+
conn,
|
|
284
|
+
connectedAt,
|
|
285
|
+
supportsPongTracking: typeof conn.onPong === 'function',
|
|
286
|
+
lastMessageAt: null,
|
|
287
|
+
lastPingAt: null,
|
|
288
|
+
lastPongAt: connectedAt,
|
|
289
|
+
waitingForPong: false,
|
|
290
|
+
missedPongs: 0,
|
|
291
|
+
disconnectReason: null,
|
|
292
|
+
closeCode: null,
|
|
293
|
+
closeReason: null,
|
|
294
|
+
socketError: null,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
attachConnectionObservers(runtime) {
|
|
298
|
+
runtime.conn.onPong?.(() => {
|
|
299
|
+
const activeRuntime = this.getRuntime(runtime.conn);
|
|
300
|
+
if (!activeRuntime) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const at = Date.now();
|
|
304
|
+
const latencyMs = activeRuntime.lastPingAt ? at - activeRuntime.lastPingAt : null;
|
|
305
|
+
activeRuntime.lastPongAt = at;
|
|
306
|
+
activeRuntime.waitingForPong = false;
|
|
307
|
+
activeRuntime.missedPongs = 0;
|
|
308
|
+
if (this.onPong) {
|
|
309
|
+
try {
|
|
310
|
+
this.onPong({ at, latencyMs });
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// Swallow callback errors to avoid impacting reconnect behavior.
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
runtime.conn.onCloseInfo?.((info) => {
|
|
318
|
+
const activeRuntime = this.getRuntime(runtime.conn);
|
|
319
|
+
if (!activeRuntime) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
activeRuntime.closeCode = typeof info.code === 'number' ? info.code : null;
|
|
323
|
+
activeRuntime.closeReason = normalizeOptionalString(info.reason);
|
|
324
|
+
});
|
|
325
|
+
runtime.conn.onErrorInfo?.((error) => {
|
|
326
|
+
const activeRuntime = this.getRuntime(runtime.conn);
|
|
327
|
+
if (!activeRuntime) {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
activeRuntime.socketError = error instanceof Error ? error.message : String(error);
|
|
331
|
+
if (!activeRuntime.disconnectReason) {
|
|
332
|
+
activeRuntime.disconnectReason = 'socket_error';
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
getRuntime(conn) {
|
|
337
|
+
if (!this.runtime || this.runtime.conn !== conn) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
return this.runtime;
|
|
341
|
+
}
|
|
342
|
+
markDisconnectReason(conn, reason) {
|
|
343
|
+
const runtime = this.getRuntime(conn);
|
|
344
|
+
if (runtime && !runtime.disconnectReason) {
|
|
345
|
+
runtime.disconnectReason = reason;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
buildDisconnectEvent(runtime) {
|
|
349
|
+
return {
|
|
350
|
+
reason: runtime?.disconnectReason || 'connection_lost',
|
|
351
|
+
disconnectedAt: Date.now(),
|
|
352
|
+
connectedAt: runtime?.connectedAt ?? null,
|
|
353
|
+
closeCode: runtime?.closeCode ?? null,
|
|
354
|
+
closeReason: runtime?.closeReason ?? null,
|
|
355
|
+
socketError: runtime?.socketError ?? null,
|
|
356
|
+
missedPongs: runtime?.missedPongs ?? 0,
|
|
357
|
+
lastPingAt: runtime?.lastPingAt ?? null,
|
|
358
|
+
lastPongAt: runtime?.lastPongAt ?? null,
|
|
359
|
+
lastMessageAt: runtime?.lastMessageAt ?? null,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
async closeConnection(conn) {
|
|
363
|
+
if (typeof conn.close === 'function') {
|
|
364
|
+
await conn.close();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
async terminateConnection(conn) {
|
|
368
|
+
if (typeof conn.terminate === 'function') {
|
|
369
|
+
await conn.terminate();
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
await this.closeConnection(conn);
|
|
373
|
+
}
|
|
238
374
|
}
|
|
239
375
|
async function wait(ms, signal) {
|
|
240
376
|
if (!signal) {
|
|
@@ -276,16 +412,34 @@ class WsAdapter {
|
|
|
276
412
|
ws;
|
|
277
413
|
queue = [];
|
|
278
414
|
waiters = [];
|
|
415
|
+
pongHandlers = new Set();
|
|
416
|
+
closeInfoHandlers = new Set();
|
|
417
|
+
errorHandlers = new Set();
|
|
279
418
|
closed = false;
|
|
280
419
|
constructor(ws) {
|
|
281
420
|
this.ws = ws;
|
|
282
421
|
ws.on('message', (data) => this.enqueue(data.toString()));
|
|
283
|
-
ws.on('
|
|
422
|
+
ws.on('pong', () => {
|
|
423
|
+
for (const handler of this.pongHandlers) {
|
|
424
|
+
handler();
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
ws.on('close', (code, reason) => {
|
|
284
428
|
this.closed = true;
|
|
429
|
+
const closeInfo = {
|
|
430
|
+
code,
|
|
431
|
+
reason: normalizeOptionalString(reason?.toString()),
|
|
432
|
+
};
|
|
433
|
+
for (const handler of this.closeInfoHandlers) {
|
|
434
|
+
handler(closeInfo);
|
|
435
|
+
}
|
|
285
436
|
this.enqueue(null);
|
|
286
437
|
});
|
|
287
|
-
ws.on('error', () => {
|
|
438
|
+
ws.on('error', (error) => {
|
|
288
439
|
this.closed = true;
|
|
440
|
+
for (const handler of this.errorHandlers) {
|
|
441
|
+
handler(error);
|
|
442
|
+
}
|
|
289
443
|
this.enqueue(null);
|
|
290
444
|
});
|
|
291
445
|
}
|
|
@@ -323,6 +477,21 @@ class WsAdapter {
|
|
|
323
477
|
this.ws.close();
|
|
324
478
|
});
|
|
325
479
|
}
|
|
480
|
+
terminate() {
|
|
481
|
+
if (this.closed) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
this.ws.terminate();
|
|
485
|
+
}
|
|
486
|
+
onPong(handler) {
|
|
487
|
+
this.pongHandlers.add(handler);
|
|
488
|
+
}
|
|
489
|
+
onCloseInfo(handler) {
|
|
490
|
+
this.closeInfoHandlers.add(handler);
|
|
491
|
+
}
|
|
492
|
+
onErrorInfo(handler) {
|
|
493
|
+
this.errorHandlers.add(handler);
|
|
494
|
+
}
|
|
326
495
|
async *[Symbol.asyncIterator]() {
|
|
327
496
|
while (true) {
|
|
328
497
|
const value = await this.nextValue();
|
|
@@ -351,3 +520,10 @@ class WsAdapter {
|
|
|
351
520
|
}
|
|
352
521
|
}
|
|
353
522
|
}
|
|
523
|
+
function normalizeOptionalString(value) {
|
|
524
|
+
if (typeof value !== 'string') {
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
const trimmed = value.trim();
|
|
528
|
+
return trimmed || null;
|
|
529
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@love-moon/conductor-sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,5 +27,5 @@
|
|
|
27
27
|
"typescript": "^5.6.3",
|
|
28
28
|
"vitest": "^2.1.4"
|
|
29
29
|
},
|
|
30
|
-
"gitCommitId": "
|
|
30
|
+
"gitCommitId": "346e048"
|
|
31
31
|
}
|