@butlerbot/sdk 0.0.28 → 0.0.29

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.
@@ -123,6 +123,8 @@ export declare class Link {
123
123
  * never needs to ask.
124
124
  */
125
125
  private lastInboundAt;
126
+ /** Ids of keepalive pulses, so their pongs can be dropped instead of shown as logs. */
127
+ private pulses;
126
128
  private closedByUs;
127
129
  constructor(options: LinkOptions);
128
130
  /** Adds a tool Alfred can call. Registered on connect, or immediately if already open. */
@@ -211,12 +213,28 @@ export declare class Link {
211
213
  * big enough to take longer than the timeout to drain got its own connection torn
212
214
  * down with `4000 heartbeat timeout` — always mid-response, always on the longest
213
215
  * answers, which are the ones a user least wants to lose.
216
+ *
217
+ * Not asking is not the same as saying nothing, though. The server reaps connections
218
+ * that have sent it no frames for 100s, because a socket the client walked away from
219
+ * still answers websocket pings at the network layer and only the client's own frames
220
+ * prove someone is still there. A link busy receiving a long turn used to go completely
221
+ * silent for as long as the turn ran and got closed as idle — `1001 idle: no frames
222
+ * received`, mid-response again. So a busy interval still sends a pulse; it just does
223
+ * not wait for the reply, which is the half that could not survive a full send queue.
214
224
  */
215
225
  private startHeartbeat;
216
226
  /** Wakes when the connection will have been silent for a full interval, not before. */
217
227
  private scheduleHeartbeat;
218
228
  /** One liveness round trip. Only ever sent to a connection that has gone quiet. */
219
229
  private ping;
230
+ /**
231
+ * A ping sent with no deadline and no interest in the answer.
232
+ *
233
+ * Its only job is to land on the server so the connection does not look abandoned. A
234
+ * failure here is not evidence of anything — inbound frames already proved the socket
235
+ * works — so it stays quiet and lets the real heartbeat make that call.
236
+ */
237
+ private pulse;
220
238
  /** Never longer than the interval itself: a second ping in flight tells us nothing new. */
221
239
  private heartbeatTimeoutMs;
222
240
  /** Any frame from the server, of any kind, is proof the connection still works. */
package/dist/link/link.js CHANGED
@@ -56,6 +56,8 @@ class Link {
56
56
  * never needs to ask.
57
57
  */
58
58
  this.lastInboundAt = 0;
59
+ /** Ids of keepalive pulses, so their pongs can be dropped instead of shown as logs. */
60
+ this.pulses = new Set();
59
61
  this.closedByUs = false;
60
62
  this.options = {
61
63
  ...DEFAULTS,
@@ -390,6 +392,14 @@ class Link {
390
392
  * big enough to take longer than the timeout to drain got its own connection torn
391
393
  * down with `4000 heartbeat timeout` — always mid-response, always on the longest
392
394
  * answers, which are the ones a user least wants to lose.
395
+ *
396
+ * Not asking is not the same as saying nothing, though. The server reaps connections
397
+ * that have sent it no frames for 100s, because a socket the client walked away from
398
+ * still answers websocket pings at the network layer and only the client's own frames
399
+ * prove someone is still there. A link busy receiving a long turn used to go completely
400
+ * silent for as long as the turn ran and got closed as idle — `1001 idle: no frames
401
+ * received`, mid-response again. So a busy interval still sends a pulse; it just does
402
+ * not wait for the reply, which is the half that could not survive a full send queue.
393
403
  */
394
404
  startHeartbeat(generation) {
395
405
  if (!this.options.heartbeatMs)
@@ -408,8 +418,10 @@ class Link {
408
418
  if (generation !== this.generation)
409
419
  return;
410
420
  // Something arrived while this was pending: the connection is demonstrably
411
- // alive and there is nothing to ask. Wait out the rest of its silence instead.
421
+ // alive and there is nothing to ask. Tell the server we are still here and
422
+ // wait out the rest of its silence instead.
412
423
  if (Date.now() - this.lastInboundAt < this.options.heartbeatMs) {
424
+ this.pulse();
413
425
  return this.scheduleHeartbeat(generation);
414
426
  }
415
427
  this.ping(generation);
@@ -440,6 +452,21 @@ class Link {
440
452
  this.dropSocket(generation, 4000, "heartbeat timeout");
441
453
  });
442
454
  }
455
+ /**
456
+ * A ping sent with no deadline and no interest in the answer.
457
+ *
458
+ * Its only job is to land on the server so the connection does not look abandoned. A
459
+ * failure here is not evidence of anything — inbound frames already proved the socket
460
+ * works — so it stays quiet and lets the real heartbeat make that call.
461
+ */
462
+ pulse() {
463
+ try {
464
+ this.pulses.add(this.send("ping", {}));
465
+ }
466
+ catch {
467
+ this.debug("could not send the keepalive pulse, leaving it to the next heartbeat");
468
+ }
469
+ }
443
470
  /** Never longer than the interval itself: a second ping in flight tells us nothing new. */
444
471
  heartbeatTimeoutMs() {
445
472
  return Math.max(250, Math.min(this.options.requestTimeoutMs, this.options.heartbeatMs));
@@ -452,6 +479,8 @@ class Link {
452
479
  if (this.heartbeat)
453
480
  clearTimeout(this.heartbeat);
454
481
  this.heartbeat = undefined;
482
+ // Pongs owed by a socket that is going away will never arrive.
483
+ this.pulses.clear();
455
484
  }
456
485
  // =============================================
457
486
  // WAITING
@@ -652,6 +681,10 @@ class Link {
652
681
  waiting.onFrame?.(frame);
653
682
  return;
654
683
  }
684
+ // The pong to a keepalive pulse. Nothing is waiting for it, and it is not a log
685
+ // anyone asked to see.
686
+ if (frame.replyTo && this.pulses.delete(frame.replyTo))
687
+ return;
655
688
  switch (frame.type) {
656
689
  case "tool.call":
657
690
  this.handleToolCall(frame);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butlerbot/sdk",
3
- "version": "0.0.28",
3
+ "version": "0.0.29",
4
4
  "description": "The official ButlerBot SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",