@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
|
@@ -1,400 +0,0 @@
|
|
|
1
|
-
import { a as FlumeSlackEnvelopeSchema, i as FlumeSlackConnectionResponseSchema, t as FlumeLogger } from "./logger-B9E8zvgj.js";
|
|
2
|
-
import { a as FlumeParseError, i as resolveFlumeReconnectConfig, n as isRecord, o as FlumeConnectionError, r as FlumeReconnector, t as safeJsonParse } from "./safe-json-parse-D8t_4Vm_.js";
|
|
3
|
-
//#region lib/errors/http-error.ts
|
|
4
|
-
var FlumeHttpError = class extends Error {
|
|
5
|
-
status;
|
|
6
|
-
constructor(props) {
|
|
7
|
-
super(props.message);
|
|
8
|
-
this.name = "FlumeHttpError";
|
|
9
|
-
this.status = props.status;
|
|
10
|
-
Object.freeze(this);
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region lib/slack/obtain-slack-url.ts
|
|
15
|
-
async function obtainSlackUrl(props) {
|
|
16
|
-
const log = new FlumeLogger({
|
|
17
|
-
source: "slack.url",
|
|
18
|
-
handler: props.onLog,
|
|
19
|
-
deps: props.deps
|
|
20
|
-
});
|
|
21
|
-
const url = "https://slack.com/api/apps.connections.open";
|
|
22
|
-
log.debug({
|
|
23
|
-
action: "http.request",
|
|
24
|
-
message: `POST ${url}`
|
|
25
|
-
});
|
|
26
|
-
const response = await safeFetch(props, url, log);
|
|
27
|
-
if (response instanceof FlumeHttpError) return response;
|
|
28
|
-
log.debug({
|
|
29
|
-
action: "http.response",
|
|
30
|
-
message: `POST ${response.status}`,
|
|
31
|
-
detail: {
|
|
32
|
-
status: response.status,
|
|
33
|
-
url
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
const raw = await response.json();
|
|
37
|
-
log.debug({
|
|
38
|
-
action: "http.body",
|
|
39
|
-
message: "apps.connections.open response",
|
|
40
|
-
detail: {
|
|
41
|
-
ok: isOk(raw),
|
|
42
|
-
error: errorField(raw)
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
const parsed = FlumeSlackConnectionResponseSchema.safeParse(raw);
|
|
46
|
-
if (!parsed.success) {
|
|
47
|
-
log.warn({
|
|
48
|
-
action: "parse.fail",
|
|
49
|
-
message: "apps.connections.open: invalid response shape",
|
|
50
|
-
detail: { issues: parsed.error.issues.map((i) => ({
|
|
51
|
-
path: i.path,
|
|
52
|
-
message: i.message
|
|
53
|
-
})) }
|
|
54
|
-
});
|
|
55
|
-
return new FlumeHttpError({
|
|
56
|
-
message: "apps.connections.open: invalid response shape",
|
|
57
|
-
status: response.status
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
if (!parsed.data.ok || !parsed.data.url) {
|
|
61
|
-
log.warn({
|
|
62
|
-
action: "api.fail",
|
|
63
|
-
message: `apps.connections.open failed: ${parsed.data.error ?? "no url"}`
|
|
64
|
-
});
|
|
65
|
-
return new FlumeHttpError({
|
|
66
|
-
message: `apps.connections.open failed: ${parsed.data.error ?? "no url"}`,
|
|
67
|
-
status: response.status
|
|
68
|
-
});
|
|
69
|
-
}
|
|
70
|
-
log.info({
|
|
71
|
-
action: "url.obtained",
|
|
72
|
-
message: "WSS URL obtained"
|
|
73
|
-
});
|
|
74
|
-
return parsed.data.url;
|
|
75
|
-
}
|
|
76
|
-
function isOk(raw) {
|
|
77
|
-
return raw?.ok;
|
|
78
|
-
}
|
|
79
|
-
function errorField(raw) {
|
|
80
|
-
return raw?.error;
|
|
81
|
-
}
|
|
82
|
-
async function safeFetch(props, url, log) {
|
|
83
|
-
try {
|
|
84
|
-
return await props.deps.fetch(url, {
|
|
85
|
-
method: "POST",
|
|
86
|
-
headers: { Authorization: `Bearer ${props.appToken}` }
|
|
87
|
-
});
|
|
88
|
-
} catch (error) {
|
|
89
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
90
|
-
log.error({
|
|
91
|
-
action: "http.error",
|
|
92
|
-
message: `network error: ${err.message}`,
|
|
93
|
-
error: err
|
|
94
|
-
});
|
|
95
|
-
return new FlumeHttpError({
|
|
96
|
-
message: err.message,
|
|
97
|
-
status: 0
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
//#endregion
|
|
102
|
-
//#region lib/slack/slack-socket-mode.ts
|
|
103
|
-
function framePreview(raw) {
|
|
104
|
-
return raw.length > 200 ? `${raw.slice(0, 200)}... (${raw.length} bytes)` : raw;
|
|
105
|
-
}
|
|
106
|
-
var FlumeSlackSocketMode = class {
|
|
107
|
-
props;
|
|
108
|
-
log;
|
|
109
|
-
ws = null;
|
|
110
|
-
stopped = false;
|
|
111
|
-
pendingResolve = null;
|
|
112
|
-
pendingResolved = false;
|
|
113
|
-
constructor(props) {
|
|
114
|
-
this.props = props;
|
|
115
|
-
this.log = new FlumeLogger({
|
|
116
|
-
source: "slack.socket-mode",
|
|
117
|
-
handler: props.onLog,
|
|
118
|
-
deps: props.deps
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
async connect() {
|
|
122
|
-
this.log.info({
|
|
123
|
-
action: "connect.start",
|
|
124
|
-
message: "opening WebSocket connection"
|
|
125
|
-
});
|
|
126
|
-
const url = await obtainSlackUrl({
|
|
127
|
-
appToken: this.props.appToken,
|
|
128
|
-
onLog: this.props.onLog,
|
|
129
|
-
deps: this.props.deps
|
|
130
|
-
});
|
|
131
|
-
if (url instanceof FlumeHttpError) {
|
|
132
|
-
this.log.error({
|
|
133
|
-
action: "http.error",
|
|
134
|
-
message: url.message,
|
|
135
|
-
error: url
|
|
136
|
-
});
|
|
137
|
-
return url;
|
|
138
|
-
}
|
|
139
|
-
this.log.info({
|
|
140
|
-
action: "url.obtained",
|
|
141
|
-
message: "WebSocket URL obtained"
|
|
142
|
-
});
|
|
143
|
-
return this.openSocket(url);
|
|
144
|
-
}
|
|
145
|
-
disconnect() {
|
|
146
|
-
this.log.info({
|
|
147
|
-
action: "disconnect",
|
|
148
|
-
message: "stopping socket mode"
|
|
149
|
-
});
|
|
150
|
-
this.stopped = true;
|
|
151
|
-
if (this.ws) {
|
|
152
|
-
this.ws.close();
|
|
153
|
-
this.ws = null;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
isConnected() {
|
|
157
|
-
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
|
|
158
|
-
}
|
|
159
|
-
openSocket(url) {
|
|
160
|
-
this.pendingResolved = false;
|
|
161
|
-
return new Promise((resolve) => {
|
|
162
|
-
this.pendingResolve = resolve;
|
|
163
|
-
const socket = new this.props.deps.WebSocket(url);
|
|
164
|
-
this.ws = socket;
|
|
165
|
-
socket.addEventListener("message", (ev) => this.onMessage(String(ev.data), socket));
|
|
166
|
-
socket.addEventListener("close", (ev) => this.onClose(ev));
|
|
167
|
-
socket.addEventListener("error", () => this.onError());
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
completeConnect(error) {
|
|
171
|
-
if (this.pendingResolved || !this.pendingResolve) return;
|
|
172
|
-
this.pendingResolved = true;
|
|
173
|
-
this.pendingResolve(error);
|
|
174
|
-
}
|
|
175
|
-
onMessage(raw, socket) {
|
|
176
|
-
this.log.debug({
|
|
177
|
-
action: "ws.recv",
|
|
178
|
-
message: framePreview(raw)
|
|
179
|
-
});
|
|
180
|
-
const json = safeJsonParse(raw);
|
|
181
|
-
if (!isRecord(json)) {
|
|
182
|
-
this.log.error({
|
|
183
|
-
action: "ws.parse-error",
|
|
184
|
-
message: "invalid JSON",
|
|
185
|
-
error: new FlumeParseError(raw.slice(0, 200))
|
|
186
|
-
});
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (json.type === "hello") {
|
|
190
|
-
this.log.info({
|
|
191
|
-
action: "ws.hello",
|
|
192
|
-
message: "connection ready"
|
|
193
|
-
});
|
|
194
|
-
this.props.onConnected();
|
|
195
|
-
this.completeConnect(null);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (json.type === "disconnect") {
|
|
199
|
-
const reason = typeof json.reason === "string" ? json.reason : "unknown";
|
|
200
|
-
this.log.info({
|
|
201
|
-
action: "ws.disconnect-requested",
|
|
202
|
-
message: `reason=${reason}`,
|
|
203
|
-
detail: { reason }
|
|
204
|
-
});
|
|
205
|
-
socket.close();
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
if (typeof json.envelope_id === "string") {
|
|
209
|
-
this.log.debug({
|
|
210
|
-
action: "ws.ack",
|
|
211
|
-
message: `envelope_id=${json.envelope_id}`
|
|
212
|
-
});
|
|
213
|
-
const ack = JSON.stringify({ envelope_id: json.envelope_id });
|
|
214
|
-
socket.send(ack);
|
|
215
|
-
this.log.debug({
|
|
216
|
-
action: "ws.send",
|
|
217
|
-
message: framePreview(ack)
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
const envelope = FlumeSlackEnvelopeSchema.safeParse(json);
|
|
221
|
-
if (envelope.success) {
|
|
222
|
-
this.log.debug({
|
|
223
|
-
action: "envelope.recv",
|
|
224
|
-
message: `type=${envelope.data.type} envelope_id=${envelope.data.envelope_id}`,
|
|
225
|
-
detail: {
|
|
226
|
-
type: envelope.data.type,
|
|
227
|
-
envelopeId: envelope.data.envelope_id
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
this.props.onMessage(envelope.data);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
this.log.warn({
|
|
234
|
-
action: "envelope.parse-fail",
|
|
235
|
-
message: "unrecognised envelope shape, dropping",
|
|
236
|
-
detail: {
|
|
237
|
-
type: typeof json.type === "string" ? json.type : "unknown",
|
|
238
|
-
issues: envelope.error.issues.map((i) => ({
|
|
239
|
-
path: i.path,
|
|
240
|
-
message: i.message
|
|
241
|
-
}))
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
onClose(ev) {
|
|
246
|
-
this.log.info({
|
|
247
|
-
action: "ws.close",
|
|
248
|
-
message: `code=${ev.code} reason=${ev.reason || "none"}`
|
|
249
|
-
});
|
|
250
|
-
this.ws = null;
|
|
251
|
-
this.props.onDisconnected();
|
|
252
|
-
this.completeConnect(new FlumeConnectionError(`WebSocket closed before hello (code=${ev.code})`));
|
|
253
|
-
}
|
|
254
|
-
onError() {
|
|
255
|
-
this.log.error({
|
|
256
|
-
action: "ws.error",
|
|
257
|
-
message: "WebSocket error event"
|
|
258
|
-
});
|
|
259
|
-
this.completeConnect(new FlumeConnectionError("WebSocket connection error"));
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
//#endregion
|
|
263
|
-
//#region lib/slack/slack-source.ts
|
|
264
|
-
var FlumeSlackSource = class FlumeSlackSource {
|
|
265
|
-
options;
|
|
266
|
-
socket = null;
|
|
267
|
-
reconnector = null;
|
|
268
|
-
handler = null;
|
|
269
|
-
currentStatus = "disconnected";
|
|
270
|
-
log;
|
|
271
|
-
deps;
|
|
272
|
-
constructor(options) {
|
|
273
|
-
this.options = options;
|
|
274
|
-
this.deps = options.deps;
|
|
275
|
-
this.log = new FlumeLogger({
|
|
276
|
-
source: "slack",
|
|
277
|
-
handler: options.onLog,
|
|
278
|
-
deps: this.deps
|
|
279
|
-
});
|
|
280
|
-
const rc = resolveFlumeReconnectConfig(options.reconnect);
|
|
281
|
-
if (rc) this.reconnector = new FlumeReconnector({
|
|
282
|
-
...rc,
|
|
283
|
-
deps: this.deps
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
async start(handler) {
|
|
287
|
-
if (this.options.signal?.aborted) return;
|
|
288
|
-
this.options.signal?.addEventListener("abort", () => this.stop(), { once: true });
|
|
289
|
-
this.handler = handler;
|
|
290
|
-
this.log.info({
|
|
291
|
-
action: "start",
|
|
292
|
-
message: "starting Slack source"
|
|
293
|
-
});
|
|
294
|
-
await this.connectInternal();
|
|
295
|
-
}
|
|
296
|
-
async stop() {
|
|
297
|
-
this.log.info({
|
|
298
|
-
action: "stop",
|
|
299
|
-
message: "stopping Slack source"
|
|
300
|
-
});
|
|
301
|
-
if (this.reconnector && !this.reconnector.aborted) this.log.debug({
|
|
302
|
-
action: "reconnect.cancel",
|
|
303
|
-
message: "aborting reconnector"
|
|
304
|
-
});
|
|
305
|
-
this.reconnector?.cancel();
|
|
306
|
-
this.socket?.disconnect();
|
|
307
|
-
this.socket = null;
|
|
308
|
-
this.handler = null;
|
|
309
|
-
this.setStatus("disconnected");
|
|
310
|
-
}
|
|
311
|
-
status() {
|
|
312
|
-
return this.currentStatus;
|
|
313
|
-
}
|
|
314
|
-
async connectInternal() {
|
|
315
|
-
this.setStatus("connecting");
|
|
316
|
-
this.socket = new FlumeSlackSocketMode({
|
|
317
|
-
appToken: this.options.appToken,
|
|
318
|
-
onLog: this.options.onLog,
|
|
319
|
-
deps: this.deps,
|
|
320
|
-
onMessage: (envelope) => this.handleMessage(envelope),
|
|
321
|
-
onConnected: () => {
|
|
322
|
-
if (this.reconnector && this.reconnector.attempt > 0) this.log.info({
|
|
323
|
-
action: "reconnect.reset",
|
|
324
|
-
message: `cleared ${this.reconnector.attempt} attempts`
|
|
325
|
-
});
|
|
326
|
-
this.reconnector?.reset();
|
|
327
|
-
this.setStatus("connected");
|
|
328
|
-
},
|
|
329
|
-
onDisconnected: () => {
|
|
330
|
-
if (!this.socket?.stopped) this.scheduleReconnect();
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
const error = await this.socket.connect();
|
|
334
|
-
if (error instanceof Error) {
|
|
335
|
-
this.log.error({
|
|
336
|
-
action: "connect.failed",
|
|
337
|
-
message: error.message,
|
|
338
|
-
error
|
|
339
|
-
});
|
|
340
|
-
this.scheduleReconnect();
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
handleMessage(envelope) {
|
|
344
|
-
const event = {
|
|
345
|
-
source: "slack",
|
|
346
|
-
type: envelope.type,
|
|
347
|
-
data: envelope.payload,
|
|
348
|
-
meta: FlumeSlackSource.extractMeta(envelope),
|
|
349
|
-
receivedAt: this.deps.now()
|
|
350
|
-
};
|
|
351
|
-
try {
|
|
352
|
-
this.handler?.(event);
|
|
353
|
-
} catch (err) {
|
|
354
|
-
this.log.error({
|
|
355
|
-
action: "handler.error",
|
|
356
|
-
message: "user handler threw",
|
|
357
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
scheduleReconnect() {
|
|
362
|
-
if (!this.reconnector || this.reconnector.aborted) {
|
|
363
|
-
this.setStatus("disconnected");
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
this.setStatus("reconnecting");
|
|
367
|
-
const delay = this.reconnector.schedule(() => this.connectInternal());
|
|
368
|
-
if (delay === -1) {
|
|
369
|
-
this.log.error({
|
|
370
|
-
action: "reconnect.exhausted",
|
|
371
|
-
message: `gave up after ${this.reconnector.attempt} attempts`
|
|
372
|
-
});
|
|
373
|
-
this.setStatus("disconnected");
|
|
374
|
-
} else this.log.info({
|
|
375
|
-
action: "reconnect.scheduled",
|
|
376
|
-
message: `next attempt in ${Math.round(delay)}ms`
|
|
377
|
-
});
|
|
378
|
-
}
|
|
379
|
-
setStatus(next) {
|
|
380
|
-
if (this.currentStatus === next) return;
|
|
381
|
-
this.log.info({
|
|
382
|
-
action: "status",
|
|
383
|
-
message: `${this.currentStatus} → ${next}`
|
|
384
|
-
});
|
|
385
|
-
this.currentStatus = next;
|
|
386
|
-
this.options.onStatus?.(next);
|
|
387
|
-
}
|
|
388
|
-
static extractMeta(envelope) {
|
|
389
|
-
const meta = { event_type: envelope.type };
|
|
390
|
-
const eventPayload = isRecord(envelope.payload.event) ? envelope.payload.event : null;
|
|
391
|
-
if (!eventPayload) return meta;
|
|
392
|
-
if (typeof eventPayload.channel === "string") meta.channel_id = eventPayload.channel;
|
|
393
|
-
if (typeof eventPayload.user === "string") meta.user_id = eventPayload.user;
|
|
394
|
-
if (typeof eventPayload.thread_ts === "string") meta.thread_ts = eventPayload.thread_ts;
|
|
395
|
-
if (typeof eventPayload.type === "string") meta.slack_event_type = eventPayload.type;
|
|
396
|
-
return meta;
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
//#endregion
|
|
400
|
-
export { FlumeHttpError as i, FlumeSlackSocketMode as n, obtainSlackUrl as r, FlumeSlackSource as t };
|