@interactive-inc/flume 0.2.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/dist/index.d.ts CHANGED
@@ -1,20 +1,66 @@
1
- import { C as FlumeTimerHandle, S 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, x as FlumeStatus, y as FlumeSourceOptions } from "./types-tnOPBc1p.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 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
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
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
9
52
  //#region lib/logger.d.ts
10
- type Props$5 = {
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$5);
63
+ constructor(props: Props$3);
18
64
  debug(entry: FlumeLogInput): void;
19
65
  info(entry: FlumeLogInput): void;
20
66
  warn(entry: FlumeLogInput): void;
@@ -22,41 +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$4 = {
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$4);
41
- schedule(fn: () => void): number;
42
- reset(): void;
43
- cancel(): void;
44
- private nextDelay;
45
- }
46
- //#endregion
47
- //#region lib/schedule-reconnect.d.ts
48
- type Props$3 = {
49
- reconnector: FlumeReconnector | null;
50
- log: FlumeLogger;
51
- setStatus: (status: FlumeStatus) => void;
52
- retry: () => void;
53
- };
54
- /**
55
- * 接続が落ちた際の共通再接続スケジューラ。reconnector の状態を見て次回試行を予約し、
56
- * 試行回数が尽きていれば disconnected に落とす
57
- */
58
- declare function scheduleFlumeReconnect(props: Props$3): void;
59
- //#endregion
60
71
  //#region lib/flume-stopped.d.ts
61
72
  type Props$2 = {
62
73
  finalStatuses: ReadonlyArray<FlumeSourceStatus>;
@@ -66,6 +77,7 @@ type Props$2 = {
66
77
  */
67
78
  declare class FlumeStopped {
68
79
  private readonly props;
80
+ readonly kind: "stopped";
69
81
  constructor(props: Props$2);
70
82
  statuses(): ReadonlyArray<FlumeSourceStatus>;
71
83
  }
@@ -74,33 +86,52 @@ declare class FlumeStopped {
74
86
  type Props$1 = {
75
87
  sources: ReadonlyArray<FlumeSource>;
76
88
  signal?: AbortSignal;
89
+ log: FlumeLogger;
77
90
  };
78
91
  /**
79
- * 稼働中の 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 に変換する
80
95
  */
81
96
  declare class FlumeRunning {
82
97
  private readonly props;
98
+ readonly kind: "running";
83
99
  private stopPromise;
84
100
  private readonly onAbort;
85
101
  constructor(props: Props$1);
86
102
  stop(): Promise<FlumeStopped>;
87
103
  statuses(): ReadonlyArray<FlumeSourceStatus>;
88
104
  private runStop;
105
+ private snapshotStatuses;
106
+ private sourceName;
89
107
  }
90
108
  //#endregion
91
109
  //#region lib/flume.d.ts
92
110
  type Props = {
93
111
  sources: ReadonlyArray<FlumeSource>;
94
112
  signal?: AbortSignal;
113
+ onLog?: FlumeLogHandler;
114
+ deps?: FlumeRuntimeDeps;
95
115
  };
96
116
  /**
97
- * 起動前の Flumestart() で FlumeRunning へ遷移する
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 しない)
98
122
  */
99
123
  declare class Flume {
100
124
  private readonly props;
101
125
  private consumed;
126
+ private readonly log;
127
+ private readonly deps;
102
128
  constructor(props: Props);
103
- start(handler: FlumeHandler): Promise<FlumeRunning | Error>;
129
+ start(handler: FlumeHandler): Promise<FlumeRunning | FlumeStartError>;
130
+ private guardStart;
131
+ private isSignalAborted;
132
+ private sourceName;
133
+ private safeStart;
134
+ private rollback;
104
135
  }
105
136
  //#endregion
106
- 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 FlumeStatus, type FlumeStatusHandler, FlumeStopped, type FlumeTimerHandle, createFlumeDefaultDeps, resolveFlumeReconnectConfig, scheduleFlumeReconnect };
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 FlumeLogger } from "./logger-CpGB9WO_.js";
2
- import { a as FlumeConnectionError, i as FlumeParseError, n as FlumeReconnector, r as resolveFlumeReconnectConfig, t as scheduleFlumeReconnect } from "./schedule-reconnect-DSxZJG3h.js";
3
- import { t as FlumeHttpError } from "./http-error-BtXonO-W.js";
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.stop();
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
- if (props.signal) props.signal.addEventListener("abort", this.onAbort, { once: true });
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,68 +65,225 @@ var FlumeRunning = class {
37
65
  return this.stopPromise;
38
66
  }
39
67
  statuses() {
40
- return this.props.sources.map((source) => ({
41
- name: source.name,
42
- status: source.status()
43
- }));
68
+ return this.snapshotStatuses();
44
69
  }
45
70
  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
- })) });
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
- * 起動前の Flumestart() で FlumeRunning へ遷移する
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
- if (this.consumed) return /* @__PURE__ */ new Error("Flume.start: already started");
67
- if (this.props.signal?.aborted) return /* @__PURE__ */ new Error("Flume.start: signal already aborted");
169
+ const guard = this.guardStart();
170
+ if (guard) return guard;
68
171
  this.consumed = true;
69
- const settled = await Promise.allSettled(this.props.sources.map((source) => source.start(handler)));
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)));
70
178
  const failures = [];
71
179
  const started = [];
72
- for (let i = 0; i < settled.length; i++) {
73
- const result = settled[i];
74
- const source = this.props.sources[i];
75
- if (result === void 0 || source === void 0) continue;
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);
76
184
  if (result.status === "rejected") {
77
- const reason = result.reason;
78
185
  failures.push({
79
- name: source.name,
80
- error: reason instanceof Error ? reason : new Error(String(reason))
186
+ name,
187
+ error: safeNormalizeError({ value: result.reason })
81
188
  });
82
- } else if (result.value instanceof Error) failures.push({
83
- name: source.name,
84
- error: result.value
85
- });
86
- else started.push(source);
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);
87
199
  }
88
200
  if (failures.length > 0) {
89
- await Promise.allSettled(started.map((source) => source.stop()));
90
- const detail = failures.map((f) => `${f.name}: ${f.error.message}`).join("; ");
91
- return /* @__PURE__ */ new Error(`Flume.start: ${failures.length} source(s) failed: ${detail}`);
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;
92
216
  }
93
- if (this.props.signal?.aborted) {
94
- await Promise.allSettled(this.props.sources.map((source) => source.stop()));
95
- return /* @__PURE__ */ new Error("Flume.start: aborted during start");
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;
96
226
  }
227
+ this.log.info({
228
+ action: "flume.start.complete",
229
+ message: "all sources started"
230
+ });
97
231
  return new FlumeRunning({
98
232
  sources: this.props.sources,
99
- signal: this.props.signal
233
+ signal: this.props.signal,
234
+ log: this.log
100
235
  });
101
236
  }
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
+ }
286
+ }
102
287
  };
103
288
  //#endregion
104
- export { Flume, FlumeConnectionError, FlumeHttpError, FlumeLogger, FlumeParseError, FlumeReconnector, FlumeRunning, FlumeStopped, createFlumeDefaultDeps, resolveFlumeReconnectConfig, scheduleFlumeReconnect };
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 };