@interactive-inc/flume 0.3.0 → 0.4.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/github.js CHANGED
@@ -1,9 +1,10 @@
1
- import { n as createFlumeDefaultDeps, t as FlumeLogger } from "./logger-CpGB9WO_.js";
2
- import { t as FlumeSerialQueue } from "./serial-queue-ExmlnpzQ.js";
3
- import { t as safeFetch } from "./safe-fetch-30ZzOKHL.js";
1
+ import { a as FlumeParseError, c as safeNormalizeError, i as FlumeStartError, l as safeErrorMessage, n as FlumeLogger, o as createFlumeDefaultDeps, r as safeNow, s as attempt, t as safeInvokeCallback } from "./safe-invoke-callback-EpWXwfwp.js";
2
+ import { t as FlumeHttpError } from "./http-error-CPSKoSie.js";
3
+ import { i as safeJsonParse, n as FlumeStatusEmitter, r as FlumeSignalRegistry, t as FlumeSerialQueue } from "./serial-queue-B9LoBc64.js";
4
+ import { t as safeReadText } from "./safe-read-text-DgrJ4Uhl.js";
4
5
  import { z } from "zod/v4";
5
6
  //#region lib/github/extract-github-meta.ts
6
- function extractGitHubMeta(notification) {
7
+ function flumeExtractGitHubMeta(notification) {
7
8
  return {
8
9
  event_type: "notification",
9
10
  reason: notification.reason,
@@ -51,15 +52,28 @@ var FlumeGitHubSeenCache = class {
51
52
  };
52
53
  //#endregion
53
54
  //#region lib/github/github-poller.ts
55
+ const NOTIFICATIONS_URL = "https://api.github.com/notifications";
56
+ const CONSECUTIVE_ERRORS_TO_DISCONNECT = 3;
57
+ const SEEN_CACHE_MAX = 5e3;
58
+ /**
59
+ * GitHub /notifications を条件付きポーリングする (ETag / Last-Modified)。
60
+ * 304 / X-Poll-Interval / レート制限を尊重し、stop() / abort() で in-flight 通信を打ち切る
61
+ */
54
62
  var FlumeGitHubPoller = class {
55
63
  props;
56
64
  log;
57
- cache = new FlumeGitHubSeenCache({ maxSize: 5e3 });
65
+ cache = new FlumeGitHubSeenCache({ maxSize: SEEN_CACHE_MAX });
58
66
  timer = null;
67
+ rateLimitTimer = null;
59
68
  since = null;
69
+ etag = null;
70
+ lastModified = null;
60
71
  bootstrapped = false;
61
- stopped = false;
72
+ isStoppedFlag = false;
73
+ inFlight = false;
62
74
  consecutiveErrors = 0;
75
+ effectiveIntervalSec;
76
+ controller = null;
63
77
  constructor(props) {
64
78
  this.props = props;
65
79
  this.log = new FlumeLogger({
@@ -67,45 +81,125 @@ var FlumeGitHubPoller = class {
67
81
  handler: props.onLog,
68
82
  deps: props.deps
69
83
  });
84
+ this.effectiveIntervalSec = props.interval;
85
+ }
86
+ get isStopped() {
87
+ return this.isStoppedFlag;
70
88
  }
71
89
  async start() {
72
- this.stopped = false;
90
+ this.isStoppedFlag = false;
91
+ const controllerResult = attempt(() => new AbortController());
92
+ if (controllerResult instanceof Error) {
93
+ this.log.error({
94
+ action: "github.abort-controller.new.error",
95
+ message: safeErrorMessage({ error: controllerResult }),
96
+ error: controllerResult
97
+ });
98
+ this.controller = null;
99
+ } else this.controller = controllerResult;
73
100
  this.log.info({
74
- action: "start",
75
- message: `polling every ${this.props.interval}s`
101
+ action: "poller.start",
102
+ message: `polling every ${this.effectiveIntervalSec}s`
76
103
  });
77
- await this.poll();
78
- this.timer = this.props.deps.setInterval(() => {
79
- this.poll().catch((err) => {
80
- this.log.error({
81
- action: "poll.unhandled",
82
- message: "unexpected error in poll loop",
83
- error: err instanceof Error ? err : new Error(String(err))
84
- });
85
- });
86
- }, this.props.interval * 1e3);
104
+ const error = await this.poll();
105
+ if (error) return error;
106
+ if (this.isStoppedFlag) return null;
107
+ this.scheduleInterval();
108
+ return null;
87
109
  }
88
110
  stop() {
89
111
  this.log.info({
90
- action: "stop",
112
+ action: "poller.stop",
91
113
  message: "stopping poller"
92
114
  });
93
- this.stopped = true;
94
- if (this.timer !== null) {
95
- this.props.deps.clearInterval(this.timer);
115
+ this.isStoppedFlag = true;
116
+ this.controller?.abort();
117
+ this.controller = null;
118
+ this.clearTimer();
119
+ this.clearRateLimitTimer();
120
+ }
121
+ scheduleInterval() {
122
+ this.clearTimer();
123
+ const intervalResult = attempt(() => this.props.deps.setInterval(() => {
124
+ this.poll().catch((err) => {
125
+ const error = safeNormalizeError({ value: err });
126
+ this.log.error({
127
+ action: "poll.unhandled",
128
+ message: safeErrorMessage({ error }),
129
+ error
130
+ });
131
+ }).catch(() => {});
132
+ }, this.effectiveIntervalSec * 1e3));
133
+ if (intervalResult instanceof Error) {
134
+ this.log.error({
135
+ action: "poller.interval.schedule.error",
136
+ message: safeErrorMessage({ error: intervalResult }),
137
+ error: intervalResult
138
+ });
96
139
  this.timer = null;
140
+ if (!this.isStoppedFlag) this.props.onDisconnected("interval scheduling rejected by runtime");
141
+ return;
97
142
  }
143
+ this.timer = intervalResult;
98
144
  }
99
145
  async poll() {
100
- const params = new URLSearchParams({ all: "false" });
146
+ if (this.inFlight || this.isStoppedFlag) return null;
147
+ this.inFlight = true;
148
+ try {
149
+ return await this.pollOnce();
150
+ } catch (err) {
151
+ const cause = safeNormalizeError({ value: err });
152
+ const error = new FlumeHttpError({
153
+ message: `poll loop threw: ${safeErrorMessage({ error: cause })}`,
154
+ status: 0,
155
+ cause
156
+ });
157
+ this.log.error({
158
+ action: "poll.unhandled",
159
+ message: safeErrorMessage({ error }),
160
+ error
161
+ });
162
+ if (!this.bootstrapped) return error;
163
+ return null;
164
+ } finally {
165
+ this.inFlight = false;
166
+ }
167
+ }
168
+ async pollOnce() {
169
+ const params = new URLSearchParams({
170
+ all: "false",
171
+ per_page: "50"
172
+ });
101
173
  if (this.since) params.set("since", this.since);
102
- const url = `https://api.github.com/notifications?${params}`;
174
+ const url = `${NOTIFICATIONS_URL}?${params}`;
103
175
  this.log.debug({
104
176
  action: "http.request",
105
177
  message: `GET ${url}`
106
178
  });
107
- const response = await this.safeFetch(url);
108
- if (response instanceof Error) return;
179
+ const headers = {
180
+ Authorization: `Bearer ${this.props.token}`,
181
+ Accept: "application/vnd.github+json",
182
+ "X-GitHub-Api-Version": "2022-11-28"
183
+ };
184
+ if (this.etag) headers["If-None-Match"] = this.etag;
185
+ if (this.lastModified) headers["If-Modified-Since"] = this.lastModified;
186
+ const response = await attempt(() => this.props.deps.fetch(url, {
187
+ headers,
188
+ signal: this.controller?.signal
189
+ }));
190
+ if (this.isStoppedFlag) return null;
191
+ if (response instanceof Error) {
192
+ this.log.error({
193
+ action: "http.error",
194
+ message: safeErrorMessage({ error: response }),
195
+ error: response
196
+ });
197
+ return this.recordFailure({
198
+ kind: "network",
199
+ message: response.message,
200
+ cause: response
201
+ });
202
+ }
109
203
  this.log.debug({
110
204
  action: "http.response",
111
205
  message: `GET ${response.status}`,
@@ -114,45 +208,175 @@ var FlumeGitHubPoller = class {
114
208
  url
115
209
  }
116
210
  });
211
+ this.maybeWidenInterval(response.headers.get("X-Poll-Interval"));
212
+ if (response.status === 304) {
213
+ this.consecutiveErrors = 0;
214
+ this.log.debug({
215
+ action: "poll.not-modified",
216
+ message: "304 Not Modified"
217
+ });
218
+ return null;
219
+ }
220
+ if (this.isRateLimited(response)) {
221
+ this.handleRateLimit(response);
222
+ return null;
223
+ }
117
224
  if (!response.ok) {
118
- this.consecutiveErrors++;
119
- this.log.error({
120
- action: "http.error",
121
- message: `HTTP ${response.status} (consecutive=${this.consecutiveErrors})`
225
+ const error = new FlumeHttpError({
226
+ message: `HTTP ${response.status}`,
227
+ status: response.status
228
+ });
229
+ return this.recordFailure({
230
+ kind: "http",
231
+ message: safeErrorMessage({ error }),
232
+ error
122
233
  });
123
- if (this.consecutiveErrors >= 3) this.props.onDisconnected(`HTTP ${response.status}`);
124
- return;
125
234
  }
126
235
  this.consecutiveErrors = 0;
127
- const body = await response.json();
128
- if (!Array.isArray(body)) {
236
+ this.etag = response.headers.get("ETag");
237
+ this.lastModified = response.headers.get("Last-Modified");
238
+ const text = await safeReadText({
239
+ response,
240
+ context: "notifications"
241
+ });
242
+ if (this.isStoppedFlag) return null;
243
+ if (text instanceof FlumeHttpError) {
129
244
  this.log.warn({
130
- action: "http.body",
245
+ action: "http.body.read",
246
+ message: safeErrorMessage({ error: text }),
247
+ error: text
248
+ });
249
+ return this.recordFailure({
250
+ kind: "http",
251
+ message: text.message,
252
+ error: text
253
+ });
254
+ }
255
+ const json = safeJsonParse(text);
256
+ if (json instanceof FlumeParseError) {
257
+ this.log.warn({
258
+ action: "http.body.parse",
259
+ message: json.message,
260
+ error: json
261
+ });
262
+ return null;
263
+ }
264
+ if (!Array.isArray(json)) {
265
+ this.log.warn({
266
+ action: "http.body.shape",
131
267
  message: "response body is not an array, dropping",
132
- detail: { bodyType: typeof body }
268
+ detail: { bodyType: typeof json }
133
269
  });
134
- return;
270
+ return null;
135
271
  }
136
- this.processNotifications(body);
272
+ this.processNotifications(json);
273
+ return null;
137
274
  }
138
- processNotifications(raw) {
139
- let dropped = 0;
140
- const notifications = raw.flatMap((item) => {
141
- const parsed = FlumeGitHubNotificationSchema.safeParse(item);
142
- if (!parsed.success) {
143
- dropped++;
144
- this.log.warn({
145
- action: "parse.skip",
146
- message: "notification did not match schema",
147
- detail: { issues: parsed.error.issues.map((i) => ({
148
- path: i.path,
149
- message: i.message
150
- })) }
151
- });
152
- return [];
275
+ recordFailure(input) {
276
+ this.consecutiveErrors++;
277
+ const error = input.error ?? new FlumeHttpError({
278
+ message: input.message,
279
+ status: 0,
280
+ cause: input.cause
281
+ });
282
+ this.log.error({
283
+ action: "http.error",
284
+ message: input.message,
285
+ error,
286
+ detail: { consecutiveErrors: this.consecutiveErrors }
287
+ });
288
+ if (this.consecutiveErrors >= CONSECUTIVE_ERRORS_TO_DISCONNECT && !this.isStoppedFlag) this.props.onDisconnected(input.kind === "network" ? "network error" : input.message);
289
+ if (!this.bootstrapped) return error;
290
+ return null;
291
+ }
292
+ isRateLimited(response) {
293
+ if (response.status === 429) return true;
294
+ if (response.status === 403 && response.headers.get("X-RateLimit-Remaining") === "0") return true;
295
+ return false;
296
+ }
297
+ handleRateLimit(response) {
298
+ const retryAfter = response.headers.get("Retry-After");
299
+ const reset = response.headers.get("X-RateLimit-Reset");
300
+ const nowSec = Math.floor(safeNow({ deps: this.props.deps }) / 1e3);
301
+ const retryAfterSec = retryAfter !== null ? Number.parseInt(retryAfter, 10) : NaN;
302
+ const resetSec = reset !== null ? Number.parseInt(reset, 10) - nowSec : NaN;
303
+ const delaySec = Number.isFinite(retryAfterSec) && retryAfterSec > 0 ? retryAfterSec : Number.isFinite(resetSec) && resetSec > 0 ? resetSec : 60;
304
+ this.log.warn({
305
+ action: "rate.limit",
306
+ message: `rate limited, pausing for ${delaySec}s`,
307
+ detail: {
308
+ status: response.status,
309
+ delaySec
153
310
  }
154
- return [parsed.data];
155
311
  });
312
+ this.clearTimer();
313
+ this.clearRateLimitTimer();
314
+ const timerResult = attempt(() => this.props.deps.setTimeout(() => {
315
+ this.rateLimitTimer = null;
316
+ if (this.isStoppedFlag) return;
317
+ this.scheduleInterval();
318
+ }, delaySec * 1e3));
319
+ if (timerResult instanceof Error) {
320
+ this.log.error({
321
+ action: "poller.rate-limit.schedule.error",
322
+ message: safeErrorMessage({ error: timerResult }),
323
+ error: timerResult
324
+ });
325
+ this.rateLimitTimer = null;
326
+ } else this.rateLimitTimer = timerResult;
327
+ }
328
+ clearTimer() {
329
+ if (this.timer === null) return;
330
+ const handle = this.timer;
331
+ const result = attempt(() => this.props.deps.clearInterval(handle));
332
+ if (result instanceof Error) this.log.error({
333
+ action: "poller.timer.clear.error",
334
+ message: safeErrorMessage({ error: result }),
335
+ error: result
336
+ });
337
+ this.timer = null;
338
+ }
339
+ clearRateLimitTimer() {
340
+ if (this.rateLimitTimer === null) return;
341
+ const handle = this.rateLimitTimer;
342
+ const result = attempt(() => this.props.deps.clearTimeout(handle));
343
+ if (result instanceof Error) this.log.error({
344
+ action: "poller.rate-limit.clear.error",
345
+ message: safeErrorMessage({ error: result }),
346
+ error: result
347
+ });
348
+ this.rateLimitTimer = null;
349
+ }
350
+ maybeWidenInterval(headerValue) {
351
+ if (headerValue === null) return;
352
+ const required = Number.parseInt(headerValue, 10);
353
+ if (!Number.isFinite(required) || required <= this.effectiveIntervalSec) return;
354
+ this.log.info({
355
+ action: "poll.widen-interval",
356
+ message: `widening interval ${this.effectiveIntervalSec}s -> ${required}s per X-Poll-Interval`,
357
+ detail: {
358
+ from: this.effectiveIntervalSec,
359
+ to: required
360
+ }
361
+ });
362
+ this.effectiveIntervalSec = required;
363
+ if (this.timer !== null) this.scheduleInterval();
364
+ }
365
+ processNotifications(raw) {
366
+ const parsedResults = raw.map((item) => FlumeGitHubNotificationSchema.safeParse(item));
367
+ for (const result of parsedResults) {
368
+ if (result.success) continue;
369
+ this.log.warn({
370
+ action: "parse.skip",
371
+ message: "notification did not match schema",
372
+ detail: { issues: result.error.issues.map((i) => ({
373
+ path: i.path,
374
+ message: i.message
375
+ })) }
376
+ });
377
+ }
378
+ const notifications = parsedResults.flatMap((r) => r.success ? [r.data] : []);
379
+ const dropped = raw.length - notifications.length;
156
380
  if (dropped > 0) this.log.warn({
157
381
  action: "parse.summary",
158
382
  message: `${dropped}/${raw.length} notifications dropped by schema`
@@ -160,53 +384,65 @@ var FlumeGitHubPoller = class {
160
384
  if (!this.bootstrapped) {
161
385
  this.bootstrapped = true;
162
386
  for (const notification of notifications) this.cache.add(notification.id, notification.updated_at);
163
- this.since = new Date(this.props.deps.now()).toISOString();
387
+ this.advanceCursor();
164
388
  this.log.info({
165
- action: "bootstrap",
389
+ action: "poller.bootstrap",
166
390
  message: `seeded ${notifications.length} existing notifications`
167
391
  });
168
392
  this.props.onConnected();
169
393
  return;
170
394
  }
171
- const fresh = [];
172
- for (const notification of notifications) {
173
- if (this.cache.has(notification.id, notification.updated_at)) continue;
395
+ const fresh = notifications.filter((notification) => {
396
+ if (this.cache.has(notification.id, notification.updated_at)) return false;
174
397
  this.cache.add(notification.id, notification.updated_at);
175
- fresh.push(notification);
176
- }
398
+ return true;
399
+ });
177
400
  this.cache.trim();
178
- this.since = new Date(this.props.deps.now()).toISOString();
401
+ this.advanceCursor();
179
402
  if (fresh.length > 0) {
180
403
  this.log.info({
181
404
  action: "poll.fresh",
182
- message: `${fresh.length} new notifications`
405
+ message: `${fresh.length} new notifications`,
406
+ detail: { count: fresh.length }
183
407
  });
184
408
  this.props.onNotifications(fresh);
185
- } else this.log.debug({
409
+ return;
410
+ }
411
+ this.log.debug({
186
412
  action: "poll.idle",
187
413
  message: "0 new notifications"
188
414
  });
189
415
  }
190
- async safeFetch(url) {
191
- const result = await safeFetch({
192
- fetch: this.props.deps.fetch,
193
- url,
194
- init: { headers: {
195
- Authorization: `Bearer ${this.props.token}`,
196
- Accept: "application/vnd.github+json",
197
- "X-GitHub-Api-Version": "2022-11-28"
198
- } },
199
- log: this.log
200
- });
201
- if (result instanceof Error) {
202
- this.consecutiveErrors++;
416
+ advanceCursor() {
417
+ if (this.lastModified !== null) {
418
+ const parsed = new Date(this.lastModified);
419
+ if (!Number.isNaN(parsed.getTime())) {
420
+ const iso = attempt(() => parsed.toISOString());
421
+ if (!(iso instanceof Error)) {
422
+ this.since = iso;
423
+ return;
424
+ }
425
+ }
426
+ }
427
+ const nowMs = safeNow({ deps: this.props.deps });
428
+ if (!Number.isFinite(nowMs)) {
203
429
  this.log.warn({
204
- action: "http.error",
205
- message: `consecutive=${this.consecutiveErrors}`
430
+ action: "cursor.advance.skip",
431
+ message: "deps.now() returned non-finite, leaving since cursor unchanged"
206
432
  });
207
- if (this.consecutiveErrors >= 3) this.props.onDisconnected("network error");
433
+ return;
208
434
  }
209
- return result;
435
+ const iso = attempt(() => new Date(nowMs).toISOString());
436
+ if (iso instanceof Error) {
437
+ const error = safeNormalizeError({ value: iso });
438
+ this.log.warn({
439
+ action: "cursor.advance.skip",
440
+ message: safeErrorMessage({ error }),
441
+ error
442
+ });
443
+ return;
444
+ }
445
+ this.since = iso;
210
446
  }
211
447
  };
212
448
  //#endregion
@@ -215,10 +451,24 @@ var FlumeGitHubSource = class {
215
451
  options;
216
452
  name = "github";
217
453
  poller = null;
218
- currentStatus = "disconnected";
454
+ handler = null;
219
455
  log;
220
456
  deps;
221
457
  queue = new FlumeSerialQueue();
458
+ signals;
459
+ statusEmitter;
460
+ onSignalAbort = () => {
461
+ safeInvokeCallback({
462
+ fn: () => this.stop(),
463
+ onError: (error) => {
464
+ this.log.error({
465
+ action: "signal.abort.stop.failed",
466
+ message: safeErrorMessage({ error }),
467
+ error
468
+ });
469
+ }
470
+ });
471
+ };
222
472
  constructor(options) {
223
473
  this.options = options;
224
474
  this.deps = options.deps ?? createFlumeDefaultDeps();
@@ -227,88 +477,94 @@ var FlumeGitHubSource = class {
227
477
  handler: options.onLog,
228
478
  deps: this.deps
229
479
  });
480
+ this.signals = new FlumeSignalRegistry({
481
+ log: this.log,
482
+ onAbort: this.onSignalAbort
483
+ });
484
+ this.statusEmitter = new FlumeStatusEmitter({
485
+ log: this.log,
486
+ onStatus: options.onStatus
487
+ });
230
488
  }
231
- async start(handler) {
232
- if (this.options.signal?.aborted) return {
233
- ok: false,
234
- error: /* @__PURE__ */ new Error("GitHub source: signal already aborted")
235
- };
236
- this.options.signal?.addEventListener("abort", () => this.stop(), { once: true });
489
+ async start(handler, options) {
490
+ if (this.signals.isAnyAborted(this.options.signal) || this.signals.isAnyAborted(options?.signal)) return new FlumeStartError("GitHub source: signal already aborted");
491
+ this.signals.register(this.options.signal);
492
+ this.signals.register(options?.signal);
493
+ this.handler = handler;
237
494
  this.log.info({
238
- action: "start",
495
+ action: "source.start",
239
496
  message: "starting GitHub source"
240
497
  });
241
- this.setStatus("connecting");
498
+ this.statusEmitter.set("connecting");
242
499
  this.poller = new FlumeGitHubPoller({
243
500
  token: this.options.token,
244
501
  interval: this.options.pollInterval ?? 60,
245
502
  onLog: this.options.onLog,
246
503
  deps: this.deps,
247
- onNotifications: (notifications) => this.handleNotifications(handler, notifications),
248
- onConnected: () => this.setStatus("connected"),
249
- onDisconnected: (detail) => this.setStatus("disconnected", detail)
504
+ onNotifications: (notifications) => this.handleNotifications(notifications),
505
+ onConnected: () => this.statusEmitter.set("connected"),
506
+ onDisconnected: (detail) => this.statusEmitter.set("disconnected", detail)
250
507
  });
251
- try {
252
- await this.poller.start();
253
- } catch (error) {
254
- const err = error instanceof Error ? error : new Error(String(error));
508
+ const result = await this.poller.start();
509
+ if (result instanceof Error) {
255
510
  this.log.error({
256
- action: "start.failed",
257
- message: err.message,
258
- error: err
511
+ action: "source.start.failed",
512
+ message: safeErrorMessage({ error: result }),
513
+ error: result
259
514
  });
260
- this.setStatus("disconnected");
261
- return {
262
- ok: false,
263
- error: err
264
- };
515
+ this.statusEmitter.set("disconnected", result.message);
516
+ return result;
265
517
  }
266
- return { ok: true };
518
+ return null;
267
519
  }
268
520
  async stop() {
521
+ this.signals.unregisterAll();
269
522
  this.log.info({
270
- action: "stop",
523
+ action: "source.stop",
271
524
  message: "stopping GitHub source"
272
525
  });
273
526
  this.poller?.stop();
274
- this.poller = null;
275
527
  await this.queue.drain();
276
- this.setStatus("disconnected");
528
+ this.poller = null;
529
+ this.handler = null;
530
+ this.statusEmitter.set("disconnected");
277
531
  }
278
532
  status() {
279
- return this.currentStatus;
533
+ return this.statusEmitter.value;
280
534
  }
281
- handleNotifications(handler, notifications) {
282
- for (const notification of notifications) {
535
+ handleNotifications(notifications) {
536
+ const handler = this.handler;
537
+ if (!handler) return;
538
+ for (const notification of notifications) this.queue.add(async () => {
283
539
  const event = {
284
540
  source: "github",
285
541
  type: "notification",
286
542
  data: notification,
287
- meta: extractGitHubMeta(notification),
288
- receivedAt: this.deps.now()
543
+ meta: this.safeExtractMeta(notification),
544
+ receivedAt: safeNow({ deps: this.deps })
289
545
  };
290
- this.queue.add(async () => {
291
- try {
292
- await handler(event);
293
- } catch (err) {
294
- this.log.error({
295
- action: "handler.error",
296
- message: "user handler threw",
297
- error: err instanceof Error ? err : new Error(String(err))
298
- });
299
- }
546
+ const r = await attempt(() => Promise.resolve(handler(event)));
547
+ if (r instanceof Error) this.log.error({
548
+ action: "handler.error",
549
+ message: safeErrorMessage({ error: r }),
550
+ error: r
300
551
  });
301
- }
302
- }
303
- setStatus(next, detail) {
304
- if (this.currentStatus === next) return;
305
- this.log.info({
306
- action: "status",
307
- message: `${this.currentStatus} → ${next}${detail ? ` (${detail})` : ""}`
308
552
  });
309
- this.currentStatus = next;
310
- this.options.onStatus?.(next, detail);
553
+ }
554
+ safeExtractMeta(notification) {
555
+ const result = attempt(() => flumeExtractGitHubMeta(notification));
556
+ if (result instanceof Error) {
557
+ const error = safeNormalizeError({ value: result });
558
+ this.log.warn({
559
+ action: "meta.extract.error",
560
+ message: safeErrorMessage({ error }),
561
+ error,
562
+ detail: { notificationId: notification.id }
563
+ });
564
+ return { event_type: "notification" };
565
+ }
566
+ return result;
311
567
  }
312
568
  };
313
569
  //#endregion
314
- export { FlumeGitHubNotificationSchema, FlumeGitHubPoller, FlumeGitHubSeenCache, FlumeGitHubSource, extractGitHubMeta };
570
+ export { FlumeGitHubSource, flumeExtractGitHubMeta };
@@ -2,7 +2,7 @@
2
2
  var FlumeHttpError = class extends Error {
3
3
  status;
4
4
  constructor(props) {
5
- super(props.message);
5
+ super(props.message, props.cause === void 0 ? void 0 : { cause: props.cause });
6
6
  this.name = "FlumeHttpError";
7
7
  this.status = props.status;
8
8
  Object.freeze(this);