@interactive-inc/flume 0.3.0 → 0.4.0
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/README.md +67 -53
- package/dist/connection-error-HUO3PC3G.js +16 -0
- package/dist/discord.d.ts +10 -85
- package/dist/discord.js +473 -183
- package/dist/github.d.ts +9 -48
- package/dist/github.js +391 -135
- package/dist/{http-error-BtXonO-W.js → http-error-CPSKoSie.js} +1 -1
- package/dist/index.d.ts +76 -34
- package/dist/index.js +225 -57
- package/dist/safe-invoke-callback-EpWXwfwp.js +170 -0
- package/dist/safe-read-text-DgrJ4Uhl.js +20 -0
- package/dist/safe-stringify-BWS-uXZP.js +172 -0
- package/dist/serial-queue-B9LoBc64.js +162 -0
- package/dist/slack.d.ts +10 -62
- package/dist/slack.js +376 -139
- package/dist/{types-Bm9uKUQz.d.ts → types-D-tO-Mh2.d.ts} +32 -21
- package/package.json +21 -18
- package/dist/connection-error-BOk97djj.d.ts +0 -6
- package/dist/http-error-K-Ym4lfK.d.ts +0 -11
- package/dist/logger-CpGB9WO_.js +0 -49
- package/dist/parse-error-BAiCLRmk.d.ts +0 -6
- package/dist/reconnector-BDoJ1xNX.js +0 -67
- package/dist/safe-fetch-30ZzOKHL.js +0 -20
- package/dist/safe-json-parse-BWlzGOLl.js +0 -41
- package/dist/serial-queue-ExmlnpzQ.js +0 -16
package/dist/discord.js
CHANGED
|
@@ -1,50 +1,8 @@
|
|
|
1
|
-
import { n as createFlumeDefaultDeps, t as
|
|
2
|
-
import {
|
|
3
|
-
import { t as
|
|
4
|
-
import { n as
|
|
1
|
+
import { a as FlumeParseError, c as safeNormalizeError, i as FlumeStartError, l as safeErrorMessage, n as FlumeLogger, o as createFlumeDefaultDeps, r as safeNow, s as attempt, t as safeInvokeCallback } from "./safe-invoke-callback-EpWXwfwp.js";
|
|
2
|
+
import { t as FlumeConnectionError } from "./connection-error-HUO3PC3G.js";
|
|
3
|
+
import { a as FlumeReconnector, i as resolveFlumeReconnectConfig, n as isRecord, o as safeRandom, r as scheduleFlumeReconnect, t as safeStringify } from "./safe-stringify-BWS-uXZP.js";
|
|
4
|
+
import { i as safeJsonParse, n as FlumeStatusEmitter, r as FlumeSignalRegistry, t as FlumeSerialQueue } from "./serial-queue-B9LoBc64.js";
|
|
5
5
|
import { z } from "zod/v4";
|
|
6
|
-
//#region lib/discord/extract-discord-meta.ts
|
|
7
|
-
function extractDiscordMeta(eventName, eventData) {
|
|
8
|
-
const meta = { event_type: eventName };
|
|
9
|
-
if (typeof eventData.channel_id === "string") meta.channel_id = eventData.channel_id;
|
|
10
|
-
if (typeof eventData.guild_id === "string") meta.guild_id = eventData.guild_id;
|
|
11
|
-
if (isRecord(eventData.author) && typeof eventData.author.id === "string") meta.user_id = eventData.author.id;
|
|
12
|
-
return meta;
|
|
13
|
-
}
|
|
14
|
-
//#endregion
|
|
15
|
-
//#region lib/discord/discord-heartbeat.ts
|
|
16
|
-
var FlumeDiscordHeartbeat = class {
|
|
17
|
-
props;
|
|
18
|
-
timer = null;
|
|
19
|
-
ackReceived = true;
|
|
20
|
-
constructor(props) {
|
|
21
|
-
this.props = props;
|
|
22
|
-
}
|
|
23
|
-
start(intervalMs) {
|
|
24
|
-
this.stop();
|
|
25
|
-
this.ackReceived = true;
|
|
26
|
-
this.timer = this.props.deps.setInterval(() => {
|
|
27
|
-
if (!this.ackReceived) {
|
|
28
|
-
this.props.onZombie();
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
this.ackReceived = false;
|
|
32
|
-
this.props.onSend();
|
|
33
|
-
}, intervalMs);
|
|
34
|
-
}
|
|
35
|
-
stop() {
|
|
36
|
-
if (this.timer === null) return;
|
|
37
|
-
this.props.deps.clearInterval(this.timer);
|
|
38
|
-
this.timer = null;
|
|
39
|
-
}
|
|
40
|
-
ack() {
|
|
41
|
-
this.ackReceived = true;
|
|
42
|
-
}
|
|
43
|
-
isRunning() {
|
|
44
|
-
return this.timer !== null;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
//#endregion
|
|
48
6
|
//#region lib/discord/discord-gateway-session.ts
|
|
49
7
|
var FlumeDiscordGatewaySession = class FlumeDiscordGatewaySession {
|
|
50
8
|
sessionId;
|
|
@@ -85,27 +43,116 @@ var FlumeDiscordGatewaySession = class FlumeDiscordGatewaySession {
|
|
|
85
43
|
}
|
|
86
44
|
};
|
|
87
45
|
//#endregion
|
|
46
|
+
//#region lib/discord/discord-heartbeat.ts
|
|
47
|
+
/**
|
|
48
|
+
* Discord Gateway のハートビート。
|
|
49
|
+
* 初回送信は `random()*heartbeat_interval` ms 後 (spec)、以降は heartbeat_interval ごと。
|
|
50
|
+
* 前回送信に対する ACK が来ていなければ zombie 通知。すべての callback / timer body は throw を
|
|
51
|
+
* 飲み込みログ記録する (host の timer queue に uncaught を漏らさない)
|
|
52
|
+
*/
|
|
53
|
+
var FlumeDiscordHeartbeat = class {
|
|
54
|
+
props;
|
|
55
|
+
initialTimer = null;
|
|
56
|
+
intervalTimer = null;
|
|
57
|
+
ackReceived = true;
|
|
58
|
+
constructor(props) {
|
|
59
|
+
this.props = props;
|
|
60
|
+
}
|
|
61
|
+
start(intervalMs) {
|
|
62
|
+
this.stop();
|
|
63
|
+
this.ackReceived = true;
|
|
64
|
+
const initialDelay = safeRandom({ deps: this.props.deps }) * intervalMs;
|
|
65
|
+
const initialResult = attempt(() => this.props.deps.setTimeout(() => {
|
|
66
|
+
this.initialTimer = null;
|
|
67
|
+
this.safeFire();
|
|
68
|
+
const intervalResult = attempt(() => this.props.deps.setInterval(() => this.safeFire(), intervalMs));
|
|
69
|
+
if (intervalResult instanceof Error) {
|
|
70
|
+
this.props.log.error({
|
|
71
|
+
action: "heartbeat.interval.schedule.error",
|
|
72
|
+
message: safeErrorMessage({ error: intervalResult }),
|
|
73
|
+
error: intervalResult
|
|
74
|
+
});
|
|
75
|
+
this.intervalTimer = null;
|
|
76
|
+
} else this.intervalTimer = intervalResult;
|
|
77
|
+
}, initialDelay));
|
|
78
|
+
if (initialResult instanceof Error) {
|
|
79
|
+
this.props.log.error({
|
|
80
|
+
action: "heartbeat.initial.schedule.error",
|
|
81
|
+
message: safeErrorMessage({ error: initialResult }),
|
|
82
|
+
error: initialResult
|
|
83
|
+
});
|
|
84
|
+
this.initialTimer = null;
|
|
85
|
+
} else this.initialTimer = initialResult;
|
|
86
|
+
}
|
|
87
|
+
stop() {
|
|
88
|
+
if (this.initialTimer !== null) {
|
|
89
|
+
const handle = this.initialTimer;
|
|
90
|
+
const r = attempt(() => this.props.deps.clearTimeout(handle));
|
|
91
|
+
if (r instanceof Error) this.props.log.error({
|
|
92
|
+
action: "heartbeat.initial.clear.error",
|
|
93
|
+
message: safeErrorMessage({ error: r }),
|
|
94
|
+
error: r
|
|
95
|
+
});
|
|
96
|
+
this.initialTimer = null;
|
|
97
|
+
}
|
|
98
|
+
if (this.intervalTimer !== null) {
|
|
99
|
+
const handle = this.intervalTimer;
|
|
100
|
+
const r = attempt(() => this.props.deps.clearInterval(handle));
|
|
101
|
+
if (r instanceof Error) this.props.log.error({
|
|
102
|
+
action: "heartbeat.interval.clear.error",
|
|
103
|
+
message: safeErrorMessage({ error: r }),
|
|
104
|
+
error: r
|
|
105
|
+
});
|
|
106
|
+
this.intervalTimer = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
ack() {
|
|
110
|
+
this.ackReceived = true;
|
|
111
|
+
}
|
|
112
|
+
isRunning() {
|
|
113
|
+
return this.initialTimer !== null || this.intervalTimer !== null;
|
|
114
|
+
}
|
|
115
|
+
safeFire() {
|
|
116
|
+
safeInvokeCallback({
|
|
117
|
+
fn: () => this.fire(),
|
|
118
|
+
onError: (error) => {
|
|
119
|
+
this.props.log.error({
|
|
120
|
+
action: "heartbeat.fire.error",
|
|
121
|
+
message: safeErrorMessage({ error }),
|
|
122
|
+
error
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
fire() {
|
|
128
|
+
if (!this.ackReceived) {
|
|
129
|
+
this.props.onZombie();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.ackReceived = false;
|
|
133
|
+
this.props.onSend();
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
//#endregion
|
|
88
137
|
//#region lib/discord/discord-gateway-message-schema.ts
|
|
89
138
|
const FlumeGatewayMessageSchema = z.object({
|
|
90
139
|
op: z.number(),
|
|
91
|
-
d: z.
|
|
140
|
+
d: z.unknown(),
|
|
92
141
|
s: z.number().nullable(),
|
|
93
142
|
t: z.string().nullable()
|
|
94
143
|
});
|
|
95
144
|
//#endregion
|
|
96
145
|
//#region lib/discord/parse-discord-gateway-message.ts
|
|
97
|
-
function
|
|
146
|
+
function parseFlumeDiscordGatewayMessage(raw) {
|
|
98
147
|
const json = safeJsonParse(raw);
|
|
148
|
+
if (json instanceof FlumeParseError) return json;
|
|
99
149
|
const parsed = FlumeGatewayMessageSchema.safeParse(json);
|
|
100
|
-
if (!parsed.success) return new FlumeParseError(`invalid gateway message
|
|
150
|
+
if (!parsed.success) return new FlumeParseError(`invalid gateway message frame (${raw.length} bytes)`, { cause: parsed.error });
|
|
101
151
|
return parsed.data;
|
|
102
152
|
}
|
|
103
153
|
//#endregion
|
|
104
154
|
//#region lib/discord/discord-gateway.ts
|
|
105
155
|
const GATEWAY_URL = "wss://gateway.discord.gg/?v=10&encoding=json";
|
|
106
|
-
function framePreview(raw) {
|
|
107
|
-
return raw.length > 200 ? `${raw.slice(0, 200)}... (${raw.length} bytes)` : raw;
|
|
108
|
-
}
|
|
109
156
|
const OP_DISPATCH = 0;
|
|
110
157
|
const OP_HEARTBEAT = 1;
|
|
111
158
|
const OP_IDENTIFY = 2;
|
|
@@ -124,15 +171,33 @@ const OP_NAMES = {
|
|
|
124
171
|
[OP_HELLO]: "HELLO",
|
|
125
172
|
[OP_HEARTBEAT_ACK]: "HEARTBEAT_ACK"
|
|
126
173
|
};
|
|
174
|
+
const WS_OPEN = 1;
|
|
175
|
+
const TERMINAL_CLOSE_CODES = new Set([
|
|
176
|
+
4004,
|
|
177
|
+
4010,
|
|
178
|
+
4011,
|
|
179
|
+
4012,
|
|
180
|
+
4013,
|
|
181
|
+
4014
|
|
182
|
+
]);
|
|
183
|
+
/**
|
|
184
|
+
* Discord Gateway v10 の最小実装。HELLO -> IDENTIFY/RESUME -> READY/RESUMED -> dispatch を扱う。
|
|
185
|
+
* READY 後の WebSocket 切断のみ `onStatus("disconnected")` を発火し source 側で再接続する。
|
|
186
|
+
* 終端 close code (4004 / 401x) を受けた場合は stopped 化して再接続を抑止。
|
|
187
|
+
* IO 境界は全て `attempt` 経由で扱い、コンストラクタ throw も `FlumeConnectionError` として返す
|
|
188
|
+
* (`connect()` は決して reject しない)
|
|
189
|
+
*/
|
|
127
190
|
var FlumeDiscordGateway = class {
|
|
128
191
|
props;
|
|
129
192
|
log;
|
|
130
193
|
ws = null;
|
|
131
194
|
heartbeat = null;
|
|
132
|
-
|
|
133
|
-
|
|
195
|
+
currentSession = FlumeDiscordGatewaySession.empty();
|
|
196
|
+
isStoppedFlag = false;
|
|
197
|
+
hasConnected = false;
|
|
134
198
|
pendingResolve = null;
|
|
135
199
|
pendingResolved = false;
|
|
200
|
+
invalidSessionTimer = null;
|
|
136
201
|
constructor(props) {
|
|
137
202
|
this.props = props;
|
|
138
203
|
this.log = new FlumeLogger({
|
|
@@ -141,20 +206,65 @@ var FlumeDiscordGateway = class {
|
|
|
141
206
|
deps: props.deps
|
|
142
207
|
});
|
|
143
208
|
}
|
|
209
|
+
get session() {
|
|
210
|
+
return this.currentSession;
|
|
211
|
+
}
|
|
212
|
+
get isStopped() {
|
|
213
|
+
return this.isStoppedFlag;
|
|
214
|
+
}
|
|
144
215
|
connect(url) {
|
|
216
|
+
const WS = this.props.deps.WebSocket;
|
|
217
|
+
if (!WS) {
|
|
218
|
+
const error = new FlumeConnectionError("WebSocket runtime not available");
|
|
219
|
+
this.log.error({
|
|
220
|
+
action: "ws.error",
|
|
221
|
+
message: safeErrorMessage({ error }),
|
|
222
|
+
error
|
|
223
|
+
});
|
|
224
|
+
return Promise.resolve(error);
|
|
225
|
+
}
|
|
145
226
|
const target = url ?? GATEWAY_URL;
|
|
227
|
+
const hostResult = attempt(() => new URL(target).hostname);
|
|
228
|
+
const host = hostResult instanceof Error ? "unknown" : hostResult;
|
|
146
229
|
this.log.info({
|
|
147
230
|
action: "connect.start",
|
|
148
|
-
message: `
|
|
231
|
+
message: `host=${host}`
|
|
149
232
|
});
|
|
150
233
|
this.pendingResolved = false;
|
|
234
|
+
this.hasConnected = false;
|
|
151
235
|
return new Promise((resolve) => {
|
|
152
236
|
this.pendingResolve = resolve;
|
|
153
|
-
const
|
|
237
|
+
const socketResult = attempt(() => new WS(target));
|
|
238
|
+
if (socketResult instanceof Error) {
|
|
239
|
+
const error = new FlumeConnectionError(`WebSocket construction failed: ${safeErrorMessage({ error: socketResult })}`, { cause: socketResult });
|
|
240
|
+
this.log.error({
|
|
241
|
+
action: "ws.construct.error",
|
|
242
|
+
message: safeErrorMessage({ error }),
|
|
243
|
+
error
|
|
244
|
+
});
|
|
245
|
+
this.ws = null;
|
|
246
|
+
this.pendingResolved = true;
|
|
247
|
+
resolve(error);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const socket = socketResult;
|
|
154
251
|
this.ws = socket;
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
252
|
+
const listenerResult = attempt(() => {
|
|
253
|
+
socket.addEventListener("message", (ev) => this.safeOnMessage(ev, socket));
|
|
254
|
+
socket.addEventListener("close", (ev) => this.safeOnClose(ev));
|
|
255
|
+
socket.addEventListener("error", () => this.safeOnError());
|
|
256
|
+
});
|
|
257
|
+
if (listenerResult instanceof Error) {
|
|
258
|
+
const error = new FlumeConnectionError(`WebSocket listener registration failed: ${safeErrorMessage({ error: listenerResult })}`, { cause: listenerResult });
|
|
259
|
+
this.log.error({
|
|
260
|
+
action: "ws.listener.error",
|
|
261
|
+
message: safeErrorMessage({ error }),
|
|
262
|
+
error
|
|
263
|
+
});
|
|
264
|
+
this.ws = null;
|
|
265
|
+
this.pendingResolved = true;
|
|
266
|
+
resolve(error);
|
|
267
|
+
}
|
|
158
268
|
});
|
|
159
269
|
}
|
|
160
270
|
disconnect() {
|
|
@@ -162,45 +272,71 @@ var FlumeDiscordGateway = class {
|
|
|
162
272
|
action: "disconnect",
|
|
163
273
|
message: "shutting down gateway"
|
|
164
274
|
});
|
|
165
|
-
this.
|
|
275
|
+
this.isStoppedFlag = true;
|
|
166
276
|
this.heartbeat?.stop();
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
this.ws
|
|
170
|
-
|
|
277
|
+
this.clearInvalidSessionTimer();
|
|
278
|
+
this.closeSocket({
|
|
279
|
+
ws: this.ws,
|
|
280
|
+
code: 1e3,
|
|
281
|
+
reason: "shutdown"
|
|
282
|
+
});
|
|
283
|
+
this.ws = null;
|
|
171
284
|
}
|
|
172
285
|
isConnected() {
|
|
173
|
-
return this.ws !== null && this.ws.readyState ===
|
|
286
|
+
return this.ws !== null && this.ws.readyState === WS_OPEN;
|
|
174
287
|
}
|
|
175
288
|
completeConnect(error) {
|
|
176
289
|
if (this.pendingResolved || !this.pendingResolve) return;
|
|
177
290
|
this.pendingResolved = true;
|
|
178
291
|
this.pendingResolve(error);
|
|
179
292
|
}
|
|
180
|
-
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
293
|
+
safeOnMessage(ev, socket) {
|
|
294
|
+
const r = attempt(() => this.onMessage(String(ev.data), socket));
|
|
295
|
+
if (r instanceof Error) this.log.error({
|
|
296
|
+
action: "ws.message.threw",
|
|
297
|
+
message: safeErrorMessage({ error: r }),
|
|
298
|
+
error: r
|
|
184
299
|
});
|
|
185
|
-
|
|
300
|
+
}
|
|
301
|
+
safeOnClose(ev) {
|
|
302
|
+
const r = attempt(() => this.onClose(ev));
|
|
303
|
+
if (r instanceof Error) this.log.error({
|
|
304
|
+
action: "ws.close.threw",
|
|
305
|
+
message: safeErrorMessage({ error: r }),
|
|
306
|
+
error: r
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
safeOnError() {
|
|
310
|
+
const r = attempt(() => this.onError());
|
|
311
|
+
if (r instanceof Error) this.log.error({
|
|
312
|
+
action: "ws.error.threw",
|
|
313
|
+
message: safeErrorMessage({ error: r }),
|
|
314
|
+
error: r
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
onMessage(raw, socket) {
|
|
318
|
+
if (this.isStoppedFlag) return;
|
|
319
|
+
const parsed = parseFlumeDiscordGatewayMessage(raw);
|
|
186
320
|
if (parsed instanceof FlumeParseError) {
|
|
187
321
|
this.log.error({
|
|
188
|
-
action: "ws.parse
|
|
322
|
+
action: "ws.parse.error",
|
|
189
323
|
message: parsed.message,
|
|
190
|
-
error: parsed
|
|
324
|
+
error: parsed,
|
|
325
|
+
detail: { length: raw.length }
|
|
191
326
|
});
|
|
192
327
|
return;
|
|
193
328
|
}
|
|
194
329
|
this.log.debug({
|
|
195
|
-
action: "ws.
|
|
196
|
-
message: `op=${OP_NAMES[parsed.op] ?? parsed.op} t=${parsed.t ?? "-"} s=${parsed.s ?? "-"}`,
|
|
330
|
+
action: "ws.recv",
|
|
331
|
+
message: `op=${OP_NAMES[parsed.op] ?? parsed.op} t=${parsed.t ?? "-"} s=${parsed.s ?? "-"} length=${raw.length}`,
|
|
197
332
|
detail: {
|
|
198
333
|
op: parsed.op,
|
|
199
334
|
t: parsed.t,
|
|
200
|
-
s: parsed.s
|
|
335
|
+
s: parsed.s,
|
|
336
|
+
length: raw.length
|
|
201
337
|
}
|
|
202
338
|
});
|
|
203
|
-
if (parsed.s !== null) this.
|
|
339
|
+
if (parsed.s !== null) this.currentSession = this.currentSession.withSeq(parsed.s);
|
|
204
340
|
if (parsed.op === OP_HELLO) return this.onHello(parsed);
|
|
205
341
|
if (parsed.op === OP_HEARTBEAT_ACK) return this.onHeartbeatAck();
|
|
206
342
|
if (parsed.op === OP_HEARTBEAT) return this.onHeartbeatRequest();
|
|
@@ -208,27 +344,31 @@ var FlumeDiscordGateway = class {
|
|
|
208
344
|
if (parsed.op === OP_INVALID_SESSION) return this.onInvalidSession(parsed, socket);
|
|
209
345
|
if (parsed.op === OP_DISPATCH) return this.onDispatch(parsed);
|
|
210
346
|
this.log.warn({
|
|
211
|
-
action: "ws.unknown
|
|
347
|
+
action: "ws.op.unknown",
|
|
212
348
|
message: `unknown op=${parsed.op}`,
|
|
213
349
|
detail: { op: parsed.op }
|
|
214
350
|
});
|
|
215
351
|
}
|
|
216
352
|
onHello(msg) {
|
|
217
|
-
const
|
|
353
|
+
const d = isRecord(msg.d) ? msg.d : null;
|
|
354
|
+
const interval = d && typeof d.heartbeat_interval === "number" ? d.heartbeat_interval : 0;
|
|
218
355
|
this.log.info({
|
|
219
|
-
action: "hello",
|
|
220
|
-
message: `heartbeat_interval=${interval}ms
|
|
356
|
+
action: "gateway.hello",
|
|
357
|
+
message: `heartbeat_interval=${interval}ms`,
|
|
358
|
+
detail: { interval }
|
|
221
359
|
});
|
|
360
|
+
this.heartbeat?.stop();
|
|
222
361
|
this.heartbeat = new FlumeDiscordHeartbeat({
|
|
362
|
+
log: this.log,
|
|
223
363
|
deps: this.props.deps,
|
|
224
364
|
onSend: () => {
|
|
225
365
|
this.log.debug({
|
|
226
366
|
action: "heartbeat.send",
|
|
227
|
-
message: `seq=${this.
|
|
367
|
+
message: `seq=${this.currentSession.seq}`
|
|
228
368
|
});
|
|
229
369
|
this.send({
|
|
230
370
|
op: OP_HEARTBEAT,
|
|
231
|
-
d: this.
|
|
371
|
+
d: this.currentSession.seq
|
|
232
372
|
});
|
|
233
373
|
},
|
|
234
374
|
onZombie: () => {
|
|
@@ -236,11 +376,15 @@ var FlumeDiscordGateway = class {
|
|
|
236
376
|
action: "heartbeat.zombie",
|
|
237
377
|
message: "no ACK received, closing connection"
|
|
238
378
|
});
|
|
239
|
-
this.
|
|
379
|
+
this.closeSocket({
|
|
380
|
+
ws: this.ws,
|
|
381
|
+
code: 4009,
|
|
382
|
+
reason: "zombie connection"
|
|
383
|
+
});
|
|
240
384
|
}
|
|
241
385
|
});
|
|
242
386
|
this.heartbeat.start(interval);
|
|
243
|
-
if (this.
|
|
387
|
+
if (this.currentSession.canResume()) this.sendResume();
|
|
244
388
|
else this.sendIdentify();
|
|
245
389
|
}
|
|
246
390
|
onHeartbeatAck() {
|
|
@@ -257,95 +401,185 @@ var FlumeDiscordGateway = class {
|
|
|
257
401
|
});
|
|
258
402
|
this.send({
|
|
259
403
|
op: OP_HEARTBEAT,
|
|
260
|
-
d: this.
|
|
404
|
+
d: this.currentSession.seq
|
|
261
405
|
});
|
|
262
406
|
}
|
|
263
407
|
onReconnectRequest(socket) {
|
|
264
408
|
this.log.info({
|
|
265
|
-
action: "reconnect.requested",
|
|
409
|
+
action: "ws.reconnect.requested",
|
|
266
410
|
message: "server requested reconnect"
|
|
267
411
|
});
|
|
268
|
-
|
|
412
|
+
this.closeSocket({
|
|
413
|
+
ws: socket,
|
|
414
|
+
code: 4e3,
|
|
415
|
+
reason: "reconnect requested"
|
|
416
|
+
});
|
|
269
417
|
}
|
|
270
418
|
onInvalidSession(msg, socket) {
|
|
271
|
-
const resumable =
|
|
419
|
+
const resumable = msg.d === true || isRecord(msg.d) && msg.d.resumable === true;
|
|
272
420
|
this.log.warn({
|
|
273
|
-
action: "invalid
|
|
274
|
-
message: `resumable=${resumable}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
421
|
+
action: "session.invalid",
|
|
422
|
+
message: `resumable=${resumable}`,
|
|
423
|
+
detail: { resumable }
|
|
424
|
+
});
|
|
425
|
+
const delay = 1e3 + safeRandom({ deps: this.props.deps }) * 4e3;
|
|
426
|
+
if (!resumable) this.currentSession = this.currentSession.withReset();
|
|
427
|
+
this.clearInvalidSessionTimer();
|
|
428
|
+
const timerResult = attempt(() => this.props.deps.setTimeout(() => {
|
|
429
|
+
this.invalidSessionTimer = null;
|
|
430
|
+
this.closeSocket({
|
|
431
|
+
ws: socket,
|
|
432
|
+
code: 4e3,
|
|
433
|
+
reason: "invalid session"
|
|
282
434
|
});
|
|
283
|
-
|
|
284
|
-
|
|
435
|
+
}, delay));
|
|
436
|
+
if (timerResult instanceof Error) {
|
|
437
|
+
this.log.error({
|
|
438
|
+
action: "session.invalid.timer.error",
|
|
439
|
+
message: safeErrorMessage({ error: timerResult }),
|
|
440
|
+
error: timerResult
|
|
441
|
+
});
|
|
442
|
+
this.invalidSessionTimer = null;
|
|
443
|
+
} else this.invalidSessionTimer = timerResult;
|
|
285
444
|
}
|
|
286
445
|
onDispatch(msg) {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
446
|
+
const d = isRecord(msg.d) ? msg.d : null;
|
|
447
|
+
if (msg.t === "READY" && d) {
|
|
448
|
+
const sessionId = typeof d.session_id === "string" ? d.session_id : "";
|
|
449
|
+
const resumeUrl = typeof d.resume_gateway_url === "string" ? d.resume_gateway_url : "";
|
|
450
|
+
this.currentSession = this.currentSession.withReady(sessionId, resumeUrl);
|
|
291
451
|
this.log.info({
|
|
292
|
-
action: "ready",
|
|
293
|
-
message: `session
|
|
452
|
+
action: "gateway.ready",
|
|
453
|
+
message: `session ready`,
|
|
454
|
+
detail: { hasResumeUrl: resumeUrl !== "" }
|
|
294
455
|
});
|
|
456
|
+
this.hasConnected = true;
|
|
295
457
|
this.props.onStatus("connected");
|
|
296
458
|
this.completeConnect(null);
|
|
297
459
|
}
|
|
298
460
|
if (msg.t === "RESUMED") {
|
|
299
461
|
this.log.info({
|
|
300
|
-
action: "resumed",
|
|
301
|
-
message: `
|
|
462
|
+
action: "gateway.resumed",
|
|
463
|
+
message: `seq=${this.currentSession.seq}`
|
|
302
464
|
});
|
|
465
|
+
this.hasConnected = true;
|
|
303
466
|
this.props.onStatus("connected");
|
|
304
467
|
this.completeConnect(null);
|
|
305
468
|
}
|
|
306
|
-
if (msg.t &&
|
|
307
|
-
else if (msg.t) this.
|
|
469
|
+
if (msg.t && d) this.props.onDispatch(msg.t, d);
|
|
470
|
+
else if (msg.t) this.log.debug({
|
|
471
|
+
action: "dispatch.empty",
|
|
472
|
+
message: `dropped ${msg.t} (no payload)`,
|
|
473
|
+
detail: { type: msg.t }
|
|
474
|
+
});
|
|
308
475
|
}
|
|
309
476
|
onClose(ev) {
|
|
477
|
+
const terminal = TERMINAL_CLOSE_CODES.has(ev.code);
|
|
310
478
|
this.log.info({
|
|
311
479
|
action: "ws.close",
|
|
312
|
-
message: `code=${ev.code} reason=${ev.reason || "none"}`,
|
|
480
|
+
message: `code=${ev.code} reason=${ev.reason || "none"}${terminal ? " (terminal)" : ""}`,
|
|
313
481
|
detail: {
|
|
314
482
|
code: ev.code,
|
|
315
|
-
reason: ev.reason
|
|
483
|
+
reason: ev.reason,
|
|
484
|
+
terminal
|
|
316
485
|
}
|
|
317
486
|
});
|
|
318
487
|
this.ws = null;
|
|
319
488
|
this.heartbeat?.stop();
|
|
320
|
-
this.
|
|
321
|
-
|
|
489
|
+
this.clearInvalidSessionTimer();
|
|
490
|
+
if (terminal) this.isStoppedFlag = true;
|
|
491
|
+
if (this.hasConnected || terminal) this.props.onStatus("disconnected");
|
|
492
|
+
if (!this.pendingResolved) {
|
|
493
|
+
const error = new FlumeConnectionError(`WebSocket closed before ready (code=${ev.code})`, { code: ev.code });
|
|
494
|
+
this.completeConnect(error);
|
|
495
|
+
}
|
|
322
496
|
}
|
|
323
497
|
onError() {
|
|
498
|
+
const error = new FlumeConnectionError("WebSocket connection error");
|
|
324
499
|
this.log.error({
|
|
325
500
|
action: "ws.error",
|
|
326
|
-
message:
|
|
501
|
+
message: safeErrorMessage({ error }),
|
|
502
|
+
error
|
|
503
|
+
});
|
|
504
|
+
this.completeConnect(error);
|
|
505
|
+
}
|
|
506
|
+
clearInvalidSessionTimer() {
|
|
507
|
+
if (this.invalidSessionTimer === null) return;
|
|
508
|
+
const handle = this.invalidSessionTimer;
|
|
509
|
+
const result = attempt(() => this.props.deps.clearTimeout(handle));
|
|
510
|
+
if (result instanceof Error) this.log.error({
|
|
511
|
+
action: "session.invalid.timer.clear.error",
|
|
512
|
+
message: safeErrorMessage({ error: result }),
|
|
513
|
+
error: result
|
|
327
514
|
});
|
|
328
|
-
this.
|
|
515
|
+
this.invalidSessionTimer = null;
|
|
329
516
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
517
|
+
closeSocket(input) {
|
|
518
|
+
if (input.ws === null) return;
|
|
519
|
+
const ws = input.ws;
|
|
520
|
+
const result = attempt(() => {
|
|
521
|
+
if (input.code !== void 0) ws.close(input.code, input.reason ?? "");
|
|
522
|
+
else ws.close();
|
|
334
523
|
});
|
|
524
|
+
if (result instanceof Error) this.log.error({
|
|
525
|
+
action: "ws.close.error",
|
|
526
|
+
message: safeErrorMessage({ error: result }),
|
|
527
|
+
error: result
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
send(input) {
|
|
531
|
+
const payload = this.safeSerialize(input);
|
|
532
|
+
if (payload === null) return;
|
|
335
533
|
this.log.debug({
|
|
336
534
|
action: "ws.send",
|
|
337
|
-
message: `op=${OP_NAMES[input.op] ?? input.op}`,
|
|
338
|
-
detail: {
|
|
535
|
+
message: `op=${OP_NAMES[input.op] ?? input.op} length=${payload.length}`,
|
|
536
|
+
detail: {
|
|
537
|
+
op: input.op,
|
|
538
|
+
length: payload.length
|
|
539
|
+
}
|
|
339
540
|
});
|
|
340
|
-
this.ws
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
541
|
+
const ws = this.ws;
|
|
542
|
+
if (ws === null) {
|
|
543
|
+
this.log.warn({
|
|
544
|
+
action: "ws.send",
|
|
545
|
+
message: "ws.send skipped: socket is null"
|
|
546
|
+
});
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (ws.readyState !== WS_OPEN) {
|
|
550
|
+
this.log.warn({
|
|
551
|
+
action: "ws.send",
|
|
552
|
+
message: `ws.send skipped: readyState=${ws.readyState} (not OPEN)`,
|
|
553
|
+
detail: { readyState: ws.readyState }
|
|
554
|
+
});
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const result = attempt(() => ws.send(payload));
|
|
558
|
+
if (result instanceof Error) this.log.error({
|
|
559
|
+
action: "ws.send",
|
|
560
|
+
message: `ws.send failed: ${safeErrorMessage({ error: result })}`,
|
|
561
|
+
error: result
|
|
344
562
|
});
|
|
345
563
|
}
|
|
564
|
+
safeSerialize(input) {
|
|
565
|
+
const result = safeStringify({
|
|
566
|
+
op: input.op,
|
|
567
|
+
d: input.d ?? null
|
|
568
|
+
});
|
|
569
|
+
if (result instanceof Error) {
|
|
570
|
+
this.log.error({
|
|
571
|
+
action: "ws.send.serialize.error",
|
|
572
|
+
message: safeErrorMessage({ error: result }),
|
|
573
|
+
error: result,
|
|
574
|
+
detail: { op: input.op }
|
|
575
|
+
});
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
346
580
|
sendIdentify() {
|
|
347
581
|
this.log.info({
|
|
348
|
-
action: "identify",
|
|
582
|
+
action: "gateway.identify",
|
|
349
583
|
message: `intents=${this.props.intents}`
|
|
350
584
|
});
|
|
351
585
|
this.send({
|
|
@@ -363,15 +597,15 @@ var FlumeDiscordGateway = class {
|
|
|
363
597
|
}
|
|
364
598
|
sendResume() {
|
|
365
599
|
this.log.info({
|
|
366
|
-
action: "resume",
|
|
367
|
-
message: `
|
|
600
|
+
action: "gateway.resume",
|
|
601
|
+
message: `seq=${this.currentSession.seq}`
|
|
368
602
|
});
|
|
369
603
|
this.send({
|
|
370
604
|
op: OP_RESUME,
|
|
371
605
|
d: {
|
|
372
606
|
token: this.props.token,
|
|
373
|
-
session_id: this.
|
|
374
|
-
seq: this.
|
|
607
|
+
session_id: this.currentSession.sessionId,
|
|
608
|
+
seq: this.currentSession.seq
|
|
375
609
|
}
|
|
376
610
|
});
|
|
377
611
|
}
|
|
@@ -402,18 +636,40 @@ const FlumeDiscordGatewayIntents = {
|
|
|
402
636
|
DirectMessagePolls: 1 << 25
|
|
403
637
|
};
|
|
404
638
|
//#endregion
|
|
639
|
+
//#region lib/discord/extract-discord-meta.ts
|
|
640
|
+
function flumeExtractDiscordMeta(eventName, eventData) {
|
|
641
|
+
const meta = { event_type: eventName };
|
|
642
|
+
if (typeof eventData.channel_id === "string") meta.channel_id = eventData.channel_id;
|
|
643
|
+
if (typeof eventData.guild_id === "string") meta.guild_id = eventData.guild_id;
|
|
644
|
+
if (isRecord(eventData.author) && typeof eventData.author.id === "string") meta.user_id = eventData.author.id;
|
|
645
|
+
return meta;
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
405
648
|
//#region lib/discord/discord-source.ts
|
|
406
|
-
const DEFAULT_INTENTS = FlumeDiscordGatewayIntents.Guilds | FlumeDiscordGatewayIntents.GuildMessages | FlumeDiscordGatewayIntents.DirectMessages
|
|
649
|
+
const DEFAULT_INTENTS = FlumeDiscordGatewayIntents.Guilds | FlumeDiscordGatewayIntents.GuildMessages | FlumeDiscordGatewayIntents.DirectMessages;
|
|
407
650
|
var FlumeDiscordSource = class {
|
|
408
651
|
options;
|
|
409
652
|
name = "discord";
|
|
410
653
|
gateway = null;
|
|
411
654
|
reconnector = null;
|
|
412
655
|
handler = null;
|
|
413
|
-
currentStatus = "disconnected";
|
|
414
656
|
log;
|
|
415
657
|
deps;
|
|
416
658
|
queue = new FlumeSerialQueue();
|
|
659
|
+
signals;
|
|
660
|
+
statusEmitter;
|
|
661
|
+
onSignalAbort = () => {
|
|
662
|
+
safeInvokeCallback({
|
|
663
|
+
fn: () => this.stop(),
|
|
664
|
+
onError: (error) => {
|
|
665
|
+
this.log.error({
|
|
666
|
+
action: "signal.abort.stop.failed",
|
|
667
|
+
message: safeErrorMessage({ error }),
|
|
668
|
+
error
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
};
|
|
417
673
|
constructor(options) {
|
|
418
674
|
this.options = options;
|
|
419
675
|
this.deps = options.deps ?? createFlumeDefaultDeps();
|
|
@@ -422,33 +678,37 @@ var FlumeDiscordSource = class {
|
|
|
422
678
|
handler: options.onLog,
|
|
423
679
|
deps: this.deps
|
|
424
680
|
});
|
|
681
|
+
this.signals = new FlumeSignalRegistry({
|
|
682
|
+
log: this.log,
|
|
683
|
+
onAbort: this.onSignalAbort
|
|
684
|
+
});
|
|
685
|
+
this.statusEmitter = new FlumeStatusEmitter({
|
|
686
|
+
log: this.log,
|
|
687
|
+
onStatus: options.onStatus
|
|
688
|
+
});
|
|
425
689
|
const rc = resolveFlumeReconnectConfig(options.reconnect);
|
|
426
690
|
if (rc) this.reconnector = new FlumeReconnector({
|
|
427
691
|
...rc,
|
|
692
|
+
log: this.log,
|
|
428
693
|
deps: this.deps
|
|
429
694
|
});
|
|
430
695
|
}
|
|
431
|
-
async start(handler) {
|
|
432
|
-
if (this.options.signal?.
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
this.options.signal?.addEventListener("abort", () => this.stop(), { once: true });
|
|
696
|
+
async start(handler, options) {
|
|
697
|
+
if (this.signals.isAnyAborted(this.options.signal) || this.signals.isAnyAborted(options?.signal)) return new FlumeStartError("Discord source: signal already aborted");
|
|
698
|
+
if (!this.hasWebSocket()) return new FlumeStartError("Discord source: deps.WebSocket is null (no WebSocket runtime available)");
|
|
699
|
+
this.signals.register(this.options.signal);
|
|
700
|
+
this.signals.register(options?.signal);
|
|
437
701
|
this.handler = handler;
|
|
438
702
|
this.log.info({
|
|
439
|
-
action: "start",
|
|
703
|
+
action: "source.start",
|
|
440
704
|
message: "starting Discord source"
|
|
441
705
|
});
|
|
442
|
-
|
|
443
|
-
if (result instanceof Error) return {
|
|
444
|
-
ok: false,
|
|
445
|
-
error: result
|
|
446
|
-
};
|
|
447
|
-
return { ok: true };
|
|
706
|
+
return await this.connectInternal();
|
|
448
707
|
}
|
|
449
708
|
async stop() {
|
|
709
|
+
this.signals.unregisterAll();
|
|
450
710
|
this.log.info({
|
|
451
|
-
action: "stop",
|
|
711
|
+
action: "source.stop",
|
|
452
712
|
message: "stopping Discord source"
|
|
453
713
|
});
|
|
454
714
|
if (this.reconnector && !this.reconnector.aborted) this.log.debug({
|
|
@@ -457,16 +717,29 @@ var FlumeDiscordSource = class {
|
|
|
457
717
|
});
|
|
458
718
|
this.reconnector?.cancel();
|
|
459
719
|
this.gateway?.disconnect();
|
|
720
|
+
await this.queue.drain();
|
|
460
721
|
this.gateway = null;
|
|
461
722
|
this.handler = null;
|
|
462
|
-
|
|
463
|
-
this.setStatus("disconnected");
|
|
723
|
+
this.statusEmitter.set("disconnected");
|
|
464
724
|
}
|
|
465
725
|
status() {
|
|
466
|
-
return this.
|
|
726
|
+
return this.statusEmitter.value;
|
|
727
|
+
}
|
|
728
|
+
hasWebSocket() {
|
|
729
|
+
const result = attempt(() => Boolean(this.deps.WebSocket));
|
|
730
|
+
if (result instanceof Error) {
|
|
731
|
+
const error = safeNormalizeError({ value: result });
|
|
732
|
+
this.log.error({
|
|
733
|
+
action: "deps.web-socket.read.error",
|
|
734
|
+
message: safeErrorMessage({ error }),
|
|
735
|
+
error
|
|
736
|
+
});
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
return result;
|
|
467
740
|
}
|
|
468
741
|
async connectInternal(resumeUrl) {
|
|
469
|
-
this.
|
|
742
|
+
this.statusEmitter.set("connecting");
|
|
470
743
|
this.gateway = new FlumeDiscordGateway({
|
|
471
744
|
token: this.options.token,
|
|
472
745
|
intents: this.options.intents ?? DEFAULT_INTENTS,
|
|
@@ -479,11 +752,11 @@ var FlumeDiscordSource = class {
|
|
|
479
752
|
if (error instanceof FlumeConnectionError) {
|
|
480
753
|
this.log.error({
|
|
481
754
|
action: "connect.failed",
|
|
482
|
-
message: error
|
|
755
|
+
message: safeErrorMessage({ error }),
|
|
483
756
|
error
|
|
484
757
|
});
|
|
485
|
-
if (!this.reconnector || this.reconnector.aborted) {
|
|
486
|
-
this.
|
|
758
|
+
if (this.gateway.isStopped || !this.reconnector || this.reconnector.aborted) {
|
|
759
|
+
this.statusEmitter.set("disconnected");
|
|
487
760
|
return error;
|
|
488
761
|
}
|
|
489
762
|
this.scheduleReconnect();
|
|
@@ -491,25 +764,38 @@ var FlumeDiscordSource = class {
|
|
|
491
764
|
return null;
|
|
492
765
|
}
|
|
493
766
|
handleDispatch(eventName, eventData) {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
type: eventName,
|
|
497
|
-
data: eventData,
|
|
498
|
-
meta: extractDiscordMeta(eventName, eventData),
|
|
499
|
-
receivedAt: this.deps.now()
|
|
500
|
-
};
|
|
767
|
+
const handler = this.handler;
|
|
768
|
+
if (!handler) return;
|
|
501
769
|
this.queue.add(async () => {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
770
|
+
const event = {
|
|
771
|
+
source: "discord",
|
|
772
|
+
type: eventName,
|
|
773
|
+
data: eventData,
|
|
774
|
+
meta: this.safeExtractMeta(eventName, eventData),
|
|
775
|
+
receivedAt: safeNow({ deps: this.deps })
|
|
776
|
+
};
|
|
777
|
+
const r = await attempt(() => Promise.resolve(handler(event)));
|
|
778
|
+
if (r instanceof Error) this.log.error({
|
|
779
|
+
action: "handler.error",
|
|
780
|
+
message: safeErrorMessage({ error: r }),
|
|
781
|
+
error: r
|
|
782
|
+
});
|
|
511
783
|
});
|
|
512
784
|
}
|
|
785
|
+
safeExtractMeta(eventName, eventData) {
|
|
786
|
+
const result = attempt(() => flumeExtractDiscordMeta(eventName, eventData));
|
|
787
|
+
if (result instanceof Error) {
|
|
788
|
+
const error = safeNormalizeError({ value: result });
|
|
789
|
+
this.log.warn({
|
|
790
|
+
action: "meta.extract.error",
|
|
791
|
+
message: safeErrorMessage({ error }),
|
|
792
|
+
error,
|
|
793
|
+
detail: { eventName }
|
|
794
|
+
});
|
|
795
|
+
return { event_type: eventName };
|
|
796
|
+
}
|
|
797
|
+
return result;
|
|
798
|
+
}
|
|
513
799
|
handleGatewayStatus(status) {
|
|
514
800
|
if (status === "connected") {
|
|
515
801
|
if (this.reconnector && this.reconnector.attempt > 0) this.log.info({
|
|
@@ -517,30 +803,34 @@ var FlumeDiscordSource = class {
|
|
|
517
803
|
message: `cleared ${this.reconnector.attempt} attempts`
|
|
518
804
|
});
|
|
519
805
|
this.reconnector?.reset();
|
|
520
|
-
this.
|
|
806
|
+
this.statusEmitter.set("connected");
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (this.gateway?.isStopped) {
|
|
810
|
+
this.statusEmitter.set("disconnected");
|
|
811
|
+
return;
|
|
521
812
|
}
|
|
522
|
-
|
|
813
|
+
this.scheduleReconnect();
|
|
523
814
|
}
|
|
524
815
|
scheduleReconnect() {
|
|
525
816
|
const url = this.gateway?.session.resumeUrl ?? void 0;
|
|
526
817
|
scheduleFlumeReconnect({
|
|
527
818
|
reconnector: this.reconnector,
|
|
528
819
|
log: this.log,
|
|
529
|
-
setStatus: (status) => this.
|
|
820
|
+
setStatus: (status) => this.statusEmitter.set(status),
|
|
530
821
|
retry: () => {
|
|
531
|
-
this.connectInternal(url)
|
|
822
|
+
this.connectInternal(url).catch((err) => {
|
|
823
|
+
const error = safeNormalizeError({ value: err });
|
|
824
|
+
this.log.error({
|
|
825
|
+
action: "reconnect.unhandled",
|
|
826
|
+
message: safeErrorMessage({ error }),
|
|
827
|
+
error
|
|
828
|
+
});
|
|
829
|
+
this.statusEmitter.set("disconnected");
|
|
830
|
+
});
|
|
532
831
|
}
|
|
533
832
|
});
|
|
534
833
|
}
|
|
535
|
-
setStatus(next) {
|
|
536
|
-
if (this.currentStatus === next) return;
|
|
537
|
-
this.log.info({
|
|
538
|
-
action: "status",
|
|
539
|
-
message: `${this.currentStatus} → ${next}`
|
|
540
|
-
});
|
|
541
|
-
this.currentStatus = next;
|
|
542
|
-
this.options.onStatus?.(next);
|
|
543
|
-
}
|
|
544
834
|
};
|
|
545
835
|
//#endregion
|
|
546
|
-
export {
|
|
836
|
+
export { FlumeDiscordGatewayIntents, FlumeDiscordSource, flumeExtractDiscordMeta };
|