@interactive-inc/flume 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +129 -86
- package/dist/discord.d.ts +5 -13
- package/dist/discord.js +46 -107
- package/dist/flume-source-DUvt9aJt.js +341 -0
- package/dist/flume-source-DuUFPhSe.d.ts +228 -0
- package/dist/github.d.ts +4 -12
- package/dist/github.js +24 -84
- package/dist/index.d.ts +26 -38
- package/dist/index.js +92 -18
- package/dist/safe-json-parse-CfJjt-RY.js +11 -0
- package/dist/{safe-read-text-DgrJ4Uhl.js → safe-read-text-JQd_5vbd.js} +1 -1
- package/dist/{safe-stringify-BWS-uXZP.js → safe-stringify-DbWQw9qe.js} +2 -17
- package/dist/slack.d.ts +5 -13
- package/dist/slack.js +56 -114
- package/package.json +2 -1
- package/dist/safe-invoke-callback-EpWXwfwp.js +0 -170
- package/dist/serial-queue-B9LoBc64.js +0 -162
- package/dist/types-D-tO-Mh2.d.ts +0 -154
package/dist/slack.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { a as
|
|
1
|
+
import { a as FlumeStartError, c as safeNormalizeError, i as safeNow, l as safeErrorMessage, o as FlumeParseError, r as FlumeLogger, s as attempt, t as FlumeSource } from "./flume-source-DUvt9aJt.js";
|
|
2
2
|
import { t as FlumeConnectionError } from "./connection-error-HUO3PC3G.js";
|
|
3
3
|
import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import { t as safeReadText } from "./safe-read-text-
|
|
4
|
+
import { i as FlumeReconnector, n as isRecord, r as scheduleFlumeReconnect, t as safeStringify } from "./safe-stringify-DbWQw9qe.js";
|
|
5
|
+
import { t as safeJsonParse } from "./safe-json-parse-CfJjt-RY.js";
|
|
6
|
+
import { t as safeReadText } from "./safe-read-text-JQd_5vbd.js";
|
|
7
7
|
import { z } from "zod/v4";
|
|
8
8
|
//#region lib/slack/extract-slack-meta.ts
|
|
9
9
|
function flumeExtractSlackMeta(envelope) {
|
|
@@ -472,108 +472,58 @@ var FlumeSlackSocketMode = class {
|
|
|
472
472
|
//#region lib/slack/slack-source.ts
|
|
473
473
|
const SEEN_CACHE_MAX = 1024;
|
|
474
474
|
const SEEN_CACHE_TTL_MS = 300 * 1e3;
|
|
475
|
-
var FlumeSlackSource = class {
|
|
475
|
+
var FlumeSlackSource = class extends FlumeSource {
|
|
476
476
|
options;
|
|
477
477
|
name = "slack";
|
|
478
478
|
socket = null;
|
|
479
479
|
reconnector = null;
|
|
480
|
-
handler = null;
|
|
481
480
|
internalController = null;
|
|
482
|
-
|
|
483
|
-
deps;
|
|
484
|
-
queue = new FlumeSerialQueue();
|
|
485
|
-
seen;
|
|
486
|
-
signals;
|
|
487
|
-
statusEmitter;
|
|
488
|
-
onSignalAbort = () => {
|
|
489
|
-
safeInvokeCallback({
|
|
490
|
-
fn: () => this.stop(),
|
|
491
|
-
onError: (error) => {
|
|
492
|
-
this.log.error({
|
|
493
|
-
action: "signal.abort.stop.failed",
|
|
494
|
-
message: safeErrorMessage({ error }),
|
|
495
|
-
error
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
};
|
|
481
|
+
seen = null;
|
|
500
482
|
constructor(options) {
|
|
483
|
+
super();
|
|
501
484
|
this.options = options;
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
handler: options.onLog,
|
|
506
|
-
deps: this.deps
|
|
507
|
-
});
|
|
508
|
-
this.signals = new FlumeSignalRegistry({
|
|
509
|
-
log: this.log,
|
|
510
|
-
onAbort: this.onSignalAbort
|
|
511
|
-
});
|
|
512
|
-
this.statusEmitter = new FlumeStatusEmitter({
|
|
513
|
-
log: this.log,
|
|
514
|
-
onStatus: options.onStatus
|
|
515
|
-
});
|
|
485
|
+
}
|
|
486
|
+
async connect(ctx) {
|
|
487
|
+
if (!this.hasWebSocket(ctx)) return new FlumeStartError("Slack source: deps.WebSocket is null (no WebSocket runtime available)");
|
|
516
488
|
this.seen = new FlumeSlackSeenCache({
|
|
517
489
|
maxSize: SEEN_CACHE_MAX,
|
|
518
490
|
ttlMs: SEEN_CACHE_TTL_MS,
|
|
519
|
-
deps:
|
|
491
|
+
deps: ctx.deps
|
|
520
492
|
});
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
deps: this.deps
|
|
493
|
+
if (ctx.reconnect && !this.reconnector) this.reconnector = new FlumeReconnector({
|
|
494
|
+
...ctx.reconnect,
|
|
495
|
+
log: ctx.log,
|
|
496
|
+
deps: ctx.deps
|
|
526
497
|
});
|
|
527
|
-
}
|
|
528
|
-
async start(handler, options) {
|
|
529
|
-
if (this.signals.isAnyAborted(this.options.signal) || this.signals.isAnyAborted(options?.signal)) return new FlumeStartError("Slack source: signal already aborted");
|
|
530
|
-
if (!this.hasWebSocket()) return new FlumeStartError("Slack source: deps.WebSocket is null (no WebSocket runtime available)");
|
|
531
|
-
this.signals.register(this.options.signal);
|
|
532
|
-
this.signals.register(options?.signal);
|
|
533
|
-
this.handler = handler;
|
|
534
498
|
const controllerResult = attempt(() => new AbortController());
|
|
535
499
|
if (controllerResult instanceof Error) {
|
|
536
500
|
const error = safeNormalizeError({ value: controllerResult });
|
|
537
|
-
|
|
501
|
+
ctx.log.error({
|
|
538
502
|
action: "slack.abort-controller.new.error",
|
|
539
503
|
message: safeErrorMessage({ error }),
|
|
540
504
|
error
|
|
541
505
|
});
|
|
542
506
|
this.internalController = null;
|
|
543
507
|
} else this.internalController = controllerResult;
|
|
544
|
-
this.
|
|
545
|
-
action: "source.start",
|
|
546
|
-
message: "starting Slack source"
|
|
547
|
-
});
|
|
548
|
-
return await this.connectInternal();
|
|
508
|
+
return await this.connectInternal(ctx);
|
|
549
509
|
}
|
|
550
|
-
|
|
551
|
-
this.
|
|
552
|
-
this.log.
|
|
553
|
-
action: "source.stop",
|
|
554
|
-
message: "stopping Slack source"
|
|
555
|
-
});
|
|
556
|
-
if (this.reconnector && !this.reconnector.aborted) this.log.debug({
|
|
510
|
+
disconnect() {
|
|
511
|
+
const ctx = this.context;
|
|
512
|
+
if (ctx && this.reconnector && !this.reconnector.aborted) ctx.log.debug({
|
|
557
513
|
action: "reconnect.cancel",
|
|
558
514
|
message: "aborting reconnector"
|
|
559
515
|
});
|
|
560
516
|
this.reconnector?.cancel();
|
|
561
517
|
this.internalController?.abort();
|
|
562
518
|
this.socket?.disconnect();
|
|
563
|
-
await this.queue.drain();
|
|
564
519
|
this.socket = null;
|
|
565
|
-
this.handler = null;
|
|
566
520
|
this.internalController = null;
|
|
567
|
-
this.statusEmitter.set("disconnected");
|
|
568
|
-
}
|
|
569
|
-
status() {
|
|
570
|
-
return this.statusEmitter.value;
|
|
571
521
|
}
|
|
572
|
-
hasWebSocket() {
|
|
573
|
-
const result = attempt(() => Boolean(
|
|
522
|
+
hasWebSocket(ctx) {
|
|
523
|
+
const result = attempt(() => Boolean(ctx.deps.WebSocket));
|
|
574
524
|
if (result instanceof Error) {
|
|
575
525
|
const error = safeNormalizeError({ value: result });
|
|
576
|
-
|
|
526
|
+
ctx.log.error({
|
|
577
527
|
action: "deps.web-socket.read.error",
|
|
578
528
|
message: safeErrorMessage({ error }),
|
|
579
529
|
error
|
|
@@ -582,47 +532,49 @@ var FlumeSlackSource = class {
|
|
|
582
532
|
}
|
|
583
533
|
return result;
|
|
584
534
|
}
|
|
585
|
-
async connectInternal() {
|
|
586
|
-
this.
|
|
535
|
+
async connectInternal(ctx) {
|
|
536
|
+
this.setStatus("connecting");
|
|
587
537
|
this.socket = new FlumeSlackSocketMode({
|
|
588
538
|
appToken: this.options.appToken,
|
|
589
|
-
onLog:
|
|
590
|
-
deps:
|
|
591
|
-
onMessage: (envelope) => this.handleMessage(envelope),
|
|
539
|
+
onLog: ctx.log.handler,
|
|
540
|
+
deps: ctx.deps,
|
|
541
|
+
onMessage: (envelope) => this.handleMessage(ctx, envelope),
|
|
592
542
|
onConnected: () => {
|
|
593
|
-
if (this.reconnector && this.reconnector.attempt > 0)
|
|
543
|
+
if (this.reconnector && this.reconnector.attempt > 0) ctx.log.info({
|
|
594
544
|
action: "reconnect.reset",
|
|
595
545
|
message: `cleared ${this.reconnector.attempt} attempts`
|
|
596
546
|
});
|
|
597
547
|
this.reconnector?.reset();
|
|
598
|
-
this.
|
|
548
|
+
this.setStatus("connected");
|
|
599
549
|
},
|
|
600
550
|
onDisconnected: () => {
|
|
601
551
|
if (this.socket?.isStopped) {
|
|
602
|
-
this.
|
|
552
|
+
this.setStatus("disconnected");
|
|
603
553
|
return;
|
|
604
554
|
}
|
|
605
|
-
this.scheduleReconnect();
|
|
555
|
+
this.scheduleReconnect(ctx);
|
|
606
556
|
}
|
|
607
557
|
});
|
|
608
558
|
const error = await this.socket.connect({ signal: this.internalController?.signal });
|
|
609
559
|
if (error instanceof Error) {
|
|
610
|
-
|
|
560
|
+
ctx.log.error({
|
|
611
561
|
action: "connect.failed",
|
|
612
562
|
message: safeErrorMessage({ error }),
|
|
613
563
|
error
|
|
614
564
|
});
|
|
615
565
|
if (this.socket.isStopped || !this.reconnector || this.reconnector.aborted) {
|
|
616
|
-
this.
|
|
566
|
+
this.setStatus("disconnected");
|
|
617
567
|
return error;
|
|
618
568
|
}
|
|
619
|
-
this.scheduleReconnect();
|
|
569
|
+
this.scheduleReconnect(ctx);
|
|
620
570
|
}
|
|
621
571
|
return null;
|
|
622
572
|
}
|
|
623
|
-
handleMessage(envelope) {
|
|
624
|
-
|
|
625
|
-
|
|
573
|
+
handleMessage(ctx, envelope) {
|
|
574
|
+
const seen = this.seen;
|
|
575
|
+
if (!seen) return;
|
|
576
|
+
if (seen.has(envelope.envelope_id)) {
|
|
577
|
+
ctx.log.debug({
|
|
626
578
|
action: "dedup.skip",
|
|
627
579
|
message: `duplicate envelope_id=${envelope.envelope_id}`,
|
|
628
580
|
detail: {
|
|
@@ -632,31 +584,21 @@ var FlumeSlackSource = class {
|
|
|
632
584
|
});
|
|
633
585
|
return;
|
|
634
586
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
data: envelope.payload,
|
|
644
|
-
meta: this.safeExtractMeta(envelope),
|
|
645
|
-
receivedAt: safeNow({ deps: this.deps })
|
|
646
|
-
};
|
|
647
|
-
const r = await attempt(() => Promise.resolve(handler(event)));
|
|
648
|
-
if (r instanceof Error) this.log.error({
|
|
649
|
-
action: "handler.error",
|
|
650
|
-
message: safeErrorMessage({ error: r }),
|
|
651
|
-
error: r
|
|
652
|
-
});
|
|
587
|
+
seen.add(envelope.envelope_id);
|
|
588
|
+
seen.trim();
|
|
589
|
+
this.emit({
|
|
590
|
+
source: "slack",
|
|
591
|
+
type: envelope.type,
|
|
592
|
+
data: envelope.payload,
|
|
593
|
+
meta: this.safeExtractMeta(ctx, envelope),
|
|
594
|
+
receivedAt: safeNow({ deps: ctx.deps })
|
|
653
595
|
});
|
|
654
596
|
}
|
|
655
|
-
safeExtractMeta(envelope) {
|
|
597
|
+
safeExtractMeta(ctx, envelope) {
|
|
656
598
|
const result = attempt(() => flumeExtractSlackMeta(envelope));
|
|
657
599
|
if (result instanceof Error) {
|
|
658
600
|
const error = safeNormalizeError({ value: result });
|
|
659
|
-
|
|
601
|
+
ctx.log.warn({
|
|
660
602
|
action: "meta.extract.error",
|
|
661
603
|
message: safeErrorMessage({ error }),
|
|
662
604
|
error,
|
|
@@ -666,20 +608,20 @@ var FlumeSlackSource = class {
|
|
|
666
608
|
}
|
|
667
609
|
return result;
|
|
668
610
|
}
|
|
669
|
-
scheduleReconnect() {
|
|
611
|
+
scheduleReconnect(ctx) {
|
|
670
612
|
scheduleFlumeReconnect({
|
|
671
613
|
reconnector: this.reconnector,
|
|
672
|
-
log:
|
|
673
|
-
setStatus: (status) => this.
|
|
614
|
+
log: ctx.log,
|
|
615
|
+
setStatus: (status) => this.setStatus(status),
|
|
674
616
|
retry: () => {
|
|
675
|
-
this.connectInternal().catch((err) => {
|
|
617
|
+
this.connectInternal(ctx).catch((err) => {
|
|
676
618
|
const error = safeNormalizeError({ value: err });
|
|
677
|
-
|
|
619
|
+
ctx.log.error({
|
|
678
620
|
action: "reconnect.unhandled",
|
|
679
621
|
message: safeErrorMessage({ error }),
|
|
680
622
|
error
|
|
681
623
|
});
|
|
682
|
-
this.
|
|
624
|
+
this.setStatus("disconnected");
|
|
683
625
|
});
|
|
684
626
|
}
|
|
685
627
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@interactive-inc/flume",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Unified notification listener for Discord, Slack, and GitHub. Raw WebSocket + fetch + Zod. No SDK dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"discord",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"zod": "^4.4.3"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
|
+
"typescript": "^5.6.0",
|
|
74
75
|
"vite-plus": "^0.1.21",
|
|
75
76
|
"vitest": "^4.1.9"
|
|
76
77
|
},
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
//#region lib/utils/safe-error-message.ts
|
|
2
|
-
/**
|
|
3
|
-
* 任意の値から人が読めるメッセージ文字列を取り出す。
|
|
4
|
-
* `Error.message` getter / `Symbol.toPrimitive` / `toString` / `valueOf` が throw しても固定文字列に fallback。
|
|
5
|
-
* 自身は決して throw しない
|
|
6
|
-
*/
|
|
7
|
-
function safeErrorMessage(props) {
|
|
8
|
-
if (props.error instanceof Error) try {
|
|
9
|
-
const message = props.error.message;
|
|
10
|
-
if (typeof message === "string") return message;
|
|
11
|
-
return "<non-string error message>";
|
|
12
|
-
} catch {
|
|
13
|
-
return "<unreadable error message>";
|
|
14
|
-
}
|
|
15
|
-
try {
|
|
16
|
-
return String(props.error);
|
|
17
|
-
} catch {
|
|
18
|
-
return "<unprintable error>";
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
//#endregion
|
|
22
|
-
//#region lib/utils/safe-normalize-error.ts
|
|
23
|
-
/**
|
|
24
|
-
* 任意の値を `Error` インスタンスへ正規化する。すでに Error ならそのまま返し、
|
|
25
|
-
* それ以外は `safeErrorMessage` で安全な文字列化を経由して new Error する。
|
|
26
|
-
* Error コンストラクタ自体が throw する病的環境でも fallback を返し、決して throw しない
|
|
27
|
-
*/
|
|
28
|
-
function safeNormalizeError(props) {
|
|
29
|
-
if (props.value instanceof Error) return props.value;
|
|
30
|
-
const message = safeErrorMessage({ error: props.value });
|
|
31
|
-
try {
|
|
32
|
-
return new Error(message);
|
|
33
|
-
} catch {
|
|
34
|
-
try {
|
|
35
|
-
return /* @__PURE__ */ new Error("unknown error");
|
|
36
|
-
} catch {
|
|
37
|
-
const fallback = Object.create(Error.prototype);
|
|
38
|
-
fallback.name = "Error";
|
|
39
|
-
fallback.message = "unknown error";
|
|
40
|
-
return fallback;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
//#endregion
|
|
45
|
-
//#region lib/utils/attempt.ts
|
|
46
|
-
function attempt(fn) {
|
|
47
|
-
try {
|
|
48
|
-
const result = fn();
|
|
49
|
-
if (result instanceof Promise) return result.catch((err) => safeNormalizeError({ value: err }));
|
|
50
|
-
return result;
|
|
51
|
-
} catch (err) {
|
|
52
|
-
return safeNormalizeError({ value: err });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
//#endregion
|
|
56
|
-
//#region lib/deps.ts
|
|
57
|
-
const wsCandidate = attempt(() => globalThis.WebSocket);
|
|
58
|
-
const cachedWebSocket = wsCandidate instanceof Error || typeof wsCandidate !== "function" ? null : wsCandidate;
|
|
59
|
-
/**
|
|
60
|
-
* platform 既定の IO を束ねた `FlumeRuntimeDeps`。
|
|
61
|
-
* `FlumeTimerHandle` は不透明型 (`unknown`) のため、setTimeout / clearTimeout の戻り値・引数を
|
|
62
|
-
* platform 型と橋渡しする際に境界で `as unknown as` を使う (IO 境界の最終手段)
|
|
63
|
-
*/
|
|
64
|
-
function createFlumeDefaultDeps() {
|
|
65
|
-
return {
|
|
66
|
-
fetch: (url, init) => globalThis.fetch(url, init),
|
|
67
|
-
WebSocket: cachedWebSocket,
|
|
68
|
-
now: () => Date.now(),
|
|
69
|
-
random: () => Math.random(),
|
|
70
|
-
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
71
|
-
clearTimeout: (id) => globalThis.clearTimeout(id),
|
|
72
|
-
setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
|
|
73
|
-
clearInterval: (id) => globalThis.clearInterval(id)
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
//#endregion
|
|
77
|
-
//#region lib/errors/parse-error.ts
|
|
78
|
-
var FlumeParseError = class extends Error {
|
|
79
|
-
constructor(message, options) {
|
|
80
|
-
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
81
|
-
this.name = "FlumeParseError";
|
|
82
|
-
Object.freeze(this);
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
//#endregion
|
|
86
|
-
//#region lib/errors/start-error.ts
|
|
87
|
-
var FlumeStartError = class extends Error {
|
|
88
|
-
constructor(message, options) {
|
|
89
|
-
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
90
|
-
this.name = "FlumeStartError";
|
|
91
|
-
Object.freeze(this);
|
|
92
|
-
}
|
|
93
|
-
};
|
|
94
|
-
//#endregion
|
|
95
|
-
//#region lib/utils/safe-now.ts
|
|
96
|
-
/**
|
|
97
|
-
* `deps.now()` を保護する。throw / 非数値が返った場合は 0 を返す。
|
|
98
|
-
* IO 境界のため呼び出し側はこの戻り値を信頼できる
|
|
99
|
-
*/
|
|
100
|
-
function safeNow(props) {
|
|
101
|
-
try {
|
|
102
|
-
const value = props.deps.now();
|
|
103
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return 0;
|
|
104
|
-
return value;
|
|
105
|
-
} catch {
|
|
106
|
-
return 0;
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
110
|
-
//#region lib/logger.ts
|
|
111
|
-
/**
|
|
112
|
-
* 構造化ログを onLog に流す。handler が throw / reject してもループは継続する
|
|
113
|
-
*/
|
|
114
|
-
var FlumeLogger = class {
|
|
115
|
-
props;
|
|
116
|
-
constructor(props) {
|
|
117
|
-
this.props = props;
|
|
118
|
-
Object.freeze(this);
|
|
119
|
-
}
|
|
120
|
-
debug(entry) {
|
|
121
|
-
this.emit("debug", entry);
|
|
122
|
-
}
|
|
123
|
-
info(entry) {
|
|
124
|
-
this.emit("info", entry);
|
|
125
|
-
}
|
|
126
|
-
warn(entry) {
|
|
127
|
-
this.emit("warn", entry);
|
|
128
|
-
}
|
|
129
|
-
error(entry) {
|
|
130
|
-
this.emit("error", entry);
|
|
131
|
-
}
|
|
132
|
-
emit(level, input) {
|
|
133
|
-
const handler = this.props.handler;
|
|
134
|
-
if (!handler) return;
|
|
135
|
-
const log = {
|
|
136
|
-
level,
|
|
137
|
-
source: this.props.source,
|
|
138
|
-
action: input.action,
|
|
139
|
-
message: input.message,
|
|
140
|
-
timestamp: safeNow({ deps: this.props.deps }),
|
|
141
|
-
error: input.error,
|
|
142
|
-
detail: input.detail
|
|
143
|
-
};
|
|
144
|
-
try {
|
|
145
|
-
Promise.resolve(handler(log)).catch(() => {});
|
|
146
|
-
} catch {}
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
//#endregion
|
|
150
|
-
//#region lib/utils/safe-invoke-callback.ts
|
|
151
|
-
/**
|
|
152
|
-
* fire-and-forget でユーザーコールバックを呼び出す。sync throw と async reject のどちらも
|
|
153
|
-
* `onError(Error)` に正規化して通知。`onError` 自身が throw しても外に漏らさない。
|
|
154
|
-
* 戻り値を持たない fire-and-forget 専用のため log/出力先には依存しない (caller が onError で決める)
|
|
155
|
-
*/
|
|
156
|
-
function safeInvokeCallback(props) {
|
|
157
|
-
try {
|
|
158
|
-
Promise.resolve(props.fn()).catch((err) => {
|
|
159
|
-
try {
|
|
160
|
-
props.onError(safeNormalizeError({ value: err }));
|
|
161
|
-
} catch {}
|
|
162
|
-
}).catch(() => {});
|
|
163
|
-
} catch (err) {
|
|
164
|
-
try {
|
|
165
|
-
props.onError(safeNormalizeError({ value: err }));
|
|
166
|
-
} catch {}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
//#endregion
|
|
170
|
-
export { FlumeParseError as a, safeNormalizeError as c, FlumeStartError as i, safeErrorMessage as l, FlumeLogger as n, createFlumeDefaultDeps as o, safeNow as r, attempt as s, safeInvokeCallback as t };
|
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { a as FlumeParseError, c as safeNormalizeError, l as safeErrorMessage, s as attempt, t as safeInvokeCallback } from "./safe-invoke-callback-EpWXwfwp.js";
|
|
2
|
-
//#region lib/utils/safe-json-parse.ts
|
|
3
|
-
function safeJsonParse(raw) {
|
|
4
|
-
try {
|
|
5
|
-
return JSON.parse(raw);
|
|
6
|
-
} catch (error) {
|
|
7
|
-
return new FlumeParseError(error instanceof Error ? `invalid JSON: ${error.message}` : "invalid JSON", { cause: error });
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
//#endregion
|
|
11
|
-
//#region lib/source-helpers/flume-signal-registry.ts
|
|
12
|
-
/**
|
|
13
|
-
* Source 群に共通する AbortSignal の登録・解除・abort 判定を集約する。
|
|
14
|
-
* 異常な polyfill / poisoned getter / 凍結 signal を吸収するため境界呼び出しは全て try/catch で
|
|
15
|
-
* 包み、失敗時はログに流して握り潰す
|
|
16
|
-
*/
|
|
17
|
-
var FlumeSignalRegistry = class {
|
|
18
|
-
props;
|
|
19
|
-
signals = [];
|
|
20
|
-
constructor(props) {
|
|
21
|
-
this.props = props;
|
|
22
|
-
}
|
|
23
|
-
isAnyAborted(extra) {
|
|
24
|
-
if (this.isAborted(extra)) return true;
|
|
25
|
-
for (const signal of this.signals) if (this.isAborted(signal)) return true;
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
register(signal) {
|
|
29
|
-
if (!signal) return;
|
|
30
|
-
const result = attempt(() => signal.addEventListener("abort", this.props.onAbort, { once: true }));
|
|
31
|
-
if (result instanceof Error) {
|
|
32
|
-
const error = safeNormalizeError({ value: result });
|
|
33
|
-
this.props.log.error({
|
|
34
|
-
action: "signal.addListener.failed",
|
|
35
|
-
message: safeErrorMessage({ error }),
|
|
36
|
-
error
|
|
37
|
-
});
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
this.signals.push(signal);
|
|
41
|
-
}
|
|
42
|
-
unregisterAll() {
|
|
43
|
-
for (const signal of this.signals) {
|
|
44
|
-
const result = attempt(() => signal.removeEventListener("abort", this.props.onAbort));
|
|
45
|
-
if (result instanceof Error) {
|
|
46
|
-
const error = safeNormalizeError({ value: result });
|
|
47
|
-
this.props.log.error({
|
|
48
|
-
action: "signal.removeListener.failed",
|
|
49
|
-
message: safeErrorMessage({ error }),
|
|
50
|
-
error
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
this.signals.length = 0;
|
|
55
|
-
}
|
|
56
|
-
get size() {
|
|
57
|
-
return this.signals.length;
|
|
58
|
-
}
|
|
59
|
-
isAborted(signal) {
|
|
60
|
-
if (!signal) return false;
|
|
61
|
-
const result = attempt(() => signal.aborted === true);
|
|
62
|
-
return result instanceof Error ? true : result;
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
//#endregion
|
|
66
|
-
//#region lib/source-helpers/flume-status-emitter.ts
|
|
67
|
-
/**
|
|
68
|
-
* Source の `currentStatus` と `onStatus` 通知を集約する。
|
|
69
|
-
* 同一 (status, detail) の連続遷移は冪等に握り潰し、ユーザーコールバックは `safeInvokeCallback`
|
|
70
|
-
* 経由で例外を隔離する
|
|
71
|
-
*/
|
|
72
|
-
var FlumeStatusEmitter = class {
|
|
73
|
-
props;
|
|
74
|
-
currentStatus = "disconnected";
|
|
75
|
-
currentDetail = null;
|
|
76
|
-
constructor(props) {
|
|
77
|
-
this.props = props;
|
|
78
|
-
}
|
|
79
|
-
get value() {
|
|
80
|
-
return this.currentStatus;
|
|
81
|
-
}
|
|
82
|
-
set(next, detail) {
|
|
83
|
-
const normalizedDetail = detail ?? null;
|
|
84
|
-
if (this.currentStatus === next && this.currentDetail === normalizedDetail) return;
|
|
85
|
-
const prev = this.currentStatus;
|
|
86
|
-
const suffix = detail ? ` (${detail})` : "";
|
|
87
|
-
this.props.log.info({
|
|
88
|
-
action: "status",
|
|
89
|
-
message: `${prev} → ${next}${suffix}`,
|
|
90
|
-
detail: {
|
|
91
|
-
from: prev,
|
|
92
|
-
to: next,
|
|
93
|
-
reason: normalizedDetail
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
this.currentStatus = next;
|
|
97
|
-
this.currentDetail = normalizedDetail;
|
|
98
|
-
const onStatus = this.props.onStatus;
|
|
99
|
-
if (!onStatus) return;
|
|
100
|
-
safeInvokeCallback({
|
|
101
|
-
fn: detail !== void 0 ? () => onStatus(next, detail) : () => onStatus(next),
|
|
102
|
-
onError: (error) => {
|
|
103
|
-
this.props.log.error({
|
|
104
|
-
action: "onStatus.error",
|
|
105
|
-
message: safeErrorMessage({ error }),
|
|
106
|
-
error
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
};
|
|
112
|
-
//#endregion
|
|
113
|
-
//#region lib/utils/serial-queue.ts
|
|
114
|
-
/**
|
|
115
|
-
* 投入順を保ったまま task を直列実行する。各 task は前の完了を待ってから走る。
|
|
116
|
-
* task が throw しても後続には伝播しない (キュー自体は止まらない)。
|
|
117
|
-
* maxDepth を超えた場合は新規 task を drop し onOverflow に通知。
|
|
118
|
-
* cancel() 後の add() は no-op となり drain() は即時 resolve する
|
|
119
|
-
*/
|
|
120
|
-
var FlumeSerialQueue = class {
|
|
121
|
-
props;
|
|
122
|
-
chain = Promise.resolve();
|
|
123
|
-
depth = 0;
|
|
124
|
-
cancelled = false;
|
|
125
|
-
constructor(props = {}) {
|
|
126
|
-
this.props = props;
|
|
127
|
-
}
|
|
128
|
-
add(task) {
|
|
129
|
-
if (this.cancelled) return;
|
|
130
|
-
if (this.props.maxDepth !== void 0 && this.depth >= this.props.maxDepth) {
|
|
131
|
-
this.props.onOverflow?.({
|
|
132
|
-
dropped: 1,
|
|
133
|
-
depth: this.depth
|
|
134
|
-
});
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
this.depth++;
|
|
138
|
-
this.chain = this.chain.then(async () => {
|
|
139
|
-
try {
|
|
140
|
-
await task();
|
|
141
|
-
} catch {} finally {
|
|
142
|
-
this.depth--;
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
}
|
|
146
|
-
async drain() {
|
|
147
|
-
await this.chain;
|
|
148
|
-
}
|
|
149
|
-
cancel() {
|
|
150
|
-
this.cancelled = true;
|
|
151
|
-
this.depth = 0;
|
|
152
|
-
this.chain = Promise.resolve();
|
|
153
|
-
}
|
|
154
|
-
size() {
|
|
155
|
-
return this.depth;
|
|
156
|
-
}
|
|
157
|
-
isCancelled() {
|
|
158
|
-
return this.cancelled;
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
//#endregion
|
|
162
|
-
export { safeJsonParse as i, FlumeStatusEmitter as n, FlumeSignalRegistry as r, FlumeSerialQueue as t };
|