@interactive-inc/flume 0.3.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 +165 -108
- package/dist/connection-error-HUO3PC3G.js +16 -0
- package/dist/discord.d.ts +10 -93
- package/dist/discord.js +446 -217
- package/dist/flume-source-DUvt9aJt.js +341 -0
- package/dist/flume-source-DuUFPhSe.d.ts +228 -0
- package/dist/github.d.ts +7 -54
- package/dist/github.js +354 -158
- package/dist/{http-error-BtXonO-W.js → http-error-CPSKoSie.js} +1 -1
- package/dist/index.d.ts +79 -49
- package/dist/index.js +304 -62
- package/dist/safe-json-parse-CfJjt-RY.js +11 -0
- package/dist/safe-read-text-JQd_5vbd.js +20 -0
- package/dist/safe-stringify-DbWQw9qe.js +157 -0
- package/dist/slack.d.ts +10 -70
- package/dist/slack.js +352 -173
- package/package.json +22 -18
- package/dist/connection-error-BOk97djj.d.ts +0 -6
- package/dist/http-error-K-Ym4lfK.d.ts +0 -11
- package/dist/logger-CpGB9WO_.js +0 -49
- package/dist/parse-error-BAiCLRmk.d.ts +0 -6
- package/dist/reconnector-BDoJ1xNX.js +0 -67
- package/dist/safe-fetch-30ZzOKHL.js +0 -20
- package/dist/safe-json-parse-BWlzGOLl.js +0 -41
- package/dist/serial-queue-ExmlnpzQ.js +0 -16
- package/dist/types-Bm9uKUQz.d.ts +0 -143
package/dist/index.d.ts
CHANGED
|
@@ -1,51 +1,56 @@
|
|
|
1
|
-
import { C as
|
|
2
|
-
import { t as FlumeConnectionError } from "./connection-error-BOk97djj.js";
|
|
3
|
-
import { t as FlumeParseError } from "./parse-error-BAiCLRmk.js";
|
|
4
|
-
import { t as FlumeHttpError } from "./http-error-K-Ym4lfK.js";
|
|
1
|
+
import { C as FlumeSourceStartContext, D as FlumeStatusHandler, E as FlumeStatusEvent, O as FlumeTimerHandle, S as FlumeSourceName, T as FlumeStatus, _ as FlumeSlackConnectionResponse, a as FlumeEventHandler, b as FlumeSlackSourceOptions, c as FlumeGitHubNotification, d as FlumeLogHandler, f as FlumeLogInput, g as FlumeRuntimeDeps, h as FlumeReconnectOptions, i as FlumeEvent, k as FlumeLogger, l as FlumeGitHubSourceOptions, m as FlumeReconnectConfig, n as FlumeDiscordEvent, o as FlumeGatewayMessage, p as FlumeLogLevel, r as FlumeDiscordSourceOptions, s as FlumeGitHubEvent, t as FlumeSource, u as FlumeLog, v as FlumeSlackEnvelope, w as FlumeSourceStatus, x as FlumeSourceLocalStatusHandler, y as FlumeSlackEvent } from "./flume-source-DuUFPhSe.js";
|
|
5
2
|
|
|
6
3
|
//#region lib/deps.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* platform 既定の IO を束ねた `FlumeRuntimeDeps`。
|
|
6
|
+
* `FlumeTimerHandle` は不透明型 (`unknown`) のため、setTimeout / clearTimeout の戻り値・引数を
|
|
7
|
+
* platform 型と橋渡しする際に境界で `as unknown as` を使う (IO 境界の最終手段)
|
|
8
|
+
*/
|
|
7
9
|
declare function createFlumeDefaultDeps(): FlumeRuntimeDeps;
|
|
8
10
|
//#endregion
|
|
9
|
-
//#region lib/
|
|
10
|
-
type
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
deps: Pick<FlumeRuntimeDeps, "now">;
|
|
11
|
+
//#region lib/errors/connection-error.d.ts
|
|
12
|
+
type Options$3 = {
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
code?: number;
|
|
14
15
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
private emit;
|
|
16
|
+
/**
|
|
17
|
+
* 接続失敗を表す。Discord Gateway / Slack Socket Mode / その他 WebSocket 系の close で発生。
|
|
18
|
+
* `code` は接続が落ちた際の close code (Discord は 4xxx 帯が再接続可否を示す)
|
|
19
|
+
*/
|
|
20
|
+
declare class FlumeConnectionError extends Error {
|
|
21
|
+
readonly code: number | null;
|
|
22
|
+
constructor(message: string, options?: Options$3);
|
|
23
23
|
}
|
|
24
24
|
//#endregion
|
|
25
|
-
//#region lib/
|
|
26
|
-
|
|
25
|
+
//#region lib/errors/http-error.d.ts
|
|
26
|
+
type Props$2 = {
|
|
27
|
+
message: string;
|
|
28
|
+
status: number;
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
};
|
|
31
|
+
declare class FlumeHttpError extends Error {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
constructor(props: Props$2);
|
|
34
|
+
}
|
|
27
35
|
//#endregion
|
|
28
|
-
//#region lib/
|
|
29
|
-
type
|
|
30
|
-
|
|
31
|
-
baseDelay: number;
|
|
32
|
-
maxDelay: number;
|
|
33
|
-
deps: Pick<FlumeRuntimeDeps, "setTimeout" | "clearTimeout" | "random">;
|
|
36
|
+
//#region lib/errors/parse-error.d.ts
|
|
37
|
+
type Options$2 = {
|
|
38
|
+
cause?: unknown;
|
|
34
39
|
};
|
|
35
|
-
declare class
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
declare class FlumeParseError extends Error {
|
|
41
|
+
constructor(message: string, options?: Options$2);
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region lib/errors/start-error.d.ts
|
|
45
|
+
type Options$1 = {
|
|
46
|
+
cause?: unknown;
|
|
47
|
+
};
|
|
48
|
+
declare class FlumeStartError extends Error {
|
|
49
|
+
constructor(message: string, options?: Options$1);
|
|
45
50
|
}
|
|
46
51
|
//#endregion
|
|
47
52
|
//#region lib/flume-stopped.d.ts
|
|
48
|
-
type Props$
|
|
53
|
+
type Props$1 = {
|
|
49
54
|
finalStatuses: ReadonlyArray<FlumeSourceStatus>;
|
|
50
55
|
};
|
|
51
56
|
/**
|
|
@@ -53,43 +58,68 @@ type Props$2 = {
|
|
|
53
58
|
*/
|
|
54
59
|
declare class FlumeStopped {
|
|
55
60
|
private readonly props;
|
|
56
|
-
|
|
61
|
+
readonly kind: "stopped";
|
|
62
|
+
constructor(props: Props$1);
|
|
57
63
|
statuses(): ReadonlyArray<FlumeSourceStatus>;
|
|
58
64
|
}
|
|
59
65
|
//#endregion
|
|
60
66
|
//#region lib/flume-running.d.ts
|
|
61
|
-
type Props
|
|
67
|
+
type Props = {
|
|
62
68
|
sources: ReadonlyArray<FlumeSource>;
|
|
63
69
|
signal?: AbortSignal;
|
|
70
|
+
log: FlumeLogger;
|
|
64
71
|
};
|
|
65
72
|
/**
|
|
66
|
-
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop
|
|
73
|
+
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop。
|
|
74
|
+
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
75
|
+
* `runStop` の最外殻 try/catch で想定外の throw も `FlumeStopped` の resolve に変換する
|
|
67
76
|
*/
|
|
68
77
|
declare class FlumeRunning {
|
|
69
78
|
private readonly props;
|
|
79
|
+
readonly kind: "running";
|
|
70
80
|
private stopPromise;
|
|
71
81
|
private readonly onAbort;
|
|
72
|
-
constructor(props: Props
|
|
82
|
+
constructor(props: Props);
|
|
73
83
|
stop(): Promise<FlumeStopped>;
|
|
74
84
|
statuses(): ReadonlyArray<FlumeSourceStatus>;
|
|
75
85
|
private runStop;
|
|
86
|
+
private snapshotStatuses;
|
|
87
|
+
private sourceName;
|
|
76
88
|
}
|
|
77
89
|
//#endregion
|
|
78
90
|
//#region lib/flume.d.ts
|
|
79
|
-
type
|
|
80
|
-
|
|
91
|
+
type Options = {
|
|
92
|
+
onEvent?: FlumeEventHandler;
|
|
81
93
|
signal?: AbortSignal;
|
|
94
|
+
onLog?: FlumeLogHandler;
|
|
95
|
+
onStatus?: FlumeStatusHandler;
|
|
96
|
+
deps?: FlumeRuntimeDeps;
|
|
97
|
+
reconnect?: FlumeReconnectOptions;
|
|
82
98
|
};
|
|
83
99
|
/**
|
|
84
|
-
* 起動前の Flume
|
|
100
|
+
* 起動前の Flume。`start()` で `FlumeRunning` へ遷移する。
|
|
101
|
+
* 第一引数は sources、第二引数は cross-cutting options (全て optional)。
|
|
102
|
+
* `onEvent` を省略するとイベントは黙って捨てられる (接続観測専用モード)。
|
|
103
|
+
* いずれかの source 失敗時は既に成功した source を全て `stop()` してロールバックし
|
|
104
|
+
* `FlumeStartError` を返す。
|
|
105
|
+
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
106
|
+
* Promise rejection に正規化して `allSettled` で捕捉する (`start()` は決して reject しない)
|
|
85
107
|
*/
|
|
86
108
|
declare class Flume {
|
|
87
|
-
private readonly
|
|
109
|
+
private readonly sources;
|
|
110
|
+
private readonly options;
|
|
88
111
|
private consumed;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
112
|
+
private readonly log;
|
|
113
|
+
private readonly deps;
|
|
114
|
+
private readonly onEvent;
|
|
115
|
+
constructor(sources: ReadonlyArray<FlumeSource>, options?: Options);
|
|
116
|
+
start(): Promise<FlumeRunning | FlumeStartError>;
|
|
117
|
+
private guardStart;
|
|
118
|
+
private isSignalAborted;
|
|
119
|
+
private sourceName;
|
|
120
|
+
private safeStart;
|
|
121
|
+
private notifyStatus;
|
|
122
|
+
private rollback;
|
|
93
123
|
}
|
|
94
124
|
//#endregion
|
|
95
|
-
export { Flume, FlumeConnectionError, type FlumeDiscordSourceOptions, type FlumeEvent, type FlumeGatewayMessage, type
|
|
125
|
+
export { Flume, FlumeConnectionError, type FlumeDiscordEvent, type FlumeDiscordSourceOptions, type FlumeEvent, type FlumeEventHandler, type FlumeGatewayMessage, type FlumeGitHubEvent, type FlumeGitHubNotification, type FlumeGitHubSourceOptions, FlumeHttpError, type FlumeLog, type FlumeLogHandler, type FlumeLogInput, type FlumeLogLevel, FlumeParseError, type FlumeReconnectConfig, type FlumeReconnectOptions, FlumeRunning, type FlumeRuntimeDeps, type FlumeSlackConnectionResponse, type FlumeSlackEnvelope, type FlumeSlackEvent, type FlumeSlackSourceOptions, FlumeSource, type FlumeSourceLocalStatusHandler, type FlumeSourceName, type FlumeSourceStartContext, type FlumeSourceStatus, FlumeStartError, type FlumeStatus, type FlumeStatusEvent, type FlumeStatusHandler, FlumeStopped, type FlumeTimerHandle, createFlumeDefaultDeps };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,34 @@
|
|
|
1
|
-
import { n as
|
|
2
|
-
import {
|
|
3
|
-
import { t as FlumeHttpError } from "./http-error-
|
|
1
|
+
import { a as FlumeStartError, c as safeNormalizeError, l as safeErrorMessage, n as safeInvokeCallback, o as FlumeParseError, r as FlumeLogger, s as attempt, t as FlumeSource } from "./flume-source-DUvt9aJt.js";
|
|
2
|
+
import { t as FlumeConnectionError } from "./connection-error-HUO3PC3G.js";
|
|
3
|
+
import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
|
|
4
|
+
//#region lib/deps.ts
|
|
5
|
+
const wsCandidate = attempt(() => globalThis.WebSocket);
|
|
6
|
+
const cachedWebSocket = wsCandidate instanceof Error || typeof wsCandidate !== "function" ? null : wsCandidate;
|
|
7
|
+
/**
|
|
8
|
+
* platform 既定の IO を束ねた `FlumeRuntimeDeps`。
|
|
9
|
+
* `FlumeTimerHandle` は不透明型 (`unknown`) のため、setTimeout / clearTimeout の戻り値・引数を
|
|
10
|
+
* platform 型と橋渡しする際に境界で `as unknown as` を使う (IO 境界の最終手段)
|
|
11
|
+
*/
|
|
12
|
+
function createFlumeDefaultDeps() {
|
|
13
|
+
return {
|
|
14
|
+
fetch: (url, init) => globalThis.fetch(url, init),
|
|
15
|
+
WebSocket: cachedWebSocket,
|
|
16
|
+
now: () => Date.now(),
|
|
17
|
+
random: () => Math.random(),
|
|
18
|
+
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
19
|
+
clearTimeout: (id) => globalThis.clearTimeout(id),
|
|
20
|
+
setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
|
|
21
|
+
clearInterval: (id) => globalThis.clearInterval(id)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
4
25
|
//#region lib/flume-stopped.ts
|
|
5
26
|
/**
|
|
6
27
|
* 停止済みの終端状態。最終ステータスのスナップショットのみ観測できる
|
|
7
28
|
*/
|
|
8
29
|
var FlumeStopped = class {
|
|
9
30
|
props;
|
|
31
|
+
kind = "stopped";
|
|
10
32
|
constructor(props) {
|
|
11
33
|
this.props = props;
|
|
12
34
|
Object.freeze(this);
|
|
@@ -18,18 +40,45 @@ var FlumeStopped = class {
|
|
|
18
40
|
//#endregion
|
|
19
41
|
//#region lib/flume-running.ts
|
|
20
42
|
/**
|
|
21
|
-
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop
|
|
43
|
+
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop。
|
|
44
|
+
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
45
|
+
* `runStop` の最外殻 try/catch で想定外の throw も `FlumeStopped` の resolve に変換する
|
|
22
46
|
*/
|
|
23
47
|
var FlumeRunning = class {
|
|
24
48
|
props;
|
|
49
|
+
kind = "running";
|
|
25
50
|
stopPromise = null;
|
|
26
51
|
onAbort;
|
|
27
52
|
constructor(props) {
|
|
28
53
|
this.props = props;
|
|
29
54
|
this.onAbort = () => {
|
|
30
|
-
this.
|
|
55
|
+
this.props.log.info({
|
|
56
|
+
action: "flume.abort",
|
|
57
|
+
message: "signal aborted, stopping"
|
|
58
|
+
});
|
|
59
|
+
safeInvokeCallback({
|
|
60
|
+
fn: () => this.stop(),
|
|
61
|
+
onError: (error) => {
|
|
62
|
+
this.props.log.error({
|
|
63
|
+
action: "flume.abort.stop.failed",
|
|
64
|
+
message: safeErrorMessage({ error }),
|
|
65
|
+
error
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
});
|
|
31
69
|
};
|
|
32
|
-
|
|
70
|
+
const signal = props.signal;
|
|
71
|
+
if (signal) {
|
|
72
|
+
const result = attempt(() => signal.addEventListener("abort", this.onAbort, { once: true }));
|
|
73
|
+
if (result instanceof Error) {
|
|
74
|
+
const error = safeNormalizeError({ value: result });
|
|
75
|
+
props.log.error({
|
|
76
|
+
action: "signal.addListener.failed",
|
|
77
|
+
message: safeErrorMessage({ error }),
|
|
78
|
+
error
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
33
82
|
}
|
|
34
83
|
stop() {
|
|
35
84
|
if (this.stopPromise) return this.stopPromise;
|
|
@@ -37,85 +86,278 @@ var FlumeRunning = class {
|
|
|
37
86
|
return this.stopPromise;
|
|
38
87
|
}
|
|
39
88
|
statuses() {
|
|
40
|
-
return this.
|
|
41
|
-
name: source.name,
|
|
42
|
-
status: source.status()
|
|
43
|
-
}));
|
|
89
|
+
return this.snapshotStatuses();
|
|
44
90
|
}
|
|
45
91
|
async runStop() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
92
|
+
try {
|
|
93
|
+
this.props.log.info({
|
|
94
|
+
action: "flume.stop",
|
|
95
|
+
message: `stopping ${this.props.sources.length} source(s)`
|
|
96
|
+
});
|
|
97
|
+
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
98
|
+
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
99
|
+
const source = this.props.sources[index];
|
|
100
|
+
const name = source ? this.sourceName(source) : "?";
|
|
101
|
+
const error = safeNormalizeError({ value: result.reason });
|
|
102
|
+
this.props.log.error({
|
|
103
|
+
action: "flume.stop.failed",
|
|
104
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
105
|
+
error,
|
|
106
|
+
detail: { source: name }
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
const signal = this.props.signal;
|
|
110
|
+
if (signal) {
|
|
111
|
+
const result = attempt(() => signal.removeEventListener("abort", this.onAbort));
|
|
112
|
+
if (result instanceof Error) {
|
|
113
|
+
const error = safeNormalizeError({ value: result });
|
|
114
|
+
this.props.log.error({
|
|
115
|
+
action: "signal.removeListener.failed",
|
|
116
|
+
message: safeErrorMessage({ error }),
|
|
117
|
+
error
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
this.props.log.info({
|
|
122
|
+
action: "flume.stop.complete",
|
|
123
|
+
message: "all sources stopped"
|
|
124
|
+
});
|
|
125
|
+
return new FlumeStopped({ finalStatuses: this.snapshotStatuses() });
|
|
126
|
+
} catch (err) {
|
|
127
|
+
const error = safeNormalizeError({ value: err });
|
|
128
|
+
this.props.log.error({
|
|
129
|
+
action: "flume.stop.unhandled",
|
|
130
|
+
message: safeErrorMessage({ error }),
|
|
131
|
+
error
|
|
132
|
+
});
|
|
133
|
+
return new FlumeStopped({ finalStatuses: this.snapshotStatuses() });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
snapshotStatuses() {
|
|
137
|
+
return this.props.sources.map((source) => {
|
|
138
|
+
const name = this.sourceName(source);
|
|
139
|
+
const status = attempt(() => source.status());
|
|
140
|
+
if (status instanceof Error) {
|
|
141
|
+
const error = safeNormalizeError({ value: status });
|
|
142
|
+
this.props.log.error({
|
|
143
|
+
action: "source.status.failed",
|
|
144
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
145
|
+
error,
|
|
146
|
+
detail: { source: name }
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
source: name,
|
|
150
|
+
status: "disconnected"
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
source: name,
|
|
155
|
+
status
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
sourceName(source) {
|
|
160
|
+
const result = attempt(() => source.name);
|
|
161
|
+
if (result instanceof Error) return "?";
|
|
162
|
+
if (typeof result !== "string") return "?";
|
|
163
|
+
return result;
|
|
52
164
|
}
|
|
53
165
|
};
|
|
54
166
|
//#endregion
|
|
167
|
+
//#region lib/reconnect-config.ts
|
|
168
|
+
const DEFAULTS = {
|
|
169
|
+
maxAttempts: Infinity,
|
|
170
|
+
baseDelay: 1e3,
|
|
171
|
+
maxDelay: 3e4
|
|
172
|
+
};
|
|
173
|
+
function resolveFlumeReconnectConfig(input) {
|
|
174
|
+
if (input === false || input === void 0) return null;
|
|
175
|
+
if (input === true) return { ...DEFAULTS };
|
|
176
|
+
return {
|
|
177
|
+
...DEFAULTS,
|
|
178
|
+
...input
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
55
182
|
//#region lib/flume.ts
|
|
183
|
+
const noopOnEvent = () => {};
|
|
56
184
|
/**
|
|
57
|
-
* 起動前の Flume
|
|
185
|
+
* 起動前の Flume。`start()` で `FlumeRunning` へ遷移する。
|
|
186
|
+
* 第一引数は sources、第二引数は cross-cutting options (全て optional)。
|
|
187
|
+
* `onEvent` を省略するとイベントは黙って捨てられる (接続観測専用モード)。
|
|
188
|
+
* いずれかの source 失敗時は既に成功した source を全て `stop()` してロールバックし
|
|
189
|
+
* `FlumeStartError` を返す。
|
|
190
|
+
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
191
|
+
* Promise rejection に正規化して `allSettled` で捕捉する (`start()` は決して reject しない)
|
|
58
192
|
*/
|
|
59
193
|
var Flume = class {
|
|
60
|
-
|
|
194
|
+
sources;
|
|
195
|
+
options;
|
|
61
196
|
consumed = false;
|
|
62
|
-
|
|
63
|
-
|
|
197
|
+
log;
|
|
198
|
+
deps;
|
|
199
|
+
onEvent;
|
|
200
|
+
constructor(sources, options = {}) {
|
|
201
|
+
this.sources = sources;
|
|
202
|
+
this.options = options;
|
|
203
|
+
this.deps = options.deps ?? createFlumeDefaultDeps();
|
|
204
|
+
this.log = new FlumeLogger({
|
|
205
|
+
source: "flume",
|
|
206
|
+
handler: options.onLog,
|
|
207
|
+
deps: this.deps
|
|
208
|
+
});
|
|
209
|
+
this.onEvent = options.onEvent ?? noopOnEvent;
|
|
64
210
|
}
|
|
65
|
-
async start(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
error: /* @__PURE__ */ new Error("Flume.start: already started")
|
|
69
|
-
};
|
|
70
|
-
if (this.props.signal?.aborted) return {
|
|
71
|
-
ok: false,
|
|
72
|
-
error: /* @__PURE__ */ new Error("Flume.start: signal already aborted")
|
|
73
|
-
};
|
|
211
|
+
async start() {
|
|
212
|
+
const guard = this.guardStart();
|
|
213
|
+
if (guard) return guard;
|
|
74
214
|
this.consumed = true;
|
|
75
|
-
|
|
215
|
+
this.log.info({
|
|
216
|
+
action: "flume.start",
|
|
217
|
+
message: `starting ${this.sources.length} source(s)`,
|
|
218
|
+
detail: { count: this.sources.length }
|
|
219
|
+
});
|
|
220
|
+
const reconnect = resolveFlumeReconnectConfig(this.options.reconnect);
|
|
221
|
+
const settled = await Promise.allSettled(this.sources.map((source) => this.safeStart(source, reconnect)));
|
|
76
222
|
const failures = [];
|
|
77
223
|
const started = [];
|
|
78
|
-
for (
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
224
|
+
for (const [index, result] of settled.entries()) {
|
|
225
|
+
const source = this.sources[index];
|
|
226
|
+
if (source === void 0) continue;
|
|
227
|
+
const name = this.sourceName(source);
|
|
82
228
|
if (result.status === "rejected") {
|
|
83
|
-
const reason = result.reason;
|
|
84
229
|
failures.push({
|
|
85
|
-
name
|
|
86
|
-
error:
|
|
230
|
+
name,
|
|
231
|
+
error: safeNormalizeError({ value: result.reason })
|
|
87
232
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
if (result.value instanceof Error) {
|
|
236
|
+
failures.push({
|
|
237
|
+
name,
|
|
238
|
+
error: result.value
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
started.push(source);
|
|
93
243
|
}
|
|
94
244
|
if (failures.length > 0) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
245
|
+
for (const failure of failures) this.log.error({
|
|
246
|
+
action: "flume.source.failed",
|
|
247
|
+
message: `${failure.name}: ${safeErrorMessage({ error: failure.error })}`,
|
|
248
|
+
error: failure.error,
|
|
249
|
+
detail: { source: failure.name }
|
|
250
|
+
});
|
|
251
|
+
await this.rollback(started);
|
|
252
|
+
const detail = failures.map((f) => `${f.name}: ${safeErrorMessage({ error: f.error })}`).join("; ");
|
|
253
|
+
const error = new FlumeStartError(`Flume.start: ${failures.length} source(s) failed: ${detail}`);
|
|
254
|
+
this.log.error({
|
|
255
|
+
action: "flume.start.failed",
|
|
256
|
+
message: safeErrorMessage({ error }),
|
|
257
|
+
error
|
|
258
|
+
});
|
|
259
|
+
return error;
|
|
101
260
|
}
|
|
102
|
-
if (this.
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
261
|
+
if (this.isSignalAborted()) {
|
|
262
|
+
await this.rollback(this.sources);
|
|
263
|
+
const error = new FlumeStartError("Flume.start: aborted during start");
|
|
264
|
+
this.log.warn({
|
|
265
|
+
action: "flume.start.aborted",
|
|
266
|
+
message: safeErrorMessage({ error }),
|
|
267
|
+
error
|
|
268
|
+
});
|
|
269
|
+
return error;
|
|
270
|
+
}
|
|
271
|
+
this.log.info({
|
|
272
|
+
action: "flume.start.complete",
|
|
273
|
+
message: "all sources started"
|
|
274
|
+
});
|
|
275
|
+
return new FlumeRunning({
|
|
276
|
+
sources: this.sources,
|
|
277
|
+
signal: this.options.signal,
|
|
278
|
+
log: this.log
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
guardStart() {
|
|
282
|
+
if (this.consumed) {
|
|
283
|
+
const error = new FlumeStartError("Flume.start: already started");
|
|
284
|
+
this.log.warn({
|
|
285
|
+
action: "flume.start.refused",
|
|
286
|
+
message: safeErrorMessage({ error }),
|
|
287
|
+
error
|
|
288
|
+
});
|
|
289
|
+
return error;
|
|
290
|
+
}
|
|
291
|
+
if (this.isSignalAborted()) {
|
|
292
|
+
const error = new FlumeStartError("Flume.start: signal already aborted");
|
|
293
|
+
this.log.warn({
|
|
294
|
+
action: "flume.start.refused",
|
|
295
|
+
message: safeErrorMessage({ error }),
|
|
296
|
+
error
|
|
297
|
+
});
|
|
298
|
+
return error;
|
|
108
299
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
isSignalAborted() {
|
|
303
|
+
const signal = this.options.signal;
|
|
304
|
+
if (!signal) return false;
|
|
305
|
+
const result = attempt(() => signal.aborted === true);
|
|
306
|
+
return result instanceof Error ? true : result;
|
|
307
|
+
}
|
|
308
|
+
sourceName(source) {
|
|
309
|
+
const result = attempt(() => source.name);
|
|
310
|
+
if (result instanceof Error) return "?";
|
|
311
|
+
if (typeof result !== "string") return "?";
|
|
312
|
+
return result;
|
|
313
|
+
}
|
|
314
|
+
safeStart(source, reconnect) {
|
|
315
|
+
const name = this.sourceName(source);
|
|
316
|
+
const ctx = {
|
|
317
|
+
onEvent: this.onEvent,
|
|
318
|
+
log: this.log.child(name),
|
|
319
|
+
deps: this.deps,
|
|
320
|
+
onStatus: (status, detail) => this.notifyStatus(name, status, detail),
|
|
321
|
+
reconnect
|
|
322
|
+
};
|
|
323
|
+
return Promise.resolve().then(() => source.start(ctx));
|
|
324
|
+
}
|
|
325
|
+
notifyStatus(name, status, detail) {
|
|
326
|
+
const handler = this.options.onStatus;
|
|
327
|
+
if (!handler) return;
|
|
328
|
+
const event = detail !== void 0 ? {
|
|
329
|
+
source: name,
|
|
330
|
+
status,
|
|
331
|
+
detail
|
|
332
|
+
} : {
|
|
333
|
+
source: name,
|
|
334
|
+
status
|
|
335
|
+
};
|
|
336
|
+
safeInvokeCallback({
|
|
337
|
+
fn: () => handler(event),
|
|
338
|
+
onError: (error) => {
|
|
339
|
+
this.log.error({
|
|
340
|
+
action: "onStatus.error",
|
|
341
|
+
message: safeErrorMessage({ error }),
|
|
342
|
+
error
|
|
343
|
+
});
|
|
344
|
+
}
|
|
112
345
|
});
|
|
113
|
-
return { ok: true };
|
|
114
346
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
347
|
+
async rollback(sources) {
|
|
348
|
+
const settled = await Promise.allSettled(sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
349
|
+
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
350
|
+
const source = sources[index];
|
|
351
|
+
const name = source ? this.sourceName(source) : "?";
|
|
352
|
+
const error = safeNormalizeError({ value: result.reason });
|
|
353
|
+
this.log.error({
|
|
354
|
+
action: "flume.rollback.failed",
|
|
355
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
356
|
+
error,
|
|
357
|
+
detail: { source: name }
|
|
358
|
+
});
|
|
359
|
+
}
|
|
118
360
|
}
|
|
119
361
|
};
|
|
120
362
|
//#endregion
|
|
121
|
-
export { Flume, FlumeConnectionError, FlumeHttpError,
|
|
363
|
+
export { Flume, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeSource, FlumeStartError, FlumeStopped, createFlumeDefaultDeps };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { o as FlumeParseError } from "./flume-source-DUvt9aJt.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
|
+
export { safeJsonParse as t };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { l as safeErrorMessage } from "./flume-source-DUvt9aJt.js";
|
|
2
|
+
import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
|
|
3
|
+
//#region lib/utils/safe-read-text.ts
|
|
4
|
+
/**
|
|
5
|
+
* `response.text()` を保護する。body 読み取り中の reject (接続切断 / 解凍失敗 / 二重消費) を
|
|
6
|
+
* `FlumeHttpError` (status / cause 保持) に変換する。log には書かない (呼び出し側で書く)
|
|
7
|
+
*/
|
|
8
|
+
async function safeReadText(props) {
|
|
9
|
+
try {
|
|
10
|
+
return await props.response.text();
|
|
11
|
+
} catch (err) {
|
|
12
|
+
return new FlumeHttpError({
|
|
13
|
+
message: `${props.context}: failed to read body: ${safeErrorMessage({ error: err })}`,
|
|
14
|
+
status: props.response.status,
|
|
15
|
+
cause: err
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { safeReadText as t };
|