@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/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
- import { a as safeNow, c as attempt, i as FlumeLogger, l as safeNormalizeError, n as FlumeSerialQueue, o as FlumeStartError, r as safeInvokeCallback, s as FlumeParseError, t as FlumeSource, u as safeErrorMessage } from "./flume-source.js";
2
- import { t as FlumeConnectionError } from "./connection-error.js";
3
- import { t as FlumeHttpError } from "./http-error.js";
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` の最外殻 try/catch で想定外の throw `FlumeClosed` の resolve に変換する
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 iterator は自然に終了し、`break` すると hub から自動 unsubscribe する。
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
- try {
294
- this.props.log.info({
295
- action: "flume.close",
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
- this.props.hub.close();
346
- return new FlumeClosed({
347
- finalStatuses: this.snapshotStatuses(),
348
- closeErrors
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
- * いずれかの source 失敗時は全 source を `stop()` してロールバックし `FlumeStartError` を返す
434
- * (失敗した source も半接続状態のリソースを持ち得るため、成功分だけでなく全数を stop する)。
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) => this.emitItem({
458
- kind: "event",
459
- event
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
- const onError = this.options.onError;
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 onEvent = this.options.onEvent;
508
- if (!onEvent) return Promise.resolve();
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(onEvent(item)));
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(this.sources);
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
- await this.callbackQueue.drain();
635
+ this.finishFailedOpen();
557
636
  return error;
558
637
  }
559
638
  if (this.isSignalAborted()) {
560
- await this.rollback(this.sources);
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
- await this.callbackQueue.drain();
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.props.onError,
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 };
@@ -1,4 +1,4 @@
1
- import { s as FlumeParseError } from "./flume-source.js";
1
+ import { c as FlumeParseError } from "./flume-source.js";
2
2
  //#region lib/utils/safe-json-parse.ts
3
3
  function safeJsonParse(raw) {
4
4
  try {
@@ -1,5 +1,19 @@
1
- import { c as attempt, u as safeErrorMessage } from "./flume-source.js";
2
- import { t as FlumeHttpError } from "./http-error.js";
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 };
@@ -1,5 +1,19 @@
1
- import { c as attempt, r as safeInvokeCallback, s as FlumeParseError, u as safeErrorMessage } from "./flume-source.js";
2
- import { t as FlumeConnectionError } from "./connection-error.js";
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 i, scheduleFlumeReconnect as n, FlumeReconnector as r, safeStringify as t };
51
+ export { safeRandom as n, FlumeConnectionError as r, safeStringify as t };