@interactive-inc/flume 0.4.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 +232 -95
- package/dist/discord.d.ts +5 -13
- package/dist/discord.js +48 -108
- package/dist/flume-source.d.ts +269 -0
- package/dist/flume-source.js +344 -0
- package/dist/github.d.ts +4 -12
- package/dist/github.js +33 -88
- package/dist/index.d.ts +133 -42
- package/dist/index.js +340 -59
- package/dist/is-record.js +6 -0
- package/dist/parse-error.d.ts +9 -0
- package/dist/safe-json-parse.js +11 -0
- package/dist/{safe-read-text-DgrJ4Uhl.js → safe-read-text.js} +2 -2
- package/dist/{safe-stringify-BWS-uXZP.js → safe-stringify.js} +7 -28
- package/dist/slack.d.ts +5 -13
- package/dist/slack.js +65 -118
- package/dist/time.d.ts +45 -0
- package/dist/time.js +319 -0
- package/package.json +7 -1
- package/dist/safe-invoke-callback-EpWXwfwp.js +0 -170
- package/dist/serial-queue-B9LoBc64.js +0 -162
- package/dist/types-D-tO-Mh2.d.ts +0 -154
- /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,13 +1,146 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import { t as FlumeConnectionError } from "./connection-error
|
|
3
|
-
import { t as FlumeHttpError } from "./http-error
|
|
4
|
-
//#region lib/
|
|
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
|
+
//#region lib/deps.ts
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* `globalThis.WebSocket` の現在の値を返す。
|
|
7
|
+
* 取得は呼び出しごとに行う — `createFlumeDefaultDeps()` がモジュール初期化時ではなく
|
|
8
|
+
* 呼ばれた瞬間の `globalThis.WebSocket` を見るので、jsdom / happy-dom / vitest の
|
|
9
|
+
* `beforeEach` で `globalThis.WebSocket` を差し込むテスト戦略がそのまま機能する。
|
|
10
|
+
* `WebSocket` が無い環境 (Node の素の global など) では `null` を返す。
|
|
7
11
|
*/
|
|
8
|
-
|
|
12
|
+
function resolveCurrentWebSocket() {
|
|
13
|
+
const candidate = attempt(() => globalThis.WebSocket);
|
|
14
|
+
if (candidate instanceof Error || typeof candidate !== "function") return null;
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* platform 既定の IO を束ねた `FlumeRuntimeDeps`。
|
|
19
|
+
* `FlumeTimerHandle` は不透明型 (`unknown`) のため、setTimeout / clearTimeout の戻り値・引数を
|
|
20
|
+
* platform 型と橋渡しする際に境界で `as unknown as` を使う (IO 境界の最終手段)。
|
|
21
|
+
*
|
|
22
|
+
* `WebSocket` を含む全 IO は呼び出しごとに `globalThis` から引く lazy lookup。
|
|
23
|
+
* モジュール初期化後に `globalThis.WebSocket` が差し替わる環境
|
|
24
|
+
* (テストの `beforeEach` パッチ、jsdom などのブラウザ環境エミュレータ) でも
|
|
25
|
+
* `createFlumeDefaultDeps()` が返した deps が常に最新の参照を見る。
|
|
26
|
+
*/
|
|
27
|
+
function createFlumeDefaultDeps() {
|
|
28
|
+
return {
|
|
29
|
+
fetch: (url, init) => globalThis.fetch(url, init),
|
|
30
|
+
WebSocket: resolveCurrentWebSocket(),
|
|
31
|
+
now: () => Date.now(),
|
|
32
|
+
random: () => Math.random(),
|
|
33
|
+
setTimeout: (fn, ms) => globalThis.setTimeout(fn, ms),
|
|
34
|
+
clearTimeout: (id) => globalThis.clearTimeout(id),
|
|
35
|
+
setInterval: (fn, ms) => globalThis.setInterval(fn, ms),
|
|
36
|
+
clearInterval: (id) => globalThis.clearInterval(id)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
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;
|
|
103
|
+
/**
|
|
104
|
+
* firehose (`onEvent` / `stream()`) の item を複数の pull consumer へ fan-out する内部ハブ。
|
|
105
|
+
* subscriber が居なければ publish は実質 no-op。Flume 停止時に close() で全 stream を終端する
|
|
106
|
+
*/
|
|
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 {
|
|
9
142
|
props;
|
|
10
|
-
kind = "
|
|
143
|
+
kind = "closed";
|
|
11
144
|
constructor(props) {
|
|
12
145
|
this.props = props;
|
|
13
146
|
Object.freeze(this);
|
|
@@ -15,31 +148,39 @@ var FlumeStopped = class {
|
|
|
15
148
|
statuses() {
|
|
16
149
|
return this.props.finalStatuses;
|
|
17
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
|
+
}
|
|
18
159
|
};
|
|
19
160
|
//#endregion
|
|
20
161
|
//#region lib/flume-running.ts
|
|
21
162
|
/**
|
|
22
|
-
* 稼働中の Flume。
|
|
163
|
+
* 稼働中の Flume。close() で FlumeClosed へ遷移する。signal が abort されると自動 close。
|
|
23
164
|
* 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
|
|
24
|
-
* `
|
|
165
|
+
* `runClose` の最外殻 try/catch で想定外の throw も `FlumeClosed` の resolve に変換する
|
|
25
166
|
*/
|
|
26
167
|
var FlumeRunning = class {
|
|
27
168
|
props;
|
|
28
169
|
kind = "running";
|
|
29
|
-
|
|
170
|
+
closePromise = null;
|
|
30
171
|
onAbort;
|
|
31
172
|
constructor(props) {
|
|
32
173
|
this.props = props;
|
|
33
174
|
this.onAbort = () => {
|
|
34
175
|
this.props.log.info({
|
|
35
176
|
action: "flume.abort",
|
|
36
|
-
message: "signal aborted,
|
|
177
|
+
message: "signal aborted, closing"
|
|
37
178
|
});
|
|
38
179
|
safeInvokeCallback({
|
|
39
|
-
fn: () => this.
|
|
180
|
+
fn: () => this.close(),
|
|
40
181
|
onError: (error) => {
|
|
41
182
|
this.props.log.error({
|
|
42
|
-
action: "flume.abort.
|
|
183
|
+
action: "flume.abort.close.failed",
|
|
43
184
|
message: safeErrorMessage({ error }),
|
|
44
185
|
error
|
|
45
186
|
});
|
|
@@ -59,27 +200,49 @@ var FlumeRunning = class {
|
|
|
59
200
|
}
|
|
60
201
|
}
|
|
61
202
|
}
|
|
62
|
-
|
|
63
|
-
if (this.
|
|
64
|
-
this.
|
|
65
|
-
return this.
|
|
203
|
+
close() {
|
|
204
|
+
if (this.closePromise) return this.closePromise;
|
|
205
|
+
this.closePromise = this.runClose();
|
|
206
|
+
return this.closePromise;
|
|
66
207
|
}
|
|
67
208
|
statuses() {
|
|
68
209
|
return this.snapshotStatuses();
|
|
69
210
|
}
|
|
70
|
-
|
|
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 = [];
|
|
71
230
|
try {
|
|
72
231
|
this.props.log.info({
|
|
73
|
-
action: "flume.
|
|
74
|
-
message: `
|
|
232
|
+
action: "flume.close",
|
|
233
|
+
message: `closing ${this.props.sources.length} source(s)`
|
|
75
234
|
});
|
|
76
235
|
const settled = await Promise.allSettled(this.props.sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
77
236
|
for (const [index, result] of settled.entries()) if (result.status === "rejected") {
|
|
78
237
|
const source = this.props.sources[index];
|
|
79
238
|
const name = source ? this.sourceName(source) : "?";
|
|
80
239
|
const error = safeNormalizeError({ value: result.reason });
|
|
240
|
+
closeErrors.push({
|
|
241
|
+
source: name,
|
|
242
|
+
error
|
|
243
|
+
});
|
|
81
244
|
this.props.log.error({
|
|
82
|
-
action: "flume.
|
|
245
|
+
action: "flume.close.failed",
|
|
83
246
|
message: `${name}: ${safeErrorMessage({ error })}`,
|
|
84
247
|
error,
|
|
85
248
|
detail: { source: name }
|
|
@@ -98,18 +261,26 @@ var FlumeRunning = class {
|
|
|
98
261
|
}
|
|
99
262
|
}
|
|
100
263
|
this.props.log.info({
|
|
101
|
-
action: "flume.
|
|
102
|
-
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
|
|
103
271
|
});
|
|
104
|
-
return new FlumeStopped({ finalStatuses: this.snapshotStatuses() });
|
|
105
272
|
} catch (err) {
|
|
106
273
|
const error = safeNormalizeError({ value: err });
|
|
107
274
|
this.props.log.error({
|
|
108
|
-
action: "flume.
|
|
275
|
+
action: "flume.close.unhandled",
|
|
109
276
|
message: safeErrorMessage({ error }),
|
|
110
277
|
error
|
|
111
278
|
});
|
|
112
|
-
|
|
279
|
+
this.props.hub.close();
|
|
280
|
+
return new FlumeClosed({
|
|
281
|
+
finalStatuses: this.snapshotStatuses(),
|
|
282
|
+
closeErrors
|
|
283
|
+
});
|
|
113
284
|
}
|
|
114
285
|
}
|
|
115
286
|
snapshotStatuses() {
|
|
@@ -143,42 +314,95 @@ var FlumeRunning = class {
|
|
|
143
314
|
}
|
|
144
315
|
};
|
|
145
316
|
//#endregion
|
|
317
|
+
//#region lib/reconnect-config.ts
|
|
318
|
+
const DEFAULTS = {
|
|
319
|
+
maxAttempts: Infinity,
|
|
320
|
+
baseDelay: 1e3,
|
|
321
|
+
maxDelay: 3e4
|
|
322
|
+
};
|
|
323
|
+
function resolveFlumeReconnectConfig(input) {
|
|
324
|
+
if (input === false || input === void 0) return null;
|
|
325
|
+
if (input === true) return { ...DEFAULTS };
|
|
326
|
+
return {
|
|
327
|
+
...DEFAULTS,
|
|
328
|
+
...input
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
146
332
|
//#region lib/flume.ts
|
|
147
333
|
/**
|
|
148
|
-
* 起動前の Flume。`
|
|
334
|
+
* 起動前の Flume。`open()` で `FlumeRunning` へ遷移する。
|
|
335
|
+
* コンストラクタは単一オブジェクト `{ sources, ...options }` を受け取る (`sources` のみ必須)。
|
|
336
|
+
* events も全ログも 1 本の firehose (`onEvent` push / `stream()` pull) に流れ、購読側が filter する。
|
|
149
337
|
* いずれかの source 失敗時は既に成功した source を全て `stop()` してロールバックし
|
|
150
338
|
* `FlumeStartError` を返す。
|
|
151
339
|
* `source.start()` / `source.stop()` の sync throw も `Promise.resolve().then` 経由で
|
|
152
|
-
* Promise rejection に正規化して `allSettled` で捕捉する (`
|
|
340
|
+
* Promise rejection に正規化して `allSettled` で捕捉する (`open()` は決して reject しない)
|
|
153
341
|
*/
|
|
154
342
|
var Flume = class {
|
|
155
|
-
|
|
343
|
+
options;
|
|
156
344
|
consumed = false;
|
|
157
345
|
log;
|
|
158
346
|
deps;
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
347
|
+
sources;
|
|
348
|
+
sourceEventHandler;
|
|
349
|
+
hub = new FlumeStreamHub();
|
|
350
|
+
constructor(options) {
|
|
351
|
+
this.options = options;
|
|
352
|
+
this.sources = options.sources;
|
|
353
|
+
this.deps = options.deps ?? createFlumeDefaultDeps();
|
|
162
354
|
this.log = new FlumeLogger({
|
|
163
355
|
source: "flume",
|
|
164
|
-
handler:
|
|
356
|
+
handler: this.buildLogHandler(),
|
|
165
357
|
deps: this.deps
|
|
166
358
|
});
|
|
359
|
+
this.sourceEventHandler = (event) => this.emitItem({
|
|
360
|
+
kind: "event",
|
|
361
|
+
event
|
|
362
|
+
});
|
|
363
|
+
}
|
|
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
|
+
};
|
|
167
377
|
}
|
|
168
|
-
|
|
169
|
-
|
|
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();
|
|
170
393
|
if (guard) return guard;
|
|
171
394
|
this.consumed = true;
|
|
172
395
|
this.log.info({
|
|
173
|
-
action: "flume.
|
|
174
|
-
message: `
|
|
175
|
-
detail: { count: this.
|
|
396
|
+
action: "flume.open",
|
|
397
|
+
message: `opening ${this.sources.length} source(s)`,
|
|
398
|
+
detail: { count: this.sources.length }
|
|
176
399
|
});
|
|
177
|
-
const
|
|
400
|
+
const reconnect = resolveFlumeReconnectConfig(this.options.reconnect);
|
|
401
|
+
const settled = await Promise.allSettled(this.sources.map((source) => this.safeStart(source, reconnect)));
|
|
178
402
|
const failures = [];
|
|
179
403
|
const started = [];
|
|
180
404
|
for (const [index, result] of settled.entries()) {
|
|
181
|
-
const source = this.
|
|
405
|
+
const source = this.sources[index];
|
|
182
406
|
if (source === void 0) continue;
|
|
183
407
|
const name = this.sourceName(source);
|
|
184
408
|
if (result.status === "rejected") {
|
|
@@ -206,48 +430,49 @@ var Flume = class {
|
|
|
206
430
|
});
|
|
207
431
|
await this.rollback(started);
|
|
208
432
|
const detail = failures.map((f) => `${f.name}: ${safeErrorMessage({ error: f.error })}`).join("; ");
|
|
209
|
-
const error = new FlumeStartError(`Flume.
|
|
433
|
+
const error = new FlumeStartError(`Flume.open: ${failures.length} source(s) failed: ${detail}`);
|
|
210
434
|
this.log.error({
|
|
211
|
-
action: "flume.
|
|
435
|
+
action: "flume.open.failed",
|
|
212
436
|
message: safeErrorMessage({ error }),
|
|
213
437
|
error
|
|
214
438
|
});
|
|
215
439
|
return error;
|
|
216
440
|
}
|
|
217
441
|
if (this.isSignalAborted()) {
|
|
218
|
-
await this.rollback(this.
|
|
219
|
-
const error = new FlumeStartError("Flume.
|
|
442
|
+
await this.rollback(this.sources);
|
|
443
|
+
const error = new FlumeStartError("Flume.open: aborted during open");
|
|
220
444
|
this.log.warn({
|
|
221
|
-
action: "flume.
|
|
445
|
+
action: "flume.open.aborted",
|
|
222
446
|
message: safeErrorMessage({ error }),
|
|
223
447
|
error
|
|
224
448
|
});
|
|
225
449
|
return error;
|
|
226
450
|
}
|
|
227
451
|
this.log.info({
|
|
228
|
-
action: "flume.
|
|
229
|
-
message: "all sources
|
|
452
|
+
action: "flume.open.complete",
|
|
453
|
+
message: "all sources opened"
|
|
230
454
|
});
|
|
231
455
|
return new FlumeRunning({
|
|
232
|
-
sources: this.
|
|
233
|
-
signal: this.
|
|
234
|
-
log: this.log
|
|
456
|
+
sources: this.sources,
|
|
457
|
+
signal: this.options.signal,
|
|
458
|
+
log: this.log,
|
|
459
|
+
hub: this.hub
|
|
235
460
|
});
|
|
236
461
|
}
|
|
237
|
-
|
|
462
|
+
guardOpen() {
|
|
238
463
|
if (this.consumed) {
|
|
239
|
-
const error = new FlumeStartError("Flume.
|
|
464
|
+
const error = new FlumeStartError("Flume.open: already opened");
|
|
240
465
|
this.log.warn({
|
|
241
|
-
action: "flume.
|
|
466
|
+
action: "flume.open.refused",
|
|
242
467
|
message: safeErrorMessage({ error }),
|
|
243
468
|
error
|
|
244
469
|
});
|
|
245
470
|
return error;
|
|
246
471
|
}
|
|
247
472
|
if (this.isSignalAborted()) {
|
|
248
|
-
const error = new FlumeStartError("Flume.
|
|
473
|
+
const error = new FlumeStartError("Flume.open: signal already aborted");
|
|
249
474
|
this.log.warn({
|
|
250
|
-
action: "flume.
|
|
475
|
+
action: "flume.open.refused",
|
|
251
476
|
message: safeErrorMessage({ error }),
|
|
252
477
|
error
|
|
253
478
|
});
|
|
@@ -256,7 +481,7 @@ var Flume = class {
|
|
|
256
481
|
return null;
|
|
257
482
|
}
|
|
258
483
|
isSignalAborted() {
|
|
259
|
-
const signal = this.
|
|
484
|
+
const signal = this.options.signal;
|
|
260
485
|
if (!signal) return false;
|
|
261
486
|
const result = attempt(() => signal.aborted === true);
|
|
262
487
|
return result instanceof Error ? true : result;
|
|
@@ -267,8 +492,16 @@ var Flume = class {
|
|
|
267
492
|
if (typeof result !== "string") return "?";
|
|
268
493
|
return result;
|
|
269
494
|
}
|
|
270
|
-
safeStart(source,
|
|
271
|
-
|
|
495
|
+
safeStart(source, reconnect) {
|
|
496
|
+
const name = this.sourceName(source);
|
|
497
|
+
const ctx = {
|
|
498
|
+
onEvent: this.sourceEventHandler,
|
|
499
|
+
log: this.log.child(name),
|
|
500
|
+
deps: this.deps,
|
|
501
|
+
reconnect,
|
|
502
|
+
signal: this.options.signal
|
|
503
|
+
};
|
|
504
|
+
return Promise.resolve().then(() => source.start(ctx));
|
|
272
505
|
}
|
|
273
506
|
async rollback(sources) {
|
|
274
507
|
const settled = await Promise.allSettled(sources.map((source) => Promise.resolve().then(() => source.stop())));
|
|
@@ -286,4 +519,52 @@ var Flume = class {
|
|
|
286
519
|
}
|
|
287
520
|
};
|
|
288
521
|
//#endregion
|
|
289
|
-
|
|
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 };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { o as FlumeParseError } from "./flume-source.js";
|
|
2
|
+
//#region lib/utils/safe-json-parse.ts
|
|
3
|
+
function safeJsonParse(raw) {
|
|
4
|
+
try {
|
|
5
|
+
return JSON.parse(raw);
|
|
6
|
+
} catch (error) {
|
|
7
|
+
return new FlumeParseError(error instanceof Error ? `invalid JSON: ${error.message}` : "invalid JSON", { cause: error });
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
//#endregion
|
|
11
|
+
export { safeJsonParse as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { l as safeErrorMessage } from "./
|
|
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,
|
|
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,28 +86,11 @@ 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
|
|
95
|
-
//#region lib/reconnect-config.ts
|
|
96
|
-
const DEFAULTS = {
|
|
97
|
-
maxAttempts: Infinity,
|
|
98
|
-
baseDelay: 1e3,
|
|
99
|
-
maxDelay: 3e4
|
|
100
|
-
};
|
|
101
|
-
function resolveFlumeReconnectConfig(input) {
|
|
102
|
-
if (input === false || input === void 0) return null;
|
|
103
|
-
if (input === true) return { ...DEFAULTS };
|
|
104
|
-
return {
|
|
105
|
-
...DEFAULTS,
|
|
106
|
-
...input
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
//#endregion
|
|
110
94
|
//#region lib/schedule-reconnect.ts
|
|
111
95
|
/**
|
|
112
96
|
* 接続が落ちた際の共通再接続スケジューラ。
|
|
@@ -155,11 +139,6 @@ function scheduleFlumeReconnect(props) {
|
|
|
155
139
|
});
|
|
156
140
|
}
|
|
157
141
|
//#endregion
|
|
158
|
-
//#region lib/utils/is-record.ts
|
|
159
|
-
function isRecord(value) {
|
|
160
|
-
return typeof value === "object" && value !== null;
|
|
161
|
-
}
|
|
162
|
-
//#endregion
|
|
163
142
|
//#region lib/utils/safe-stringify.ts
|
|
164
143
|
/**
|
|
165
144
|
* `JSON.stringify` を `string | Error` に変換するだけのラッパ。
|
|
@@ -169,4 +148,4 @@ function safeStringify(value) {
|
|
|
169
148
|
return attempt(() => JSON.stringify(value));
|
|
170
149
|
}
|
|
171
150
|
//#endregion
|
|
172
|
-
export {
|
|
151
|
+
export { safeRandom as i, scheduleFlumeReconnect as n, FlumeReconnector as r, safeStringify as t };
|
package/dist/slack.d.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
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
|
-
declare class FlumeSlackSource {
|
|
4
|
+
declare class FlumeSlackSource extends FlumeSource {
|
|
5
5
|
private readonly options;
|
|
6
6
|
readonly name: "slack";
|
|
7
7
|
private socket;
|
|
8
8
|
private reconnector;
|
|
9
|
-
private handler;
|
|
10
9
|
private internalController;
|
|
11
|
-
private
|
|
12
|
-
private readonly deps;
|
|
13
|
-
private readonly queue;
|
|
14
|
-
private readonly seen;
|
|
15
|
-
private readonly signals;
|
|
16
|
-
private readonly statusEmitter;
|
|
17
|
-
private readonly onSignalAbort;
|
|
10
|
+
private seen;
|
|
18
11
|
constructor(options: FlumeSlackSourceOptions);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
status(): FlumeStatus;
|
|
12
|
+
protected connect(ctx: FlumeSourceStartContext): Promise<Error | null>;
|
|
13
|
+
protected disconnect(): void;
|
|
22
14
|
private hasWebSocket;
|
|
23
15
|
private connectInternal;
|
|
24
16
|
private handleMessage;
|