@interactive-inc/flume 0.3.0 → 0.4.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 +67 -53
- package/dist/connection-error-HUO3PC3G.js +16 -0
- package/dist/discord.d.ts +10 -85
- package/dist/discord.js +473 -183
- package/dist/github.d.ts +9 -48
- package/dist/github.js +391 -135
- package/dist/{http-error-BtXonO-W.js → http-error-CPSKoSie.js} +1 -1
- package/dist/index.d.ts +76 -34
- package/dist/index.js +225 -57
- package/dist/safe-invoke-callback-EpWXwfwp.js +170 -0
- package/dist/safe-read-text-DgrJ4Uhl.js +20 -0
- package/dist/safe-stringify-BWS-uXZP.js +172 -0
- package/dist/serial-queue-B9LoBc64.js +162 -0
- package/dist/slack.d.ts +10 -62
- package/dist/slack.js +376 -139
- package/dist/{types-Bm9uKUQz.d.ts → types-D-tO-Mh2.d.ts} +32 -21
- package/package.json +21 -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/index.d.ts
CHANGED
|
@@ -1,20 +1,66 @@
|
|
|
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 FlumeSourceStartOptions, D as FlumeTimerHandle, E as FlumeStatusHandler, S as FlumeSourceOptions, T as FlumeStatus, _ as FlumeSlackEnvelope, a as FlumeGitHubEvent, b as FlumeSource, c as FlumeHandler, d as FlumeLogInput, f as FlumeLogLevel, g as FlumeSlackConnectionResponse, h as FlumeRuntimeDeps, i as FlumeGatewayMessage, l as FlumeLog, m as FlumeReconnectOptions, n as FlumeDiscordSourceOptions, o as FlumeGitHubNotification, p as FlumeReconnectConfig, r as FlumeEvent, s as FlumeGitHubSourceOptions, t as FlumeDiscordEvent, u as FlumeLogHandler, v as FlumeSlackEvent, w as FlumeSourceStatus, x as FlumeSourceName, y as FlumeSlackSourceOptions } from "./types-D-tO-Mh2.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/
|
|
11
|
+
//#region lib/errors/connection-error.d.ts
|
|
12
|
+
type Options$2 = {
|
|
13
|
+
cause?: unknown;
|
|
14
|
+
code?: number;
|
|
15
|
+
};
|
|
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$2);
|
|
23
|
+
}
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region lib/errors/http-error.d.ts
|
|
10
26
|
type Props$4 = {
|
|
27
|
+
message: string;
|
|
28
|
+
status: number;
|
|
29
|
+
cause?: unknown;
|
|
30
|
+
};
|
|
31
|
+
declare class FlumeHttpError extends Error {
|
|
32
|
+
readonly status: number;
|
|
33
|
+
constructor(props: Props$4);
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region lib/errors/parse-error.d.ts
|
|
37
|
+
type Options$1 = {
|
|
38
|
+
cause?: unknown;
|
|
39
|
+
};
|
|
40
|
+
declare class FlumeParseError extends Error {
|
|
41
|
+
constructor(message: string, options?: Options$1);
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region lib/errors/start-error.d.ts
|
|
45
|
+
type Options = {
|
|
46
|
+
cause?: unknown;
|
|
47
|
+
};
|
|
48
|
+
declare class FlumeStartError extends Error {
|
|
49
|
+
constructor(message: string, options?: Options);
|
|
50
|
+
}
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region lib/logger.d.ts
|
|
53
|
+
type Props$3 = {
|
|
11
54
|
source: string;
|
|
12
55
|
handler?: FlumeLogHandler;
|
|
13
56
|
deps: Pick<FlumeRuntimeDeps, "now">;
|
|
14
57
|
};
|
|
58
|
+
/**
|
|
59
|
+
* 構造化ログを onLog に流す。handler が throw / reject してもループは継続する
|
|
60
|
+
*/
|
|
15
61
|
declare class FlumeLogger {
|
|
16
62
|
private readonly props;
|
|
17
|
-
constructor(props: Props$
|
|
63
|
+
constructor(props: Props$3);
|
|
18
64
|
debug(entry: FlumeLogInput): void;
|
|
19
65
|
info(entry: FlumeLogInput): void;
|
|
20
66
|
warn(entry: FlumeLogInput): void;
|
|
@@ -22,28 +68,6 @@ declare class FlumeLogger {
|
|
|
22
68
|
private emit;
|
|
23
69
|
}
|
|
24
70
|
//#endregion
|
|
25
|
-
//#region lib/reconnect-config.d.ts
|
|
26
|
-
declare function resolveFlumeReconnectConfig(input: boolean | FlumeReconnectOptions | undefined): FlumeReconnectConfig | null;
|
|
27
|
-
//#endregion
|
|
28
|
-
//#region lib/reconnector.d.ts
|
|
29
|
-
type Props$3 = {
|
|
30
|
-
maxAttempts: number;
|
|
31
|
-
baseDelay: number;
|
|
32
|
-
maxDelay: number;
|
|
33
|
-
deps: Pick<FlumeRuntimeDeps, "setTimeout" | "clearTimeout" | "random">;
|
|
34
|
-
};
|
|
35
|
-
declare class FlumeReconnector {
|
|
36
|
-
private readonly props;
|
|
37
|
-
attempt: number;
|
|
38
|
-
aborted: boolean;
|
|
39
|
-
private timer;
|
|
40
|
-
constructor(props: Props$3);
|
|
41
|
-
schedule(fn: () => void): number;
|
|
42
|
-
reset(): void;
|
|
43
|
-
cancel(): void;
|
|
44
|
-
private nextDelay;
|
|
45
|
-
}
|
|
46
|
-
//#endregion
|
|
47
71
|
//#region lib/flume-stopped.d.ts
|
|
48
72
|
type Props$2 = {
|
|
49
73
|
finalStatuses: ReadonlyArray<FlumeSourceStatus>;
|
|
@@ -53,6 +77,7 @@ type Props$2 = {
|
|
|
53
77
|
*/
|
|
54
78
|
declare class FlumeStopped {
|
|
55
79
|
private readonly props;
|
|
80
|
+
readonly kind: "stopped";
|
|
56
81
|
constructor(props: Props$2);
|
|
57
82
|
statuses(): ReadonlyArray<FlumeSourceStatus>;
|
|
58
83
|
}
|
|
@@ -61,35 +86,52 @@ declare class FlumeStopped {
|
|
|
61
86
|
type Props$1 = {
|
|
62
87
|
sources: ReadonlyArray<FlumeSource>;
|
|
63
88
|
signal?: AbortSignal;
|
|
89
|
+
log: FlumeLogger;
|
|
64
90
|
};
|
|
65
91
|
/**
|
|
66
|
-
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop
|
|
92
|
+
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop。
|
|
93
|
+
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
94
|
+
* `runStop` の最外殻 try/catch で想定外の throw も `FlumeStopped` の resolve に変換する
|
|
67
95
|
*/
|
|
68
96
|
declare class FlumeRunning {
|
|
69
97
|
private readonly props;
|
|
98
|
+
readonly kind: "running";
|
|
70
99
|
private stopPromise;
|
|
71
100
|
private readonly onAbort;
|
|
72
101
|
constructor(props: Props$1);
|
|
73
102
|
stop(): Promise<FlumeStopped>;
|
|
74
103
|
statuses(): ReadonlyArray<FlumeSourceStatus>;
|
|
75
104
|
private runStop;
|
|
105
|
+
private snapshotStatuses;
|
|
106
|
+
private sourceName;
|
|
76
107
|
}
|
|
77
108
|
//#endregion
|
|
78
109
|
//#region lib/flume.d.ts
|
|
79
110
|
type Props = {
|
|
80
111
|
sources: ReadonlyArray<FlumeSource>;
|
|
81
112
|
signal?: AbortSignal;
|
|
113
|
+
onLog?: FlumeLogHandler;
|
|
114
|
+
deps?: FlumeRuntimeDeps;
|
|
82
115
|
};
|
|
83
116
|
/**
|
|
84
|
-
* 起動前の Flume
|
|
117
|
+
* 起動前の Flume。`start()` で `FlumeRunning` へ遷移する。
|
|
118
|
+
* いずれかの source 失敗時は既に成功した source を全て `stop()` してロールバックし
|
|
119
|
+
* `FlumeStartError` を返す。
|
|
120
|
+
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
121
|
+
* Promise rejection に正規化して `allSettled` で捕捉する (`start()` は決して reject しない)
|
|
85
122
|
*/
|
|
86
123
|
declare class Flume {
|
|
87
124
|
private readonly props;
|
|
88
125
|
private consumed;
|
|
126
|
+
private readonly log;
|
|
127
|
+
private readonly deps;
|
|
89
128
|
constructor(props: Props);
|
|
90
|
-
start(handler: FlumeHandler): Promise<
|
|
91
|
-
private
|
|
92
|
-
|
|
129
|
+
start(handler: FlumeHandler): Promise<FlumeRunning | FlumeStartError>;
|
|
130
|
+
private guardStart;
|
|
131
|
+
private isSignalAborted;
|
|
132
|
+
private sourceName;
|
|
133
|
+
private safeStart;
|
|
134
|
+
private rollback;
|
|
93
135
|
}
|
|
94
136
|
//#endregion
|
|
95
|
-
export { Flume, FlumeConnectionError, type FlumeDiscordSourceOptions, type FlumeEvent, type FlumeGatewayMessage, type FlumeGitHubNotification, type FlumeGitHubSourceOptions, type FlumeHandler, FlumeHttpError, type FlumeLog, type FlumeLogHandler, type FlumeLogInput, type FlumeLogLevel,
|
|
137
|
+
export { Flume, FlumeConnectionError, type FlumeDiscordEvent, type FlumeDiscordSourceOptions, type FlumeEvent, type FlumeGatewayMessage, type FlumeGitHubEvent, type FlumeGitHubNotification, type FlumeGitHubSourceOptions, type FlumeHandler, 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, type FlumeSource, type FlumeSourceName, type FlumeSourceOptions, type FlumeSourceStartOptions, type FlumeSourceStatus, FlumeStartError, type FlumeStatus, type FlumeStatusHandler, FlumeStopped, type FlumeTimerHandle, createFlumeDefaultDeps };
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { n as createFlumeDefaultDeps, t as
|
|
2
|
-
import {
|
|
3
|
-
import { t as FlumeHttpError } from "./http-error-
|
|
1
|
+
import { a as FlumeParseError, c as safeNormalizeError, i as FlumeStartError, l as safeErrorMessage, n as FlumeLogger, o as createFlumeDefaultDeps, s as attempt, t as safeInvokeCallback } from "./safe-invoke-callback-EpWXwfwp.js";
|
|
2
|
+
import { t as FlumeConnectionError } from "./connection-error-HUO3PC3G.js";
|
|
3
|
+
import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
|
|
4
4
|
//#region lib/flume-stopped.ts
|
|
5
5
|
/**
|
|
6
6
|
* 停止済みの終端状態。最終ステータスのスナップショットのみ観測できる
|
|
7
7
|
*/
|
|
8
8
|
var FlumeStopped = class {
|
|
9
9
|
props;
|
|
10
|
+
kind = "stopped";
|
|
10
11
|
constructor(props) {
|
|
11
12
|
this.props = props;
|
|
12
13
|
Object.freeze(this);
|
|
@@ -18,18 +19,45 @@ var FlumeStopped = class {
|
|
|
18
19
|
//#endregion
|
|
19
20
|
//#region lib/flume-running.ts
|
|
20
21
|
/**
|
|
21
|
-
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop
|
|
22
|
+
* 稼働中の Flume。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop。
|
|
23
|
+
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
24
|
+
* `runStop` の最外殻 try/catch で想定外の throw も `FlumeStopped` の resolve に変換する
|
|
22
25
|
*/
|
|
23
26
|
var FlumeRunning = class {
|
|
24
27
|
props;
|
|
28
|
+
kind = "running";
|
|
25
29
|
stopPromise = null;
|
|
26
30
|
onAbort;
|
|
27
31
|
constructor(props) {
|
|
28
32
|
this.props = props;
|
|
29
33
|
this.onAbort = () => {
|
|
30
|
-
this.
|
|
34
|
+
this.props.log.info({
|
|
35
|
+
action: "flume.abort",
|
|
36
|
+
message: "signal aborted, stopping"
|
|
37
|
+
});
|
|
38
|
+
safeInvokeCallback({
|
|
39
|
+
fn: () => this.stop(),
|
|
40
|
+
onError: (error) => {
|
|
41
|
+
this.props.log.error({
|
|
42
|
+
action: "flume.abort.stop.failed",
|
|
43
|
+
message: safeErrorMessage({ error }),
|
|
44
|
+
error
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
});
|
|
31
48
|
};
|
|
32
|
-
|
|
49
|
+
const signal = props.signal;
|
|
50
|
+
if (signal) {
|
|
51
|
+
const result = attempt(() => signal.addEventListener("abort", this.onAbort, { once: true }));
|
|
52
|
+
if (result instanceof Error) {
|
|
53
|
+
const error = safeNormalizeError({ value: result });
|
|
54
|
+
props.log.error({
|
|
55
|
+
action: "signal.addListener.failed",
|
|
56
|
+
message: safeErrorMessage({ error }),
|
|
57
|
+
error
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
33
61
|
}
|
|
34
62
|
stop() {
|
|
35
63
|
if (this.stopPromise) return this.stopPromise;
|
|
@@ -37,85 +65,225 @@ var FlumeRunning = class {
|
|
|
37
65
|
return this.stopPromise;
|
|
38
66
|
}
|
|
39
67
|
statuses() {
|
|
40
|
-
return this.
|
|
41
|
-
name: source.name,
|
|
42
|
-
status: source.status()
|
|
43
|
-
}));
|
|
68
|
+
return this.snapshotStatuses();
|
|
44
69
|
}
|
|
45
70
|
async runStop() {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
71
|
+
try {
|
|
72
|
+
this.props.log.info({
|
|
73
|
+
action: "flume.stop",
|
|
74
|
+
message: `stopping ${this.props.sources.length} source(s)`
|
|
75
|
+
});
|
|
76
|
+
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
77
|
+
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
78
|
+
const source = this.props.sources[index];
|
|
79
|
+
const name = source ? this.sourceName(source) : "?";
|
|
80
|
+
const error = safeNormalizeError({ value: result.reason });
|
|
81
|
+
this.props.log.error({
|
|
82
|
+
action: "flume.stop.failed",
|
|
83
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
84
|
+
error,
|
|
85
|
+
detail: { source: name }
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
const signal = this.props.signal;
|
|
89
|
+
if (signal) {
|
|
90
|
+
const result = attempt(() => signal.removeEventListener("abort", this.onAbort));
|
|
91
|
+
if (result instanceof Error) {
|
|
92
|
+
const error = safeNormalizeError({ value: result });
|
|
93
|
+
this.props.log.error({
|
|
94
|
+
action: "signal.removeListener.failed",
|
|
95
|
+
message: safeErrorMessage({ error }),
|
|
96
|
+
error
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this.props.log.info({
|
|
101
|
+
action: "flume.stop.complete",
|
|
102
|
+
message: "all sources stopped"
|
|
103
|
+
});
|
|
104
|
+
return new FlumeStopped({ finalStatuses: this.snapshotStatuses() });
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const error = safeNormalizeError({ value: err });
|
|
107
|
+
this.props.log.error({
|
|
108
|
+
action: "flume.stop.unhandled",
|
|
109
|
+
message: safeErrorMessage({ error }),
|
|
110
|
+
error
|
|
111
|
+
});
|
|
112
|
+
return new FlumeStopped({ finalStatuses: this.snapshotStatuses() });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
snapshotStatuses() {
|
|
116
|
+
return this.props.sources.map((source) => {
|
|
117
|
+
const name = this.sourceName(source);
|
|
118
|
+
const status = attempt(() => source.status());
|
|
119
|
+
if (status instanceof Error) {
|
|
120
|
+
const error = safeNormalizeError({ value: status });
|
|
121
|
+
this.props.log.error({
|
|
122
|
+
action: "source.status.failed",
|
|
123
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
124
|
+
error,
|
|
125
|
+
detail: { source: name }
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
source: name,
|
|
129
|
+
status: "disconnected"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
source: name,
|
|
134
|
+
status
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
sourceName(source) {
|
|
139
|
+
const result = attempt(() => source.name);
|
|
140
|
+
if (result instanceof Error) return "?";
|
|
141
|
+
if (typeof result !== "string") return "?";
|
|
142
|
+
return result;
|
|
52
143
|
}
|
|
53
144
|
};
|
|
54
145
|
//#endregion
|
|
55
146
|
//#region lib/flume.ts
|
|
56
147
|
/**
|
|
57
|
-
* 起動前の Flume
|
|
148
|
+
* 起動前の Flume。`start()` で `FlumeRunning` へ遷移する。
|
|
149
|
+
* いずれかの source 失敗時は既に成功した source を全て `stop()` してロールバックし
|
|
150
|
+
* `FlumeStartError` を返す。
|
|
151
|
+
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
152
|
+
* Promise rejection に正規化して `allSettled` で捕捉する (`start()` は決して reject しない)
|
|
58
153
|
*/
|
|
59
154
|
var Flume = class {
|
|
60
155
|
props;
|
|
61
156
|
consumed = false;
|
|
157
|
+
log;
|
|
158
|
+
deps;
|
|
62
159
|
constructor(props) {
|
|
63
160
|
this.props = props;
|
|
161
|
+
this.deps = props.deps ?? createFlumeDefaultDeps();
|
|
162
|
+
this.log = new FlumeLogger({
|
|
163
|
+
source: "flume",
|
|
164
|
+
handler: props.onLog,
|
|
165
|
+
deps: this.deps
|
|
166
|
+
});
|
|
64
167
|
}
|
|
65
168
|
async start(handler) {
|
|
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
|
-
};
|
|
169
|
+
const guard = this.guardStart();
|
|
170
|
+
if (guard) return guard;
|
|
74
171
|
this.consumed = true;
|
|
75
|
-
|
|
172
|
+
this.log.info({
|
|
173
|
+
action: "flume.start",
|
|
174
|
+
message: `starting ${this.props.sources.length} source(s)`,
|
|
175
|
+
detail: { count: this.props.sources.length }
|
|
176
|
+
});
|
|
177
|
+
const settled = await Promise.allSettled(this.props.sources.map((source) => this.safeStart(source, handler)));
|
|
76
178
|
const failures = [];
|
|
77
179
|
const started = [];
|
|
78
|
-
for (
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
180
|
+
for (const [index, result] of settled.entries()) {
|
|
181
|
+
const source = this.props.sources[index];
|
|
182
|
+
if (source === void 0) continue;
|
|
183
|
+
const name = this.sourceName(source);
|
|
82
184
|
if (result.status === "rejected") {
|
|
83
|
-
const reason = result.reason;
|
|
84
185
|
failures.push({
|
|
85
|
-
name
|
|
86
|
-
error:
|
|
186
|
+
name,
|
|
187
|
+
error: safeNormalizeError({ value: result.reason })
|
|
87
188
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (result.value instanceof Error) {
|
|
192
|
+
failures.push({
|
|
193
|
+
name,
|
|
194
|
+
error: result.value
|
|
195
|
+
});
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
started.push(source);
|
|
93
199
|
}
|
|
94
200
|
if (failures.length > 0) {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
};
|
|
201
|
+
for (const failure of failures) this.log.error({
|
|
202
|
+
action: "flume.source.failed",
|
|
203
|
+
message: `${failure.name}: ${safeErrorMessage({ error: failure.error })}`,
|
|
204
|
+
error: failure.error,
|
|
205
|
+
detail: { source: failure.name }
|
|
206
|
+
});
|
|
207
|
+
await this.rollback(started);
|
|
208
|
+
const detail = failures.map((f) => `${f.name}: ${safeErrorMessage({ error: f.error })}`).join("; ");
|
|
209
|
+
const error = new FlumeStartError(`Flume.start: ${failures.length} source(s) failed: ${detail}`);
|
|
210
|
+
this.log.error({
|
|
211
|
+
action: "flume.start.failed",
|
|
212
|
+
message: safeErrorMessage({ error }),
|
|
213
|
+
error
|
|
214
|
+
});
|
|
215
|
+
return error;
|
|
101
216
|
}
|
|
102
|
-
if (this.
|
|
103
|
-
await
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
217
|
+
if (this.isSignalAborted()) {
|
|
218
|
+
await this.rollback(this.props.sources);
|
|
219
|
+
const error = new FlumeStartError("Flume.start: aborted during start");
|
|
220
|
+
this.log.warn({
|
|
221
|
+
action: "flume.start.aborted",
|
|
222
|
+
message: safeErrorMessage({ error }),
|
|
223
|
+
error
|
|
224
|
+
});
|
|
225
|
+
return error;
|
|
108
226
|
}
|
|
109
|
-
this.
|
|
227
|
+
this.log.info({
|
|
228
|
+
action: "flume.start.complete",
|
|
229
|
+
message: "all sources started"
|
|
230
|
+
});
|
|
231
|
+
return new FlumeRunning({
|
|
110
232
|
sources: this.props.sources,
|
|
111
|
-
signal: this.props.signal
|
|
233
|
+
signal: this.props.signal,
|
|
234
|
+
log: this.log
|
|
112
235
|
});
|
|
113
|
-
return { ok: true };
|
|
114
236
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
237
|
+
guardStart() {
|
|
238
|
+
if (this.consumed) {
|
|
239
|
+
const error = new FlumeStartError("Flume.start: already started");
|
|
240
|
+
this.log.warn({
|
|
241
|
+
action: "flume.start.refused",
|
|
242
|
+
message: safeErrorMessage({ error }),
|
|
243
|
+
error
|
|
244
|
+
});
|
|
245
|
+
return error;
|
|
246
|
+
}
|
|
247
|
+
if (this.isSignalAborted()) {
|
|
248
|
+
const error = new FlumeStartError("Flume.start: signal already aborted");
|
|
249
|
+
this.log.warn({
|
|
250
|
+
action: "flume.start.refused",
|
|
251
|
+
message: safeErrorMessage({ error }),
|
|
252
|
+
error
|
|
253
|
+
});
|
|
254
|
+
return error;
|
|
255
|
+
}
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
isSignalAborted() {
|
|
259
|
+
const signal = this.props.signal;
|
|
260
|
+
if (!signal) return false;
|
|
261
|
+
const result = attempt(() => signal.aborted === true);
|
|
262
|
+
return result instanceof Error ? true : result;
|
|
263
|
+
}
|
|
264
|
+
sourceName(source) {
|
|
265
|
+
const result = attempt(() => source.name);
|
|
266
|
+
if (result instanceof Error) return "?";
|
|
267
|
+
if (typeof result !== "string") return "?";
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
safeStart(source, handler) {
|
|
271
|
+
return Promise.resolve().then(() => source.start(handler, { signal: this.props.signal }));
|
|
272
|
+
}
|
|
273
|
+
async rollback(sources) {
|
|
274
|
+
const settled = await Promise.allSettled(sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
275
|
+
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
276
|
+
const source = sources[index];
|
|
277
|
+
const name = source ? this.sourceName(source) : "?";
|
|
278
|
+
const error = safeNormalizeError({ value: result.reason });
|
|
279
|
+
this.log.error({
|
|
280
|
+
action: "flume.rollback.failed",
|
|
281
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
282
|
+
error,
|
|
283
|
+
detail: { source: name }
|
|
284
|
+
});
|
|
285
|
+
}
|
|
118
286
|
}
|
|
119
287
|
};
|
|
120
288
|
//#endregion
|
|
121
|
-
export { Flume, FlumeConnectionError, FlumeHttpError,
|
|
289
|
+
export { Flume, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeStartError, FlumeStopped, createFlumeDefaultDeps };
|
|
@@ -0,0 +1,170 @@
|
|
|
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 };
|