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