@interactive-inc/flume 0.1.0 → 0.2.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.js CHANGED
@@ -1,66 +1,104 @@
1
- import { a as FlumeSlackEnvelopeSchema, i as FlumeSlackConnectionResponseSchema, n as FlumeGatewayMessageSchema, r as FlumeGitHubNotificationSchema, t as FlumeLogger } from "./logger-B9E8zvgj.js";
2
- import { a as FlumeParseError, i as resolveFlumeReconnectConfig, o as FlumeConnectionError, r as FlumeReconnector } from "./safe-json-parse-D8t_4Vm_.js";
3
- import { i as FlumeHttpError, n as FlumeSlackSocketMode, r as obtainSlackUrl, t as FlumeSlackSource } from "./slack-source-CszepStG.js";
4
- import { a as FlumeDiscordGatewaySession, i as parseDiscordGatewayMessage, n as FlumeDiscordGatewayIntents, o as FlumeDiscordHeartbeat, r as FlumeDiscordGateway, t as FlumeDiscordSource } from "./discord-source-Q4JRIbNs.js";
5
- import { n as FlumeGitHubPoller, r as FlumeGitHubSeenCache, t as FlumeGitHubSource } from "./github-source-D7Z1RUbe.js";
6
- //#region lib/deps.ts
7
- function createFlumeDefaultDeps() {
8
- return {
9
- fetch: (url, init) => globalThis.fetch(url, init),
10
- WebSocket: globalThis.WebSocket,
11
- now: () => Date.now(),
12
- random: () => Math.random(),
13
- setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
14
- clearTimeout: (id) => globalThis.clearTimeout(id),
15
- setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
16
- clearInterval: (id) => globalThis.clearInterval(id)
17
- };
18
- }
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";
4
+ //#region lib/flume-stopped.ts
5
+ /**
6
+ * 停止済みの終端状態。最終ステータスのスナップショットのみ観測できる
7
+ */
8
+ var FlumeStopped = class {
9
+ props;
10
+ constructor(props) {
11
+ this.props = props;
12
+ Object.freeze(this);
13
+ }
14
+ statuses() {
15
+ return this.props.finalStatuses;
16
+ }
17
+ };
19
18
  //#endregion
20
- //#region lib/flume.ts
19
+ //#region lib/flume-running.ts
21
20
  /**
22
- * 共有設定を持つ DI コンテナ。各 Source に共通の deps / logging / reconnect を注入する
21
+ * 稼働中の Flume。stop() FlumeStopped へ遷移する。signal abort されると自動 stop
23
22
  */
24
- var Flume = class {
23
+ var FlumeRunning = class {
25
24
  props;
26
- resolvedDeps;
25
+ stopPromise = null;
26
+ onAbort;
27
27
  constructor(props) {
28
28
  this.props = props;
29
- this.resolvedDeps = {
30
- ...createFlumeDefaultDeps(),
31
- ...props.deps
29
+ this.onAbort = () => {
30
+ this.stop();
32
31
  };
32
+ if (props.signal) props.signal.addEventListener("abort", this.onAbort, { once: true });
33
33
  }
34
- discord(options) {
35
- return new FlumeDiscordSource({
36
- onLog: this.props.onLog,
37
- onStatus: this.props.onStatus,
38
- reconnect: this.props.reconnect,
39
- signal: this.props.signal,
40
- deps: this.resolvedDeps,
41
- ...options
42
- });
34
+ stop() {
35
+ if (this.stopPromise) return this.stopPromise;
36
+ this.stopPromise = this.runStop();
37
+ return this.stopPromise;
43
38
  }
44
- slack(options) {
45
- return new FlumeSlackSource({
46
- onLog: this.props.onLog,
47
- onStatus: this.props.onStatus,
48
- reconnect: this.props.reconnect,
49
- signal: this.props.signal,
50
- deps: this.resolvedDeps,
51
- ...options
52
- });
39
+ statuses() {
40
+ return this.props.sources.map((source) => ({
41
+ name: source.name,
42
+ status: source.status()
43
+ }));
44
+ }
45
+ 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
+ })) });
52
+ }
53
+ };
54
+ //#endregion
55
+ //#region lib/flume.ts
56
+ /**
57
+ * 起動前の Flume。start() で FlumeRunning へ遷移する
58
+ */
59
+ var Flume = class {
60
+ props;
61
+ consumed = false;
62
+ constructor(props) {
63
+ this.props = props;
53
64
  }
54
- github(options) {
55
- return new FlumeGitHubSource({
56
- onLog: this.props.onLog,
57
- onStatus: this.props.onStatus,
58
- reconnect: this.props.reconnect,
59
- signal: this.props.signal,
60
- deps: this.resolvedDeps,
61
- ...options
65
+ 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");
68
+ this.consumed = true;
69
+ const settled = await Promise.allSettled(this.props.sources.map((source) => source.start(handler)));
70
+ const failures = [];
71
+ 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;
76
+ if (result.status === "rejected") {
77
+ const reason = result.reason;
78
+ failures.push({
79
+ name: source.name,
80
+ error: reason instanceof Error ? reason : new Error(String(reason))
81
+ });
82
+ } else if (result.value instanceof Error) failures.push({
83
+ name: source.name,
84
+ error: result.value
85
+ });
86
+ else started.push(source);
87
+ }
88
+ 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}`);
92
+ }
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");
96
+ }
97
+ return new FlumeRunning({
98
+ sources: this.props.sources,
99
+ signal: this.props.signal
62
100
  });
63
101
  }
64
102
  };
65
103
  //#endregion
66
- export { Flume, FlumeConnectionError, FlumeDiscordGateway, FlumeDiscordGatewayIntents, FlumeDiscordGatewaySession, FlumeDiscordHeartbeat, FlumeDiscordSource, FlumeGatewayMessageSchema, FlumeGitHubNotificationSchema, FlumeGitHubPoller, FlumeGitHubSeenCache, FlumeGitHubSource, FlumeHttpError, FlumeLogger, FlumeParseError, FlumeReconnector, FlumeSlackConnectionResponseSchema, FlumeSlackEnvelopeSchema, FlumeSlackSocketMode, FlumeSlackSource, createFlumeDefaultDeps, obtainSlackUrl, parseDiscordGatewayMessage, resolveFlumeReconnectConfig };
104
+ export { Flume, FlumeConnectionError, FlumeHttpError, FlumeLogger, FlumeParseError, FlumeReconnector, FlumeRunning, FlumeStopped, createFlumeDefaultDeps, resolveFlumeReconnectConfig, scheduleFlumeReconnect };
@@ -0,0 +1,49 @@
1
+ //#region lib/deps.ts
2
+ function createFlumeDefaultDeps() {
3
+ return {
4
+ fetch: (url, init) => globalThis.fetch(url, init),
5
+ WebSocket: globalThis.WebSocket,
6
+ now: () => Date.now(),
7
+ random: () => Math.random(),
8
+ setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
9
+ clearTimeout: (id) => globalThis.clearTimeout(id),
10
+ setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
11
+ clearInterval: (id) => globalThis.clearInterval(id)
12
+ };
13
+ }
14
+ //#endregion
15
+ //#region lib/logger.ts
16
+ var FlumeLogger = class {
17
+ props;
18
+ constructor(props) {
19
+ this.props = props;
20
+ Object.freeze(this);
21
+ }
22
+ debug(entry) {
23
+ this.emit("debug", entry);
24
+ }
25
+ info(entry) {
26
+ this.emit("info", entry);
27
+ }
28
+ warn(entry) {
29
+ this.emit("warn", entry);
30
+ }
31
+ error(entry) {
32
+ this.emit("error", entry);
33
+ }
34
+ emit(level, input) {
35
+ if (!this.props.handler) return;
36
+ const log = {
37
+ level,
38
+ source: this.props.source,
39
+ action: input.action,
40
+ message: input.message,
41
+ timestamp: this.props.deps.now(),
42
+ error: input.error,
43
+ detail: input.detail
44
+ };
45
+ this.props.handler(log);
46
+ }
47
+ };
48
+ //#endregion
49
+ export { createFlumeDefaultDeps as n, FlumeLogger as t };
@@ -0,0 +1,6 @@
1
+ //#region lib/errors/parse-error.d.ts
2
+ declare class FlumeParseError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ //#endregion
6
+ export { FlumeParseError as t };
@@ -0,0 +1,20 @@
1
+ //#region lib/utils/safe-fetch.ts
2
+ /**
3
+ * deps.fetch を try/catch で包み、ネットワーク例外を Error として返す。
4
+ * HTTP ステータスの解釈や追加の副作用 (リトライ計数等) は呼び出し側の責務
5
+ */
6
+ async function safeFetch(props) {
7
+ try {
8
+ return await props.fetch(props.url, props.init);
9
+ } catch (error) {
10
+ const err = error instanceof Error ? error : new Error(String(error));
11
+ props.log.error({
12
+ action: "http.error",
13
+ message: `network error: ${err.message}`,
14
+ error: err
15
+ });
16
+ return err;
17
+ }
18
+ }
19
+ //#endregion
20
+ export { safeFetch as t };
@@ -0,0 +1,15 @@
1
+ //#region lib/utils/is-record.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+ //#endregion
6
+ //#region lib/utils/safe-json-parse.ts
7
+ function safeJsonParse(raw) {
8
+ try {
9
+ return JSON.parse(raw);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
14
+ //#endregion
15
+ export { isRecord as n, safeJsonParse as t };
@@ -64,18 +64,30 @@ var FlumeReconnector = class {
64
64
  }
65
65
  };
66
66
  //#endregion
67
- //#region lib/utils/is-record.ts
68
- function isRecord(value) {
69
- return typeof value === "object" && value !== null;
70
- }
71
- //#endregion
72
- //#region lib/utils/safe-json-parse.ts
73
- function safeJsonParse(raw) {
74
- try {
75
- return JSON.parse(raw);
76
- } catch {
77
- return null;
67
+ //#region lib/schedule-reconnect.ts
68
+ /**
69
+ * 接続が落ちた際の共通再接続スケジューラ。reconnector の状態を見て次回試行を予約し、
70
+ * 試行回数が尽きていれば disconnected に落とす
71
+ */
72
+ function scheduleFlumeReconnect(props) {
73
+ if (!props.reconnector || props.reconnector.aborted) {
74
+ props.setStatus("disconnected");
75
+ return;
76
+ }
77
+ props.setStatus("reconnecting");
78
+ const delay = props.reconnector.schedule(props.retry);
79
+ if (delay === -1) {
80
+ props.log.error({
81
+ action: "reconnect.exhausted",
82
+ message: `gave up after ${props.reconnector.attempt} attempts`
83
+ });
84
+ props.setStatus("disconnected");
85
+ return;
78
86
  }
87
+ props.log.info({
88
+ action: "reconnect.scheduled",
89
+ message: `next attempt in ${Math.round(delay)}ms`
90
+ });
79
91
  }
80
92
  //#endregion
81
- export { FlumeParseError as a, resolveFlumeReconnectConfig as i, isRecord as n, FlumeConnectionError as o, FlumeReconnector as r, safeJsonParse as t };
93
+ export { FlumeConnectionError as a, FlumeParseError as i, FlumeReconnector as n, resolveFlumeReconnectConfig as r, scheduleFlumeReconnect as t };
@@ -0,0 +1,16 @@
1
+ //#region lib/utils/serial-queue.ts
2
+ /**
3
+ * 投入順を保ったまま task を直列実行する。各 task は前の完了を待ってから走る。
4
+ * task が throw しても後続には伝播しない (キュー自体は止まらない)
5
+ */
6
+ var FlumeSerialQueue = class {
7
+ chain = Promise.resolve();
8
+ add(task) {
9
+ this.chain = this.chain.then(task).catch(() => {});
10
+ }
11
+ async drain() {
12
+ await this.chain;
13
+ }
14
+ };
15
+ //#endregion
16
+ export { FlumeSerialQueue as t };
package/dist/slack.d.ts CHANGED
@@ -1,23 +1,84 @@
1
- import { g as FlumeSlackSourceOptions, h as FlumeSlackEnvelope, o as FlumeHandler, y as FlumeStatus } from "./types-BVQSU336.js";
1
+ import { T as FlumeSlackConnectionResponseSchema, c as FlumeLogHandler, g as FlumeSlackSourceOptions, h as FlumeSlackEnvelope, o as FlumeHandler, p as FlumeRuntimeDeps, w as FlumeSlackEnvelopeSchema, x as FlumeStatus } from "./types-tnOPBc1p.js";
2
+ import { t as FlumeConnectionError } from "./connection-error-BOk97djj.js";
3
+ import { t as FlumeHttpError } from "./http-error-K-Ym4lfK.js";
2
4
 
3
5
  //#region lib/slack/slack-source.d.ts
4
6
  declare class FlumeSlackSource {
5
7
  private readonly options;
8
+ readonly name: "slack";
6
9
  private socket;
7
10
  private reconnector;
8
11
  private handler;
9
12
  private currentStatus;
10
13
  private readonly log;
11
14
  private readonly deps;
15
+ private readonly queue;
16
+ private readonly seen;
12
17
  constructor(options: FlumeSlackSourceOptions);
13
- start(handler: FlumeHandler): Promise<void>;
18
+ start(handler: FlumeHandler): Promise<void | Error>;
14
19
  stop(): Promise<void>;
15
20
  status(): FlumeStatus;
16
21
  private connectInternal;
17
22
  private handleMessage;
18
23
  private scheduleReconnect;
19
24
  private setStatus;
20
- static extractMeta(envelope: FlumeSlackEnvelope): Record<string, string>;
21
25
  }
22
26
  //#endregion
23
- export { FlumeSlackSource };
27
+ //#region lib/slack/extract-slack-meta.d.ts
28
+ declare function extractSlackMeta(envelope: FlumeSlackEnvelope): Record<string, string>;
29
+ //#endregion
30
+ //#region lib/slack/slack-socket-mode.d.ts
31
+ type Deps = Pick<FlumeRuntimeDeps, "WebSocket" | "fetch" | "now">;
32
+ type Props$2 = {
33
+ appToken: string;
34
+ onMessage: (envelope: FlumeSlackEnvelope) => void;
35
+ onConnected: () => void;
36
+ onDisconnected: () => void;
37
+ onLog?: FlumeLogHandler;
38
+ deps: Deps;
39
+ };
40
+ declare class FlumeSlackSocketMode {
41
+ private readonly props;
42
+ private readonly log;
43
+ private ws;
44
+ stopped: boolean;
45
+ private pendingResolve;
46
+ private pendingResolved;
47
+ constructor(props: Props$2);
48
+ connect(): Promise<FlumeConnectionError | FlumeHttpError | null>;
49
+ disconnect(): void;
50
+ isConnected(): boolean;
51
+ private openSocket;
52
+ private completeConnect;
53
+ private onMessage;
54
+ private onClose;
55
+ private onError;
56
+ }
57
+ //#endregion
58
+ //#region lib/slack/slack-seen-cache.d.ts
59
+ type Props$1 = {
60
+ maxSize: number;
61
+ };
62
+ /**
63
+ * Slack envelope_id の LRU 風キャッシュ。Slack は ack 失敗時に同じ envelope を再送するため、
64
+ * source レイヤで handler への重複配送を防ぐ
65
+ */
66
+ declare class FlumeSlackSeenCache {
67
+ private readonly props;
68
+ private seen;
69
+ constructor(props: Props$1);
70
+ has(envelopeId: string): boolean;
71
+ add(envelopeId: string): void;
72
+ trim(): void;
73
+ get size(): number;
74
+ }
75
+ //#endregion
76
+ //#region lib/slack/obtain-slack-url.d.ts
77
+ type Props = {
78
+ appToken: string;
79
+ onLog?: FlumeLogHandler;
80
+ deps: Pick<FlumeRuntimeDeps, "fetch" | "now">;
81
+ };
82
+ declare function obtainSlackUrl(props: Props): Promise<string | FlumeHttpError>;
83
+ //#endregion
84
+ export { FlumeSlackConnectionResponseSchema, FlumeSlackEnvelopeSchema, FlumeSlackSeenCache, FlumeSlackSocketMode, FlumeSlackSource, extractSlackMeta, obtainSlackUrl };