@marktoflow/core 2.0.2 → 2.0.4-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/README.md +69 -6
  2. package/dist/built-in-operations.d.ts +2 -136
  3. package/dist/built-in-operations.d.ts.map +1 -1
  4. package/dist/built-in-operations.js +11 -743
  5. package/dist/built-in-operations.js.map +1 -1
  6. package/dist/engine/conditions.d.ts +29 -0
  7. package/dist/engine/conditions.d.ts.map +1 -0
  8. package/dist/engine/conditions.js +109 -0
  9. package/dist/engine/conditions.js.map +1 -0
  10. package/dist/engine/control-flow.d.ts +35 -0
  11. package/dist/engine/control-flow.d.ts.map +1 -0
  12. package/dist/engine/control-flow.js +653 -0
  13. package/dist/engine/control-flow.js.map +1 -0
  14. package/dist/engine/index.d.ts +12 -0
  15. package/dist/engine/index.d.ts.map +1 -0
  16. package/dist/engine/index.js +11 -0
  17. package/dist/engine/index.js.map +1 -0
  18. package/dist/engine/retry.d.ts +35 -0
  19. package/dist/engine/retry.d.ts.map +1 -0
  20. package/dist/engine/retry.js +86 -0
  21. package/dist/engine/retry.js.map +1 -0
  22. package/dist/engine/subworkflow.d.ts +31 -0
  23. package/dist/engine/subworkflow.d.ts.map +1 -0
  24. package/dist/engine/subworkflow.js +240 -0
  25. package/dist/engine/subworkflow.js.map +1 -0
  26. package/dist/engine/types.d.ts +55 -0
  27. package/dist/engine/types.d.ts.map +1 -0
  28. package/dist/engine/types.js +5 -0
  29. package/dist/{secrets → engine}/types.js.map +1 -1
  30. package/dist/engine/variable-resolution.d.ts +29 -0
  31. package/dist/engine/variable-resolution.d.ts.map +1 -0
  32. package/dist/engine/variable-resolution.js +130 -0
  33. package/dist/engine/variable-resolution.js.map +1 -0
  34. package/dist/engine.d.ts +24 -211
  35. package/dist/engine.d.ts.map +1 -1
  36. package/dist/engine.js +218 -1363
  37. package/dist/engine.js.map +1 -1
  38. package/dist/event-operations.d.ts +59 -0
  39. package/dist/event-operations.d.ts.map +1 -0
  40. package/dist/event-operations.js +99 -0
  41. package/dist/event-operations.js.map +1 -0
  42. package/dist/event-source.d.ts +195 -0
  43. package/dist/event-source.d.ts.map +1 -0
  44. package/dist/event-source.js +757 -0
  45. package/dist/event-source.js.map +1 -0
  46. package/dist/file-operations.js +1 -1
  47. package/dist/file-operations.js.map +1 -1
  48. package/dist/filters/array.d.ts +9 -0
  49. package/dist/filters/array.d.ts.map +1 -0
  50. package/dist/filters/array.js +41 -0
  51. package/dist/filters/array.js.map +1 -0
  52. package/dist/filters/date.d.ts +9 -0
  53. package/dist/filters/date.d.ts.map +1 -0
  54. package/dist/filters/date.js +51 -0
  55. package/dist/filters/date.js.map +1 -0
  56. package/dist/filters/index.d.ts +13 -0
  57. package/dist/filters/index.d.ts.map +1 -0
  58. package/dist/filters/index.js +13 -0
  59. package/dist/filters/index.js.map +1 -0
  60. package/dist/filters/json.d.ts +6 -0
  61. package/dist/filters/json.d.ts.map +1 -0
  62. package/dist/filters/json.js +15 -0
  63. package/dist/filters/json.js.map +1 -0
  64. package/dist/filters/logic.d.ts +8 -0
  65. package/dist/filters/logic.d.ts.map +1 -0
  66. package/dist/filters/logic.js +28 -0
  67. package/dist/filters/logic.js.map +1 -0
  68. package/dist/filters/math.d.ts +13 -0
  69. package/dist/filters/math.d.ts.map +1 -0
  70. package/dist/filters/math.js +39 -0
  71. package/dist/filters/math.js.map +1 -0
  72. package/dist/filters/object.d.ts +11 -0
  73. package/dist/filters/object.d.ts.map +1 -0
  74. package/dist/filters/object.js +64 -0
  75. package/dist/filters/object.js.map +1 -0
  76. package/dist/filters/regex.d.ts +7 -0
  77. package/dist/filters/regex.d.ts.map +1 -0
  78. package/dist/filters/regex.js +38 -0
  79. package/dist/filters/regex.js.map +1 -0
  80. package/dist/filters/string.d.ts +11 -0
  81. package/dist/filters/string.d.ts.map +1 -0
  82. package/dist/filters/string.js +35 -0
  83. package/dist/filters/string.js.map +1 -0
  84. package/dist/filters/type-checks.d.ts +10 -0
  85. package/dist/filters/type-checks.d.ts.map +1 -0
  86. package/dist/filters/type-checks.js +30 -0
  87. package/dist/filters/type-checks.js.map +1 -0
  88. package/dist/index.d.ts +7 -1
  89. package/dist/index.d.ts.map +1 -1
  90. package/dist/index.js +11 -1
  91. package/dist/index.js.map +1 -1
  92. package/dist/models.d.ts +87 -0
  93. package/dist/models.d.ts.map +1 -1
  94. package/dist/models.js +28 -0
  95. package/dist/models.js.map +1 -1
  96. package/dist/nunjucks-filters.d.ts +2 -261
  97. package/dist/nunjucks-filters.d.ts.map +1 -1
  98. package/dist/nunjucks-filters.js +24 -582
  99. package/dist/nunjucks-filters.js.map +1 -1
  100. package/dist/operations/compress.d.ts +6 -0
  101. package/dist/operations/compress.d.ts.map +1 -0
  102. package/dist/operations/compress.js +36 -0
  103. package/dist/operations/compress.js.map +1 -0
  104. package/dist/operations/crypto.d.ts +5 -0
  105. package/dist/operations/crypto.d.ts.map +1 -0
  106. package/dist/operations/crypto.js +61 -0
  107. package/dist/operations/crypto.js.map +1 -0
  108. package/dist/operations/data-ops.d.ts +10 -0
  109. package/dist/operations/data-ops.d.ts.map +1 -0
  110. package/dist/operations/data-ops.js +124 -0
  111. package/dist/operations/data-ops.js.map +1 -0
  112. package/dist/operations/datetime.d.ts +5 -0
  113. package/dist/operations/datetime.d.ts.map +1 -0
  114. package/dist/operations/datetime.js +86 -0
  115. package/dist/operations/datetime.js.map +1 -0
  116. package/dist/operations/extract.d.ts +23 -0
  117. package/dist/operations/extract.d.ts.map +1 -0
  118. package/dist/operations/extract.js +31 -0
  119. package/dist/operations/extract.js.map +1 -0
  120. package/dist/operations/format.d.ts +14 -0
  121. package/dist/operations/format.d.ts.map +1 -0
  122. package/dist/operations/format.js +84 -0
  123. package/dist/operations/format.js.map +1 -0
  124. package/dist/operations/index.d.ts +13 -0
  125. package/dist/operations/index.d.ts.map +1 -0
  126. package/dist/operations/index.js +13 -0
  127. package/dist/operations/index.js.map +1 -0
  128. package/dist/operations/parse.d.ts +5 -0
  129. package/dist/operations/parse.d.ts.map +1 -0
  130. package/dist/operations/parse.js +59 -0
  131. package/dist/operations/parse.js.map +1 -0
  132. package/dist/operations/set.d.ts +21 -0
  133. package/dist/operations/set.d.ts.map +1 -0
  134. package/dist/operations/set.js +25 -0
  135. package/dist/operations/set.js.map +1 -0
  136. package/dist/operations/transform.d.ts +15 -0
  137. package/dist/operations/transform.d.ts.map +1 -0
  138. package/dist/operations/transform.js +110 -0
  139. package/dist/operations/transform.js.map +1 -0
  140. package/dist/parallel.d.ts +114 -0
  141. package/dist/parallel.d.ts.map +1 -0
  142. package/dist/parallel.js +329 -0
  143. package/dist/parallel.js.map +1 -0
  144. package/dist/parser.d.ts.map +1 -1
  145. package/dist/parser.js +23 -4
  146. package/dist/parser.js.map +1 -1
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/permissions.js +17 -2
  149. package/dist/permissions.js.map +1 -1
  150. package/dist/routing.js +2 -2
  151. package/dist/routing.js.map +1 -1
  152. package/dist/sdk-registry.d.ts.map +1 -1
  153. package/dist/sdk-registry.js +9 -3
  154. package/dist/sdk-registry.js.map +1 -1
  155. package/dist/template-engine.d.ts.map +1 -1
  156. package/dist/template-engine.js +10 -15
  157. package/dist/template-engine.js.map +1 -1
  158. package/dist/utils/duration.d.ts +23 -0
  159. package/dist/utils/duration.d.ts.map +1 -0
  160. package/dist/utils/duration.js +41 -0
  161. package/dist/utils/duration.js.map +1 -0
  162. package/dist/utils/errors.d.ts +64 -0
  163. package/dist/utils/errors.d.ts.map +1 -0
  164. package/dist/utils/errors.js +188 -0
  165. package/dist/utils/errors.js.map +1 -0
  166. package/dist/utils/index.d.ts +3 -0
  167. package/dist/utils/index.d.ts.map +1 -0
  168. package/dist/utils/index.js +3 -0
  169. package/dist/utils/index.js.map +1 -0
  170. package/dist/{templates.d.ts → workflow-templates.d.ts} +1 -1
  171. package/dist/workflow-templates.d.ts.map +1 -0
  172. package/dist/{templates.js → workflow-templates.js} +1 -1
  173. package/dist/workflow-templates.js.map +1 -0
  174. package/package.json +34 -7
  175. package/dist/secrets/index.d.ts +0 -12
  176. package/dist/secrets/index.d.ts.map +0 -1
  177. package/dist/secrets/index.js +0 -11
  178. package/dist/secrets/index.js.map +0 -1
  179. package/dist/secrets/providers/aws.d.ts +0 -32
  180. package/dist/secrets/providers/aws.d.ts.map +0 -1
  181. package/dist/secrets/providers/aws.js +0 -118
  182. package/dist/secrets/providers/aws.js.map +0 -1
  183. package/dist/secrets/providers/azure.d.ts +0 -40
  184. package/dist/secrets/providers/azure.d.ts.map +0 -1
  185. package/dist/secrets/providers/azure.js +0 -170
  186. package/dist/secrets/providers/azure.js.map +0 -1
  187. package/dist/secrets/providers/env.d.ts +0 -26
  188. package/dist/secrets/providers/env.d.ts.map +0 -1
  189. package/dist/secrets/providers/env.js +0 -59
  190. package/dist/secrets/providers/env.js.map +0 -1
  191. package/dist/secrets/providers/vault.d.ts +0 -39
  192. package/dist/secrets/providers/vault.d.ts.map +0 -1
  193. package/dist/secrets/providers/vault.js +0 -180
  194. package/dist/secrets/providers/vault.js.map +0 -1
  195. package/dist/secrets/secret-manager.d.ts +0 -72
  196. package/dist/secrets/secret-manager.d.ts.map +0 -1
  197. package/dist/secrets/secret-manager.js +0 -226
  198. package/dist/secrets/secret-manager.js.map +0 -1
  199. package/dist/secrets/types.d.ts +0 -105
  200. package/dist/secrets/types.d.ts.map +0 -1
  201. package/dist/secrets/types.js +0 -8
  202. package/dist/templates.d.ts.map +0 -1
  203. package/dist/templates.js.map +0 -1
@@ -0,0 +1,757 @@
1
+ /**
2
+ * Event sources for event-driven workflows.
3
+ *
4
+ * Provides persistent connections to external services (Discord, Slack, WebSocket, etc.)
5
+ * that emit events to trigger workflow steps or restart workflows.
6
+ *
7
+ * Event sources:
8
+ * - websocket: Connect to any WebSocket endpoint
9
+ * - discord: Listen for Discord events via bot gateway
10
+ * - slack: Listen for Slack events via Socket Mode
11
+ * - cron: Emit events on a schedule (wraps the existing scheduler)
12
+ * - http-stream: SSE (Server-Sent Events) listener
13
+ * - rss: Poll RSS/Atom feeds for new items
14
+ */
15
+ import { EventEmitter } from "node:events";
16
+ import WebSocket from "ws";
17
+ import { parseDuration } from "./utils/duration.js";
18
+ // ── Abstract Base ────────────────────────────────────────────────────────────
19
+ export class BaseEventSource extends EventEmitter {
20
+ id;
21
+ kind;
22
+ config;
23
+ _status = "disconnected";
24
+ _eventsReceived = 0;
25
+ _lastEventAt;
26
+ _connectedAt;
27
+ _reconnectAttempts = 0;
28
+ _reconnectTimer;
29
+ constructor(config) {
30
+ super();
31
+ this.id = config.id;
32
+ this.kind = config.kind;
33
+ this.config = config;
34
+ }
35
+ get status() {
36
+ return this._status;
37
+ }
38
+ get stats() {
39
+ return {
40
+ id: this.id,
41
+ kind: this.kind,
42
+ status: this._status,
43
+ eventsReceived: this._eventsReceived,
44
+ lastEventAt: this._lastEventAt,
45
+ connectedAt: this._connectedAt,
46
+ reconnectAttempts: this._reconnectAttempts,
47
+ };
48
+ }
49
+ /** Emit an event through the source */
50
+ emitEvent(type, data, raw) {
51
+ if (this.config.filter && !this.config.filter.includes(type)) {
52
+ return; // filtered out
53
+ }
54
+ this._eventsReceived++;
55
+ this._lastEventAt = new Date().toISOString();
56
+ const event = {
57
+ source: this.id,
58
+ type,
59
+ data,
60
+ timestamp: this._lastEventAt,
61
+ raw,
62
+ };
63
+ this.emit("event", event);
64
+ }
65
+ /** Handle disconnection with optional reconnect */
66
+ handleDisconnect(reason) {
67
+ // Don't reconnect if explicitly stopped
68
+ if (this._status === "stopped") {
69
+ return;
70
+ }
71
+ this._status = "disconnected";
72
+ this._connectedAt = undefined;
73
+ this.emit("disconnected", { source: this.id, reason });
74
+ if (this.config.reconnect !== false) {
75
+ const maxAttempts = this.config.maxReconnectAttempts ?? Infinity;
76
+ if (this._reconnectAttempts < maxAttempts) {
77
+ const delay = this.config.reconnectDelay ?? 5000;
78
+ this._reconnectTimer = setTimeout(() => {
79
+ // Re-check status in case stop() was called during the delay
80
+ if (this._status === "stopped")
81
+ return;
82
+ this._reconnectAttempts++;
83
+ this.connect().catch((err) => {
84
+ this.emit("error", err);
85
+ });
86
+ }, delay);
87
+ }
88
+ }
89
+ }
90
+ /** Stop the source (no reconnect) */
91
+ async stop() {
92
+ this._status = "stopped";
93
+ if (this._reconnectTimer) {
94
+ clearTimeout(this._reconnectTimer);
95
+ this._reconnectTimer = undefined;
96
+ }
97
+ await this.disconnect();
98
+ }
99
+ }
100
+ // ── WebSocket Source ──────────────────────────────────────────────────────────
101
+ export class WebSocketEventSource extends BaseEventSource {
102
+ ws;
103
+ constructor(config) {
104
+ super({ ...config, kind: "websocket" });
105
+ }
106
+ async connect() {
107
+ const url = this.config.options.url;
108
+ if (!url)
109
+ throw new Error("WebSocket event source requires 'url' option");
110
+ this._status = "connecting";
111
+ this.emit("connecting", { source: this.id });
112
+ return new Promise((resolve, reject) => {
113
+ const headers = this.config.options.headers ?? {};
114
+ this.ws = new WebSocket(url, { headers });
115
+ this.ws.on("open", () => {
116
+ this._status = "connected";
117
+ this._connectedAt = new Date().toISOString();
118
+ this._reconnectAttempts = 0;
119
+ this.emit("connected", { source: this.id });
120
+ resolve();
121
+ });
122
+ this.ws.on("message", (data) => {
123
+ const raw = data.toString();
124
+ let parsed;
125
+ try {
126
+ parsed = JSON.parse(raw);
127
+ }
128
+ catch {
129
+ parsed = { message: raw };
130
+ }
131
+ const type = parsed.type ?? parsed.event ?? "message";
132
+ this.emitEvent(type, parsed, raw);
133
+ });
134
+ this.ws.on("close", (code, reason) => {
135
+ this.handleDisconnect(`code=${code} reason=${reason.toString()}`);
136
+ });
137
+ this.ws.on("error", (err) => {
138
+ if (this._status === "connecting") {
139
+ reject(err);
140
+ }
141
+ this._status = "error";
142
+ this.emit("error", err);
143
+ });
144
+ });
145
+ }
146
+ async disconnect() {
147
+ if (this.ws) {
148
+ this.ws.removeAllListeners();
149
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
150
+ this.ws.close();
151
+ }
152
+ this.ws = undefined;
153
+ }
154
+ }
155
+ /** Send a message through the WebSocket */
156
+ send(data) {
157
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
158
+ throw new Error(`WebSocket source '${this.id}' is not connected`);
159
+ }
160
+ const payload = typeof data === "string" ? data : JSON.stringify(data);
161
+ this.ws.send(payload);
162
+ }
163
+ }
164
+ // ── Discord Source ───────────────────────────────────────────────────────────
165
+ /**
166
+ * Discord event source via the Discord Gateway (WebSocket).
167
+ * Uses the Discord Bot Gateway API directly — no external library needed.
168
+ *
169
+ * Required options:
170
+ * - token: Discord bot token
171
+ * - intents: Gateway intents bitmask (e.g., 513 for GUILDS + GUILD_MESSAGES)
172
+ *
173
+ * Optional:
174
+ * - filter: Array of event types to listen for (e.g., ["MESSAGE_CREATE"])
175
+ */
176
+ export class DiscordEventSource extends BaseEventSource {
177
+ ws;
178
+ heartbeatInterval;
179
+ lastSequence = null;
180
+ sessionId;
181
+ resumeGatewayUrl;
182
+ constructor(config) {
183
+ super({ ...config, kind: "discord" });
184
+ }
185
+ async connect() {
186
+ const token = this.config.options.token;
187
+ if (!token)
188
+ throw new Error("Discord event source requires 'token' option");
189
+ const intents = this.config.options.intents ?? 513; // GUILDS + GUILD_MESSAGES
190
+ const gatewayUrl = this.resumeGatewayUrl ?? "wss://gateway.discord.gg/?v=10&encoding=json";
191
+ this._status = "connecting";
192
+ this.emit("connecting", { source: this.id });
193
+ return new Promise((resolve, reject) => {
194
+ this.ws = new WebSocket(gatewayUrl);
195
+ this.ws.on("open", () => {
196
+ // Wait for HELLO before identifying
197
+ });
198
+ this.ws.on("message", (raw) => {
199
+ const payload = JSON.parse(raw.toString());
200
+ const { op, d, s, t } = payload;
201
+ if (s !== null)
202
+ this.lastSequence = s;
203
+ switch (op) {
204
+ case 10: // HELLO
205
+ this.startHeartbeat(d.heartbeat_interval);
206
+ if (this.sessionId && this.resumeGatewayUrl) {
207
+ // Resume
208
+ this.ws.send(JSON.stringify({
209
+ op: 6,
210
+ d: { token, session_id: this.sessionId, seq: this.lastSequence },
211
+ }));
212
+ }
213
+ else {
214
+ // Identify
215
+ this.ws.send(JSON.stringify({
216
+ op: 2,
217
+ d: {
218
+ token,
219
+ intents,
220
+ properties: { os: "linux", browser: "marktoflow", device: "marktoflow" },
221
+ },
222
+ }));
223
+ }
224
+ break;
225
+ case 11: // HEARTBEAT_ACK
226
+ break;
227
+ case 0: // DISPATCH
228
+ if (t === "READY") {
229
+ this.sessionId = d.session_id;
230
+ this.resumeGatewayUrl = d.resume_gateway_url;
231
+ this._status = "connected";
232
+ this._connectedAt = new Date().toISOString();
233
+ this._reconnectAttempts = 0;
234
+ this.emit("connected", { source: this.id, user: d.user });
235
+ resolve();
236
+ }
237
+ // Emit all dispatch events
238
+ this.emitEvent(t, d, payload);
239
+ break;
240
+ case 7: // RECONNECT
241
+ this.ws.close();
242
+ this.handleDisconnect("server requested reconnect");
243
+ break;
244
+ case 9: // INVALID SESSION
245
+ this.sessionId = undefined;
246
+ this.resumeGatewayUrl = undefined;
247
+ this.ws.close();
248
+ this.handleDisconnect("invalid session");
249
+ break;
250
+ }
251
+ });
252
+ this.ws.on("close", (code) => {
253
+ this.stopHeartbeat();
254
+ if (this._status === "connecting") {
255
+ reject(new Error(`Discord gateway closed during connect: ${code}`));
256
+ }
257
+ else {
258
+ this.handleDisconnect(`code=${code}`);
259
+ }
260
+ });
261
+ this.ws.on("error", (err) => {
262
+ if (this._status === "connecting") {
263
+ reject(err);
264
+ }
265
+ this.emit("error", err);
266
+ });
267
+ });
268
+ }
269
+ async disconnect() {
270
+ this.stopHeartbeat();
271
+ if (this.ws) {
272
+ this.ws.removeAllListeners();
273
+ if (this.ws.readyState === WebSocket.OPEN) {
274
+ this.ws.close(1000, "disconnect");
275
+ }
276
+ this.ws = undefined;
277
+ }
278
+ }
279
+ startHeartbeat(intervalMs) {
280
+ this.stopHeartbeat();
281
+ // Send first heartbeat immediately
282
+ this.ws?.send(JSON.stringify({ op: 1, d: this.lastSequence }));
283
+ this.heartbeatInterval = setInterval(() => {
284
+ this.ws?.send(JSON.stringify({ op: 1, d: this.lastSequence }));
285
+ }, intervalMs);
286
+ }
287
+ stopHeartbeat() {
288
+ if (this.heartbeatInterval) {
289
+ clearInterval(this.heartbeatInterval);
290
+ this.heartbeatInterval = undefined;
291
+ }
292
+ }
293
+ }
294
+ // ── Slack Socket Mode Source ─────────────────────────────────────────────────
295
+ /**
296
+ * Slack event source via Socket Mode (WebSocket).
297
+ *
298
+ * Required options:
299
+ * - appToken: Slack app-level token (xapp-...)
300
+ *
301
+ * Optional:
302
+ * - filter: Array of event types (e.g., ["message", "reaction_added"])
303
+ */
304
+ export class SlackEventSource extends BaseEventSource {
305
+ ws;
306
+ constructor(config) {
307
+ super({ ...config, kind: "slack" });
308
+ }
309
+ async connect() {
310
+ const appToken = this.config.options.appToken;
311
+ if (!appToken)
312
+ throw new Error("Slack event source requires 'appToken' option");
313
+ this._status = "connecting";
314
+ this.emit("connecting", { source: this.id });
315
+ // Get WebSocket URL from Slack
316
+ const res = await fetch("https://slack.com/api/apps.connections.open", {
317
+ method: "POST",
318
+ headers: {
319
+ Authorization: `Bearer ${appToken}`,
320
+ "Content-Type": "application/x-www-form-urlencoded",
321
+ },
322
+ });
323
+ const body = await res.json();
324
+ if (!body.ok || !body.url) {
325
+ throw new Error(`Slack connections.open failed: ${body.error ?? "unknown"}`);
326
+ }
327
+ return new Promise((resolve, reject) => {
328
+ this.ws = new WebSocket(body.url);
329
+ this.ws.on("open", () => {
330
+ this._status = "connected";
331
+ this._connectedAt = new Date().toISOString();
332
+ this._reconnectAttempts = 0;
333
+ this.emit("connected", { source: this.id });
334
+ resolve();
335
+ });
336
+ this.ws.on("message", (raw) => {
337
+ const payload = JSON.parse(raw.toString());
338
+ // Acknowledge envelope
339
+ if (payload.envelope_id) {
340
+ this.ws.send(JSON.stringify({ envelope_id: payload.envelope_id }));
341
+ }
342
+ if (payload.type === "events_api") {
343
+ const event = payload.payload?.event;
344
+ if (event) {
345
+ this.emitEvent(event.type, event, payload);
346
+ }
347
+ }
348
+ else if (payload.type === "interactive") {
349
+ this.emitEvent("interactive", payload.payload ?? {}, payload);
350
+ }
351
+ else if (payload.type === "slash_commands") {
352
+ this.emitEvent("slash_command", payload.payload ?? {}, payload);
353
+ }
354
+ else if (payload.type === "disconnect") {
355
+ this.handleDisconnect("slack requested disconnect");
356
+ }
357
+ });
358
+ this.ws.on("close", () => {
359
+ if (this._status === "connecting") {
360
+ reject(new Error("Slack WebSocket closed during connect"));
361
+ }
362
+ else {
363
+ this.handleDisconnect("connection closed");
364
+ }
365
+ });
366
+ this.ws.on("error", (err) => {
367
+ if (this._status === "connecting")
368
+ reject(err);
369
+ this.emit("error", err);
370
+ });
371
+ });
372
+ }
373
+ async disconnect() {
374
+ if (this.ws) {
375
+ this.ws.removeAllListeners();
376
+ if (this.ws.readyState === WebSocket.OPEN) {
377
+ this.ws.close();
378
+ }
379
+ this.ws = undefined;
380
+ }
381
+ }
382
+ }
383
+ // ── Cron Event Source ────────────────────────────────────────────────────────
384
+ /**
385
+ * Cron event source — emits events on a schedule.
386
+ *
387
+ * Options:
388
+ * - schedule: Cron expression or interval string (e.g., "30m", "1h")
389
+ * - payload: Optional static payload to include with each event
390
+ */
391
+ export class CronEventSource extends BaseEventSource {
392
+ timer;
393
+ intervalMs;
394
+ constructor(config) {
395
+ super({ ...config, kind: "cron" });
396
+ this.intervalMs = parseDuration(config.options.schedule);
397
+ }
398
+ async connect() {
399
+ this._status = "connected";
400
+ this._connectedAt = new Date().toISOString();
401
+ this.emit("connected", { source: this.id });
402
+ // Emit first event immediately if configured
403
+ if (this.config.options.immediate) {
404
+ this.tick();
405
+ }
406
+ this.timer = setInterval(() => this.tick(), this.intervalMs);
407
+ }
408
+ async disconnect() {
409
+ if (this.timer) {
410
+ clearInterval(this.timer);
411
+ this.timer = undefined;
412
+ }
413
+ }
414
+ tick() {
415
+ const payload = this.config.options.payload ?? {};
416
+ this.emitEvent("tick", { ...payload, scheduledAt: new Date().toISOString() });
417
+ }
418
+ }
419
+ // ── SSE (Server-Sent Events) Source ──────────────────────────────────────────
420
+ /**
421
+ * HTTP Server-Sent Events listener.
422
+ *
423
+ * Options:
424
+ * - url: SSE endpoint URL
425
+ * - headers: Optional headers
426
+ */
427
+ export class SSEEventSource extends BaseEventSource {
428
+ controller;
429
+ constructor(config) {
430
+ super({ ...config, kind: "http-stream" });
431
+ }
432
+ async connect() {
433
+ const url = this.config.options.url;
434
+ if (!url)
435
+ throw new Error("SSE event source requires 'url' option");
436
+ this._status = "connecting";
437
+ this.controller = new AbortController();
438
+ const headers = this.config.options.headers ?? {};
439
+ const res = await fetch(url, {
440
+ headers: { Accept: "text/event-stream", ...headers },
441
+ signal: this.controller.signal,
442
+ });
443
+ if (!res.ok) {
444
+ throw new Error(`SSE connect failed: ${res.status} ${res.statusText}`);
445
+ }
446
+ this._status = "connected";
447
+ this._connectedAt = new Date().toISOString();
448
+ this._reconnectAttempts = 0;
449
+ this.emit("connected", { source: this.id });
450
+ // Parse SSE stream
451
+ const reader = res.body?.getReader();
452
+ if (!reader)
453
+ throw new Error("SSE response has no body");
454
+ const decoder = new TextDecoder();
455
+ let buffer = "";
456
+ const readLoop = async () => {
457
+ try {
458
+ while (true) {
459
+ const { done, value } = await reader.read();
460
+ if (done)
461
+ break;
462
+ buffer += decoder.decode(value, { stream: true });
463
+ const lines = buffer.split("\n");
464
+ buffer = lines.pop() ?? "";
465
+ let eventType = "message";
466
+ let eventData = "";
467
+ for (const line of lines) {
468
+ if (line.startsWith("event:")) {
469
+ eventType = line.slice(6).trim();
470
+ }
471
+ else if (line.startsWith("data:")) {
472
+ eventData += (eventData ? "\n" : "") + line.slice(5).trim();
473
+ }
474
+ else if (line === "") {
475
+ // End of event
476
+ if (eventData) {
477
+ let parsed;
478
+ try {
479
+ parsed = JSON.parse(eventData);
480
+ }
481
+ catch {
482
+ parsed = { data: eventData };
483
+ }
484
+ this.emitEvent(eventType, parsed, eventData);
485
+ }
486
+ eventType = "message";
487
+ eventData = "";
488
+ }
489
+ }
490
+ }
491
+ }
492
+ catch (err) {
493
+ if (err.name !== "AbortError") {
494
+ this.emit("error", err);
495
+ }
496
+ }
497
+ this.handleDisconnect("stream ended");
498
+ };
499
+ readLoop();
500
+ }
501
+ async disconnect() {
502
+ this.controller?.abort();
503
+ this.controller = undefined;
504
+ }
505
+ }
506
+ // ── RSS Event Source ─────────────────────────────────────────────────────────
507
+ /**
508
+ * RSS/Atom feed event source — polls a feed and emits events for new items.
509
+ *
510
+ * Options:
511
+ * - url: Feed URL (required)
512
+ * - interval: Polling interval as duration string (default: "5m")
513
+ * - immediate: Poll immediately on connect (default: false)
514
+ * - headers: Custom HTTP headers for feed requests
515
+ * - maxItems: Max new items to emit per poll (default: unlimited)
516
+ */
517
+ export class RssEventSource extends BaseEventSource {
518
+ timer;
519
+ intervalMs;
520
+ seenIds = new Set();
521
+ firstPoll = true;
522
+ static MAX_SEEN_IDS = 10_000;
523
+ constructor(config) {
524
+ super({ ...config, kind: "rss" });
525
+ const interval = config.options.interval ?? "5m";
526
+ this.intervalMs = parseDuration(interval);
527
+ }
528
+ async connect() {
529
+ const url = this.config.options.url;
530
+ if (!url)
531
+ throw new Error("RSS event source requires 'url' option");
532
+ this._status = "connected";
533
+ this._connectedAt = new Date().toISOString();
534
+ this.emit("connected", { source: this.id });
535
+ if (this.config.options.immediate) {
536
+ await this.poll();
537
+ }
538
+ this.timer = setInterval(() => {
539
+ this.poll().catch((err) => {
540
+ this.emit("error", err);
541
+ });
542
+ }, this.intervalMs);
543
+ }
544
+ async disconnect() {
545
+ if (this.timer) {
546
+ clearInterval(this.timer);
547
+ this.timer = undefined;
548
+ }
549
+ }
550
+ async poll() {
551
+ const url = this.config.options.url;
552
+ const headers = this.config.options.headers ?? {};
553
+ const maxItems = this.config.options.maxItems;
554
+ let xml;
555
+ try {
556
+ const res = await fetch(url, {
557
+ headers: { Accept: "application/rss+xml, application/atom+xml, application/xml, text/xml", ...headers },
558
+ });
559
+ if (!res.ok) {
560
+ throw new Error(`RSS fetch failed: ${res.status} ${res.statusText}`);
561
+ }
562
+ xml = await res.text();
563
+ }
564
+ catch (err) {
565
+ this.emit("error", err);
566
+ return;
567
+ }
568
+ const items = parseRssItems(xml);
569
+ const newItems = [];
570
+ for (const item of items) {
571
+ const id = item.guid || item.link || item.title;
572
+ if (!id)
573
+ continue;
574
+ if (!this.seenIds.has(id)) {
575
+ this.seenIds.add(id);
576
+ if (!this.firstPoll) {
577
+ newItems.push(item);
578
+ }
579
+ }
580
+ }
581
+ this.firstPoll = false;
582
+ // Evict oldest entries if seenIds grows too large
583
+ if (this.seenIds.size > RssEventSource.MAX_SEEN_IDS) {
584
+ const entries = Array.from(this.seenIds);
585
+ const toRemove = entries.length - RssEventSource.MAX_SEEN_IDS;
586
+ for (let i = 0; i < toRemove; i++) {
587
+ this.seenIds.delete(entries[i]);
588
+ }
589
+ }
590
+ const toEmit = maxItems ? newItems.slice(0, maxItems) : newItems;
591
+ for (const item of toEmit) {
592
+ this.emitEvent("new_item", {
593
+ title: item.title,
594
+ link: item.link,
595
+ description: item.description,
596
+ pubDate: item.pubDate,
597
+ guid: item.guid,
598
+ author: item.author,
599
+ categories: item.categories,
600
+ feedUrl: url,
601
+ });
602
+ }
603
+ }
604
+ }
605
+ function extractTag(xml, tag) {
606
+ const re = new RegExp(`<${tag}[^>]*>\\s*(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([\\s\\S]*?))\\s*</${tag}>`, "i");
607
+ const m = xml.match(re);
608
+ if (!m)
609
+ return "";
610
+ return (m[1] ?? m[2] ?? "").trim();
611
+ }
612
+ function extractAtomLink(xml) {
613
+ const m = xml.match(/<link[^>]+href\s*=\s*["']([^"']+)["'][^>]*\/?>/i);
614
+ return m ? m[1] : "";
615
+ }
616
+ function extractAllTags(xml, tag) {
617
+ const re = new RegExp(`<${tag}[^>]*>\\s*(?:<!\\[CDATA\\[([\\s\\S]*?)\\]\\]>|([\\s\\S]*?))\\s*</${tag}>`, "gi");
618
+ const results = [];
619
+ let m;
620
+ while ((m = re.exec(xml)) !== null) {
621
+ const val = (m[1] ?? m[2] ?? "").trim();
622
+ if (val)
623
+ results.push(val);
624
+ }
625
+ return results;
626
+ }
627
+ function parseRssItems(xml) {
628
+ const items = [];
629
+ // Detect RSS 2.0 vs Atom
630
+ const isAtom = /<feed[\s>]/i.test(xml);
631
+ if (isAtom) {
632
+ // Atom: split on <entry>
633
+ const entries = xml.split(/<entry[\s>]/i).slice(1);
634
+ for (const entry of entries) {
635
+ const block = entry.split(/<\/entry>/i)[0];
636
+ items.push({
637
+ title: extractTag(block, "title"),
638
+ link: extractAtomLink(block) || extractTag(block, "link"),
639
+ description: extractTag(block, "summary") || extractTag(block, "content"),
640
+ pubDate: extractTag(block, "updated") || extractTag(block, "published"),
641
+ guid: extractTag(block, "id"),
642
+ author: extractTag(block, "name") || extractTag(block, "author"),
643
+ categories: extractAllTags(block, "category"),
644
+ });
645
+ }
646
+ }
647
+ else {
648
+ // RSS 2.0: split on <item>
649
+ const rawItems = xml.split(/<item[\s>]/i).slice(1);
650
+ for (const raw of rawItems) {
651
+ const block = raw.split(/<\/item>/i)[0];
652
+ items.push({
653
+ title: extractTag(block, "title"),
654
+ link: extractTag(block, "link"),
655
+ description: extractTag(block, "description"),
656
+ pubDate: extractTag(block, "pubDate"),
657
+ guid: extractTag(block, "guid"),
658
+ author: extractTag(block, "author") || extractTag(block, "dc:creator"),
659
+ categories: extractAllTags(block, "category"),
660
+ });
661
+ }
662
+ }
663
+ return items;
664
+ }
665
+ // ── Factory ──────────────────────────────────────────────────────────────────
666
+ export function createEventSource(config) {
667
+ switch (config.kind) {
668
+ case "websocket":
669
+ return new WebSocketEventSource(config);
670
+ case "discord":
671
+ return new DiscordEventSource(config);
672
+ case "slack":
673
+ return new SlackEventSource(config);
674
+ case "cron":
675
+ return new CronEventSource(config);
676
+ case "http-stream":
677
+ return new SSEEventSource(config);
678
+ case "rss":
679
+ return new RssEventSource(config);
680
+ default:
681
+ throw new Error(`Unknown event source kind: ${config.kind}`);
682
+ }
683
+ }
684
+ // ── Event Source Manager ─────────────────────────────────────────────────────
685
+ /**
686
+ * Manages multiple event sources and provides a unified event stream.
687
+ */
688
+ export class EventSourceManager extends EventEmitter {
689
+ sources = new Map();
690
+ /** Add and connect an event source */
691
+ async add(config) {
692
+ if (this.sources.has(config.id)) {
693
+ throw new Error(`Event source '${config.id}' already exists`);
694
+ }
695
+ const source = createEventSource(config);
696
+ // Forward events
697
+ source.on("event", (event) => {
698
+ this.emit("event", event);
699
+ });
700
+ source.on("connected", (info) => this.emit("source:connected", info));
701
+ source.on("disconnected", (info) => this.emit("source:disconnected", info));
702
+ source.on("error", (err) => this.emit("source:error", { source: config.id, error: err }));
703
+ this.sources.set(config.id, source);
704
+ await source.connect();
705
+ return source;
706
+ }
707
+ /** Remove and disconnect an event source */
708
+ async remove(id) {
709
+ const source = this.sources.get(id);
710
+ if (source) {
711
+ await source.stop();
712
+ source.removeAllListeners();
713
+ this.sources.delete(id);
714
+ }
715
+ }
716
+ /** Get a source by id */
717
+ get(id) {
718
+ return this.sources.get(id);
719
+ }
720
+ /** Get stats for all sources */
721
+ stats() {
722
+ return Array.from(this.sources.values()).map((s) => s.stats);
723
+ }
724
+ /** Wait for the next event from any source, with optional filter */
725
+ waitForEvent(options) {
726
+ return new Promise((resolve, reject) => {
727
+ let timer;
728
+ const handler = (event) => {
729
+ if (options?.source && event.source !== options.source)
730
+ return;
731
+ if (options?.type && event.type !== options.type)
732
+ return;
733
+ if (options?.filter && !options.filter(event))
734
+ return;
735
+ if (timer)
736
+ clearTimeout(timer);
737
+ this.removeListener("event", handler);
738
+ resolve(event);
739
+ };
740
+ this.on("event", handler);
741
+ if (options?.timeout) {
742
+ timer = setTimeout(() => {
743
+ this.removeListener("event", handler);
744
+ reject(new Error(`Timed out waiting for event after ${options.timeout}ms`));
745
+ }, options.timeout);
746
+ }
747
+ });
748
+ }
749
+ /** Stop all sources */
750
+ async stopAll() {
751
+ const promises = Array.from(this.sources.values()).map((s) => s.stop());
752
+ await Promise.all(promises);
753
+ this.sources.clear();
754
+ }
755
+ }
756
+ // parseDuration imported from ./utils/duration.js (single source of truth)
757
+ //# sourceMappingURL=event-source.js.map