@butlerbot/sdk 0.0.24 → 0.0.25

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.
@@ -88,6 +88,8 @@ export declare class Link {
88
88
  private readonly pending;
89
89
  private readonly calls;
90
90
  private socket;
91
+ /** Every socket this link has opened and not yet closed, by generation. */
92
+ private readonly sockets;
91
93
  private frameCounter;
92
94
  private currentState;
93
95
  private identity?;
@@ -162,6 +164,15 @@ export declare class Link {
162
164
  * means the real event, whenever it turns up, is ignored.
163
165
  */
164
166
  private dropSocket;
167
+ /**
168
+ * Closes a socket this end has stopped using, whatever generation it belongs to.
169
+ *
170
+ * Every path that walks away from a socket goes through here. Forgetting one is not harmless:
171
+ * the server has no way to tell an abandoned connection from a live one — it has said hello,
172
+ * registered its tools and claimed its link id — so it keeps it, keeps serving from it, and
173
+ * hands the link id back and forth between it and its replacements.
174
+ */
175
+ private abandon;
165
176
  private onClose;
166
177
  /** Retries when it is allowed to, and tells everyone waiting when it is not. */
167
178
  private retryOrGiveUp;
package/dist/link/link.js CHANGED
@@ -28,6 +28,8 @@ class Link {
28
28
  this.pending = new Map();
29
29
  this.calls = new Map();
30
30
  this.socket = null;
31
+ /** Every socket this link has opened and not yet closed, by generation. */
32
+ this.sockets = new Map();
31
33
  this.frameCounter = 0;
32
34
  this.currentState = "idle";
33
35
  /**
@@ -171,7 +173,10 @@ class Link {
171
173
  this.failPending(closed);
172
174
  this.settleWaiters(this.attemptWaiters, closed);
173
175
  this.settleWaiters(this.openWaiters, closed);
174
- this.socket?.close(1000, reason);
176
+ // Every socket, not just the current one: a link that is being closed for good must not
177
+ // leave anything of itself attached to the server.
178
+ for (const generation of [...this.sockets.keys()])
179
+ this.abandon(generation, reason);
175
180
  this.socket = null;
176
181
  }
177
182
  /**
@@ -194,14 +199,23 @@ class Link {
194
199
  const { url, protocols } = (0, socket_1.buildHandshake)(this.options.serverUrl, "link", this.options.apiKey);
195
200
  this.debug(`connecting to ${url.replace(/api_key=[^&]+/, "api_key=***")}`);
196
201
  try {
197
- this.socket = this.options.socketFactory(url, protocols, {
202
+ const socket = this.options.socketFactory(url, protocols, {
198
203
  onOpen: () => this.onOpen(generation),
199
204
  onMessage: (data) => { if (generation === this.generation)
200
205
  this.onMessage(data); },
201
- onClose: (code, reason) => this.onClose(generation, code, reason),
206
+ onClose: (code, reason) => {
207
+ this.sockets.delete(generation);
208
+ this.onClose(generation, code, reason);
209
+ },
202
210
  onError: (error) => { if (generation === this.generation)
203
211
  this.emitter.emit("error", asError(error)); },
204
212
  });
213
+ // Held by generation, not only as `this.socket`, so a socket that stops being the
214
+ // current one can still be closed. One that is merely forgotten stays open at the far
215
+ // end: it has already said hello and registered its tools, so the server goes on serving
216
+ // it, and it answers websocket pings forever because the network stack does that for it.
217
+ this.sockets.set(generation, socket);
218
+ this.socket = socket;
205
219
  }
206
220
  catch (error) {
207
221
  // A factory that throws never produces a close event, so this failure is
@@ -230,9 +244,11 @@ class Link {
230
244
  const identity = { connectionId: welcome.connectionId, scope: welcome.scope };
231
245
  this.identity = identity;
232
246
  await this.registerAll();
233
- // Registration is several round trips; the socket may have gone during them.
247
+ // Registration is several round trips; the socket may have gone during them. It is fully
248
+ // registered at the server by now, so abandoning it quietly would leave it serving tools
249
+ // nothing here will ever answer.
234
250
  if (generation !== this.generation)
235
- return;
251
+ return this.abandon(generation, "superseded during handshake");
236
252
  this.attempting = false;
237
253
  this.currentState = "open";
238
254
  this.reconnectAttempt = 0;
@@ -243,7 +259,7 @@ class Link {
243
259
  this.emitter.emit("connect", identity);
244
260
  }).catch((error) => {
245
261
  if (generation !== this.generation)
246
- return;
262
+ return this.abandon(generation, "superseded during handshake");
247
263
  this.emitter.emit("error", error);
248
264
  this.settleWaiters(this.attemptWaiters, error);
249
265
  // Only the server saying "do not come back" stops us: a rejected claim or an
@@ -266,12 +282,28 @@ class Link {
266
282
  dropSocket(generation, code, reason) {
267
283
  if (generation !== this.generation)
268
284
  return;
269
- const socket = this.socket;
270
285
  this.onClose(generation, code, reason);
286
+ // 1006 is reserved and rejected by browsers; anything else we raise is a valid
287
+ // application code.
288
+ this.abandon(generation, reason, code === 1006 ? 1000 : code);
289
+ }
290
+ /**
291
+ * Closes a socket this end has stopped using, whatever generation it belongs to.
292
+ *
293
+ * Every path that walks away from a socket goes through here. Forgetting one is not harmless:
294
+ * the server has no way to tell an abandoned connection from a live one — it has said hello,
295
+ * registered its tools and claimed its link id — so it keeps it, keeps serving from it, and
296
+ * hands the link id back and forth between it and its replacements.
297
+ */
298
+ abandon(generation, reason, code = 1000) {
299
+ const socket = this.sockets.get(generation);
300
+ if (!socket)
301
+ return;
302
+ this.sockets.delete(generation);
303
+ if (this.socket === socket)
304
+ this.socket = null;
271
305
  try {
272
- // 1006 is reserved and rejected by browsers; anything else we raise is a valid
273
- // application code.
274
- socket?.close(code === 1006 ? 1000 : code, reason);
306
+ socket.close(code, reason);
275
307
  }
276
308
  catch {
277
309
  // Already gone, which is the outcome we wanted anyway.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@butlerbot/sdk",
3
- "version": "0.0.24",
3
+ "version": "0.0.25",
4
4
  "description": "The official ButlerBot SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",