@interactive-inc/flume 0.10.0 → 0.11.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/README.md +93 -32
- package/dist/discord.d.ts +15 -1
- package/dist/discord.js +304 -35
- package/dist/flume-source.d.ts +58 -14
- package/dist/flume-source.js +142 -87
- package/dist/github.d.ts +5 -1
- package/dist/github.js +217 -69
- package/dist/index.d.ts +223 -46
- package/dist/index.js +466 -149
- package/dist/parse-error.d.ts +9 -0
- package/dist/safe-json-parse.js +1 -1
- package/dist/safe-read-text.js +22 -5
- package/dist/safe-stringify.js +34 -134
- package/dist/schedule-reconnect.js +153 -0
- package/dist/slack.d.ts +8 -1
- package/dist/slack.js +230 -34
- package/dist/time.d.ts +81 -2
- package/dist/time.js +625 -2
- package/package.json +11 -4
- package/dist/connection-error.js +0 -16
- package/dist/http-error.js +0 -12
- package/dist/parse-cron.d.ts +0 -55
- package/dist/time-source.js +0 -427
package/dist/index.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
1
|
+
import { a as safeInvokeCallback, c as FlumeParseError, d as safeErrorMessage, i as safeNow, l as attempt, n as FlumeSerialQueue, o as FlumeSourceReuseError, r as FlumeLogger, s as FlumeStartError, t as FlumeSource, u as safeNormalizeError } from "./flume-source.js";
|
|
2
|
+
import { n as safeRandom, r as FlumeConnectionError, t as safeStringify } from "./safe-stringify.js";
|
|
3
|
+
import { n as FlumeHttpError, t as safeReadText } from "./safe-read-text.js";
|
|
4
|
+
import { t as isRecord } from "./is-record.js";
|
|
5
|
+
import { t as safeJsonParse } from "./safe-json-parse.js";
|
|
5
6
|
//#region lib/deps.ts
|
|
6
7
|
/**
|
|
7
8
|
* `globalThis.WebSocket` の現在の値を返す。
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* `beforeEach`
|
|
9
|
+
* `createFlumeDefaultDeps()` が返す deps の `WebSocket` は getter でここへ委譲するため、
|
|
10
|
+
* `new Flume()` 後に `globalThis.WebSocket` を差し込む (jsdom / happy-dom / vitest の
|
|
11
|
+
* `beforeEach` パッチ) テスト戦略がそのまま機能する — 参照は常にアクセス時点の最新値。
|
|
11
12
|
* `WebSocket` が無い環境 (Node の素の global など) では `null` を返す。
|
|
12
13
|
*/
|
|
13
14
|
function resolveCurrentWebSocket() {
|
|
@@ -28,7 +29,9 @@ function resolveCurrentWebSocket() {
|
|
|
28
29
|
function createFlumeDefaultDeps() {
|
|
29
30
|
return {
|
|
30
31
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
31
|
-
WebSocket
|
|
32
|
+
get WebSocket() {
|
|
33
|
+
return resolveCurrentWebSocket();
|
|
34
|
+
},
|
|
32
35
|
now: () => Date.now(),
|
|
33
36
|
random: () => Math.random(),
|
|
34
37
|
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
@@ -39,20 +42,25 @@ function createFlumeDefaultDeps() {
|
|
|
39
42
|
}
|
|
40
43
|
//#endregion
|
|
41
44
|
//#region lib/flume-stream.ts
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
function doneResult() {
|
|
46
|
+
return {
|
|
47
|
+
value: void 0,
|
|
48
|
+
done: true
|
|
49
|
+
};
|
|
50
|
+
}
|
|
46
51
|
/**
|
|
47
52
|
* push (`FlumeStreamHub.publish`) を pull (`for await`) に変換する async iterator。
|
|
48
53
|
* consumer が待っていれば即 resolve、いなければ buffer に積み、溢れたら onOverflow に従う。
|
|
49
|
-
*
|
|
54
|
+
* hub.close() ではバッファ済み item を吐き切ってから done へ落ちる (graceful tail drain)。
|
|
55
|
+
* consumer 側の `return()` (break / 例外) はバッファを破棄して即 done になる (iterator 仕様)。
|
|
56
|
+
* drop の観測は onDrop 経由 — drop 通知自体が firehose に還流して再帰しないよう初回のみ発火
|
|
50
57
|
*/
|
|
51
58
|
var FlumeStream = class {
|
|
52
59
|
props;
|
|
53
60
|
items = [];
|
|
54
61
|
resolvers = [];
|
|
55
62
|
closed = false;
|
|
63
|
+
droppedCount = 0;
|
|
56
64
|
constructor(props) {
|
|
57
65
|
this.props = props;
|
|
58
66
|
}
|
|
@@ -67,6 +75,7 @@ var FlumeStream = class {
|
|
|
67
75
|
return;
|
|
68
76
|
}
|
|
69
77
|
if (this.items.length >= this.props.buffer) {
|
|
78
|
+
this.recordDrop();
|
|
70
79
|
if (this.props.onOverflow === "drop-newest") return;
|
|
71
80
|
this.items.shift();
|
|
72
81
|
}
|
|
@@ -77,7 +86,7 @@ var FlumeStream = class {
|
|
|
77
86
|
this.closed = true;
|
|
78
87
|
while (this.resolvers.length > 0) {
|
|
79
88
|
const resolver = this.resolvers.shift();
|
|
80
|
-
if (resolver) resolver(
|
|
89
|
+
if (resolver) resolver(doneResult());
|
|
81
90
|
}
|
|
82
91
|
}
|
|
83
92
|
next() {
|
|
@@ -86,42 +95,90 @@ var FlumeStream = class {
|
|
|
86
95
|
value: item,
|
|
87
96
|
done: false
|
|
88
97
|
});
|
|
89
|
-
if (this.closed) return Promise.resolve(
|
|
98
|
+
if (this.closed) return Promise.resolve(doneResult());
|
|
90
99
|
return new Promise((resolve) => this.resolvers.push(resolve));
|
|
91
100
|
}
|
|
92
101
|
return() {
|
|
102
|
+
this.items.splice(0);
|
|
103
|
+
this.close();
|
|
104
|
+
this.props.onClose();
|
|
105
|
+
return Promise.resolve(doneResult());
|
|
106
|
+
}
|
|
107
|
+
throw(error) {
|
|
108
|
+
this.items.splice(0);
|
|
93
109
|
this.close();
|
|
94
110
|
this.props.onClose();
|
|
95
|
-
return Promise.
|
|
111
|
+
return Promise.reject(error);
|
|
96
112
|
}
|
|
97
113
|
[Symbol.asyncIterator]() {
|
|
98
114
|
return this;
|
|
99
115
|
}
|
|
116
|
+
recordDrop() {
|
|
117
|
+
this.droppedCount++;
|
|
118
|
+
if (this.droppedCount > 1) return;
|
|
119
|
+
this.props.onDrop?.({ dropped: 1 });
|
|
120
|
+
}
|
|
100
121
|
};
|
|
101
122
|
//#endregion
|
|
102
123
|
//#region lib/flume-stream-hub.ts
|
|
103
124
|
const DEFAULT_BUFFER = 1e3;
|
|
104
125
|
/**
|
|
126
|
+
* NaN / Infinity / 0 以下を弾いて必ず 1 以上の有限整数にする。
|
|
127
|
+
* 不正値で backpressure 上限が実質無効化される (比較が常に false → 無制限成長) のを防ぐ
|
|
128
|
+
*/
|
|
129
|
+
function sanitizeBufferSize(value) {
|
|
130
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_BUFFER;
|
|
131
|
+
if (value < 1) return 1;
|
|
132
|
+
return Math.floor(value);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
105
135
|
* firehose (`onEvent` / `stream()`) の item を複数の pull consumer へ fan-out する内部ハブ。
|
|
106
136
|
* subscriber が居なければ publish は実質 no-op。Flume 停止時に close() で全 stream を終端する
|
|
107
137
|
*/
|
|
108
138
|
var FlumeStreamHub = class {
|
|
139
|
+
props;
|
|
109
140
|
streams = /* @__PURE__ */ new Set();
|
|
141
|
+
startupItems = [];
|
|
142
|
+
startupDropNotified = false;
|
|
143
|
+
hasSubscribed = false;
|
|
110
144
|
closed = false;
|
|
145
|
+
constructor(props = {}) {
|
|
146
|
+
this.props = props;
|
|
147
|
+
}
|
|
148
|
+
get isClosed() {
|
|
149
|
+
return this.closed;
|
|
150
|
+
}
|
|
111
151
|
publish(item) {
|
|
112
152
|
if (this.closed) return;
|
|
153
|
+
if (!this.hasSubscribed) {
|
|
154
|
+
if (this.startupItems.length >= DEFAULT_BUFFER) {
|
|
155
|
+
this.startupItems.shift();
|
|
156
|
+
if (!this.startupDropNotified) {
|
|
157
|
+
this.startupDropNotified = true;
|
|
158
|
+
this.props.onDrop?.({ dropped: 1 });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.startupItems.push(item);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
113
164
|
for (const stream of this.streams) stream.push(item);
|
|
114
165
|
}
|
|
115
166
|
subscribe(options) {
|
|
116
167
|
const stream = new FlumeStream({
|
|
117
|
-
buffer: options?.buffer
|
|
168
|
+
buffer: sanitizeBufferSize(options?.buffer),
|
|
118
169
|
onOverflow: options?.onOverflow ?? "drop-oldest",
|
|
119
|
-
onClose: () => this.streams.delete(stream)
|
|
170
|
+
onClose: () => this.streams.delete(stream),
|
|
171
|
+
onDrop: this.props.onDrop
|
|
120
172
|
});
|
|
121
173
|
if (this.closed) {
|
|
122
174
|
stream.close();
|
|
123
175
|
return stream;
|
|
124
176
|
}
|
|
177
|
+
if (!this.hasSubscribed) {
|
|
178
|
+
this.hasSubscribed = true;
|
|
179
|
+
for (const item of this.startupItems) stream.push(item);
|
|
180
|
+
this.startupItems.splice(0);
|
|
181
|
+
}
|
|
125
182
|
this.streams.add(stream);
|
|
126
183
|
return stream;
|
|
127
184
|
}
|
|
@@ -130,6 +187,7 @@ var FlumeStreamHub = class {
|
|
|
130
187
|
this.closed = true;
|
|
131
188
|
for (const stream of this.streams) stream.close();
|
|
132
189
|
this.streams.clear();
|
|
190
|
+
this.startupItems.splice(0);
|
|
133
191
|
}
|
|
134
192
|
};
|
|
135
193
|
//#endregion
|
|
@@ -140,14 +198,16 @@ var FlumeStreamHub = class {
|
|
|
140
198
|
* きれいに close したか / どれが失敗したか」を直接判定できる
|
|
141
199
|
*/
|
|
142
200
|
var FlumeClosed = class {
|
|
143
|
-
props;
|
|
144
201
|
kind = "closed";
|
|
202
|
+
finalStatuses;
|
|
203
|
+
closeErrors;
|
|
145
204
|
constructor(props) {
|
|
146
|
-
this.
|
|
205
|
+
this.finalStatuses = Object.freeze(props.finalStatuses.map((status) => Object.freeze({ ...status })));
|
|
206
|
+
this.closeErrors = Object.freeze(props.closeErrors.map((closeError) => Object.freeze({ ...closeError })));
|
|
147
207
|
Object.freeze(this);
|
|
148
208
|
}
|
|
149
209
|
statuses() {
|
|
150
|
-
return this.
|
|
210
|
+
return this.finalStatuses;
|
|
151
211
|
}
|
|
152
212
|
/**
|
|
153
213
|
* `runClose` 中に `source.stop()` が rejected で settle した source の名前と
|
|
@@ -155,7 +215,7 @@ var FlumeClosed = class {
|
|
|
155
215
|
* 全 source が clean close した場合は空配列。
|
|
156
216
|
*/
|
|
157
217
|
errors() {
|
|
158
|
-
return this.
|
|
218
|
+
return this.closeErrors;
|
|
159
219
|
}
|
|
160
220
|
};
|
|
161
221
|
//#endregion
|
|
@@ -163,7 +223,7 @@ var FlumeClosed = class {
|
|
|
163
223
|
/**
|
|
164
224
|
* 稼働中の Flume。close() で FlumeClosed へ遷移する。signal が abort されると自動 close。
|
|
165
225
|
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
166
|
-
* `runClose`
|
|
226
|
+
* `runClose` の最外殻でも `attempt` を通して想定外の throw を `FlumeClosed` の resolve に変換する
|
|
167
227
|
*/
|
|
168
228
|
var FlumeRunning = class {
|
|
169
229
|
props;
|
|
@@ -201,88 +261,103 @@ var FlumeRunning = class {
|
|
|
201
261
|
}
|
|
202
262
|
}
|
|
203
263
|
}
|
|
264
|
+
/** Source の停止まで待つ。callback の完了は callback 外から drain() で待つ。 */
|
|
204
265
|
close() {
|
|
205
266
|
if (this.closePromise) return this.closePromise;
|
|
206
267
|
this.closePromise = this.runClose();
|
|
207
268
|
return this.closePromise;
|
|
208
269
|
}
|
|
270
|
+
/**
|
|
271
|
+
* 配送済み callback とその失敗診断が完了するまで待つ。通常は close() の後に呼ぶ。
|
|
272
|
+
* onEvent / onError 内では自身の完了待ちになるため呼ばない。
|
|
273
|
+
*/
|
|
274
|
+
async drain() {
|
|
275
|
+
if (this.closePromise) await this.closePromise;
|
|
276
|
+
await this.props.callbackQueue.drain();
|
|
277
|
+
}
|
|
209
278
|
statuses() {
|
|
210
279
|
return this.snapshotStatuses();
|
|
211
280
|
}
|
|
212
281
|
/**
|
|
213
282
|
* 統合 firehose を pull で受け取る async iterator。`for await (const item of running.stream())`。
|
|
214
283
|
* item は events + 全ログの union (`FlumeStreamItem`)。`item.kind` で判別する。
|
|
215
|
-
* close() / signal abort
|
|
284
|
+
* close() / signal abort 後、callback の失敗診断まで配送して終了する。
|
|
285
|
+
* `break` すると hub から自動 unsubscribe する。
|
|
216
286
|
* consumer が遅れて buffer を超えたら `onOverflow` (既定 drop-oldest) に従う
|
|
217
287
|
*/
|
|
218
288
|
stream(options) {
|
|
219
289
|
return this.props.hub.subscribe(options);
|
|
220
290
|
}
|
|
221
291
|
/**
|
|
222
|
-
*
|
|
292
|
+
* `Flume({ signal })` に渡された AbortSignal をそのまま公開する。
|
|
223
293
|
* 直接の controller を持っていない呼び出し元が `running.signal?.aborted`
|
|
224
|
-
* で abort
|
|
294
|
+
* で abort 状態を確認できる。`FlumeConfluence` 経由で開かれたグループでは
|
|
295
|
+
* host の signal ではなく confluence 内部の timeout controller の signal になる点に注意
|
|
225
296
|
*/
|
|
226
297
|
get signal() {
|
|
227
298
|
return this.props.signal;
|
|
228
299
|
}
|
|
229
300
|
async runClose() {
|
|
230
301
|
const closeErrors = [];
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
message: `closing ${this.props.sources.length} source(s)`
|
|
235
|
-
});
|
|
236
|
-
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
237
|
-
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
238
|
-
const source = this.props.sources[index];
|
|
239
|
-
const name = source ? this.sourceName(source) : "?";
|
|
240
|
-
const error = safeNormalizeError({ value: result.reason });
|
|
241
|
-
closeErrors.push({
|
|
242
|
-
source: name,
|
|
243
|
-
error
|
|
244
|
-
});
|
|
245
|
-
this.props.log.error({
|
|
246
|
-
action: "flume.close.failed",
|
|
247
|
-
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
248
|
-
error,
|
|
249
|
-
detail: { source: name }
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
const signal = this.props.signal;
|
|
253
|
-
if (signal) {
|
|
254
|
-
const result = attempt(() => signal.removeEventListener("abort", this.onAbort));
|
|
255
|
-
if (result instanceof Error) {
|
|
256
|
-
const error = safeNormalizeError({ value: result });
|
|
257
|
-
this.props.log.error({
|
|
258
|
-
action: "signal.removeListener.failed",
|
|
259
|
-
message: safeErrorMessage({ error }),
|
|
260
|
-
error
|
|
261
|
-
});
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
this.props.log.info({
|
|
265
|
-
action: "flume.close.complete",
|
|
266
|
-
message: "all sources closed"
|
|
267
|
-
});
|
|
268
|
-
this.props.hub.close();
|
|
269
|
-
return new FlumeClosed({
|
|
270
|
-
finalStatuses: this.snapshotStatuses(),
|
|
271
|
-
closeErrors
|
|
272
|
-
});
|
|
273
|
-
} catch (err) {
|
|
274
|
-
const error = safeNormalizeError({ value: err });
|
|
302
|
+
const result = await attempt(() => this.closeSources(closeErrors));
|
|
303
|
+
if (result instanceof Error) {
|
|
304
|
+
const error = safeNormalizeError({ value: result });
|
|
275
305
|
this.props.log.error({
|
|
276
306
|
action: "flume.close.unhandled",
|
|
277
307
|
message: safeErrorMessage({ error }),
|
|
278
308
|
error
|
|
279
309
|
});
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
310
|
+
}
|
|
311
|
+
const sealed = attempt(() => this.props.seal());
|
|
312
|
+
if (sealed instanceof Error) this.props.log.error({
|
|
313
|
+
action: "flume.close.seal.failed",
|
|
314
|
+
message: safeErrorMessage({ error: sealed }),
|
|
315
|
+
error: sealed
|
|
316
|
+
});
|
|
317
|
+
this.props.callbackQueue.drain().then(() => this.props.hub.close());
|
|
318
|
+
return new FlumeClosed({
|
|
319
|
+
finalStatuses: this.snapshotStatuses(),
|
|
320
|
+
closeErrors
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async closeSources(closeErrors) {
|
|
324
|
+
this.props.log.info({
|
|
325
|
+
action: "flume.close",
|
|
326
|
+
message: `closing ${this.props.sources.length} source(s)`
|
|
327
|
+
});
|
|
328
|
+
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
329
|
+
for (const [index, result] of settled.entries()) {
|
|
330
|
+
const source = this.props.sources[index];
|
|
331
|
+
const name = source ? this.sourceName(source) : "?";
|
|
332
|
+
const error = result.status === "rejected" ? safeNormalizeError({ value: result.reason }) : result.value instanceof Error ? result.value : null;
|
|
333
|
+
if (error === null) continue;
|
|
334
|
+
closeErrors.push({
|
|
335
|
+
source: name,
|
|
336
|
+
error
|
|
337
|
+
});
|
|
338
|
+
this.props.log.error({
|
|
339
|
+
action: "flume.close.failed",
|
|
340
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
341
|
+
error,
|
|
342
|
+
detail: { source: name }
|
|
284
343
|
});
|
|
285
344
|
}
|
|
345
|
+
const signal = this.props.signal;
|
|
346
|
+
if (signal) {
|
|
347
|
+
const result = attempt(() => signal.removeEventListener("abort", this.onAbort));
|
|
348
|
+
if (result instanceof Error) {
|
|
349
|
+
const error = safeNormalizeError({ value: result });
|
|
350
|
+
this.props.log.error({
|
|
351
|
+
action: "signal.removeListener.failed",
|
|
352
|
+
message: safeErrorMessage({ error }),
|
|
353
|
+
error
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
this.props.log.info({
|
|
358
|
+
action: "flume.close.complete",
|
|
359
|
+
message: "all sources closed"
|
|
360
|
+
});
|
|
286
361
|
}
|
|
287
362
|
snapshotStatuses() {
|
|
288
363
|
return this.props.sources.map((source) => {
|
|
@@ -321,12 +396,42 @@ const DEFAULTS = {
|
|
|
321
396
|
baseDelay: 1e3,
|
|
322
397
|
maxDelay: 3e4
|
|
323
398
|
};
|
|
399
|
+
/**
|
|
400
|
+
* `baseDelay` として受理できる値のみ透過する。NaN / Infinity / 1ms 未満 /
|
|
401
|
+
* 非数値 / 明示的 `undefined` (spread でデフォルトを潰すケース) は既定値へ落とす。
|
|
402
|
+
* 0 や負値を許すと `maxAttempts: Infinity` と組み合わさって 0ms 再接続ホットループになる
|
|
403
|
+
*/
|
|
404
|
+
function sanitizeBaseDelay(value) {
|
|
405
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULTS.baseDelay;
|
|
406
|
+
if (value < 1) return DEFAULTS.baseDelay;
|
|
407
|
+
return value;
|
|
408
|
+
}
|
|
409
|
+
function sanitizeMaxDelay(value, baseDelay) {
|
|
410
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return Math.max(DEFAULTS.maxDelay, baseDelay);
|
|
411
|
+
if (value < baseDelay) return baseDelay;
|
|
412
|
+
return value;
|
|
413
|
+
}
|
|
414
|
+
/** 1 以上の整数 or Infinity のみ透過。NaN / 0 / 負値 / 小数は既定 (Infinity) へ */
|
|
415
|
+
function sanitizeMaxAttempts(value) {
|
|
416
|
+
if (typeof value !== "number" || Number.isNaN(value)) return DEFAULTS.maxAttempts;
|
|
417
|
+
if (value === Infinity) return Infinity;
|
|
418
|
+
if (!Number.isInteger(value) || value < 1) return DEFAULTS.maxAttempts;
|
|
419
|
+
return value;
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* ユーザー入力の reconnect 指定を検証済み `FlumeReconnectConfig` へ解決する。
|
|
423
|
+
* `false` / `undefined` は再接続無効 (null)。`true` は既定値。
|
|
424
|
+
* オブジェクトはフィールドごとに検証し、不正値 (NaN / 負値 / Infinity delay 等) は
|
|
425
|
+
* 既定値へフォールバックする — throw しない
|
|
426
|
+
*/
|
|
324
427
|
function resolveFlumeReconnectConfig(input) {
|
|
325
428
|
if (input === false || input === void 0) return null;
|
|
326
429
|
if (input === true) return { ...DEFAULTS };
|
|
430
|
+
const baseDelay = sanitizeBaseDelay(input.baseDelay);
|
|
327
431
|
return {
|
|
328
|
-
|
|
329
|
-
|
|
432
|
+
maxAttempts: sanitizeMaxAttempts(input.maxAttempts),
|
|
433
|
+
baseDelay,
|
|
434
|
+
maxDelay: sanitizeMaxDelay(input.maxDelay, baseDelay)
|
|
330
435
|
};
|
|
331
436
|
}
|
|
332
437
|
//#endregion
|
|
@@ -335,59 +440,152 @@ function resolveFlumeReconnectConfig(input) {
|
|
|
335
440
|
* 起動前の Flume。`open()` で `FlumeRunning` へ遷移する。
|
|
336
441
|
* コンストラクタは単一オブジェクト `{ sources, ...options }` を受け取る (`sources` のみ必須)。
|
|
337
442
|
* events も全ログも 1 本の firehose (`onEvent` push / `stream()` pull) に流れ、購読側が filter する。
|
|
338
|
-
*
|
|
339
|
-
*
|
|
443
|
+
* 起動失敗時はこの open が取得した source を `stop()` してロールバックする。
|
|
444
|
+
* 半接続状態で失敗した source も含むが、再利用を拒否した source は他の所有者のため停止しない。
|
|
340
445
|
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
341
446
|
* Promise rejection に正規化して `allSettled` で捕捉する (`open()` は決して reject しない)
|
|
342
447
|
*/
|
|
343
448
|
var Flume = class {
|
|
344
449
|
options;
|
|
345
450
|
consumed = false;
|
|
451
|
+
isAcceptingItems = true;
|
|
346
452
|
log;
|
|
347
453
|
deps;
|
|
348
454
|
sources;
|
|
349
455
|
sourceEventHandler;
|
|
350
|
-
hub
|
|
456
|
+
hub;
|
|
457
|
+
callbackQueue = new FlumeSerialQueue();
|
|
351
458
|
constructor(options) {
|
|
352
459
|
this.options = options;
|
|
353
|
-
this.sources = options.sources;
|
|
460
|
+
this.sources = [...options.sources];
|
|
354
461
|
this.deps = options.deps ?? createFlumeDefaultDeps();
|
|
462
|
+
this.hub = new FlumeStreamHub({ onDrop: () => this.notifyStreamOverflow() });
|
|
355
463
|
this.log = new FlumeLogger({
|
|
356
464
|
source: "flume",
|
|
357
465
|
handler: this.buildLogHandler(),
|
|
358
466
|
deps: this.deps
|
|
359
467
|
});
|
|
360
|
-
this.sourceEventHandler = (event) =>
|
|
361
|
-
|
|
362
|
-
|
|
468
|
+
this.sourceEventHandler = (event) => {
|
|
469
|
+
this.emitItem({
|
|
470
|
+
kind: "event",
|
|
471
|
+
event
|
|
472
|
+
});
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* stream の buffer 溢れ通知 (stream ごとに初回 1 回)。
|
|
477
|
+
* firehose (hub) には流さない — 溢れている stream 自身に還流して実イベントを
|
|
478
|
+
* さらに追い出す自己破壊になるため、push の `onEvent` にだけ warn log として届ける
|
|
479
|
+
*/
|
|
480
|
+
notifyStreamOverflow() {
|
|
481
|
+
const log = {
|
|
482
|
+
level: "warn",
|
|
483
|
+
source: "flume",
|
|
484
|
+
action: "stream.overflow",
|
|
485
|
+
message: "stream buffer overflowed, dropping items (notified once per stream; see FlumeStreamOptions.buffer)",
|
|
486
|
+
timestamp: safeNow({ deps: this.deps })
|
|
487
|
+
};
|
|
488
|
+
this.enqueueCallback({
|
|
489
|
+
kind: "log",
|
|
490
|
+
log
|
|
363
491
|
});
|
|
364
492
|
}
|
|
365
493
|
/** source が受信したログを firehose へ流す handler。error は onError にも分岐する */
|
|
366
494
|
buildLogHandler() {
|
|
367
495
|
return (log) => {
|
|
496
|
+
if (!this.isAcceptingItems) return;
|
|
368
497
|
this.emitItem({
|
|
369
498
|
kind: "log",
|
|
370
499
|
log
|
|
371
500
|
});
|
|
372
|
-
|
|
373
|
-
if (!onError || log.level !== "error") return;
|
|
374
|
-
try {
|
|
375
|
-
Promise.resolve(onError(log)).catch(() => {});
|
|
376
|
-
} catch {}
|
|
501
|
+
if (log.level === "error") this.invokeOnError(log);
|
|
377
502
|
};
|
|
378
503
|
}
|
|
379
504
|
/**
|
|
380
505
|
* firehose の単一 sink: pull の hub と push の onEvent の両方へ item を配る。
|
|
381
|
-
*
|
|
382
|
-
*
|
|
506
|
+
* close 後の遅延 emit (stop 中の straggler) は push 側にも流さない (pull 側と対称にする)。
|
|
507
|
+
* onEvent への転送は this.log を経由しない (経由すると log item 経路で再帰する)。
|
|
508
|
+
* callback failure は reportCallbackFailure が pull hub と peer callback へ直接診断する。
|
|
383
509
|
*/
|
|
384
510
|
emitItem(item) {
|
|
511
|
+
if (!this.isAcceptingItems || this.hub.isClosed) return Promise.resolve();
|
|
385
512
|
this.hub.publish(item);
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
513
|
+
return this.enqueueCallback(item);
|
|
514
|
+
}
|
|
515
|
+
enqueueCallback(item, notifyPeerOnFailure = true) {
|
|
516
|
+
const onEventResult = attempt(() => this.options.onEvent);
|
|
517
|
+
if (onEventResult instanceof Error) {
|
|
518
|
+
this.reportCallbackFailure("onEvent", onEventResult, notifyPeerOnFailure, item);
|
|
519
|
+
return Promise.resolve();
|
|
520
|
+
}
|
|
521
|
+
if (!onEventResult) return Promise.resolve();
|
|
522
|
+
return this.callbackQueue.add(async () => {
|
|
523
|
+
const result = await attempt(() => Promise.resolve(onEventResult(item)));
|
|
524
|
+
if (result instanceof Error) this.reportCallbackFailure("onEvent", result, notifyPeerOnFailure, item);
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* error log 専用 sink も callbackQueue に載せ、drain() が in-flight callback と
|
|
529
|
+
* その失敗診断まで drain できるようにする
|
|
530
|
+
*/
|
|
531
|
+
invokeOnError(log, notifyPeerOnFailure = true) {
|
|
532
|
+
const onErrorResult = attempt(() => this.options.onError);
|
|
533
|
+
if (onErrorResult instanceof Error) {
|
|
534
|
+
this.reportCallbackFailure("onError", onErrorResult, notifyPeerOnFailure, {
|
|
535
|
+
kind: "log",
|
|
536
|
+
log
|
|
537
|
+
});
|
|
538
|
+
return Promise.resolve();
|
|
539
|
+
}
|
|
540
|
+
if (!onErrorResult) return Promise.resolve();
|
|
541
|
+
return this.callbackQueue.add(async () => {
|
|
542
|
+
const result = await attempt(() => Promise.resolve(onErrorResult(log)));
|
|
543
|
+
if (result instanceof Error) this.reportCallbackFailure("onError", result, notifyPeerOnFailure, {
|
|
544
|
+
kind: "log",
|
|
545
|
+
log
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
/**
|
|
550
|
+
* 観測 sink 自身の失敗は同じ sink へ戻すと再帰するため、まず pull stream へ直接 publish し、
|
|
551
|
+
* もう一方の callback にだけ転送する。peer も失敗した場合は hub-only の診断を残して終端する
|
|
552
|
+
*/
|
|
553
|
+
reportCallbackFailure(callback, error, notifyPeer, failedItem) {
|
|
554
|
+
const itemDetail = failedItem?.kind === "event" ? {
|
|
555
|
+
itemKind: failedItem.kind,
|
|
556
|
+
itemSource: failedItem.event.source,
|
|
557
|
+
itemType: failedItem.event.type
|
|
558
|
+
} : failedItem?.kind === "log" ? {
|
|
559
|
+
itemKind: failedItem.kind,
|
|
560
|
+
itemSource: failedItem.log.source,
|
|
561
|
+
itemAction: failedItem.log.action,
|
|
562
|
+
itemDetail: failedItem.log.detail
|
|
563
|
+
} : {};
|
|
564
|
+
const log = {
|
|
565
|
+
level: "error",
|
|
566
|
+
source: "flume",
|
|
567
|
+
action: `${callback}.error`,
|
|
568
|
+
message: `${callback} callback failed: ${safeErrorMessage({ error })}`,
|
|
569
|
+
error,
|
|
570
|
+
detail: {
|
|
571
|
+
callback,
|
|
572
|
+
...itemDetail
|
|
573
|
+
},
|
|
574
|
+
timestamp: safeNow({ deps: this.deps })
|
|
575
|
+
};
|
|
576
|
+
this.hub.publish({
|
|
577
|
+
kind: "log",
|
|
578
|
+
log
|
|
579
|
+
});
|
|
580
|
+
if (!notifyPeer) return;
|
|
581
|
+
if (callback === "onEvent") {
|
|
582
|
+
this.invokeOnError(log, false);
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
this.enqueueCallback({
|
|
586
|
+
kind: "log",
|
|
587
|
+
log
|
|
588
|
+
}, false);
|
|
391
589
|
}
|
|
392
590
|
async open() {
|
|
393
591
|
const guard = this.guardOpen();
|
|
@@ -398,14 +596,15 @@ var Flume = class {
|
|
|
398
596
|
message: `opening ${this.sources.length} source(s)`,
|
|
399
597
|
detail: { count: this.sources.length }
|
|
400
598
|
});
|
|
401
|
-
const reconnect =
|
|
599
|
+
const reconnect = this.resolveReconnect();
|
|
402
600
|
const settled = await Promise.allSettled(this.sources.map((source) => this.safeStart(source, reconnect)));
|
|
403
601
|
const failures = [];
|
|
404
|
-
const
|
|
602
|
+
const ownedSources = /* @__PURE__ */ new Set();
|
|
405
603
|
for (const [index, result] of settled.entries()) {
|
|
406
604
|
const source = this.sources[index];
|
|
407
605
|
if (source === void 0) continue;
|
|
408
606
|
const name = this.sourceName(source);
|
|
607
|
+
if (!((result.status === "rejected" ? result.reason : result.value) instanceof FlumeSourceReuseError)) ownedSources.add(source);
|
|
409
608
|
if (result.status === "rejected") {
|
|
410
609
|
failures.push({
|
|
411
610
|
name,
|
|
@@ -413,14 +612,10 @@ var Flume = class {
|
|
|
413
612
|
});
|
|
414
613
|
continue;
|
|
415
614
|
}
|
|
416
|
-
if (result.value instanceof Error) {
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
});
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
started.push(source);
|
|
615
|
+
if (result.value instanceof Error) failures.push({
|
|
616
|
+
name,
|
|
617
|
+
error: result.value
|
|
618
|
+
});
|
|
424
619
|
}
|
|
425
620
|
if (failures.length > 0) {
|
|
426
621
|
for (const failure of failures) this.log.error({
|
|
@@ -429,7 +624,7 @@ var Flume = class {
|
|
|
429
624
|
error: failure.error,
|
|
430
625
|
detail: { source: failure.name }
|
|
431
626
|
});
|
|
432
|
-
await this.rollback(
|
|
627
|
+
await this.rollback([...ownedSources]);
|
|
433
628
|
const detail = failures.map((f) => `${f.name}: ${safeErrorMessage({ error: f.error })}`).join("; ");
|
|
434
629
|
const error = new FlumeStartError(`Flume.open: ${failures.length} source(s) failed: ${detail}`);
|
|
435
630
|
this.log.error({
|
|
@@ -437,28 +632,48 @@ var Flume = class {
|
|
|
437
632
|
message: safeErrorMessage({ error }),
|
|
438
633
|
error
|
|
439
634
|
});
|
|
635
|
+
this.finishFailedOpen();
|
|
440
636
|
return error;
|
|
441
637
|
}
|
|
442
638
|
if (this.isSignalAborted()) {
|
|
443
|
-
await this.rollback(
|
|
639
|
+
await this.rollback([...ownedSources]);
|
|
444
640
|
const error = new FlumeStartError("Flume.open: aborted during open");
|
|
445
641
|
this.log.warn({
|
|
446
642
|
action: "flume.open.aborted",
|
|
447
643
|
message: safeErrorMessage({ error }),
|
|
448
644
|
error
|
|
449
645
|
});
|
|
646
|
+
this.finishFailedOpen();
|
|
450
647
|
return error;
|
|
451
648
|
}
|
|
452
649
|
this.log.info({
|
|
453
650
|
action: "flume.open.complete",
|
|
454
651
|
message: "all sources opened"
|
|
455
652
|
});
|
|
456
|
-
|
|
653
|
+
const running = new FlumeRunning({
|
|
457
654
|
sources: this.sources,
|
|
458
655
|
signal: this.options.signal,
|
|
459
656
|
log: this.log,
|
|
460
|
-
hub: this.hub
|
|
657
|
+
hub: this.hub,
|
|
658
|
+
callbackQueue: this.callbackQueue,
|
|
659
|
+
seal: () => {
|
|
660
|
+
this.isAcceptingItems = false;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
if (!this.isSignalAborted()) return running;
|
|
664
|
+
await running.close();
|
|
665
|
+
const error = new FlumeStartError("Flume.open: aborted while entering running state");
|
|
666
|
+
this.log.warn({
|
|
667
|
+
action: "flume.open.aborted",
|
|
668
|
+
message: safeErrorMessage({ error }),
|
|
669
|
+
error
|
|
461
670
|
});
|
|
671
|
+
return error;
|
|
672
|
+
}
|
|
673
|
+
/** 起動失敗の戻り値は callback を待たず、配送済み診断の完了後に hub を閉じる。 */
|
|
674
|
+
finishFailedOpen() {
|
|
675
|
+
this.isAcceptingItems = false;
|
|
676
|
+
this.callbackQueue.drain().then(() => this.hub.close());
|
|
462
677
|
}
|
|
463
678
|
guardOpen() {
|
|
464
679
|
if (this.consumed) {
|
|
@@ -481,6 +696,19 @@ var Flume = class {
|
|
|
481
696
|
}
|
|
482
697
|
return null;
|
|
483
698
|
}
|
|
699
|
+
/** reconnect オプションの解決。throwing getter を持つ hostile 入力でも open() を reject させない */
|
|
700
|
+
resolveReconnect() {
|
|
701
|
+
const result = attempt(() => resolveFlumeReconnectConfig(this.options.reconnect));
|
|
702
|
+
if (result instanceof Error) {
|
|
703
|
+
this.log.warn({
|
|
704
|
+
action: "reconnect.config.invalid",
|
|
705
|
+
message: safeErrorMessage({ error: result }),
|
|
706
|
+
error: result
|
|
707
|
+
});
|
|
708
|
+
return null;
|
|
709
|
+
}
|
|
710
|
+
return result;
|
|
711
|
+
}
|
|
484
712
|
isSignalAborted() {
|
|
485
713
|
const signal = this.options.signal;
|
|
486
714
|
if (!signal) return false;
|
|
@@ -506,18 +734,25 @@ var Flume = class {
|
|
|
506
734
|
}
|
|
507
735
|
async rollback(sources) {
|
|
508
736
|
const settled = await Promise.allSettled(sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
509
|
-
for (const [index, result] of settled.entries())
|
|
737
|
+
for (const [index, result] of settled.entries()) {
|
|
510
738
|
const source = sources[index];
|
|
511
739
|
const name = source ? this.sourceName(source) : "?";
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
});
|
|
740
|
+
if (result.status === "rejected") {
|
|
741
|
+
const error = safeNormalizeError({ value: result.reason });
|
|
742
|
+
this.logRollbackFailure(name, error);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (result.value instanceof Error) this.logRollbackFailure(name, result.value);
|
|
519
746
|
}
|
|
520
747
|
}
|
|
748
|
+
logRollbackFailure(name, error) {
|
|
749
|
+
this.log.error({
|
|
750
|
+
action: "flume.rollback.failed",
|
|
751
|
+
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
752
|
+
error,
|
|
753
|
+
detail: { source: name }
|
|
754
|
+
});
|
|
755
|
+
}
|
|
521
756
|
};
|
|
522
757
|
//#endregion
|
|
523
758
|
//#region lib/flume-confluence.ts
|
|
@@ -531,28 +766,61 @@ const DEFAULT_REPLACE_TIMEOUT_MS = 1e4;
|
|
|
531
766
|
* `replace(id, sources)` は同じ id のグループを差し替える。新グループを先に起動し、
|
|
532
767
|
* 起動成功時にのみ旧グループを停止するので連続稼働を維持できる (token rotation 用途)。
|
|
533
768
|
* 起動失敗時は旧グループはそのまま走り続ける。
|
|
769
|
+
* 注意: 失敗した replace / add に渡した source インスタンスは consumed になるため、
|
|
770
|
+
* リトライには新しいインスタンスを構築する必要がある。
|
|
771
|
+
*
|
|
772
|
+
* `closeAll()` は終端操作。以後の `add()` / `replace()` は拒否され、closeAll と並行して
|
|
773
|
+
* 起動中だったグループも abort して完了を待つ (シャットダウン後に誰にも止められない
|
|
774
|
+
* グループが残らない)。
|
|
534
775
|
*
|
|
535
776
|
* throw しない流儀に従い `add()` / `replace()` は `Error | null` を返す
|
|
536
777
|
*/
|
|
537
778
|
var FlumeConfluence = class {
|
|
538
779
|
props;
|
|
539
780
|
running = /* @__PURE__ */ new Map();
|
|
781
|
+
/** open() を await 中でまだ Map に commit されていないグループの id (add 重複と remove 追跡用) */
|
|
782
|
+
pendingIds = /* @__PURE__ */ new Set();
|
|
783
|
+
removedWhilePending = /* @__PURE__ */ new Set();
|
|
784
|
+
pendingOpens = /* @__PURE__ */ new Set();
|
|
785
|
+
isClosedFlag = false;
|
|
540
786
|
deps;
|
|
541
787
|
constructor(props = {}) {
|
|
542
788
|
this.props = props;
|
|
543
789
|
this.deps = props.deps ?? createFlumeDefaultDeps();
|
|
544
790
|
}
|
|
791
|
+
get isClosed() {
|
|
792
|
+
return this.isClosedFlag;
|
|
793
|
+
}
|
|
545
794
|
/** sources を 1 グループとして起動。id 重複や起動失敗は `Error` で返す (throw しない) */
|
|
546
795
|
async add(id, sources) {
|
|
547
|
-
if (this.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
796
|
+
if (this.isClosedFlag) return new FlumeStartError(`FlumeConfluence: already closed: ${id}`);
|
|
797
|
+
if (this.running.has(id) || this.pendingIds.has(id)) return new FlumeStartError(`FlumeConfluence: id already added: ${id}`);
|
|
798
|
+
this.pendingIds.add(id);
|
|
799
|
+
const pending = this.createPendingOpen();
|
|
800
|
+
this.pendingOpens.add(pending);
|
|
801
|
+
try {
|
|
802
|
+
const running = await this.openGroup(id, sources, pending.controller, void 0);
|
|
803
|
+
if (running instanceof Error) return running;
|
|
804
|
+
if (this.isClosedFlag) {
|
|
805
|
+
await running.close();
|
|
806
|
+
return new FlumeStartError(`FlumeConfluence: closed during add: ${id}`);
|
|
807
|
+
}
|
|
808
|
+
if (this.removedWhilePending.has(id)) {
|
|
809
|
+
await running.close();
|
|
810
|
+
return new FlumeStartError(`FlumeConfluence: removed during add: ${id}`);
|
|
811
|
+
}
|
|
812
|
+
if (this.running.has(id)) {
|
|
813
|
+
await running.close();
|
|
814
|
+
return new FlumeStartError(`FlumeConfluence: id already added: ${id}`);
|
|
815
|
+
}
|
|
816
|
+
this.running.set(id, running);
|
|
817
|
+
return null;
|
|
818
|
+
} finally {
|
|
819
|
+
this.pendingIds.delete(id);
|
|
820
|
+
this.removedWhilePending.delete(id);
|
|
821
|
+
this.pendingOpens.delete(pending);
|
|
822
|
+
pending.finish();
|
|
553
823
|
}
|
|
554
|
-
this.running.set(id, running);
|
|
555
|
-
return null;
|
|
556
824
|
}
|
|
557
825
|
/**
|
|
558
826
|
* 既存グループを新しい sources で差し替える。新グループを先に起動し、成功時のみ旧を停止する。
|
|
@@ -560,27 +828,48 @@ var FlumeConfluence = class {
|
|
|
560
828
|
* 旧グループが存在しない場合は `Error` を返す (replace は add と違ってグループの存在を前提とする)
|
|
561
829
|
*/
|
|
562
830
|
async replace(id, sources, options) {
|
|
831
|
+
if (this.isClosedFlag) return new FlumeStartError(`FlumeConfluence: already closed: ${id}`);
|
|
563
832
|
const previous = this.running.get(id);
|
|
564
833
|
if (!previous) return new FlumeStartError(`FlumeConfluence: id not running: ${id}`);
|
|
565
|
-
const
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
await
|
|
570
|
-
|
|
834
|
+
const pending = this.createPendingOpen();
|
|
835
|
+
this.pendingOpens.add(pending);
|
|
836
|
+
try {
|
|
837
|
+
const timeoutMs = options?.replaceTimeoutMs ?? DEFAULT_REPLACE_TIMEOUT_MS;
|
|
838
|
+
const next = await this.openGroup(id, sources, pending.controller, timeoutMs);
|
|
839
|
+
if (next instanceof Error) return next;
|
|
840
|
+
if (this.isClosedFlag) {
|
|
841
|
+
await next.close();
|
|
842
|
+
return new FlumeStartError(`FlumeConfluence: closed during replace: ${id}`);
|
|
843
|
+
}
|
|
844
|
+
if (this.running.get(id) !== previous) {
|
|
845
|
+
await next.close();
|
|
846
|
+
return new FlumeStartError(`FlumeConfluence: ${id} concurrently mutated during replace`);
|
|
847
|
+
}
|
|
848
|
+
this.running.set(id, next);
|
|
849
|
+
await previous.close();
|
|
850
|
+
return null;
|
|
851
|
+
} finally {
|
|
852
|
+
this.pendingOpens.delete(pending);
|
|
853
|
+
pending.finish();
|
|
571
854
|
}
|
|
572
|
-
this.running.set(id, next);
|
|
573
|
-
await previous.close();
|
|
574
|
-
return null;
|
|
575
855
|
}
|
|
576
|
-
/**
|
|
856
|
+
/**
|
|
857
|
+
* 指定グループだけ close。他グループは無停止。未知の id は no-op。
|
|
858
|
+
* 起動中 (add が open を await 中) の id は commit 時点で破棄されるよう予約する
|
|
859
|
+
*/
|
|
577
860
|
async remove(id) {
|
|
861
|
+
if (this.pendingIds.has(id)) this.removedWhilePending.add(id);
|
|
578
862
|
const running = this.running.get(id);
|
|
579
863
|
if (!running) return;
|
|
580
864
|
this.running.delete(id);
|
|
581
865
|
await running.close();
|
|
582
866
|
}
|
|
867
|
+
/** 終端操作。全グループを close し、以後の add / replace を拒否する */
|
|
583
868
|
async closeAll() {
|
|
869
|
+
this.isClosedFlag = true;
|
|
870
|
+
const pending = [...this.pendingOpens];
|
|
871
|
+
for (const operation of pending) attempt(() => operation.controller.abort());
|
|
872
|
+
await Promise.allSettled(pending.map((operation) => operation.done));
|
|
584
873
|
const ids = [...this.running.keys()];
|
|
585
874
|
await Promise.all(ids.map((id) => this.remove(id)));
|
|
586
875
|
}
|
|
@@ -591,38 +880,66 @@ var FlumeConfluence = class {
|
|
|
591
880
|
return [...this.running.keys()];
|
|
592
881
|
}
|
|
593
882
|
/**
|
|
594
|
-
* 1 グループ分の Flume を開いて FlumeRunning を返す。timeoutMs を指定すると
|
|
595
|
-
*
|
|
596
|
-
*
|
|
883
|
+
* 1 グループ分の Flume を開いて FlumeRunning を返す。timeoutMs を指定すると AbortSignal を
|
|
884
|
+
* ctx.signal として各 source へ注入し、超過時に abort して進行中の connect ごと中止する
|
|
885
|
+
* (source 側は base クラスが signal を購読して stop() を発火する)。
|
|
886
|
+
* 失敗時の rollback は Flume 本体に任せる
|
|
597
887
|
*/
|
|
598
|
-
async openGroup(id, sources, timeoutMs) {
|
|
599
|
-
const
|
|
600
|
-
const
|
|
601
|
-
|
|
888
|
+
async openGroup(id, sources, controller, timeoutMs) {
|
|
889
|
+
const timeoutState = { isArmed: timeoutMs !== void 0 };
|
|
890
|
+
const timeoutResult = timeoutMs === void 0 ? null : attempt(() => this.deps.setTimeout(() => {
|
|
891
|
+
if (timeoutState.isArmed) controller.abort();
|
|
892
|
+
}, timeoutMs));
|
|
893
|
+
if (timeoutResult instanceof Error) {
|
|
894
|
+
attempt(() => controller.abort());
|
|
895
|
+
return new FlumeStartError(`FlumeConfluence: failed to schedule timeout for "${id}"`, { cause: timeoutResult });
|
|
896
|
+
}
|
|
897
|
+
const flume = new Flume({
|
|
602
898
|
sources,
|
|
603
899
|
onEvent: this.wrapOnEvent(id),
|
|
604
|
-
onError: this.
|
|
900
|
+
onError: this.wrapOnError(id),
|
|
605
901
|
deps: this.props.deps,
|
|
606
902
|
reconnect: this.props.reconnect,
|
|
607
|
-
signal: controller
|
|
608
|
-
})
|
|
609
|
-
|
|
903
|
+
signal: controller.signal
|
|
904
|
+
});
|
|
905
|
+
const result = await attempt(() => flume.open());
|
|
906
|
+
timeoutState.isArmed = false;
|
|
907
|
+
if (timeoutResult !== null) attempt(() => this.deps.clearTimeout(timeoutResult));
|
|
610
908
|
if (result instanceof Error) {
|
|
611
|
-
if (controller !==
|
|
909
|
+
if (controller.signal.aborted && timeoutMs !== void 0) return new FlumeStartError(`FlumeConfluence: open of "${id}" timed out after ${timeoutMs}ms`, { cause: result });
|
|
612
910
|
return result;
|
|
613
911
|
}
|
|
614
912
|
return result;
|
|
615
913
|
}
|
|
914
|
+
createPendingOpen() {
|
|
915
|
+
const completion = Promise.withResolvers();
|
|
916
|
+
return {
|
|
917
|
+
controller: new AbortController(),
|
|
918
|
+
done: completion.promise,
|
|
919
|
+
finish: completion.resolve
|
|
920
|
+
};
|
|
921
|
+
}
|
|
616
922
|
wrapOnEvent(id) {
|
|
617
923
|
const onEvent = this.props.onEvent;
|
|
618
924
|
if (!onEvent) return void 0;
|
|
619
925
|
return (item) => {
|
|
620
|
-
onEvent({
|
|
926
|
+
return onEvent({
|
|
621
927
|
...item,
|
|
622
928
|
groupId: id
|
|
623
929
|
});
|
|
624
930
|
};
|
|
625
931
|
}
|
|
932
|
+
wrapOnError(id) {
|
|
933
|
+
const onError = this.props.onError;
|
|
934
|
+
if (!onError) return void 0;
|
|
935
|
+
return (log) => onError({
|
|
936
|
+
...log,
|
|
937
|
+
detail: {
|
|
938
|
+
...log.detail,
|
|
939
|
+
groupId: id
|
|
940
|
+
}
|
|
941
|
+
});
|
|
942
|
+
}
|
|
626
943
|
};
|
|
627
944
|
//#endregion
|
|
628
|
-
export { Flume, FlumeClosed, FlumeConfluence, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeSource, FlumeStartError,
|
|
945
|
+
export { Flume, FlumeClosed, FlumeConfluence, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeSource, FlumeStartError, attempt, createFlumeDefaultDeps, isRecord, safeErrorMessage, safeInvokeCallback, safeJsonParse, safeNormalizeError, safeNow, safeRandom, safeReadText, safeStringify };
|