@interactive-inc/flume 0.10.0 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/discord.d.ts +15 -1
- package/dist/discord.js +303 -34
- package/dist/flume-source.d.ts +53 -13
- package/dist/flume-source.js +141 -68
- package/dist/github.d.ts +5 -1
- package/dist/github.js +211 -61
- package/dist/http-error.js +4 -0
- package/dist/index.d.ts +116 -45
- package/dist/index.js +318 -86
- package/dist/parse-error.d.ts +9 -0
- package/dist/safe-json-parse.js +1 -1
- package/dist/safe-read-text.js +6 -3
- package/dist/safe-stringify.js +64 -28
- package/dist/slack.d.ts +8 -1
- package/dist/slack.js +228 -31
- package/dist/time.d.ts +79 -2
- package/dist/time.js +621 -2
- package/package.json +1 -1
- package/dist/parse-cron.d.ts +0 -55
- package/dist/time-source.js +0 -427
package/dist/time-source.js
DELETED
|
@@ -1,427 +0,0 @@
|
|
|
1
|
-
import { a as FlumeStartError, c as safeNormalizeError, i as safeNow, 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 isRecord } from "./is-record.js";
|
|
3
|
-
//#region lib/time/parse-cron-field.ts
|
|
4
|
-
/**
|
|
5
|
-
* 単一 cron フィールド (minute など) の spec を許可値の Set に展開する。
|
|
6
|
-
* 対応: `*` / `* /n` / `a` / `a-b` / `a-b/n` とそれらのカンマ区切り。名前 (JAN, MON) は非対応
|
|
7
|
-
*/
|
|
8
|
-
function parseCronField(spec, min, max) {
|
|
9
|
-
const values = /* @__PURE__ */ new Set();
|
|
10
|
-
for (const part of spec.split(",")) {
|
|
11
|
-
const expanded = expandCronPart(part, min, max);
|
|
12
|
-
if (expanded instanceof FlumeParseError) return expanded;
|
|
13
|
-
for (const value of expanded) values.add(value);
|
|
14
|
-
}
|
|
15
|
-
if (values.size === 0) return new FlumeParseError(`cron field empty: "${spec}"`);
|
|
16
|
-
return values;
|
|
17
|
-
}
|
|
18
|
-
function expandCronPart(part, min, max) {
|
|
19
|
-
let range = part;
|
|
20
|
-
let step = 1;
|
|
21
|
-
const slash = part.indexOf("/");
|
|
22
|
-
if (slash !== -1) {
|
|
23
|
-
range = part.slice(0, slash);
|
|
24
|
-
const parsed = Number(part.slice(slash + 1));
|
|
25
|
-
if (!Number.isInteger(parsed) || parsed <= 0) return new FlumeParseError(`invalid cron step: "${part}"`);
|
|
26
|
-
step = parsed;
|
|
27
|
-
}
|
|
28
|
-
const bounds = resolveBounds(range, min, max);
|
|
29
|
-
if (bounds instanceof FlumeParseError) return bounds;
|
|
30
|
-
const numbers = [];
|
|
31
|
-
for (let value = bounds.lo; value <= bounds.hi; value += step) numbers.push(value);
|
|
32
|
-
return numbers;
|
|
33
|
-
}
|
|
34
|
-
function resolveBounds(range, min, max) {
|
|
35
|
-
if (range === "*") return {
|
|
36
|
-
lo: min,
|
|
37
|
-
hi: max
|
|
38
|
-
};
|
|
39
|
-
const dash = range.indexOf("-");
|
|
40
|
-
const lo = dash === -1 ? Number(range) : Number(range.slice(0, dash));
|
|
41
|
-
const hi = dash === -1 ? lo : Number(range.slice(dash + 1));
|
|
42
|
-
if (!Number.isInteger(lo) || !Number.isInteger(hi)) return new FlumeParseError(`invalid cron range: "${range}"`);
|
|
43
|
-
if (lo < min || hi > max || lo > hi) return new FlumeParseError(`cron value out of range [${min}-${max}]: "${range}"`);
|
|
44
|
-
return {
|
|
45
|
-
lo,
|
|
46
|
-
hi
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
//#endregion
|
|
50
|
-
//#region lib/time/parse-cron.ts
|
|
51
|
-
/**
|
|
52
|
-
* 5 フィールド cron 式をパースする。dow は 0-7 を許可し 7 を 0 (日曜) に正規化する
|
|
53
|
-
*/
|
|
54
|
-
function parseCron(expression) {
|
|
55
|
-
const trimmed = expression.trim();
|
|
56
|
-
const fields = trimmed.split(/\s+/);
|
|
57
|
-
if (fields.length !== 5) return new FlumeParseError(`cron must have 5 fields, got ${fields.length}: "${expression}"`);
|
|
58
|
-
const minutes = parseCronField(fields[0] ?? "", 0, 59);
|
|
59
|
-
if (minutes instanceof FlumeParseError) return minutes;
|
|
60
|
-
const hours = parseCronField(fields[1] ?? "", 0, 23);
|
|
61
|
-
if (hours instanceof FlumeParseError) return hours;
|
|
62
|
-
const daysOfMonth = parseCronField(fields[2] ?? "", 1, 31);
|
|
63
|
-
if (daysOfMonth instanceof FlumeParseError) return daysOfMonth;
|
|
64
|
-
const months = parseCronField(fields[3] ?? "", 1, 12);
|
|
65
|
-
if (months instanceof FlumeParseError) return months;
|
|
66
|
-
const rawDaysOfWeek = parseCronField(fields[4] ?? "", 0, 7);
|
|
67
|
-
if (rawDaysOfWeek instanceof FlumeParseError) return rawDaysOfWeek;
|
|
68
|
-
const daysOfWeek = /* @__PURE__ */ new Set();
|
|
69
|
-
for (const value of rawDaysOfWeek) daysOfWeek.add(value === 7 ? 0 : value);
|
|
70
|
-
return {
|
|
71
|
-
source: trimmed,
|
|
72
|
-
minutes,
|
|
73
|
-
hours,
|
|
74
|
-
daysOfMonth,
|
|
75
|
-
months,
|
|
76
|
-
daysOfWeek,
|
|
77
|
-
domRestricted: fields[2] !== "*",
|
|
78
|
-
dowRestricted: fields[4] !== "*"
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
//#endregion
|
|
82
|
-
//#region lib/time/cron-next.ts
|
|
83
|
-
const MINUTE_MS = 6e4;
|
|
84
|
-
const MAX_ITERATIONS = 5e5;
|
|
85
|
-
/**
|
|
86
|
-
* `afterMs` より後の最初の cron マッチ時刻 (epoch ms) を壁時計 (local time) で求める。
|
|
87
|
-
* 到達不能なら FlumeParseError を返す
|
|
88
|
-
*/
|
|
89
|
-
function flumeCronNext(cron, afterMs) {
|
|
90
|
-
let candidate = Math.floor(afterMs / MINUTE_MS) * MINUTE_MS + MINUTE_MS;
|
|
91
|
-
for (let iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
|
|
92
|
-
const date = new Date(candidate);
|
|
93
|
-
if (!cron.months.has(date.getMonth() + 1)) {
|
|
94
|
-
candidate = new Date(date.getFullYear(), date.getMonth() + 1, 1, 0, 0, 0, 0).getTime();
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
if (!matchesDay(cron, date)) {
|
|
98
|
-
candidate = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, 0, 0, 0, 0).getTime();
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
if (!cron.hours.has(date.getHours())) {
|
|
102
|
-
candidate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours() + 1, 0, 0, 0).getTime();
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (!cron.minutes.has(date.getMinutes())) {
|
|
106
|
-
candidate += MINUTE_MS;
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
return candidate;
|
|
110
|
-
}
|
|
111
|
-
return new FlumeParseError(`cron "${cron.source}" has no next time within bound`);
|
|
112
|
-
}
|
|
113
|
-
function matchesDay(cron, date) {
|
|
114
|
-
const domMatch = cron.daysOfMonth.has(date.getDate());
|
|
115
|
-
const dowMatch = cron.daysOfWeek.has(date.getDay());
|
|
116
|
-
if (cron.domRestricted && cron.dowRestricted) return domMatch || dowMatch;
|
|
117
|
-
if (cron.domRestricted) return domMatch;
|
|
118
|
-
if (cron.dowRestricted) return dowMatch;
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
//#endregion
|
|
122
|
-
//#region lib/time/time-scheduler.ts
|
|
123
|
-
const MAX_TIMEOUT_MS = 2e9;
|
|
124
|
-
const FIRE_TOLERANCE_MS = 1e3;
|
|
125
|
-
/**
|
|
126
|
-
* cron に従って `onTick` を駆動するタイマーループ。外部接続を持たないため reconnect 不要。
|
|
127
|
-
* IO 境界は全て `attempt` 経由で扱い、停止後はコールバックを発火しない
|
|
128
|
-
*/
|
|
129
|
-
var FlumeTimeScheduler = class {
|
|
130
|
-
props;
|
|
131
|
-
log;
|
|
132
|
-
isStoppedFlag = false;
|
|
133
|
-
timer = null;
|
|
134
|
-
target = 0;
|
|
135
|
-
constructor(props) {
|
|
136
|
-
this.props = props;
|
|
137
|
-
this.log = new FlumeLogger({
|
|
138
|
-
source: "time.scheduler",
|
|
139
|
-
handler: props.onLog,
|
|
140
|
-
deps: props.deps
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
get isStopped() {
|
|
144
|
-
return this.isStoppedFlag;
|
|
145
|
-
}
|
|
146
|
-
start() {
|
|
147
|
-
const next = flumeCronNext(this.props.cron, safeNow({ deps: this.props.deps }));
|
|
148
|
-
if (next instanceof FlumeParseError) {
|
|
149
|
-
this.log.error({
|
|
150
|
-
action: "cron.no-next",
|
|
151
|
-
message: next.message,
|
|
152
|
-
error: next
|
|
153
|
-
});
|
|
154
|
-
return next;
|
|
155
|
-
}
|
|
156
|
-
this.target = next;
|
|
157
|
-
this.log.info({
|
|
158
|
-
action: "scheduler.start",
|
|
159
|
-
message: `next fire at ${new Date(next).toISOString()}`,
|
|
160
|
-
detail: { target: next }
|
|
161
|
-
});
|
|
162
|
-
this.arm();
|
|
163
|
-
return null;
|
|
164
|
-
}
|
|
165
|
-
stop() {
|
|
166
|
-
this.isStoppedFlag = true;
|
|
167
|
-
this.clearTimer();
|
|
168
|
-
}
|
|
169
|
-
arm() {
|
|
170
|
-
this.clearTimer();
|
|
171
|
-
const delay = Math.max(0, this.target - safeNow({ deps: this.props.deps }));
|
|
172
|
-
const capped = Math.min(delay, MAX_TIMEOUT_MS);
|
|
173
|
-
const result = attempt(() => this.props.deps.setTimeout(() => this.onWake(), capped));
|
|
174
|
-
if (result instanceof Error) {
|
|
175
|
-
this.log.error({
|
|
176
|
-
action: "scheduler.arm.error",
|
|
177
|
-
message: safeErrorMessage({ error: result }),
|
|
178
|
-
error: result
|
|
179
|
-
});
|
|
180
|
-
this.timer = null;
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
this.timer = result;
|
|
184
|
-
}
|
|
185
|
-
onWake() {
|
|
186
|
-
this.timer = null;
|
|
187
|
-
if (this.isStoppedFlag) return;
|
|
188
|
-
if (this.target - safeNow({ deps: this.props.deps }) > FIRE_TOLERANCE_MS) {
|
|
189
|
-
this.arm();
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const firedAt = this.target;
|
|
193
|
-
safeInvokeCallback({
|
|
194
|
-
fn: () => this.props.onTick(firedAt),
|
|
195
|
-
onError: (error) => {
|
|
196
|
-
this.log.error({
|
|
197
|
-
action: "scheduler.tick.error",
|
|
198
|
-
message: safeErrorMessage({ error }),
|
|
199
|
-
error
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
});
|
|
203
|
-
const next = flumeCronNext(this.props.cron, firedAt);
|
|
204
|
-
if (next instanceof FlumeParseError) {
|
|
205
|
-
this.log.error({
|
|
206
|
-
action: "cron.no-next",
|
|
207
|
-
message: next.message,
|
|
208
|
-
error: next
|
|
209
|
-
});
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
this.target = next;
|
|
213
|
-
this.arm();
|
|
214
|
-
}
|
|
215
|
-
clearTimer() {
|
|
216
|
-
if (this.timer === null) return;
|
|
217
|
-
const handle = this.timer;
|
|
218
|
-
const result = attempt(() => this.props.deps.clearTimeout(handle));
|
|
219
|
-
if (result instanceof Error) this.log.error({
|
|
220
|
-
action: "scheduler.timer.clear.error",
|
|
221
|
-
message: safeErrorMessage({ error: result }),
|
|
222
|
-
error: result
|
|
223
|
-
});
|
|
224
|
-
this.timer = null;
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
//#endregion
|
|
228
|
-
//#region lib/time/time-catchup.ts
|
|
229
|
-
const DEFAULT_MISSED_WINDOW_MS = 1440 * 60 * 1e3;
|
|
230
|
-
const MAX_CATCHUP_MATCHES = 1e4;
|
|
231
|
-
/**
|
|
232
|
-
* `lastFiredAt` から `now` までに過ぎ去った cron マッチを policy に従って列挙する。
|
|
233
|
-
*
|
|
234
|
-
* - policy.mode === "off" : 常に空配列
|
|
235
|
-
* - policy.mode === "lastOnly" : 過ぎ去ったマッチの中で最も新しいもの 1 件
|
|
236
|
-
* - policy.mode === "missed" : maxWindowMs (既定 24h) 以内に過ぎ去ったすべてのマッチ。
|
|
237
|
-
* window の起点は `max(lastFiredAt, now - maxWindowMs)`
|
|
238
|
-
*
|
|
239
|
-
* 到達不能 cron や catastrophic な policy ミス指定の場合は FlumeParseError を返す
|
|
240
|
-
* (catchup 列挙だけで失敗させる。source 本体の起動は別判断)
|
|
241
|
-
*/
|
|
242
|
-
function flumeCollectCatchupMatches(props) {
|
|
243
|
-
if (props.policy.mode === "off") return [];
|
|
244
|
-
if (props.lastFiredAt >= props.now) return [];
|
|
245
|
-
const windowStart = props.policy.mode === "missed" ? Math.max(props.lastFiredAt, props.now - (props.policy.maxWindowMs ?? DEFAULT_MISSED_WINDOW_MS)) : props.lastFiredAt;
|
|
246
|
-
const matches = [];
|
|
247
|
-
let cursor = windowStart;
|
|
248
|
-
for (let i = 0; i < MAX_CATCHUP_MATCHES; i++) {
|
|
249
|
-
const next = flumeCronNext(props.cron, cursor);
|
|
250
|
-
if (next instanceof FlumeParseError) return next;
|
|
251
|
-
if (next > props.now) break;
|
|
252
|
-
matches.push(next);
|
|
253
|
-
cursor = next;
|
|
254
|
-
}
|
|
255
|
-
if (props.policy.mode === "lastOnly") {
|
|
256
|
-
const last = matches[matches.length - 1];
|
|
257
|
-
return last === void 0 ? [] : [last];
|
|
258
|
-
}
|
|
259
|
-
return matches;
|
|
260
|
-
}
|
|
261
|
-
//#endregion
|
|
262
|
-
//#region lib/time/time-source.ts
|
|
263
|
-
/**
|
|
264
|
-
* cron スケジュールで tick を emit する Source。外部接続を持たないため
|
|
265
|
-
* 起動成功と同時に `connected` になり reconnect の対象外。
|
|
266
|
-
*
|
|
267
|
-
* options.statePersister + options.catchupPolicy を渡すと:
|
|
268
|
-
* 1. 起動時に lastFiredAt を読み出す
|
|
269
|
-
* 2. lastFiredAt から now までの過ぎ去った cron マッチを policy に従って再発火する
|
|
270
|
-
* 3. 各 tick 後に lastFiredAt を保存する (best-effort, ブロックしない)
|
|
271
|
-
*
|
|
272
|
-
* 保存先や形式は flume の関知ではなく statePersister の実装が決める (純粋 DI)
|
|
273
|
-
*/
|
|
274
|
-
var FlumeTimeSource = class extends FlumeSource {
|
|
275
|
-
options;
|
|
276
|
-
name = "time";
|
|
277
|
-
scheduler = null;
|
|
278
|
-
constructor(options) {
|
|
279
|
-
super();
|
|
280
|
-
this.options = options;
|
|
281
|
-
}
|
|
282
|
-
async connect(ctx) {
|
|
283
|
-
this.setStatus("connecting");
|
|
284
|
-
const cron = parseCron(this.options.cron);
|
|
285
|
-
if (cron instanceof FlumeParseError) {
|
|
286
|
-
const error = new FlumeStartError(`Time source: invalid cron "${this.options.cron}": ${cron.message}`);
|
|
287
|
-
ctx.log.error({
|
|
288
|
-
action: "source.start.failed",
|
|
289
|
-
message: safeErrorMessage({ error }),
|
|
290
|
-
error
|
|
291
|
-
});
|
|
292
|
-
this.setStatus("disconnected", error.message);
|
|
293
|
-
return error;
|
|
294
|
-
}
|
|
295
|
-
const persister = this.options.statePersister ?? null;
|
|
296
|
-
const lastFiredAt = persister === null ? null : await this.loadLastFiredAt(ctx, persister);
|
|
297
|
-
this.scheduler = new FlumeTimeScheduler({
|
|
298
|
-
cron,
|
|
299
|
-
onLog: ctx.log.handler,
|
|
300
|
-
deps: ctx.deps,
|
|
301
|
-
onTick: (firedAt) => this.handleTick(ctx, firedAt, persister)
|
|
302
|
-
});
|
|
303
|
-
const result = this.scheduler.start();
|
|
304
|
-
if (result instanceof Error) {
|
|
305
|
-
const error = new FlumeStartError(`Time source: ${safeErrorMessage({ error: result })}`);
|
|
306
|
-
this.setStatus("disconnected", error.message);
|
|
307
|
-
return error;
|
|
308
|
-
}
|
|
309
|
-
this.setStatus("connected");
|
|
310
|
-
if (lastFiredAt !== null && persister !== null) this.runCatchup({
|
|
311
|
-
ctx,
|
|
312
|
-
cron,
|
|
313
|
-
lastFiredAt,
|
|
314
|
-
persister
|
|
315
|
-
});
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
disconnect() {
|
|
319
|
-
this.scheduler?.stop();
|
|
320
|
-
this.scheduler = null;
|
|
321
|
-
}
|
|
322
|
-
handleTick(ctx, firedAt, persister) {
|
|
323
|
-
this.emitTick(ctx, firedAt);
|
|
324
|
-
if (persister !== null) this.saveLastFiredAt(ctx, persister, firedAt);
|
|
325
|
-
}
|
|
326
|
-
emitTick(ctx, firedAt) {
|
|
327
|
-
const tick = {
|
|
328
|
-
firedAt,
|
|
329
|
-
cron: this.options.cron
|
|
330
|
-
};
|
|
331
|
-
const custom = this.safeMessage(ctx, tick);
|
|
332
|
-
this.emit({
|
|
333
|
-
source: "time",
|
|
334
|
-
type: typeof custom.type === "string" ? custom.type : "tick",
|
|
335
|
-
data: isRecord(custom.data) ? custom.data : {
|
|
336
|
-
firedAt,
|
|
337
|
-
cron: this.options.cron
|
|
338
|
-
},
|
|
339
|
-
meta: this.normalizeMeta(custom.meta, this.options.cron),
|
|
340
|
-
receivedAt: safeNow({ deps: ctx.deps })
|
|
341
|
-
});
|
|
342
|
-
}
|
|
343
|
-
runCatchup(props) {
|
|
344
|
-
const policy = this.options.catchupPolicy ?? { mode: "off" };
|
|
345
|
-
if (policy.mode === "off") return;
|
|
346
|
-
const matches = flumeCollectCatchupMatches({
|
|
347
|
-
cron: props.cron,
|
|
348
|
-
lastFiredAt: props.lastFiredAt,
|
|
349
|
-
now: safeNow({ deps: props.ctx.deps }),
|
|
350
|
-
policy
|
|
351
|
-
});
|
|
352
|
-
if (matches instanceof FlumeParseError) {
|
|
353
|
-
props.ctx.log.warn({
|
|
354
|
-
action: "time.catchup.failed",
|
|
355
|
-
message: matches.message,
|
|
356
|
-
error: matches
|
|
357
|
-
});
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
if (matches.length === 0) return;
|
|
361
|
-
props.ctx.log.info({
|
|
362
|
-
action: "time.catchup.fired",
|
|
363
|
-
message: `catchup ${matches.length} missed tick(s) since ${new Date(props.lastFiredAt).toISOString()}`,
|
|
364
|
-
detail: {
|
|
365
|
-
count: matches.length,
|
|
366
|
-
policy: policy.mode
|
|
367
|
-
}
|
|
368
|
-
});
|
|
369
|
-
for (const firedAt of matches) this.emitTick(props.ctx, firedAt);
|
|
370
|
-
const last = matches[matches.length - 1];
|
|
371
|
-
if (last !== void 0) this.saveLastFiredAt(props.ctx, props.persister, last);
|
|
372
|
-
}
|
|
373
|
-
async loadLastFiredAt(ctx, persister) {
|
|
374
|
-
const result = await attempt(() => persister.load());
|
|
375
|
-
if (result instanceof Error) {
|
|
376
|
-
ctx.log.warn({
|
|
377
|
-
action: "time.state.load.error",
|
|
378
|
-
message: safeErrorMessage({ error: result }),
|
|
379
|
-
error: result
|
|
380
|
-
});
|
|
381
|
-
return null;
|
|
382
|
-
}
|
|
383
|
-
if (result === null) return null;
|
|
384
|
-
if (typeof result.lastFiredAt !== "number" || !Number.isFinite(result.lastFiredAt)) return null;
|
|
385
|
-
return result.lastFiredAt;
|
|
386
|
-
}
|
|
387
|
-
saveLastFiredAt(ctx, persister, lastFiredAt) {
|
|
388
|
-
safeInvokeCallback({
|
|
389
|
-
fn: () => persister.save({ lastFiredAt }),
|
|
390
|
-
onError: (error) => {
|
|
391
|
-
ctx.log.warn({
|
|
392
|
-
action: "time.state.save.error",
|
|
393
|
-
message: safeErrorMessage({ error: safeNormalizeError({ value: error }) }),
|
|
394
|
-
error
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
safeMessage(ctx, tick) {
|
|
400
|
-
const message = this.options.message;
|
|
401
|
-
if (!message) return {};
|
|
402
|
-
const result = attempt(() => message(tick));
|
|
403
|
-
if (result instanceof Error) {
|
|
404
|
-
const error = safeNormalizeError({ value: result });
|
|
405
|
-
ctx.log.warn({
|
|
406
|
-
action: "message.error",
|
|
407
|
-
message: safeErrorMessage({ error }),
|
|
408
|
-
error,
|
|
409
|
-
detail: { firedAt: tick.firedAt }
|
|
410
|
-
});
|
|
411
|
-
return {};
|
|
412
|
-
}
|
|
413
|
-
return isRecord(result) ? result : {};
|
|
414
|
-
}
|
|
415
|
-
normalizeMeta(meta, cron) {
|
|
416
|
-
if (!isRecord(meta)) return { cron };
|
|
417
|
-
const normalized = {};
|
|
418
|
-
for (const key of Object.keys(meta)) {
|
|
419
|
-
const value = meta[key];
|
|
420
|
-
if (typeof value === "string") normalized[key] = value;
|
|
421
|
-
}
|
|
422
|
-
if (Object.keys(normalized).length === 0) return { cron };
|
|
423
|
-
return normalized;
|
|
424
|
-
}
|
|
425
|
-
};
|
|
426
|
-
//#endregion
|
|
427
|
-
export { parseCron as i, flumeCollectCatchupMatches as n, flumeCronNext as r, FlumeTimeSource as t };
|