@fdcn/sourcefed 0.3.2 → 0.3.4

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.
@@ -0,0 +1,962 @@
1
+ // packages/core/dist/index.js
2
+ import { link, mkdir, open, readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ var store$4;
6
+ var DEFAULT_CONFIG = {
7
+ lang: void 0,
8
+ message: void 0,
9
+ abortEarly: void 0,
10
+ abortPipeEarly: void 0
11
+ };
12
+ // @__NO_SIDE_EFFECTS__
13
+ function getGlobalConfig(config$1) {
14
+ if (!config$1 && !store$4) return DEFAULT_CONFIG;
15
+ return {
16
+ lang: config$1?.lang ?? store$4?.lang,
17
+ message: config$1?.message,
18
+ abortEarly: config$1?.abortEarly ?? store$4?.abortEarly,
19
+ abortPipeEarly: config$1?.abortPipeEarly ?? store$4?.abortPipeEarly
20
+ };
21
+ }
22
+ var store$3;
23
+ // @__NO_SIDE_EFFECTS__
24
+ function getGlobalMessage(lang) {
25
+ return store$3?.get(lang);
26
+ }
27
+ var store$2;
28
+ // @__NO_SIDE_EFFECTS__
29
+ function getSchemaMessage(lang) {
30
+ return store$2?.get(lang);
31
+ }
32
+ var store$1;
33
+ // @__NO_SIDE_EFFECTS__
34
+ function getSpecificMessage(reference, lang) {
35
+ return store$1?.get(reference)?.get(lang);
36
+ }
37
+ // @__NO_SIDE_EFFECTS__
38
+ function _stringify(input) {
39
+ const type = typeof input;
40
+ if (type === "string") return `"${input}"`;
41
+ if (type === "number" || type === "bigint" || type === "boolean") return `${input}`;
42
+ if (type === "object" || type === "function") return (input && Object.getPrototypeOf(input)?.constructor?.name) ?? "null";
43
+ return type;
44
+ }
45
+ function _addIssue(context, label, dataset, config$1, other) {
46
+ const input = other && "input" in other ? other.input : dataset.value;
47
+ const expected = other?.expected ?? context.expects ?? null;
48
+ const received = other?.received ?? /* @__PURE__ */ _stringify(input);
49
+ const issue = {
50
+ kind: context.kind,
51
+ type: context.type,
52
+ input,
53
+ expected,
54
+ received,
55
+ message: `Invalid ${label}: ${expected ? `Expected ${expected} but r` : "R"}eceived ${received}`,
56
+ requirement: context.requirement,
57
+ path: other?.path,
58
+ issues: other?.issues,
59
+ lang: config$1.lang,
60
+ abortEarly: config$1.abortEarly,
61
+ abortPipeEarly: config$1.abortPipeEarly
62
+ };
63
+ const isSchema = context.kind === "schema";
64
+ const message$1 = other?.message ?? context.message ?? /* @__PURE__ */ getSpecificMessage(context.reference, issue.lang) ?? (isSchema ? /* @__PURE__ */ getSchemaMessage(issue.lang) : null) ?? config$1.message ?? /* @__PURE__ */ getGlobalMessage(issue.lang);
65
+ if (message$1 !== void 0) issue.message = typeof message$1 === "function" ? message$1(issue) : message$1;
66
+ if (isSchema) dataset.typed = false;
67
+ if (dataset.issues) dataset.issues.push(issue);
68
+ else dataset.issues = [issue];
69
+ }
70
+ var _standardCache = /* @__PURE__ */ new WeakMap();
71
+ // @__NO_SIDE_EFFECTS__
72
+ function _getStandardProps(context) {
73
+ let cached = _standardCache.get(context);
74
+ if (!cached) {
75
+ cached = {
76
+ version: 1,
77
+ vendor: "valibot",
78
+ validate(value$1) {
79
+ return context["~run"]({ value: value$1 }, /* @__PURE__ */ getGlobalConfig());
80
+ }
81
+ };
82
+ _standardCache.set(context, cached);
83
+ }
84
+ return cached;
85
+ }
86
+ // @__NO_SIDE_EFFECTS__
87
+ function _isValidObjectKey(object$1, key) {
88
+ return Object.prototype.hasOwnProperty.call(object$1, key) && key !== "__proto__" && key !== "prototype" && key !== "constructor";
89
+ }
90
+ // @__NO_SIDE_EFFECTS__
91
+ function _joinExpects(values$1, separator) {
92
+ const list = [...new Set(values$1)];
93
+ if (list.length > 1) return `(${list.join(` ${separator} `)})`;
94
+ return list[0] ?? "never";
95
+ }
96
+ var EMOJI_REGEX = new RegExp("^(?:[\\u{1F1E6}-\\u{1F1FF}]{2}|\\u{1F3F4}[\\u{E0061}-\\u{E007A}]{2}[\\u{E0030}-\\u{E0039}\\u{E0061}-\\u{E007A}]{1,3}\\u{E007F}|(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|(?![\\p{Emoji_Modifier_Base}\\u{1F1E6}-\\u{1F1FF}])\\p{Emoji_Presentation})(?:\\u200D(?:\\p{Emoji}\\uFE0F\\u20E3?|\\p{Emoji_Modifier_Base}\\p{Emoji_Modifier}?|(?![\\p{Emoji_Modifier_Base}\\u{1F1E6}-\\u{1F1FF}])\\p{Emoji_Presentation}))*)+$", "u");
97
+ // @__NO_SIDE_EFFECTS__
98
+ function getFallback(schema, dataset, config$1) {
99
+ return typeof schema.fallback === "function" ? schema.fallback(dataset, config$1) : schema.fallback;
100
+ }
101
+ // @__NO_SIDE_EFFECTS__
102
+ function getDefault(schema, dataset, config$1) {
103
+ return typeof schema.default === "function" ? schema.default(dataset, config$1) : schema.default;
104
+ }
105
+ // @__NO_SIDE_EFFECTS__
106
+ function array(item, message$1) {
107
+ return {
108
+ kind: "schema",
109
+ type: "array",
110
+ reference: array,
111
+ expects: "Array",
112
+ async: false,
113
+ item,
114
+ message: message$1,
115
+ get "~standard"() {
116
+ return /* @__PURE__ */ _getStandardProps(this);
117
+ },
118
+ "~run"(dataset, config$1) {
119
+ const input = dataset.value;
120
+ if (Array.isArray(input)) {
121
+ dataset.typed = true;
122
+ dataset.value = [];
123
+ for (let key = 0; key < input.length; key++) {
124
+ const value$1 = input[key];
125
+ const itemDataset = this.item["~run"]({ value: value$1 }, config$1);
126
+ if (itemDataset.issues) {
127
+ const pathItem = {
128
+ type: "array",
129
+ origin: "value",
130
+ input,
131
+ key,
132
+ value: value$1
133
+ };
134
+ for (const issue of itemDataset.issues) {
135
+ if (issue.path) issue.path.unshift(pathItem);
136
+ else issue.path = [pathItem];
137
+ dataset.issues?.push(issue);
138
+ }
139
+ if (!dataset.issues) dataset.issues = itemDataset.issues;
140
+ if (config$1.abortEarly) {
141
+ dataset.typed = false;
142
+ break;
143
+ }
144
+ }
145
+ if (!itemDataset.typed) dataset.typed = false;
146
+ dataset.value.push(itemDataset.value);
147
+ }
148
+ } else _addIssue(this, "type", dataset, config$1);
149
+ return dataset;
150
+ }
151
+ };
152
+ }
153
+ // @__NO_SIDE_EFFECTS__
154
+ function boolean(message$1) {
155
+ return {
156
+ kind: "schema",
157
+ type: "boolean",
158
+ reference: boolean,
159
+ expects: "boolean",
160
+ async: false,
161
+ message: message$1,
162
+ get "~standard"() {
163
+ return /* @__PURE__ */ _getStandardProps(this);
164
+ },
165
+ "~run"(dataset, config$1) {
166
+ if (typeof dataset.value === "boolean") dataset.typed = true;
167
+ else _addIssue(this, "type", dataset, config$1);
168
+ return dataset;
169
+ }
170
+ };
171
+ }
172
+ // @__NO_SIDE_EFFECTS__
173
+ function _merge(value1, value2) {
174
+ if (typeof value1 === typeof value2) {
175
+ if (value1 === value2 || value1 instanceof Date && value2 instanceof Date && +value1 === +value2) return { value: value1 };
176
+ if (value1 && value2 && value1.constructor === Object && value2.constructor === Object) {
177
+ const nextValue = { ...value1 };
178
+ for (const key in value2) if (Object.prototype.hasOwnProperty.call(value1, key)) {
179
+ const dataset = /* @__PURE__ */ _merge(value1[key], value2[key]);
180
+ if (dataset.issue) return dataset;
181
+ nextValue[key] = dataset.value;
182
+ } else nextValue[key] = value2[key];
183
+ return { value: nextValue };
184
+ }
185
+ if (Array.isArray(value1) && Array.isArray(value2)) {
186
+ if (value1.length === value2.length) {
187
+ const nextValue = [...value1];
188
+ for (let index = 0; index < value1.length; index++) {
189
+ const dataset = /* @__PURE__ */ _merge(value1[index], value2[index]);
190
+ if (dataset.issue) return dataset;
191
+ nextValue[index] = dataset.value;
192
+ }
193
+ return { value: nextValue };
194
+ }
195
+ }
196
+ }
197
+ return { issue: true };
198
+ }
199
+ // @__NO_SIDE_EFFECTS__
200
+ function intersect(options, message$1) {
201
+ return {
202
+ kind: "schema",
203
+ type: "intersect",
204
+ reference: intersect,
205
+ expects: /* @__PURE__ */ _joinExpects(options.map((option) => option.expects), "&"),
206
+ async: false,
207
+ options,
208
+ message: message$1,
209
+ get "~standard"() {
210
+ return /* @__PURE__ */ _getStandardProps(this);
211
+ },
212
+ "~run"(dataset, config$1) {
213
+ if (this.options.length) {
214
+ const input = dataset.value;
215
+ let outputs;
216
+ dataset.typed = true;
217
+ for (const schema of this.options) {
218
+ const optionDataset = schema["~run"]({ value: input }, config$1);
219
+ if (optionDataset.issues) {
220
+ if (dataset.issues) for (const issue of optionDataset.issues) dataset.issues.push(issue);
221
+ else dataset.issues = optionDataset.issues;
222
+ if (config$1.abortEarly) {
223
+ dataset.typed = false;
224
+ break;
225
+ }
226
+ }
227
+ if (!optionDataset.typed) dataset.typed = false;
228
+ if (dataset.typed) if (outputs) outputs.push(optionDataset.value);
229
+ else outputs = [optionDataset.value];
230
+ }
231
+ if (dataset.typed) {
232
+ dataset.value = outputs[0];
233
+ for (let index = 1; index < outputs.length; index++) {
234
+ const mergeDataset = /* @__PURE__ */ _merge(dataset.value, outputs[index]);
235
+ if (mergeDataset.issue) {
236
+ _addIssue(this, "type", dataset, config$1, { received: "unknown" });
237
+ break;
238
+ }
239
+ dataset.value = mergeDataset.value;
240
+ }
241
+ }
242
+ } else _addIssue(this, "type", dataset, config$1);
243
+ return dataset;
244
+ }
245
+ };
246
+ }
247
+ // @__NO_SIDE_EFFECTS__
248
+ function number(message$1) {
249
+ return {
250
+ kind: "schema",
251
+ type: "number",
252
+ reference: number,
253
+ expects: "number",
254
+ async: false,
255
+ message: message$1,
256
+ get "~standard"() {
257
+ return /* @__PURE__ */ _getStandardProps(this);
258
+ },
259
+ "~run"(dataset, config$1) {
260
+ if (typeof dataset.value === "number" && !isNaN(dataset.value)) dataset.typed = true;
261
+ else _addIssue(this, "type", dataset, config$1);
262
+ return dataset;
263
+ }
264
+ };
265
+ }
266
+ // @__NO_SIDE_EFFECTS__
267
+ function object(entries$1, message$1) {
268
+ return {
269
+ kind: "schema",
270
+ type: "object",
271
+ reference: object,
272
+ expects: "Object",
273
+ async: false,
274
+ entries: entries$1,
275
+ message: message$1,
276
+ get "~standard"() {
277
+ return /* @__PURE__ */ _getStandardProps(this);
278
+ },
279
+ "~run"(dataset, config$1) {
280
+ const input = dataset.value;
281
+ if (input && typeof input === "object") {
282
+ dataset.typed = true;
283
+ dataset.value = {};
284
+ for (const key in this.entries) {
285
+ const valueSchema = this.entries[key];
286
+ if (key in input || (valueSchema.type === "exact_optional" || valueSchema.type === "optional" || valueSchema.type === "nullish") && valueSchema.default !== void 0) {
287
+ const value$1 = key in input ? input[key] : /* @__PURE__ */ getDefault(valueSchema);
288
+ const valueDataset = valueSchema["~run"]({ value: value$1 }, config$1);
289
+ if (valueDataset.issues) {
290
+ const pathItem = {
291
+ type: "object",
292
+ origin: "value",
293
+ input,
294
+ key,
295
+ value: value$1
296
+ };
297
+ for (const issue of valueDataset.issues) {
298
+ if (issue.path) issue.path.unshift(pathItem);
299
+ else issue.path = [pathItem];
300
+ dataset.issues?.push(issue);
301
+ }
302
+ if (!dataset.issues) dataset.issues = valueDataset.issues;
303
+ if (config$1.abortEarly) {
304
+ dataset.typed = false;
305
+ break;
306
+ }
307
+ }
308
+ if (!valueDataset.typed) dataset.typed = false;
309
+ dataset.value[key] = valueDataset.value;
310
+ } else if (valueSchema.fallback !== void 0) dataset.value[key] = /* @__PURE__ */ getFallback(valueSchema);
311
+ else if (valueSchema.type !== "exact_optional" && valueSchema.type !== "optional" && valueSchema.type !== "nullish") {
312
+ _addIssue(this, "key", dataset, config$1, {
313
+ input: void 0,
314
+ expected: `"${key}"`,
315
+ path: [{
316
+ type: "object",
317
+ origin: "key",
318
+ input,
319
+ key,
320
+ value: input[key]
321
+ }]
322
+ });
323
+ if (config$1.abortEarly) break;
324
+ }
325
+ }
326
+ } else _addIssue(this, "type", dataset, config$1);
327
+ return dataset;
328
+ }
329
+ };
330
+ }
331
+ // @__NO_SIDE_EFFECTS__
332
+ function picklist(options, message$1) {
333
+ return {
334
+ kind: "schema",
335
+ type: "picklist",
336
+ reference: picklist,
337
+ expects: /* @__PURE__ */ _joinExpects(options.map(_stringify), "|"),
338
+ async: false,
339
+ options,
340
+ message: message$1,
341
+ get "~standard"() {
342
+ return /* @__PURE__ */ _getStandardProps(this);
343
+ },
344
+ "~run"(dataset, config$1) {
345
+ if (this.options.includes(dataset.value)) dataset.typed = true;
346
+ else _addIssue(this, "type", dataset, config$1);
347
+ return dataset;
348
+ }
349
+ };
350
+ }
351
+ // @__NO_SIDE_EFFECTS__
352
+ function record(key, value$1, message$1) {
353
+ return {
354
+ kind: "schema",
355
+ type: "record",
356
+ reference: record,
357
+ expects: "Object",
358
+ async: false,
359
+ key,
360
+ value: value$1,
361
+ message: message$1,
362
+ get "~standard"() {
363
+ return /* @__PURE__ */ _getStandardProps(this);
364
+ },
365
+ "~run"(dataset, config$1) {
366
+ const input = dataset.value;
367
+ if (input && typeof input === "object") {
368
+ dataset.typed = true;
369
+ dataset.value = {};
370
+ for (const entryKey in input) if (/* @__PURE__ */ _isValidObjectKey(input, entryKey)) {
371
+ const entryValue = input[entryKey];
372
+ const keyDataset = this.key["~run"]({ value: entryKey }, config$1);
373
+ if (keyDataset.issues) {
374
+ const pathItem = {
375
+ type: "object",
376
+ origin: "key",
377
+ input,
378
+ key: entryKey,
379
+ value: entryValue
380
+ };
381
+ for (const issue of keyDataset.issues) {
382
+ issue.path = [pathItem];
383
+ dataset.issues?.push(issue);
384
+ }
385
+ if (!dataset.issues) dataset.issues = keyDataset.issues;
386
+ if (config$1.abortEarly) {
387
+ dataset.typed = false;
388
+ break;
389
+ }
390
+ }
391
+ const valueDataset = this.value["~run"]({ value: entryValue }, config$1);
392
+ if (valueDataset.issues) {
393
+ const pathItem = {
394
+ type: "object",
395
+ origin: "value",
396
+ input,
397
+ key: entryKey,
398
+ value: entryValue
399
+ };
400
+ for (const issue of valueDataset.issues) {
401
+ if (issue.path) issue.path.unshift(pathItem);
402
+ else issue.path = [pathItem];
403
+ dataset.issues?.push(issue);
404
+ }
405
+ if (!dataset.issues) dataset.issues = valueDataset.issues;
406
+ if (config$1.abortEarly) {
407
+ dataset.typed = false;
408
+ break;
409
+ }
410
+ }
411
+ if (!keyDataset.typed || !valueDataset.typed) dataset.typed = false;
412
+ if (keyDataset.typed) dataset.value[keyDataset.value] = valueDataset.value;
413
+ }
414
+ } else _addIssue(this, "type", dataset, config$1);
415
+ return dataset;
416
+ }
417
+ };
418
+ }
419
+ // @__NO_SIDE_EFFECTS__
420
+ function string(message$1) {
421
+ return {
422
+ kind: "schema",
423
+ type: "string",
424
+ reference: string,
425
+ expects: "string",
426
+ async: false,
427
+ message: message$1,
428
+ get "~standard"() {
429
+ return /* @__PURE__ */ _getStandardProps(this);
430
+ },
431
+ "~run"(dataset, config$1) {
432
+ if (typeof dataset.value === "string") dataset.typed = true;
433
+ else _addIssue(this, "type", dataset, config$1);
434
+ return dataset;
435
+ }
436
+ };
437
+ }
438
+ // @__NO_SIDE_EFFECTS__
439
+ function unknown() {
440
+ return {
441
+ kind: "schema",
442
+ type: "unknown",
443
+ reference: unknown,
444
+ expects: "unknown",
445
+ async: false,
446
+ get "~standard"() {
447
+ return /* @__PURE__ */ _getStandardProps(this);
448
+ },
449
+ "~run"(dataset) {
450
+ dataset.typed = true;
451
+ return dataset;
452
+ }
453
+ };
454
+ }
455
+ // @__NO_SIDE_EFFECTS__
456
+ function safeParse(schema, input, config$1) {
457
+ const dataset = schema["~run"]({ value: input }, /* @__PURE__ */ getGlobalConfig(config$1));
458
+ return {
459
+ typed: dataset.typed,
460
+ success: !dataset.issues,
461
+ output: dataset.value,
462
+ issues: dataset.issues
463
+ };
464
+ }
465
+ function monitorSourceIdentity(source) {
466
+ return JSON.stringify(stableValue(source));
467
+ }
468
+ function stableValue(value) {
469
+ if (Array.isArray(value)) return value.map(stableValue);
470
+ if (!value || typeof value !== "object") return value;
471
+ return Object.fromEntries(
472
+ Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, stableValue(entry)])
473
+ );
474
+ }
475
+ function monitorIdentity(target, source) {
476
+ return JSON.stringify([target.kind, target.id, monitorSourceIdentity(source)]);
477
+ }
478
+ function dedupeActiveMonitors(monitors) {
479
+ const seen = /* @__PURE__ */ new Set();
480
+ const result = [];
481
+ for (const monitor of monitors) {
482
+ if (!monitor.enabled) {
483
+ result.push(monitor);
484
+ continue;
485
+ }
486
+ const identity = monitorIdentity(monitor.target, monitor.source);
487
+ if (seen.has(identity)) continue;
488
+ seen.add(identity);
489
+ result.push(monitor);
490
+ }
491
+ return result;
492
+ }
493
+ var MonitorService = class {
494
+ constructor(store) {
495
+ this.store = store;
496
+ }
497
+ registryQueue = Promise.resolve();
498
+ async list() {
499
+ return (await this.store.load()).monitors;
500
+ }
501
+ async get(id) {
502
+ return (await this.store.load()).monitors.find((monitor) => monitor.id === id);
503
+ }
504
+ async create(input) {
505
+ return this.transact((registry) => {
506
+ registry.monitors = dedupeActiveMonitors(registry.monitors);
507
+ const identity = monitorIdentity(input.target, input.source);
508
+ const existing = registry.monitors.find(
509
+ (monitor2) => monitor2.enabled && monitorIdentity(monitor2.target, monitor2.source) === identity
510
+ );
511
+ if (existing) return { monitor: existing, created: false };
512
+ const now = (/* @__PURE__ */ new Date()).toISOString();
513
+ const monitor = {
514
+ id: `m-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 6)}`,
515
+ name: input.name,
516
+ source: input.source,
517
+ delivery: input.delivery,
518
+ target: input.target,
519
+ pollIntervalSec: Math.max(15, input.pollIntervalSec ?? 60),
520
+ enabled: true,
521
+ createdAt: now,
522
+ updatedAt: now,
523
+ cursors: {}
524
+ };
525
+ registry.monitors.push(monitor);
526
+ return { monitor, created: true };
527
+ });
528
+ }
529
+ async stop(id) {
530
+ return this.transact((registry) => {
531
+ const monitor = registry.monitors.find((entry) => entry.id === id);
532
+ if (!monitor) return { error: `monitor ${id} was not found` };
533
+ monitor.enabled = false;
534
+ monitor.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
535
+ return monitor;
536
+ });
537
+ }
538
+ async start(id) {
539
+ return this.transact((registry) => {
540
+ const monitor = registry.monitors.find((entry) => entry.id === id);
541
+ if (!monitor) return { error: `monitor ${id} was not found` };
542
+ const identity = monitorIdentity(monitor.target, monitor.source);
543
+ const duplicate = registry.monitors.find(
544
+ (entry) => entry.id !== id && entry.enabled && monitorIdentity(entry.target, entry.source) === identity
545
+ );
546
+ if (duplicate) return { error: `monitor ${id} duplicates active monitor ${duplicate.id} for the same source` };
547
+ monitor.enabled = true;
548
+ monitor.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
549
+ return monitor;
550
+ });
551
+ }
552
+ async remove(id) {
553
+ await this.transact((registry) => {
554
+ registry.monitors = registry.monitors.filter((monitor) => monitor.id !== id);
555
+ });
556
+ }
557
+ async setDelivery(id, delivery) {
558
+ await this.updateMonitor(id, (monitor) => {
559
+ monitor.delivery = delivery;
560
+ });
561
+ }
562
+ async updateCursor(monitor, key, value) {
563
+ await this.updateMonitor(monitor.id, (current) => {
564
+ current.cursors[key] = value;
565
+ });
566
+ }
567
+ async updateCursorValue(monitor, key, update) {
568
+ await this.updateMonitor(monitor.id, (current) => {
569
+ current.cursors[key] = update(current.cursors[key]);
570
+ });
571
+ }
572
+ async dedupe() {
573
+ return this.transact((registry) => {
574
+ const deduped = dedupeActiveMonitors(registry.monitors);
575
+ const removed = registry.monitors.length - deduped.length;
576
+ registry.monitors = deduped;
577
+ return removed;
578
+ });
579
+ }
580
+ async updateMonitor(id, update) {
581
+ await this.transact((registry) => {
582
+ const monitor = registry.monitors.find((entry) => entry.id === id);
583
+ if (!monitor) return;
584
+ update(monitor);
585
+ monitor.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
586
+ });
587
+ }
588
+ transact(operation) {
589
+ const next = this.registryQueue.then(async () => {
590
+ const registry = await this.store.load();
591
+ const result = await operation(registry);
592
+ await this.store.save(registry);
593
+ return result;
594
+ });
595
+ this.registryQueue = next.then(() => void 0, () => void 0);
596
+ return next;
597
+ }
598
+ };
599
+ async function tick(context) {
600
+ const monitors = await context.service.list();
601
+ for (const record2 of monitors) {
602
+ if (!record2.enabled) continue;
603
+ const source = context.sources[record2.source.type];
604
+ if (!source) {
605
+ console.error(`[sourcefed] no monitor provider for source type ${record2.source.type}`);
606
+ continue;
607
+ }
608
+ try {
609
+ await source.tick(context, record2);
610
+ } catch (error) {
611
+ console.error(`[sourcefed] monitor ${record2.id} poll failed:`, error);
612
+ await context.service.updateCursor(record2, "__lastAttemptAt", Date.now()).catch((updateError) => {
613
+ console.error(`[sourcefed] monitor ${record2.id} poll timestamp update failed:`, updateError);
614
+ });
615
+ }
616
+ }
617
+ }
618
+ async function deliverWebhook(context, source, event, deliveryId) {
619
+ let delivered = 0;
620
+ const monitors = await context.service.list();
621
+ for (const monitor of monitors) {
622
+ const monitorSource = context.sources[monitor.source.type];
623
+ if (!monitorSource) continue;
624
+ if (monitorSource.type !== source.type) continue;
625
+ delivered += await monitorSource.deliver(context, monitor, event, deliveryId);
626
+ }
627
+ return delivered;
628
+ }
629
+ async function heartbeatWebhooks(context, source, eventSource) {
630
+ const sourceKey = source.key(source.validateSource(eventSource));
631
+ const monitors = await context.service.list();
632
+ for (const monitor of monitors) {
633
+ if (!monitor.enabled || monitor.source.type !== source.type) continue;
634
+ if (source.key(source.validateSource(monitor.source)) !== sourceKey) continue;
635
+ await context.service.updateCursor(monitor, "__webhookHeartbeatAt", Date.now());
636
+ }
637
+ }
638
+ var MAX_WEBHOOK_BODY_BYTES = 1024 * 1024;
639
+ async function handleWebhook(request, context) {
640
+ const source = context.sourceForWebhookPath(new URL(request.url).pathname);
641
+ if (!source || !source.webhook) return new Response("not found", { status: 404 });
642
+ const body = await readBody(request);
643
+ if (body === void 0) return new Response("payload too large", { status: 413 });
644
+ if (!source.webhook.verify(body, request)) return new Response("invalid signature", { status: 401 });
645
+ let payload;
646
+ try {
647
+ payload = JSON.parse(body);
648
+ } catch {
649
+ return new Response("invalid json", { status: 400 });
650
+ }
651
+ const challenge = source.webhook.challenge?.(payload);
652
+ if (challenge) return Response.json(challenge);
653
+ const deliveryId = source.webhook.deliveryId(request, payload);
654
+ if (!deliveryId) return new Response("missing delivery id", { status: 400 });
655
+ const eventName = source.webhook.eventName(request, payload);
656
+ const event = source.webhook.parse(payload, eventName, deliveryId);
657
+ if (!event) return new Response("ignored", { status: 202 });
658
+ void heartbeatWebhooks(context, source, event.source).catch((error) => console.error("[sourcefed] webhook heartbeat failed:", error));
659
+ if (source.webhook.acknowledgeBeforeDelivery) {
660
+ void deliverWebhook(context, source, event, deliveryId).catch((error) => console.error("[sourcefed] webhook delivery failed:", error));
661
+ return new Response("accepted", { status: 202 });
662
+ }
663
+ await deliverWebhook(context, source, event, deliveryId);
664
+ return new Response("ok");
665
+ }
666
+ async function readBody(request) {
667
+ const contentLength = Number(request.headers.get("content-length"));
668
+ if (Number.isFinite(contentLength) && contentLength > MAX_WEBHOOK_BODY_BYTES) return void 0;
669
+ const reader = request.body?.getReader();
670
+ if (!reader) return "";
671
+ const chunks = [];
672
+ let length = 0;
673
+ try {
674
+ while (true) {
675
+ const { done, value } = await reader.read();
676
+ if (done) break;
677
+ length += value.byteLength;
678
+ if (length > MAX_WEBHOOK_BODY_BYTES) {
679
+ await reader.cancel();
680
+ return void 0;
681
+ }
682
+ chunks.push(value);
683
+ }
684
+ } finally {
685
+ reader.releaseLock();
686
+ }
687
+ const bytes = new Uint8Array(length);
688
+ let offset = 0;
689
+ for (const chunk of chunks) {
690
+ bytes.set(chunk, offset);
691
+ offset += chunk.byteLength;
692
+ }
693
+ return new TextDecoder().decode(bytes);
694
+ }
695
+ var MonitorRuntime = class {
696
+ service;
697
+ context;
698
+ timer;
699
+ activeTicks = /* @__PURE__ */ new Set();
700
+ constructor(options) {
701
+ this.service = new MonitorService(options.store);
702
+ this.context = {
703
+ service: this.service,
704
+ sink: options.sink,
705
+ sources: options.sources,
706
+ sourceForWebhookPath: options.sourceForWebhookPath
707
+ };
708
+ this.pollLoopSec = Math.max(1, options.pollLoopSec ?? 5);
709
+ }
710
+ pollLoopSec;
711
+ async start() {
712
+ await this.service.dedupe();
713
+ if (this.timer) return;
714
+ this.timer = setInterval(() => {
715
+ const tick2 = this.tick().catch((error) => console.error("[sourcefed] monitor tick error:", error));
716
+ this.activeTicks.add(tick2);
717
+ void tick2.finally(() => this.activeTicks.delete(tick2));
718
+ }, this.pollLoopSec * 1e3);
719
+ const initial = this.tick().catch((error) => console.error("[sourcefed] monitor tick error:", error));
720
+ this.activeTicks.add(initial);
721
+ void initial.finally(() => this.activeTicks.delete(initial));
722
+ }
723
+ async tick() {
724
+ await tick(this.context);
725
+ }
726
+ async webhook(request) {
727
+ return handleWebhook(request, this.context);
728
+ }
729
+ async stop() {
730
+ if (this.timer) {
731
+ clearInterval(this.timer);
732
+ this.timer = void 0;
733
+ }
734
+ const ticks = [...this.activeTicks];
735
+ if (ticks.length === 0) return true;
736
+ let timer;
737
+ const settled = await Promise.race([
738
+ Promise.allSettled(ticks).then(() => true),
739
+ new Promise((resolve) => {
740
+ timer = setTimeout(() => {
741
+ console.warn("[sourcefed] stop waited 30s for in-flight monitor ticks; retaining state lock");
742
+ resolve(false);
743
+ }, 3e4);
744
+ })
745
+ ]);
746
+ if (timer) clearTimeout(timer);
747
+ return settled;
748
+ }
749
+ };
750
+ function createQueuedMonitorEvent(monitor, event) {
751
+ return {
752
+ id: `${monitor.id}:${monitorEventID(event)}`,
753
+ monitorID: monitor.id,
754
+ target: monitor.target,
755
+ event,
756
+ queuedAt: (/* @__PURE__ */ new Date()).toISOString()
757
+ };
758
+ }
759
+ function monitorEventID(event) {
760
+ if (event.id) return event.id;
761
+ return `${event.kind}:${event.at}:${event.summary}`;
762
+ }
763
+ var MonitorSourceSchema = /* @__PURE__ */ intersect([
764
+ /* @__PURE__ */ object({ type: /* @__PURE__ */ string() }),
765
+ /* @__PURE__ */ record(/* @__PURE__ */ string(), /* @__PURE__ */ unknown())
766
+ ]);
767
+ var MonitorRecordSchema = /* @__PURE__ */ object({
768
+ id: /* @__PURE__ */ string(),
769
+ name: /* @__PURE__ */ string(),
770
+ source: MonitorSourceSchema,
771
+ delivery: /* @__PURE__ */ picklist(["poll", "webhook"]),
772
+ target: /* @__PURE__ */ object({
773
+ kind: /* @__PURE__ */ string(),
774
+ id: /* @__PURE__ */ string()
775
+ }),
776
+ pollIntervalSec: /* @__PURE__ */ number(),
777
+ enabled: /* @__PURE__ */ boolean(),
778
+ createdAt: /* @__PURE__ */ string(),
779
+ updatedAt: /* @__PURE__ */ string(),
780
+ cursors: /* @__PURE__ */ record(/* @__PURE__ */ string(), /* @__PURE__ */ unknown())
781
+ });
782
+ var MonitorRegistrySchema = /* @__PURE__ */ object({
783
+ monitors: /* @__PURE__ */ array(MonitorRecordSchema)
784
+ });
785
+ var JsonMonitorStore = class {
786
+ stateDir;
787
+ stateFile;
788
+ constructor(options = {}) {
789
+ this.stateDir = options.stateDir ?? process.env.SOURCEFED_STATE_DIR ?? path.join(process.env.XDG_STATE_HOME ?? path.join(os.homedir(), ".local", "state"), "sourcefed");
790
+ this.stateFile = path.join(this.stateDir, "monitors.json");
791
+ }
792
+ async load() {
793
+ const raw = await this.readRaw(this.stateFile);
794
+ if (raw === void 0) return { monitors: [] };
795
+ const parsed = parseRegistry(raw);
796
+ if (!parsed) throw new Error(`invalid monitor state at ${this.stateFile}; refusing to overwrite`);
797
+ return parsed;
798
+ }
799
+ async acquireExclusive() {
800
+ const lockPath = path.join(this.stateDir, ".monitors.lock");
801
+ await mkdir(this.stateDir, { recursive: true });
802
+ const pid = String(process.pid);
803
+ for (let attempt = 0; attempt < 5; attempt++) {
804
+ try {
805
+ const handle = await open(lockPath, "wx");
806
+ await handle.writeFile(pid);
807
+ let released = false;
808
+ return async () => {
809
+ if (released) return;
810
+ released = true;
811
+ await handle.close();
812
+ const current = await readLockPid(lockPath);
813
+ if (current === process.pid) await rm(lockPath, { force: true });
814
+ };
815
+ } catch (error) {
816
+ const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
817
+ if (code !== "EEXIST") throw error;
818
+ const holder = await readLockPid(lockPath);
819
+ if (holder === void 0 || processAlive(holder)) {
820
+ throw new Error(`another sourcefed daemon (pid ${holder ?? "unknown"}) is using ${this.stateDir}; stop it or set SOURCEFED_STATE_DIR`);
821
+ }
822
+ const contender = `${lockPath}.${process.pid}.${Date.now().toString(36)}`;
823
+ try {
824
+ await rename(lockPath, contender);
825
+ } catch {
826
+ continue;
827
+ }
828
+ await writeFile(contender, pid);
829
+ try {
830
+ await link(contender, lockPath);
831
+ } catch {
832
+ await rm(contender, { force: true });
833
+ continue;
834
+ }
835
+ await rm(contender, { force: true });
836
+ let released = false;
837
+ return async () => {
838
+ if (released) return;
839
+ released = true;
840
+ const current = await readLockPid(lockPath);
841
+ if (current === process.pid) await rm(lockPath, { force: true });
842
+ };
843
+ }
844
+ }
845
+ throw new Error(`could not acquire the sourcefed state lock at ${lockPath}`);
846
+ }
847
+ async save(registry) {
848
+ await mkdir(this.stateDir, { recursive: true, mode: 448 });
849
+ const temporary = `${this.stateFile}.${process.pid}.tmp`;
850
+ await writeFile(temporary, `${JSON.stringify(registry, null, 2)}
851
+ `, { mode: 384 });
852
+ await rename(temporary, this.stateFile);
853
+ }
854
+ async readRaw(filePath) {
855
+ try {
856
+ const contents = await readFile(filePath, "utf8");
857
+ return JSON.parse(contents);
858
+ } catch (error) {
859
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return void 0;
860
+ throw error;
861
+ }
862
+ }
863
+ };
864
+ async function readLockPid(lockPath) {
865
+ try {
866
+ const contents = await readFile(lockPath, "utf8");
867
+ const pid = Number(contents.trim());
868
+ return Number.isInteger(pid) && pid > 0 ? pid : void 0;
869
+ } catch {
870
+ return void 0;
871
+ }
872
+ }
873
+ function processAlive(pid) {
874
+ try {
875
+ process.kill(pid, 0);
876
+ return true;
877
+ } catch (error) {
878
+ return Boolean(error && typeof error === "object" && "code" in error && error.code === "EPERM");
879
+ }
880
+ }
881
+ function parseRegistry(value) {
882
+ const result = /* @__PURE__ */ safeParse(MonitorRegistrySchema, value);
883
+ return result.success ? result.output : void 0;
884
+ }
885
+ var JsonMonitorEventQueue = class {
886
+ constructor(stateDir) {
887
+ this.stateDir = stateDir;
888
+ this.eventsFile = path.join(stateDir, "events.json");
889
+ }
890
+ eventsFile;
891
+ queue = Promise.resolve();
892
+ async enqueue(input) {
893
+ return this.transact((events) => {
894
+ const queued = createQueuedMonitorEvent(input.monitor, input.event);
895
+ const existing = events.find((event) => event.id === queued.id);
896
+ if (existing) return existing;
897
+ events.push(queued);
898
+ return queued;
899
+ });
900
+ }
901
+ async read(target) {
902
+ const events = await this.loadEvents();
903
+ return events.filter((event) => sameTarget2(event.target, target));
904
+ }
905
+ async acknowledge(target, eventIDs) {
906
+ await this.transact((events) => {
907
+ const ids = new Set(eventIDs);
908
+ for (let index = events.length - 1; index >= 0; index -= 1) {
909
+ if (sameTarget2(events[index].target, target) && ids.has(events[index].id)) events.splice(index, 1);
910
+ }
911
+ });
912
+ }
913
+ async transact(operation) {
914
+ const next = this.queue.then(async () => {
915
+ const events = await this.loadEvents();
916
+ const result = await operation(events);
917
+ await this.saveEvents(events);
918
+ return result;
919
+ });
920
+ this.queue = next.then(() => void 0, () => void 0);
921
+ return next;
922
+ }
923
+ async loadEvents() {
924
+ try {
925
+ const contents = await readFile(this.eventsFile, "utf8");
926
+ const parsed = JSON.parse(contents);
927
+ if (!Array.isArray(parsed)) throw new Error(`invalid event queue at ${this.eventsFile}`);
928
+ return parsed;
929
+ } catch (error) {
930
+ if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
931
+ throw error;
932
+ }
933
+ }
934
+ async saveEvents(events) {
935
+ await mkdir(this.stateDir, { recursive: true, mode: 448 });
936
+ const temporary = `${this.eventsFile}.${process.pid}.tmp`;
937
+ await writeFile(temporary, `${JSON.stringify(events, null, 2)}
938
+ `, { mode: 384 });
939
+ await rename(temporary, this.eventsFile);
940
+ }
941
+ };
942
+ function sameTarget2(left, right) {
943
+ return left.kind === right.kind && left.id === right.id;
944
+ }
945
+ function eventToText(event) {
946
+ const summary = event.summary.trim();
947
+ let detail = "";
948
+ if (event.kind !== "merged" && event.body) detail = event.body.trim();
949
+ let duplicateDetail = detail;
950
+ if (detail && summary.includes(detail)) duplicateDetail = "";
951
+ const lines = [`[sourcefed monitor] ${summary}`];
952
+ if (duplicateDetail) lines.push(`
953
+ ${duplicateDetail}`);
954
+ return lines.filter(Boolean).join("\n");
955
+ }
956
+
957
+ export {
958
+ MonitorRuntime,
959
+ JsonMonitorStore,
960
+ JsonMonitorEventQueue,
961
+ eventToText
962
+ };