@interactive-inc/flume 0.1.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/LICENSE +21 -0
- package/README.md +244 -0
- package/dist/discord-source-Q4JRIbNs.js +525 -0
- package/dist/discord.d.ts +24 -0
- package/dist/discord.js +2 -0
- package/dist/github-source-D7Z1RUbe.js +280 -0
- package/dist/github.d.ts +19 -0
- package/dist/github.js +2 -0
- package/dist/index.d.ts +278 -0
- package/dist/index.js +66 -0
- package/dist/logger-B9E8zvgj.js +69 -0
- package/dist/safe-json-parse-D8t_4Vm_.js +81 -0
- package/dist/slack-source-CszepStG.js +400 -0
- package/dist/slack.d.ts +23 -0
- package/dist/slack.js +2 -0
- package/dist/types-BVQSU336.d.ts +112 -0
- package/package.json +77 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
//#region lib/schema.ts
|
|
3
|
+
const FlumeGatewayMessageSchema = z.object({
|
|
4
|
+
op: z.number(),
|
|
5
|
+
d: z.record(z.string(), z.unknown()).nullable(),
|
|
6
|
+
s: z.number().nullable(),
|
|
7
|
+
t: z.string().nullable()
|
|
8
|
+
});
|
|
9
|
+
const FlumeSlackEnvelopeSchema = z.object({
|
|
10
|
+
envelope_id: z.string(),
|
|
11
|
+
type: z.string(),
|
|
12
|
+
payload: z.record(z.string(), z.unknown()),
|
|
13
|
+
accepts_response_payload: z.boolean().optional(),
|
|
14
|
+
retry_attempt: z.number().optional(),
|
|
15
|
+
retry_reason: z.string().optional()
|
|
16
|
+
});
|
|
17
|
+
const FlumeSlackConnectionResponseSchema = z.object({
|
|
18
|
+
ok: z.boolean(),
|
|
19
|
+
url: z.string().optional(),
|
|
20
|
+
error: z.string().optional()
|
|
21
|
+
});
|
|
22
|
+
const FlumeGitHubNotificationSchema = z.object({
|
|
23
|
+
id: z.string(),
|
|
24
|
+
reason: z.string(),
|
|
25
|
+
unread: z.boolean(),
|
|
26
|
+
updated_at: z.string(),
|
|
27
|
+
subject: z.object({
|
|
28
|
+
title: z.string(),
|
|
29
|
+
url: z.string().nullable(),
|
|
30
|
+
type: z.string()
|
|
31
|
+
}),
|
|
32
|
+
repository: z.object({ full_name: z.string() })
|
|
33
|
+
});
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region lib/logger.ts
|
|
36
|
+
var FlumeLogger = class {
|
|
37
|
+
props;
|
|
38
|
+
constructor(props) {
|
|
39
|
+
this.props = props;
|
|
40
|
+
Object.freeze(this);
|
|
41
|
+
}
|
|
42
|
+
debug(entry) {
|
|
43
|
+
this.emit("debug", entry);
|
|
44
|
+
}
|
|
45
|
+
info(entry) {
|
|
46
|
+
this.emit("info", entry);
|
|
47
|
+
}
|
|
48
|
+
warn(entry) {
|
|
49
|
+
this.emit("warn", entry);
|
|
50
|
+
}
|
|
51
|
+
error(entry) {
|
|
52
|
+
this.emit("error", entry);
|
|
53
|
+
}
|
|
54
|
+
emit(level, input) {
|
|
55
|
+
if (!this.props.handler) return;
|
|
56
|
+
const log = {
|
|
57
|
+
level,
|
|
58
|
+
source: this.props.source,
|
|
59
|
+
action: input.action,
|
|
60
|
+
message: input.message,
|
|
61
|
+
timestamp: this.props.deps.now(),
|
|
62
|
+
error: input.error,
|
|
63
|
+
detail: input.detail
|
|
64
|
+
};
|
|
65
|
+
this.props.handler(log);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
export { FlumeSlackEnvelopeSchema as a, FlumeSlackConnectionResponseSchema as i, FlumeGatewayMessageSchema as n, FlumeGitHubNotificationSchema as r, FlumeLogger as t };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
//#region lib/errors/connection-error.ts
|
|
2
|
+
var FlumeConnectionError = class extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "FlumeConnectionError";
|
|
6
|
+
Object.freeze(this);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region lib/errors/parse-error.ts
|
|
11
|
+
var FlumeParseError = class extends Error {
|
|
12
|
+
constructor(message) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "FlumeParseError";
|
|
15
|
+
Object.freeze(this);
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region lib/reconnect-config.ts
|
|
20
|
+
const DEFAULTS = {
|
|
21
|
+
maxAttempts: Infinity,
|
|
22
|
+
baseDelay: 1e3,
|
|
23
|
+
maxDelay: 3e4
|
|
24
|
+
};
|
|
25
|
+
function resolveFlumeReconnectConfig(input) {
|
|
26
|
+
if (input === false || input === void 0) return null;
|
|
27
|
+
if (input === true) return { ...DEFAULTS };
|
|
28
|
+
return {
|
|
29
|
+
...DEFAULTS,
|
|
30
|
+
...input
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region lib/reconnector.ts
|
|
35
|
+
var FlumeReconnector = class {
|
|
36
|
+
props;
|
|
37
|
+
attempt = 0;
|
|
38
|
+
aborted = false;
|
|
39
|
+
timer = null;
|
|
40
|
+
constructor(props) {
|
|
41
|
+
this.props = props;
|
|
42
|
+
}
|
|
43
|
+
schedule(fn) {
|
|
44
|
+
if (this.aborted) return 0;
|
|
45
|
+
if (this.attempt >= this.props.maxAttempts) return -1;
|
|
46
|
+
const delay = this.nextDelay();
|
|
47
|
+
this.timer = this.props.deps.setTimeout(fn, delay);
|
|
48
|
+
return delay;
|
|
49
|
+
}
|
|
50
|
+
reset() {
|
|
51
|
+
this.attempt = 0;
|
|
52
|
+
}
|
|
53
|
+
cancel() {
|
|
54
|
+
this.aborted = true;
|
|
55
|
+
if (this.timer !== null) {
|
|
56
|
+
this.props.deps.clearTimeout(this.timer);
|
|
57
|
+
this.timer = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
nextDelay() {
|
|
61
|
+
const jitter = Math.min(this.props.baseDelay * 2 ** this.attempt, this.props.maxDelay) * (.5 + this.props.deps.random() * .5);
|
|
62
|
+
this.attempt++;
|
|
63
|
+
return jitter;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region lib/utils/is-record.ts
|
|
68
|
+
function isRecord(value) {
|
|
69
|
+
return typeof value === "object" && value !== null;
|
|
70
|
+
}
|
|
71
|
+
//#endregion
|
|
72
|
+
//#region lib/utils/safe-json-parse.ts
|
|
73
|
+
function safeJsonParse(raw) {
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
//#endregion
|
|
81
|
+
export { FlumeParseError as a, resolveFlumeReconnectConfig as i, isRecord as n, FlumeConnectionError as o, FlumeReconnector as r, safeJsonParse as t };
|
|
@@ -0,0 +1,400 @@
|
|
|
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 };
|
package/dist/slack.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { g as FlumeSlackSourceOptions, h as FlumeSlackEnvelope, o as FlumeHandler, y as FlumeStatus } from "./types-BVQSU336.js";
|
|
2
|
+
|
|
3
|
+
//#region lib/slack/slack-source.d.ts
|
|
4
|
+
declare class FlumeSlackSource {
|
|
5
|
+
private readonly options;
|
|
6
|
+
private socket;
|
|
7
|
+
private reconnector;
|
|
8
|
+
private handler;
|
|
9
|
+
private currentStatus;
|
|
10
|
+
private readonly log;
|
|
11
|
+
private readonly deps;
|
|
12
|
+
constructor(options: FlumeSlackSourceOptions);
|
|
13
|
+
start(handler: FlumeHandler): Promise<void>;
|
|
14
|
+
stop(): Promise<void>;
|
|
15
|
+
status(): FlumeStatus;
|
|
16
|
+
private connectInternal;
|
|
17
|
+
private handleMessage;
|
|
18
|
+
private scheduleReconnect;
|
|
19
|
+
private setStatus;
|
|
20
|
+
static extractMeta(envelope: FlumeSlackEnvelope): Record<string, string>;
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
export { FlumeSlackSource };
|
package/dist/slack.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { z } from "zod/v4";
|
|
2
|
+
|
|
3
|
+
//#region lib/schema.d.ts
|
|
4
|
+
declare const FlumeGatewayMessageSchema: z.ZodObject<{
|
|
5
|
+
op: z.ZodNumber;
|
|
6
|
+
d: z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
7
|
+
s: z.ZodNullable<z.ZodNumber>;
|
|
8
|
+
t: z.ZodNullable<z.ZodString>;
|
|
9
|
+
}, z.core.$strip>;
|
|
10
|
+
declare const FlumeSlackEnvelopeSchema: z.ZodObject<{
|
|
11
|
+
envelope_id: z.ZodString;
|
|
12
|
+
type: z.ZodString;
|
|
13
|
+
payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
14
|
+
accepts_response_payload: z.ZodOptional<z.ZodBoolean>;
|
|
15
|
+
retry_attempt: z.ZodOptional<z.ZodNumber>;
|
|
16
|
+
retry_reason: z.ZodOptional<z.ZodString>;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
declare const FlumeSlackConnectionResponseSchema: z.ZodObject<{
|
|
19
|
+
ok: z.ZodBoolean;
|
|
20
|
+
url: z.ZodOptional<z.ZodString>;
|
|
21
|
+
error: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
declare const FlumeGitHubNotificationSchema: z.ZodObject<{
|
|
24
|
+
id: z.ZodString;
|
|
25
|
+
reason: z.ZodString;
|
|
26
|
+
unread: z.ZodBoolean;
|
|
27
|
+
updated_at: z.ZodString;
|
|
28
|
+
subject: z.ZodObject<{
|
|
29
|
+
title: z.ZodString;
|
|
30
|
+
url: z.ZodNullable<z.ZodString>;
|
|
31
|
+
type: z.ZodString;
|
|
32
|
+
}, z.core.$strip>;
|
|
33
|
+
repository: z.ZodObject<{
|
|
34
|
+
full_name: z.ZodString;
|
|
35
|
+
}, z.core.$strip>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region lib/types.d.ts
|
|
39
|
+
type FlumeTimerHandle = ReturnType<typeof setTimeout>;
|
|
40
|
+
type FlumeRuntimeDeps = {
|
|
41
|
+
fetch(url: string | URL, init?: RequestInit): Promise<Response>;
|
|
42
|
+
WebSocket: new (url: string | URL) => WebSocket;
|
|
43
|
+
now(): number;
|
|
44
|
+
random(): number;
|
|
45
|
+
setTimeout(fn: () => void, ms: number): FlumeTimerHandle;
|
|
46
|
+
clearTimeout(id: FlumeTimerHandle): void;
|
|
47
|
+
setInterval(fn: () => void, ms: number): FlumeTimerHandle;
|
|
48
|
+
clearInterval(id: FlumeTimerHandle): void;
|
|
49
|
+
};
|
|
50
|
+
type FlumeSourceName = "discord" | "slack" | "github";
|
|
51
|
+
type FlumeEvent = {
|
|
52
|
+
source: FlumeSourceName;
|
|
53
|
+
type: string;
|
|
54
|
+
data: unknown;
|
|
55
|
+
meta: Record<string, string>;
|
|
56
|
+
receivedAt: number;
|
|
57
|
+
};
|
|
58
|
+
type FlumeHandler = (event: FlumeEvent) => void | Promise<void>;
|
|
59
|
+
type FlumeStatus = "disconnected" | "connecting" | "connected" | "reconnecting";
|
|
60
|
+
type FlumeStatusHandler = (status: FlumeStatus, detail?: string) => void;
|
|
61
|
+
type FlumeLogLevel = "debug" | "info" | "warn" | "error";
|
|
62
|
+
type FlumeLog = {
|
|
63
|
+
level: FlumeLogLevel;
|
|
64
|
+
source: string;
|
|
65
|
+
action: string;
|
|
66
|
+
message: string;
|
|
67
|
+
error?: Error;
|
|
68
|
+
detail?: Record<string, unknown>;
|
|
69
|
+
timestamp: number;
|
|
70
|
+
};
|
|
71
|
+
type FlumeLogHandler = (log: FlumeLog) => void;
|
|
72
|
+
type FlumeLogInput = {
|
|
73
|
+
action: string;
|
|
74
|
+
message: string;
|
|
75
|
+
error?: Error;
|
|
76
|
+
detail?: Record<string, unknown>;
|
|
77
|
+
};
|
|
78
|
+
type FlumeReconnectOptions = {
|
|
79
|
+
maxAttempts?: number;
|
|
80
|
+
baseDelay?: number;
|
|
81
|
+
maxDelay?: number;
|
|
82
|
+
};
|
|
83
|
+
type FlumeReconnectConfig = {
|
|
84
|
+
maxAttempts: number;
|
|
85
|
+
baseDelay: number;
|
|
86
|
+
maxDelay: number;
|
|
87
|
+
};
|
|
88
|
+
type FlumeSourceOptions = {
|
|
89
|
+
reconnect?: boolean | FlumeReconnectOptions;
|
|
90
|
+
onStatus?: FlumeStatusHandler;
|
|
91
|
+
onLog?: FlumeLogHandler;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
deps: FlumeRuntimeDeps;
|
|
94
|
+
};
|
|
95
|
+
type FlumeDiscordSourceOptions = FlumeSourceOptions & {
|
|
96
|
+
token: string;
|
|
97
|
+
intents?: number;
|
|
98
|
+
};
|
|
99
|
+
type FlumeSlackSourceOptions = FlumeSourceOptions & {
|
|
100
|
+
appToken: string;
|
|
101
|
+
botToken?: string;
|
|
102
|
+
};
|
|
103
|
+
type FlumeGitHubSourceOptions = FlumeSourceOptions & {
|
|
104
|
+
token: string;
|
|
105
|
+
pollInterval?: number;
|
|
106
|
+
};
|
|
107
|
+
type FlumeGatewayMessage = z.infer<typeof FlumeGatewayMessageSchema>;
|
|
108
|
+
type FlumeSlackEnvelope = z.infer<typeof FlumeSlackEnvelopeSchema>;
|
|
109
|
+
type FlumeSlackConnectionResponse = z.infer<typeof FlumeSlackConnectionResponseSchema>;
|
|
110
|
+
type FlumeGitHubNotification = z.infer<typeof FlumeGitHubNotificationSchema>;
|
|
111
|
+
//#endregion
|
|
112
|
+
export { FlumeGitHubNotificationSchema as C, FlumeGatewayMessageSchema as S, FlumeSlackEnvelopeSchema as T, FlumeSourceName as _, FlumeGitHubSourceOptions as a, FlumeStatusHandler as b, FlumeLogHandler as c, FlumeReconnectConfig as d, FlumeReconnectOptions as f, FlumeSlackSourceOptions as g, FlumeSlackEnvelope as h, FlumeGitHubNotification as i, FlumeLogInput as l, FlumeSlackConnectionResponse as m, FlumeEvent as n, FlumeHandler as o, FlumeRuntimeDeps as p, FlumeGatewayMessage as r, FlumeLog as s, FlumeDiscordSourceOptions as t, FlumeLogLevel as u, FlumeSourceOptions as v, FlumeSlackConnectionResponseSchema as w, FlumeTimerHandle as x, FlumeStatus as y };
|