@interactive-inc/flume 0.3.0 → 0.6.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 +165 -108
- package/dist/connection-error-HUO3PC3G.js +16 -0
- package/dist/discord.d.ts +10 -93
- package/dist/discord.js +446 -217
- package/dist/flume-source-DUvt9aJt.js +341 -0
- package/dist/flume-source-DuUFPhSe.d.ts +228 -0
- package/dist/github.d.ts +7 -54
- package/dist/github.js +354 -158
- package/dist/{http-error-BtXonO-W.js → http-error-CPSKoSie.js} +1 -1
- package/dist/index.d.ts +79 -49
- package/dist/index.js +304 -62
- package/dist/safe-json-parse-CfJjt-RY.js +11 -0
- package/dist/safe-read-text-JQd_5vbd.js +20 -0
- package/dist/safe-stringify-DbWQw9qe.js +157 -0
- package/dist/slack.d.ts +10 -70
- package/dist/slack.js +352 -173
- package/package.json +22 -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/types-Bm9uKUQz.d.ts +0 -143
package/dist/github.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as
|
|
3
|
-
import { t as
|
|
1
|
+
import { c as safeNormalizeError, i as safeNow, l as safeErrorMessage, o as FlumeParseError, r as FlumeLogger, s as attempt, t as FlumeSource } from "./flume-source-DUvt9aJt.js";
|
|
2
|
+
import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
|
|
3
|
+
import { t as safeJsonParse } from "./safe-json-parse-CfJjt-RY.js";
|
|
4
|
+
import { t as safeReadText } from "./safe-read-text-JQd_5vbd.js";
|
|
4
5
|
import { z } from "zod/v4";
|
|
5
6
|
//#region lib/github/extract-github-meta.ts
|
|
6
|
-
function
|
|
7
|
+
function flumeExtractGitHubMeta(notification) {
|
|
7
8
|
return {
|
|
8
9
|
event_type: "notification",
|
|
9
10
|
reason: notification.reason,
|
|
@@ -51,15 +52,28 @@ var FlumeGitHubSeenCache = class {
|
|
|
51
52
|
};
|
|
52
53
|
//#endregion
|
|
53
54
|
//#region lib/github/github-poller.ts
|
|
55
|
+
const NOTIFICATIONS_URL = "https://api.github.com/notifications";
|
|
56
|
+
const CONSECUTIVE_ERRORS_TO_DISCONNECT = 3;
|
|
57
|
+
const SEEN_CACHE_MAX = 5e3;
|
|
58
|
+
/**
|
|
59
|
+
* GitHub /notifications を条件付きポーリングする (ETag / Last-Modified)。
|
|
60
|
+
* 304 / X-Poll-Interval / レート制限を尊重し、stop() / abort() で in-flight 通信を打ち切る
|
|
61
|
+
*/
|
|
54
62
|
var FlumeGitHubPoller = class {
|
|
55
63
|
props;
|
|
56
64
|
log;
|
|
57
|
-
cache = new FlumeGitHubSeenCache({ maxSize:
|
|
65
|
+
cache = new FlumeGitHubSeenCache({ maxSize: SEEN_CACHE_MAX });
|
|
58
66
|
timer = null;
|
|
67
|
+
rateLimitTimer = null;
|
|
59
68
|
since = null;
|
|
69
|
+
etag = null;
|
|
70
|
+
lastModified = null;
|
|
60
71
|
bootstrapped = false;
|
|
61
|
-
|
|
72
|
+
isStoppedFlag = false;
|
|
73
|
+
inFlight = false;
|
|
62
74
|
consecutiveErrors = 0;
|
|
75
|
+
effectiveIntervalSec;
|
|
76
|
+
controller = null;
|
|
63
77
|
constructor(props) {
|
|
64
78
|
this.props = props;
|
|
65
79
|
this.log = new FlumeLogger({
|
|
@@ -67,45 +81,125 @@ var FlumeGitHubPoller = class {
|
|
|
67
81
|
handler: props.onLog,
|
|
68
82
|
deps: props.deps
|
|
69
83
|
});
|
|
84
|
+
this.effectiveIntervalSec = props.interval;
|
|
85
|
+
}
|
|
86
|
+
get isStopped() {
|
|
87
|
+
return this.isStoppedFlag;
|
|
70
88
|
}
|
|
71
89
|
async start() {
|
|
72
|
-
this.
|
|
90
|
+
this.isStoppedFlag = false;
|
|
91
|
+
const controllerResult = attempt(() => new AbortController());
|
|
92
|
+
if (controllerResult instanceof Error) {
|
|
93
|
+
this.log.error({
|
|
94
|
+
action: "github.abort-controller.new.error",
|
|
95
|
+
message: safeErrorMessage({ error: controllerResult }),
|
|
96
|
+
error: controllerResult
|
|
97
|
+
});
|
|
98
|
+
this.controller = null;
|
|
99
|
+
} else this.controller = controllerResult;
|
|
73
100
|
this.log.info({
|
|
74
|
-
action: "start",
|
|
75
|
-
message: `polling every ${this.
|
|
101
|
+
action: "poller.start",
|
|
102
|
+
message: `polling every ${this.effectiveIntervalSec}s`
|
|
76
103
|
});
|
|
77
|
-
await this.poll();
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
message: "unexpected error in poll loop",
|
|
83
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
}, this.props.interval * 1e3);
|
|
104
|
+
const error = await this.poll();
|
|
105
|
+
if (error) return error;
|
|
106
|
+
if (this.isStoppedFlag) return null;
|
|
107
|
+
this.scheduleInterval();
|
|
108
|
+
return null;
|
|
87
109
|
}
|
|
88
110
|
stop() {
|
|
89
111
|
this.log.info({
|
|
90
|
-
action: "stop",
|
|
112
|
+
action: "poller.stop",
|
|
91
113
|
message: "stopping poller"
|
|
92
114
|
});
|
|
93
|
-
this.
|
|
94
|
-
|
|
95
|
-
|
|
115
|
+
this.isStoppedFlag = true;
|
|
116
|
+
this.controller?.abort();
|
|
117
|
+
this.controller = null;
|
|
118
|
+
this.clearTimer();
|
|
119
|
+
this.clearRateLimitTimer();
|
|
120
|
+
}
|
|
121
|
+
scheduleInterval() {
|
|
122
|
+
this.clearTimer();
|
|
123
|
+
const intervalResult = attempt(() => this.props.deps.setInterval(() => {
|
|
124
|
+
this.poll().catch((err) => {
|
|
125
|
+
const error = safeNormalizeError({ value: err });
|
|
126
|
+
this.log.error({
|
|
127
|
+
action: "poll.unhandled",
|
|
128
|
+
message: safeErrorMessage({ error }),
|
|
129
|
+
error
|
|
130
|
+
});
|
|
131
|
+
}).catch(() => {});
|
|
132
|
+
}, this.effectiveIntervalSec * 1e3));
|
|
133
|
+
if (intervalResult instanceof Error) {
|
|
134
|
+
this.log.error({
|
|
135
|
+
action: "poller.interval.schedule.error",
|
|
136
|
+
message: safeErrorMessage({ error: intervalResult }),
|
|
137
|
+
error: intervalResult
|
|
138
|
+
});
|
|
96
139
|
this.timer = null;
|
|
140
|
+
if (!this.isStoppedFlag) this.props.onDisconnected("interval scheduling rejected by runtime");
|
|
141
|
+
return;
|
|
97
142
|
}
|
|
143
|
+
this.timer = intervalResult;
|
|
98
144
|
}
|
|
99
145
|
async poll() {
|
|
100
|
-
|
|
146
|
+
if (this.inFlight || this.isStoppedFlag) return null;
|
|
147
|
+
this.inFlight = true;
|
|
148
|
+
try {
|
|
149
|
+
return await this.pollOnce();
|
|
150
|
+
} catch (err) {
|
|
151
|
+
const cause = safeNormalizeError({ value: err });
|
|
152
|
+
const error = new FlumeHttpError({
|
|
153
|
+
message: `poll loop threw: ${safeErrorMessage({ error: cause })}`,
|
|
154
|
+
status: 0,
|
|
155
|
+
cause
|
|
156
|
+
});
|
|
157
|
+
this.log.error({
|
|
158
|
+
action: "poll.unhandled",
|
|
159
|
+
message: safeErrorMessage({ error }),
|
|
160
|
+
error
|
|
161
|
+
});
|
|
162
|
+
if (!this.bootstrapped) return error;
|
|
163
|
+
return null;
|
|
164
|
+
} finally {
|
|
165
|
+
this.inFlight = false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
async pollOnce() {
|
|
169
|
+
const params = new URLSearchParams({
|
|
170
|
+
all: "false",
|
|
171
|
+
per_page: "50"
|
|
172
|
+
});
|
|
101
173
|
if (this.since) params.set("since", this.since);
|
|
102
|
-
const url =
|
|
174
|
+
const url = `${NOTIFICATIONS_URL}?${params}`;
|
|
103
175
|
this.log.debug({
|
|
104
176
|
action: "http.request",
|
|
105
177
|
message: `GET ${url}`
|
|
106
178
|
});
|
|
107
|
-
const
|
|
108
|
-
|
|
179
|
+
const headers = {
|
|
180
|
+
Authorization: `Bearer ${this.props.token}`,
|
|
181
|
+
Accept: "application/vnd.github+json",
|
|
182
|
+
"X-GitHub-Api-Version": "2022-11-28"
|
|
183
|
+
};
|
|
184
|
+
if (this.etag) headers["If-None-Match"] = this.etag;
|
|
185
|
+
if (this.lastModified) headers["If-Modified-Since"] = this.lastModified;
|
|
186
|
+
const response = await attempt(() => this.props.deps.fetch(url, {
|
|
187
|
+
headers,
|
|
188
|
+
signal: this.controller?.signal
|
|
189
|
+
}));
|
|
190
|
+
if (this.isStoppedFlag) return null;
|
|
191
|
+
if (response instanceof Error) {
|
|
192
|
+
this.log.error({
|
|
193
|
+
action: "http.error",
|
|
194
|
+
message: safeErrorMessage({ error: response }),
|
|
195
|
+
error: response
|
|
196
|
+
});
|
|
197
|
+
return this.recordFailure({
|
|
198
|
+
kind: "network",
|
|
199
|
+
message: response.message,
|
|
200
|
+
cause: response
|
|
201
|
+
});
|
|
202
|
+
}
|
|
109
203
|
this.log.debug({
|
|
110
204
|
action: "http.response",
|
|
111
205
|
message: `GET ${response.status}`,
|
|
@@ -114,45 +208,175 @@ var FlumeGitHubPoller = class {
|
|
|
114
208
|
url
|
|
115
209
|
}
|
|
116
210
|
});
|
|
211
|
+
this.maybeWidenInterval(response.headers.get("X-Poll-Interval"));
|
|
212
|
+
if (response.status === 304) {
|
|
213
|
+
this.consecutiveErrors = 0;
|
|
214
|
+
this.log.debug({
|
|
215
|
+
action: "poll.not-modified",
|
|
216
|
+
message: "304 Not Modified"
|
|
217
|
+
});
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
if (this.isRateLimited(response)) {
|
|
221
|
+
this.handleRateLimit(response);
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
117
224
|
if (!response.ok) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
225
|
+
const error = new FlumeHttpError({
|
|
226
|
+
message: `HTTP ${response.status}`,
|
|
227
|
+
status: response.status
|
|
228
|
+
});
|
|
229
|
+
return this.recordFailure({
|
|
230
|
+
kind: "http",
|
|
231
|
+
message: safeErrorMessage({ error }),
|
|
232
|
+
error
|
|
122
233
|
});
|
|
123
|
-
if (this.consecutiveErrors >= 3) this.props.onDisconnected(`HTTP ${response.status}`);
|
|
124
|
-
return;
|
|
125
234
|
}
|
|
126
235
|
this.consecutiveErrors = 0;
|
|
127
|
-
|
|
128
|
-
|
|
236
|
+
this.etag = response.headers.get("ETag");
|
|
237
|
+
this.lastModified = response.headers.get("Last-Modified");
|
|
238
|
+
const text = await safeReadText({
|
|
239
|
+
response,
|
|
240
|
+
context: "notifications"
|
|
241
|
+
});
|
|
242
|
+
if (this.isStoppedFlag) return null;
|
|
243
|
+
if (text instanceof FlumeHttpError) {
|
|
244
|
+
this.log.warn({
|
|
245
|
+
action: "http.body.read",
|
|
246
|
+
message: safeErrorMessage({ error: text }),
|
|
247
|
+
error: text
|
|
248
|
+
});
|
|
249
|
+
return this.recordFailure({
|
|
250
|
+
kind: "http",
|
|
251
|
+
message: text.message,
|
|
252
|
+
error: text
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
const json = safeJsonParse(text);
|
|
256
|
+
if (json instanceof FlumeParseError) {
|
|
257
|
+
this.log.warn({
|
|
258
|
+
action: "http.body.parse",
|
|
259
|
+
message: json.message,
|
|
260
|
+
error: json
|
|
261
|
+
});
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
if (!Array.isArray(json)) {
|
|
129
265
|
this.log.warn({
|
|
130
|
-
action: "http.body",
|
|
266
|
+
action: "http.body.shape",
|
|
131
267
|
message: "response body is not an array, dropping",
|
|
132
|
-
detail: { bodyType: typeof
|
|
268
|
+
detail: { bodyType: typeof json }
|
|
133
269
|
});
|
|
134
|
-
return;
|
|
270
|
+
return null;
|
|
135
271
|
}
|
|
136
|
-
this.processNotifications(
|
|
272
|
+
this.processNotifications(json);
|
|
273
|
+
return null;
|
|
137
274
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
275
|
+
recordFailure(input) {
|
|
276
|
+
this.consecutiveErrors++;
|
|
277
|
+
const error = input.error ?? new FlumeHttpError({
|
|
278
|
+
message: input.message,
|
|
279
|
+
status: 0,
|
|
280
|
+
cause: input.cause
|
|
281
|
+
});
|
|
282
|
+
this.log.error({
|
|
283
|
+
action: "http.error",
|
|
284
|
+
message: input.message,
|
|
285
|
+
error,
|
|
286
|
+
detail: { consecutiveErrors: this.consecutiveErrors }
|
|
287
|
+
});
|
|
288
|
+
if (this.consecutiveErrors >= CONSECUTIVE_ERRORS_TO_DISCONNECT && !this.isStoppedFlag) this.props.onDisconnected(input.kind === "network" ? "network error" : input.message);
|
|
289
|
+
if (!this.bootstrapped) return error;
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
isRateLimited(response) {
|
|
293
|
+
if (response.status === 429) return true;
|
|
294
|
+
if (response.status === 403 && response.headers.get("X-RateLimit-Remaining") === "0") return true;
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
handleRateLimit(response) {
|
|
298
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
299
|
+
const reset = response.headers.get("X-RateLimit-Reset");
|
|
300
|
+
const nowSec = Math.floor(safeNow({ deps: this.props.deps }) / 1e3);
|
|
301
|
+
const retryAfterSec = retryAfter !== null ? Number.parseInt(retryAfter, 10) : NaN;
|
|
302
|
+
const resetSec = reset !== null ? Number.parseInt(reset, 10) - nowSec : NaN;
|
|
303
|
+
const delaySec = Number.isFinite(retryAfterSec) && retryAfterSec > 0 ? retryAfterSec : Number.isFinite(resetSec) && resetSec > 0 ? resetSec : 60;
|
|
304
|
+
this.log.warn({
|
|
305
|
+
action: "rate.limit",
|
|
306
|
+
message: `rate limited, pausing for ${delaySec}s`,
|
|
307
|
+
detail: {
|
|
308
|
+
status: response.status,
|
|
309
|
+
delaySec
|
|
153
310
|
}
|
|
154
|
-
return [parsed.data];
|
|
155
311
|
});
|
|
312
|
+
this.clearTimer();
|
|
313
|
+
this.clearRateLimitTimer();
|
|
314
|
+
const timerResult = attempt(() => this.props.deps.setTimeout(() => {
|
|
315
|
+
this.rateLimitTimer = null;
|
|
316
|
+
if (this.isStoppedFlag) return;
|
|
317
|
+
this.scheduleInterval();
|
|
318
|
+
}, delaySec * 1e3));
|
|
319
|
+
if (timerResult instanceof Error) {
|
|
320
|
+
this.log.error({
|
|
321
|
+
action: "poller.rate-limit.schedule.error",
|
|
322
|
+
message: safeErrorMessage({ error: timerResult }),
|
|
323
|
+
error: timerResult
|
|
324
|
+
});
|
|
325
|
+
this.rateLimitTimer = null;
|
|
326
|
+
} else this.rateLimitTimer = timerResult;
|
|
327
|
+
}
|
|
328
|
+
clearTimer() {
|
|
329
|
+
if (this.timer === null) return;
|
|
330
|
+
const handle = this.timer;
|
|
331
|
+
const result = attempt(() => this.props.deps.clearInterval(handle));
|
|
332
|
+
if (result instanceof Error) this.log.error({
|
|
333
|
+
action: "poller.timer.clear.error",
|
|
334
|
+
message: safeErrorMessage({ error: result }),
|
|
335
|
+
error: result
|
|
336
|
+
});
|
|
337
|
+
this.timer = null;
|
|
338
|
+
}
|
|
339
|
+
clearRateLimitTimer() {
|
|
340
|
+
if (this.rateLimitTimer === null) return;
|
|
341
|
+
const handle = this.rateLimitTimer;
|
|
342
|
+
const result = attempt(() => this.props.deps.clearTimeout(handle));
|
|
343
|
+
if (result instanceof Error) this.log.error({
|
|
344
|
+
action: "poller.rate-limit.clear.error",
|
|
345
|
+
message: safeErrorMessage({ error: result }),
|
|
346
|
+
error: result
|
|
347
|
+
});
|
|
348
|
+
this.rateLimitTimer = null;
|
|
349
|
+
}
|
|
350
|
+
maybeWidenInterval(headerValue) {
|
|
351
|
+
if (headerValue === null) return;
|
|
352
|
+
const required = Number.parseInt(headerValue, 10);
|
|
353
|
+
if (!Number.isFinite(required) || required <= this.effectiveIntervalSec) return;
|
|
354
|
+
this.log.info({
|
|
355
|
+
action: "poll.widen-interval",
|
|
356
|
+
message: `widening interval ${this.effectiveIntervalSec}s -> ${required}s per X-Poll-Interval`,
|
|
357
|
+
detail: {
|
|
358
|
+
from: this.effectiveIntervalSec,
|
|
359
|
+
to: required
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
this.effectiveIntervalSec = required;
|
|
363
|
+
if (this.timer !== null) this.scheduleInterval();
|
|
364
|
+
}
|
|
365
|
+
processNotifications(raw) {
|
|
366
|
+
const parsedResults = raw.map((item) => FlumeGitHubNotificationSchema.safeParse(item));
|
|
367
|
+
for (const result of parsedResults) {
|
|
368
|
+
if (result.success) continue;
|
|
369
|
+
this.log.warn({
|
|
370
|
+
action: "parse.skip",
|
|
371
|
+
message: "notification did not match schema",
|
|
372
|
+
detail: { issues: result.error.issues.map((i) => ({
|
|
373
|
+
path: i.path,
|
|
374
|
+
message: i.message
|
|
375
|
+
})) }
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
const notifications = parsedResults.flatMap((r) => r.success ? [r.data] : []);
|
|
379
|
+
const dropped = raw.length - notifications.length;
|
|
156
380
|
if (dropped > 0) this.log.warn({
|
|
157
381
|
action: "parse.summary",
|
|
158
382
|
message: `${dropped}/${raw.length} notifications dropped by schema`
|
|
@@ -160,155 +384,127 @@ var FlumeGitHubPoller = class {
|
|
|
160
384
|
if (!this.bootstrapped) {
|
|
161
385
|
this.bootstrapped = true;
|
|
162
386
|
for (const notification of notifications) this.cache.add(notification.id, notification.updated_at);
|
|
163
|
-
this.
|
|
387
|
+
this.advanceCursor();
|
|
164
388
|
this.log.info({
|
|
165
|
-
action: "bootstrap",
|
|
389
|
+
action: "poller.bootstrap",
|
|
166
390
|
message: `seeded ${notifications.length} existing notifications`
|
|
167
391
|
});
|
|
168
392
|
this.props.onConnected();
|
|
169
393
|
return;
|
|
170
394
|
}
|
|
171
|
-
const fresh =
|
|
172
|
-
|
|
173
|
-
if (this.cache.has(notification.id, notification.updated_at)) continue;
|
|
395
|
+
const fresh = notifications.filter((notification) => {
|
|
396
|
+
if (this.cache.has(notification.id, notification.updated_at)) return false;
|
|
174
397
|
this.cache.add(notification.id, notification.updated_at);
|
|
175
|
-
|
|
176
|
-
}
|
|
398
|
+
return true;
|
|
399
|
+
});
|
|
177
400
|
this.cache.trim();
|
|
178
|
-
this.
|
|
401
|
+
this.advanceCursor();
|
|
179
402
|
if (fresh.length > 0) {
|
|
180
403
|
this.log.info({
|
|
181
404
|
action: "poll.fresh",
|
|
182
|
-
message: `${fresh.length} new notifications
|
|
405
|
+
message: `${fresh.length} new notifications`,
|
|
406
|
+
detail: { count: fresh.length }
|
|
183
407
|
});
|
|
184
408
|
this.props.onNotifications(fresh);
|
|
185
|
-
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.log.debug({
|
|
186
412
|
action: "poll.idle",
|
|
187
413
|
message: "0 new notifications"
|
|
188
414
|
});
|
|
189
415
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
|
|
416
|
+
advanceCursor() {
|
|
417
|
+
if (this.lastModified !== null) {
|
|
418
|
+
const parsed = new Date(this.lastModified);
|
|
419
|
+
if (!Number.isNaN(parsed.getTime())) {
|
|
420
|
+
const iso = attempt(() => parsed.toISOString());
|
|
421
|
+
if (!(iso instanceof Error)) {
|
|
422
|
+
this.since = iso;
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
const nowMs = safeNow({ deps: this.props.deps });
|
|
428
|
+
if (!Number.isFinite(nowMs)) {
|
|
203
429
|
this.log.warn({
|
|
204
|
-
action: "
|
|
205
|
-
message:
|
|
430
|
+
action: "cursor.advance.skip",
|
|
431
|
+
message: "deps.now() returned non-finite, leaving since cursor unchanged"
|
|
206
432
|
});
|
|
207
|
-
|
|
433
|
+
return;
|
|
208
434
|
}
|
|
209
|
-
|
|
435
|
+
const iso = attempt(() => new Date(nowMs).toISOString());
|
|
436
|
+
if (iso instanceof Error) {
|
|
437
|
+
const error = safeNormalizeError({ value: iso });
|
|
438
|
+
this.log.warn({
|
|
439
|
+
action: "cursor.advance.skip",
|
|
440
|
+
message: safeErrorMessage({ error }),
|
|
441
|
+
error
|
|
442
|
+
});
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
this.since = iso;
|
|
210
446
|
}
|
|
211
447
|
};
|
|
212
448
|
//#endregion
|
|
213
449
|
//#region lib/github/github-source.ts
|
|
214
|
-
var FlumeGitHubSource = class {
|
|
450
|
+
var FlumeGitHubSource = class extends FlumeSource {
|
|
215
451
|
options;
|
|
216
452
|
name = "github";
|
|
217
453
|
poller = null;
|
|
218
|
-
currentStatus = "disconnected";
|
|
219
|
-
log;
|
|
220
|
-
deps;
|
|
221
|
-
queue = new FlumeSerialQueue();
|
|
222
454
|
constructor(options) {
|
|
455
|
+
super();
|
|
223
456
|
this.options = options;
|
|
224
|
-
this.deps = options.deps ?? createFlumeDefaultDeps();
|
|
225
|
-
this.log = new FlumeLogger({
|
|
226
|
-
source: "github",
|
|
227
|
-
handler: options.onLog,
|
|
228
|
-
deps: this.deps
|
|
229
|
-
});
|
|
230
457
|
}
|
|
231
|
-
async
|
|
232
|
-
if (this.options.signal?.aborted) return {
|
|
233
|
-
ok: false,
|
|
234
|
-
error: /* @__PURE__ */ new Error("GitHub source: signal already aborted")
|
|
235
|
-
};
|
|
236
|
-
this.options.signal?.addEventListener("abort", () => this.stop(), { once: true });
|
|
237
|
-
this.log.info({
|
|
238
|
-
action: "start",
|
|
239
|
-
message: "starting GitHub source"
|
|
240
|
-
});
|
|
458
|
+
async connect(ctx) {
|
|
241
459
|
this.setStatus("connecting");
|
|
242
460
|
this.poller = new FlumeGitHubPoller({
|
|
243
461
|
token: this.options.token,
|
|
244
462
|
interval: this.options.pollInterval ?? 60,
|
|
245
|
-
onLog:
|
|
246
|
-
deps:
|
|
247
|
-
onNotifications: (notifications) => this.handleNotifications(
|
|
463
|
+
onLog: ctx.log.handler,
|
|
464
|
+
deps: ctx.deps,
|
|
465
|
+
onNotifications: (notifications) => this.handleNotifications(ctx, notifications),
|
|
248
466
|
onConnected: () => this.setStatus("connected"),
|
|
249
467
|
onDisconnected: (detail) => this.setStatus("disconnected", detail)
|
|
250
468
|
});
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
message: err.message,
|
|
258
|
-
error: err
|
|
469
|
+
const result = await this.poller.start();
|
|
470
|
+
if (result instanceof Error) {
|
|
471
|
+
ctx.log.error({
|
|
472
|
+
action: "source.start.failed",
|
|
473
|
+
message: safeErrorMessage({ error: result }),
|
|
474
|
+
error: result
|
|
259
475
|
});
|
|
260
|
-
this.setStatus("disconnected");
|
|
261
|
-
return
|
|
262
|
-
ok: false,
|
|
263
|
-
error: err
|
|
264
|
-
};
|
|
476
|
+
this.setStatus("disconnected", result.message);
|
|
477
|
+
return result;
|
|
265
478
|
}
|
|
266
|
-
return
|
|
479
|
+
return null;
|
|
267
480
|
}
|
|
268
|
-
|
|
269
|
-
this.log.info({
|
|
270
|
-
action: "stop",
|
|
271
|
-
message: "stopping GitHub source"
|
|
272
|
-
});
|
|
481
|
+
disconnect() {
|
|
273
482
|
this.poller?.stop();
|
|
274
483
|
this.poller = null;
|
|
275
|
-
await this.queue.drain();
|
|
276
|
-
this.setStatus("disconnected");
|
|
277
484
|
}
|
|
278
|
-
|
|
279
|
-
|
|
485
|
+
handleNotifications(ctx, notifications) {
|
|
486
|
+
for (const notification of notifications) this.emit({
|
|
487
|
+
source: "github",
|
|
488
|
+
type: "notification",
|
|
489
|
+
data: notification,
|
|
490
|
+
meta: this.safeExtractMeta(ctx, notification),
|
|
491
|
+
receivedAt: safeNow({ deps: ctx.deps })
|
|
492
|
+
});
|
|
280
493
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
this.queue.add(async () => {
|
|
291
|
-
try {
|
|
292
|
-
await handler(event);
|
|
293
|
-
} catch (err) {
|
|
294
|
-
this.log.error({
|
|
295
|
-
action: "handler.error",
|
|
296
|
-
message: "user handler threw",
|
|
297
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
298
|
-
});
|
|
299
|
-
}
|
|
494
|
+
safeExtractMeta(ctx, notification) {
|
|
495
|
+
const result = attempt(() => flumeExtractGitHubMeta(notification));
|
|
496
|
+
if (result instanceof Error) {
|
|
497
|
+
const error = safeNormalizeError({ value: result });
|
|
498
|
+
ctx.log.warn({
|
|
499
|
+
action: "meta.extract.error",
|
|
500
|
+
message: safeErrorMessage({ error }),
|
|
501
|
+
error,
|
|
502
|
+
detail: { notificationId: notification.id }
|
|
300
503
|
});
|
|
504
|
+
return { event_type: "notification" };
|
|
301
505
|
}
|
|
302
|
-
|
|
303
|
-
setStatus(next, detail) {
|
|
304
|
-
if (this.currentStatus === next) return;
|
|
305
|
-
this.log.info({
|
|
306
|
-
action: "status",
|
|
307
|
-
message: `${this.currentStatus} → ${next}${detail ? ` (${detail})` : ""}`
|
|
308
|
-
});
|
|
309
|
-
this.currentStatus = next;
|
|
310
|
-
this.options.onStatus?.(next, detail);
|
|
506
|
+
return result;
|
|
311
507
|
}
|
|
312
508
|
};
|
|
313
509
|
//#endregion
|
|
314
|
-
export {
|
|
510
|
+
export { FlumeGitHubSource, flumeExtractGitHubMeta };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
var FlumeHttpError = class extends Error {
|
|
3
3
|
status;
|
|
4
4
|
constructor(props) {
|
|
5
|
-
super(props.message);
|
|
5
|
+
super(props.message, props.cause === void 0 ? void 0 : { cause: props.cause });
|
|
6
6
|
this.name = "FlumeHttpError";
|
|
7
7
|
this.status = props.status;
|
|
8
8
|
Object.freeze(this);
|