@interactive-inc/flume 0.1.0 → 0.2.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.
@@ -1,280 +0,0 @@
1
- import { r as FlumeGitHubNotificationSchema, t as FlumeLogger } from "./logger-B9E8zvgj.js";
2
- //#region lib/github/github-seen-cache.ts
3
- var FlumeGitHubSeenCache = class {
4
- props;
5
- seen = /* @__PURE__ */ new Map();
6
- constructor(props) {
7
- this.props = props;
8
- }
9
- has(id, updatedAt) {
10
- return this.seen.get(id) === updatedAt;
11
- }
12
- add(id, updatedAt) {
13
- this.seen.set(id, updatedAt);
14
- }
15
- trim() {
16
- if (this.seen.size <= this.props.maxSize) return;
17
- const entries = [...this.seen.entries()];
18
- this.seen = new Map(entries.slice(entries.length - this.props.maxSize));
19
- }
20
- get size() {
21
- return this.seen.size;
22
- }
23
- };
24
- //#endregion
25
- //#region lib/github/github-poller.ts
26
- var FlumeGitHubPoller = class {
27
- props;
28
- log;
29
- cache = new FlumeGitHubSeenCache({ maxSize: 5e3 });
30
- timer = null;
31
- since = null;
32
- bootstrapped = false;
33
- stopped = false;
34
- consecutiveErrors = 0;
35
- constructor(props) {
36
- this.props = props;
37
- this.log = new FlumeLogger({
38
- source: "github.poller",
39
- handler: props.onLog,
40
- deps: props.deps
41
- });
42
- }
43
- async start() {
44
- this.stopped = false;
45
- this.log.info({
46
- action: "start",
47
- message: `polling every ${this.props.interval}s`
48
- });
49
- await this.poll();
50
- this.timer = this.props.deps.setInterval(() => {
51
- this.poll().catch((err) => {
52
- this.log.error({
53
- action: "poll.unhandled",
54
- message: "unexpected error in poll loop",
55
- error: err instanceof Error ? err : new Error(String(err))
56
- });
57
- });
58
- }, this.props.interval * 1e3);
59
- }
60
- stop() {
61
- this.log.info({
62
- action: "stop",
63
- message: "stopping poller"
64
- });
65
- this.stopped = true;
66
- if (this.timer !== null) {
67
- this.props.deps.clearInterval(this.timer);
68
- this.timer = null;
69
- }
70
- }
71
- async poll() {
72
- const params = new URLSearchParams({ all: "false" });
73
- if (this.since) params.set("since", this.since);
74
- const url = `https://api.github.com/notifications?${params}`;
75
- this.log.debug({
76
- action: "http.request",
77
- message: `GET ${url}`
78
- });
79
- const response = await this.safeFetch(url);
80
- if (response instanceof Error) return;
81
- this.log.debug({
82
- action: "http.response",
83
- message: `GET ${response.status}`,
84
- detail: {
85
- status: response.status,
86
- url
87
- }
88
- });
89
- if (!response.ok) {
90
- this.consecutiveErrors++;
91
- this.log.error({
92
- action: "http.error",
93
- message: `HTTP ${response.status} (consecutive=${this.consecutiveErrors})`
94
- });
95
- if (this.consecutiveErrors >= 3) this.props.onDisconnected(`HTTP ${response.status}`);
96
- return;
97
- }
98
- this.consecutiveErrors = 0;
99
- const body = await response.json();
100
- if (!Array.isArray(body)) {
101
- this.log.warn({
102
- action: "http.body",
103
- message: "response body is not an array, dropping",
104
- detail: { bodyType: typeof body }
105
- });
106
- return;
107
- }
108
- this.processNotifications(body);
109
- }
110
- processNotifications(raw) {
111
- let dropped = 0;
112
- const notifications = raw.flatMap((item) => {
113
- const parsed = FlumeGitHubNotificationSchema.safeParse(item);
114
- if (!parsed.success) {
115
- dropped++;
116
- this.log.warn({
117
- action: "parse.skip",
118
- message: "notification did not match schema",
119
- detail: { issues: parsed.error.issues.map((i) => ({
120
- path: i.path,
121
- message: i.message
122
- })) }
123
- });
124
- return [];
125
- }
126
- return [parsed.data];
127
- });
128
- if (dropped > 0) this.log.warn({
129
- action: "parse.summary",
130
- message: `${dropped}/${raw.length} notifications dropped by schema`
131
- });
132
- if (!this.bootstrapped) {
133
- this.bootstrapped = true;
134
- for (const notification of notifications) this.cache.add(notification.id, notification.updated_at);
135
- this.since = new Date(this.props.deps.now()).toISOString();
136
- this.log.info({
137
- action: "bootstrap",
138
- message: `seeded ${notifications.length} existing notifications`
139
- });
140
- this.props.onConnected();
141
- return;
142
- }
143
- const fresh = [];
144
- for (const notification of notifications) {
145
- if (this.cache.has(notification.id, notification.updated_at)) continue;
146
- this.cache.add(notification.id, notification.updated_at);
147
- fresh.push(notification);
148
- }
149
- this.cache.trim();
150
- this.since = new Date(this.props.deps.now()).toISOString();
151
- if (fresh.length > 0) {
152
- this.log.info({
153
- action: "poll.fresh",
154
- message: `${fresh.length} new notifications`
155
- });
156
- this.props.onNotifications(fresh);
157
- } else this.log.debug({
158
- action: "poll.idle",
159
- message: "0 new notifications"
160
- });
161
- }
162
- async safeFetch(url) {
163
- try {
164
- return await this.props.deps.fetch(url, { headers: {
165
- Authorization: `Bearer ${this.props.token}`,
166
- Accept: "application/vnd.github+json",
167
- "X-GitHub-Api-Version": "2022-11-28"
168
- } });
169
- } catch (error) {
170
- this.consecutiveErrors++;
171
- const err = error instanceof Error ? error : new Error(String(error));
172
- this.log.error({
173
- action: "http.error",
174
- message: `network error (consecutive=${this.consecutiveErrors})`,
175
- error: err
176
- });
177
- if (this.consecutiveErrors >= 3) this.props.onDisconnected("network error");
178
- return err;
179
- }
180
- }
181
- };
182
- //#endregion
183
- //#region lib/github/github-source.ts
184
- var FlumeGitHubSource = class FlumeGitHubSource {
185
- options;
186
- poller = null;
187
- currentStatus = "disconnected";
188
- log;
189
- deps;
190
- constructor(options) {
191
- this.options = options;
192
- this.deps = options.deps;
193
- this.log = new FlumeLogger({
194
- source: "github",
195
- handler: options.onLog,
196
- deps: this.deps
197
- });
198
- }
199
- async start(handler) {
200
- if (this.options.signal?.aborted) return;
201
- this.options.signal?.addEventListener("abort", () => this.stop(), { once: true });
202
- this.log.info({
203
- action: "start",
204
- message: "starting GitHub source"
205
- });
206
- this.setStatus("connecting");
207
- this.poller = new FlumeGitHubPoller({
208
- token: this.options.token,
209
- interval: this.options.pollInterval ?? 60,
210
- onLog: this.options.onLog,
211
- deps: this.deps,
212
- onNotifications: (notifications) => this.handleNotifications(handler, notifications),
213
- onConnected: () => this.setStatus("connected"),
214
- onDisconnected: (detail) => this.setStatus("disconnected", detail)
215
- });
216
- try {
217
- await this.poller.start();
218
- } catch (error) {
219
- const err = error instanceof Error ? error : new Error(String(error));
220
- this.log.error({
221
- action: "start.failed",
222
- message: err.message,
223
- error: err
224
- });
225
- this.setStatus("disconnected");
226
- }
227
- }
228
- async stop() {
229
- this.log.info({
230
- action: "stop",
231
- message: "stopping GitHub source"
232
- });
233
- this.poller?.stop();
234
- this.poller = null;
235
- this.setStatus("disconnected");
236
- }
237
- status() {
238
- return this.currentStatus;
239
- }
240
- handleNotifications(handler, notifications) {
241
- for (const notification of notifications) {
242
- const event = {
243
- source: "github",
244
- type: "notification",
245
- data: notification,
246
- meta: FlumeGitHubSource.extractMeta(notification),
247
- receivedAt: this.deps.now()
248
- };
249
- try {
250
- handler(event);
251
- } catch (err) {
252
- this.log.error({
253
- action: "handler.error",
254
- message: "user handler threw",
255
- error: err instanceof Error ? err : new Error(String(err))
256
- });
257
- }
258
- }
259
- }
260
- setStatus(next, detail) {
261
- if (this.currentStatus === next) return;
262
- this.log.info({
263
- action: "status",
264
- message: `${this.currentStatus} → ${next}${detail ? ` (${detail})` : ""}`
265
- });
266
- this.currentStatus = next;
267
- this.options.onStatus?.(next, detail);
268
- }
269
- static extractMeta(notification) {
270
- return {
271
- event_type: "notification",
272
- reason: notification.reason,
273
- subject_type: notification.subject.type,
274
- repository: notification.repository.full_name,
275
- thread_id: notification.id
276
- };
277
- }
278
- };
279
- //#endregion
280
- export { FlumeGitHubPoller as n, FlumeGitHubSeenCache as r, FlumeGitHubSource as t };
@@ -1,69 +0,0 @@
1
- import { z } from "zod/v4";
2
- //#region lib/schema.ts
3
- const FlumeGatewayMessageSchema = z.object({
4
- op: z.number(),
5
- d: z.record(z.string(), z.unknown()).nullable(),
6
- s: z.number().nullable(),
7
- t: z.string().nullable()
8
- });
9
- const FlumeSlackEnvelopeSchema = z.object({
10
- envelope_id: z.string(),
11
- type: z.string(),
12
- payload: z.record(z.string(), z.unknown()),
13
- accepts_response_payload: z.boolean().optional(),
14
- retry_attempt: z.number().optional(),
15
- retry_reason: z.string().optional()
16
- });
17
- const FlumeSlackConnectionResponseSchema = z.object({
18
- ok: z.boolean(),
19
- url: z.string().optional(),
20
- error: z.string().optional()
21
- });
22
- const FlumeGitHubNotificationSchema = z.object({
23
- id: z.string(),
24
- reason: z.string(),
25
- unread: z.boolean(),
26
- updated_at: z.string(),
27
- subject: z.object({
28
- title: z.string(),
29
- url: z.string().nullable(),
30
- type: z.string()
31
- }),
32
- repository: z.object({ full_name: z.string() })
33
- });
34
- //#endregion
35
- //#region lib/logger.ts
36
- var FlumeLogger = class {
37
- props;
38
- constructor(props) {
39
- this.props = props;
40
- Object.freeze(this);
41
- }
42
- debug(entry) {
43
- this.emit("debug", entry);
44
- }
45
- info(entry) {
46
- this.emit("info", entry);
47
- }
48
- warn(entry) {
49
- this.emit("warn", entry);
50
- }
51
- error(entry) {
52
- this.emit("error", entry);
53
- }
54
- emit(level, input) {
55
- if (!this.props.handler) return;
56
- const log = {
57
- level,
58
- source: this.props.source,
59
- action: input.action,
60
- message: input.message,
61
- timestamp: this.props.deps.now(),
62
- error: input.error,
63
- detail: input.detail
64
- };
65
- this.props.handler(log);
66
- }
67
- };
68
- //#endregion
69
- export { FlumeSlackEnvelopeSchema as a, FlumeSlackConnectionResponseSchema as i, FlumeGatewayMessageSchema as n, FlumeGitHubNotificationSchema as r, FlumeLogger as t };