@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/dist/index.d.ts CHANGED
@@ -1,51 +1,56 @@
1
- import { C as FlumeStartResult, E as FlumeTimerHandle, S as FlumeStartOk, T as FlumeStatusHandler, _ as FlumeSource, a as FlumeGitHubSourceOptions, b as FlumeSourceStatus, c as FlumeLogHandler, d as FlumeReconnectConfig, f as FlumeReconnectOptions, g as FlumeSlackSourceOptions, h as FlumeSlackEnvelope, i as FlumeGitHubNotification, l as FlumeLogInput, m as FlumeSlackConnectionResponse, n as FlumeEvent, o as FlumeHandler, p as FlumeRuntimeDeps, r as FlumeGatewayMessage, s as FlumeLog, t as FlumeDiscordSourceOptions, u as FlumeLogLevel, v as FlumeSourceName, w as FlumeStatus, x as FlumeStartErr, y as FlumeSourceOptions } from "./types-Bm9uKUQz.js";
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/logger.d.ts
10
- type Props$4 = {
11
- source: string;
12
- handler?: FlumeLogHandler;
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
- declare class FlumeLogger {
16
- private readonly props;
17
- constructor(props: Props$4);
18
- debug(entry: FlumeLogInput): void;
19
- info(entry: FlumeLogInput): void;
20
- warn(entry: FlumeLogInput): void;
21
- error(entry: FlumeLogInput): void;
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/reconnect-config.d.ts
26
- declare function resolveFlumeReconnectConfig(input: boolean | FlumeReconnectOptions | undefined): FlumeReconnectConfig | null;
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/reconnector.d.ts
29
- type Props$3 = {
30
- maxAttempts: number;
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 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;
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$2 = {
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
- constructor(props: Props$2);
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$1 = {
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$1);
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 Props = {
80
- sources: ReadonlyArray<FlumeSource>;
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
- * 起動前の Flumestart() で FlumeRunning へ遷移する
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 props;
109
+ private readonly sources;
110
+ private readonly options;
88
111
  private consumed;
89
- constructor(props: Props);
90
- start(handler: FlumeHandler): Promise<FlumeStartResult>;
91
- private running;
92
- runningState(): FlumeRunning | null;
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 FlumeGitHubNotification, type FlumeGitHubSourceOptions, type FlumeHandler, FlumeHttpError, type FlumeLog, type FlumeLogHandler, type FlumeLogInput, type FlumeLogLevel, FlumeLogger, FlumeParseError, type FlumeReconnectConfig, type FlumeReconnectOptions, FlumeReconnector, FlumeRunning, type FlumeRuntimeDeps, type FlumeSlackConnectionResponse, type FlumeSlackEnvelope, type FlumeSlackSourceOptions, type FlumeSource, type FlumeSourceName, type FlumeSourceOptions, type FlumeSourceStatus, type FlumeStartErr, type FlumeStartOk, type FlumeStartResult, type FlumeStatus, type FlumeStatusHandler, FlumeStopped, type FlumeTimerHandle, createFlumeDefaultDeps, resolveFlumeReconnectConfig };
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 createFlumeDefaultDeps, t as FlumeLogger } from "./logger-CpGB9WO_.js";
2
- import { i as FlumeConnectionError, n as resolveFlumeReconnectConfig, r as FlumeParseError, t as FlumeReconnector } from "./reconnector-BDoJ1xNX.js";
3
- import { t as FlumeHttpError } from "./http-error-BtXonO-W.js";
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.stop();
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
- if (props.signal) props.signal.addEventListener("abort", this.onAbort, { once: true });
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.props.sources.map((source) => ({
41
- name: source.name,
42
- status: source.status()
43
- }));
89
+ return this.snapshotStatuses();
44
90
  }
45
91
  async runStop() {
46
- await Promise.allSettled(this.props.sources.map((source) => source.stop()));
47
- this.props.signal?.removeEventListener("abort", this.onAbort);
48
- return new FlumeStopped({ finalStatuses: this.props.sources.map((source) => ({
49
- name: source.name,
50
- status: source.status()
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
- * 起動前の Flumestart() で FlumeRunning へ遷移する
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
- props;
194
+ sources;
195
+ options;
61
196
  consumed = false;
62
- constructor(props) {
63
- this.props = props;
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(handler) {
66
- if (this.consumed) return {
67
- ok: false,
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
- const settled = await Promise.allSettled(this.props.sources.map((source) => source.start(handler)));
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 (let i = 0; i < settled.length; i++) {
79
- const result = settled[i];
80
- const source = this.props.sources[i];
81
- if (result === void 0 || source === void 0) continue;
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: source.name,
86
- error: reason instanceof Error ? reason : new Error(String(reason))
230
+ name,
231
+ error: safeNormalizeError({ value: result.reason })
87
232
  });
88
- } else if (!result.value.ok) failures.push({
89
- name: source.name,
90
- error: result.value.error
91
- });
92
- else started.push(source);
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
- await Promise.allSettled(started.map((source) => source.stop()));
96
- const detail = failures.map((f) => `${f.name}: ${f.error.message}`).join("; ");
97
- return {
98
- ok: false,
99
- error: /* @__PURE__ */ new Error(`Flume.start: ${failures.length} source(s) failed: ${detail}`)
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.props.signal?.aborted) {
103
- await Promise.allSettled(this.props.sources.map((source) => source.stop()));
104
- return {
105
- ok: false,
106
- error: /* @__PURE__ */ new Error("Flume.start: aborted during start")
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
- this.running = new FlumeRunning({
110
- sources: this.props.sources,
111
- signal: this.props.signal
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
- running = null;
116
- runningState() {
117
- return this.running;
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, FlumeLogger, FlumeParseError, FlumeReconnector, FlumeRunning, FlumeStopped, createFlumeDefaultDeps, resolveFlumeReconnectConfig };
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 };