@butlerbot/sdk 0.0.23 → 0.0.24
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/link/link.d.ts +67 -4
- package/dist/link/link.js +222 -54
- package/package.json +1 -1
package/dist/link/link.d.ts
CHANGED
|
@@ -91,8 +91,22 @@ export declare class Link {
|
|
|
91
91
|
private frameCounter;
|
|
92
92
|
private currentState;
|
|
93
93
|
private identity?;
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
/**
|
|
95
|
+
* Which socket the callbacks below belong to.
|
|
96
|
+
*
|
|
97
|
+
* Every handler carries the generation it was made for and does nothing once that is
|
|
98
|
+
* no longer current. Without it a late `close` from a socket we have already replaced
|
|
99
|
+
* tears down its successor — one blip turning into a link that flaps for the life of
|
|
100
|
+
* the process.
|
|
101
|
+
*/
|
|
102
|
+
private generation;
|
|
103
|
+
/** True from the moment a socket is created until it is open or gone. */
|
|
104
|
+
private attempting;
|
|
105
|
+
private reconnectTimer?;
|
|
106
|
+
/** Settled per attempt: this is what `connect()` awaits. */
|
|
107
|
+
private readonly attemptWaiters;
|
|
108
|
+
/** Settled when the link opens, however many attempts that takes. `ready()` awaits these. */
|
|
109
|
+
private readonly openWaiters;
|
|
96
110
|
private reconnectAttempt;
|
|
97
111
|
private reconnectAfterMs;
|
|
98
112
|
private heartbeat?;
|
|
@@ -112,15 +126,45 @@ export declare class Link {
|
|
|
112
126
|
get scope(): LinkScopeKind | undefined;
|
|
113
127
|
on<K extends keyof LinkEvents>(event: K, listener: (...args: LinkEvents[K]) => unknown): string;
|
|
114
128
|
off<K extends keyof LinkEvents>(event: K, id: string): void;
|
|
115
|
-
/**
|
|
129
|
+
/**
|
|
130
|
+
* Connects, resolving once every tool and hook has been registered.
|
|
131
|
+
*
|
|
132
|
+
* Rejects if *this* attempt fails. When `reconnect` is on the link keeps trying in
|
|
133
|
+
* the background regardless, so a caller can either await this again or just listen
|
|
134
|
+
* for the `connect` event.
|
|
135
|
+
*/
|
|
116
136
|
connect(): Promise<this>;
|
|
117
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Resolves when the link is usable, connecting first if it has not been asked to yet.
|
|
139
|
+
*
|
|
140
|
+
* Bounded by `requestTimeoutMs`, and it never opens a socket of its own while one is
|
|
141
|
+
* in flight: the callers are hook emits and tool replies, which are worth sending now
|
|
142
|
+
* or not at all. Waiting out a long outage here used to mean a new connection per
|
|
143
|
+
* event.
|
|
144
|
+
*/
|
|
118
145
|
ready(): Promise<void>;
|
|
119
146
|
/** Closes for good. Registrations are released server-side as the socket drops. */
|
|
120
147
|
close(reason?: string): void;
|
|
148
|
+
/**
|
|
149
|
+
* Starts an attempt, but only when there is nothing to join.
|
|
150
|
+
*
|
|
151
|
+
* The single door to opening a socket: an attempt in flight, or a reconnect already
|
|
152
|
+
* waiting out its backoff, is the attempt.
|
|
153
|
+
*/
|
|
154
|
+
private ensureAttempting;
|
|
121
155
|
private openSocket;
|
|
122
156
|
private onOpen;
|
|
157
|
+
/**
|
|
158
|
+
* Abandons a socket and treats it as closed right now.
|
|
159
|
+
*
|
|
160
|
+
* A connection that died without a close frame can take minutes to report it, or
|
|
161
|
+
* never, so the close is synthesised rather than waited for. Bumping the generation
|
|
162
|
+
* means the real event, whenever it turns up, is ignored.
|
|
163
|
+
*/
|
|
164
|
+
private dropSocket;
|
|
123
165
|
private onClose;
|
|
166
|
+
/** Retries when it is allowed to, and tells everyone waiting when it is not. */
|
|
167
|
+
private retryOrGiveUp;
|
|
124
168
|
/**
|
|
125
169
|
* Reconnects with full jitter on top of any delay the server asked for.
|
|
126
170
|
*
|
|
@@ -128,8 +172,27 @@ export declare class Link {
|
|
|
128
172
|
* a fixed delay produces.
|
|
129
173
|
*/
|
|
130
174
|
private scheduleReconnect;
|
|
175
|
+
private clearReconnect;
|
|
176
|
+
/**
|
|
177
|
+
* Pings on an interval and, the important half, notices when a ping goes unanswered.
|
|
178
|
+
*
|
|
179
|
+
* A websocket can die without a close frame — a dropped route, a proxy that forgets
|
|
180
|
+
* the connection, a suspended machine — leaving both ends convinced they are
|
|
181
|
+
* connected while every frame sent into it vanishes. An unanswered ping is the only
|
|
182
|
+
* evidence this end will ever get, so it is treated as a dead connection and
|
|
183
|
+
* reconnected rather than swallowed.
|
|
184
|
+
*/
|
|
131
185
|
private startHeartbeat;
|
|
186
|
+
/**
|
|
187
|
+
* Never longer than the interval itself: a second ping in flight tells us nothing new.
|
|
188
|
+
*
|
|
189
|
+
* Floored so that a very short interval cannot declare a merely busy connection dead.
|
|
190
|
+
*/
|
|
191
|
+
private heartbeatTimeoutMs;
|
|
132
192
|
private stopHeartbeat;
|
|
193
|
+
/** Parks a caller until someone settles the list it was parked in. 0 waits forever. */
|
|
194
|
+
private wait;
|
|
195
|
+
private settleWaiters;
|
|
133
196
|
private registerAll;
|
|
134
197
|
private registerTools;
|
|
135
198
|
private registerHook;
|
package/dist/link/link.js
CHANGED
|
@@ -30,6 +30,21 @@ class Link {
|
|
|
30
30
|
this.socket = null;
|
|
31
31
|
this.frameCounter = 0;
|
|
32
32
|
this.currentState = "idle";
|
|
33
|
+
/**
|
|
34
|
+
* Which socket the callbacks below belong to.
|
|
35
|
+
*
|
|
36
|
+
* Every handler carries the generation it was made for and does nothing once that is
|
|
37
|
+
* no longer current. Without it a late `close` from a socket we have already replaced
|
|
38
|
+
* tears down its successor — one blip turning into a link that flaps for the life of
|
|
39
|
+
* the process.
|
|
40
|
+
*/
|
|
41
|
+
this.generation = 0;
|
|
42
|
+
/** True from the moment a socket is created until it is open or gone. */
|
|
43
|
+
this.attempting = false;
|
|
44
|
+
/** Settled per attempt: this is what `connect()` awaits. */
|
|
45
|
+
this.attemptWaiters = [];
|
|
46
|
+
/** Settled when the link opens, however many attempts that takes. `ready()` awaits these. */
|
|
47
|
+
this.openWaiters = [];
|
|
33
48
|
this.reconnectAttempt = 0;
|
|
34
49
|
this.reconnectAfterMs = 0;
|
|
35
50
|
this.closedByUs = false;
|
|
@@ -107,58 +122,101 @@ class Link {
|
|
|
107
122
|
// =============================================
|
|
108
123
|
// LIFECYCLE
|
|
109
124
|
// =============================================
|
|
110
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Connects, resolving once every tool and hook has been registered.
|
|
127
|
+
*
|
|
128
|
+
* Rejects if *this* attempt fails. When `reconnect` is on the link keeps trying in
|
|
129
|
+
* the background regardless, so a caller can either await this again or just listen
|
|
130
|
+
* for the `connect` event.
|
|
131
|
+
*/
|
|
111
132
|
connect() {
|
|
112
133
|
if (this.currentState === "open")
|
|
113
134
|
return Promise.resolve(this);
|
|
114
|
-
if (this.connecting)
|
|
115
|
-
return this.connecting;
|
|
116
135
|
this.closedByUs = false;
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
136
|
+
if (this.currentState === "closed")
|
|
137
|
+
this.currentState = "idle";
|
|
138
|
+
// Joins whatever attempt is already in flight — or already scheduled — rather than
|
|
139
|
+
// racing a second socket against it.
|
|
140
|
+
const waited = this.wait(this.attemptWaiters, 0);
|
|
141
|
+
this.ensureAttempting();
|
|
142
|
+
return waited.then(() => this);
|
|
122
143
|
}
|
|
123
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Resolves when the link is usable, connecting first if it has not been asked to yet.
|
|
146
|
+
*
|
|
147
|
+
* Bounded by `requestTimeoutMs`, and it never opens a socket of its own while one is
|
|
148
|
+
* in flight: the callers are hook emits and tool replies, which are worth sending now
|
|
149
|
+
* or not at all. Waiting out a long outage here used to mean a new connection per
|
|
150
|
+
* event.
|
|
151
|
+
*/
|
|
124
152
|
async ready() {
|
|
125
153
|
if (this.currentState === "open")
|
|
126
154
|
return;
|
|
127
155
|
if (this.currentState === "closed")
|
|
128
|
-
throw new
|
|
129
|
-
|
|
156
|
+
throw new protocol_1.LinkError("closed", "This link has been closed.");
|
|
157
|
+
const waited = this.wait(this.openWaiters, this.options.requestTimeoutMs);
|
|
158
|
+
this.ensureAttempting();
|
|
159
|
+
await waited;
|
|
130
160
|
}
|
|
131
161
|
/** Closes for good. Registrations are released server-side as the socket drops. */
|
|
132
162
|
close(reason = "client closed") {
|
|
163
|
+
const closed = new protocol_1.LinkError("closed", "The link was closed.");
|
|
133
164
|
this.closedByUs = true;
|
|
134
165
|
this.currentState = "closed";
|
|
166
|
+
// Orphans the live socket's callbacks, so its close cannot reopen anything.
|
|
167
|
+
this.generation += 1;
|
|
168
|
+
this.attempting = false;
|
|
135
169
|
this.stopHeartbeat();
|
|
136
|
-
this.
|
|
170
|
+
this.clearReconnect();
|
|
171
|
+
this.failPending(closed);
|
|
172
|
+
this.settleWaiters(this.attemptWaiters, closed);
|
|
173
|
+
this.settleWaiters(this.openWaiters, closed);
|
|
137
174
|
this.socket?.close(1000, reason);
|
|
138
175
|
this.socket = null;
|
|
139
|
-
|
|
140
|
-
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Starts an attempt, but only when there is nothing to join.
|
|
179
|
+
*
|
|
180
|
+
* The single door to opening a socket: an attempt in flight, or a reconnect already
|
|
181
|
+
* waiting out its backoff, is the attempt.
|
|
182
|
+
*/
|
|
183
|
+
ensureAttempting() {
|
|
184
|
+
if (this.closedByUs || this.currentState === "open")
|
|
185
|
+
return;
|
|
186
|
+
if (this.attempting || this.reconnectTimer)
|
|
187
|
+
return;
|
|
188
|
+
this.openSocket();
|
|
141
189
|
}
|
|
142
190
|
openSocket() {
|
|
143
191
|
this.currentState = "connecting";
|
|
144
|
-
|
|
145
|
-
|
|
192
|
+
this.attempting = true;
|
|
193
|
+
const generation = ++this.generation;
|
|
146
194
|
const { url, protocols } = (0, socket_1.buildHandshake)(this.options.serverUrl, "link", this.options.apiKey);
|
|
147
195
|
this.debug(`connecting to ${url.replace(/api_key=[^&]+/, "api_key=***")}`);
|
|
148
196
|
try {
|
|
149
197
|
this.socket = this.options.socketFactory(url, protocols, {
|
|
150
|
-
onOpen: () => this.onOpen(),
|
|
151
|
-
onMessage: (data) => this.
|
|
152
|
-
|
|
153
|
-
|
|
198
|
+
onOpen: () => this.onOpen(generation),
|
|
199
|
+
onMessage: (data) => { if (generation === this.generation)
|
|
200
|
+
this.onMessage(data); },
|
|
201
|
+
onClose: (code, reason) => this.onClose(generation, code, reason),
|
|
202
|
+
onError: (error) => { if (generation === this.generation)
|
|
203
|
+
this.emitter.emit("error", asError(error)); },
|
|
154
204
|
});
|
|
155
205
|
}
|
|
156
206
|
catch (error) {
|
|
157
|
-
|
|
207
|
+
// A factory that throws never produces a close event, so this failure is
|
|
208
|
+
// reported and retried from here instead.
|
|
209
|
+
const failure = asError(error);
|
|
210
|
+
this.socket = null;
|
|
211
|
+
this.attempting = false;
|
|
212
|
+
this.emitter.emit("error", failure);
|
|
213
|
+
this.settleWaiters(this.attemptWaiters, failure);
|
|
214
|
+
this.retryOrGiveUp(failure);
|
|
158
215
|
}
|
|
159
|
-
return signal.promise;
|
|
160
216
|
}
|
|
161
|
-
onOpen() {
|
|
217
|
+
onOpen(generation) {
|
|
218
|
+
if (generation !== this.generation)
|
|
219
|
+
return;
|
|
162
220
|
// The handshake declares who we are; nothing else may be sent before it.
|
|
163
221
|
void this.exchange("hello", {
|
|
164
222
|
linkId: this.options.linkId,
|
|
@@ -169,39 +227,88 @@ class Link {
|
|
|
169
227
|
isDone: (frame) => frame.type === "welcome",
|
|
170
228
|
}).then(async (frame) => {
|
|
171
229
|
const welcome = frame.payload;
|
|
172
|
-
|
|
230
|
+
const identity = { connectionId: welcome.connectionId, scope: welcome.scope };
|
|
231
|
+
this.identity = identity;
|
|
173
232
|
await this.registerAll();
|
|
233
|
+
// Registration is several round trips; the socket may have gone during them.
|
|
234
|
+
if (generation !== this.generation)
|
|
235
|
+
return;
|
|
236
|
+
this.attempting = false;
|
|
174
237
|
this.currentState = "open";
|
|
175
238
|
this.reconnectAttempt = 0;
|
|
176
239
|
this.reconnectAfterMs = 0;
|
|
177
|
-
this.startHeartbeat();
|
|
178
|
-
this.
|
|
179
|
-
this.
|
|
240
|
+
this.startHeartbeat(generation);
|
|
241
|
+
this.settleWaiters(this.attemptWaiters);
|
|
242
|
+
this.settleWaiters(this.openWaiters);
|
|
243
|
+
this.emitter.emit("connect", identity);
|
|
180
244
|
}).catch((error) => {
|
|
245
|
+
if (generation !== this.generation)
|
|
246
|
+
return;
|
|
181
247
|
this.emitter.emit("error", error);
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
248
|
+
this.settleWaiters(this.attemptWaiters, error);
|
|
249
|
+
// Only the server saying "do not come back" stops us: a rejected claim or an
|
|
250
|
+
// unsupported protocol will not fix itself. Everything else that can land here —
|
|
251
|
+
// a timeout, a socket dropped mid-registration, a transient server error — is
|
|
252
|
+
// precisely what reconnecting is for. Treating all of it as fatal left the link
|
|
253
|
+
// dead for the life of the process.
|
|
254
|
+
if (error instanceof protocol_1.LinkError && error.fatal)
|
|
255
|
+
this.closedByUs = true;
|
|
256
|
+
this.dropSocket(generation, 1000, "handshake failed");
|
|
187
257
|
});
|
|
188
258
|
}
|
|
189
|
-
|
|
259
|
+
/**
|
|
260
|
+
* Abandons a socket and treats it as closed right now.
|
|
261
|
+
*
|
|
262
|
+
* A connection that died without a close frame can take minutes to report it, or
|
|
263
|
+
* never, so the close is synthesised rather than waited for. Bumping the generation
|
|
264
|
+
* means the real event, whenever it turns up, is ignored.
|
|
265
|
+
*/
|
|
266
|
+
dropSocket(generation, code, reason) {
|
|
267
|
+
if (generation !== this.generation)
|
|
268
|
+
return;
|
|
269
|
+
const socket = this.socket;
|
|
270
|
+
this.onClose(generation, code, reason);
|
|
271
|
+
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);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Already gone, which is the outcome we wanted anyway.
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
onClose(generation, code, reason) {
|
|
281
|
+
if (generation !== this.generation)
|
|
282
|
+
return;
|
|
283
|
+
// Whatever else this socket has to say is now somebody else's news.
|
|
284
|
+
this.generation += 1;
|
|
190
285
|
const willReconnect = this.options.reconnect && !this.closedByUs;
|
|
286
|
+
const error = new protocol_1.LinkError("disconnected", `The link disconnected (${code}${reason ? `: ${reason}` : ""}).`);
|
|
191
287
|
this.socket = null;
|
|
192
288
|
this.identity = undefined;
|
|
193
|
-
this.
|
|
289
|
+
this.attempting = false;
|
|
194
290
|
this.currentState = willReconnect ? "connecting" : "closed";
|
|
195
291
|
// Nothing about subscriptions survives a socket: the server re-sends the set on every
|
|
196
292
|
// connect, so holding the old one would only risk reporting against ids that are gone.
|
|
197
293
|
this.subscriptionStore.reset();
|
|
198
294
|
this.stopHeartbeat();
|
|
199
|
-
this.failPending(
|
|
295
|
+
this.failPending(error);
|
|
296
|
+
this.settleWaiters(this.attemptWaiters, error);
|
|
200
297
|
this.emitter.emit("disconnect", { code, reason, willReconnect });
|
|
201
|
-
this.readySignal?.reject(new protocol_1.LinkError("disconnected", `The link disconnected (${code}).`));
|
|
202
|
-
this.readySignal = undefined;
|
|
203
298
|
if (willReconnect)
|
|
204
299
|
this.scheduleReconnect();
|
|
300
|
+
else
|
|
301
|
+
this.settleWaiters(this.openWaiters, error);
|
|
302
|
+
}
|
|
303
|
+
/** Retries when it is allowed to, and tells everyone waiting when it is not. */
|
|
304
|
+
retryOrGiveUp(error) {
|
|
305
|
+
if (this.options.reconnect && !this.closedByUs) {
|
|
306
|
+
this.currentState = "connecting";
|
|
307
|
+
this.scheduleReconnect();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
this.currentState = "closed";
|
|
311
|
+
this.settleWaiters(this.openWaiters, error);
|
|
205
312
|
}
|
|
206
313
|
/**
|
|
207
314
|
* Reconnects with full jitter on top of any delay the server asked for.
|
|
@@ -210,33 +317,97 @@ class Link {
|
|
|
210
317
|
* a fixed delay produces.
|
|
211
318
|
*/
|
|
212
319
|
scheduleReconnect() {
|
|
320
|
+
this.clearReconnect();
|
|
213
321
|
const ceiling = Math.min(this.options.maxReconnectDelayMs, this.options.minReconnectDelayMs * 2 ** this.reconnectAttempt);
|
|
214
322
|
const delay = this.reconnectAfterMs + Math.random() * ceiling;
|
|
215
323
|
this.reconnectAttempt += 1;
|
|
216
324
|
this.debug(`reconnecting in ${Math.round(delay)}ms (attempt ${this.reconnectAttempt})`);
|
|
217
|
-
|
|
218
|
-
|
|
325
|
+
this.reconnectTimer = setTimeout(() => {
|
|
326
|
+
this.reconnectTimer = undefined;
|
|
327
|
+
if (this.closedByUs || this.currentState === "open" || this.attempting)
|
|
219
328
|
return;
|
|
220
|
-
this.openSocket()
|
|
329
|
+
this.openSocket();
|
|
221
330
|
}, delay);
|
|
222
|
-
unref(
|
|
331
|
+
unref(this.reconnectTimer);
|
|
332
|
+
}
|
|
333
|
+
clearReconnect() {
|
|
334
|
+
if (this.reconnectTimer)
|
|
335
|
+
clearTimeout(this.reconnectTimer);
|
|
336
|
+
this.reconnectTimer = undefined;
|
|
223
337
|
}
|
|
224
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Pings on an interval and, the important half, notices when a ping goes unanswered.
|
|
340
|
+
*
|
|
341
|
+
* A websocket can die without a close frame — a dropped route, a proxy that forgets
|
|
342
|
+
* the connection, a suspended machine — leaving both ends convinced they are
|
|
343
|
+
* connected while every frame sent into it vanishes. An unanswered ping is the only
|
|
344
|
+
* evidence this end will ever get, so it is treated as a dead connection and
|
|
345
|
+
* reconnected rather than swallowed.
|
|
346
|
+
*/
|
|
347
|
+
startHeartbeat(generation) {
|
|
225
348
|
if (!this.options.heartbeatMs)
|
|
226
349
|
return;
|
|
350
|
+
this.stopHeartbeat();
|
|
227
351
|
this.heartbeat = setInterval(() => {
|
|
352
|
+
if (generation !== this.generation)
|
|
353
|
+
return;
|
|
228
354
|
// The reply is consumed by the exchange, so it never reaches log listeners.
|
|
229
|
-
this.exchange("ping", {}, {
|
|
230
|
-
|
|
355
|
+
this.exchange("ping", {}, {
|
|
356
|
+
awaitReady: false,
|
|
357
|
+
isDone: (frame) => frame.type === "log",
|
|
358
|
+
timeoutMs: this.heartbeatTimeoutMs(),
|
|
359
|
+
}).catch(() => {
|
|
360
|
+
if (generation !== this.generation)
|
|
361
|
+
return;
|
|
362
|
+
this.debug("heartbeat went unanswered, treating the connection as dead");
|
|
363
|
+
this.dropSocket(generation, 4000, "heartbeat timeout");
|
|
364
|
+
});
|
|
231
365
|
}, this.options.heartbeatMs);
|
|
232
366
|
unref(this.heartbeat);
|
|
233
367
|
}
|
|
368
|
+
/**
|
|
369
|
+
* Never longer than the interval itself: a second ping in flight tells us nothing new.
|
|
370
|
+
*
|
|
371
|
+
* Floored so that a very short interval cannot declare a merely busy connection dead.
|
|
372
|
+
*/
|
|
373
|
+
heartbeatTimeoutMs() {
|
|
374
|
+
return Math.max(250, Math.min(this.options.requestTimeoutMs, this.options.heartbeatMs));
|
|
375
|
+
}
|
|
234
376
|
stopHeartbeat() {
|
|
235
377
|
if (this.heartbeat)
|
|
236
378
|
clearInterval(this.heartbeat);
|
|
237
379
|
this.heartbeat = undefined;
|
|
238
380
|
}
|
|
239
381
|
// =============================================
|
|
382
|
+
// WAITING
|
|
383
|
+
// =============================================
|
|
384
|
+
/** Parks a caller until someone settles the list it was parked in. 0 waits forever. */
|
|
385
|
+
wait(waiters, timeoutMs) {
|
|
386
|
+
return new Promise((resolve, reject) => {
|
|
387
|
+
const waiter = { resolve, reject };
|
|
388
|
+
if (timeoutMs > 0) {
|
|
389
|
+
waiter.timer = setTimeout(() => {
|
|
390
|
+
const index = waiters.indexOf(waiter);
|
|
391
|
+
if (index >= 0)
|
|
392
|
+
waiters.splice(index, 1);
|
|
393
|
+
reject(new protocol_1.LinkError("timeout", `The link was not open within ${timeoutMs}ms.`));
|
|
394
|
+
}, timeoutMs);
|
|
395
|
+
unref(waiter.timer);
|
|
396
|
+
}
|
|
397
|
+
waiters.push(waiter);
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
settleWaiters(waiters, error) {
|
|
401
|
+
for (const waiter of waiters.splice(0, waiters.length)) {
|
|
402
|
+
if (waiter.timer)
|
|
403
|
+
clearTimeout(waiter.timer);
|
|
404
|
+
if (error)
|
|
405
|
+
waiter.reject(error);
|
|
406
|
+
else
|
|
407
|
+
waiter.resolve();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// =============================================
|
|
240
411
|
// REGISTRATION
|
|
241
412
|
// =============================================
|
|
242
413
|
async registerAll() {
|
|
@@ -269,7 +440,13 @@ class Link {
|
|
|
269
440
|
* that was valid a moment ago is a race, not a bug worth complaining about.
|
|
270
441
|
*/
|
|
271
442
|
async reportHookEvent(hookId, event, payload, chosenIds) {
|
|
272
|
-
|
|
443
|
+
// Not `ready()`: a report is only worth anything now, and while the link is down the
|
|
444
|
+
// subscription set is empty anyway — so waiting would mean holding a busy guild's
|
|
445
|
+
// events open to match them against nothing.
|
|
446
|
+
if (this.currentState !== "open") {
|
|
447
|
+
this.debug(`dropped ${event}: the link is not connected`);
|
|
448
|
+
return [];
|
|
449
|
+
}
|
|
273
450
|
const sourceId = this.hooks.get(hookId)?.sourceId ?? hookId;
|
|
274
451
|
// Explicit ids are still checked against what this link actually holds. Not out of distrust of
|
|
275
452
|
// the caller — the server checks again anyway — but because an id it was never given can only
|
|
@@ -490,15 +667,6 @@ exports.Link = Link;
|
|
|
490
667
|
// =============================================
|
|
491
668
|
// HELPERS
|
|
492
669
|
// =============================================
|
|
493
|
-
function deferred() {
|
|
494
|
-
let resolve;
|
|
495
|
-
let reject;
|
|
496
|
-
const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
|
|
497
|
-
// Rejections are surfaced through connect() and the error event; an unobserved
|
|
498
|
-
// one here must not take the process down.
|
|
499
|
-
promise.catch(() => undefined);
|
|
500
|
-
return { promise, resolve, reject };
|
|
501
|
-
}
|
|
502
670
|
function asError(value) {
|
|
503
671
|
if (value instanceof Error)
|
|
504
672
|
return value;
|