@pingagent/sdk 0.1.6 → 0.1.8
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/chunk-4W44SPLT.js +1459 -0
- package/dist/chunk-GBG3JF75.js +1454 -0
- package/dist/chunk-OVLKR4JF.js +1500 -0
- package/dist/chunk-SHTDIRC6.js +1455 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +1 -1
- package/dist/web-server.js +1 -1
- package/package.json +1 -1
- package/src/ws-subscription.ts +76 -9
package/dist/index.d.ts
CHANGED
|
@@ -486,8 +486,19 @@ interface WsSubscriptionOptions {
|
|
|
486
486
|
onMessage: (envelope: any, conversationId: string) => void;
|
|
487
487
|
onControl?: (control: WsControlPayload, conversationId: string) => void;
|
|
488
488
|
onError?: (err: Error) => void;
|
|
489
|
+
/** Optional debug hook for observability (non-fatal events). */
|
|
490
|
+
onDebug?: (info: {
|
|
491
|
+
event: string;
|
|
492
|
+
conversationId?: string;
|
|
493
|
+
detail?: Record<string, unknown>;
|
|
494
|
+
}) => void;
|
|
489
495
|
/** Called when a WebSocket opens or reopens; e.g. Skill can run catchUp to fill gaps. */
|
|
490
496
|
onOpen?: (conversationId: string) => void;
|
|
497
|
+
/**
|
|
498
|
+
* When true (default), drop ws_message envelopes sent by myself (sender_did === myDid).
|
|
499
|
+
* Set to false when you intentionally run multiple devices/processes under the same DID and want cross-device delivery.
|
|
500
|
+
*/
|
|
501
|
+
ignoreSelfMessages?: boolean;
|
|
491
502
|
/**
|
|
492
503
|
* Keepalive heartbeat for idle connections to reduce "WS silently dropped" periods.
|
|
493
504
|
* This is client-side only (WS ping/pong control frames) and does not add inbox HTTP fetch cost.
|
|
@@ -517,6 +528,8 @@ declare class WsSubscription {
|
|
|
517
528
|
private stopped;
|
|
518
529
|
/** Conversation IDs that were explicitly stopped (e.g. revoke); do not reconnect. */
|
|
519
530
|
private stoppedConversations;
|
|
531
|
+
private lastParseErrorAtByConv;
|
|
532
|
+
private lastFrameAtByConv;
|
|
520
533
|
private heartbeatTimer;
|
|
521
534
|
private heartbeatStates;
|
|
522
535
|
constructor(opts: WsSubscriptionOptions);
|
|
@@ -531,6 +544,7 @@ declare class WsSubscription {
|
|
|
531
544
|
private connectAsync;
|
|
532
545
|
private heartbeatTick;
|
|
533
546
|
private scheduleReconnect;
|
|
547
|
+
private isSubscribableConversationType;
|
|
534
548
|
private connectAll;
|
|
535
549
|
private syncConnections;
|
|
536
550
|
}
|
package/dist/index.js
CHANGED
package/dist/web-server.js
CHANGED
package/package.json
CHANGED
package/src/ws-subscription.ts
CHANGED
|
@@ -19,8 +19,15 @@ export interface WsSubscriptionOptions {
|
|
|
19
19
|
onMessage: (envelope: any, conversationId: string) => void;
|
|
20
20
|
onControl?: (control: WsControlPayload, conversationId: string) => void;
|
|
21
21
|
onError?: (err: Error) => void;
|
|
22
|
+
/** Optional debug hook for observability (non-fatal events). */
|
|
23
|
+
onDebug?: (info: { event: string; conversationId?: string; detail?: Record<string, unknown> }) => void;
|
|
22
24
|
/** Called when a WebSocket opens or reopens; e.g. Skill can run catchUp to fill gaps. */
|
|
23
25
|
onOpen?: (conversationId: string) => void;
|
|
26
|
+
/**
|
|
27
|
+
* When true (default), drop ws_message envelopes sent by myself (sender_did === myDid).
|
|
28
|
+
* Set to false when you intentionally run multiple devices/processes under the same DID and want cross-device delivery.
|
|
29
|
+
*/
|
|
30
|
+
ignoreSelfMessages?: boolean;
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
33
|
* Keepalive heartbeat for idle connections to reduce "WS silently dropped" periods.
|
|
@@ -67,6 +74,8 @@ export class WsSubscription {
|
|
|
67
74
|
private stopped = false;
|
|
68
75
|
/** Conversation IDs that were explicitly stopped (e.g. revoke); do not reconnect. */
|
|
69
76
|
private stoppedConversations = new Set<string>();
|
|
77
|
+
private lastParseErrorAtByConv = new Map<string, number>();
|
|
78
|
+
private lastFrameAtByConv = new Map<string, number>();
|
|
70
79
|
|
|
71
80
|
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
|
|
72
81
|
private heartbeatStates = new Map<
|
|
@@ -119,6 +128,8 @@ export class WsSubscription {
|
|
|
119
128
|
}
|
|
120
129
|
this.reconnectAttempts.clear();
|
|
121
130
|
this.stoppedConversations.clear();
|
|
131
|
+
this.lastParseErrorAtByConv.clear();
|
|
132
|
+
this.lastFrameAtByConv.clear();
|
|
122
133
|
|
|
123
134
|
for (const state of this.heartbeatStates.values()) {
|
|
124
135
|
if (state.pongTimeout) clearTimeout(state.pongTimeout);
|
|
@@ -150,6 +161,8 @@ export class WsSubscription {
|
|
|
150
161
|
}
|
|
151
162
|
this.connections.delete(conversationId);
|
|
152
163
|
}
|
|
164
|
+
this.lastParseErrorAtByConv.delete(conversationId);
|
|
165
|
+
this.lastFrameAtByConv.delete(conversationId);
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
private wsUrl(conversationId: string): string {
|
|
@@ -187,7 +200,7 @@ export class WsSubscription {
|
|
|
187
200
|
// ws_connected will arrive as first message
|
|
188
201
|
});
|
|
189
202
|
|
|
190
|
-
ws.on('message', (data:
|
|
203
|
+
ws.on('message', (data: any) => {
|
|
191
204
|
try {
|
|
192
205
|
// Any server frame counts as activity; keep connection stable and avoid unnecessary pings.
|
|
193
206
|
const state = this.heartbeatStates.get(conversationId);
|
|
@@ -200,18 +213,67 @@ export class WsSubscription {
|
|
|
200
213
|
}
|
|
201
214
|
}
|
|
202
215
|
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
const rawText =
|
|
217
|
+
typeof data === 'string'
|
|
218
|
+
? data
|
|
219
|
+
: Buffer.isBuffer(data)
|
|
220
|
+
? data.toString()
|
|
221
|
+
: Array.isArray(data)
|
|
222
|
+
? Buffer.concat(data).toString()
|
|
223
|
+
: data instanceof ArrayBuffer
|
|
224
|
+
? Buffer.from(new Uint8Array(data)).toString()
|
|
225
|
+
: // Fallback: try best-effort stringification
|
|
226
|
+
String(data);
|
|
227
|
+
|
|
228
|
+
this.lastFrameAtByConv.set(conversationId, Date.now());
|
|
229
|
+
|
|
230
|
+
const msg = JSON.parse(rawText);
|
|
231
|
+
|
|
232
|
+
if (msg.type === 'ws_connected') {
|
|
233
|
+
this.opts.onDebug?.({
|
|
234
|
+
event: 'ws_connected',
|
|
235
|
+
conversationId,
|
|
236
|
+
detail: {
|
|
237
|
+
your_did: msg.your_did,
|
|
238
|
+
server_ts_ms: msg.server_ts_ms,
|
|
239
|
+
conversation_id: msg.conversation_id,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
} else if (msg.type === 'ws_message' && msg.envelope) {
|
|
206
243
|
const env = msg.envelope;
|
|
207
|
-
|
|
244
|
+
const ignoreSelf = this.opts.ignoreSelfMessages ?? true;
|
|
245
|
+
if (!ignoreSelf || env.sender_did !== this.opts.myDid) {
|
|
208
246
|
this.opts.onMessage(env, conversationId);
|
|
247
|
+
} else {
|
|
248
|
+
this.opts.onDebug?.({
|
|
249
|
+
event: 'ws_message_ignored_self',
|
|
250
|
+
conversationId,
|
|
251
|
+
detail: { sender_did: env.sender_did, message_id: env.message_id, seq: env.seq },
|
|
252
|
+
});
|
|
209
253
|
}
|
|
210
254
|
} else if (msg.type === 'ws_control' && msg.control) {
|
|
211
255
|
this.opts.onControl?.(msg.control, conversationId);
|
|
256
|
+
this.opts.onDebug?.({ event: 'ws_control', conversationId, detail: msg.control });
|
|
257
|
+
} else if (msg.type === 'ws_receipt' && msg.receipt) {
|
|
258
|
+
this.opts.onDebug?.({ event: 'ws_receipt', conversationId, detail: msg.receipt });
|
|
259
|
+
} else if (msg?.type) {
|
|
260
|
+
this.opts.onDebug?.({ event: 'ws_unknown_type', conversationId, detail: { type: msg.type } });
|
|
261
|
+
}
|
|
262
|
+
} catch (e: any) {
|
|
263
|
+
// Observability: parse failures are otherwise silent and look like "server never pushes".
|
|
264
|
+
const now = Date.now();
|
|
265
|
+
const last = this.lastParseErrorAtByConv.get(conversationId) ?? 0;
|
|
266
|
+
if (now - last > 30_000) {
|
|
267
|
+
this.lastParseErrorAtByConv.set(conversationId, now);
|
|
268
|
+
this.opts.onDebug?.({
|
|
269
|
+
event: 'ws_parse_error',
|
|
270
|
+
conversationId,
|
|
271
|
+
detail: {
|
|
272
|
+
message: e?.message ?? String(e),
|
|
273
|
+
last_frame_at_ms: this.lastFrameAtByConv.get(conversationId) ?? null,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
212
276
|
}
|
|
213
|
-
} catch {
|
|
214
|
-
// ignore parse errors
|
|
215
277
|
}
|
|
216
278
|
});
|
|
217
279
|
|
|
@@ -320,10 +382,15 @@ export class WsSubscription {
|
|
|
320
382
|
this.reconnectTimers.set(conversationId, timer);
|
|
321
383
|
}
|
|
322
384
|
|
|
385
|
+
private isSubscribableConversationType(type: string): boolean {
|
|
386
|
+
// Server may return pending_dm alongside dm listings. WS upgrade is supported for any conversation_id.
|
|
387
|
+
return type === 'dm' || type === 'pending_dm' || type === 'channel' || type === 'group';
|
|
388
|
+
}
|
|
389
|
+
|
|
323
390
|
private async connectAll(): Promise<void> {
|
|
324
391
|
try {
|
|
325
392
|
const convos = await this.opts.listConversations();
|
|
326
|
-
const subscribable = convos.filter((c) =>
|
|
393
|
+
const subscribable = convos.filter((c) => this.isSubscribableConversationType(c.type));
|
|
327
394
|
for (const c of subscribable) {
|
|
328
395
|
void this.connectAsync(c.conversation_id);
|
|
329
396
|
}
|
|
@@ -337,7 +404,7 @@ export class WsSubscription {
|
|
|
337
404
|
try {
|
|
338
405
|
const convos = await this.opts.listConversations();
|
|
339
406
|
const subscribableIds = new Set(
|
|
340
|
-
convos.filter((c) =>
|
|
407
|
+
convos.filter((c) => this.isSubscribableConversationType(c.type)).map((c) => c.conversation_id),
|
|
341
408
|
);
|
|
342
409
|
for (const convId of subscribableIds) {
|
|
343
410
|
if (!this.stoppedConversations.has(convId) && !this.connections.has(convId)) {
|