@blokjs/trigger-pubsub 0.6.17 → 0.6.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/PubSubTrigger.js +20 -1
- package/package.json +5 -4
- package/CHANGELOG.md +0 -22
- package/__tests__/integration/gcp-pubsub.real-emulator.test.ts +0 -235
- package/__tests__/integration/kafka-pubsub.real-kafka.test.ts +0 -269
- package/__tests__/integration/nats-pubsub.real-nats.test.ts +0 -138
- package/src/PubSubTrigger.test.ts +0 -151
- package/src/PubSubTrigger.ts +0 -402
- package/src/adapters/AWSSNSAdapter.ts +0 -322
- package/src/adapters/AzureServiceBusAdapter.ts +0 -263
- package/src/adapters/GCPPubSubAdapter.ts +0 -236
- package/src/adapters/KafkaPubSubAdapter.ts +0 -194
- package/src/adapters/NATSPubSubAdapter.ts +0 -326
- package/src/adapters/RedisStreamsPubSubAdapter.ts +0 -225
- package/src/adapters/factory.test.ts +0 -87
- package/src/adapters/factory.ts +0 -88
- package/src/adapters/new-adapters.test.ts +0 -108
- package/src/index.ts +0 -67
- package/template/.env.example +0 -8
- package/template/package.json +0 -44
- package/template/src/Nodes.ts +0 -10
- package/template/src/Workflows.ts +0 -8
- package/template/src/index.ts +0 -41
- package/template/src/runner/PubSubServer.ts +0 -39
- package/template/src/runner/types/Workflows.ts +0 -7
- package/template/src/workflows/messages/on-message.ts +0 -48
- package/template/tsconfig.json +0 -31
- package/template/vitest.config.ts +0 -39
- package/tsconfig.json +0 -32
package/src/PubSubTrigger.ts
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PubSubTrigger - Base class for pub/sub-based workflow triggers
|
|
3
|
-
*
|
|
4
|
-
* Extends TriggerBase to support pub/sub triggers:
|
|
5
|
-
* - GCP Pub/Sub
|
|
6
|
-
* - AWS SNS (via SQS subscription)
|
|
7
|
-
* - Azure Service Bus
|
|
8
|
-
*
|
|
9
|
-
* Pattern:
|
|
10
|
-
* 1. loadNodes() - Load available nodes into NodeMap
|
|
11
|
-
* 2. loadWorkflows() - Load workflows with pubsub triggers
|
|
12
|
-
* 3. startSubscriber() - Connect to pub/sub and start receiving messages
|
|
13
|
-
* 4. For each message:
|
|
14
|
-
* - Match workflow by trigger config (topic/subscription)
|
|
15
|
-
* - Create context with this.createContext()
|
|
16
|
-
* - Populate ctx.request with message data
|
|
17
|
-
* - Execute workflow via this.run(ctx)
|
|
18
|
-
* - Ack/nack based on response
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import type { HelperResponse, PubSubProvider, PubSubTriggerOpts } from "@blokjs/helper";
|
|
22
|
-
import {
|
|
23
|
-
type BlokService,
|
|
24
|
-
DefaultLogger,
|
|
25
|
-
type GlobalOptions,
|
|
26
|
-
NodeMap,
|
|
27
|
-
TriggerBase,
|
|
28
|
-
type TriggerResponse,
|
|
29
|
-
} from "@blokjs/runner";
|
|
30
|
-
import type { Context, RequestContext } from "@blokjs/shared";
|
|
31
|
-
import { type Span, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
32
|
-
import { v4 as uuid } from "uuid";
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Message received from pub/sub
|
|
36
|
-
*/
|
|
37
|
-
export interface PubSubMessage {
|
|
38
|
-
/** Unique message ID */
|
|
39
|
-
id: string;
|
|
40
|
-
/** Message body (parsed) */
|
|
41
|
-
body: unknown;
|
|
42
|
-
/** Message attributes/metadata */
|
|
43
|
-
attributes: Record<string, string>;
|
|
44
|
-
/** Original raw message from provider */
|
|
45
|
-
raw: unknown;
|
|
46
|
-
/** Topic name */
|
|
47
|
-
topic: string;
|
|
48
|
-
/** Subscription name */
|
|
49
|
-
subscription?: string;
|
|
50
|
-
/** Publish timestamp */
|
|
51
|
-
publishTime?: Date;
|
|
52
|
-
/** Acknowledge the message */
|
|
53
|
-
ack: () => Promise<void>;
|
|
54
|
-
/** Reject/nack the message */
|
|
55
|
-
nack: () => Promise<void>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Pub/Sub adapter interface - implemented by each provider
|
|
60
|
-
*/
|
|
61
|
-
export interface PubSubAdapter {
|
|
62
|
-
/** Provider name */
|
|
63
|
-
readonly provider: PubSubProvider;
|
|
64
|
-
|
|
65
|
-
/** Connect to the pub/sub system */
|
|
66
|
-
connect(): Promise<void>;
|
|
67
|
-
|
|
68
|
-
/** Disconnect from the pub/sub system */
|
|
69
|
-
disconnect(): Promise<void>;
|
|
70
|
-
|
|
71
|
-
/** Subscribe to a topic and receive messages */
|
|
72
|
-
subscribe(config: PubSubTriggerOpts, handler: (message: PubSubMessage) => Promise<void>): Promise<void>;
|
|
73
|
-
|
|
74
|
-
/** Unsubscribe from a topic */
|
|
75
|
-
unsubscribe(subscription: string): Promise<void>;
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* v0.7 PR 6 — publish a single message to a topic. Used by the
|
|
79
|
-
* `@blokjs/pubsub-publish` helper and any workflow that fan-outs
|
|
80
|
-
* events to subscribers. Provider-portable: each adapter wraps its
|
|
81
|
-
* native producer client.
|
|
82
|
-
*
|
|
83
|
-
* Optional `partitionKey` / `orderingKey` is honored by providers
|
|
84
|
-
* that support per-key ordering (Kafka, GCP Pub/Sub ordered
|
|
85
|
-
* delivery). Ignored otherwise.
|
|
86
|
-
*/
|
|
87
|
-
publish(topic: string, payload: unknown, opts?: { partitionKey?: string; orderingKey?: string }): Promise<void>;
|
|
88
|
-
|
|
89
|
-
/** Check if connected */
|
|
90
|
-
isConnected(): boolean;
|
|
91
|
-
|
|
92
|
-
/** Health check */
|
|
93
|
-
healthCheck(): Promise<boolean>;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Workflow model with pub/sub trigger configuration
|
|
98
|
-
*/
|
|
99
|
-
interface PubSubWorkflowModel {
|
|
100
|
-
path: string;
|
|
101
|
-
config: {
|
|
102
|
-
name: string;
|
|
103
|
-
version: string;
|
|
104
|
-
trigger?: {
|
|
105
|
-
pubsub?: PubSubTriggerOpts;
|
|
106
|
-
[key: string]: unknown;
|
|
107
|
-
};
|
|
108
|
-
[key: string]: unknown;
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* PubSubTrigger - Abstract base class for pub/sub-based triggers
|
|
114
|
-
*/
|
|
115
|
-
export abstract class PubSubTrigger extends TriggerBase {
|
|
116
|
-
protected nodeMap: GlobalOptions = {} as GlobalOptions;
|
|
117
|
-
protected readonly tracer = trace.getTracer(
|
|
118
|
-
process.env.PROJECT_NAME || "trigger-pubsub-workflow",
|
|
119
|
-
process.env.PROJECT_VERSION || "0.0.1",
|
|
120
|
-
);
|
|
121
|
-
protected readonly logger = new DefaultLogger();
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* v0.7 PR 6 — back-compat default adapter. When subclasses set
|
|
125
|
-
* `protected adapter = new GCPPubSubAdapter()` (pre-v0.7 pattern),
|
|
126
|
-
* ALL workflows route through it regardless of their `provider`
|
|
127
|
-
* field. When unset, each workflow's `provider` is resolved via
|
|
128
|
-
* the factory.
|
|
129
|
-
*/
|
|
130
|
-
protected adapter?: PubSubAdapter;
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* v0.7 PR 6 — adapter pool, keyed by provider. Populated lazily in
|
|
134
|
-
* `listen()` as workflows are matched to providers. Drained in
|
|
135
|
-
* `stop()`. One adapter (one broker connection) per provider.
|
|
136
|
-
*/
|
|
137
|
-
protected adapterPool: Map<string, PubSubAdapter> = new Map();
|
|
138
|
-
|
|
139
|
-
// Subclasses provide these
|
|
140
|
-
protected abstract nodes: Record<string, BlokService<unknown>>;
|
|
141
|
-
protected abstract workflows: Record<string, HelperResponse>;
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Load nodes into the node map
|
|
145
|
-
*/
|
|
146
|
-
loadNodes(): void {
|
|
147
|
-
this.nodeMap.nodes = new NodeMap();
|
|
148
|
-
const nodeKeys = Object.keys(this.nodes);
|
|
149
|
-
for (const key of nodeKeys) {
|
|
150
|
-
this.nodeMap.nodes.addNode(key, this.nodes[key]);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Load workflows into the workflow map
|
|
156
|
-
*/
|
|
157
|
-
loadWorkflows(): void {
|
|
158
|
-
this.nodeMap.workflows = this.workflows;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Start the pub/sub subscriber - main entry point
|
|
163
|
-
*/
|
|
164
|
-
async listen(): Promise<number> {
|
|
165
|
-
const startTime = this.startCounter();
|
|
166
|
-
|
|
167
|
-
// Initialize nodes and workflows (called here because subclass properties
|
|
168
|
-
// aren't available in parent constructor)
|
|
169
|
-
this.loadNodes();
|
|
170
|
-
this.loadWorkflows();
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
// Find all workflows with pub/sub triggers
|
|
174
|
-
const pubsubWorkflows = this.getPubSubWorkflows();
|
|
175
|
-
|
|
176
|
-
if (pubsubWorkflows.length === 0) {
|
|
177
|
-
this.logger.log("No workflows with pub/sub triggers found");
|
|
178
|
-
return this.endCounter(startTime);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Subscribe to each topic via the adapter that owns its
|
|
182
|
-
// provider. Per-workflow `provider` field with subclass-
|
|
183
|
-
// adapter back-compat (handled in resolveAdapterForWorkflow).
|
|
184
|
-
for (const workflow of pubsubWorkflows) {
|
|
185
|
-
const config = workflow.config.trigger?.pubsub as PubSubTriggerOpts;
|
|
186
|
-
const adapter = await this.resolveAdapterForWorkflow(config);
|
|
187
|
-
this.logger.log(
|
|
188
|
-
`Subscribing to topic: ${config.topic} via ${adapter.provider} (subscription: ${config.subscription ?? "<auto>"}, group: ${config.consumerGroup ?? "<fan-out>"})`,
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
await adapter.subscribe(config, async (message) => {
|
|
192
|
-
await this.handleMessage(message, workflow, config);
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
this.logger.log(`Pub/Sub trigger started. Listening to ${pubsubWorkflows.length} subscription(s)`);
|
|
197
|
-
|
|
198
|
-
// Enable HMR in development mode
|
|
199
|
-
if (process.env.BLOK_HMR === "true" || process.env.NODE_ENV === "development") {
|
|
200
|
-
await this.enableHotReload();
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return this.endCounter(startTime);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
this.logger.error(`Failed to start pub/sub trigger: ${(error as Error).message}`);
|
|
206
|
-
throw error;
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Stop the pub/sub subscriber — drains every adapter in the pool
|
|
212
|
-
* plus the subclass-set adapter (if any).
|
|
213
|
-
*/
|
|
214
|
-
async stop(): Promise<void> {
|
|
215
|
-
for (const adapter of this.adapterPool.values()) {
|
|
216
|
-
try {
|
|
217
|
-
await adapter.disconnect();
|
|
218
|
-
} catch (err) {
|
|
219
|
-
this.logger.error(`[blok][pubsub] disconnect failed: ${(err as Error).message}`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
this.adapterPool.clear();
|
|
223
|
-
this.logger.log("Pub/Sub trigger stopped");
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* v0.7 PR 6 — pick the adapter for a workflow's `provider` field.
|
|
228
|
-
*
|
|
229
|
-
* Resolution order:
|
|
230
|
-
* 1. Subclass-set `this.adapter` (back-compat).
|
|
231
|
-
* 2. Per-workflow `provider` field via the factory.
|
|
232
|
-
* 3. `BLOK_PUBSUB_ADAPTER` env var.
|
|
233
|
-
* 4. `"nats"` fallback.
|
|
234
|
-
*
|
|
235
|
-
* Adapters are connected on first use and pooled per provider.
|
|
236
|
-
*/
|
|
237
|
-
protected async resolveAdapterForWorkflow(config: PubSubTriggerOpts): Promise<PubSubAdapter> {
|
|
238
|
-
if (this.adapter) {
|
|
239
|
-
if (!this.adapter.isConnected()) {
|
|
240
|
-
await this.adapter.connect();
|
|
241
|
-
this.logger.log(`Connected to ${this.adapter.provider} pub/sub system (subclass adapter)`);
|
|
242
|
-
}
|
|
243
|
-
this.adapterPool.set(this.adapter.provider, this.adapter);
|
|
244
|
-
return this.adapter;
|
|
245
|
-
}
|
|
246
|
-
const { resolveProvider, createPubSubAdapter } = await import("./adapters/factory");
|
|
247
|
-
const provider = resolveProvider(config.provider);
|
|
248
|
-
let adapter = this.adapterPool.get(provider);
|
|
249
|
-
if (!adapter) {
|
|
250
|
-
adapter = createPubSubAdapter(provider);
|
|
251
|
-
await adapter.connect();
|
|
252
|
-
this.logger.log(`Connected to ${adapter.provider} pub/sub system`);
|
|
253
|
-
this.adapterPool.set(provider, adapter);
|
|
254
|
-
}
|
|
255
|
-
return adapter;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
protected override async onHmrWorkflowChange(): Promise<void> {
|
|
259
|
-
this.logger.log("[HMR] Pub/Sub workflow changed, reloading...");
|
|
260
|
-
await this.waitForInFlightRequests();
|
|
261
|
-
await this.stop();
|
|
262
|
-
this.loadWorkflows();
|
|
263
|
-
await this.listen();
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Get all workflows that have pub/sub triggers
|
|
268
|
-
*/
|
|
269
|
-
protected getPubSubWorkflows(): PubSubWorkflowModel[] {
|
|
270
|
-
const workflows: PubSubWorkflowModel[] = [];
|
|
271
|
-
|
|
272
|
-
for (const [path, workflow] of Object.entries(this.nodeMap.workflows || {})) {
|
|
273
|
-
// HelperResponse has a protected _config property
|
|
274
|
-
const workflowConfig = (workflow as unknown as { _config: PubSubWorkflowModel["config"] })._config;
|
|
275
|
-
|
|
276
|
-
if (workflowConfig?.trigger) {
|
|
277
|
-
const triggerType = Object.keys(workflowConfig.trigger)[0];
|
|
278
|
-
|
|
279
|
-
if (triggerType === "pubsub" && workflowConfig.trigger.pubsub) {
|
|
280
|
-
workflows.push({
|
|
281
|
-
path,
|
|
282
|
-
config: workflowConfig,
|
|
283
|
-
});
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
return workflows;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
/**
|
|
292
|
-
* Handle an incoming message
|
|
293
|
-
*/
|
|
294
|
-
protected async handleMessage(
|
|
295
|
-
message: PubSubMessage,
|
|
296
|
-
workflow: PubSubWorkflowModel,
|
|
297
|
-
config: PubSubTriggerOpts,
|
|
298
|
-
): Promise<void> {
|
|
299
|
-
const id = message.id || uuid();
|
|
300
|
-
const defaultMeter = metrics.getMeter("default");
|
|
301
|
-
const pubsubMessages = defaultMeter.createCounter("pubsub_messages", {
|
|
302
|
-
description: "Pub/Sub messages processed",
|
|
303
|
-
});
|
|
304
|
-
const pubsubErrors = defaultMeter.createCounter("pubsub_errors", {
|
|
305
|
-
description: "Pub/Sub message processing errors",
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
await this.tracer.startActiveSpan(`pubsub:${config.topic}`, async (span: Span) => {
|
|
309
|
-
try {
|
|
310
|
-
const start = performance.now();
|
|
311
|
-
|
|
312
|
-
// Initialize configuration for this workflow
|
|
313
|
-
await this.configuration.init(workflow.path, this.nodeMap);
|
|
314
|
-
|
|
315
|
-
// Create context
|
|
316
|
-
const ctx: Context = this.createContext(undefined, workflow.path, id);
|
|
317
|
-
|
|
318
|
-
// Populate request with message data
|
|
319
|
-
ctx.request = {
|
|
320
|
-
body: message.body,
|
|
321
|
-
headers: message.attributes,
|
|
322
|
-
query: {},
|
|
323
|
-
params: {
|
|
324
|
-
topic: message.topic,
|
|
325
|
-
subscription: message.subscription || "",
|
|
326
|
-
messageId: message.id,
|
|
327
|
-
},
|
|
328
|
-
} as unknown as RequestContext;
|
|
329
|
-
|
|
330
|
-
// Store message metadata in context
|
|
331
|
-
if (!ctx.vars) ctx.vars = {};
|
|
332
|
-
ctx.vars._pubsub_message = {
|
|
333
|
-
topic: message.topic,
|
|
334
|
-
subscription: message.subscription || "",
|
|
335
|
-
publishTime: message.publishTime?.toISOString() ?? "",
|
|
336
|
-
attributes: JSON.stringify(message.attributes),
|
|
337
|
-
};
|
|
338
|
-
|
|
339
|
-
ctx.logger.log(`Processing message from ${config.topic}: ${id}`);
|
|
340
|
-
|
|
341
|
-
// Execute workflow
|
|
342
|
-
const response: TriggerResponse = await this.run(ctx);
|
|
343
|
-
const end = performance.now();
|
|
344
|
-
|
|
345
|
-
// Set span attributes
|
|
346
|
-
span.setAttribute("success", true);
|
|
347
|
-
span.setAttribute("message_id", id);
|
|
348
|
-
span.setAttribute("topic", config.topic);
|
|
349
|
-
span.setAttribute("subscription", config.subscription ?? "<auto>");
|
|
350
|
-
span.setAttribute("provider", config.provider ?? "<default>");
|
|
351
|
-
span.setAttribute("elapsed_ms", end - start);
|
|
352
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
353
|
-
|
|
354
|
-
// Record metrics
|
|
355
|
-
pubsubMessages.add(1, {
|
|
356
|
-
env: process.env.NODE_ENV,
|
|
357
|
-
topic: config.topic,
|
|
358
|
-
subscription: config.subscription ?? "<auto>",
|
|
359
|
-
provider: config.provider ?? "<default>",
|
|
360
|
-
workflow_name: this.configuration.name,
|
|
361
|
-
success: "true",
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
ctx.logger.log(`Message processed in ${(end - start).toFixed(2)}ms: ${id}`);
|
|
365
|
-
|
|
366
|
-
// Acknowledge message if configured
|
|
367
|
-
if (config.ack !== false) {
|
|
368
|
-
await message.ack();
|
|
369
|
-
ctx.logger.log(`Message acknowledged: ${id}`);
|
|
370
|
-
}
|
|
371
|
-
} catch (error) {
|
|
372
|
-
const errorMessage = (error as Error).message;
|
|
373
|
-
|
|
374
|
-
// Set span error
|
|
375
|
-
span.setAttribute("success", false);
|
|
376
|
-
span.recordException(error as Error);
|
|
377
|
-
span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });
|
|
378
|
-
|
|
379
|
-
// Record error metrics
|
|
380
|
-
pubsubErrors.add(1, {
|
|
381
|
-
env: process.env.NODE_ENV,
|
|
382
|
-
topic: config.topic,
|
|
383
|
-
subscription: config.subscription ?? "<auto>",
|
|
384
|
-
provider: config.provider ?? "<default>",
|
|
385
|
-
workflow_name: this.configuration?.name || "unknown",
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
this.logger.error(`Failed to process message ${id}: ${errorMessage}`, (error as Error).stack);
|
|
389
|
-
|
|
390
|
-
// Nack message
|
|
391
|
-
if (config.ack !== false) {
|
|
392
|
-
await message.nack();
|
|
393
|
-
this.logger.log(`Message nacked: ${id}`);
|
|
394
|
-
}
|
|
395
|
-
} finally {
|
|
396
|
-
span.end();
|
|
397
|
-
}
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
export default PubSubTrigger;
|