@quarry-systems/drift-event-listener 0.1.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/README.md +430 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
# MCG Event Listener Plugin
|
|
2
|
+
|
|
3
|
+
A powerful event listening plugin for Managed Cyclic Graph (MCG) that enables nodes to pause and wait for external events from webhooks, polling, pub/sub systems, or custom sources.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ **Webhook Listening**: Wait for HTTP webhook calls
|
|
8
|
+
- ✅ **Polling**: Poll endpoints until conditions are met
|
|
9
|
+
- ✅ **Pub/Sub**: Subscribe to topics and wait for messages
|
|
10
|
+
- ✅ **Custom Listeners**: Implement your own event sources
|
|
11
|
+
- ✅ **Event Filtering**: Validate events before accepting
|
|
12
|
+
- ✅ **Event Transformation**: Process event payloads
|
|
13
|
+
- ✅ **Timeouts**: Configure maximum wait times
|
|
14
|
+
- ✅ **Callbacks**: React to events and timeouts
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @quarry-systems/mcg-event-listener
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### Plugin-Based Approach
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { ManagedCyclicGraph } from '@quarry-systems/managed-cyclic-graph';
|
|
28
|
+
import { mcgEventListenerPlugin, webhook, pubsub } from '@quarry-systems/mcg-event-listener';
|
|
29
|
+
|
|
30
|
+
const graph = new ManagedCyclicGraph()
|
|
31
|
+
.use(mcgEventListenerPlugin)
|
|
32
|
+
|
|
33
|
+
.node('waitForPayment', {
|
|
34
|
+
type: 'eventnode',
|
|
35
|
+
meta: {
|
|
36
|
+
event: webhook('/payment-complete', {
|
|
37
|
+
secret: 'my-webhook-secret',
|
|
38
|
+
secretHeader: 'X-Webhook-Secret'
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
.node('waitForInventory', {
|
|
44
|
+
type: 'eventnode',
|
|
45
|
+
meta: {
|
|
46
|
+
event: pubsub('inventory-updated')
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
.build();
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Action-Based Approach
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
import { ManagedCyclicGraph } from '@quarry-systems/managed-cyclic-graph';
|
|
57
|
+
import { createEventAction, poll } from '@quarry-systems/mcg-event-listener';
|
|
58
|
+
|
|
59
|
+
const graph = new ManagedCyclicGraph()
|
|
60
|
+
.node('submitJob', {
|
|
61
|
+
execute: [
|
|
62
|
+
// ... submit job logic
|
|
63
|
+
]
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
.node('waitForCompletion', {
|
|
67
|
+
execute: [
|
|
68
|
+
createEventAction('waitForCompletion',
|
|
69
|
+
poll('https://api.example.com/job/status',
|
|
70
|
+
(response) => response.status === 'completed',
|
|
71
|
+
5000 // Poll every 5 seconds
|
|
72
|
+
)
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
.node('processResults', {
|
|
78
|
+
execute: [
|
|
79
|
+
// ... process results
|
|
80
|
+
]
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
.build();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API Reference
|
|
87
|
+
|
|
88
|
+
### Helper Functions
|
|
89
|
+
|
|
90
|
+
#### `webhook(path: string, options?: WebhookConfig)`
|
|
91
|
+
Create a webhook listener that waits for HTTP requests.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
webhook('/payment-complete')
|
|
95
|
+
|
|
96
|
+
webhook('/order-update', {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
secret: 'my-secret',
|
|
99
|
+
secretHeader: 'X-Webhook-Secret',
|
|
100
|
+
port: 3000
|
|
101
|
+
})
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `poll(url: string, condition: (response) => boolean, intervalMs?: number)`
|
|
105
|
+
Create a polling listener that checks an endpoint repeatedly.
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
poll(
|
|
109
|
+
'https://api.example.com/job/123',
|
|
110
|
+
(response) => response.status === 'done',
|
|
111
|
+
2000 // Check every 2 seconds
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### `pubsub(topic: string, provider?: 'memory' | 'redis' | 'custom')`
|
|
116
|
+
Create a pub/sub listener that waits for messages on a topic.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
pubsub('order-completed')
|
|
120
|
+
pubsub('inventory-updated', 'memory')
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
#### `custom(listen: (ctx) => Promise<any>)`
|
|
124
|
+
Create a custom event listener with your own logic.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
custom(async (ctx) => {
|
|
128
|
+
// Your custom event listening logic
|
|
129
|
+
return await myEventSource.waitForEvent();
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Configuration Options
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
interface EventListenerConfig {
|
|
137
|
+
source: 'webhook' | 'poll' | 'pubsub' | 'custom';
|
|
138
|
+
sourceConfig: WebhookConfig | PollConfig | PubSubConfig | CustomConfig;
|
|
139
|
+
timeoutMs?: number; // Timeout in milliseconds
|
|
140
|
+
filter?: (event: any) => boolean; // Event filter function
|
|
141
|
+
transform?: (event: any, ctx) => any; // Event transformation
|
|
142
|
+
storePath?: string; // Custom storage path
|
|
143
|
+
onEvent?: (event: any, ctx) => void; // Event callback
|
|
144
|
+
onTimeout?: (ctx) => void; // Timeout callback
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Webhook Configuration
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
interface WebhookConfig {
|
|
152
|
+
path: string; // Webhook endpoint path
|
|
153
|
+
method?: 'GET' | 'POST' | ...; // HTTP method
|
|
154
|
+
port?: number; // Port to listen on
|
|
155
|
+
secret?: string; // Validation secret
|
|
156
|
+
secretHeader?: string; // Header name for secret
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Poll Configuration
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
interface PollConfig {
|
|
164
|
+
url: string; // URL to poll
|
|
165
|
+
intervalMs: number; // Polling interval
|
|
166
|
+
maxAttempts?: number; // Max poll attempts
|
|
167
|
+
method?: 'GET' | 'POST'; // HTTP method
|
|
168
|
+
headers?: Record<string, string>; // Request headers
|
|
169
|
+
condition: (response: any) => boolean; // Success condition
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Pub/Sub Configuration
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
interface PubSubConfig {
|
|
177
|
+
topic: string; // Topic/channel name
|
|
178
|
+
provider?: 'memory' | 'redis' | 'custom';
|
|
179
|
+
subscribe?: (topic, callback) => unsubscribe; // Custom subscriber
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Examples
|
|
184
|
+
|
|
185
|
+
### Payment Processing Workflow
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
const graph = new ManagedCyclicGraph()
|
|
189
|
+
.use(mcgEventListenerPlugin)
|
|
190
|
+
|
|
191
|
+
.node('createOrder', {
|
|
192
|
+
execute: [/* create order */]
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
.node('waitForPayment', {
|
|
196
|
+
type: 'eventnode',
|
|
197
|
+
meta: {
|
|
198
|
+
event: {
|
|
199
|
+
...webhook('/stripe-webhook'),
|
|
200
|
+
timeoutMs: 300000, // 5 minute timeout
|
|
201
|
+
filter: (event) => event.type === 'payment_intent.succeeded',
|
|
202
|
+
onTimeout: (ctx) => {
|
|
203
|
+
console.log('Payment timeout, canceling order');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
.node('fulfillOrder', {
|
|
210
|
+
execute: [/* fulfill order */]
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
.build();
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Job Status Polling
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const graph = new ManagedCyclicGraph()
|
|
220
|
+
.use(mcgEventListenerPlugin)
|
|
221
|
+
|
|
222
|
+
.node('submitJob', {
|
|
223
|
+
execute: [
|
|
224
|
+
{
|
|
225
|
+
id: 'submit',
|
|
226
|
+
run: async (ctx) => {
|
|
227
|
+
const jobId = await submitJob();
|
|
228
|
+
ctx.data.jobId = jobId;
|
|
229
|
+
return ctx;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
]
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
.node('pollStatus', {
|
|
236
|
+
type: 'eventnode',
|
|
237
|
+
meta: {
|
|
238
|
+
event: poll(
|
|
239
|
+
'https://api.example.com/jobs/${data.jobId}',
|
|
240
|
+
(response) => response.status === 'completed',
|
|
241
|
+
5000
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
.node('processResults', {
|
|
247
|
+
execute: [/* process results */]
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
.build();
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Multi-Event Coordination
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const graph = new ManagedCyclicGraph()
|
|
257
|
+
.use(mcgEventListenerPlugin)
|
|
258
|
+
|
|
259
|
+
.node('waitForInventory', {
|
|
260
|
+
type: 'eventnode',
|
|
261
|
+
meta: {
|
|
262
|
+
event: pubsub('inventory-available')
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
.node('waitForApproval', {
|
|
267
|
+
type: 'eventnode',
|
|
268
|
+
meta: {
|
|
269
|
+
event: pubsub('manager-approved')
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
|
|
273
|
+
.node('processOrder', {
|
|
274
|
+
execute: [/* both events received, process order */]
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
.build();
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Event Transformation
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
const graph = new ManagedCyclicGraph()
|
|
284
|
+
.use(mcgEventListenerPlugin)
|
|
285
|
+
|
|
286
|
+
.node('waitForWebhook', {
|
|
287
|
+
type: 'eventnode',
|
|
288
|
+
meta: {
|
|
289
|
+
event: {
|
|
290
|
+
...webhook('/data-update'),
|
|
291
|
+
transform: (event, ctx) => {
|
|
292
|
+
// Extract only what you need
|
|
293
|
+
return {
|
|
294
|
+
userId: event.body.user.id,
|
|
295
|
+
timestamp: event.body.timestamp,
|
|
296
|
+
changes: event.body.changes
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
onEvent: (event, ctx) => {
|
|
300
|
+
console.log('Received event:', event);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
|
|
306
|
+
.build();
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### Custom Event Source
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import { custom } from '@quarry-systems/mcg-event-listener';
|
|
313
|
+
|
|
314
|
+
const graph = new ManagedCyclicGraph()
|
|
315
|
+
.use(mcgEventListenerPlugin)
|
|
316
|
+
|
|
317
|
+
.node('waitForCustomEvent', {
|
|
318
|
+
type: 'eventnode',
|
|
319
|
+
meta: {
|
|
320
|
+
event: custom(async (ctx) => {
|
|
321
|
+
// Connect to your custom event source
|
|
322
|
+
const connection = await connectToEventSource();
|
|
323
|
+
|
|
324
|
+
return new Promise((resolve) => {
|
|
325
|
+
connection.on('myEvent', (data) => {
|
|
326
|
+
resolve(data);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
.build();
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Metadata Storage
|
|
337
|
+
|
|
338
|
+
Event metadata is stored in the context at `data.events.{nodeId}` by default:
|
|
339
|
+
|
|
340
|
+
```typescript
|
|
341
|
+
{
|
|
342
|
+
data: {
|
|
343
|
+
events: {
|
|
344
|
+
waitForPayment: {
|
|
345
|
+
source: 'webhook',
|
|
346
|
+
receivedAt: 1701234567890,
|
|
347
|
+
event: { /* event payload */ },
|
|
348
|
+
timedOut: false
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Publishing Events
|
|
356
|
+
|
|
357
|
+
For testing or integration, you can publish events to the global event bus:
|
|
358
|
+
|
|
359
|
+
```typescript
|
|
360
|
+
import { globalEventBus } from '@quarry-systems/mcg-event-listener';
|
|
361
|
+
|
|
362
|
+
// Publish to webhook
|
|
363
|
+
globalEventBus.publish('webhook:/payment-complete', {
|
|
364
|
+
method: 'POST',
|
|
365
|
+
headers: { 'X-Webhook-Secret': 'my-secret' },
|
|
366
|
+
body: { orderId: '123', status: 'paid' }
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Publish to pub/sub
|
|
370
|
+
globalEventBus.publish('order-completed', {
|
|
371
|
+
orderId: '123',
|
|
372
|
+
total: 99.99
|
|
373
|
+
});
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
## Use Cases
|
|
377
|
+
|
|
378
|
+
- **Payment Processing**: Wait for payment confirmations
|
|
379
|
+
- **Job Completion**: Poll job status until done
|
|
380
|
+
- **Inventory Management**: React to stock updates
|
|
381
|
+
- **Approval Workflows**: Wait for human approval
|
|
382
|
+
- **External Integrations**: Respond to third-party events
|
|
383
|
+
- **Real-time Updates**: React to live data changes
|
|
384
|
+
- **Async Operations**: Coordinate distributed systems
|
|
385
|
+
- **Event-Driven Architecture**: Build reactive workflows
|
|
386
|
+
|
|
387
|
+
## Best Practices
|
|
388
|
+
|
|
389
|
+
1. **Always Set Timeouts** to prevent infinite waits
|
|
390
|
+
2. **Use Filters** to validate events before processing
|
|
391
|
+
3. **Transform Events** to extract only needed data
|
|
392
|
+
4. **Add Callbacks** for logging and monitoring
|
|
393
|
+
5. **Secure Webhooks** with secrets and validation
|
|
394
|
+
6. **Poll Responsibly** with appropriate intervals
|
|
395
|
+
7. **Handle Timeouts** gracefully with fallback logic
|
|
396
|
+
|
|
397
|
+
## Integration with Other Plugins
|
|
398
|
+
|
|
399
|
+
Combine with Timer plugin for retry logic:
|
|
400
|
+
|
|
401
|
+
```typescript
|
|
402
|
+
import { mcgEventListenerPlugin, poll } from '@quarry-systems/mcg-event-listener';
|
|
403
|
+
import { mcgTimerPlugin, sleep } from '@quarry-systems/mcg-timer';
|
|
404
|
+
|
|
405
|
+
const graph = new ManagedCyclicGraph()
|
|
406
|
+
.use(mcgEventListenerPlugin)
|
|
407
|
+
.use(mcgTimerPlugin)
|
|
408
|
+
|
|
409
|
+
.node('pollJob', {
|
|
410
|
+
type: 'eventnode',
|
|
411
|
+
meta: {
|
|
412
|
+
event: poll(url, condition, 5000),
|
|
413
|
+
timeoutMs: 30000
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
.node('waitBeforeRetry', {
|
|
418
|
+
type: 'timernode',
|
|
419
|
+
meta: { timer: sleep(10000) }
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
.edge('pollJob', 'waitBeforeRetry', ['timedOut'])
|
|
423
|
+
.edge('waitBeforeRetry', 'pollJob', 'any')
|
|
424
|
+
|
|
425
|
+
.build();
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
## License
|
|
429
|
+
|
|
430
|
+
ISC
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quarry-systems/drift-event-listener",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Event listener and webhook plugin for Drift",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"types": "./src/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p .",
|
|
9
|
+
"clean": "rimraf dist || rm -rf dist",
|
|
10
|
+
"dev": "tsc -p . --watch",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"example": "npx ts-node examples/basic-usage.ts",
|
|
14
|
+
"example:basic": "npx ts-node examples/basic-usage.ts",
|
|
15
|
+
"example:plugin": "npx ts-node examples/plugin-based-usage.ts"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"drift",
|
|
19
|
+
"plugin",
|
|
20
|
+
"event",
|
|
21
|
+
"webhook",
|
|
22
|
+
"pubsub",
|
|
23
|
+
"listener"
|
|
24
|
+
],
|
|
25
|
+
"author": "Brett Nye",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@types/node": "^20.10.0",
|
|
29
|
+
"ts-node": "^10.9.1",
|
|
30
|
+
"typescript": "^5.3.0",
|
|
31
|
+
"vitest": "^2.1.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md",
|
|
36
|
+
"LICENSE.md",
|
|
37
|
+
"CHANGELOG.md"
|
|
38
|
+
],
|
|
39
|
+
"type": "commonjs"
|
|
40
|
+
}
|