@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/safe-json-parse.js
CHANGED
package/dist/safe-read-text.js
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { d as safeErrorMessage, l as attempt } from "./flume-source.js";
|
|
2
|
+
//#region lib/errors/http-error.ts
|
|
3
|
+
var FlumeHttpError = class extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
retryAfterMs;
|
|
7
|
+
constructor(props) {
|
|
8
|
+
super(props.message, props.cause === void 0 ? void 0 : { cause: props.cause });
|
|
9
|
+
this.name = "FlumeHttpError";
|
|
10
|
+
this.status = props.status;
|
|
11
|
+
this.code = props.code ?? null;
|
|
12
|
+
this.retryAfterMs = props.retryAfterMs ?? null;
|
|
13
|
+
Object.freeze(this);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
3
17
|
//#region lib/utils/safe-read-text.ts
|
|
4
18
|
/**
|
|
5
19
|
* `response.text()` を保護する。body 読み取り中の reject (接続切断 / 解凍失敗 / 二重消費) を
|
|
6
|
-
* `FlumeHttpError` (status / cause 保持) に変換する。
|
|
20
|
+
* `FlumeHttpError` (status / cause 保持) に変換する。DI モックの `status` getter が throw
|
|
21
|
+
* しても reject しない。log には書かない (呼び出し側で書く)
|
|
7
22
|
*/
|
|
8
23
|
async function safeReadText(props) {
|
|
9
24
|
try {
|
|
10
25
|
return await props.response.text();
|
|
11
26
|
} catch (err) {
|
|
27
|
+
const statusResult = attempt(() => props.response.status);
|
|
28
|
+
const status = typeof statusResult === "number" ? statusResult : 0;
|
|
12
29
|
return new FlumeHttpError({
|
|
13
30
|
message: `${props.context}: failed to read body: ${safeErrorMessage({ error: err })}`,
|
|
14
|
-
status
|
|
31
|
+
status,
|
|
15
32
|
cause: err
|
|
16
33
|
});
|
|
17
34
|
}
|
|
18
35
|
}
|
|
19
36
|
//#endregion
|
|
20
|
-
export { safeReadText as t };
|
|
37
|
+
export { FlumeHttpError as n, safeReadText as t };
|
package/dist/safe-stringify.js
CHANGED
|
@@ -1,151 +1,51 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { c as FlumeParseError, l as attempt } from "./flume-source.js";
|
|
2
|
+
//#region lib/errors/connection-error.ts
|
|
3
|
+
/**
|
|
4
|
+
* 接続失敗を表す。Discord Gateway / Slack Socket Mode / その他 WebSocket 系の close で発生。
|
|
5
|
+
* `code` は接続が落ちた際の close code (Discord は 4xxx 帯が再接続可否を示す)
|
|
6
|
+
*/
|
|
7
|
+
var FlumeConnectionError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
constructor(message, options) {
|
|
10
|
+
super(message, options?.cause === void 0 ? void 0 : { cause: options.cause });
|
|
11
|
+
this.name = "FlumeConnectionError";
|
|
12
|
+
this.code = options?.code ?? null;
|
|
13
|
+
Object.freeze(this);
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
3
17
|
//#region lib/utils/safe-random.ts
|
|
4
18
|
/**
|
|
5
|
-
* `deps.random()` を保護する。throw / 範囲外値 / 非数値が返った場合は
|
|
6
|
-
* 0 以上 1 未満 (Math.random と同等)
|
|
19
|
+
* `deps.random()` を保護する。throw / 範囲外値 / 非数値が返った場合は `Math.random()` へ
|
|
20
|
+
* フォールバックする。0 以上 1 未満 (Math.random と同等) の値のみそのまま透過。
|
|
21
|
+
* `Math.random` 自体まで壊れている病的環境でのみ 0.5 を返す
|
|
7
22
|
*/
|
|
8
23
|
function safeRandom(props) {
|
|
9
24
|
try {
|
|
10
25
|
const value = props.deps.random();
|
|
11
|
-
if (typeof value
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
if (typeof value === "number" && Number.isFinite(value) && value >= 0 && value < 1) return value;
|
|
27
|
+
} catch {}
|
|
28
|
+
try {
|
|
29
|
+
const native = Math.random();
|
|
30
|
+
if (typeof native === "number" && Number.isFinite(native) && native >= 0 && native < 1) return native;
|
|
31
|
+
return .5;
|
|
14
32
|
} catch {
|
|
15
33
|
return .5;
|
|
16
34
|
}
|
|
17
35
|
}
|
|
18
36
|
//#endregion
|
|
19
|
-
//#region lib/reconnector.ts
|
|
20
|
-
/**
|
|
21
|
-
* 指数バックオフ + ジッタ付きの再接続スケジューラ。
|
|
22
|
-
* `schedule()` の戻り値: 正の delay = 予約成功 / -1 = 試行上限到達 / 0 = cancel 済み or 内部 timer 拒否。
|
|
23
|
-
* setTimeout コールバック内のユーザー fn が throw / reject しても reconnect ループは止めない
|
|
24
|
-
*/
|
|
25
|
-
var FlumeReconnector = class {
|
|
26
|
-
props;
|
|
27
|
-
currentAttempt = 0;
|
|
28
|
-
isAborted = false;
|
|
29
|
-
timer = null;
|
|
30
|
-
constructor(props) {
|
|
31
|
-
this.props = props;
|
|
32
|
-
}
|
|
33
|
-
get attempt() {
|
|
34
|
-
return this.currentAttempt;
|
|
35
|
-
}
|
|
36
|
-
get aborted() {
|
|
37
|
-
return this.isAborted;
|
|
38
|
-
}
|
|
39
|
-
schedule(fn) {
|
|
40
|
-
if (this.isAborted) return 0;
|
|
41
|
-
if (this.currentAttempt >= this.props.maxAttempts) return -1;
|
|
42
|
-
this.clearTimer();
|
|
43
|
-
const delay = this.computeDelay();
|
|
44
|
-
const timerResult = attempt(() => this.props.deps.setTimeout(() => this.runRetry(fn), delay));
|
|
45
|
-
if (timerResult instanceof Error) {
|
|
46
|
-
this.props.log.error({
|
|
47
|
-
action: "reconnect.timer.schedule.error",
|
|
48
|
-
message: safeErrorMessage({ error: timerResult }),
|
|
49
|
-
error: timerResult
|
|
50
|
-
});
|
|
51
|
-
this.timer = null;
|
|
52
|
-
return 0;
|
|
53
|
-
}
|
|
54
|
-
this.currentAttempt++;
|
|
55
|
-
this.timer = timerResult;
|
|
56
|
-
return delay;
|
|
57
|
-
}
|
|
58
|
-
reset() {
|
|
59
|
-
this.currentAttempt = 0;
|
|
60
|
-
}
|
|
61
|
-
cancel() {
|
|
62
|
-
this.isAborted = true;
|
|
63
|
-
this.clearTimer();
|
|
64
|
-
}
|
|
65
|
-
runRetry(fn) {
|
|
66
|
-
this.timer = null;
|
|
67
|
-
safeInvokeCallback({
|
|
68
|
-
fn,
|
|
69
|
-
onError: (error) => {
|
|
70
|
-
this.props.log.error({
|
|
71
|
-
action: "reconnect.timer.error",
|
|
72
|
-
message: safeErrorMessage({ error }),
|
|
73
|
-
error
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
clearTimer() {
|
|
79
|
-
if (this.timer === null) return;
|
|
80
|
-
const handle = this.timer;
|
|
81
|
-
const result = attempt(() => this.props.deps.clearTimeout(handle));
|
|
82
|
-
if (result instanceof Error) this.props.log.error({
|
|
83
|
-
action: "reconnect.timer.clear.error",
|
|
84
|
-
message: safeErrorMessage({ error: result }),
|
|
85
|
-
error: result
|
|
86
|
-
});
|
|
87
|
-
this.timer = null;
|
|
88
|
-
}
|
|
89
|
-
computeDelay() {
|
|
90
|
-
return Math.min(this.props.baseDelay * 2 ** this.currentAttempt, this.props.maxDelay) * (.5 + safeRandom({ deps: this.props.deps }) * .5);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
//#endregion
|
|
94
|
-
//#region lib/schedule-reconnect.ts
|
|
95
|
-
/**
|
|
96
|
-
* 接続が落ちた際の共通再接続スケジューラ。
|
|
97
|
-
* 再接続の設定状況 (無効 / 中止 / 試行尽き) を見極めてからステータス遷移する。
|
|
98
|
-
* - reconnector が無ければ reconnect.disabled を info ログし disconnected へ
|
|
99
|
-
* - cancel 済みなら reconnect.aborted を info ログし disconnected へ
|
|
100
|
-
* - schedule() が -1 を返したら reconnect.exhausted を error ログし disconnected へ
|
|
101
|
-
* - それ以外は reconnecting へ遷移し reconnect.scheduled を info ログ
|
|
102
|
-
*/
|
|
103
|
-
function scheduleFlumeReconnect(props) {
|
|
104
|
-
if (!props.reconnector) {
|
|
105
|
-
props.log.info({
|
|
106
|
-
action: "reconnect.disabled",
|
|
107
|
-
message: "reconnect is disabled, staying disconnected"
|
|
108
|
-
});
|
|
109
|
-
props.setStatus("disconnected");
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (props.reconnector.aborted) {
|
|
113
|
-
props.log.info({
|
|
114
|
-
action: "reconnect.aborted",
|
|
115
|
-
message: "reconnector cancelled, staying disconnected"
|
|
116
|
-
});
|
|
117
|
-
props.setStatus("disconnected");
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
const delay = props.reconnector.schedule(props.retry);
|
|
121
|
-
if (delay === -1) {
|
|
122
|
-
const error = new FlumeConnectionError(`reconnect exhausted after ${props.reconnector.attempt} attempts`);
|
|
123
|
-
props.log.error({
|
|
124
|
-
action: "reconnect.exhausted",
|
|
125
|
-
message: safeErrorMessage({ error }),
|
|
126
|
-
error
|
|
127
|
-
});
|
|
128
|
-
props.setStatus("disconnected");
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
props.setStatus("reconnecting");
|
|
132
|
-
props.log.info({
|
|
133
|
-
action: "reconnect.scheduled",
|
|
134
|
-
message: `next attempt in ${Math.round(delay)}ms`,
|
|
135
|
-
detail: {
|
|
136
|
-
attempt: props.reconnector.attempt,
|
|
137
|
-
delayMs: Math.round(delay)
|
|
138
|
-
}
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
//#endregion
|
|
142
37
|
//#region lib/utils/safe-stringify.ts
|
|
143
38
|
/**
|
|
144
|
-
* `JSON.stringify` を `string | Error`
|
|
145
|
-
* cyclic / BigInt / throwing toJSON など標準が throw するケースを Error
|
|
39
|
+
* `JSON.stringify` を `string | Error` に変換するラッパ。
|
|
40
|
+
* cyclic / BigInt / throwing toJSON など標準が throw するケースを Error として返す。
|
|
41
|
+
* `undefined` / function / symbol は `JSON.stringify` が (型定義に反して) `undefined` を
|
|
42
|
+
* 返すため、これも Error に正規化して戻り値を必ず string にする
|
|
146
43
|
*/
|
|
147
44
|
function safeStringify(value) {
|
|
148
|
-
|
|
45
|
+
const result = attempt(() => JSON.stringify(value));
|
|
46
|
+
if (result instanceof Error) return result;
|
|
47
|
+
if (typeof result !== "string") return new FlumeParseError("value is not JSON-serializable (undefined / function / symbol)");
|
|
48
|
+
return result;
|
|
149
49
|
}
|
|
150
50
|
//#endregion
|
|
151
|
-
export { safeRandom as
|
|
51
|
+
export { safeRandom as n, FlumeConnectionError as r, safeStringify as t };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { a as safeInvokeCallback, d as safeErrorMessage, l as attempt } from "./flume-source.js";
|
|
2
|
+
import { n as safeRandom, r as FlumeConnectionError } from "./safe-stringify.js";
|
|
3
|
+
//#region lib/reconnector.ts
|
|
4
|
+
const MAX_TIMER_DELAY_MS = 2147483647;
|
|
5
|
+
/**
|
|
6
|
+
* 指数バックオフ + ジッタ付きの再接続スケジューラ。
|
|
7
|
+
* setTimeout コールバック内のユーザー fn が throw / reject しても reconnect ループは止めない。
|
|
8
|
+
* `generation` は clearTimeout が throw して古い timer が生き残った場合でも
|
|
9
|
+
* stale な発火を無視するための世代トークン
|
|
10
|
+
*/
|
|
11
|
+
var FlumeReconnector = class {
|
|
12
|
+
props;
|
|
13
|
+
currentAttempt = 0;
|
|
14
|
+
isAborted = false;
|
|
15
|
+
timer = null;
|
|
16
|
+
generation = 0;
|
|
17
|
+
constructor(props) {
|
|
18
|
+
this.props = props;
|
|
19
|
+
}
|
|
20
|
+
get attempt() {
|
|
21
|
+
return this.currentAttempt;
|
|
22
|
+
}
|
|
23
|
+
get aborted() {
|
|
24
|
+
return this.isAborted;
|
|
25
|
+
}
|
|
26
|
+
schedule(fn, options) {
|
|
27
|
+
if (this.isAborted) return { kind: "refused" };
|
|
28
|
+
if (this.currentAttempt >= this.props.maxAttempts) {
|
|
29
|
+
this.generation++;
|
|
30
|
+
this.clearTimer();
|
|
31
|
+
return { kind: "exhausted" };
|
|
32
|
+
}
|
|
33
|
+
this.clearTimer();
|
|
34
|
+
const delay = this.computeDelay(options?.minDelayMs ?? 0);
|
|
35
|
+
const scheduledGeneration = ++this.generation;
|
|
36
|
+
const timerResult = attempt(() => this.props.deps.setTimeout(() => this.runRetry(fn, scheduledGeneration), delay));
|
|
37
|
+
if (timerResult instanceof Error) {
|
|
38
|
+
this.props.log.error({
|
|
39
|
+
action: "reconnect.timer.schedule.error",
|
|
40
|
+
message: safeErrorMessage({ error: timerResult }),
|
|
41
|
+
error: timerResult
|
|
42
|
+
});
|
|
43
|
+
this.timer = null;
|
|
44
|
+
return { kind: "refused" };
|
|
45
|
+
}
|
|
46
|
+
this.currentAttempt++;
|
|
47
|
+
this.timer = timerResult;
|
|
48
|
+
return {
|
|
49
|
+
kind: "scheduled",
|
|
50
|
+
delayMs: delay
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
reset() {
|
|
54
|
+
this.currentAttempt = 0;
|
|
55
|
+
}
|
|
56
|
+
cancel() {
|
|
57
|
+
this.isAborted = true;
|
|
58
|
+
this.generation++;
|
|
59
|
+
this.clearTimer();
|
|
60
|
+
}
|
|
61
|
+
runRetry(fn, scheduledGeneration) {
|
|
62
|
+
if (this.isAborted) return;
|
|
63
|
+
if (scheduledGeneration !== this.generation) return;
|
|
64
|
+
this.timer = null;
|
|
65
|
+
safeInvokeCallback({
|
|
66
|
+
fn,
|
|
67
|
+
onError: (error) => {
|
|
68
|
+
this.props.log.error({
|
|
69
|
+
action: "reconnect.timer.error",
|
|
70
|
+
message: safeErrorMessage({ error }),
|
|
71
|
+
error
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
clearTimer() {
|
|
77
|
+
if (this.timer === null) return;
|
|
78
|
+
const handle = this.timer;
|
|
79
|
+
const result = attempt(() => this.props.deps.clearTimeout(handle));
|
|
80
|
+
if (result instanceof Error) this.props.log.error({
|
|
81
|
+
action: "reconnect.timer.clear.error",
|
|
82
|
+
message: safeErrorMessage({ error: result }),
|
|
83
|
+
error: result
|
|
84
|
+
});
|
|
85
|
+
this.timer = null;
|
|
86
|
+
}
|
|
87
|
+
computeDelay(minDelayMs) {
|
|
88
|
+
const jittered = Math.min(this.props.baseDelay * 2 ** this.currentAttempt, this.props.maxDelay) * (.5 + safeRandom({ deps: this.props.deps }) * .5);
|
|
89
|
+
return Math.min(Math.max(jittered, Number.isFinite(minDelayMs) && minDelayMs > 0 ? minDelayMs : 0), MAX_TIMER_DELAY_MS);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region lib/schedule-reconnect.ts
|
|
94
|
+
/**
|
|
95
|
+
* 接続が落ちた際の共通再接続スケジューラ。
|
|
96
|
+
* 再接続の設定状況 (無効 / 中止 / 試行尽き / timer 拒否) を見極めてからステータス遷移する。
|
|
97
|
+
* - reconnector が無ければ reconnect.disabled を info ログし disconnected へ
|
|
98
|
+
* - cancel 済みなら reconnect.aborted を info ログし disconnected へ
|
|
99
|
+
* - schedule() が exhausted なら reconnect.exhausted を error ログし disconnected へ
|
|
100
|
+
* - schedule() が refused (timer 予約失敗) なら reconnect.refused を error ログし disconnected へ
|
|
101
|
+
* ("reconnecting" のまま発火しない timer を待ち続けるハングを防ぐ)
|
|
102
|
+
* - scheduled なら reconnecting へ遷移し reconnect.scheduled を info ログ
|
|
103
|
+
*/
|
|
104
|
+
function scheduleFlumeReconnect(props) {
|
|
105
|
+
if (!props.reconnector) {
|
|
106
|
+
props.log.info({
|
|
107
|
+
action: "reconnect.disabled",
|
|
108
|
+
message: "reconnect is disabled, staying disconnected"
|
|
109
|
+
});
|
|
110
|
+
props.setStatus("disconnected");
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (props.reconnector.aborted) {
|
|
114
|
+
props.log.info({
|
|
115
|
+
action: "reconnect.aborted",
|
|
116
|
+
message: "reconnector cancelled, staying disconnected"
|
|
117
|
+
});
|
|
118
|
+
props.setStatus("disconnected");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
const schedule = props.reconnector.schedule(props.retry, { minDelayMs: props.minDelayMs });
|
|
122
|
+
if (schedule.kind === "exhausted") {
|
|
123
|
+
const error = new FlumeConnectionError(`reconnect exhausted after ${props.reconnector.attempt} attempts`);
|
|
124
|
+
props.log.error({
|
|
125
|
+
action: "reconnect.exhausted",
|
|
126
|
+
message: safeErrorMessage({ error }),
|
|
127
|
+
error
|
|
128
|
+
});
|
|
129
|
+
props.setStatus("disconnected");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (schedule.kind === "refused") {
|
|
133
|
+
const error = new FlumeConnectionError("reconnect timer could not be scheduled");
|
|
134
|
+
props.log.error({
|
|
135
|
+
action: "reconnect.refused",
|
|
136
|
+
message: safeErrorMessage({ error }),
|
|
137
|
+
error
|
|
138
|
+
});
|
|
139
|
+
props.setStatus("disconnected");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
props.setStatus("reconnecting");
|
|
143
|
+
props.log.info({
|
|
144
|
+
action: "reconnect.scheduled",
|
|
145
|
+
message: `next attempt in ${Math.round(schedule.delayMs)}ms`,
|
|
146
|
+
detail: {
|
|
147
|
+
attempt: props.reconnector.attempt,
|
|
148
|
+
delayMs: Math.round(schedule.delayMs)
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
//#endregion
|
|
153
|
+
export { FlumeReconnector as n, scheduleFlumeReconnect as t };
|
package/dist/slack.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as FlumeSlackEnvelope, O as FlumeSourceStartContext, T as FlumeSlackSourceOptions, t as FlumeSource } from "./flume-source.js";
|
|
2
2
|
|
|
3
3
|
//#region lib/slack/slack-source.d.ts
|
|
4
4
|
declare class FlumeSlackSource extends FlumeSource {
|
|
@@ -13,7 +13,14 @@ declare class FlumeSlackSource extends FlumeSource {
|
|
|
13
13
|
protected disconnect(): void;
|
|
14
14
|
private hasWebSocket;
|
|
15
15
|
private connectInternal;
|
|
16
|
+
private isTerminalSlackError;
|
|
17
|
+
private minRetryDelayMs;
|
|
16
18
|
private handleMessage;
|
|
19
|
+
/**
|
|
20
|
+
* Events API の再配送は envelope_id が変わり得るため、payload.event_id があれば
|
|
21
|
+
* そちらを重複判定キーとして優先する (再配送をまたいで安定な識別子)
|
|
22
|
+
*/
|
|
23
|
+
private toDedupKey;
|
|
17
24
|
private safeExtractMeta;
|
|
18
25
|
private scheduleReconnect;
|
|
19
26
|
}
|