@blokjs/trigger-pubsub 0.2.2 → 0.6.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 (36) hide show
  1. package/__tests__/integration/gcp-pubsub.real-emulator.test.ts +235 -0
  2. package/__tests__/integration/kafka-pubsub.real-kafka.test.ts +269 -0
  3. package/__tests__/integration/nats-pubsub.real-nats.test.ts +138 -0
  4. package/dist/PubSubTrigger.d.ts +43 -4
  5. package/dist/PubSubTrigger.js +74 -21
  6. package/dist/adapters/AWSSNSAdapter.d.ts +16 -0
  7. package/dist/adapters/AWSSNSAdapter.js +52 -9
  8. package/dist/adapters/AzureServiceBusAdapter.d.ts +15 -0
  9. package/dist/adapters/AzureServiceBusAdapter.js +44 -11
  10. package/dist/adapters/GCPPubSubAdapter.d.ts +16 -0
  11. package/dist/adapters/GCPPubSubAdapter.js +42 -8
  12. package/dist/adapters/KafkaPubSubAdapter.d.ts +53 -0
  13. package/dist/adapters/KafkaPubSubAdapter.js +168 -0
  14. package/dist/adapters/NATSPubSubAdapter.d.ts +52 -0
  15. package/dist/adapters/NATSPubSubAdapter.js +260 -0
  16. package/dist/adapters/RedisStreamsPubSubAdapter.d.ts +49 -0
  17. package/dist/adapters/RedisStreamsPubSubAdapter.js +193 -0
  18. package/dist/adapters/factory.d.ts +22 -0
  19. package/dist/adapters/factory.js +80 -0
  20. package/dist/index.d.ts +36 -45
  21. package/dist/index.js +39 -46
  22. package/package.json +22 -10
  23. package/src/PubSubTrigger.ts +89 -24
  24. package/src/adapters/AWSSNSAdapter.ts +76 -12
  25. package/src/adapters/AzureServiceBusAdapter.ts +57 -14
  26. package/src/adapters/GCPPubSubAdapter.ts +50 -10
  27. package/src/adapters/KafkaPubSubAdapter.ts +194 -0
  28. package/src/adapters/NATSPubSubAdapter.ts +326 -0
  29. package/src/adapters/RedisStreamsPubSubAdapter.ts +225 -0
  30. package/src/adapters/factory.test.ts +87 -0
  31. package/src/adapters/factory.ts +88 -0
  32. package/src/adapters/new-adapters.test.ts +108 -0
  33. package/src/index.ts +40 -41
  34. package/template/package.json +6 -6
  35. package/template/src/runner/PubSubServer.ts +2 -2
  36. package/template/src/workflows/messages/on-message.ts +38 -34
@@ -0,0 +1,88 @@
1
+ /**
2
+ * v0.7 PR 6 — pub/sub adapter factory.
3
+ *
4
+ * Resolves a `provider` string to a concrete `PubSubAdapter` instance.
5
+ * Used by `PubSubTrigger` (per-workflow provider dispatch) and by the
6
+ * `@blokjs/pubsub-publish` helper.
7
+ *
8
+ * Provider resolution order:
9
+ * 1. Explicit `provider` field on the workflow.
10
+ * 2. `BLOK_PUBSUB_ADAPTER` env var.
11
+ * 3. `"nats"` fallback (cheapest infra; matches the v0.7 plan's
12
+ * "default for pub/sub" recommendation).
13
+ *
14
+ * Each adapter lazy-imports its broker SDK on first use; workflows
15
+ * that don't use a given provider don't pay the install cost.
16
+ */
17
+
18
+ import type { PubSubProvider } from "@blokjs/helper";
19
+ import type { PubSubAdapter } from "../PubSubTrigger";
20
+ import { AWSSNSAdapter } from "./AWSSNSAdapter";
21
+ import { AzureServiceBusAdapter } from "./AzureServiceBusAdapter";
22
+ import { GCPPubSubAdapter } from "./GCPPubSubAdapter";
23
+ import { KafkaPubSubAdapter } from "./KafkaPubSubAdapter";
24
+ import { NATSPubSubAdapter } from "./NATSPubSubAdapter";
25
+ import { RedisStreamsPubSubAdapter } from "./RedisStreamsPubSubAdapter";
26
+
27
+ export function resolveProvider(provider?: PubSubProvider): PubSubProvider {
28
+ if (provider) return provider;
29
+ const envValue = process.env.BLOK_PUBSUB_ADAPTER;
30
+ if (envValue && isPubSubProvider(envValue)) return envValue;
31
+ return "nats";
32
+ }
33
+
34
+ function isPubSubProvider(value: string): value is PubSubProvider {
35
+ return (
36
+ value === "nats" ||
37
+ value === "redis-streams" ||
38
+ value === "kafka" ||
39
+ value === "gcp" ||
40
+ value === "aws" ||
41
+ value === "azure"
42
+ );
43
+ }
44
+
45
+ export function createPubSubAdapter(provider: PubSubProvider): PubSubAdapter {
46
+ switch (provider) {
47
+ case "nats":
48
+ return new NATSPubSubAdapter();
49
+ case "redis-streams":
50
+ return new RedisStreamsPubSubAdapter();
51
+ case "kafka":
52
+ return new KafkaPubSubAdapter();
53
+ case "gcp":
54
+ return new GCPPubSubAdapter();
55
+ case "aws":
56
+ return new AWSSNSAdapter();
57
+ case "azure":
58
+ return new AzureServiceBusAdapter();
59
+ default: {
60
+ const exhaustive: never = provider;
61
+ throw new Error(`[blok][pubsub] unknown provider "${exhaustive as string}". Check PubSubProviderSchema.`);
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Process-singleton adapter pool — one instance per provider. The
68
+ * trigger calls `getOrCreateAdapter("nats")` once per workflow, and
69
+ * subsequent workflows on the same provider share the broker
70
+ * connection.
71
+ */
72
+ const pool: Map<PubSubProvider, PubSubAdapter> = new Map();
73
+
74
+ export function getOrCreateAdapter(provider: PubSubProvider): PubSubAdapter {
75
+ let adapter = pool.get(provider);
76
+ if (!adapter) {
77
+ adapter = createPubSubAdapter(provider);
78
+ pool.set(provider, adapter);
79
+ }
80
+ return adapter;
81
+ }
82
+
83
+ export function _resetAdapterPoolForTests(): void {
84
+ for (const adapter of pool.values()) {
85
+ void adapter.disconnect?.().catch(() => {});
86
+ }
87
+ pool.clear();
88
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Smoke tests for the v0.7 PR 6 pub/sub adapters (NATS, Redis
3
+ * Streams, Kafka) plus the v0.7 publish() backfill on the existing
4
+ * 3 (GCP, AWS, Azure). Boundary-only — constructor, provider name,
5
+ * initial state, `disconnect()` before connect.
6
+ *
7
+ * Live broker round-trips need docker-compose CI (see PR 6 plan,
8
+ * deferred to follow-up).
9
+ */
10
+
11
+ import { describe, expect, it } from "vitest";
12
+
13
+ import { AWSSNSAdapter } from "./AWSSNSAdapter";
14
+ import { AzureServiceBusAdapter } from "./AzureServiceBusAdapter";
15
+ import { GCPPubSubAdapter } from "./GCPPubSubAdapter";
16
+ import { KafkaPubSubAdapter } from "./KafkaPubSubAdapter";
17
+ import { NATSPubSubAdapter } from "./NATSPubSubAdapter";
18
+ import { RedisStreamsPubSubAdapter } from "./RedisStreamsPubSubAdapter";
19
+
20
+ describe("NATSPubSubAdapter — v0.7 PR 6", () => {
21
+ it("reports provider 'nats'", () => {
22
+ expect(new NATSPubSubAdapter().provider).toBe("nats");
23
+ });
24
+
25
+ it("is not connected before connect()", () => {
26
+ expect(new NATSPubSubAdapter().isConnected()).toBe(false);
27
+ });
28
+
29
+ it("disconnect() before connect is a no-op", async () => {
30
+ await expect(new NATSPubSubAdapter().disconnect()).resolves.toBeUndefined();
31
+ });
32
+
33
+ it("reads server list from NATS_SERVERS env var", () => {
34
+ process.env.NATS_SERVERS = "nats-a:4222,nats-b:4222";
35
+ const adapter = new NATSPubSubAdapter();
36
+ expect((adapter as unknown as { config: { servers: string[] } }).config.servers).toEqual([
37
+ "nats-a:4222",
38
+ "nats-b:4222",
39
+ ]);
40
+ process.env.NATS_SERVERS = undefined;
41
+ });
42
+ });
43
+
44
+ describe("RedisStreamsPubSubAdapter — v0.7 PR 6", () => {
45
+ it("reports provider 'redis-streams'", () => {
46
+ expect(new RedisStreamsPubSubAdapter().provider).toBe("redis-streams");
47
+ });
48
+
49
+ it("is not connected before connect()", () => {
50
+ expect(new RedisStreamsPubSubAdapter().isConnected()).toBe(false);
51
+ });
52
+
53
+ it("disconnect() before connect is a no-op", async () => {
54
+ await expect(new RedisStreamsPubSubAdapter().disconnect()).resolves.toBeUndefined();
55
+ });
56
+
57
+ it("generates a unique consumer name per instance (fan-out isolation)", () => {
58
+ const a = new RedisStreamsPubSubAdapter();
59
+ const b = new RedisStreamsPubSubAdapter();
60
+ expect((a as unknown as { consumerName: string }).consumerName).not.toBe(
61
+ (b as unknown as { consumerName: string }).consumerName,
62
+ );
63
+ });
64
+ });
65
+
66
+ describe("KafkaPubSubAdapter — v0.7 PR 6", () => {
67
+ it("reports provider 'kafka'", () => {
68
+ expect(new KafkaPubSubAdapter().provider).toBe("kafka");
69
+ });
70
+
71
+ it("is not connected before connect()", () => {
72
+ expect(new KafkaPubSubAdapter().isConnected()).toBe(false);
73
+ });
74
+
75
+ it("disconnect() before connect is a no-op", async () => {
76
+ await expect(new KafkaPubSubAdapter().disconnect()).resolves.toBeUndefined();
77
+ });
78
+
79
+ it("honors the explicit broker list override", () => {
80
+ const adapter = new KafkaPubSubAdapter({ brokers: ["kafka-prod:9092"] });
81
+ expect((adapter as unknown as { config: { brokers: string[] } }).config.brokers).toEqual(["kafka-prod:9092"]);
82
+ });
83
+ });
84
+
85
+ describe("Existing adapters — provider names + publish() surface (v0.7 PR 6 backfill)", () => {
86
+ it("GCPPubSubAdapter reports provider 'gcp'", () => {
87
+ expect(new GCPPubSubAdapter().provider).toBe("gcp");
88
+ });
89
+
90
+ it("AWSSNSAdapter reports provider 'aws'", () => {
91
+ expect(new AWSSNSAdapter().provider).toBe("aws");
92
+ });
93
+
94
+ it("AzureServiceBusAdapter reports provider 'azure'", () => {
95
+ const adapter = new AzureServiceBusAdapter({
96
+ connectionString: "Endpoint=sb://example.servicebus.windows.net/;...",
97
+ });
98
+ expect(adapter.provider).toBe("azure");
99
+ });
100
+
101
+ it("all three now expose a publish() method (added in PR 6)", () => {
102
+ expect(typeof new GCPPubSubAdapter().publish).toBe("function");
103
+ expect(typeof new AWSSNSAdapter().publish).toBe("function");
104
+ expect(
105
+ typeof new AzureServiceBusAdapter({ connectionString: "Endpoint=sb://x.servicebus.windows.net/;Y" }).publish,
106
+ ).toBe("function");
107
+ });
108
+ });
package/src/index.ts CHANGED
@@ -1,50 +1,37 @@
1
1
  /**
2
2
  * @blokjs/trigger-pubsub
3
3
  *
4
- * Pub/Sub-based trigger for Blok workflows.
5
- * Supports multiple pub/sub providers:
6
- * - Google Cloud Pub/Sub
7
- * - AWS SNS/SQS
8
- * - Azure Service Bus
4
+ * Pub/Sub-based trigger for Blok workflows. Supports 6 providers:
9
5
  *
10
- * @example GCP Pub/Sub
11
- * ```typescript
12
- * import { PubSubTrigger, GCPPubSubAdapter } from "@blokjs/trigger-pubsub";
6
+ * - **NATS** (Core + JetStream) — cheapest infra; subject wildcards.
7
+ * - **Redis Streams** — when Redis is already in stack.
8
+ * - **Kafka** — high-throughput streaming.
9
+ * - **GCP Pub/Sub** — Google Cloud-locked.
10
+ * - **AWS SNS+SQS** — SNS fan-out → SQS queueing.
11
+ * - **Azure Service Bus** — Azure Service Bus.
13
12
  *
14
- * class MyPubSubTrigger extends PubSubTrigger {
15
- * protected adapter = new GCPPubSubAdapter({
16
- * projectId: "my-project",
17
- * });
13
+ * v0.7+ pick the adapter per workflow via `trigger.pubsub.provider`.
14
+ * `BLOK_PUBSUB_ADAPTER` env var sets the default (falls back to NATS).
15
+ * Subclasses can still set `protected adapter` directly for back-
16
+ * compat with the pre-v0.7 single-adapter pattern.
18
17
  *
19
- * protected nodes = myNodes;
20
- * protected workflows = myWorkflows;
21
- * }
22
- *
23
- * const trigger = new MyPubSubTrigger();
24
- * await trigger.listen();
25
- * ```
26
- *
27
- * @example AWS SNS/SQS
28
- * ```typescript
29
- * import { PubSubTrigger, AWSSNSAdapter } from "@blokjs/trigger-pubsub";
30
- *
31
- * class MyPubSubTrigger extends PubSubTrigger {
32
- * protected adapter = new AWSSNSAdapter({
33
- * region: "us-east-1",
34
- * });
35
- * // ...
36
- * }
37
- * ```
38
- *
39
- * @example Azure Service Bus
40
- * ```typescript
41
- * import { PubSubTrigger, AzureServiceBusAdapter } from "@blokjs/trigger-pubsub";
18
+ * **Fan-out vs competing-consumer**: omit `consumerGroup` for fan-out
19
+ * (every subscriber sees every message); set it for competing-consumer
20
+ * (1 of N within group). One field disambiguates the two semantics.
42
21
  *
43
- * class MyPubSubTrigger extends PubSubTrigger {
44
- * protected adapter = new AzureServiceBusAdapter({
45
- * connectionString: process.env.AZURE_SERVICE_BUS_CONNECTION_STRING,
46
- * });
47
- * // ...
22
+ * @example v0.7 NATS subject hierarchy with JSON workflow
23
+ * ```json
24
+ * {
25
+ * "name": "audit-all-order-events",
26
+ * "trigger": {
27
+ * "pubsub": {
28
+ * "provider": "nats",
29
+ * "topic": "orders.>",
30
+ * "durable": true,
31
+ * "startFrom": "earliest"
32
+ * }
33
+ * },
34
+ * "steps": [...]
48
35
  * }
49
36
  * ```
50
37
  */
@@ -57,9 +44,21 @@ export {
57
44
  } from "./PubSubTrigger";
58
45
 
59
46
  // Adapters
60
- export { GCPPubSubAdapter, type GCPPubSubConfig } from "./adapters/GCPPubSubAdapter";
61
47
  export { AWSSNSAdapter, type AWSSNSConfig } from "./adapters/AWSSNSAdapter";
62
48
  export { AzureServiceBusAdapter, type AzureServiceBusConfig } from "./adapters/AzureServiceBusAdapter";
49
+ export { GCPPubSubAdapter, type GCPPubSubConfig } from "./adapters/GCPPubSubAdapter";
50
+ export { KafkaPubSubAdapter, type KafkaPubSubConfig } from "./adapters/KafkaPubSubAdapter";
51
+ export { NATSPubSubAdapter, type NATSPubSubConfig } from "./adapters/NATSPubSubAdapter";
52
+ export { RedisStreamsPubSubAdapter, type RedisStreamsPubSubConfig } from "./adapters/RedisStreamsPubSubAdapter";
53
+
54
+ // v0.7 PR 6 — factory + pool used by PubSubTrigger and exposed for
55
+ // helper nodes (`@blokjs/pubsub-publish`).
56
+ export {
57
+ _resetAdapterPoolForTests,
58
+ createPubSubAdapter,
59
+ getOrCreateAdapter,
60
+ resolveProvider,
61
+ } from "./adapters/factory";
63
62
 
64
63
  // Re-export types from helper for convenience
65
64
  export type {
@@ -25,12 +25,12 @@
25
25
  "vitest": "^4.0.18"
26
26
  },
27
27
  "dependencies": {
28
- "@blokjs/api-call": "^0.2.0",
29
- "@blokjs/helper": "^0.2.0",
30
- "@blokjs/if-else": "^0.2.0",
31
- "@blokjs/runner": "^0.2.0",
32
- "@blokjs/shared": "^0.2.0",
33
- "@blokjs/trigger-pubsub": "^0.2.0",
28
+ "@blokjs/api-call": "^0.6.1",
29
+ "@blokjs/helper": "^0.6.1",
30
+ "@blokjs/if-else": "^0.6.1",
31
+ "@blokjs/runner": "^0.6.1",
32
+ "@blokjs/shared": "^0.6.1",
33
+ "@blokjs/trigger-pubsub": "^0.6.1",
34
34
  "@opentelemetry/api": "^1.9.0",
35
35
  "@opentelemetry/exporter-prometheus": "^0.57.2",
36
36
  "@opentelemetry/resources": "^1.30.1",
@@ -34,6 +34,6 @@ export default class PubSubServer extends PubSubTrigger {
34
34
  projectId: process.env.GCP_PROJECT_ID || "my-project",
35
35
  });
36
36
 
37
- protected nodes = nodes;
38
- protected workflows = workflows;
37
+ protected nodes: Record<string, import("@blokjs/runner").BlokService<unknown>> = nodes;
38
+ protected workflows: Record<string, import("@blokjs/helper").HelperResponse> = workflows;
39
39
  }
@@ -1,44 +1,48 @@
1
- import { type Step, Workflow } from "@blokjs/helper";
1
+ import { workflow } from "@blokjs/helper";
2
2
 
3
3
  /**
4
- * Example Pub/Sub workflow - triggered when a message is received
4
+ * Example Pub/Sub workflow fires when a message arrives on a subscription.
5
5
  *
6
- * The message data is available in ctx.request:
7
- * - ctx.request.body: The message payload
8
- * - ctx.request.headers: Message attributes
9
- * - ctx.request.params.topic: The topic name
10
- * - ctx.request.params.subscription: The subscription name
11
- * - ctx.request.params.messageId: Unique message ID
6
+ * Message payload + metadata on ctx.request:
7
+ * - ctx.request.body the message payload
8
+ * - ctx.request.headers message attributes
9
+ * - ctx.request.params.topic topic name
10
+ * - ctx.request.params.subscription subscription name
11
+ * - ctx.request.params.messageId unique message ID
12
+ * - ctx.vars._pubsub_message — full broker metadata
12
13
  *
13
- * Additional metadata is available in ctx.vars._pubsub_message:
14
- * - topic: Topic name
15
- * - subscription: Subscription name
16
- * - publishTime: When the message was published (ISO string)
17
- * - attributes: JSON string of message attributes
14
+ * Pick a provider in the trigger config:
15
+ * provider: "gcp" | "aws" | "azure"
16
+ *
17
+ * v2 reliability knobs available on each step (uncomment to use):
18
+ * idempotencyKey: "$.req.params.messageId" at-most-once delivery semantics
19
+ * retry: { maxAttempts: 3 } — retry on transient failures
18
20
  */
19
- const step: Step = Workflow({
21
+ export default workflow({
20
22
  name: "On Pub/Sub Message",
21
23
  version: "1.0.0",
22
24
  description: "Handles incoming Pub/Sub messages",
23
- })
24
- .addTrigger("pubsub", {
25
- provider: "gcp",
26
- topic: "my-topic",
27
- subscription: "my-subscription",
28
- })
29
- .addStep({
30
- name: "log-message",
31
- node: "@blokjs/api-call",
32
- type: "module",
33
- inputs: {
34
- url: "https://httpbin.org/post",
35
- method: "POST",
36
- body: {
37
- message: "js/ctx.request.body",
38
- topic: "js/ctx.request.params.topic",
39
- messageId: "js/ctx.request.params.messageId",
25
+ trigger: {
26
+ pubsub: {
27
+ provider: "gcp",
28
+ topic: "my-topic",
29
+ subscription: "my-subscription",
30
+ },
31
+ },
32
+ steps: [
33
+ {
34
+ id: "log-message",
35
+ use: "@blokjs/api-call",
36
+ type: "module",
37
+ inputs: {
38
+ url: "https://httpbin.org/post",
39
+ method: "POST",
40
+ body: {
41
+ message: "js/ctx.request.body",
42
+ topic: "js/ctx.request.params.topic",
43
+ messageId: "js/ctx.request.params.messageId",
44
+ },
40
45
  },
41
46
  },
42
- });
43
-
44
- export default step;
47
+ ],
48
+ });