@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.
@@ -1,236 +0,0 @@
1
- /**
2
- * GCPPubSubAdapter - Google Cloud Pub/Sub adapter for PubSubTrigger
3
- *
4
- * Uses @google-cloud/pubsub for GCP Pub/Sub connectivity.
5
- * Requires: npm install @google-cloud/pubsub
6
- *
7
- * Environment variables:
8
- * - GOOGLE_CLOUD_PROJECT: GCP project ID
9
- * - GOOGLE_APPLICATION_CREDENTIALS: Path to service account key file (optional if using default credentials)
10
- * - PUBSUB_EMULATOR_HOST: Pub/Sub emulator host for local development (optional)
11
- */
12
-
13
- import type { PubSubTriggerOpts } from "@blokjs/helper";
14
- import type { Message, PubSub, Subscription } from "@google-cloud/pubsub";
15
- import { v4 as uuid } from "uuid";
16
- import type { PubSubAdapter, PubSubMessage } from "../PubSubTrigger";
17
-
18
- /**
19
- * GCP Pub/Sub configuration
20
- */
21
- export interface GCPPubSubConfig {
22
- projectId?: string;
23
- credentials?: {
24
- client_email: string;
25
- private_key: string;
26
- };
27
- }
28
-
29
- /**
30
- * GCPPubSubAdapter - Google Cloud Pub/Sub implementation
31
- */
32
- export class GCPPubSubAdapter implements PubSubAdapter {
33
- readonly provider = "gcp" as const;
34
-
35
- private client: PubSub | undefined;
36
- private subscriptions: Map<string, Subscription> = new Map();
37
- private connected = false;
38
- private config: GCPPubSubConfig;
39
-
40
- /**
41
- * Type-narrowing accessor for `this.client`. Field is undefined until
42
- * `connect()` runs.
43
- */
44
- private requireClient(): PubSub {
45
- if (!this.client) {
46
- throw new Error("[GCPPubSubAdapter] client is not initialised — call connect() first");
47
- }
48
- return this.client;
49
- }
50
-
51
- constructor(config?: GCPPubSubConfig) {
52
- this.config = {
53
- projectId: config?.projectId || process.env.GOOGLE_CLOUD_PROJECT,
54
- credentials: config?.credentials,
55
- };
56
- }
57
-
58
- /**
59
- * Connect to GCP Pub/Sub
60
- */
61
- async connect(): Promise<void> {
62
- if (this.connected) return;
63
-
64
- try {
65
- // Dynamic import of @google-cloud/pubsub
66
- const { PubSub } = await import("@google-cloud/pubsub");
67
-
68
- this.client = new PubSub({
69
- projectId: this.config.projectId,
70
- credentials: this.config.credentials,
71
- });
72
-
73
- this.connected = true;
74
- console.log(`[GCPPubSubAdapter] Connected to GCP Pub/Sub: ${this.config.projectId}`);
75
- } catch (error) {
76
- throw new Error(
77
- `Failed to connect to GCP Pub/Sub: ${(error as Error).message}. Make sure @google-cloud/pubsub is installed: npm install @google-cloud/pubsub`,
78
- );
79
- }
80
- }
81
-
82
- /**
83
- * Disconnect from GCP Pub/Sub
84
- */
85
- async disconnect(): Promise<void> {
86
- if (!this.connected) return;
87
-
88
- try {
89
- // Close all subscriptions
90
- for (const [name, subscription] of this.subscriptions) {
91
- await subscription.close();
92
- }
93
- this.subscriptions.clear();
94
-
95
- await this.requireClient().close();
96
- this.connected = false;
97
- console.log("[GCPPubSubAdapter] Disconnected from GCP Pub/Sub");
98
- } catch (error) {
99
- console.error(`[GCPPubSubAdapter] Error disconnecting: ${(error as Error).message}`);
100
- }
101
- }
102
-
103
- /**
104
- * Subscribe to a GCP Pub/Sub topic
105
- */
106
- async subscribe(config: PubSubTriggerOpts, handler: (message: PubSubMessage) => Promise<void>): Promise<void> {
107
- if (!this.connected) {
108
- throw new Error("Not connected to GCP Pub/Sub. Call connect() first.");
109
- }
110
-
111
- if (!config.subscription) {
112
- throw new Error("[GCPPubSubAdapter] `subscription` is required — must be the GCP subscription name.");
113
- }
114
- const subscriptionName = config.subscription;
115
-
116
- // Get the subscription. The GCP SDK calls the per-subscription
117
- // deadline `maxAckDeadline` (a `Duration`); our author-facing
118
- // field is `ackDeadline` (seconds) for parity with other
119
- // providers. `Duration` is the GCP SDK's tc39-Temporal-shaped
120
- // shim — construct via `Duration.from({seconds})`.
121
- const { Duration } = await import("@google-cloud/pubsub");
122
- const subscription = this.requireClient().subscription(subscriptionName, {
123
- flowControl: {
124
- maxMessages: config.maxMessages || 10,
125
- },
126
- maxAckDeadline: Duration.from({ seconds: config.ackDeadline || 30 }),
127
- });
128
-
129
- // Message handler
130
- const messageHandler = async (gcpMessage: Message) => {
131
- // Parse message data
132
- let body: unknown;
133
- try {
134
- const data = gcpMessage.data.toString();
135
- body = JSON.parse(data);
136
- } catch {
137
- body = gcpMessage.data.toString();
138
- }
139
-
140
- // Create pub/sub message
141
- const pubsubMessage: PubSubMessage = {
142
- id: gcpMessage.id || uuid(),
143
- body,
144
- attributes: gcpMessage.attributes || {},
145
- raw: gcpMessage,
146
- topic: config.topic,
147
- subscription: subscriptionName,
148
- publishTime: gcpMessage.publishTime ? new Date(gcpMessage.publishTime) : new Date(),
149
- ack: async () => {
150
- gcpMessage.ack();
151
- },
152
- nack: async () => {
153
- gcpMessage.nack();
154
- },
155
- };
156
-
157
- // Process message
158
- try {
159
- await handler(pubsubMessage);
160
- } catch (error) {
161
- console.error(`[GCPPubSubAdapter] Error processing message: ${(error as Error).message}`);
162
- }
163
- };
164
-
165
- // Error handler
166
- const errorHandler = (error: Error) => {
167
- console.error(`[GCPPubSubAdapter] Subscription error: ${error.message}`);
168
- };
169
-
170
- // Attach listeners
171
- subscription.on("message", messageHandler);
172
- subscription.on("error", errorHandler);
173
-
174
- // Store subscription reference
175
- this.subscriptions.set(subscriptionName, subscription);
176
-
177
- console.log(`[GCPPubSubAdapter] Subscribed to: ${subscriptionName}`);
178
- }
179
-
180
- /**
181
- * Unsubscribe from a GCP Pub/Sub subscription
182
- */
183
- async unsubscribe(subscriptionName: string): Promise<void> {
184
- const subscription = this.subscriptions.get(subscriptionName);
185
- if (subscription) {
186
- await subscription.close();
187
- this.subscriptions.delete(subscriptionName);
188
- console.log(`[GCPPubSubAdapter] Unsubscribed from: ${subscriptionName}`);
189
- }
190
- }
191
-
192
- /**
193
- * Check if connected to GCP Pub/Sub
194
- */
195
- isConnected(): boolean {
196
- return this.connected;
197
- }
198
-
199
- /**
200
- * v0.7 PR 6 — publish to a GCP Pub/Sub topic.
201
- *
202
- * `partitionKey` / `orderingKey` map to GCP's `orderingKey` (the
203
- * topic must have message ordering enabled — otherwise the field
204
- * is ignored by the broker).
205
- */
206
- async publish(
207
- topic: string,
208
- payload: unknown,
209
- opts?: { partitionKey?: string; orderingKey?: string },
210
- ): Promise<void> {
211
- if (!this.connected) throw new Error("[blok][pubsub-gcp] not connected. Call connect() first.");
212
- const body = typeof payload === "string" ? payload : JSON.stringify(payload);
213
- const t = this.requireClient().topic(topic);
214
- await t.publishMessage({
215
- data: Buffer.from(body),
216
- orderingKey: opts?.orderingKey ?? opts?.partitionKey,
217
- });
218
- }
219
-
220
- /**
221
- * Health check - verify GCP Pub/Sub connectivity
222
- */
223
- async healthCheck(): Promise<boolean> {
224
- if (!this.connected) return false;
225
-
226
- try {
227
- // List subscriptions as a health check
228
- const [subscriptions] = await this.requireClient().getSubscriptions({ pageSize: 1 });
229
- return true;
230
- } catch {
231
- return false;
232
- }
233
- }
234
- }
235
-
236
- export default GCPPubSubAdapter;
@@ -1,194 +0,0 @@
1
- /**
2
- * KafkaPubSubAdapter — v0.7 PR 6 — Pub/Sub adapter backed by Apache
3
- * Kafka via `kafkajs`.
4
- *
5
- * Pub/Sub vs Worker semantics: this adapter uses **per-subscriber
6
- * consumer groups** so multiple subscribers each receive every message
7
- * (fan-out). When `consumerGroup` is explicitly set, all subscribers
8
- * share that group and compete (1 of N gets each message).
9
- *
10
- * Replay cursors via `startFrom`:
11
- * - `"earliest"` → `fromBeginning: true` on first subscribe.
12
- * - `"latest"` (default) → only new messages.
13
- * - `{seq: N}` / `{timestamp: ms}` — provider-specific. Cleanest
14
- * long-term path is Kafka's `admin.seek({offset|timestamp})` post-
15
- * subscribe; v1 honors `earliest` / `latest` and `{seq}` via
16
- * `auto.offset.reset` only.
17
- *
18
- * Requires `kafkajs` as a peer dependency.
19
- *
20
- * Environment variables:
21
- * - `KAFKA_BROKERS` — comma-separated (default `localhost:9092`).
22
- * - `KAFKA_CLIENT_ID` — default `"blok-pubsub"`.
23
- * - `KAFKA_SASL_USERNAME` / `KAFKA_SASL_PASSWORD` — SASL/PLAIN.
24
- * - `KAFKA_SSL` — `"true"` enables TLS.
25
- */
26
-
27
- import type { PubSubTriggerOpts } from "@blokjs/helper";
28
- import { v4 as uuid } from "uuid";
29
- import type { PubSubAdapter, PubSubMessage } from "../PubSubTrigger";
30
-
31
- export interface KafkaPubSubConfig {
32
- brokers: string[];
33
- clientId: string;
34
- saslUsername?: string;
35
- saslPassword?: string;
36
- ssl: boolean;
37
- }
38
-
39
- interface KafkaConsumerHandle {
40
- disconnect: () => Promise<void>;
41
- stop: () => Promise<void>;
42
- }
43
-
44
- export class KafkaPubSubAdapter implements PubSubAdapter {
45
- readonly provider = "kafka" as const;
46
- private readonly config: KafkaPubSubConfig;
47
- // biome-ignore lint/suspicious/noExplicitAny: kafkajs's exported `Kafka` constructor is loosely typed.
48
- private kafka: any = null;
49
- // biome-ignore lint/suspicious/noExplicitAny: producer is created lazily and typed loosely.
50
- private producer: any = null;
51
- private consumers: Map<string, KafkaConsumerHandle> = new Map();
52
- private connected = false;
53
-
54
- constructor(config?: Partial<KafkaPubSubConfig>) {
55
- this.config = {
56
- brokers: config?.brokers ?? (process.env.KAFKA_BROKERS ?? "localhost:9092").split(",").map((s) => s.trim()),
57
- clientId: config?.clientId ?? process.env.KAFKA_CLIENT_ID ?? "blok-pubsub",
58
- saslUsername: config?.saslUsername ?? process.env.KAFKA_SASL_USERNAME,
59
- saslPassword: config?.saslPassword ?? process.env.KAFKA_SASL_PASSWORD,
60
- ssl: config?.ssl ?? process.env.KAFKA_SSL === "true",
61
- };
62
- }
63
-
64
- async connect(): Promise<void> {
65
- if (this.connected) return;
66
- try {
67
- // biome-ignore lint/suspicious/noExplicitAny: kafkajs is a runtime peer dep.
68
- const kafkajs: any = await import("kafkajs");
69
- const sasl =
70
- this.config.saslUsername && this.config.saslPassword
71
- ? { mechanism: "plain", username: this.config.saslUsername, password: this.config.saslPassword }
72
- : undefined;
73
- this.kafka = new kafkajs.Kafka({
74
- clientId: this.config.clientId,
75
- brokers: this.config.brokers,
76
- ssl: this.config.ssl,
77
- sasl,
78
- });
79
- this.producer = this.kafka.producer();
80
- await this.producer.connect();
81
- this.connected = true;
82
- } catch (err) {
83
- throw new Error(
84
- `[blok][pubsub-kafka] connect failed: ${(err as Error).message}. Install kafkajs as a peer dependency: bun add kafkajs`,
85
- );
86
- }
87
- }
88
-
89
- async disconnect(): Promise<void> {
90
- if (!this.connected) return;
91
- for (const consumer of this.consumers.values()) {
92
- try {
93
- await consumer.disconnect();
94
- } catch {
95
- /* ignore */
96
- }
97
- }
98
- this.consumers.clear();
99
- try {
100
- await this.producer?.disconnect();
101
- } catch {
102
- /* ignore */
103
- }
104
- this.producer = null;
105
- this.connected = false;
106
- }
107
-
108
- async subscribe(config: PubSubTriggerOpts, handler: (message: PubSubMessage) => Promise<void>): Promise<void> {
109
- if (!this.connected) throw new Error("[blok][pubsub-kafka] not connected. Call connect() first.");
110
- // Fan-out: distinct group id per subscriber instance. Competing-
111
- // consumer: explicit consumerGroup shared across all subscribers.
112
- const groupId =
113
- config.consumerGroup ?? `blok-fanout-${uuid().slice(0, 8)}-${config.topic.replace(/[^a-zA-Z0-9_]/g, "_")}`;
114
- const consumer = this.kafka.consumer({ groupId });
115
- await consumer.connect();
116
- const fromBeginning = config.startFrom === "earliest";
117
- await consumer.subscribe({ topic: config.topic, fromBeginning });
118
- this.consumers.set(`${config.topic}#${groupId}`, consumer);
119
-
120
- await consumer.run({
121
- autoCommit: config.ack !== false,
122
- eachMessage: async ({
123
- message,
124
- }: {
125
- message: { key?: Buffer; value?: Buffer; offset: string; timestamp: string; headers?: Record<string, Buffer> };
126
- }) => {
127
- const text = message.value?.toString("utf8") ?? "";
128
- let body: unknown = text;
129
- try {
130
- body = text.length > 0 ? JSON.parse(text) : null;
131
- } catch {
132
- /* leave as text */
133
- }
134
- const attributes: Record<string, string> = {};
135
- if (message.headers) {
136
- for (const [k, v] of Object.entries(message.headers)) attributes[k] = v?.toString("utf8") ?? "";
137
- }
138
- const msg: PubSubMessage = {
139
- id: `${config.topic}:${message.offset}`,
140
- body,
141
- attributes,
142
- raw: message,
143
- topic: config.topic,
144
- subscription: groupId,
145
- publishTime: new Date(Number.parseInt(message.timestamp, 10)),
146
- ack: async () => {
147
- /* autoCommit handles ack */
148
- },
149
- nack: async () => {
150
- /* kafkajs has no per-message nack; the offset commit is suppressed by throwing */
151
- },
152
- };
153
- // On handler failure: re-throw so kafkajs suppresses the
154
- // offset commit; the consumer will re-poll the message
155
- // on the next cycle. The throw is intentional.
156
- await handler(msg);
157
- },
158
- });
159
- }
160
-
161
- async unsubscribe(subscription: string): Promise<void> {
162
- const consumer = this.consumers.get(subscription);
163
- if (!consumer) return;
164
- try {
165
- await consumer.stop();
166
- await consumer.disconnect();
167
- } catch {
168
- /* ignore */
169
- }
170
- this.consumers.delete(subscription);
171
- }
172
-
173
- async publish(
174
- topic: string,
175
- payload: unknown,
176
- opts?: { partitionKey?: string; orderingKey?: string },
177
- ): Promise<void> {
178
- if (!this.connected || !this.producer) throw new Error("[blok][pubsub-kafka] not connected. Call connect() first.");
179
- const body = typeof payload === "string" ? payload : JSON.stringify(payload);
180
- const key = opts?.partitionKey ?? opts?.orderingKey;
181
- await this.producer.send({
182
- topic,
183
- messages: [{ key, value: body }],
184
- });
185
- }
186
-
187
- isConnected(): boolean {
188
- return this.connected;
189
- }
190
-
191
- async healthCheck(): Promise<boolean> {
192
- return this.connected;
193
- }
194
- }