@interactive-inc/flume 0.10.0 → 0.10.1

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.
@@ -24,9 +24,9 @@ declare class FlumeLogger {
24
24
  //#region lib/discord/discord-gateway-message-schema.d.ts
25
25
  declare const FlumeGatewayMessageSchema: z.ZodObject<{
26
26
  op: z.ZodNumber;
27
- d: z.ZodUnknown;
28
- s: z.ZodNullable<z.ZodNumber>;
29
- t: z.ZodNullable<z.ZodString>;
27
+ d: z.ZodOptional<z.ZodUnknown>;
28
+ s: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
29
+ t: z.ZodOptional<z.ZodNullable<z.ZodString>>;
30
30
  }, z.core.$strip>;
31
31
  //#endregion
32
32
  //#region lib/github/github-notification-schema.d.ts
@@ -103,7 +103,15 @@ type FlumeTimeEvent = {
103
103
  meta: Record<string, string>;
104
104
  receivedAt: number;
105
105
  };
106
- type FlumeEvent = FlumeDiscordEvent | FlumeSlackEvent | FlumeGitHubEvent | FlumeTimeEvent;
106
+ type FlumeCustomEvent = {
107
+ source: "custom";
108
+ sourceName: string;
109
+ type: string;
110
+ data: Record<string, unknown>;
111
+ meta: Record<string, string>;
112
+ receivedAt: number;
113
+ };
114
+ type FlumeEvent = FlumeDiscordEvent | FlumeSlackEvent | FlumeGitHubEvent | FlumeTimeEvent | FlumeCustomEvent;
107
115
  type FlumeEventHandler = (event: FlumeEvent) => void | Promise<void>;
108
116
  type FlumeStreamItem = {
109
117
  kind: "event";
@@ -112,7 +120,7 @@ type FlumeStreamItem = {
112
120
  kind: "log";
113
121
  log: FlumeLog;
114
122
  };
115
- type FlumeStreamHandler = (item: FlumeStreamItem) => void;
123
+ type FlumeStreamHandler = (item: FlumeStreamItem) => unknown | Promise<unknown>;
116
124
  /**
117
125
  * `FlumeConfluence` が onEvent に渡す item。Flume 単体の `FlumeStreamItem` に
118
126
  * `groupId` (`add(id, ...)` で渡したグループ識別子) をスタンプしたもの。
@@ -121,7 +129,7 @@ type FlumeStreamHandler = (item: FlumeStreamItem) => void;
121
129
  type FlumeConfluenceItem = FlumeStreamItem & {
122
130
  readonly groupId: string;
123
131
  };
124
- type FlumeConfluenceItemHandler = (item: FlumeConfluenceItem) => void;
132
+ type FlumeConfluenceItemHandler = (item: FlumeConfluenceItem) => unknown | Promise<unknown>;
125
133
  type FlumeStreamOverflow = "drop-oldest" | "drop-newest";
126
134
  type FlumeStreamOptions = {
127
135
  /** バッファ上限 (既定 1000)。consumer が遅れて溢れたら onOverflow に従う */buffer?: number; /** バッファ溢れ時の方針 (既定 "drop-oldest") */
@@ -173,17 +181,28 @@ type FlumeSourceStartContext = {
173
181
  onStatus?: FlumeSourceLocalStatusHandler;
174
182
  reconnect: FlumeReconnectConfig | null;
175
183
  /**
176
- * Flume.start() に渡された signal をそのまま転送する。
177
- * source 実装が自前で `fetch(url, { signal })` / `setTimeout` cancel / WS close を
178
- * host abort 経由で発火させたい時に使う (Flume 自身は最外殻で runClose を駆動するので
179
- * source signal を無視しても動作的には停止する 自然な伝播パスが欲しい場合のみ)
180
- * Flume.options.signal が未設定なら省略される。
184
+ * Flume に渡された signal をそのまま転送する。
185
+ * connect 中の abort `FlumeSource` 基底クラスが購読して `stop()` を発火する
186
+ * (進行中の接続を中断して `Flume.open()` を解放する)。connect 完了後の abort
187
+ * FlumeRunning が runClose を駆動する。source 実装が自前で `fetch(url, { signal })`
188
+ * などへ伝播させたい場合にも使える。Flume.options.signal が未設定なら省略される。
181
189
  */
182
190
  signal?: AbortSignal;
183
191
  };
184
192
  type FlumeDiscordSourceOptions = {
185
193
  token: string;
194
+ /**
195
+ * Gateway intent ビットフラグ。既定は Guilds | GuildMessages | DirectMessages。
196
+ * message の `content` 本文が必要な場合は privileged intent の `MessageContent` を
197
+ * Developer Portal で有効化した上で明示的に足す (未承認のまま足すと close 4014 で終端する)
198
+ */
186
199
  intents?: number;
200
+ /**
201
+ * WebSocket open から READY/RESUMED までの上限 (ms)。既定 30_000。
202
+ * HELLO が来ない half-open socket で `connect()` (ひいては `Flume.open()`) が
203
+ * 永久にハングするのを防ぐ
204
+ */
205
+ handshakeTimeoutMs?: number;
187
206
  };
188
207
  type FlumeSlackSourceOptions = {
189
208
  appToken: string;
@@ -198,6 +217,12 @@ type FlumeSlackSourceOptions = {
198
217
  * inbound frame.
199
218
  */
200
219
  idleTimeoutMs?: number | null;
220
+ /**
221
+ * WebSocket open から hello 受信までの上限 (ms)。既定 30_000。
222
+ * hello が来ない half-open socket で `connect()` (ひいては `Flume.open()`) が
223
+ * 永久にハングするのを防ぐ
224
+ */
225
+ handshakeTimeoutMs?: number;
201
226
  };
202
227
  type FlumeGitHubSourceOptions = {
203
228
  token: string;
@@ -267,6 +292,10 @@ type FlumeGitHubNotification = z.infer<typeof FlumeGitHubNotificationSchema>;
267
292
  * `FlumeSourceStartContext` (handler / log / deps / onStatus / reconnect) を
268
293
  * `start()` で受け取り、subclass の `connect(ctx)` に手渡す。
269
294
  *
295
+ * `ctx.signal` の購読も base が行う: connect 中に abort されたら `stop()` を発火して
296
+ * 進行中の接続を中断する (subclass の `disconnect()` が pending な connect を解決する契約)。
297
+ * connect 完了後の abort は Flume / FlumeRunning が runClose 経由で駆動する。
298
+ *
270
299
  * subclass のテンプレート:
271
300
  *
272
301
  * ```ts
@@ -290,11 +319,19 @@ declare abstract class FlumeSource {
290
319
  abstract readonly name: string;
291
320
  private consumed;
292
321
  private stopped;
322
+ private stopPromise;
293
323
  private ctx;
294
324
  private statusEmitter;
325
+ private abortHandler;
295
326
  private readonly queue;
296
327
  start(ctx: FlumeSourceStartContext): Promise<Error | null>;
297
- stop(): Promise<void>;
328
+ /**
329
+ * 冪等。`disconnect()` の throw は捕捉して `Error` として返す (公開境界から reject しない)。
330
+ * Flume.runClose / Flume.rollback は戻り値の Error を `flume.close.failed` /
331
+ * `flume.rollback.failed` として firehose に流す
332
+ */
333
+ stop(): Promise<Error | null>;
334
+ private runStop;
298
335
  status(): FlumeStatus;
299
336
  /**
300
337
  * subclass が受信した protocol イベントを `FlumeEvent` として handler へ流す。
@@ -309,10 +346,13 @@ declare abstract class FlumeSource {
309
346
  protected get currentStatus(): FlumeStatus;
310
347
  /** subclass が start ctx を再参照したい場合 (stop 後は null) */
311
348
  protected get context(): FlumeSourceStartContext | null;
349
+ private isSignalAborted;
350
+ private attachAbortListener;
351
+ private detachAbortListener;
312
352
  /** protocol 接続。subclass 実装 */
313
353
  protected abstract connect(ctx: FlumeSourceStartContext): Promise<Error | null>;
314
354
  /** protocol 切断。subclass 実装。base が `stop()` 内で必ず呼ぶ */
315
355
  protected abstract disconnect(): Promise<void> | void;
316
356
  }
317
357
  //#endregion
318
- export { FlumeStatus as A, FlumeTimerHandle as B, FlumeSlackEvent as C, FlumeSourceStartContext as D, FlumeSourceName as E, FlumeTimeEvent as F, FlumeTimeMessage as I, FlumeTimeSourceOptions as L, FlumeStreamItem as M, FlumeStreamOptions as N, FlumeSourceStatus as O, FlumeStreamOverflow as P, FlumeTimeSourceState as R, FlumeSlackEnvelope as S, FlumeSourceLocalStatusHandler as T, FlumeLogger as V, FlumeLogLevel as _, FlumeDiscordEvent as a, FlumeRuntimeDeps as b, FlumeEvent as c, FlumeGitHubEvent as d, FlumeGitHubNotification as f, FlumeLogInput as g, FlumeLogHandler as h, FlumeConfluenceItemHandler as i, FlumeStreamHandler as j, FlumeStatePersister as k, FlumeEventHandler as l, FlumeLog as m, FlumeCatchupPolicy as n, FlumeDiscordSourceOptions as o, FlumeGitHubSourceOptions as p, FlumeConfluenceItem as r, FlumeErrorHandler as s, FlumeSource as t, FlumeGatewayMessage as u, FlumeReconnectConfig as v, FlumeSlackSourceOptions as w, FlumeSlackConnectionResponse as x, FlumeReconnectOptions as y, FlumeTimeTick as z };
358
+ export { FlumeStatePersister as A, FlumeTimeTick as B, FlumeSlackEnvelope as C, FlumeSourceName as D, FlumeSourceLocalStatusHandler as E, FlumeStreamOverflow as F, FlumeLogger as H, FlumeTimeEvent as I, FlumeTimeMessage as L, FlumeStreamHandler as M, FlumeStreamItem as N, FlumeSourceStartContext as O, FlumeStreamOptions as P, FlumeTimeSourceOptions as R, FlumeSlackConnectionResponse as S, FlumeSlackSourceOptions as T, FlumeTimerHandle as V, FlumeLogInput as _, FlumeCustomEvent as a, FlumeReconnectOptions as b, FlumeErrorHandler as c, FlumeGatewayMessage as d, FlumeGitHubEvent as f, FlumeLogHandler as g, FlumeLog as h, FlumeConfluenceItemHandler as i, FlumeStatus as j, FlumeSourceStatus as k, FlumeEvent as l, FlumeGitHubSourceOptions as m, FlumeCatchupPolicy as n, FlumeDiscordEvent as o, FlumeGitHubNotification as p, FlumeConfluenceItem as r, FlumeDiscordSourceOptions as s, FlumeSource as t, FlumeEventHandler as u, FlumeLogLevel as v, FlumeSlackEvent as w, FlumeRuntimeDeps as x, FlumeReconnectConfig as y, FlumeTimeSourceState as z };
@@ -2,13 +2,15 @@
2
2
  /**
3
3
  * 任意の値から人が読めるメッセージ文字列を取り出す。
4
4
  * `Error.message` getter / `Symbol.toPrimitive` / `toString` / `valueOf` が throw しても固定文字列に fallback。
5
- * 自身は決して throw しない
5
+ * `instanceof` 自体が throw する値 (revoked Proxy 等) にも耐える。自身は決して throw しない
6
6
  */
7
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>";
8
+ try {
9
+ if (props.error instanceof Error) {
10
+ const message = props.error.message;
11
+ if (typeof message === "string") return message;
12
+ return "<non-string error message>";
13
+ }
12
14
  } catch {
13
15
  return "<unreadable error message>";
14
16
  }
@@ -23,10 +25,13 @@ function safeErrorMessage(props) {
23
25
  /**
24
26
  * 任意の値を `Error` インスタンスへ正規化する。すでに Error ならそのまま返し、
25
27
  * それ以外は `safeErrorMessage` で安全な文字列化を経由して new Error する。
26
- * Error コンストラクタ自体が throw する病的環境でも fallback を返し、決して throw しない
28
+ * `instanceof` 自体が throw する値 (revoked Proxy 等) や Error コンストラクタが throw する
29
+ * 病的環境でも fallback を返し、決して throw しない
27
30
  */
28
31
  function safeNormalizeError(props) {
29
- if (props.value instanceof Error) return props.value;
32
+ try {
33
+ if (props.value instanceof Error) return props.value;
34
+ } catch {}
30
35
  const message = safeErrorMessage({ error: props.value });
31
36
  try {
32
37
  return new Error(message);
@@ -43,10 +48,16 @@ function safeNormalizeError(props) {
43
48
  }
44
49
  //#endregion
45
50
  //#region lib/utils/attempt.ts
51
+ function isThenable(value) {
52
+ if (typeof value !== "object" && typeof value !== "function") return false;
53
+ if (value === null) return false;
54
+ return "then" in value && typeof value.then === "function";
55
+ }
46
56
  function attempt(fn) {
47
57
  try {
48
58
  const result = fn();
49
59
  if (result instanceof Promise) return result.catch((err) => safeNormalizeError({ value: err }));
60
+ if (isThenable(result)) return Promise.resolve(result).then((value) => value, (err) => safeNormalizeError({ value: err }));
50
61
  return result;
51
62
  } catch (err) {
52
63
  return safeNormalizeError({ value: err });
@@ -73,14 +84,18 @@ var FlumeStartError = class extends Error {
73
84
  //#endregion
74
85
  //#region lib/utils/safe-now.ts
75
86
  /**
76
- * `deps.now()` を保護する。throw / 非数値が返った場合は 0 を返す。
87
+ * `deps.now()` を保護する。throw / 非数値 / 非有限値が返った場合は `Date.now()` へ
88
+ * フォールバックする (0 を返すと epoch 1970 が TTL / cron / レート計算へ伝播するため)。
89
+ * `Date.now` 自体まで壊れている病的環境でのみ 0 を返す。
77
90
  * IO 境界のため呼び出し側はこの戻り値を信頼できる
78
91
  */
79
92
  function safeNow(props) {
80
93
  try {
81
94
  const value = props.deps.now();
82
- if (typeof value !== "number" || !Number.isFinite(value)) return 0;
83
- return value;
95
+ if (typeof value === "number" && Number.isFinite(value)) return value;
96
+ } catch {}
97
+ try {
98
+ return Date.now();
84
99
  } catch {
85
100
  return 0;
86
101
  }
@@ -156,6 +171,60 @@ function safeInvokeCallback(props) {
156
171
  }
157
172
  }
158
173
  //#endregion
174
+ //#region lib/utils/serial-queue.ts
175
+ /**
176
+ * 投入順を保ったまま task を直列実行する。各 task は前の完了を待ってから走る。
177
+ * task が throw しても後続には伝播しない (キュー自体は止まらない)。
178
+ * maxDepth を超えた場合は新規 task を drop し onOverflow に通知。
179
+ * cancel() 後は add() が no-op になり、既に積まれた未実行 task も実行せずに流れ落ちる。
180
+ * drain() は待機中に追加された task も含めてキューが空になるまで待つ
181
+ */
182
+ var FlumeSerialQueue = class {
183
+ props;
184
+ chain = Promise.resolve();
185
+ depth = 0;
186
+ cancelled = false;
187
+ constructor(props = {}) {
188
+ this.props = props;
189
+ }
190
+ add(task) {
191
+ if (this.cancelled) return Promise.resolve();
192
+ if (this.props.maxDepth !== void 0 && this.depth >= this.props.maxDepth) {
193
+ this.props.onOverflow?.({
194
+ dropped: 1,
195
+ depth: this.depth
196
+ });
197
+ return Promise.resolve();
198
+ }
199
+ this.depth++;
200
+ const completion = this.chain.then(async () => {
201
+ try {
202
+ if (!this.cancelled) await task();
203
+ } catch {} finally {
204
+ this.depth--;
205
+ }
206
+ });
207
+ this.chain = completion;
208
+ return completion;
209
+ }
210
+ async drain() {
211
+ while (true) {
212
+ const current = this.chain;
213
+ await current;
214
+ if (this.chain === current) return;
215
+ }
216
+ }
217
+ cancel() {
218
+ this.cancelled = true;
219
+ }
220
+ size() {
221
+ return this.depth;
222
+ }
223
+ isCancelled() {
224
+ return this.cancelled;
225
+ }
226
+ };
227
+ //#endregion
159
228
  //#region lib/source-helpers/flume-status-emitter.ts
160
229
  /**
161
230
  * Source の `currentStatus` と `onStatus` 通知を集約する。
@@ -203,55 +272,6 @@ var FlumeStatusEmitter = class {
203
272
  }
204
273
  };
205
274
  //#endregion
206
- //#region lib/utils/serial-queue.ts
207
- /**
208
- * 投入順を保ったまま task を直列実行する。各 task は前の完了を待ってから走る。
209
- * task が throw しても後続には伝播しない (キュー自体は止まらない)。
210
- * maxDepth を超えた場合は新規 task を drop し onOverflow に通知。
211
- * cancel() 後の add() は no-op となり drain() は即時 resolve する
212
- */
213
- var FlumeSerialQueue = class {
214
- props;
215
- chain = Promise.resolve();
216
- depth = 0;
217
- cancelled = false;
218
- constructor(props = {}) {
219
- this.props = props;
220
- }
221
- add(task) {
222
- if (this.cancelled) return;
223
- if (this.props.maxDepth !== void 0 && this.depth >= this.props.maxDepth) {
224
- this.props.onOverflow?.({
225
- dropped: 1,
226
- depth: this.depth
227
- });
228
- return;
229
- }
230
- this.depth++;
231
- this.chain = this.chain.then(async () => {
232
- try {
233
- await task();
234
- } catch {} finally {
235
- this.depth--;
236
- }
237
- });
238
- }
239
- async drain() {
240
- await this.chain;
241
- }
242
- cancel() {
243
- this.cancelled = true;
244
- this.depth = 0;
245
- this.chain = Promise.resolve();
246
- }
247
- size() {
248
- return this.depth;
249
- }
250
- isCancelled() {
251
- return this.cancelled;
252
- }
253
- };
254
- //#endregion
255
275
  //#region lib/flume-source.ts
256
276
  /**
257
277
  * 全 Source の基底クラス。protocol 固有のロジック (`connect` / `disconnect`) のみ
@@ -260,6 +280,10 @@ var FlumeSerialQueue = class {
260
280
  * `FlumeSourceStartContext` (handler / log / deps / onStatus / reconnect) を
261
281
  * `start()` で受け取り、subclass の `connect(ctx)` に手渡す。
262
282
  *
283
+ * `ctx.signal` の購読も base が行う: connect 中に abort されたら `stop()` を発火して
284
+ * 進行中の接続を中断する (subclass の `disconnect()` が pending な connect を解決する契約)。
285
+ * connect 完了後の abort は Flume / FlumeRunning が runClose 経由で駆動する。
286
+ *
263
287
  * subclass のテンプレート:
264
288
  *
265
289
  * ```ts
@@ -282,29 +306,45 @@ var FlumeSerialQueue = class {
282
306
  var FlumeSource = class {
283
307
  consumed = false;
284
308
  stopped = false;
309
+ stopPromise = null;
285
310
  ctx = null;
286
311
  statusEmitter = null;
312
+ abortHandler = null;
287
313
  queue = new FlumeSerialQueue();
288
314
  async start(ctx) {
289
315
  if (this.consumed) return new FlumeStartError(`${this.name}: already started`);
316
+ if (this.stopped) return new FlumeStartError(`${this.name}: already stopped`);
290
317
  this.consumed = true;
291
318
  this.ctx = ctx;
292
319
  this.statusEmitter = new FlumeStatusEmitter({
293
320
  log: ctx.log,
294
321
  onStatus: ctx.onStatus
295
322
  });
296
- return await this.connect(ctx);
323
+ if (this.isSignalAborted(ctx)) return new FlumeStartError(`${this.name}: aborted before connect`);
324
+ this.attachAbortListener(ctx);
325
+ const result = await attempt(async () => await this.connect(ctx));
326
+ this.detachAbortListener(ctx);
327
+ return result instanceof Error ? safeNormalizeError({ value: result }) : result;
297
328
  }
329
+ /**
330
+ * 冪等。`disconnect()` の throw は捕捉して `Error` として返す (公開境界から reject しない)。
331
+ * Flume.runClose / Flume.rollback は戻り値の Error を `flume.close.failed` /
332
+ * `flume.rollback.failed` として firehose に流す
333
+ */
298
334
  async stop() {
299
- if (this.stopped) return;
335
+ if (this.stopPromise !== null) return this.stopPromise;
300
336
  this.stopped = true;
301
- try {
337
+ this.stopPromise = this.runStop();
338
+ return this.stopPromise;
339
+ }
340
+ async runStop() {
341
+ const disconnectResult = await attempt(async () => {
302
342
  await this.disconnect();
303
- } finally {
304
- await this.queue.drain();
305
- this.statusEmitter?.set("disconnected");
306
- this.ctx = null;
307
- }
343
+ });
344
+ await this.queue.drain();
345
+ this.statusEmitter?.set("disconnected");
346
+ this.ctx = null;
347
+ return disconnectResult instanceof Error ? disconnectResult : null;
308
348
  }
309
349
  status() {
310
350
  return this.statusEmitter?.value ?? "disconnected";
@@ -339,6 +379,39 @@ var FlumeSource = class {
339
379
  get context() {
340
380
  return this.ctx;
341
381
  }
382
+ isSignalAborted(ctx) {
383
+ const signal = ctx.signal;
384
+ if (!signal) return false;
385
+ const result = attempt(() => signal.aborted === true);
386
+ return result instanceof Error ? true : result;
387
+ }
388
+ attachAbortListener(ctx) {
389
+ const signal = ctx.signal;
390
+ if (!signal) return;
391
+ const handler = () => {
392
+ attempt(async () => {
393
+ await this.stop();
394
+ });
395
+ };
396
+ const result = attempt(() => signal.addEventListener("abort", handler, { once: true }));
397
+ if (result instanceof Error) {
398
+ ctx.log.warn({
399
+ action: "signal.addListener.failed",
400
+ message: safeErrorMessage({ error: result }),
401
+ error: result
402
+ });
403
+ return;
404
+ }
405
+ this.abortHandler = handler;
406
+ }
407
+ detachAbortListener(ctx) {
408
+ const handler = this.abortHandler;
409
+ if (!handler) return;
410
+ this.abortHandler = null;
411
+ const signal = ctx.signal;
412
+ if (!signal) return;
413
+ attempt(() => signal.removeEventListener("abort", handler));
414
+ }
342
415
  };
343
416
  //#endregion
344
- export { FlumeStartError as a, safeNormalizeError as c, safeNow as i, safeErrorMessage as l, safeInvokeCallback as n, FlumeParseError as o, FlumeLogger as r, attempt as s, FlumeSource as t };
417
+ export { safeNow as a, attempt as c, FlumeLogger as i, safeNormalizeError as l, FlumeSerialQueue as n, FlumeStartError as o, safeInvokeCallback as r, FlumeParseError as s, FlumeSource as t, safeErrorMessage as u };
package/dist/github.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { D as FlumeSourceStartContext, f as FlumeGitHubNotification, p as FlumeGitHubSourceOptions, t as FlumeSource } from "./flume-source.js";
1
+ import { O as FlumeSourceStartContext, m as FlumeGitHubSourceOptions, p as FlumeGitHubNotification, t as FlumeSource } from "./flume-source.js";
2
2
 
3
3
  //#region lib/github/github-source.d.ts
4
4
  declare class FlumeGitHubSource extends FlumeSource {
@@ -8,6 +8,10 @@ declare class FlumeGitHubSource extends FlumeSource {
8
8
  constructor(options: FlumeGitHubSourceOptions);
9
9
  protected connect(ctx: FlumeSourceStartContext): Promise<Error | null>;
10
10
  protected disconnect(): void;
11
+ /**
12
+ * pollInterval が非数値・非有限・0 以下の場合は既定値へフォールバックする
13
+ */
14
+ private getPollIntervalSec;
11
15
  private handleNotifications;
12
16
  private safeExtractMeta;
13
17
  }