@blokjs/trigger-pubsub 0.2.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/CHANGELOG.md +22 -0
- package/dist/PubSubTrigger.d.ts +116 -0
- package/dist/PubSubTrigger.js +219 -0
- package/dist/adapters/AWSSNSAdapter.d.ts +67 -0
- package/dist/adapters/AWSSNSAdapter.js +257 -0
- package/dist/adapters/AzureServiceBusAdapter.d.ts +55 -0
- package/dist/adapters/AzureServiceBusAdapter.js +229 -0
- package/dist/adapters/GCPPubSubAdapter.d.ts +59 -0
- package/dist/adapters/GCPPubSubAdapter.js +201 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.js +64 -0
- package/package.json +53 -0
- package/src/PubSubTrigger.test.ts +151 -0
- package/src/PubSubTrigger.ts +337 -0
- package/src/adapters/AWSSNSAdapter.ts +258 -0
- package/src/adapters/AzureServiceBusAdapter.ts +220 -0
- package/src/adapters/GCPPubSubAdapter.ts +196 -0
- package/src/index.ts +68 -0
- package/template/.env.example +8 -0
- package/template/package.json +44 -0
- package/template/src/Nodes.ts +10 -0
- package/template/src/Workflows.ts +8 -0
- package/template/src/index.ts +41 -0
- package/template/src/runner/PubSubServer.ts +39 -0
- package/template/src/runner/types/Workflows.ts +7 -0
- package/template/src/workflows/messages/on-message.ts +44 -0
- package/template/tsconfig.json +31 -0
- package/template/vitest.config.ts +39 -0
- package/tsconfig.json +32 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AzureServiceBusAdapter - Azure Service Bus adapter for PubSubTrigger
|
|
3
|
+
*
|
|
4
|
+
* Uses @azure/service-bus for Azure Service Bus connectivity.
|
|
5
|
+
* Requires: npm install @azure/service-bus
|
|
6
|
+
*
|
|
7
|
+
* Environment variables:
|
|
8
|
+
* - AZURE_SERVICE_BUS_CONNECTION_STRING: Azure Service Bus connection string
|
|
9
|
+
* - AZURE_SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE: Fully qualified namespace (if using DefaultAzureCredential)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { PubSubTriggerOpts } from "@blokjs/helper";
|
|
13
|
+
import { v4 as uuid } from "uuid";
|
|
14
|
+
import type { PubSubAdapter, PubSubMessage } from "../PubSubTrigger";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Azure Service Bus configuration
|
|
18
|
+
*/
|
|
19
|
+
export interface AzureServiceBusConfig {
|
|
20
|
+
connectionString?: string;
|
|
21
|
+
fullyQualifiedNamespace?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* AzureServiceBusAdapter - Azure Service Bus implementation
|
|
26
|
+
*/
|
|
27
|
+
export class AzureServiceBusAdapter implements PubSubAdapter {
|
|
28
|
+
readonly provider = "azure" as const;
|
|
29
|
+
|
|
30
|
+
private client: any;
|
|
31
|
+
private receivers: Map<string, any> = new Map();
|
|
32
|
+
private connected = false;
|
|
33
|
+
private config: AzureServiceBusConfig;
|
|
34
|
+
|
|
35
|
+
constructor(config?: AzureServiceBusConfig) {
|
|
36
|
+
this.config = {
|
|
37
|
+
connectionString: config?.connectionString || process.env.AZURE_SERVICE_BUS_CONNECTION_STRING,
|
|
38
|
+
fullyQualifiedNamespace:
|
|
39
|
+
config?.fullyQualifiedNamespace || process.env.AZURE_SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Connect to Azure Service Bus
|
|
45
|
+
*/
|
|
46
|
+
async connect(): Promise<void> {
|
|
47
|
+
if (this.connected) return;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Dynamic import of @azure/service-bus
|
|
51
|
+
const { ServiceBusClient } = await import("@azure/service-bus");
|
|
52
|
+
|
|
53
|
+
if (this.config.connectionString) {
|
|
54
|
+
this.client = new ServiceBusClient(this.config.connectionString);
|
|
55
|
+
} else if (this.config.fullyQualifiedNamespace) {
|
|
56
|
+
// Would need @azure/identity for DefaultAzureCredential
|
|
57
|
+
throw new Error("Managed identity authentication requires @azure/identity package");
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error("Either connectionString or fullyQualifiedNamespace is required");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.connected = true;
|
|
63
|
+
console.log("[AzureServiceBusAdapter] Connected to Azure Service Bus");
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Failed to connect to Azure Service Bus: ${(error as Error).message}. ` +
|
|
67
|
+
`Make sure @azure/service-bus is installed: npm install @azure/service-bus`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Disconnect from Azure Service Bus
|
|
74
|
+
*/
|
|
75
|
+
async disconnect(): Promise<void> {
|
|
76
|
+
if (!this.connected) return;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Close all receivers
|
|
80
|
+
for (const [name, receiver] of this.receivers) {
|
|
81
|
+
await receiver.close();
|
|
82
|
+
}
|
|
83
|
+
this.receivers.clear();
|
|
84
|
+
|
|
85
|
+
await this.client.close();
|
|
86
|
+
this.connected = false;
|
|
87
|
+
console.log("[AzureServiceBusAdapter] Disconnected from Azure Service Bus");
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`[AzureServiceBusAdapter] Error disconnecting: ${(error as Error).message}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Subscribe to an Azure Service Bus topic/subscription or queue
|
|
95
|
+
*/
|
|
96
|
+
async subscribe(config: PubSubTriggerOpts, handler: (message: PubSubMessage) => Promise<void>): Promise<void> {
|
|
97
|
+
if (!this.connected) {
|
|
98
|
+
throw new Error("Not connected to Azure Service Bus. Call connect() first.");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let receiver: any;
|
|
102
|
+
|
|
103
|
+
// Determine if this is a topic subscription or a queue
|
|
104
|
+
if (config.subscription && config.topic) {
|
|
105
|
+
// Topic with subscription
|
|
106
|
+
receiver = this.client.createReceiver(config.topic, config.subscription, {
|
|
107
|
+
receiveMode: config.ack !== false ? "peekLock" : "receiveAndDelete",
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
// Queue
|
|
111
|
+
receiver = this.client.createReceiver(config.subscription || config.topic, {
|
|
112
|
+
receiveMode: config.ack !== false ? "peekLock" : "receiveAndDelete",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const subscriptionKey = `${config.topic}/${config.subscription}`;
|
|
117
|
+
|
|
118
|
+
// Message handler
|
|
119
|
+
const processMessage = async (sbMessage: any) => {
|
|
120
|
+
// Parse message body
|
|
121
|
+
let body: unknown;
|
|
122
|
+
try {
|
|
123
|
+
if (typeof sbMessage.body === "string") {
|
|
124
|
+
body = JSON.parse(sbMessage.body);
|
|
125
|
+
} else {
|
|
126
|
+
body = sbMessage.body;
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
body = sbMessage.body;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Extract application properties as attributes
|
|
133
|
+
const attributes: Record<string, string> = {};
|
|
134
|
+
if (sbMessage.applicationProperties) {
|
|
135
|
+
for (const [key, value] of Object.entries(sbMessage.applicationProperties)) {
|
|
136
|
+
attributes[key] = String(value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create pub/sub message
|
|
141
|
+
const pubsubMessage: PubSubMessage = {
|
|
142
|
+
id: sbMessage.messageId || uuid(),
|
|
143
|
+
body,
|
|
144
|
+
attributes,
|
|
145
|
+
raw: sbMessage,
|
|
146
|
+
topic: config.topic,
|
|
147
|
+
subscription: config.subscription,
|
|
148
|
+
publishTime: sbMessage.enqueuedTimeUtc ? new Date(sbMessage.enqueuedTimeUtc) : new Date(),
|
|
149
|
+
ack: async () => {
|
|
150
|
+
await receiver.completeMessage(sbMessage);
|
|
151
|
+
},
|
|
152
|
+
nack: async () => {
|
|
153
|
+
await receiver.abandonMessage(sbMessage);
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Process message
|
|
158
|
+
try {
|
|
159
|
+
await handler(pubsubMessage);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.error(`[AzureServiceBusAdapter] Error processing message: ${(error as Error).message}`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// Error handler
|
|
166
|
+
const processError = async (error: Error) => {
|
|
167
|
+
console.error(`[AzureServiceBusAdapter] Error: ${error.message}`);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// Subscribe to messages
|
|
171
|
+
receiver.subscribe({
|
|
172
|
+
processMessage,
|
|
173
|
+
processError,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
this.receivers.set(subscriptionKey, receiver);
|
|
177
|
+
|
|
178
|
+
console.log(`[AzureServiceBusAdapter] Subscribed to: ${subscriptionKey}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Unsubscribe from Azure Service Bus
|
|
183
|
+
*/
|
|
184
|
+
async unsubscribe(subscriptionKey: string): Promise<void> {
|
|
185
|
+
const receiver = this.receivers.get(subscriptionKey);
|
|
186
|
+
if (receiver) {
|
|
187
|
+
await receiver.close();
|
|
188
|
+
this.receivers.delete(subscriptionKey);
|
|
189
|
+
console.log(`[AzureServiceBusAdapter] Unsubscribed from: ${subscriptionKey}`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if connected to Azure Service Bus
|
|
195
|
+
*/
|
|
196
|
+
isConnected(): boolean {
|
|
197
|
+
return this.connected;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Health check - verify Azure Service Bus connectivity
|
|
202
|
+
*/
|
|
203
|
+
async healthCheck(): Promise<boolean> {
|
|
204
|
+
if (!this.connected) return false;
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// Create a temporary receiver to test connectivity
|
|
208
|
+
const testReceiver = this.client.createReceiver("$default", {
|
|
209
|
+
receiveMode: "peekLock",
|
|
210
|
+
});
|
|
211
|
+
await testReceiver.close();
|
|
212
|
+
return true;
|
|
213
|
+
} catch {
|
|
214
|
+
// Queue might not exist but connection is healthy if we got here
|
|
215
|
+
return this.connected;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default AzureServiceBusAdapter;
|
|
@@ -0,0 +1,196 @@
|
|
|
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 { v4 as uuid } from "uuid";
|
|
15
|
+
import type { PubSubAdapter, PubSubMessage } from "../PubSubTrigger";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* GCP Pub/Sub configuration
|
|
19
|
+
*/
|
|
20
|
+
export interface GCPPubSubConfig {
|
|
21
|
+
projectId?: string;
|
|
22
|
+
credentials?: {
|
|
23
|
+
client_email: string;
|
|
24
|
+
private_key: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GCPPubSubAdapter - Google Cloud Pub/Sub implementation
|
|
30
|
+
*/
|
|
31
|
+
export class GCPPubSubAdapter implements PubSubAdapter {
|
|
32
|
+
readonly provider = "gcp" as const;
|
|
33
|
+
|
|
34
|
+
private client: any;
|
|
35
|
+
private subscriptions: Map<string, any> = new Map();
|
|
36
|
+
private connected = false;
|
|
37
|
+
private config: GCPPubSubConfig;
|
|
38
|
+
|
|
39
|
+
constructor(config?: GCPPubSubConfig) {
|
|
40
|
+
this.config = {
|
|
41
|
+
projectId: config?.projectId || process.env.GOOGLE_CLOUD_PROJECT,
|
|
42
|
+
credentials: config?.credentials,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Connect to GCP Pub/Sub
|
|
48
|
+
*/
|
|
49
|
+
async connect(): Promise<void> {
|
|
50
|
+
if (this.connected) return;
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
// Dynamic import of @google-cloud/pubsub
|
|
54
|
+
const { PubSub } = await import("@google-cloud/pubsub");
|
|
55
|
+
|
|
56
|
+
this.client = new PubSub({
|
|
57
|
+
projectId: this.config.projectId,
|
|
58
|
+
credentials: this.config.credentials,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
this.connected = true;
|
|
62
|
+
console.log(`[GCPPubSubAdapter] Connected to GCP Pub/Sub: ${this.config.projectId}`);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Failed to connect to GCP Pub/Sub: ${(error as Error).message}. ` +
|
|
66
|
+
`Make sure @google-cloud/pubsub is installed: npm install @google-cloud/pubsub`,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Disconnect from GCP Pub/Sub
|
|
73
|
+
*/
|
|
74
|
+
async disconnect(): Promise<void> {
|
|
75
|
+
if (!this.connected) return;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Close all subscriptions
|
|
79
|
+
for (const [name, subscription] of this.subscriptions) {
|
|
80
|
+
await subscription.close();
|
|
81
|
+
}
|
|
82
|
+
this.subscriptions.clear();
|
|
83
|
+
|
|
84
|
+
await this.client.close();
|
|
85
|
+
this.connected = false;
|
|
86
|
+
console.log("[GCPPubSubAdapter] Disconnected from GCP Pub/Sub");
|
|
87
|
+
} catch (error) {
|
|
88
|
+
console.error(`[GCPPubSubAdapter] Error disconnecting: ${(error as Error).message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Subscribe to a GCP Pub/Sub topic
|
|
94
|
+
*/
|
|
95
|
+
async subscribe(config: PubSubTriggerOpts, handler: (message: PubSubMessage) => Promise<void>): Promise<void> {
|
|
96
|
+
if (!this.connected) {
|
|
97
|
+
throw new Error("Not connected to GCP Pub/Sub. Call connect() first.");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const subscriptionName = config.subscription;
|
|
101
|
+
|
|
102
|
+
// Get the subscription
|
|
103
|
+
const subscription = this.client.subscription(subscriptionName, {
|
|
104
|
+
flowControl: {
|
|
105
|
+
maxMessages: config.maxMessages || 10,
|
|
106
|
+
},
|
|
107
|
+
ackDeadline: config.ackDeadline || 30,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Message handler
|
|
111
|
+
const messageHandler = async (gcpMessage: any) => {
|
|
112
|
+
// Parse message data
|
|
113
|
+
let body: unknown;
|
|
114
|
+
try {
|
|
115
|
+
const data = gcpMessage.data.toString();
|
|
116
|
+
body = JSON.parse(data);
|
|
117
|
+
} catch {
|
|
118
|
+
body = gcpMessage.data.toString();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Create pub/sub message
|
|
122
|
+
const pubsubMessage: PubSubMessage = {
|
|
123
|
+
id: gcpMessage.id || uuid(),
|
|
124
|
+
body,
|
|
125
|
+
attributes: gcpMessage.attributes || {},
|
|
126
|
+
raw: gcpMessage,
|
|
127
|
+
topic: config.topic,
|
|
128
|
+
subscription: subscriptionName,
|
|
129
|
+
publishTime: gcpMessage.publishTime ? new Date(gcpMessage.publishTime) : new Date(),
|
|
130
|
+
ack: async () => {
|
|
131
|
+
gcpMessage.ack();
|
|
132
|
+
},
|
|
133
|
+
nack: async () => {
|
|
134
|
+
gcpMessage.nack();
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// Process message
|
|
139
|
+
try {
|
|
140
|
+
await handler(pubsubMessage);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error(`[GCPPubSubAdapter] Error processing message: ${(error as Error).message}`);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// Error handler
|
|
147
|
+
const errorHandler = (error: Error) => {
|
|
148
|
+
console.error(`[GCPPubSubAdapter] Subscription error: ${error.message}`);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
// Attach listeners
|
|
152
|
+
subscription.on("message", messageHandler);
|
|
153
|
+
subscription.on("error", errorHandler);
|
|
154
|
+
|
|
155
|
+
// Store subscription reference
|
|
156
|
+
this.subscriptions.set(subscriptionName, subscription);
|
|
157
|
+
|
|
158
|
+
console.log(`[GCPPubSubAdapter] Subscribed to: ${subscriptionName}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Unsubscribe from a GCP Pub/Sub subscription
|
|
163
|
+
*/
|
|
164
|
+
async unsubscribe(subscriptionName: string): Promise<void> {
|
|
165
|
+
const subscription = this.subscriptions.get(subscriptionName);
|
|
166
|
+
if (subscription) {
|
|
167
|
+
await subscription.close();
|
|
168
|
+
this.subscriptions.delete(subscriptionName);
|
|
169
|
+
console.log(`[GCPPubSubAdapter] Unsubscribed from: ${subscriptionName}`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if connected to GCP Pub/Sub
|
|
175
|
+
*/
|
|
176
|
+
isConnected(): boolean {
|
|
177
|
+
return this.connected;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Health check - verify GCP Pub/Sub connectivity
|
|
182
|
+
*/
|
|
183
|
+
async healthCheck(): Promise<boolean> {
|
|
184
|
+
if (!this.connected) return false;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
// List subscriptions as a health check
|
|
188
|
+
const [subscriptions] = await this.client.getSubscriptions({ pageSize: 1 });
|
|
189
|
+
return true;
|
|
190
|
+
} catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export default GCPPubSubAdapter;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blokjs/trigger-pubsub
|
|
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
|
|
9
|
+
*
|
|
10
|
+
* @example GCP Pub/Sub
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { PubSubTrigger, GCPPubSubAdapter } from "@blokjs/trigger-pubsub";
|
|
13
|
+
*
|
|
14
|
+
* class MyPubSubTrigger extends PubSubTrigger {
|
|
15
|
+
* protected adapter = new GCPPubSubAdapter({
|
|
16
|
+
* projectId: "my-project",
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
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";
|
|
42
|
+
*
|
|
43
|
+
* class MyPubSubTrigger extends PubSubTrigger {
|
|
44
|
+
* protected adapter = new AzureServiceBusAdapter({
|
|
45
|
+
* connectionString: process.env.AZURE_SERVICE_BUS_CONNECTION_STRING,
|
|
46
|
+
* });
|
|
47
|
+
* // ...
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
// Core exports
|
|
53
|
+
export {
|
|
54
|
+
PubSubTrigger,
|
|
55
|
+
type PubSubAdapter,
|
|
56
|
+
type PubSubMessage,
|
|
57
|
+
} from "./PubSubTrigger";
|
|
58
|
+
|
|
59
|
+
// Adapters
|
|
60
|
+
export { GCPPubSubAdapter, type GCPPubSubConfig } from "./adapters/GCPPubSubAdapter";
|
|
61
|
+
export { AWSSNSAdapter, type AWSSNSConfig } from "./adapters/AWSSNSAdapter";
|
|
62
|
+
export { AzureServiceBusAdapter, type AzureServiceBusConfig } from "./adapters/AzureServiceBusAdapter";
|
|
63
|
+
|
|
64
|
+
// Re-export types from helper for convenience
|
|
65
|
+
export type {
|
|
66
|
+
PubSubProvider,
|
|
67
|
+
PubSubTriggerOpts,
|
|
68
|
+
} from "@blokjs/helper";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
PROJECT_NAME=trigger-pubsub-server
|
|
2
|
+
PROJECT_VERSION=0.0.1
|
|
3
|
+
PORT=4006
|
|
4
|
+
WORKFLOWS_PATH=PROJECT_PATH/workflows
|
|
5
|
+
NODES_PATH=PROJECT_PATH/src/nodes
|
|
6
|
+
CONSOLE_LOG_ACTIVE=true
|
|
7
|
+
APP_NAME=blok-pubsub
|
|
8
|
+
DISABLE_TRIGGER_RUN=false # Set to true to disable trigger run and use this project as a module
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blok-pubsub-trigger",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pub/Sub trigger for Blok workflows",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18.0.0"
|
|
8
|
+
},
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"types": "dist/index.d.ts",
|
|
11
|
+
"author": "",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "bun --watch run src/index.ts",
|
|
15
|
+
"start": "bun run dist/index.js",
|
|
16
|
+
"build": "rimraf ./dist && tsc",
|
|
17
|
+
"test": "vitest run",
|
|
18
|
+
"test:dev": "vitest"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22.15.21",
|
|
22
|
+
"@types/uuid": "^11.0.0",
|
|
23
|
+
"rimraf": "^6.1.2",
|
|
24
|
+
"typescript": "^5.8.3",
|
|
25
|
+
"vitest": "^4.0.18"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@blokjs/api-call": "workspace:*",
|
|
29
|
+
"@blokjs/helper": "workspace:*",
|
|
30
|
+
"@blokjs/if-else": "workspace:*",
|
|
31
|
+
"@blokjs/runner": "workspace:*",
|
|
32
|
+
"@blokjs/shared": "workspace:*",
|
|
33
|
+
"@blokjs/trigger-pubsub": "workspace:*",
|
|
34
|
+
"@opentelemetry/api": "^1.9.0",
|
|
35
|
+
"@opentelemetry/exporter-prometheus": "^0.57.2",
|
|
36
|
+
"@opentelemetry/resources": "^1.30.1",
|
|
37
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
38
|
+
"@opentelemetry/sdk-trace-base": "^1.30.1",
|
|
39
|
+
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
40
|
+
"uuid": "^11.1.0",
|
|
41
|
+
"zod": "^3.24.2"
|
|
42
|
+
},
|
|
43
|
+
"private": true
|
|
44
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import ApiCall from "@blokjs/api-call";
|
|
2
|
+
import IfElse from "@blokjs/if-else";
|
|
3
|
+
import type { BlokService } from "@blokjs/runner";
|
|
4
|
+
|
|
5
|
+
const nodes: Record<string, BlokService<unknown>> = {
|
|
6
|
+
"@blokjs/api-call": ApiCall,
|
|
7
|
+
"@blokjs/if-else": IfElse,
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default nodes;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { DefaultLogger } from "@blokjs/runner";
|
|
2
|
+
import { type Span, metrics, trace } from "@opentelemetry/api";
|
|
3
|
+
import PubSubServer from "./runner/PubSubServer";
|
|
4
|
+
|
|
5
|
+
export default class App {
|
|
6
|
+
private pubsubServer: PubSubServer = <PubSubServer>{};
|
|
7
|
+
protected trigger_initializer = 0;
|
|
8
|
+
protected initializer = 0;
|
|
9
|
+
protected tracer = trace.getTracer(
|
|
10
|
+
process.env.PROJECT_NAME || "trigger-pubsub-server",
|
|
11
|
+
process.env.PROJECT_VERSION || "0.0.1",
|
|
12
|
+
);
|
|
13
|
+
private logger = new DefaultLogger();
|
|
14
|
+
protected app_cold_start = metrics.getMeter("default").createGauge("initialization", {
|
|
15
|
+
description: "Application cold start",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
this.initializer = performance.now();
|
|
20
|
+
this.pubsubServer = new PubSubServer();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async run() {
|
|
24
|
+
this.tracer.startActiveSpan("initialization", async (span: Span) => {
|
|
25
|
+
await this.pubsubServer.listen();
|
|
26
|
+
this.initializer = performance.now() - this.initializer;
|
|
27
|
+
|
|
28
|
+
this.logger.log(`Pub/Sub trigger initialized in ${this.initializer.toFixed(2)}ms`);
|
|
29
|
+
this.app_cold_start.record(this.initializer, {
|
|
30
|
+
pid: process.pid,
|
|
31
|
+
env: process.env.NODE_ENV,
|
|
32
|
+
app: process.env.APP_NAME,
|
|
33
|
+
});
|
|
34
|
+
span.end();
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (process.env.DISABLE_TRIGGER_RUN !== "true") {
|
|
40
|
+
new App().run();
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { GCPPubSubAdapter, PubSubTrigger } from "@blokjs/trigger-pubsub";
|
|
2
|
+
import nodes from "../Nodes";
|
|
3
|
+
import workflows from "../Workflows";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* PubSubServer - Concrete Pub/Sub trigger implementation
|
|
7
|
+
*
|
|
8
|
+
* This server extends the abstract PubSubTrigger and provides:
|
|
9
|
+
* - A specific adapter (GCP Pub/Sub by default, can be changed to AWS or Azure)
|
|
10
|
+
* - Node and workflow registries
|
|
11
|
+
*
|
|
12
|
+
* To change the provider, replace:
|
|
13
|
+
* - GCPPubSubAdapter with AWSSNSAdapter or AzureServiceBusAdapter
|
|
14
|
+
* - Update the adapter configuration accordingly
|
|
15
|
+
*
|
|
16
|
+
* @example AWS SNS/SQS
|
|
17
|
+
* ```typescript
|
|
18
|
+
* import { AWSSNSAdapter } from "@blokjs/trigger-pubsub";
|
|
19
|
+
* protected adapter = new AWSSNSAdapter({
|
|
20
|
+
* region: process.env.AWS_REGION || "us-east-1",
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example Azure Service Bus
|
|
25
|
+
* ```typescript
|
|
26
|
+
* import { AzureServiceBusAdapter } from "@blokjs/trigger-pubsub";
|
|
27
|
+
* protected adapter = new AzureServiceBusAdapter({
|
|
28
|
+
* connectionString: process.env.AZURE_SERVICE_BUS_CONNECTION_STRING || "",
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export default class PubSubServer extends PubSubTrigger {
|
|
33
|
+
protected adapter = new GCPPubSubAdapter({
|
|
34
|
+
projectId: process.env.GCP_PROJECT_ID || "my-project",
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
protected nodes = nodes;
|
|
38
|
+
protected workflows = workflows;
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Step, Workflow } from "@blokjs/helper";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Example Pub/Sub workflow - triggered when a message is received
|
|
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
|
|
12
|
+
*
|
|
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
|
|
18
|
+
*/
|
|
19
|
+
const step: Step = Workflow({
|
|
20
|
+
name: "On Pub/Sub Message",
|
|
21
|
+
version: "1.0.0",
|
|
22
|
+
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",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
export default step;
|