@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/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-DUvt9aJt.js";
2
- import { t as FlumeConnectionError } from "./connection-error-HUO3PC3G.js";
3
- import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
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
- const wsCandidate = attempt(() => globalThis.WebSocket);
6
- const cachedWebSocket = wsCandidate instanceof Error || typeof wsCandidate !== "function" ? null : wsCandidate;
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: cachedWebSocket,
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-stopped.ts
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 FlumeStopped = class {
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 = "stopped";
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。stop() で FlumeStopped へ遷移する。signal が abort されると自動 stop
163
+ * 稼働中の Flume。close() で FlumeClosed へ遷移する。signal が abort されると自動 close
44
164
  * 全ての source 呼び出し・signal 操作・status 読み取りを `attempt` 経由で扱い、
45
- * `runStop` の最外殻 try/catch で想定外の throw も `FlumeStopped` の resolve に変換する
165
+ * `runClose` の最外殻 try/catch で想定外の throw も `FlumeClosed` の resolve に変換する
46
166
  */
47
167
  var FlumeRunning = class {
48
168
  props;
49
169
  kind = "running";
50
- stopPromise = null;
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, stopping"
177
+ message: "signal aborted, closing"
58
178
  });
59
179
  safeInvokeCallback({
60
- fn: () => this.stop(),
180
+ fn: () => this.close(),
61
181
  onError: (error) => {
62
182
  this.props.log.error({
63
- action: "flume.abort.stop.failed",
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
- stop() {
84
- if (this.stopPromise) return this.stopPromise;
85
- this.stopPromise = this.runStop();
86
- return this.stopPromise;
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
- async runStop() {
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.stop",
95
- message: `stopping ${this.props.sources.length} source(s)`
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.stop.failed",
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.stop.complete",
123
- message: "all sources stopped"
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.stop.unhandled",
275
+ action: "flume.close.unhandled",
130
276
  message: safeErrorMessage({ error }),
131
277
  error
132
278
  });
133
- return new FlumeStopped({ finalStatuses: this.snapshotStatuses() });
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。`start()` で `FlumeRunning` へ遷移する。
186
- * 第一引数は sources、第二引数は cross-cutting options (全て optional)。
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` で捕捉する (`start()` は決して reject しない)
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
- onEvent;
200
- constructor(sources, options = {}) {
201
- this.sources = sources;
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: options.onLog,
356
+ handler: this.buildLogHandler(),
207
357
  deps: this.deps
208
358
  });
209
- this.onEvent = options.onEvent ?? noopOnEvent;
359
+ this.sourceEventHandler = (event) => this.emitItem({
360
+ kind: "event",
361
+ event
362
+ });
210
363
  }
211
- async start() {
212
- const guard = this.guardStart();
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.start",
217
- message: `starting ${this.sources.length} source(s)`,
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.start: ${failures.length} source(s) failed: ${detail}`);
433
+ const error = new FlumeStartError(`Flume.open: ${failures.length} source(s) failed: ${detail}`);
254
434
  this.log.error({
255
- action: "flume.start.failed",
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.start: aborted during start");
443
+ const error = new FlumeStartError("Flume.open: aborted during open");
264
444
  this.log.warn({
265
- action: "flume.start.aborted",
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.start.complete",
273
- message: "all sources started"
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
- guardStart() {
462
+ guardOpen() {
282
463
  if (this.consumed) {
283
- const error = new FlumeStartError("Flume.start: already started");
464
+ const error = new FlumeStartError("Flume.open: already opened");
284
465
  this.log.warn({
285
- action: "flume.start.refused",
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.start: signal already aborted");
473
+ const error = new FlumeStartError("Flume.open: signal already aborted");
293
474
  this.log.warn({
294
- action: "flume.start.refused",
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.onEvent,
498
+ onEvent: this.sourceEventHandler,
318
499
  log: this.log.child(name),
319
500
  deps: this.deps,
320
- onStatus: (status, detail) => this.notifyStatus(name, status, detail),
321
- reconnect
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
- export { Flume, FlumeConnectionError, FlumeHttpError, FlumeParseError, FlumeRunning, FlumeSource, FlumeStartError, FlumeStopped, createFlumeDefaultDeps };
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,6 @@
1
+ //#region lib/utils/is-record.ts
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null;
4
+ }
5
+ //#endregion
6
+ export { isRecord as t };
@@ -0,0 +1,9 @@
1
+ //#region lib/errors/parse-error.d.ts
2
+ type Options = {
3
+ cause?: unknown;
4
+ };
5
+ declare class FlumeParseError extends Error {
6
+ constructor(message: string, options?: Options);
7
+ }
8
+ //#endregion
9
+ export { FlumeParseError as t };
@@ -1,4 +1,4 @@
1
- import { o as FlumeParseError } from "./flume-source-DUvt9aJt.js";
1
+ import { o as FlumeParseError } from "./flume-source.js";
2
2
  //#region lib/utils/safe-json-parse.ts
3
3
  function safeJsonParse(raw) {
4
4
  try {
@@ -1,5 +1,5 @@
1
- import { l as safeErrorMessage } from "./flume-source-DUvt9aJt.js";
2
- import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
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-DUvt9aJt.js";
2
- import { t as FlumeConnectionError } from "./connection-error-HUO3PC3G.js";
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.nextDelay();
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
- nextDelay() {
89
- const jitter = Math.min(this.props.baseDelay * 2 ** this.currentAttempt, this.props.maxDelay) * (.5 + safeRandom({ deps: this.props.deps }) * .5);
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 a, FlumeReconnector as i, isRecord as n, scheduleFlumeReconnect as r, safeStringify as t };
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 { C as FlumeSourceStartContext, b as FlumeSlackSourceOptions, t as FlumeSource, v as FlumeSlackEnvelope } from "./flume-source-DuUFPhSe.js";
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-DUvt9aJt.js";
2
- import { t as FlumeConnectionError } from "./connection-error-HUO3PC3G.js";
3
- import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
4
- import { i as FlumeReconnector, n as isRecord, r as scheduleFlumeReconnect, t as safeStringify } from "./safe-stringify-DbWQw9qe.js";
5
- import { t as safeJsonParse } from "./safe-json-parse-CfJjt-RY.js";
6
- import { t as safeReadText } from "./safe-read-text-JQd_5vbd.js";
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
- const entries = [...this.seen.entries()];
49
- this.seen = new Map(entries.slice(entries.length - this.props.maxSize));
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;