@interactive-inc/flume 0.9.4 → 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.
- package/README.md +3 -2
- package/dist/discord.d.ts +15 -1
- package/dist/discord.js +302 -33
- package/dist/flume-source.d.ts +95 -12
- package/dist/flume-source.js +141 -68
- package/dist/github.d.ts +5 -1
- package/dist/github.js +211 -61
- package/dist/http-error.js +4 -0
- package/dist/index.d.ts +138 -23
- package/dist/index.js +358 -72
- package/dist/safe-json-parse.js +1 -1
- package/dist/safe-read-text.js +6 -3
- package/dist/safe-stringify.js +64 -28
- package/dist/slack.d.ts +8 -1
- package/dist/slack.js +227 -30
- package/dist/time.d.ts +46 -4
- package/dist/time.js +342 -40
- package/package.json +1 -1
package/dist/flume-source.d.ts
CHANGED
|
@@ -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
|
|
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,16 @@ type FlumeStreamItem = {
|
|
|
112
120
|
kind: "log";
|
|
113
121
|
log: FlumeLog;
|
|
114
122
|
};
|
|
115
|
-
type FlumeStreamHandler = (item: FlumeStreamItem) =>
|
|
123
|
+
type FlumeStreamHandler = (item: FlumeStreamItem) => unknown | Promise<unknown>;
|
|
124
|
+
/**
|
|
125
|
+
* `FlumeConfluence` が onEvent に渡す item。Flume 単体の `FlumeStreamItem` に
|
|
126
|
+
* `groupId` (`add(id, ...)` で渡したグループ識別子) をスタンプしたもの。
|
|
127
|
+
* 合流ストリームの provenance を呼び出し側で復元する必要がなくなる
|
|
128
|
+
*/
|
|
129
|
+
type FlumeConfluenceItem = FlumeStreamItem & {
|
|
130
|
+
readonly groupId: string;
|
|
131
|
+
};
|
|
132
|
+
type FlumeConfluenceItemHandler = (item: FlumeConfluenceItem) => unknown | Promise<unknown>;
|
|
116
133
|
type FlumeStreamOverflow = "drop-oldest" | "drop-newest";
|
|
117
134
|
type FlumeStreamOptions = {
|
|
118
135
|
/** バッファ上限 (既定 1000)。consumer が遅れて溢れたら onOverflow に従う */buffer?: number; /** バッファ溢れ時の方針 (既定 "drop-oldest") */
|
|
@@ -164,17 +181,28 @@ type FlumeSourceStartContext = {
|
|
|
164
181
|
onStatus?: FlumeSourceLocalStatusHandler;
|
|
165
182
|
reconnect: FlumeReconnectConfig | null;
|
|
166
183
|
/**
|
|
167
|
-
* Flume
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
* source
|
|
171
|
-
* 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 が未設定なら省略される。
|
|
172
189
|
*/
|
|
173
190
|
signal?: AbortSignal;
|
|
174
191
|
};
|
|
175
192
|
type FlumeDiscordSourceOptions = {
|
|
176
193
|
token: string;
|
|
194
|
+
/**
|
|
195
|
+
* Gateway intent ビットフラグ。既定は Guilds | GuildMessages | DirectMessages。
|
|
196
|
+
* message の `content` 本文が必要な場合は privileged intent の `MessageContent` を
|
|
197
|
+
* Developer Portal で有効化した上で明示的に足す (未承認のまま足すと close 4014 で終端する)
|
|
198
|
+
*/
|
|
177
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;
|
|
178
206
|
};
|
|
179
207
|
type FlumeSlackSourceOptions = {
|
|
180
208
|
appToken: string;
|
|
@@ -189,6 +217,12 @@ type FlumeSlackSourceOptions = {
|
|
|
189
217
|
* inbound frame.
|
|
190
218
|
*/
|
|
191
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;
|
|
192
226
|
};
|
|
193
227
|
type FlumeGitHubSourceOptions = {
|
|
194
228
|
token: string;
|
|
@@ -207,9 +241,43 @@ type FlumeTimeMessage = {
|
|
|
207
241
|
data?: Record<string, unknown>;
|
|
208
242
|
meta?: Record<string, string>;
|
|
209
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* 起動 / 終了をまたいだ状態を 1 つ載せる純粋な DI ポート。flume 内部で fs / db / network を
|
|
246
|
+
* 触らないように、I/O の場所と方式は host が決める。load の失敗は null 復帰扱い、save の
|
|
247
|
+
* 失敗は best-effort (source 側で log するが throw しない)
|
|
248
|
+
*/
|
|
249
|
+
type FlumeStatePersister<S> = {
|
|
250
|
+
load(): Promise<S | null>;
|
|
251
|
+
save(state: S): Promise<void>;
|
|
252
|
+
};
|
|
253
|
+
/** `FlumeTimeSource` の statePersister が保存する形 */
|
|
254
|
+
type FlumeTimeSourceState = {
|
|
255
|
+
readonly lastFiredAt: number;
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* `FlumeTimeSource` の起動時 catch-up 方針。statePersister が読めた lastFiredAt から
|
|
259
|
+
* 現在時刻までに過ぎ去った cron マッチを再発火するかどうかを決める。
|
|
260
|
+
* - "off" : 何もしない (既定。後方互換)
|
|
261
|
+
* - "lastOnly" : 直近に過ぎ去ったマッチを 1 件だけ再発火
|
|
262
|
+
* - "missed" : maxWindowMs (既定 24h) 以内のすべての過ぎ去ったマッチを順に再発火
|
|
263
|
+
*/
|
|
264
|
+
type FlumeCatchupPolicy = {
|
|
265
|
+
readonly mode: "off";
|
|
266
|
+
} | {
|
|
267
|
+
readonly mode: "lastOnly";
|
|
268
|
+
} | {
|
|
269
|
+
readonly mode: "missed";
|
|
270
|
+
readonly maxWindowMs?: number;
|
|
271
|
+
};
|
|
210
272
|
type FlumeTimeSourceOptions = {
|
|
211
273
|
/** 5 フィールド cron 式 (minute hour day-of-month month day-of-week)。壁時計 (local time) 基準 */cron: string;
|
|
212
274
|
message?: (tick: FlumeTimeTick) => FlumeTimeMessage;
|
|
275
|
+
/**
|
|
276
|
+
* 起動 / 終了をまたいで `lastFiredAt` を覚えておく口。未指定なら catchup は無効化される
|
|
277
|
+
* (lastFiredAt が分からないので)。flume は fs / db を触らないので host が実装を渡す
|
|
278
|
+
*/
|
|
279
|
+
statePersister?: FlumeStatePersister<FlumeTimeSourceState>; /** statePersister が読めた lastFiredAt を元に過去 tick を再発火する方針 (既定 "off") */
|
|
280
|
+
catchupPolicy?: FlumeCatchupPolicy;
|
|
213
281
|
};
|
|
214
282
|
type FlumeGatewayMessage = z.infer<typeof FlumeGatewayMessageSchema>;
|
|
215
283
|
type FlumeSlackEnvelope = z.infer<typeof FlumeSlackEnvelopeSchema>;
|
|
@@ -224,6 +292,10 @@ type FlumeGitHubNotification = z.infer<typeof FlumeGitHubNotificationSchema>;
|
|
|
224
292
|
* `FlumeSourceStartContext` (handler / log / deps / onStatus / reconnect) を
|
|
225
293
|
* `start()` で受け取り、subclass の `connect(ctx)` に手渡す。
|
|
226
294
|
*
|
|
295
|
+
* `ctx.signal` の購読も base が行う: connect 中に abort されたら `stop()` を発火して
|
|
296
|
+
* 進行中の接続を中断する (subclass の `disconnect()` が pending な connect を解決する契約)。
|
|
297
|
+
* connect 完了後の abort は Flume / FlumeRunning が runClose 経由で駆動する。
|
|
298
|
+
*
|
|
227
299
|
* subclass のテンプレート:
|
|
228
300
|
*
|
|
229
301
|
* ```ts
|
|
@@ -247,11 +319,19 @@ declare abstract class FlumeSource {
|
|
|
247
319
|
abstract readonly name: string;
|
|
248
320
|
private consumed;
|
|
249
321
|
private stopped;
|
|
322
|
+
private stopPromise;
|
|
250
323
|
private ctx;
|
|
251
324
|
private statusEmitter;
|
|
325
|
+
private abortHandler;
|
|
252
326
|
private readonly queue;
|
|
253
327
|
start(ctx: FlumeSourceStartContext): Promise<Error | null>;
|
|
254
|
-
|
|
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;
|
|
255
335
|
status(): FlumeStatus;
|
|
256
336
|
/**
|
|
257
337
|
* subclass が受信した protocol イベントを `FlumeEvent` として handler へ流す。
|
|
@@ -266,10 +346,13 @@ declare abstract class FlumeSource {
|
|
|
266
346
|
protected get currentStatus(): FlumeStatus;
|
|
267
347
|
/** subclass が start ctx を再参照したい場合 (stop 後は null) */
|
|
268
348
|
protected get context(): FlumeSourceStartContext | null;
|
|
349
|
+
private isSignalAborted;
|
|
350
|
+
private attachAbortListener;
|
|
351
|
+
private detachAbortListener;
|
|
269
352
|
/** protocol 接続。subclass 実装 */
|
|
270
353
|
protected abstract connect(ctx: FlumeSourceStartContext): Promise<Error | null>;
|
|
271
354
|
/** protocol 切断。subclass 実装。base が `stop()` 内で必ず呼ぶ */
|
|
272
355
|
protected abstract disconnect(): Promise<void> | void;
|
|
273
356
|
}
|
|
274
357
|
//#endregion
|
|
275
|
-
export {
|
|
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 };
|
package/dist/flume-source.js
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* 任意の値から人が読めるメッセージ文字列を取り出す。
|
|
4
4
|
* `Error.message` getter / `Symbol.toPrimitive` / `toString` / `valueOf` が throw しても固定文字列に fallback。
|
|
5
|
-
*
|
|
5
|
+
* `instanceof` 自体が throw する値 (revoked Proxy 等) にも耐える。自身は決して throw しない
|
|
6
6
|
*/
|
|
7
7
|
function safeErrorMessage(props) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
*
|
|
28
|
+
* `instanceof` 自体が throw する値 (revoked Proxy 等) や Error コンストラクタが throw する
|
|
29
|
+
* 病的環境でも fallback を返し、決して throw しない
|
|
27
30
|
*/
|
|
28
31
|
function safeNormalizeError(props) {
|
|
29
|
-
|
|
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 /
|
|
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
|
|
83
|
-
|
|
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
|
|
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.
|
|
335
|
+
if (this.stopPromise !== null) return this.stopPromise;
|
|
300
336
|
this.stopped = true;
|
|
301
|
-
|
|
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
|
-
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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 {
|
|
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 {
|
|
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
|
}
|