@interactive-inc/flume 0.6.0 → 0.9.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 +156 -62
- package/dist/discord.d.ts +1 -1
- package/dist/discord.js +5 -4
- package/dist/{flume-source-DuUFPhSe.d.ts → flume-source.d.ts} +52 -11
- package/dist/{flume-source-DUvt9aJt.js → flume-source.js} +7 -4
- package/dist/github.d.ts +1 -1
- package/dist/github.js +12 -7
- package/dist/index.d.ts +146 -43
- package/dist/index.js +290 -83
- package/dist/is-record.js +6 -0
- package/dist/parse-error.d.ts +9 -0
- package/dist/{safe-json-parse-CfJjt-RY.js → safe-json-parse.js} +1 -1
- package/dist/{safe-read-text-JQd_5vbd.js → safe-read-text.js} +2 -2
- package/dist/{safe-stringify-DbWQw9qe.js → safe-stringify.js} +7 -13
- package/dist/slack.d.ts +1 -1
- package/dist/slack.js +13 -8
- package/dist/time.d.ts +45 -0
- package/dist/time.js +319 -0
- package/package.json +6 -1
- /package/dist/{connection-error-HUO3PC3G.js → connection-error.js} +0 -0
- /package/dist/{http-error-CPSKoSie.js → http-error.js} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
import { a as FlumeStartError, c as safeNormalizeError, l as safeErrorMessage, n as safeInvokeCallback, o as FlumeParseError, r as FlumeLogger, s as attempt, t as FlumeSource } from "./flume-source
|
|
2
|
-
import { t as FlumeConnectionError } from "./connection-error
|
|
3
|
-
import { t as FlumeHttpError } from "./http-error
|
|
1
|
+
import { a as FlumeStartError, c as safeNormalizeError, l as safeErrorMessage, n as safeInvokeCallback, o as FlumeParseError, r as FlumeLogger, s as attempt, t as FlumeSource } from "./flume-source.js";
|
|
2
|
+
import { t as FlumeConnectionError } from "./connection-error.js";
|
|
3
|
+
import { t as FlumeHttpError } from "./http-error.js";
|
|
4
4
|
//#region lib/deps.ts
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
/**
|
|
6
|
+
* `globalThis.WebSocket` の現在の値を返す。
|
|
7
|
+
* 取得は呼び出しごとに行う — `createFlumeDefaultDeps()` がモジュール初期化時ではなく
|
|
8
|
+
* 呼ばれた瞬間の `globalThis.WebSocket` を見るので、jsdom / happy-dom / vitest の
|
|
9
|
+
* `beforeEach` で `globalThis.WebSocket` を差し込むテスト戦略がそのまま機能する。
|
|
10
|
+
* `WebSocket` が無い環境 (Node の素の global など) では `null` を返す。
|
|
11
|
+
*/
|
|
12
|
+
function resolveCurrentWebSocket() {
|
|
13
|
+
const candidate = attempt(() => globalThis.WebSocket);
|
|
14
|
+
if (candidate instanceof Error || typeof candidate !== "function") return null;
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
7
17
|
/**
|
|
8
18
|
* platform 既定の IO を束ねた `FlumeRuntimeDeps`。
|
|
9
19
|
* `FlumeTimerHandle` は不透明型 (`unknown`) のため、setTimeout / clearTimeout の戻り値・引数を
|
|
10
|
-
* platform 型と橋渡しする際に境界で `as unknown as` を使う (IO 境界の最終手段)
|
|
20
|
+
* platform 型と橋渡しする際に境界で `as unknown as` を使う (IO 境界の最終手段)。
|
|
21
|
+
*
|
|
22
|
+
* `WebSocket` を含む全 IO は呼び出しごとに `globalThis` から引く lazy lookup。
|
|
23
|
+
* モジュール初期化後に `globalThis.WebSocket` が差し替わる環境
|
|
24
|
+
* (テストの `beforeEach` パッチ、jsdom などのブラウザ環境エミュレータ) でも
|
|
25
|
+
* `createFlumeDefaultDeps()` が返した deps が常に最新の参照を見る。
|
|
11
26
|
*/
|
|
12
27
|
function createFlumeDefaultDeps() {
|
|
13
28
|
return {
|
|
14
29
|
fetch: (url, init) => globalThis.fetch(url, init),
|
|
15
|
-
WebSocket:
|
|
30
|
+
WebSocket: resolveCurrentWebSocket(),
|
|
16
31
|
now: () => Date.now(),
|
|
17
32
|
random: () => Math.random(),
|
|
18
33
|
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
@@ -22,13 +37,110 @@ function createFlumeDefaultDeps() {
|
|
|
22
37
|
};
|
|
23
38
|
}
|
|
24
39
|
//#endregion
|
|
25
|
-
//#region lib/flume-
|
|
40
|
+
//#region lib/flume-stream.ts
|
|
41
|
+
const DONE = {
|
|
42
|
+
value: void 0,
|
|
43
|
+
done: true
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* push (`FlumeStreamHub.publish`) を pull (`for await`) に変換する async iterator。
|
|
47
|
+
* consumer が待っていれば即 resolve、いなければ buffer に積み、溢れたら onOverflow に従う。
|
|
48
|
+
* `return()` (break / 例外) と hub.close() のどちらでも自然に done へ落ちる
|
|
49
|
+
*/
|
|
50
|
+
var FlumeStream = class {
|
|
51
|
+
props;
|
|
52
|
+
items = [];
|
|
53
|
+
resolvers = [];
|
|
54
|
+
closed = false;
|
|
55
|
+
constructor(props) {
|
|
56
|
+
this.props = props;
|
|
57
|
+
}
|
|
58
|
+
push(item) {
|
|
59
|
+
if (this.closed) return;
|
|
60
|
+
const resolver = this.resolvers.shift();
|
|
61
|
+
if (resolver) {
|
|
62
|
+
resolver({
|
|
63
|
+
value: item,
|
|
64
|
+
done: false
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (this.items.length >= this.props.buffer) {
|
|
69
|
+
if (this.props.onOverflow === "drop-newest") return;
|
|
70
|
+
this.items.shift();
|
|
71
|
+
}
|
|
72
|
+
this.items.push(item);
|
|
73
|
+
}
|
|
74
|
+
close() {
|
|
75
|
+
if (this.closed) return;
|
|
76
|
+
this.closed = true;
|
|
77
|
+
while (this.resolvers.length > 0) {
|
|
78
|
+
const resolver = this.resolvers.shift();
|
|
79
|
+
if (resolver) resolver(DONE);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
next() {
|
|
83
|
+
const item = this.items.shift();
|
|
84
|
+
if (item !== void 0) return Promise.resolve({
|
|
85
|
+
value: item,
|
|
86
|
+
done: false
|
|
87
|
+
});
|
|
88
|
+
if (this.closed) return Promise.resolve(DONE);
|
|
89
|
+
return new Promise((resolve) => this.resolvers.push(resolve));
|
|
90
|
+
}
|
|
91
|
+
return() {
|
|
92
|
+
this.close();
|
|
93
|
+
this.props.onClose();
|
|
94
|
+
return Promise.resolve(DONE);
|
|
95
|
+
}
|
|
96
|
+
[Symbol.asyncIterator]() {
|
|
97
|
+
return this;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region lib/flume-stream-hub.ts
|
|
102
|
+
const DEFAULT_BUFFER = 1e3;
|
|
26
103
|
/**
|
|
27
|
-
*
|
|
104
|
+
* firehose (`onEvent` / `stream()`) の item を複数の pull consumer へ fan-out する内部ハブ。
|
|
105
|
+
* subscriber が居なければ publish は実質 no-op。Flume 停止時に close() で全 stream を終端する
|
|
28
106
|
*/
|
|
29
|
-
var
|
|
107
|
+
var FlumeStreamHub = class {
|
|
108
|
+
streams = /* @__PURE__ */ new Set();
|
|
109
|
+
closed = false;
|
|
110
|
+
publish(item) {
|
|
111
|
+
if (this.closed) return;
|
|
112
|
+
for (const stream of this.streams) stream.push(item);
|
|
113
|
+
}
|
|
114
|
+
subscribe(options) {
|
|
115
|
+
const stream = new FlumeStream({
|
|
116
|
+
buffer: options?.buffer ?? DEFAULT_BUFFER,
|
|
117
|
+
onOverflow: options?.onOverflow ?? "drop-oldest",
|
|
118
|
+
onClose: () => this.streams.delete(stream)
|
|
119
|
+
});
|
|
120
|
+
if (this.closed) {
|
|
121
|
+
stream.close();
|
|
122
|
+
return stream;
|
|
123
|
+
}
|
|
124
|
+
this.streams.add(stream);
|
|
125
|
+
return stream;
|
|
126
|
+
}
|
|
127
|
+
close() {
|
|
128
|
+
if (this.closed) return;
|
|
129
|
+
this.closed = true;
|
|
130
|
+
for (const stream of this.streams) stream.close();
|
|
131
|
+
this.streams.clear();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region lib/flume-closed.ts
|
|
136
|
+
/**
|
|
137
|
+
* 停止済みの終端状態。最終ステータスと、停止時に source.disconnect が throw した
|
|
138
|
+
* エラー一覧を観測できる。`errors()` を読めば `onLog` を grep せずに「どの source が
|
|
139
|
+
* きれいに close したか / どれが失敗したか」を直接判定できる
|
|
140
|
+
*/
|
|
141
|
+
var FlumeClosed = class {
|
|
30
142
|
props;
|
|
31
|
-
kind = "
|
|
143
|
+
kind = "closed";
|
|
32
144
|
constructor(props) {
|
|
33
145
|
this.props = props;
|
|
34
146
|
Object.freeze(this);
|
|
@@ -36,31 +148,39 @@ var FlumeStopped = class {
|
|
|
36
148
|
statuses() {
|
|
37
149
|
return this.props.finalStatuses;
|
|
38
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* `runClose` 中に `source.stop()` が rejected で settle した source の名前と
|
|
153
|
+
* 正規化済み Error の組。`onEvent` firehose の `flume.close.failed` log と 1:1 対応する。
|
|
154
|
+
* 全 source が clean close した場合は空配列。
|
|
155
|
+
*/
|
|
156
|
+
errors() {
|
|
157
|
+
return this.props.closeErrors;
|
|
158
|
+
}
|
|
39
159
|
};
|
|
40
160
|
//#endregion
|
|
41
161
|
//#region lib/flume-running.ts
|
|
42
162
|
/**
|
|
43
|
-
* 稼働中の Flume。
|
|
163
|
+
* 稼働中の Flume。close() で FlumeClosed へ遷移する。signal が abort されると自動 close。
|
|
44
164
|
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
45
|
-
* `
|
|
165
|
+
* `runClose` の最外殻 try/catch で想定外の throw も `FlumeClosed` の resolve に変換する
|
|
46
166
|
*/
|
|
47
167
|
var FlumeRunning = class {
|
|
48
168
|
props;
|
|
49
169
|
kind = "running";
|
|
50
|
-
|
|
170
|
+
closePromise = null;
|
|
51
171
|
onAbort;
|
|
52
172
|
constructor(props) {
|
|
53
173
|
this.props = props;
|
|
54
174
|
this.onAbort = () => {
|
|
55
175
|
this.props.log.info({
|
|
56
176
|
action: "flume.abort",
|
|
57
|
-
message: "signal aborted,
|
|
177
|
+
message: "signal aborted, closing"
|
|
58
178
|
});
|
|
59
179
|
safeInvokeCallback({
|
|
60
|
-
fn: () => this.
|
|
180
|
+
fn: () => this.close(),
|
|
61
181
|
onError: (error) => {
|
|
62
182
|
this.props.log.error({
|
|
63
|
-
action: "flume.abort.
|
|
183
|
+
action: "flume.abort.close.failed",
|
|
64
184
|
message: safeErrorMessage({ error }),
|
|
65
185
|
error
|
|
66
186
|
});
|
|
@@ -80,27 +200,49 @@ var FlumeRunning = class {
|
|
|
80
200
|
}
|
|
81
201
|
}
|
|
82
202
|
}
|
|
83
|
-
|
|
84
|
-
if (this.
|
|
85
|
-
this.
|
|
86
|
-
return this.
|
|
203
|
+
close() {
|
|
204
|
+
if (this.closePromise) return this.closePromise;
|
|
205
|
+
this.closePromise = this.runClose();
|
|
206
|
+
return this.closePromise;
|
|
87
207
|
}
|
|
88
208
|
statuses() {
|
|
89
209
|
return this.snapshotStatuses();
|
|
90
210
|
}
|
|
91
|
-
|
|
211
|
+
/**
|
|
212
|
+
* 統合 firehose を pull で受け取る async iterator。`for await (const item of running.stream())`。
|
|
213
|
+
* item は events + 全ログの union (`FlumeStreamItem`)。`item.kind` で判別する。
|
|
214
|
+
* close() / signal abort で iterator は自然に終了し、`break` すると hub から自動 unsubscribe する。
|
|
215
|
+
* consumer が遅れて buffer を超えたら `onOverflow` (既定 drop-oldest) に従う
|
|
216
|
+
*/
|
|
217
|
+
stream(options) {
|
|
218
|
+
return this.props.hub.subscribe(options);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Host が `Flume({ signal })` で渡した AbortSignal をそのまま公開する。
|
|
222
|
+
* 直接の controller を持っていない呼び出し元が `running.signal?.aborted`
|
|
223
|
+
* で abort 状態を確認できる
|
|
224
|
+
*/
|
|
225
|
+
get signal() {
|
|
226
|
+
return this.props.signal;
|
|
227
|
+
}
|
|
228
|
+
async runClose() {
|
|
229
|
+
const closeErrors = [];
|
|
92
230
|
try {
|
|
93
231
|
this.props.log.info({
|
|
94
|
-
action: "flume.
|
|
95
|
-
message: `
|
|
232
|
+
action: "flume.close",
|
|
233
|
+
message: `closing ${this.props.sources.length} source(s)`
|
|
96
234
|
});
|
|
97
235
|
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
98
236
|
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
99
237
|
const source = this.props.sources[index];
|
|
100
238
|
const name = source ? this.sourceName(source) : "?";
|
|
101
239
|
const error = safeNormalizeError({ value: result.reason });
|
|
240
|
+
closeErrors.push({
|
|
241
|
+
source: name,
|
|
242
|
+
error
|
|
243
|
+
});
|
|
102
244
|
this.props.log.error({
|
|
103
|
-
action: "flume.
|
|
245
|
+
action: "flume.close.failed",
|
|
104
246
|
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
105
247
|
error,
|
|
106
248
|
detail: { source: name }
|
|
@@ -119,18 +261,26 @@ var FlumeRunning = class {
|
|
|
119
261
|
}
|
|
120
262
|
}
|
|
121
263
|
this.props.log.info({
|
|
122
|
-
action: "flume.
|
|
123
|
-
message: "all sources
|
|
264
|
+
action: "flume.close.complete",
|
|
265
|
+
message: "all sources closed"
|
|
266
|
+
});
|
|
267
|
+
this.props.hub.close();
|
|
268
|
+
return new FlumeClosed({
|
|
269
|
+
finalStatuses: this.snapshotStatuses(),
|
|
270
|
+
closeErrors
|
|
124
271
|
});
|
|
125
|
-
return new FlumeStopped({ finalStatuses: this.snapshotStatuses() });
|
|
126
272
|
} catch (err) {
|
|
127
273
|
const error = safeNormalizeError({ value: err });
|
|
128
274
|
this.props.log.error({
|
|
129
|
-
action: "flume.
|
|
275
|
+
action: "flume.close.unhandled",
|
|
130
276
|
message: safeErrorMessage({ error }),
|
|
131
277
|
error
|
|
132
278
|
});
|
|
133
|
-
|
|
279
|
+
this.props.hub.close();
|
|
280
|
+
return new FlumeClosed({
|
|
281
|
+
finalStatuses: this.snapshotStatuses(),
|
|
282
|
+
closeErrors
|
|
283
|
+
});
|
|
134
284
|
}
|
|
135
285
|
}
|
|
136
286
|
snapshotStatuses() {
|
|
@@ -180,41 +330,71 @@ function resolveFlumeReconnectConfig(input) {
|
|
|
180
330
|
}
|
|
181
331
|
//#endregion
|
|
182
332
|
//#region lib/flume.ts
|
|
183
|
-
const noopOnEvent = () => {};
|
|
184
333
|
/**
|
|
185
|
-
* 起動前の Flume。`
|
|
186
|
-
*
|
|
187
|
-
* `onEvent`
|
|
334
|
+
* 起動前の Flume。`open()` で `FlumeRunning` へ遷移する。
|
|
335
|
+
* コンストラクタは単一オブジェクト `{ sources, ...options }` を受け取る (`sources` のみ必須)。
|
|
336
|
+
* events も全ログも 1 本の firehose (`onEvent` push / `stream()` pull) に流れ、購読側が filter する。
|
|
188
337
|
* いずれかの source 失敗時は既に成功した source を全て `stop()` してロールバックし
|
|
189
338
|
* `FlumeStartError` を返す。
|
|
190
339
|
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
191
|
-
* Promise rejection に正規化して `allSettled` で捕捉する (`
|
|
340
|
+
* Promise rejection に正規化して `allSettled` で捕捉する (`open()` は決して reject しない)
|
|
192
341
|
*/
|
|
193
342
|
var Flume = class {
|
|
194
|
-
sources;
|
|
195
343
|
options;
|
|
196
344
|
consumed = false;
|
|
197
345
|
log;
|
|
198
346
|
deps;
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
347
|
+
sources;
|
|
348
|
+
sourceEventHandler;
|
|
349
|
+
hub = new FlumeStreamHub();
|
|
350
|
+
constructor(options) {
|
|
202
351
|
this.options = options;
|
|
352
|
+
this.sources = options.sources;
|
|
203
353
|
this.deps = options.deps ?? createFlumeDefaultDeps();
|
|
204
354
|
this.log = new FlumeLogger({
|
|
205
355
|
source: "flume",
|
|
206
|
-
handler:
|
|
356
|
+
handler: this.buildLogHandler(),
|
|
207
357
|
deps: this.deps
|
|
208
358
|
});
|
|
209
|
-
this.
|
|
359
|
+
this.sourceEventHandler = (event) => this.emitItem({
|
|
360
|
+
kind: "event",
|
|
361
|
+
event
|
|
362
|
+
});
|
|
210
363
|
}
|
|
211
|
-
|
|
212
|
-
|
|
364
|
+
/** source が受信したログを firehose へ流す handler。error は onError にも分岐する */
|
|
365
|
+
buildLogHandler() {
|
|
366
|
+
return (log) => {
|
|
367
|
+
this.emitItem({
|
|
368
|
+
kind: "log",
|
|
369
|
+
log
|
|
370
|
+
});
|
|
371
|
+
const onError = this.options.onError;
|
|
372
|
+
if (!onError || log.level !== "error") return;
|
|
373
|
+
try {
|
|
374
|
+
Promise.resolve(onError(log)).catch(() => {});
|
|
375
|
+
} catch {}
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* firehose の単一 sink: pull の hub と push の onEvent の両方へ item を配る。
|
|
380
|
+
* onEvent への転送は this.log を経由しない (経由すると log item 経路で再帰する) ため
|
|
381
|
+
* 例外をここで握り潰す
|
|
382
|
+
*/
|
|
383
|
+
emitItem(item) {
|
|
384
|
+
this.hub.publish(item);
|
|
385
|
+
const onEvent = this.options.onEvent;
|
|
386
|
+
if (!onEvent) return;
|
|
387
|
+
try {
|
|
388
|
+
Promise.resolve(onEvent(item)).catch(() => {});
|
|
389
|
+
} catch {}
|
|
390
|
+
}
|
|
391
|
+
async open() {
|
|
392
|
+
const guard = this.guardOpen();
|
|
213
393
|
if (guard) return guard;
|
|
214
394
|
this.consumed = true;
|
|
215
395
|
this.log.info({
|
|
216
|
-
action: "flume.
|
|
217
|
-
message: `
|
|
396
|
+
action: "flume.open",
|
|
397
|
+
message: `opening ${this.sources.length} source(s)`,
|
|
218
398
|
detail: { count: this.sources.length }
|
|
219
399
|
});
|
|
220
400
|
const reconnect = resolveFlumeReconnectConfig(this.options.reconnect);
|
|
@@ -250,9 +430,9 @@ var Flume = class {
|
|
|
250
430
|
});
|
|
251
431
|
await this.rollback(started);
|
|
252
432
|
const detail = failures.map((f) => `${f.name}: ${safeErrorMessage({ error: f.error })}`).join("; ");
|
|
253
|
-
const error = new FlumeStartError(`Flume.
|
|
433
|
+
const error = new FlumeStartError(`Flume.open: ${failures.length} source(s) failed: ${detail}`);
|
|
254
434
|
this.log.error({
|
|
255
|
-
action: "flume.
|
|
435
|
+
action: "flume.open.failed",
|
|
256
436
|
message: safeErrorMessage({ error }),
|
|
257
437
|
error
|
|
258
438
|
});
|
|
@@ -260,38 +440,39 @@ var Flume = class {
|
|
|
260
440
|
}
|
|
261
441
|
if (this.isSignalAborted()) {
|
|
262
442
|
await this.rollback(this.sources);
|
|
263
|
-
const error = new FlumeStartError("Flume.
|
|
443
|
+
const error = new FlumeStartError("Flume.open: aborted during open");
|
|
264
444
|
this.log.warn({
|
|
265
|
-
action: "flume.
|
|
445
|
+
action: "flume.open.aborted",
|
|
266
446
|
message: safeErrorMessage({ error }),
|
|
267
447
|
error
|
|
268
448
|
});
|
|
269
449
|
return error;
|
|
270
450
|
}
|
|
271
451
|
this.log.info({
|
|
272
|
-
action: "flume.
|
|
273
|
-
message: "all sources
|
|
452
|
+
action: "flume.open.complete",
|
|
453
|
+
message: "all sources opened"
|
|
274
454
|
});
|
|
275
455
|
return new FlumeRunning({
|
|
276
456
|
sources: this.sources,
|
|
277
457
|
signal: this.options.signal,
|
|
278
|
-
log: this.log
|
|
458
|
+
log: this.log,
|
|
459
|
+
hub: this.hub
|
|
279
460
|
});
|
|
280
461
|
}
|
|
281
|
-
|
|
462
|
+
guardOpen() {
|
|
282
463
|
if (this.consumed) {
|
|
283
|
-
const error = new FlumeStartError("Flume.
|
|
464
|
+
const error = new FlumeStartError("Flume.open: already opened");
|
|
284
465
|
this.log.warn({
|
|
285
|
-
action: "flume.
|
|
466
|
+
action: "flume.open.refused",
|
|
286
467
|
message: safeErrorMessage({ error }),
|
|
287
468
|
error
|
|
288
469
|
});
|
|
289
470
|
return error;
|
|
290
471
|
}
|
|
291
472
|
if (this.isSignalAborted()) {
|
|
292
|
-
const error = new FlumeStartError("Flume.
|
|
473
|
+
const error = new FlumeStartError("Flume.open: signal already aborted");
|
|
293
474
|
this.log.warn({
|
|
294
|
-
action: "flume.
|
|
475
|
+
action: "flume.open.refused",
|
|
295
476
|
message: safeErrorMessage({ error }),
|
|
296
477
|
error
|
|
297
478
|
});
|
|
@@ -314,36 +495,14 @@ var Flume = class {
|
|
|
314
495
|
safeStart(source, reconnect) {
|
|
315
496
|
const name = this.sourceName(source);
|
|
316
497
|
const ctx = {
|
|
317
|
-
onEvent: this.
|
|
498
|
+
onEvent: this.sourceEventHandler,
|
|
318
499
|
log: this.log.child(name),
|
|
319
500
|
deps: this.deps,
|
|
320
|
-
|
|
321
|
-
|
|
501
|
+
reconnect,
|
|
502
|
+
signal: this.options.signal
|
|
322
503
|
};
|
|
323
504
|
return Promise.resolve().then(() => source.start(ctx));
|
|
324
505
|
}
|
|
325
|
-
notifyStatus(name, status, detail) {
|
|
326
|
-
const handler = this.options.onStatus;
|
|
327
|
-
if (!handler) return;
|
|
328
|
-
const event = detail !== void 0 ? {
|
|
329
|
-
source: name,
|
|
330
|
-
status,
|
|
331
|
-
detail
|
|
332
|
-
} : {
|
|
333
|
-
source: name,
|
|
334
|
-
status
|
|
335
|
-
};
|
|
336
|
-
safeInvokeCallback({
|
|
337
|
-
fn: () => handler(event),
|
|
338
|
-
onError: (error) => {
|
|
339
|
-
this.log.error({
|
|
340
|
-
action: "onStatus.error",
|
|
341
|
-
message: safeErrorMessage({ error }),
|
|
342
|
-
error
|
|
343
|
-
});
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
506
|
async rollback(sources) {
|
|
348
507
|
const settled = await Promise.allSettled(sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
349
508
|
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
@@ -360,4 +519,52 @@ var Flume = class {
|
|
|
360
519
|
}
|
|
361
520
|
};
|
|
362
521
|
//#endregion
|
|
363
|
-
|
|
522
|
+
//#region lib/flume-confluence.ts
|
|
523
|
+
/**
|
|
524
|
+
* 複数の `Flume` を束ねて動的に増減させる上位レイヤー。各 Flume は immutable のまま、
|
|
525
|
+
* `add()` で新しいグループを起動し `remove()` で個別に停止する。全グループの firehose は
|
|
526
|
+
* `onEvent` 1 本に合流する。Flume 本体の FSM / rollback / reconnect はそのまま再利用される。
|
|
527
|
+
*
|
|
528
|
+
* id はグループの管理ハンドル (add/remove 用)。合流ストリーム自体は id を持たず、
|
|
529
|
+
* item の中の source 名で発信元を判別する。throw しない流儀に従い `add()` は `Error | null` を返す
|
|
530
|
+
*/
|
|
531
|
+
var FlumeConfluence = class {
|
|
532
|
+
props;
|
|
533
|
+
running = /* @__PURE__ */ new Map();
|
|
534
|
+
constructor(props = {}) {
|
|
535
|
+
this.props = props;
|
|
536
|
+
}
|
|
537
|
+
/** sources を 1 グループとして起動。id 重複や起動失敗は `Error` で返す (throw しない) */
|
|
538
|
+
async add(id, sources) {
|
|
539
|
+
if (this.running.has(id)) return new FlumeStartError(`FlumeConfluence: id already added: ${id}`);
|
|
540
|
+
const running = await new Flume({
|
|
541
|
+
sources,
|
|
542
|
+
onEvent: this.props.onEvent,
|
|
543
|
+
onError: this.props.onError,
|
|
544
|
+
deps: this.props.deps,
|
|
545
|
+
reconnect: this.props.reconnect
|
|
546
|
+
}).open();
|
|
547
|
+
if (running instanceof Error) return running;
|
|
548
|
+
this.running.set(id, running);
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
/** 指定グループだけ close。他グループは無停止。未知の id は no-op */
|
|
552
|
+
async remove(id) {
|
|
553
|
+
const running = this.running.get(id);
|
|
554
|
+
if (!running) return;
|
|
555
|
+
this.running.delete(id);
|
|
556
|
+
await running.close();
|
|
557
|
+
}
|
|
558
|
+
async closeAll() {
|
|
559
|
+
const ids = [...this.running.keys()];
|
|
560
|
+
await Promise.all(ids.map((id) => this.remove(id)));
|
|
561
|
+
}
|
|
562
|
+
has(id) {
|
|
563
|
+
return this.running.has(id);
|
|
564
|
+
}
|
|
565
|
+
ids() {
|
|
566
|
+
return [...this.running.keys()];
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
//#endregion
|
|
570
|
+
export { Flume, FlumeClosed, FlumeConfluence, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeSource, FlumeStartError, createFlumeDefaultDeps };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { l as safeErrorMessage } from "./flume-source
|
|
2
|
-
import { t as FlumeHttpError } from "./http-error
|
|
1
|
+
import { l as safeErrorMessage } from "./flume-source.js";
|
|
2
|
+
import { t as FlumeHttpError } from "./http-error.js";
|
|
3
3
|
//#region lib/utils/safe-read-text.ts
|
|
4
4
|
/**
|
|
5
5
|
* `response.text()` を保護する。body 読み取り中の reject (接続切断 / 解凍失敗 / 二重消費) を
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { l as safeErrorMessage, n as safeInvokeCallback, s as attempt } from "./flume-source
|
|
2
|
-
import { t as FlumeConnectionError } from "./connection-error
|
|
1
|
+
import { l as safeErrorMessage, n as safeInvokeCallback, s as attempt } from "./flume-source.js";
|
|
2
|
+
import { t as FlumeConnectionError } from "./connection-error.js";
|
|
3
3
|
//#region lib/utils/safe-random.ts
|
|
4
4
|
/**
|
|
5
5
|
* `deps.random()` を保護する。throw / 範囲外値 / 非数値が返った場合は 0.5 を返す。
|
|
@@ -40,7 +40,7 @@ var FlumeReconnector = class {
|
|
|
40
40
|
if (this.isAborted) return 0;
|
|
41
41
|
if (this.currentAttempt >= this.props.maxAttempts) return -1;
|
|
42
42
|
this.clearTimer();
|
|
43
|
-
const delay = this.
|
|
43
|
+
const delay = this.computeDelay();
|
|
44
44
|
const timerResult = attempt(() => this.props.deps.setTimeout(() => this.runRetry(fn), delay));
|
|
45
45
|
if (timerResult instanceof Error) {
|
|
46
46
|
this.props.log.error({
|
|
@@ -51,6 +51,7 @@ var FlumeReconnector = class {
|
|
|
51
51
|
this.timer = null;
|
|
52
52
|
return 0;
|
|
53
53
|
}
|
|
54
|
+
this.currentAttempt++;
|
|
54
55
|
this.timer = timerResult;
|
|
55
56
|
return delay;
|
|
56
57
|
}
|
|
@@ -85,10 +86,8 @@ var FlumeReconnector = class {
|
|
|
85
86
|
});
|
|
86
87
|
this.timer = null;
|
|
87
88
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
this.currentAttempt++;
|
|
91
|
-
return jitter;
|
|
89
|
+
computeDelay() {
|
|
90
|
+
return Math.min(this.props.baseDelay * 2 ** this.currentAttempt, this.props.maxDelay) * (.5 + safeRandom({ deps: this.props.deps }) * .5);
|
|
92
91
|
}
|
|
93
92
|
};
|
|
94
93
|
//#endregion
|
|
@@ -140,11 +139,6 @@ function scheduleFlumeReconnect(props) {
|
|
|
140
139
|
});
|
|
141
140
|
}
|
|
142
141
|
//#endregion
|
|
143
|
-
//#region lib/utils/is-record.ts
|
|
144
|
-
function isRecord(value) {
|
|
145
|
-
return typeof value === "object" && value !== null;
|
|
146
|
-
}
|
|
147
|
-
//#endregion
|
|
148
142
|
//#region lib/utils/safe-stringify.ts
|
|
149
143
|
/**
|
|
150
144
|
* `JSON.stringify` を `string | Error` に変換するだけのラッパ。
|
|
@@ -154,4 +148,4 @@ function safeStringify(value) {
|
|
|
154
148
|
return attempt(() => JSON.stringify(value));
|
|
155
149
|
}
|
|
156
150
|
//#endregion
|
|
157
|
-
export { safeRandom as
|
|
151
|
+
export { safeRandom as i, scheduleFlumeReconnect as n, FlumeReconnector as r, safeStringify as t };
|
package/dist/slack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { t as FlumeSource, w as FlumeSourceStartContext, x as FlumeSlackSourceOptions, y as FlumeSlackEnvelope } from "./flume-source.js";
|
|
2
2
|
|
|
3
3
|
//#region lib/slack/slack-source.d.ts
|
|
4
4
|
declare class FlumeSlackSource extends FlumeSource {
|
package/dist/slack.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { a as FlumeStartError, c as safeNormalizeError, i as safeNow, l as safeErrorMessage, o as FlumeParseError, r as FlumeLogger, s as attempt, t as FlumeSource } from "./flume-source
|
|
2
|
-
import { t as FlumeConnectionError } from "./connection-error
|
|
3
|
-
import { t as FlumeHttpError } from "./http-error
|
|
4
|
-
import {
|
|
5
|
-
import { t as
|
|
6
|
-
import { t as
|
|
1
|
+
import { a as FlumeStartError, c as safeNormalizeError, i as safeNow, l as safeErrorMessage, o as FlumeParseError, r as FlumeLogger, s as attempt, t as FlumeSource } from "./flume-source.js";
|
|
2
|
+
import { t as FlumeConnectionError } from "./connection-error.js";
|
|
3
|
+
import { t as FlumeHttpError } from "./http-error.js";
|
|
4
|
+
import { n as scheduleFlumeReconnect, r as FlumeReconnector, t as safeStringify } from "./safe-stringify.js";
|
|
5
|
+
import { t as isRecord } from "./is-record.js";
|
|
6
|
+
import { t as safeJsonParse } from "./safe-json-parse.js";
|
|
7
|
+
import { t as safeReadText } from "./safe-read-text.js";
|
|
7
8
|
import { z } from "zod/v4";
|
|
8
9
|
//#region lib/slack/extract-slack-meta.ts
|
|
9
10
|
function flumeExtractSlackMeta(envelope) {
|
|
@@ -45,8 +46,12 @@ var FlumeSlackSeenCache = class {
|
|
|
45
46
|
const cutoff = safeNow({ deps: this.props.deps }) - this.props.ttlMs;
|
|
46
47
|
for (const [id, timestamp] of this.seen) if (timestamp < cutoff) this.seen.delete(id);
|
|
47
48
|
if (this.seen.size <= this.props.maxSize) return;
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
let removeCount = this.seen.size - this.props.maxSize;
|
|
50
|
+
for (const id of this.seen.keys()) {
|
|
51
|
+
if (removeCount <= 0) break;
|
|
52
|
+
this.seen.delete(id);
|
|
53
|
+
removeCount--;
|
|
54
|
+
}
|
|
50
55
|
}
|
|
51
56
|
get size() {
|
|
52
57
|
return this.seen.size;
|