@interactive-inc/flume 0.10.0 → 0.11.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 +93 -32
- package/dist/discord.d.ts +15 -1
- package/dist/discord.js +304 -35
- package/dist/flume-source.d.ts +58 -14
- package/dist/flume-source.js +142 -87
- package/dist/github.d.ts +5 -1
- package/dist/github.js +217 -69
- package/dist/index.d.ts +223 -46
- package/dist/index.js +466 -149
- package/dist/parse-error.d.ts +9 -0
- package/dist/safe-json-parse.js +1 -1
- package/dist/safe-read-text.js +22 -5
- package/dist/safe-stringify.js +34 -134
- package/dist/schedule-reconnect.js +153 -0
- package/dist/slack.d.ts +8 -1
- package/dist/slack.js +230 -34
- package/dist/time.d.ts +81 -2
- package/dist/time.js +625 -2
- package/package.json +11 -4
- package/dist/connection-error.js +0 -16
- package/dist/http-error.js +0 -12
- package/dist/parse-cron.d.ts +0 -55
- package/dist/time-source.js +0 -427
package/dist/github.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
import {
|
|
1
|
+
import { c as FlumeParseError, d as safeErrorMessage, i as safeNow, l as attempt, r as FlumeLogger, t as FlumeSource, u as safeNormalizeError } from "./flume-source.js";
|
|
2
|
+
import { n as FlumeHttpError, t as safeReadText } from "./safe-read-text.js";
|
|
3
3
|
import { t as safeJsonParse } from "./safe-json-parse.js";
|
|
4
|
-
import { t as safeReadText } from "./safe-read-text.js";
|
|
5
4
|
import { z } from "zod/v4";
|
|
6
5
|
//#region lib/github/extract-github-meta.ts
|
|
7
6
|
function flumeExtractGitHubMeta(notification) {
|
|
@@ -38,7 +37,12 @@ var FlumeGitHubSeenCache = class {
|
|
|
38
37
|
has(id, updatedAt) {
|
|
39
38
|
return this.seen.get(id) === updatedAt;
|
|
40
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* 既存キーは delete → set で挿入順を末尾へ更新する (LRU)。
|
|
42
|
+
* これをしないと trim() が「長寿命で更新され続ける thread」から先に追い出す
|
|
43
|
+
*/
|
|
41
44
|
add(id, updatedAt) {
|
|
45
|
+
this.seen.delete(id);
|
|
42
46
|
this.seen.set(id, updatedAt);
|
|
43
47
|
}
|
|
44
48
|
trim() {
|
|
@@ -59,8 +63,11 @@ var FlumeGitHubSeenCache = class {
|
|
|
59
63
|
const NOTIFICATIONS_URL = "https://api.github.com/notifications";
|
|
60
64
|
const CONSECUTIVE_ERRORS_TO_DISCONNECT = 3;
|
|
61
65
|
const SEEN_CACHE_MAX = 5e3;
|
|
66
|
+
const MAX_PAGES = 1e3;
|
|
67
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
62
68
|
/**
|
|
63
69
|
* GitHub /notifications を条件付きポーリングする (ETag / Last-Modified)。
|
|
70
|
+
* Link: rel="next" を MAX_PAGES まで辿って全ページを集約し、
|
|
64
71
|
* 304 / X-Poll-Interval / レート制限を尊重し、stop() / abort() で in-flight 通信を打ち切る
|
|
65
72
|
*/
|
|
66
73
|
var FlumeGitHubPoller = class {
|
|
@@ -69,6 +76,7 @@ var FlumeGitHubPoller = class {
|
|
|
69
76
|
cache = new FlumeGitHubSeenCache({ maxSize: SEEN_CACHE_MAX });
|
|
70
77
|
timer = null;
|
|
71
78
|
rateLimitTimer = null;
|
|
79
|
+
rateLimitPauseActive = false;
|
|
72
80
|
since = null;
|
|
73
81
|
etag = null;
|
|
74
82
|
lastModified = null;
|
|
@@ -76,6 +84,7 @@ var FlumeGitHubPoller = class {
|
|
|
76
84
|
isStoppedFlag = false;
|
|
77
85
|
inFlight = false;
|
|
78
86
|
consecutiveErrors = 0;
|
|
87
|
+
degraded = false;
|
|
79
88
|
effectiveIntervalSec;
|
|
80
89
|
controller = null;
|
|
81
90
|
constructor(props) {
|
|
@@ -108,7 +117,7 @@ var FlumeGitHubPoller = class {
|
|
|
108
117
|
const error = await this.poll();
|
|
109
118
|
if (error) return error;
|
|
110
119
|
if (this.isStoppedFlag) return null;
|
|
111
|
-
if (this.
|
|
120
|
+
if (this.rateLimitPauseActive) return null;
|
|
112
121
|
this.scheduleInterval();
|
|
113
122
|
return null;
|
|
114
123
|
}
|
|
@@ -122,6 +131,7 @@ var FlumeGitHubPoller = class {
|
|
|
122
131
|
this.controller = null;
|
|
123
132
|
this.clearTimer();
|
|
124
133
|
this.clearRateLimitTimer();
|
|
134
|
+
this.rateLimitPauseActive = false;
|
|
125
135
|
}
|
|
126
136
|
scheduleInterval() {
|
|
127
137
|
this.clearTimer();
|
|
@@ -134,7 +144,7 @@ var FlumeGitHubPoller = class {
|
|
|
134
144
|
error
|
|
135
145
|
});
|
|
136
146
|
});
|
|
137
|
-
}, this.effectiveIntervalSec * 1e3));
|
|
147
|
+
}, Math.min(this.effectiveIntervalSec * 1e3, MAX_TIMER_DELAY_MS)));
|
|
138
148
|
if (intervalResult instanceof Error) {
|
|
139
149
|
this.log.error({
|
|
140
150
|
action: "poller.interval.schedule.error",
|
|
@@ -150,10 +160,10 @@ var FlumeGitHubPoller = class {
|
|
|
150
160
|
async poll() {
|
|
151
161
|
if (this.inFlight || this.isStoppedFlag) return null;
|
|
152
162
|
this.inFlight = true;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const cause = safeNormalizeError({ value:
|
|
163
|
+
const attempted = await attempt(async () => ({ result: await this.pollOnce() }));
|
|
164
|
+
this.inFlight = false;
|
|
165
|
+
if (attempted instanceof Error) {
|
|
166
|
+
const cause = safeNormalizeError({ value: attempted });
|
|
157
167
|
const error = new FlumeHttpError({
|
|
158
168
|
message: `poll loop threw: ${safeErrorMessage({ error: cause })}`,
|
|
159
169
|
status: 0,
|
|
@@ -166,9 +176,8 @@ var FlumeGitHubPoller = class {
|
|
|
166
176
|
});
|
|
167
177
|
if (!this.bootstrapped) return error;
|
|
168
178
|
return null;
|
|
169
|
-
} finally {
|
|
170
|
-
this.inFlight = false;
|
|
171
179
|
}
|
|
180
|
+
return attempted.result;
|
|
172
181
|
}
|
|
173
182
|
async pollOnce() {
|
|
174
183
|
const params = new URLSearchParams({
|
|
@@ -176,7 +185,114 @@ var FlumeGitHubPoller = class {
|
|
|
176
185
|
per_page: "50"
|
|
177
186
|
});
|
|
178
187
|
if (this.since) params.set("since", this.since);
|
|
179
|
-
const
|
|
188
|
+
const response = await this.fetchPage(`${NOTIFICATIONS_URL}?${params}`, true);
|
|
189
|
+
if (this.isStoppedFlag) return null;
|
|
190
|
+
if (response instanceof Error) return this.recordFailure({
|
|
191
|
+
kind: "network",
|
|
192
|
+
message: response.message,
|
|
193
|
+
cause: response
|
|
194
|
+
});
|
|
195
|
+
this.followPollIntervalHeader(response.headers.get("X-Poll-Interval"));
|
|
196
|
+
if (response.status === 304) {
|
|
197
|
+
this.consecutiveErrors = 0;
|
|
198
|
+
this.log.debug({
|
|
199
|
+
action: "poll.not-modified",
|
|
200
|
+
message: "304 Not Modified"
|
|
201
|
+
});
|
|
202
|
+
this.notifyRecoveredIfDegraded();
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
if (await this.isRateLimited(response)) {
|
|
206
|
+
this.handleRateLimit(response);
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
if (!response.ok) {
|
|
210
|
+
const error = new FlumeHttpError({
|
|
211
|
+
message: `HTTP ${response.status}`,
|
|
212
|
+
status: response.status
|
|
213
|
+
});
|
|
214
|
+
return this.recordFailure({
|
|
215
|
+
kind: "http",
|
|
216
|
+
message: safeErrorMessage({ error }),
|
|
217
|
+
error
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
const nextEtag = response.headers.get("ETag");
|
|
221
|
+
const nextLastModified = response.headers.get("Last-Modified");
|
|
222
|
+
const rawNotifications = await this.collectPages(response);
|
|
223
|
+
if (!Array.isArray(rawNotifications)) return rawNotifications;
|
|
224
|
+
this.processNotifications(rawNotifications);
|
|
225
|
+
this.consecutiveErrors = 0;
|
|
226
|
+
this.etag = nextEtag;
|
|
227
|
+
this.lastModified = nextLastModified;
|
|
228
|
+
this.advanceCursor();
|
|
229
|
+
this.notifyRecoveredIfDegraded();
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* 先頭ページの body を読み、Link: rel="next" を MAX_PAGES まで辿って
|
|
234
|
+
* 全ページの生 notifications を集約する。2 ページ目以降は無条件リクエスト
|
|
235
|
+
* (If-None-Match / If-Modified-Since なし)。失敗・停止・rate limit 時は
|
|
236
|
+
* Error | null を返し pollOnce がそのまま返す
|
|
237
|
+
*/
|
|
238
|
+
async collectPages(firstResponse) {
|
|
239
|
+
const rawNotifications = [];
|
|
240
|
+
let pageResponse = firstResponse;
|
|
241
|
+
let pageCount = 1;
|
|
242
|
+
while (true) {
|
|
243
|
+
const body = await this.readPageBody(pageResponse);
|
|
244
|
+
if (this.isStoppedFlag) return null;
|
|
245
|
+
if (body instanceof Error) return this.recordFailure({
|
|
246
|
+
kind: "http",
|
|
247
|
+
message: body.message,
|
|
248
|
+
error: body
|
|
249
|
+
});
|
|
250
|
+
if (body === null) return null;
|
|
251
|
+
for (const item of body) rawNotifications.push(item);
|
|
252
|
+
const nextUrl = this.findNextPageUrl(pageResponse.headers.get("Link"));
|
|
253
|
+
if (nextUrl === null) return rawNotifications;
|
|
254
|
+
if (pageCount >= MAX_PAGES) {
|
|
255
|
+
const error = new FlumeHttpError({
|
|
256
|
+
message: `pagination exceeded the safety limit of ${MAX_PAGES} pages`,
|
|
257
|
+
status: 0
|
|
258
|
+
});
|
|
259
|
+
return this.recordFailure({
|
|
260
|
+
kind: "http",
|
|
261
|
+
message: error.message,
|
|
262
|
+
error
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const nextResponse = await this.fetchPage(nextUrl, false);
|
|
266
|
+
if (this.isStoppedFlag) return null;
|
|
267
|
+
if (nextResponse instanceof Error) return this.recordFailure({
|
|
268
|
+
kind: "network",
|
|
269
|
+
message: nextResponse.message,
|
|
270
|
+
cause: nextResponse
|
|
271
|
+
});
|
|
272
|
+
if (await this.isRateLimited(nextResponse)) {
|
|
273
|
+
this.handleRateLimit(nextResponse);
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
if (!nextResponse.ok) {
|
|
277
|
+
const error = new FlumeHttpError({
|
|
278
|
+
message: `HTTP ${nextResponse.status}`,
|
|
279
|
+
status: nextResponse.status
|
|
280
|
+
});
|
|
281
|
+
return this.recordFailure({
|
|
282
|
+
kind: "http",
|
|
283
|
+
message: safeErrorMessage({ error }),
|
|
284
|
+
error
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
pageResponse = nextResponse;
|
|
288
|
+
pageCount++;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* 1 ページ分を fetch する。条件付きヘッダ (If-None-Match / If-Modified-Since) は
|
|
293
|
+
* 先頭ページのみ付与する
|
|
294
|
+
*/
|
|
295
|
+
async fetchPage(url, isFirstPage) {
|
|
180
296
|
this.log.debug({
|
|
181
297
|
action: "http.request",
|
|
182
298
|
message: `GET ${url}`
|
|
@@ -186,24 +302,19 @@ var FlumeGitHubPoller = class {
|
|
|
186
302
|
Accept: "application/vnd.github+json",
|
|
187
303
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
188
304
|
};
|
|
189
|
-
if (this.etag) headers["If-None-Match"] = this.etag;
|
|
190
|
-
if (this.lastModified) headers["If-Modified-Since"] = this.lastModified;
|
|
305
|
+
if (isFirstPage && this.etag) headers["If-None-Match"] = this.etag;
|
|
306
|
+
if (isFirstPage && this.lastModified) headers["If-Modified-Since"] = this.lastModified;
|
|
191
307
|
const response = await attempt(() => this.props.deps.fetch(url, {
|
|
192
308
|
headers,
|
|
193
309
|
signal: this.controller?.signal
|
|
194
310
|
}));
|
|
195
|
-
if (this.isStoppedFlag) return null;
|
|
196
311
|
if (response instanceof Error) {
|
|
197
312
|
this.log.error({
|
|
198
313
|
action: "http.error",
|
|
199
314
|
message: safeErrorMessage({ error: response }),
|
|
200
315
|
error: response
|
|
201
316
|
});
|
|
202
|
-
return
|
|
203
|
-
kind: "network",
|
|
204
|
-
message: response.message,
|
|
205
|
-
cause: response
|
|
206
|
-
});
|
|
317
|
+
return response;
|
|
207
318
|
}
|
|
208
319
|
this.log.debug({
|
|
209
320
|
action: "http.response",
|
|
@@ -213,49 +324,24 @@ var FlumeGitHubPoller = class {
|
|
|
213
324
|
url
|
|
214
325
|
}
|
|
215
326
|
});
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return null;
|
|
224
|
-
}
|
|
225
|
-
if (this.isRateLimited(response)) {
|
|
226
|
-
this.handleRateLimit(response);
|
|
227
|
-
return null;
|
|
228
|
-
}
|
|
229
|
-
if (!response.ok) {
|
|
230
|
-
const error = new FlumeHttpError({
|
|
231
|
-
message: `HTTP ${response.status}`,
|
|
232
|
-
status: response.status
|
|
233
|
-
});
|
|
234
|
-
return this.recordFailure({
|
|
235
|
-
kind: "http",
|
|
236
|
-
message: safeErrorMessage({ error }),
|
|
237
|
-
error
|
|
238
|
-
});
|
|
239
|
-
}
|
|
240
|
-
this.consecutiveErrors = 0;
|
|
241
|
-
this.etag = response.headers.get("ETag");
|
|
242
|
-
this.lastModified = response.headers.get("Last-Modified");
|
|
327
|
+
return response;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* body を読んで JSON 配列に解決する。読み取り失敗は FlumeHttpError (呼び出し側で
|
|
331
|
+
* recordFailure する)、JSON 破損・非配列は warn 済みの null (poll ごと破棄) を返す
|
|
332
|
+
*/
|
|
333
|
+
async readPageBody(response) {
|
|
243
334
|
const text = await safeReadText({
|
|
244
335
|
response,
|
|
245
336
|
context: "notifications"
|
|
246
337
|
});
|
|
247
|
-
if (this.isStoppedFlag) return null;
|
|
248
338
|
if (text instanceof FlumeHttpError) {
|
|
249
339
|
this.log.warn({
|
|
250
340
|
action: "http.body.read",
|
|
251
341
|
message: safeErrorMessage({ error: text }),
|
|
252
342
|
error: text
|
|
253
343
|
});
|
|
254
|
-
return
|
|
255
|
-
kind: "http",
|
|
256
|
-
message: text.message,
|
|
257
|
-
error: text
|
|
258
|
-
});
|
|
344
|
+
return text;
|
|
259
345
|
}
|
|
260
346
|
const json = safeJsonParse(text);
|
|
261
347
|
if (json instanceof FlumeParseError) {
|
|
@@ -274,7 +360,15 @@ var FlumeGitHubPoller = class {
|
|
|
274
360
|
});
|
|
275
361
|
return null;
|
|
276
362
|
}
|
|
277
|
-
|
|
363
|
+
return json;
|
|
364
|
+
}
|
|
365
|
+
findNextPageUrl(linkHeader) {
|
|
366
|
+
if (linkHeader === null) return null;
|
|
367
|
+
for (const part of linkHeader.split(",")) {
|
|
368
|
+
if (!part.includes("rel=\"next\"")) continue;
|
|
369
|
+
const url = part.match(/<([^>]+)>/)?.[1];
|
|
370
|
+
if (url !== void 0) return url;
|
|
371
|
+
}
|
|
278
372
|
return null;
|
|
279
373
|
}
|
|
280
374
|
recordFailure(input) {
|
|
@@ -290,14 +384,40 @@ var FlumeGitHubPoller = class {
|
|
|
290
384
|
error,
|
|
291
385
|
detail: { consecutiveErrors: this.consecutiveErrors }
|
|
292
386
|
});
|
|
293
|
-
if (this.consecutiveErrors >= CONSECUTIVE_ERRORS_TO_DISCONNECT && !this.isStoppedFlag)
|
|
387
|
+
if (this.consecutiveErrors >= CONSECUTIVE_ERRORS_TO_DISCONNECT && !this.isStoppedFlag) {
|
|
388
|
+
this.degraded = true;
|
|
389
|
+
this.props.onDisconnected(input.kind === "network" ? "network error" : input.message);
|
|
390
|
+
}
|
|
294
391
|
if (!this.bootstrapped) return error;
|
|
295
392
|
return null;
|
|
296
393
|
}
|
|
297
|
-
|
|
394
|
+
/**
|
|
395
|
+
* onDisconnected 通知後に poll が完全成功したら connected へ戻す。
|
|
396
|
+
* degraded でなければ何もしない (冪等)
|
|
397
|
+
*/
|
|
398
|
+
notifyRecoveredIfDegraded() {
|
|
399
|
+
if (!this.degraded) return;
|
|
400
|
+
this.degraded = false;
|
|
401
|
+
this.props.onConnected();
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 429 は常に rate limit。403 は Retry-After 付き、X-RateLimit-Remaining が 0、
|
|
405
|
+
* または本文が secondary rate limit / abuse detection を示す場合
|
|
406
|
+
*/
|
|
407
|
+
async isRateLimited(response) {
|
|
298
408
|
if (response.status === 429) return true;
|
|
299
|
-
if (response.status
|
|
300
|
-
return
|
|
409
|
+
if (response.status !== 403) return false;
|
|
410
|
+
if (response.headers.get("Retry-After") !== null) return true;
|
|
411
|
+
if (response.headers.get("X-RateLimit-Remaining") === "0") return true;
|
|
412
|
+
const cloned = attempt(() => response.clone());
|
|
413
|
+
if (cloned instanceof Error) return false;
|
|
414
|
+
const body = await safeReadText({
|
|
415
|
+
response: cloned,
|
|
416
|
+
context: "rate limit response"
|
|
417
|
+
});
|
|
418
|
+
if (body instanceof Error) return false;
|
|
419
|
+
const normalized = body.toLowerCase();
|
|
420
|
+
return normalized.includes("secondary rate limit") || normalized.includes("abuse detection");
|
|
301
421
|
}
|
|
302
422
|
handleRateLimit(response) {
|
|
303
423
|
const retryAfter = response.headers.get("Retry-After");
|
|
@@ -316,11 +436,21 @@ var FlumeGitHubPoller = class {
|
|
|
316
436
|
});
|
|
317
437
|
this.clearTimer();
|
|
318
438
|
this.clearRateLimitTimer();
|
|
439
|
+
this.rateLimitPauseActive = true;
|
|
319
440
|
const timerResult = attempt(() => this.props.deps.setTimeout(() => {
|
|
320
441
|
this.rateLimitTimer = null;
|
|
442
|
+
this.rateLimitPauseActive = false;
|
|
321
443
|
if (this.isStoppedFlag) return;
|
|
322
444
|
this.scheduleInterval();
|
|
323
|
-
|
|
445
|
+
this.poll().catch((err) => {
|
|
446
|
+
const error = safeNormalizeError({ value: err });
|
|
447
|
+
this.log.error({
|
|
448
|
+
action: "poll.unhandled",
|
|
449
|
+
message: safeErrorMessage({ error }),
|
|
450
|
+
error
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
}, Math.min(delaySec * 1e3, MAX_TIMER_DELAY_MS)));
|
|
324
454
|
if (timerResult instanceof Error) {
|
|
325
455
|
this.log.error({
|
|
326
456
|
action: "poller.rate-limit.schedule.error",
|
|
@@ -328,6 +458,7 @@ var FlumeGitHubPoller = class {
|
|
|
328
458
|
error: timerResult
|
|
329
459
|
});
|
|
330
460
|
this.rateLimitTimer = null;
|
|
461
|
+
if (!this.isStoppedFlag) this.props.onDisconnected("rate-limit timer scheduling rejected by runtime");
|
|
331
462
|
} else this.rateLimitTimer = timerResult;
|
|
332
463
|
}
|
|
333
464
|
clearTimer() {
|
|
@@ -352,19 +483,25 @@ var FlumeGitHubPoller = class {
|
|
|
352
483
|
});
|
|
353
484
|
this.rateLimitTimer = null;
|
|
354
485
|
}
|
|
355
|
-
|
|
486
|
+
/**
|
|
487
|
+
* X-Poll-Interval を双方向に追従する。下限はユーザー指定 interval。
|
|
488
|
+
* ヘッダ欠落・非数値は現在の実効値を維持する
|
|
489
|
+
*/
|
|
490
|
+
followPollIntervalHeader(headerValue) {
|
|
356
491
|
if (headerValue === null) return;
|
|
357
|
-
const
|
|
358
|
-
if (!Number.isFinite(
|
|
492
|
+
const headerSec = Number.parseInt(headerValue, 10);
|
|
493
|
+
if (!Number.isFinite(headerSec)) return;
|
|
494
|
+
const nextSec = Math.min(Math.max(this.props.interval, headerSec), Math.floor(MAX_TIMER_DELAY_MS / 1e3));
|
|
495
|
+
if (nextSec === this.effectiveIntervalSec) return;
|
|
359
496
|
this.log.info({
|
|
360
|
-
action: "poll.
|
|
361
|
-
message: `
|
|
497
|
+
action: "poll.interval.follow",
|
|
498
|
+
message: `adjusting interval ${this.effectiveIntervalSec}s -> ${nextSec}s per X-Poll-Interval`,
|
|
362
499
|
detail: {
|
|
363
500
|
from: this.effectiveIntervalSec,
|
|
364
|
-
to:
|
|
501
|
+
to: nextSec
|
|
365
502
|
}
|
|
366
503
|
});
|
|
367
|
-
this.effectiveIntervalSec =
|
|
504
|
+
this.effectiveIntervalSec = nextSec;
|
|
368
505
|
if (this.timer !== null) this.scheduleInterval();
|
|
369
506
|
}
|
|
370
507
|
processNotifications(raw) {
|
|
@@ -389,11 +526,11 @@ var FlumeGitHubPoller = class {
|
|
|
389
526
|
if (!this.bootstrapped) {
|
|
390
527
|
this.bootstrapped = true;
|
|
391
528
|
for (const notification of notifications) this.cache.add(notification.id, notification.updated_at);
|
|
392
|
-
this.advanceCursor();
|
|
393
529
|
this.log.info({
|
|
394
530
|
action: "poller.bootstrap",
|
|
395
531
|
message: `seeded ${notifications.length} existing notifications`
|
|
396
532
|
});
|
|
533
|
+
this.degraded = false;
|
|
397
534
|
this.props.onConnected();
|
|
398
535
|
return;
|
|
399
536
|
}
|
|
@@ -403,7 +540,6 @@ var FlumeGitHubPoller = class {
|
|
|
403
540
|
return true;
|
|
404
541
|
});
|
|
405
542
|
this.cache.trim();
|
|
406
|
-
this.advanceCursor();
|
|
407
543
|
if (fresh.length > 0) {
|
|
408
544
|
this.log.info({
|
|
409
545
|
action: "poll.fresh",
|
|
@@ -452,6 +588,8 @@ var FlumeGitHubPoller = class {
|
|
|
452
588
|
};
|
|
453
589
|
//#endregion
|
|
454
590
|
//#region lib/github/github-source.ts
|
|
591
|
+
const DEFAULT_POLL_INTERVAL_SEC = 60;
|
|
592
|
+
const MAX_POLL_INTERVAL_SEC = Math.floor(2147483647 / 1e3);
|
|
455
593
|
var FlumeGitHubSource = class extends FlumeSource {
|
|
456
594
|
options;
|
|
457
595
|
name = "github";
|
|
@@ -464,7 +602,7 @@ var FlumeGitHubSource = class extends FlumeSource {
|
|
|
464
602
|
this.setStatus("connecting");
|
|
465
603
|
this.poller = new FlumeGitHubPoller({
|
|
466
604
|
token: this.options.token,
|
|
467
|
-
interval: this.
|
|
605
|
+
interval: this.getPollIntervalSec(),
|
|
468
606
|
onLog: ctx.log.handler,
|
|
469
607
|
deps: ctx.deps,
|
|
470
608
|
onNotifications: (notifications) => this.handleNotifications(ctx, notifications),
|
|
@@ -487,6 +625,16 @@ var FlumeGitHubSource = class extends FlumeSource {
|
|
|
487
625
|
this.poller?.stop();
|
|
488
626
|
this.poller = null;
|
|
489
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* pollInterval が非数値・非有限・0 以下の場合は既定値へフォールバックする
|
|
630
|
+
*/
|
|
631
|
+
getPollIntervalSec() {
|
|
632
|
+
const requested = this.options.pollInterval;
|
|
633
|
+
if (typeof requested !== "number") return DEFAULT_POLL_INTERVAL_SEC;
|
|
634
|
+
if (!Number.isFinite(requested)) return DEFAULT_POLL_INTERVAL_SEC;
|
|
635
|
+
if (requested <= 0) return DEFAULT_POLL_INTERVAL_SEC;
|
|
636
|
+
return Math.min(requested, MAX_POLL_INTERVAL_SEC);
|
|
637
|
+
}
|
|
490
638
|
handleNotifications(ctx, notifications) {
|
|
491
639
|
for (const notification of notifications) this.emit({
|
|
492
640
|
source: "github",
|