@interactive-inc/flume 0.1.0 → 0.2.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 +98 -46
- package/dist/connection-error-BOk97djj.d.ts +6 -0
- package/dist/discord.d.ts +111 -4
- package/dist/discord.js +537 -2
- package/dist/github.d.ts +50 -4
- package/dist/github.js +307 -2
- package/dist/http-error-BtXonO-W.js +12 -0
- package/dist/http-error-K-Ym4lfK.d.ts +11 -0
- package/dist/index.d.ts +48 -220
- package/dist/index.js +90 -52
- package/dist/logger-CpGB9WO_.js +49 -0
- package/dist/parse-error-BAiCLRmk.d.ts +6 -0
- package/dist/safe-fetch-30ZzOKHL.js +20 -0
- package/dist/safe-json-parse-WCg_x1JS.js +15 -0
- package/dist/{safe-json-parse-D8t_4Vm_.js → schedule-reconnect-DSxZJG3h.js} +24 -12
- package/dist/serial-queue-ExmlnpzQ.js +16 -0
- package/dist/slack.d.ts +65 -4
- package/dist/slack.js +443 -2
- package/dist/{types-BVQSU336.d.ts → types-tnOPBc1p.d.ts} +32 -16
- package/package.json +1 -1
- package/dist/discord-source-Q4JRIbNs.js +0 -525
- package/dist/github-source-D7Z1RUbe.js +0 -280
- package/dist/logger-B9E8zvgj.js +0 -69
- package/dist/slack-source-CszepStG.js +0 -400
package/dist/discord.js
CHANGED
|
@@ -1,2 +1,537 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
|
|
1
|
+
import { n as createFlumeDefaultDeps, t as FlumeLogger } from "./logger-CpGB9WO_.js";
|
|
2
|
+
import { a as FlumeConnectionError, i as FlumeParseError, n as FlumeReconnector, r as resolveFlumeReconnectConfig, t as scheduleFlumeReconnect } from "./schedule-reconnect-DSxZJG3h.js";
|
|
3
|
+
import { t as FlumeSerialQueue } from "./serial-queue-ExmlnpzQ.js";
|
|
4
|
+
import { n as isRecord, t as safeJsonParse } from "./safe-json-parse-WCg_x1JS.js";
|
|
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
|
+
//#region lib/discord/discord-gateway-session.ts
|
|
49
|
+
var FlumeDiscordGatewaySession = class FlumeDiscordGatewaySession {
|
|
50
|
+
sessionId;
|
|
51
|
+
resumeUrl;
|
|
52
|
+
seq;
|
|
53
|
+
constructor(props) {
|
|
54
|
+
this.sessionId = props.sessionId;
|
|
55
|
+
this.resumeUrl = props.resumeUrl;
|
|
56
|
+
this.seq = props.seq;
|
|
57
|
+
Object.freeze(this);
|
|
58
|
+
}
|
|
59
|
+
static empty() {
|
|
60
|
+
return new FlumeDiscordGatewaySession({
|
|
61
|
+
sessionId: null,
|
|
62
|
+
resumeUrl: null,
|
|
63
|
+
seq: null
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
canResume() {
|
|
67
|
+
return this.sessionId !== null;
|
|
68
|
+
}
|
|
69
|
+
withSeq(seq) {
|
|
70
|
+
return new FlumeDiscordGatewaySession({
|
|
71
|
+
sessionId: this.sessionId,
|
|
72
|
+
resumeUrl: this.resumeUrl,
|
|
73
|
+
seq
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
withReady(sessionId, resumeUrl) {
|
|
77
|
+
return new FlumeDiscordGatewaySession({
|
|
78
|
+
sessionId,
|
|
79
|
+
resumeUrl,
|
|
80
|
+
seq: this.seq
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
withReset() {
|
|
84
|
+
return FlumeDiscordGatewaySession.empty();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
//#endregion
|
|
88
|
+
//#region lib/discord/discord-gateway-message-schema.ts
|
|
89
|
+
const FlumeGatewayMessageSchema = z.object({
|
|
90
|
+
op: z.number(),
|
|
91
|
+
d: z.record(z.string(), z.unknown()).nullable(),
|
|
92
|
+
s: z.number().nullable(),
|
|
93
|
+
t: z.string().nullable()
|
|
94
|
+
});
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region lib/discord/parse-discord-gateway-message.ts
|
|
97
|
+
function parseDiscordGatewayMessage(raw) {
|
|
98
|
+
const json = safeJsonParse(raw);
|
|
99
|
+
const parsed = FlumeGatewayMessageSchema.safeParse(json);
|
|
100
|
+
if (!parsed.success) return new FlumeParseError(`invalid gateway message: ${raw.slice(0, 200)}`);
|
|
101
|
+
return parsed.data;
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region lib/discord/discord-gateway.ts
|
|
105
|
+
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
|
+
const OP_DISPATCH = 0;
|
|
110
|
+
const OP_HEARTBEAT = 1;
|
|
111
|
+
const OP_IDENTIFY = 2;
|
|
112
|
+
const OP_RESUME = 6;
|
|
113
|
+
const OP_RECONNECT = 7;
|
|
114
|
+
const OP_INVALID_SESSION = 9;
|
|
115
|
+
const OP_HELLO = 10;
|
|
116
|
+
const OP_HEARTBEAT_ACK = 11;
|
|
117
|
+
const OP_NAMES = {
|
|
118
|
+
[OP_DISPATCH]: "DISPATCH",
|
|
119
|
+
[OP_HEARTBEAT]: "HEARTBEAT",
|
|
120
|
+
[OP_IDENTIFY]: "IDENTIFY",
|
|
121
|
+
[OP_RESUME]: "RESUME",
|
|
122
|
+
[OP_RECONNECT]: "RECONNECT",
|
|
123
|
+
[OP_INVALID_SESSION]: "INVALID_SESSION",
|
|
124
|
+
[OP_HELLO]: "HELLO",
|
|
125
|
+
[OP_HEARTBEAT_ACK]: "HEARTBEAT_ACK"
|
|
126
|
+
};
|
|
127
|
+
var FlumeDiscordGateway = class {
|
|
128
|
+
props;
|
|
129
|
+
log;
|
|
130
|
+
ws = null;
|
|
131
|
+
heartbeat = null;
|
|
132
|
+
session = FlumeDiscordGatewaySession.empty();
|
|
133
|
+
stopped = false;
|
|
134
|
+
pendingResolve = null;
|
|
135
|
+
pendingResolved = false;
|
|
136
|
+
constructor(props) {
|
|
137
|
+
this.props = props;
|
|
138
|
+
this.log = new FlumeLogger({
|
|
139
|
+
source: "discord.gateway",
|
|
140
|
+
handler: props.onLog,
|
|
141
|
+
deps: props.deps
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
connect(url) {
|
|
145
|
+
const target = url ?? GATEWAY_URL;
|
|
146
|
+
this.log.info({
|
|
147
|
+
action: "connect.start",
|
|
148
|
+
message: `url=${new URL(target).hostname}`
|
|
149
|
+
});
|
|
150
|
+
this.pendingResolved = false;
|
|
151
|
+
return new Promise((resolve) => {
|
|
152
|
+
this.pendingResolve = resolve;
|
|
153
|
+
const socket = new this.props.deps.WebSocket(target);
|
|
154
|
+
this.ws = socket;
|
|
155
|
+
socket.addEventListener("message", (ev) => this.onMessage(String(ev.data), socket));
|
|
156
|
+
socket.addEventListener("close", (ev) => this.onClose(ev));
|
|
157
|
+
socket.addEventListener("error", () => this.onError());
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
disconnect() {
|
|
161
|
+
this.log.info({
|
|
162
|
+
action: "disconnect",
|
|
163
|
+
message: "shutting down gateway"
|
|
164
|
+
});
|
|
165
|
+
this.stopped = true;
|
|
166
|
+
this.heartbeat?.stop();
|
|
167
|
+
if (this.ws) {
|
|
168
|
+
this.ws.close(1e3, "shutdown");
|
|
169
|
+
this.ws = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
isConnected() {
|
|
173
|
+
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
174
|
+
}
|
|
175
|
+
completeConnect(error) {
|
|
176
|
+
if (this.pendingResolved || !this.pendingResolve) return;
|
|
177
|
+
this.pendingResolved = true;
|
|
178
|
+
this.pendingResolve(error);
|
|
179
|
+
}
|
|
180
|
+
onMessage(raw, socket) {
|
|
181
|
+
this.log.debug({
|
|
182
|
+
action: "ws.recv",
|
|
183
|
+
message: framePreview(raw)
|
|
184
|
+
});
|
|
185
|
+
const parsed = parseDiscordGatewayMessage(raw);
|
|
186
|
+
if (parsed instanceof FlumeParseError) {
|
|
187
|
+
this.log.error({
|
|
188
|
+
action: "ws.parse-error",
|
|
189
|
+
message: parsed.message,
|
|
190
|
+
error: parsed
|
|
191
|
+
});
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.log.debug({
|
|
195
|
+
action: "ws.frame",
|
|
196
|
+
message: `op=${OP_NAMES[parsed.op] ?? parsed.op} t=${parsed.t ?? "-"} s=${parsed.s ?? "-"}`,
|
|
197
|
+
detail: {
|
|
198
|
+
op: parsed.op,
|
|
199
|
+
t: parsed.t,
|
|
200
|
+
s: parsed.s
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
if (parsed.s !== null) this.session = this.session.withSeq(parsed.s);
|
|
204
|
+
if (parsed.op === OP_HELLO) return this.onHello(parsed);
|
|
205
|
+
if (parsed.op === OP_HEARTBEAT_ACK) return this.onHeartbeatAck();
|
|
206
|
+
if (parsed.op === OP_HEARTBEAT) return this.onHeartbeatRequest();
|
|
207
|
+
if (parsed.op === OP_RECONNECT) return this.onReconnectRequest(socket);
|
|
208
|
+
if (parsed.op === OP_INVALID_SESSION) return this.onInvalidSession(parsed, socket);
|
|
209
|
+
if (parsed.op === OP_DISPATCH) return this.onDispatch(parsed);
|
|
210
|
+
this.log.warn({
|
|
211
|
+
action: "ws.unknown-op",
|
|
212
|
+
message: `unknown op=${parsed.op}`,
|
|
213
|
+
detail: { op: parsed.op }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
onHello(msg) {
|
|
217
|
+
const interval = typeof msg.d?.heartbeat_interval === "number" ? msg.d.heartbeat_interval : 0;
|
|
218
|
+
this.log.info({
|
|
219
|
+
action: "hello",
|
|
220
|
+
message: `heartbeat_interval=${interval}ms`
|
|
221
|
+
});
|
|
222
|
+
this.heartbeat = new FlumeDiscordHeartbeat({
|
|
223
|
+
deps: this.props.deps,
|
|
224
|
+
onSend: () => {
|
|
225
|
+
this.log.debug({
|
|
226
|
+
action: "heartbeat.send",
|
|
227
|
+
message: `seq=${this.session.seq}`
|
|
228
|
+
});
|
|
229
|
+
this.send({
|
|
230
|
+
op: OP_HEARTBEAT,
|
|
231
|
+
d: this.session.seq
|
|
232
|
+
});
|
|
233
|
+
},
|
|
234
|
+
onZombie: () => {
|
|
235
|
+
this.log.warn({
|
|
236
|
+
action: "heartbeat.zombie",
|
|
237
|
+
message: "no ACK received, closing connection"
|
|
238
|
+
});
|
|
239
|
+
this.ws?.close(4009, "zombie connection");
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
this.heartbeat.start(interval);
|
|
243
|
+
if (this.session.canResume()) this.sendResume();
|
|
244
|
+
else this.sendIdentify();
|
|
245
|
+
}
|
|
246
|
+
onHeartbeatAck() {
|
|
247
|
+
this.log.debug({
|
|
248
|
+
action: "heartbeat.ack",
|
|
249
|
+
message: "received"
|
|
250
|
+
});
|
|
251
|
+
this.heartbeat?.ack();
|
|
252
|
+
}
|
|
253
|
+
onHeartbeatRequest() {
|
|
254
|
+
this.log.debug({
|
|
255
|
+
action: "heartbeat.requested",
|
|
256
|
+
message: "server requested heartbeat"
|
|
257
|
+
});
|
|
258
|
+
this.send({
|
|
259
|
+
op: OP_HEARTBEAT,
|
|
260
|
+
d: this.session.seq
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
onReconnectRequest(socket) {
|
|
264
|
+
this.log.info({
|
|
265
|
+
action: "reconnect.requested",
|
|
266
|
+
message: "server requested reconnect"
|
|
267
|
+
});
|
|
268
|
+
socket.close(4e3, "reconnect requested");
|
|
269
|
+
}
|
|
270
|
+
onInvalidSession(msg, socket) {
|
|
271
|
+
const resumable = !!msg.d;
|
|
272
|
+
this.log.warn({
|
|
273
|
+
action: "invalid-session",
|
|
274
|
+
message: `resumable=${resumable}`
|
|
275
|
+
});
|
|
276
|
+
this.session = this.session.withReset();
|
|
277
|
+
if (resumable) {
|
|
278
|
+
const delay = 1e3 + this.props.deps.random() * 4e3;
|
|
279
|
+
this.log.info({
|
|
280
|
+
action: "identify.delayed",
|
|
281
|
+
message: `re-identify in ${Math.round(delay)}ms`
|
|
282
|
+
});
|
|
283
|
+
this.props.deps.setTimeout(() => this.sendIdentify(), delay);
|
|
284
|
+
} else socket.close(4e3, "invalid session");
|
|
285
|
+
}
|
|
286
|
+
onDispatch(msg) {
|
|
287
|
+
if (msg.t === "READY" && msg.d) {
|
|
288
|
+
const sessionId = typeof msg.d.session_id === "string" ? msg.d.session_id : "";
|
|
289
|
+
const resumeUrl = typeof msg.d.resume_gateway_url === "string" ? msg.d.resume_gateway_url : "";
|
|
290
|
+
this.session = this.session.withReady(sessionId, resumeUrl);
|
|
291
|
+
this.log.info({
|
|
292
|
+
action: "ready",
|
|
293
|
+
message: `session=${sessionId}`
|
|
294
|
+
});
|
|
295
|
+
this.props.onStatus("connected");
|
|
296
|
+
this.completeConnect(null);
|
|
297
|
+
}
|
|
298
|
+
if (msg.t === "RESUMED") {
|
|
299
|
+
this.log.info({
|
|
300
|
+
action: "resumed",
|
|
301
|
+
message: `session=${this.session.sessionId} seq=${this.session.seq}`
|
|
302
|
+
});
|
|
303
|
+
this.props.onStatus("connected");
|
|
304
|
+
this.completeConnect(null);
|
|
305
|
+
}
|
|
306
|
+
if (msg.t && msg.d) this.props.onDispatch(msg.t, msg.d);
|
|
307
|
+
else if (msg.t) this.props.onDispatch(msg.t, {});
|
|
308
|
+
}
|
|
309
|
+
onClose(ev) {
|
|
310
|
+
this.log.info({
|
|
311
|
+
action: "ws.close",
|
|
312
|
+
message: `code=${ev.code} reason=${ev.reason || "none"}`,
|
|
313
|
+
detail: {
|
|
314
|
+
code: ev.code,
|
|
315
|
+
reason: ev.reason
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
this.ws = null;
|
|
319
|
+
this.heartbeat?.stop();
|
|
320
|
+
this.props.onStatus("disconnected");
|
|
321
|
+
this.completeConnect(new FlumeConnectionError(`WebSocket closed before ready (code=${ev.code})`));
|
|
322
|
+
}
|
|
323
|
+
onError() {
|
|
324
|
+
this.log.error({
|
|
325
|
+
action: "ws.error",
|
|
326
|
+
message: "WebSocket error event"
|
|
327
|
+
});
|
|
328
|
+
this.completeConnect(new FlumeConnectionError("WebSocket connection error"));
|
|
329
|
+
}
|
|
330
|
+
send(input) {
|
|
331
|
+
const payload = JSON.stringify({
|
|
332
|
+
op: input.op,
|
|
333
|
+
d: input.d ?? null
|
|
334
|
+
});
|
|
335
|
+
this.log.debug({
|
|
336
|
+
action: "ws.send",
|
|
337
|
+
message: `op=${OP_NAMES[input.op] ?? input.op}`,
|
|
338
|
+
detail: { op: input.op }
|
|
339
|
+
});
|
|
340
|
+
this.ws?.send(payload);
|
|
341
|
+
this.log.debug({
|
|
342
|
+
action: "ws.sent",
|
|
343
|
+
message: framePreview(payload)
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
sendIdentify() {
|
|
347
|
+
this.log.info({
|
|
348
|
+
action: "identify",
|
|
349
|
+
message: `intents=${this.props.intents}`
|
|
350
|
+
});
|
|
351
|
+
this.send({
|
|
352
|
+
op: OP_IDENTIFY,
|
|
353
|
+
d: {
|
|
354
|
+
token: this.props.token,
|
|
355
|
+
intents: this.props.intents,
|
|
356
|
+
properties: {
|
|
357
|
+
os: "linux",
|
|
358
|
+
browser: "open-flume",
|
|
359
|
+
device: "open-flume"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
sendResume() {
|
|
365
|
+
this.log.info({
|
|
366
|
+
action: "resume",
|
|
367
|
+
message: `session=${this.session.sessionId} seq=${this.session.seq}`
|
|
368
|
+
});
|
|
369
|
+
this.send({
|
|
370
|
+
op: OP_RESUME,
|
|
371
|
+
d: {
|
|
372
|
+
token: this.props.token,
|
|
373
|
+
session_id: this.session.sessionId,
|
|
374
|
+
seq: this.session.seq
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
//#endregion
|
|
380
|
+
//#region lib/discord/discord-gateway-intents.ts
|
|
381
|
+
const FlumeDiscordGatewayIntents = {
|
|
382
|
+
Guilds: 1,
|
|
383
|
+
GuildMembers: 2,
|
|
384
|
+
GuildModeration: 4,
|
|
385
|
+
GuildExpressions: 8,
|
|
386
|
+
GuildIntegrations: 16,
|
|
387
|
+
GuildWebhooks: 32,
|
|
388
|
+
GuildInvites: 64,
|
|
389
|
+
GuildVoiceStates: 128,
|
|
390
|
+
GuildPresences: 256,
|
|
391
|
+
GuildMessages: 512,
|
|
392
|
+
GuildMessageReactions: 1024,
|
|
393
|
+
GuildMessageTyping: 2048,
|
|
394
|
+
DirectMessages: 4096,
|
|
395
|
+
DirectMessageReactions: 8192,
|
|
396
|
+
DirectMessageTyping: 16384,
|
|
397
|
+
MessageContent: 32768,
|
|
398
|
+
GuildScheduledEvents: 65536,
|
|
399
|
+
AutoModerationConfiguration: 1 << 20,
|
|
400
|
+
AutoModerationExecution: 1 << 21,
|
|
401
|
+
GuildMessagePolls: 1 << 24,
|
|
402
|
+
DirectMessagePolls: 1 << 25
|
|
403
|
+
};
|
|
404
|
+
//#endregion
|
|
405
|
+
//#region lib/discord/discord-source.ts
|
|
406
|
+
const DEFAULT_INTENTS = FlumeDiscordGatewayIntents.Guilds | FlumeDiscordGatewayIntents.GuildMessages | FlumeDiscordGatewayIntents.DirectMessages | FlumeDiscordGatewayIntents.MessageContent;
|
|
407
|
+
var FlumeDiscordSource = class {
|
|
408
|
+
options;
|
|
409
|
+
name = "discord";
|
|
410
|
+
gateway = null;
|
|
411
|
+
reconnector = null;
|
|
412
|
+
handler = null;
|
|
413
|
+
currentStatus = "disconnected";
|
|
414
|
+
log;
|
|
415
|
+
deps;
|
|
416
|
+
queue = new FlumeSerialQueue();
|
|
417
|
+
constructor(options) {
|
|
418
|
+
this.options = options;
|
|
419
|
+
this.deps = options.deps ?? createFlumeDefaultDeps();
|
|
420
|
+
this.log = new FlumeLogger({
|
|
421
|
+
source: "discord",
|
|
422
|
+
handler: options.onLog,
|
|
423
|
+
deps: this.deps
|
|
424
|
+
});
|
|
425
|
+
const rc = resolveFlumeReconnectConfig(options.reconnect);
|
|
426
|
+
if (rc) this.reconnector = new FlumeReconnector({
|
|
427
|
+
...rc,
|
|
428
|
+
deps: this.deps
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
async start(handler) {
|
|
432
|
+
if (this.options.signal?.aborted) return /* @__PURE__ */ new Error("Discord source: signal already aborted");
|
|
433
|
+
this.options.signal?.addEventListener("abort", () => this.stop(), { once: true });
|
|
434
|
+
this.handler = handler;
|
|
435
|
+
this.log.info({
|
|
436
|
+
action: "start",
|
|
437
|
+
message: "starting Discord source"
|
|
438
|
+
});
|
|
439
|
+
return this.connectInternal();
|
|
440
|
+
}
|
|
441
|
+
async stop() {
|
|
442
|
+
this.log.info({
|
|
443
|
+
action: "stop",
|
|
444
|
+
message: "stopping Discord source"
|
|
445
|
+
});
|
|
446
|
+
if (this.reconnector && !this.reconnector.aborted) this.log.debug({
|
|
447
|
+
action: "reconnect.cancel",
|
|
448
|
+
message: "aborting reconnector"
|
|
449
|
+
});
|
|
450
|
+
this.reconnector?.cancel();
|
|
451
|
+
this.gateway?.disconnect();
|
|
452
|
+
this.gateway = null;
|
|
453
|
+
this.handler = null;
|
|
454
|
+
await this.queue.drain();
|
|
455
|
+
this.setStatus("disconnected");
|
|
456
|
+
}
|
|
457
|
+
status() {
|
|
458
|
+
return this.currentStatus;
|
|
459
|
+
}
|
|
460
|
+
async connectInternal(resumeUrl) {
|
|
461
|
+
this.setStatus("connecting");
|
|
462
|
+
this.gateway = new FlumeDiscordGateway({
|
|
463
|
+
token: this.options.token,
|
|
464
|
+
intents: this.options.intents ?? DEFAULT_INTENTS,
|
|
465
|
+
onLog: this.options.onLog,
|
|
466
|
+
deps: this.deps,
|
|
467
|
+
onDispatch: (eventName, eventData) => this.handleDispatch(eventName, eventData),
|
|
468
|
+
onStatus: (status) => this.handleGatewayStatus(status)
|
|
469
|
+
});
|
|
470
|
+
const error = await this.gateway.connect(resumeUrl);
|
|
471
|
+
if (error instanceof FlumeConnectionError) {
|
|
472
|
+
this.log.error({
|
|
473
|
+
action: "connect.failed",
|
|
474
|
+
message: error.message,
|
|
475
|
+
error
|
|
476
|
+
});
|
|
477
|
+
if (!this.reconnector || this.reconnector.aborted) {
|
|
478
|
+
this.setStatus("disconnected");
|
|
479
|
+
return error;
|
|
480
|
+
}
|
|
481
|
+
this.scheduleReconnect();
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
handleDispatch(eventName, eventData) {
|
|
485
|
+
const event = {
|
|
486
|
+
source: "discord",
|
|
487
|
+
type: eventName,
|
|
488
|
+
data: eventData,
|
|
489
|
+
meta: extractDiscordMeta(eventName, eventData),
|
|
490
|
+
receivedAt: this.deps.now()
|
|
491
|
+
};
|
|
492
|
+
this.queue.add(async () => {
|
|
493
|
+
try {
|
|
494
|
+
await this.handler?.(event);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
this.log.error({
|
|
497
|
+
action: "handler.error",
|
|
498
|
+
message: "user handler threw",
|
|
499
|
+
error: err instanceof Error ? err : new Error(String(err))
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
handleGatewayStatus(status) {
|
|
505
|
+
if (status === "connected") {
|
|
506
|
+
if (this.reconnector && this.reconnector.attempt > 0) this.log.info({
|
|
507
|
+
action: "reconnect.reset",
|
|
508
|
+
message: `cleared ${this.reconnector.attempt} attempts`
|
|
509
|
+
});
|
|
510
|
+
this.reconnector?.reset();
|
|
511
|
+
this.setStatus("connected");
|
|
512
|
+
}
|
|
513
|
+
if (status === "disconnected" && !this.gateway?.stopped) this.scheduleReconnect();
|
|
514
|
+
}
|
|
515
|
+
scheduleReconnect() {
|
|
516
|
+
const url = this.gateway?.session.resumeUrl ?? void 0;
|
|
517
|
+
scheduleFlumeReconnect({
|
|
518
|
+
reconnector: this.reconnector,
|
|
519
|
+
log: this.log,
|
|
520
|
+
setStatus: (status) => this.setStatus(status),
|
|
521
|
+
retry: () => {
|
|
522
|
+
this.connectInternal(url);
|
|
523
|
+
}
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
setStatus(next) {
|
|
527
|
+
if (this.currentStatus === next) return;
|
|
528
|
+
this.log.info({
|
|
529
|
+
action: "status",
|
|
530
|
+
message: `${this.currentStatus} → ${next}`
|
|
531
|
+
});
|
|
532
|
+
this.currentStatus = next;
|
|
533
|
+
this.options.onStatus?.(next);
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
//#endregion
|
|
537
|
+
export { FlumeDiscordGateway, FlumeDiscordGatewayIntents, FlumeDiscordGatewaySession, FlumeDiscordHeartbeat, FlumeDiscordSource, FlumeGatewayMessageSchema, extractDiscordMeta, parseDiscordGatewayMessage };
|
package/dist/github.d.ts
CHANGED
|
@@ -1,19 +1,65 @@
|
|
|
1
|
-
import { a as FlumeGitHubSourceOptions, i as FlumeGitHubNotification, o as FlumeHandler,
|
|
1
|
+
import { E as FlumeGitHubNotificationSchema, a as FlumeGitHubSourceOptions, c as FlumeLogHandler, i as FlumeGitHubNotification, o as FlumeHandler, p as FlumeRuntimeDeps, x as FlumeStatus } from "./types-tnOPBc1p.js";
|
|
2
2
|
|
|
3
3
|
//#region lib/github/github-source.d.ts
|
|
4
4
|
declare class FlumeGitHubSource {
|
|
5
5
|
private readonly options;
|
|
6
|
+
readonly name: "github";
|
|
6
7
|
private poller;
|
|
7
8
|
private currentStatus;
|
|
8
9
|
private readonly log;
|
|
9
10
|
private readonly deps;
|
|
11
|
+
private readonly queue;
|
|
10
12
|
constructor(options: FlumeGitHubSourceOptions);
|
|
11
|
-
start(handler: FlumeHandler): Promise<void>;
|
|
13
|
+
start(handler: FlumeHandler): Promise<void | Error>;
|
|
12
14
|
stop(): Promise<void>;
|
|
13
15
|
status(): FlumeStatus;
|
|
14
16
|
private handleNotifications;
|
|
15
17
|
private setStatus;
|
|
16
|
-
static extractMeta(notification: FlumeGitHubNotification): Record<string, string>;
|
|
17
18
|
}
|
|
18
19
|
//#endregion
|
|
19
|
-
|
|
20
|
+
//#region lib/github/extract-github-meta.d.ts
|
|
21
|
+
declare function extractGitHubMeta(notification: FlumeGitHubNotification): Record<string, string>;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region lib/github/github-poller.d.ts
|
|
24
|
+
type Deps = Pick<FlumeRuntimeDeps, "fetch" | "setInterval" | "clearInterval" | "now">;
|
|
25
|
+
type Props$1 = {
|
|
26
|
+
token: string;
|
|
27
|
+
interval: number;
|
|
28
|
+
onNotifications: (notifications: FlumeGitHubNotification[]) => void;
|
|
29
|
+
onConnected: () => void;
|
|
30
|
+
onDisconnected: (detail: string) => void;
|
|
31
|
+
onLog?: FlumeLogHandler;
|
|
32
|
+
deps: Deps;
|
|
33
|
+
};
|
|
34
|
+
declare class FlumeGitHubPoller {
|
|
35
|
+
private readonly props;
|
|
36
|
+
private readonly log;
|
|
37
|
+
private readonly cache;
|
|
38
|
+
private timer;
|
|
39
|
+
private since;
|
|
40
|
+
private bootstrapped;
|
|
41
|
+
stopped: boolean;
|
|
42
|
+
private consecutiveErrors;
|
|
43
|
+
constructor(props: Props$1);
|
|
44
|
+
start(): Promise<void>;
|
|
45
|
+
stop(): void;
|
|
46
|
+
private poll;
|
|
47
|
+
private processNotifications;
|
|
48
|
+
private safeFetch;
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
//#region lib/github/github-seen-cache.d.ts
|
|
52
|
+
type Props = {
|
|
53
|
+
maxSize: number;
|
|
54
|
+
};
|
|
55
|
+
declare class FlumeGitHubSeenCache {
|
|
56
|
+
private readonly props;
|
|
57
|
+
private seen;
|
|
58
|
+
constructor(props: Props);
|
|
59
|
+
has(id: string, updatedAt: string): boolean;
|
|
60
|
+
add(id: string, updatedAt: string): void;
|
|
61
|
+
trim(): void;
|
|
62
|
+
get size(): number;
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { FlumeGitHubNotificationSchema, FlumeGitHubPoller, FlumeGitHubSeenCache, FlumeGitHubSource, extractGitHubMeta };
|