@interactive-inc/flume 0.10.1 → 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 +90 -30
- package/dist/discord.js +3 -3
- package/dist/flume-source.d.ts +5 -1
- package/dist/flume-source.js +39 -57
- package/dist/github.js +7 -9
- package/dist/index.d.ts +143 -37
- package/dist/index.js +183 -98
- package/dist/safe-json-parse.js +1 -1
- package/dist/safe-read-text.js +17 -3
- package/dist/safe-stringify.js +17 -153
- package/dist/schedule-reconnect.js +153 -0
- package/dist/slack.js +4 -5
- package/dist/time.d.ts +4 -2
- package/dist/time.js +17 -13
- package/package.json +11 -4
- package/dist/connection-error.js +0 -16
- package/dist/http-error.js +0 -16
package/dist/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { a as safeInvokeCallback, c as FlumeParseError, d as safeErrorMessage, i as safeNow, l as attempt, n as FlumeSerialQueue, o as FlumeSourceReuseError, r as FlumeLogger, s as FlumeStartError, t as FlumeSource, u as safeNormalizeError } from "./flume-source.js";
|
|
2
|
+
import { n as safeRandom, r as FlumeConnectionError, t as safeStringify } from "./safe-stringify.js";
|
|
3
|
+
import { n as FlumeHttpError, t as safeReadText } from "./safe-read-text.js";
|
|
4
|
+
import { t as isRecord } from "./is-record.js";
|
|
5
|
+
import { t as safeJsonParse } from "./safe-json-parse.js";
|
|
4
6
|
//#region lib/deps.ts
|
|
5
7
|
/**
|
|
6
8
|
* `globalThis.WebSocket` の現在の値を返す。
|
|
@@ -87,9 +89,6 @@ var FlumeStream = class {
|
|
|
87
89
|
if (resolver) resolver(doneResult());
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
|
-
get dropped() {
|
|
91
|
-
return this.droppedCount;
|
|
92
|
-
}
|
|
93
92
|
next() {
|
|
94
93
|
const item = this.items.shift();
|
|
95
94
|
if (item !== void 0) return Promise.resolve({
|
|
@@ -224,7 +223,7 @@ var FlumeClosed = class {
|
|
|
224
223
|
/**
|
|
225
224
|
* 稼働中の Flume。close() で FlumeClosed へ遷移する。signal が abort されると自動 close。
|
|
226
225
|
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
227
|
-
* `runClose`
|
|
226
|
+
* `runClose` の最外殻でも `attempt` を通して想定外の throw を `FlumeClosed` の resolve に変換する
|
|
228
227
|
*/
|
|
229
228
|
var FlumeRunning = class {
|
|
230
229
|
props;
|
|
@@ -262,18 +261,28 @@ var FlumeRunning = class {
|
|
|
262
261
|
}
|
|
263
262
|
}
|
|
264
263
|
}
|
|
264
|
+
/** Source の停止まで待つ。callback の完了は callback 外から drain() で待つ。 */
|
|
265
265
|
close() {
|
|
266
266
|
if (this.closePromise) return this.closePromise;
|
|
267
267
|
this.closePromise = this.runClose();
|
|
268
268
|
return this.closePromise;
|
|
269
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* 配送済み callback とその失敗診断が完了するまで待つ。通常は close() の後に呼ぶ。
|
|
272
|
+
* onEvent / onError 内では自身の完了待ちになるため呼ばない。
|
|
273
|
+
*/
|
|
274
|
+
async drain() {
|
|
275
|
+
if (this.closePromise) await this.closePromise;
|
|
276
|
+
await this.props.callbackQueue.drain();
|
|
277
|
+
}
|
|
270
278
|
statuses() {
|
|
271
279
|
return this.snapshotStatuses();
|
|
272
280
|
}
|
|
273
281
|
/**
|
|
274
282
|
* 統合 firehose を pull で受け取る async iterator。`for await (const item of running.stream())`。
|
|
275
283
|
* item は events + 全ログの union (`FlumeStreamItem`)。`item.kind` で判別する。
|
|
276
|
-
* close() / signal abort
|
|
284
|
+
* close() / signal abort 後、callback の失敗診断まで配送して終了する。
|
|
285
|
+
* `break` すると hub から自動 unsubscribe する。
|
|
277
286
|
* consumer が遅れて buffer を超えたら `onOverflow` (既定 drop-oldest) に従う
|
|
278
287
|
*/
|
|
279
288
|
stream(options) {
|
|
@@ -290,64 +299,65 @@ var FlumeRunning = class {
|
|
|
290
299
|
}
|
|
291
300
|
async runClose() {
|
|
292
301
|
const closeErrors = [];
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
message: `closing ${this.props.sources.length} source(s)`
|
|
297
|
-
});
|
|
298
|
-
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
299
|
-
for (const [index, result] of settled.entries()) {
|
|
300
|
-
const source = this.props.sources[index];
|
|
301
|
-
const name = source ? this.sourceName(source) : "?";
|
|
302
|
-
const error = result.status === "rejected" ? safeNormalizeError({ value: result.reason }) : result.value instanceof Error ? result.value : null;
|
|
303
|
-
if (error === null) continue;
|
|
304
|
-
closeErrors.push({
|
|
305
|
-
source: name,
|
|
306
|
-
error
|
|
307
|
-
});
|
|
308
|
-
this.props.log.error({
|
|
309
|
-
action: "flume.close.failed",
|
|
310
|
-
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
311
|
-
error,
|
|
312
|
-
detail: { source: name }
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
const signal = this.props.signal;
|
|
316
|
-
if (signal) {
|
|
317
|
-
const result = attempt(() => signal.removeEventListener("abort", this.onAbort));
|
|
318
|
-
if (result instanceof Error) {
|
|
319
|
-
const error = safeNormalizeError({ value: result });
|
|
320
|
-
this.props.log.error({
|
|
321
|
-
action: "signal.removeListener.failed",
|
|
322
|
-
message: safeErrorMessage({ error }),
|
|
323
|
-
error
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
await this.props.callbackQueue.drain();
|
|
328
|
-
this.props.log.info({
|
|
329
|
-
action: "flume.close.complete",
|
|
330
|
-
message: "all sources closed"
|
|
331
|
-
});
|
|
332
|
-
await this.props.callbackQueue.drain();
|
|
333
|
-
this.props.hub.close();
|
|
334
|
-
return new FlumeClosed({
|
|
335
|
-
finalStatuses: this.snapshotStatuses(),
|
|
336
|
-
closeErrors
|
|
337
|
-
});
|
|
338
|
-
} catch (err) {
|
|
339
|
-
const error = safeNormalizeError({ value: err });
|
|
302
|
+
const result = await attempt(() => this.closeSources(closeErrors));
|
|
303
|
+
if (result instanceof Error) {
|
|
304
|
+
const error = safeNormalizeError({ value: result });
|
|
340
305
|
this.props.log.error({
|
|
341
306
|
action: "flume.close.unhandled",
|
|
342
307
|
message: safeErrorMessage({ error }),
|
|
343
308
|
error
|
|
344
309
|
});
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
310
|
+
}
|
|
311
|
+
const sealed = attempt(() => this.props.seal());
|
|
312
|
+
if (sealed instanceof Error) this.props.log.error({
|
|
313
|
+
action: "flume.close.seal.failed",
|
|
314
|
+
message: safeErrorMessage({ error: sealed }),
|
|
315
|
+
error: sealed
|
|
316
|
+
});
|
|
317
|
+
this.props.callbackQueue.drain().then(() => this.props.hub.close());
|
|
318
|
+
return new FlumeClosed({
|
|
319
|
+
finalStatuses: this.snapshotStatuses(),
|
|
320
|
+
closeErrors
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async closeSources(closeErrors) {
|
|
324
|
+
this.props.log.info({
|
|
325
|
+
action: "flume.close",
|
|
326
|
+
message: `closing ${this.props.sources.length} source(s)`
|
|
327
|
+
});
|
|
328
|
+
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
329
|
+
for (const [index, result] of settled.entries()) {
|
|
330
|
+
const source = this.props.sources[index];
|
|
331
|
+
const name = source ? this.sourceName(source) : "?";
|
|
332
|
+
const error = result.status === "rejected" ? safeNormalizeError({ value: result.reason }) : result.value instanceof Error ? result.value : null;
|
|
333
|
+
if (error === null) continue;
|
|
334
|
+
closeErrors.push({
|
|
335
|
+
source: name,
|
|
336
|
+
error
|
|
337
|
+
});
|
|
338
|
+
this.props.log.error({
|
|
339
|
+
action: "flume.close.failed",
|
|
340
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
341
|
+
error,
|
|
342
|
+
detail: { source: name }
|
|
349
343
|
});
|
|
350
344
|
}
|
|
345
|
+
const signal = this.props.signal;
|
|
346
|
+
if (signal) {
|
|
347
|
+
const result = attempt(() => signal.removeEventListener("abort", this.onAbort));
|
|
348
|
+
if (result instanceof Error) {
|
|
349
|
+
const error = safeNormalizeError({ value: result });
|
|
350
|
+
this.props.log.error({
|
|
351
|
+
action: "signal.removeListener.failed",
|
|
352
|
+
message: safeErrorMessage({ error }),
|
|
353
|
+
error
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.props.log.info({
|
|
358
|
+
action: "flume.close.complete",
|
|
359
|
+
message: "all sources closed"
|
|
360
|
+
});
|
|
351
361
|
}
|
|
352
362
|
snapshotStatuses() {
|
|
353
363
|
return this.props.sources.map((source) => {
|
|
@@ -430,14 +440,15 @@ function resolveFlumeReconnectConfig(input) {
|
|
|
430
440
|
* 起動前の Flume。`open()` で `FlumeRunning` へ遷移する。
|
|
431
441
|
* コンストラクタは単一オブジェクト `{ sources, ...options }` を受け取る (`sources` のみ必須)。
|
|
432
442
|
* events も全ログも 1 本の firehose (`onEvent` push / `stream()` pull) に流れ、購読側が filter する。
|
|
433
|
-
*
|
|
434
|
-
*
|
|
443
|
+
* 起動失敗時はこの open が取得した source を `stop()` してロールバックする。
|
|
444
|
+
* 半接続状態で失敗した source も含むが、再利用を拒否した source は他の所有者のため停止しない。
|
|
435
445
|
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
436
446
|
* Promise rejection に正規化して `allSettled` で捕捉する (`open()` は決して reject しない)
|
|
437
447
|
*/
|
|
438
448
|
var Flume = class {
|
|
439
449
|
options;
|
|
440
450
|
consumed = false;
|
|
451
|
+
isAcceptingItems = true;
|
|
441
452
|
log;
|
|
442
453
|
deps;
|
|
443
454
|
sources;
|
|
@@ -454,10 +465,12 @@ var Flume = class {
|
|
|
454
465
|
handler: this.buildLogHandler(),
|
|
455
466
|
deps: this.deps
|
|
456
467
|
});
|
|
457
|
-
this.sourceEventHandler = (event) =>
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
468
|
+
this.sourceEventHandler = (event) => {
|
|
469
|
+
this.emitItem({
|
|
470
|
+
kind: "event",
|
|
471
|
+
event
|
|
472
|
+
});
|
|
473
|
+
};
|
|
461
474
|
}
|
|
462
475
|
/**
|
|
463
476
|
* stream の buffer 溢れ通知 (stream ごとに初回 1 回)。
|
|
@@ -465,7 +478,6 @@ var Flume = class {
|
|
|
465
478
|
* さらに追い出す自己破壊になるため、push の `onEvent` にだけ warn log として届ける
|
|
466
479
|
*/
|
|
467
480
|
notifyStreamOverflow() {
|
|
468
|
-
if (!this.options.onEvent) return;
|
|
469
481
|
const log = {
|
|
470
482
|
level: "warn",
|
|
471
483
|
source: "flume",
|
|
@@ -481,35 +493,100 @@ var Flume = class {
|
|
|
481
493
|
/** source が受信したログを firehose へ流す handler。error は onError にも分岐する */
|
|
482
494
|
buildLogHandler() {
|
|
483
495
|
return (log) => {
|
|
496
|
+
if (!this.isAcceptingItems) return;
|
|
484
497
|
this.emitItem({
|
|
485
498
|
kind: "log",
|
|
486
499
|
log
|
|
487
500
|
});
|
|
488
|
-
|
|
489
|
-
if (!onError || log.level !== "error") return;
|
|
490
|
-
try {
|
|
491
|
-
Promise.resolve(onError(log)).catch(() => {});
|
|
492
|
-
} catch {}
|
|
501
|
+
if (log.level === "error") this.invokeOnError(log);
|
|
493
502
|
};
|
|
494
503
|
}
|
|
495
504
|
/**
|
|
496
505
|
* firehose の単一 sink: pull の hub と push の onEvent の両方へ item を配る。
|
|
497
506
|
* close 後の遅延 emit (stop 中の straggler) は push 側にも流さない (pull 側と対称にする)。
|
|
498
|
-
* onEvent への転送は this.log を経由しない (経由すると log item 経路で再帰する)
|
|
499
|
-
*
|
|
507
|
+
* onEvent への転送は this.log を経由しない (経由すると log item 経路で再帰する)。
|
|
508
|
+
* callback failure は reportCallbackFailure が pull hub と peer callback へ直接診断する。
|
|
500
509
|
*/
|
|
501
510
|
emitItem(item) {
|
|
502
|
-
if (this.hub.isClosed) return Promise.resolve();
|
|
511
|
+
if (!this.isAcceptingItems || this.hub.isClosed) return Promise.resolve();
|
|
503
512
|
this.hub.publish(item);
|
|
504
513
|
return this.enqueueCallback(item);
|
|
505
514
|
}
|
|
506
|
-
enqueueCallback(item) {
|
|
507
|
-
const
|
|
508
|
-
if (
|
|
515
|
+
enqueueCallback(item, notifyPeerOnFailure = true) {
|
|
516
|
+
const onEventResult = attempt(() => this.options.onEvent);
|
|
517
|
+
if (onEventResult instanceof Error) {
|
|
518
|
+
this.reportCallbackFailure("onEvent", onEventResult, notifyPeerOnFailure, item);
|
|
519
|
+
return Promise.resolve();
|
|
520
|
+
}
|
|
521
|
+
if (!onEventResult) return Promise.resolve();
|
|
522
|
+
return this.callbackQueue.add(async () => {
|
|
523
|
+
const result = await attempt(() => Promise.resolve(onEventResult(item)));
|
|
524
|
+
if (result instanceof Error) this.reportCallbackFailure("onEvent", result, notifyPeerOnFailure, item);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* error log 専用 sink も callbackQueue に載せ、drain() が in-flight callback と
|
|
529
|
+
* その失敗診断まで drain できるようにする
|
|
530
|
+
*/
|
|
531
|
+
invokeOnError(log, notifyPeerOnFailure = true) {
|
|
532
|
+
const onErrorResult = attempt(() => this.options.onError);
|
|
533
|
+
if (onErrorResult instanceof Error) {
|
|
534
|
+
this.reportCallbackFailure("onError", onErrorResult, notifyPeerOnFailure, {
|
|
535
|
+
kind: "log",
|
|
536
|
+
log
|
|
537
|
+
});
|
|
538
|
+
return Promise.resolve();
|
|
539
|
+
}
|
|
540
|
+
if (!onErrorResult) return Promise.resolve();
|
|
509
541
|
return this.callbackQueue.add(async () => {
|
|
510
|
-
await attempt(() => Promise.resolve(
|
|
542
|
+
const result = await attempt(() => Promise.resolve(onErrorResult(log)));
|
|
543
|
+
if (result instanceof Error) this.reportCallbackFailure("onError", result, notifyPeerOnFailure, {
|
|
544
|
+
kind: "log",
|
|
545
|
+
log
|
|
546
|
+
});
|
|
511
547
|
});
|
|
512
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* 観測 sink 自身の失敗は同じ sink へ戻すと再帰するため、まず pull stream へ直接 publish し、
|
|
551
|
+
* もう一方の callback にだけ転送する。peer も失敗した場合は hub-only の診断を残して終端する
|
|
552
|
+
*/
|
|
553
|
+
reportCallbackFailure(callback, error, notifyPeer, failedItem) {
|
|
554
|
+
const itemDetail = failedItem?.kind === "event" ? {
|
|
555
|
+
itemKind: failedItem.kind,
|
|
556
|
+
itemSource: failedItem.event.source,
|
|
557
|
+
itemType: failedItem.event.type
|
|
558
|
+
} : failedItem?.kind === "log" ? {
|
|
559
|
+
itemKind: failedItem.kind,
|
|
560
|
+
itemSource: failedItem.log.source,
|
|
561
|
+
itemAction: failedItem.log.action,
|
|
562
|
+
itemDetail: failedItem.log.detail
|
|
563
|
+
} : {};
|
|
564
|
+
const log = {
|
|
565
|
+
level: "error",
|
|
566
|
+
source: "flume",
|
|
567
|
+
action: `${callback}.error`,
|
|
568
|
+
message: `${callback} callback failed: ${safeErrorMessage({ error })}`,
|
|
569
|
+
error,
|
|
570
|
+
detail: {
|
|
571
|
+
callback,
|
|
572
|
+
...itemDetail
|
|
573
|
+
},
|
|
574
|
+
timestamp: safeNow({ deps: this.deps })
|
|
575
|
+
};
|
|
576
|
+
this.hub.publish({
|
|
577
|
+
kind: "log",
|
|
578
|
+
log
|
|
579
|
+
});
|
|
580
|
+
if (!notifyPeer) return;
|
|
581
|
+
if (callback === "onEvent") {
|
|
582
|
+
this.invokeOnError(log, false);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.enqueueCallback({
|
|
586
|
+
kind: "log",
|
|
587
|
+
log
|
|
588
|
+
}, false);
|
|
589
|
+
}
|
|
513
590
|
async open() {
|
|
514
591
|
const guard = this.guardOpen();
|
|
515
592
|
if (guard) return guard;
|
|
@@ -522,10 +599,12 @@ var Flume = class {
|
|
|
522
599
|
const reconnect = this.resolveReconnect();
|
|
523
600
|
const settled = await Promise.allSettled(this.sources.map((source) => this.safeStart(source, reconnect)));
|
|
524
601
|
const failures = [];
|
|
602
|
+
const ownedSources = /* @__PURE__ */ new Set();
|
|
525
603
|
for (const [index, result] of settled.entries()) {
|
|
526
604
|
const source = this.sources[index];
|
|
527
605
|
if (source === void 0) continue;
|
|
528
606
|
const name = this.sourceName(source);
|
|
607
|
+
if (!((result.status === "rejected" ? result.reason : result.value) instanceof FlumeSourceReuseError)) ownedSources.add(source);
|
|
529
608
|
if (result.status === "rejected") {
|
|
530
609
|
failures.push({
|
|
531
610
|
name,
|
|
@@ -545,7 +624,7 @@ var Flume = class {
|
|
|
545
624
|
error: failure.error,
|
|
546
625
|
detail: { source: failure.name }
|
|
547
626
|
});
|
|
548
|
-
await this.rollback(
|
|
627
|
+
await this.rollback([...ownedSources]);
|
|
549
628
|
const detail = failures.map((f) => `${f.name}: ${safeErrorMessage({ error: f.error })}`).join("; ");
|
|
550
629
|
const error = new FlumeStartError(`Flume.open: ${failures.length} source(s) failed: ${detail}`);
|
|
551
630
|
this.log.error({
|
|
@@ -553,42 +632,33 @@ var Flume = class {
|
|
|
553
632
|
message: safeErrorMessage({ error }),
|
|
554
633
|
error
|
|
555
634
|
});
|
|
556
|
-
|
|
635
|
+
this.finishFailedOpen();
|
|
557
636
|
return error;
|
|
558
637
|
}
|
|
559
638
|
if (this.isSignalAborted()) {
|
|
560
|
-
await this.rollback(
|
|
639
|
+
await this.rollback([...ownedSources]);
|
|
561
640
|
const error = new FlumeStartError("Flume.open: aborted during open");
|
|
562
641
|
this.log.warn({
|
|
563
642
|
action: "flume.open.aborted",
|
|
564
643
|
message: safeErrorMessage({ error }),
|
|
565
644
|
error
|
|
566
645
|
});
|
|
567
|
-
|
|
646
|
+
this.finishFailedOpen();
|
|
568
647
|
return error;
|
|
569
648
|
}
|
|
570
649
|
this.log.info({
|
|
571
650
|
action: "flume.open.complete",
|
|
572
651
|
message: "all sources opened"
|
|
573
652
|
});
|
|
574
|
-
await this.callbackQueue.drain();
|
|
575
|
-
if (this.isSignalAborted()) {
|
|
576
|
-
await this.rollback(this.sources);
|
|
577
|
-
const error = new FlumeStartError("Flume.open: aborted during completion");
|
|
578
|
-
this.log.warn({
|
|
579
|
-
action: "flume.open.aborted",
|
|
580
|
-
message: safeErrorMessage({ error }),
|
|
581
|
-
error
|
|
582
|
-
});
|
|
583
|
-
await this.callbackQueue.drain();
|
|
584
|
-
return error;
|
|
585
|
-
}
|
|
586
653
|
const running = new FlumeRunning({
|
|
587
654
|
sources: this.sources,
|
|
588
655
|
signal: this.options.signal,
|
|
589
656
|
log: this.log,
|
|
590
657
|
hub: this.hub,
|
|
591
|
-
callbackQueue: this.callbackQueue
|
|
658
|
+
callbackQueue: this.callbackQueue,
|
|
659
|
+
seal: () => {
|
|
660
|
+
this.isAcceptingItems = false;
|
|
661
|
+
}
|
|
592
662
|
});
|
|
593
663
|
if (!this.isSignalAborted()) return running;
|
|
594
664
|
await running.close();
|
|
@@ -598,9 +668,13 @@ var Flume = class {
|
|
|
598
668
|
message: safeErrorMessage({ error }),
|
|
599
669
|
error
|
|
600
670
|
});
|
|
601
|
-
await this.callbackQueue.drain();
|
|
602
671
|
return error;
|
|
603
672
|
}
|
|
673
|
+
/** 起動失敗の戻り値は callback を待たず、配送済み診断の完了後に hub を閉じる。 */
|
|
674
|
+
finishFailedOpen() {
|
|
675
|
+
this.isAcceptingItems = false;
|
|
676
|
+
this.callbackQueue.drain().then(() => this.hub.close());
|
|
677
|
+
}
|
|
604
678
|
guardOpen() {
|
|
605
679
|
if (this.consumed) {
|
|
606
680
|
const error = new FlumeStartError("Flume.open: already opened");
|
|
@@ -823,7 +897,7 @@ var FlumeConfluence = class {
|
|
|
823
897
|
const flume = new Flume({
|
|
824
898
|
sources,
|
|
825
899
|
onEvent: this.wrapOnEvent(id),
|
|
826
|
-
onError: this.
|
|
900
|
+
onError: this.wrapOnError(id),
|
|
827
901
|
deps: this.props.deps,
|
|
828
902
|
reconnect: this.props.reconnect,
|
|
829
903
|
signal: controller.signal
|
|
@@ -855,6 +929,17 @@ var FlumeConfluence = class {
|
|
|
855
929
|
});
|
|
856
930
|
};
|
|
857
931
|
}
|
|
932
|
+
wrapOnError(id) {
|
|
933
|
+
const onError = this.props.onError;
|
|
934
|
+
if (!onError) return void 0;
|
|
935
|
+
return (log) => onError({
|
|
936
|
+
...log,
|
|
937
|
+
detail: {
|
|
938
|
+
...log.detail,
|
|
939
|
+
groupId: id
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
858
943
|
};
|
|
859
944
|
//#endregion
|
|
860
|
-
export { Flume, FlumeClosed, FlumeConfluence, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeSource, FlumeStartError, createFlumeDefaultDeps };
|
|
945
|
+
export { Flume, FlumeClosed, FlumeConfluence, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeSource, FlumeStartError, attempt, createFlumeDefaultDeps, isRecord, safeErrorMessage, safeInvokeCallback, safeJsonParse, safeNormalizeError, safeNow, safeRandom, safeReadText, safeStringify };
|
package/dist/safe-json-parse.js
CHANGED
package/dist/safe-read-text.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { d as safeErrorMessage, l as attempt } from "./flume-source.js";
|
|
2
|
+
//#region lib/errors/http-error.ts
|
|
3
|
+
var FlumeHttpError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
retryAfterMs;
|
|
7
|
+
constructor(props) {
|
|
8
|
+
super(props.message, props.cause === void 0 ? void 0 : { cause: props.cause });
|
|
9
|
+
this.name = "FlumeHttpError";
|
|
10
|
+
this.status = props.status;
|
|
11
|
+
this.code = props.code ?? null;
|
|
12
|
+
this.retryAfterMs = props.retryAfterMs ?? null;
|
|
13
|
+
Object.freeze(this);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
3
17
|
//#region lib/utils/safe-read-text.ts
|
|
4
18
|
/**
|
|
5
19
|
* `response.text()` を保護する。body 読み取り中の reject (接続切断 / 解凍失敗 / 二重消費) を
|
|
@@ -20,4 +34,4 @@ async function safeReadText(props) {
|
|
|
20
34
|
}
|
|
21
35
|
}
|
|
22
36
|
//#endregion
|
|
23
|
-
export { safeReadText as t };
|
|
37
|
+
export { FlumeHttpError as n, safeReadText as t };
|
package/dist/safe-stringify.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
|
-
import { c as
|
|
2
|
-
|
|
1
|
+
import { c as FlumeParseError, l as attempt } from "./flume-source.js";
|
|
2
|
+
//#region lib/errors/connection-error.ts
|
|
3
|
+
/**
|
|
4
|
+
* 接続失敗を表す。Discord Gateway / Slack Socket Mode / その他 WebSocket 系の close で発生。
|
|
5
|
+
* `code` は接続が落ちた際の close code (Discord は 4xxx 帯が再接続可否を示す)
|
|
6
|
+
*/
|
|
7
|
+
var FlumeConnectionError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
11
|
+
this.name = "FlumeConnectionError";
|
|
12
|
+
this.code = options?.code ?? null;
|
|
13
|
+
Object.freeze(this);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
3
17
|
//#region lib/utils/safe-random.ts
|
|
4
18
|
/**
|
|
5
19
|
* `deps.random()` を保護する。throw / 範囲外値 / 非数値が返った場合は `Math.random()` へ
|
|
@@ -20,156 +34,6 @@ function safeRandom(props) {
|
|
|
20
34
|
}
|
|
21
35
|
}
|
|
22
36
|
//#endregion
|
|
23
|
-
//#region lib/reconnector.ts
|
|
24
|
-
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
25
|
-
/**
|
|
26
|
-
* 指数バックオフ + ジッタ付きの再接続スケジューラ。
|
|
27
|
-
* setTimeout コールバック内のユーザー fn が throw / reject しても reconnect ループは止めない。
|
|
28
|
-
* `generation` は clearTimeout が throw して古い timer が生き残った場合でも
|
|
29
|
-
* stale な発火を無視するための世代トークン
|
|
30
|
-
*/
|
|
31
|
-
var FlumeReconnector = class {
|
|
32
|
-
props;
|
|
33
|
-
currentAttempt = 0;
|
|
34
|
-
isAborted = false;
|
|
35
|
-
timer = null;
|
|
36
|
-
generation = 0;
|
|
37
|
-
constructor(props) {
|
|
38
|
-
this.props = props;
|
|
39
|
-
}
|
|
40
|
-
get attempt() {
|
|
41
|
-
return this.currentAttempt;
|
|
42
|
-
}
|
|
43
|
-
get aborted() {
|
|
44
|
-
return this.isAborted;
|
|
45
|
-
}
|
|
46
|
-
schedule(fn, options) {
|
|
47
|
-
if (this.isAborted) return { kind: "refused" };
|
|
48
|
-
if (this.currentAttempt >= this.props.maxAttempts) {
|
|
49
|
-
this.generation++;
|
|
50
|
-
this.clearTimer();
|
|
51
|
-
return { kind: "exhausted" };
|
|
52
|
-
}
|
|
53
|
-
this.clearTimer();
|
|
54
|
-
const delay = this.computeDelay(options?.minDelayMs ?? 0);
|
|
55
|
-
const scheduledGeneration = ++this.generation;
|
|
56
|
-
const timerResult = attempt(() => this.props.deps.setTimeout(() => this.runRetry(fn, scheduledGeneration), delay));
|
|
57
|
-
if (timerResult instanceof Error) {
|
|
58
|
-
this.props.log.error({
|
|
59
|
-
action: "reconnect.timer.schedule.error",
|
|
60
|
-
message: safeErrorMessage({ error: timerResult }),
|
|
61
|
-
error: timerResult
|
|
62
|
-
});
|
|
63
|
-
this.timer = null;
|
|
64
|
-
return { kind: "refused" };
|
|
65
|
-
}
|
|
66
|
-
this.currentAttempt++;
|
|
67
|
-
this.timer = timerResult;
|
|
68
|
-
return {
|
|
69
|
-
kind: "scheduled",
|
|
70
|
-
delayMs: delay
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
reset() {
|
|
74
|
-
this.currentAttempt = 0;
|
|
75
|
-
}
|
|
76
|
-
cancel() {
|
|
77
|
-
this.isAborted = true;
|
|
78
|
-
this.generation++;
|
|
79
|
-
this.clearTimer();
|
|
80
|
-
}
|
|
81
|
-
runRetry(fn, scheduledGeneration) {
|
|
82
|
-
if (this.isAborted) return;
|
|
83
|
-
if (scheduledGeneration !== this.generation) return;
|
|
84
|
-
this.timer = null;
|
|
85
|
-
safeInvokeCallback({
|
|
86
|
-
fn,
|
|
87
|
-
onError: (error) => {
|
|
88
|
-
this.props.log.error({
|
|
89
|
-
action: "reconnect.timer.error",
|
|
90
|
-
message: safeErrorMessage({ error }),
|
|
91
|
-
error
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
clearTimer() {
|
|
97
|
-
if (this.timer === null) return;
|
|
98
|
-
const handle = this.timer;
|
|
99
|
-
const result = attempt(() => this.props.deps.clearTimeout(handle));
|
|
100
|
-
if (result instanceof Error) this.props.log.error({
|
|
101
|
-
action: "reconnect.timer.clear.error",
|
|
102
|
-
message: safeErrorMessage({ error: result }),
|
|
103
|
-
error: result
|
|
104
|
-
});
|
|
105
|
-
this.timer = null;
|
|
106
|
-
}
|
|
107
|
-
computeDelay(minDelayMs) {
|
|
108
|
-
const jittered = Math.min(this.props.baseDelay * 2 ** this.currentAttempt, this.props.maxDelay) * (.5 + safeRandom({ deps: this.props.deps }) * .5);
|
|
109
|
-
return Math.min(Math.max(jittered, Number.isFinite(minDelayMs) && minDelayMs > 0 ? minDelayMs : 0), MAX_TIMER_DELAY_MS);
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region lib/schedule-reconnect.ts
|
|
114
|
-
/**
|
|
115
|
-
* 接続が落ちた際の共通再接続スケジューラ。
|
|
116
|
-
* 再接続の設定状況 (無効 / 中止 / 試行尽き / timer 拒否) を見極めてからステータス遷移する。
|
|
117
|
-
* - reconnector が無ければ reconnect.disabled を info ログし disconnected へ
|
|
118
|
-
* - cancel 済みなら reconnect.aborted を info ログし disconnected へ
|
|
119
|
-
* - schedule() が exhausted なら reconnect.exhausted を error ログし disconnected へ
|
|
120
|
-
* - schedule() が refused (timer 予約失敗) なら reconnect.refused を error ログし disconnected へ
|
|
121
|
-
* ("reconnecting" のまま発火しない timer を待ち続けるハングを防ぐ)
|
|
122
|
-
* - scheduled なら reconnecting へ遷移し reconnect.scheduled を info ログ
|
|
123
|
-
*/
|
|
124
|
-
function scheduleFlumeReconnect(props) {
|
|
125
|
-
if (!props.reconnector) {
|
|
126
|
-
props.log.info({
|
|
127
|
-
action: "reconnect.disabled",
|
|
128
|
-
message: "reconnect is disabled, staying disconnected"
|
|
129
|
-
});
|
|
130
|
-
props.setStatus("disconnected");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
if (props.reconnector.aborted) {
|
|
134
|
-
props.log.info({
|
|
135
|
-
action: "reconnect.aborted",
|
|
136
|
-
message: "reconnector cancelled, staying disconnected"
|
|
137
|
-
});
|
|
138
|
-
props.setStatus("disconnected");
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
const schedule = props.reconnector.schedule(props.retry, { minDelayMs: props.minDelayMs });
|
|
142
|
-
if (schedule.kind === "exhausted") {
|
|
143
|
-
const error = new FlumeConnectionError(`reconnect exhausted after ${props.reconnector.attempt} attempts`);
|
|
144
|
-
props.log.error({
|
|
145
|
-
action: "reconnect.exhausted",
|
|
146
|
-
message: safeErrorMessage({ error }),
|
|
147
|
-
error
|
|
148
|
-
});
|
|
149
|
-
props.setStatus("disconnected");
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
if (schedule.kind === "refused") {
|
|
153
|
-
const error = new FlumeConnectionError("reconnect timer could not be scheduled");
|
|
154
|
-
props.log.error({
|
|
155
|
-
action: "reconnect.refused",
|
|
156
|
-
message: safeErrorMessage({ error }),
|
|
157
|
-
error
|
|
158
|
-
});
|
|
159
|
-
props.setStatus("disconnected");
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
props.setStatus("reconnecting");
|
|
163
|
-
props.log.info({
|
|
164
|
-
action: "reconnect.scheduled",
|
|
165
|
-
message: `next attempt in ${Math.round(schedule.delayMs)}ms`,
|
|
166
|
-
detail: {
|
|
167
|
-
attempt: props.reconnector.attempt,
|
|
168
|
-
delayMs: Math.round(schedule.delayMs)
|
|
169
|
-
}
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
//#endregion
|
|
173
37
|
//#region lib/utils/safe-stringify.ts
|
|
174
38
|
/**
|
|
175
39
|
* `JSON.stringify` を `string | Error` に変換するラッパ。
|
|
@@ -184,4 +48,4 @@ function safeStringify(value) {
|
|
|
184
48
|
return result;
|
|
185
49
|
}
|
|
186
50
|
//#endregion
|
|
187
|
-
export { safeRandom as
|
|
51
|
+
export { safeRandom as n, FlumeConnectionError as r, safeStringify as t };
|