@saga-bus/transport-gcp-pubsub 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/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/index.cjs +177 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +61 -0
- package/dist/index.d.ts +61 -0
- package/dist/index.js +150 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Dean Foran
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# @saga-bus/transport-gcp-pubsub
|
|
2
|
+
|
|
3
|
+
Google Cloud Pub/Sub transport for saga-bus.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @saga-bus/transport-gcp-pubsub @google-cloud/pubsub
|
|
9
|
+
# or
|
|
10
|
+
pnpm add @saga-bus/transport-gcp-pubsub @google-cloud/pubsub
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- **Topic/Subscription Model**: GCP Pub/Sub native architecture
|
|
16
|
+
- **Message Ordering**: Optional ordering key support
|
|
17
|
+
- **Auto-Creation**: Automatically creates topics and subscriptions
|
|
18
|
+
- **Dead-Letter Queues**: Support for DLQ configuration
|
|
19
|
+
- **Authentication**: Google Cloud ADC and service account support
|
|
20
|
+
|
|
21
|
+
## Quick Start
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { createBus } from "@saga-bus/core";
|
|
25
|
+
import { GcpPubSubTransport } from "@saga-bus/transport-gcp-pubsub";
|
|
26
|
+
|
|
27
|
+
const transport = new GcpPubSubTransport({
|
|
28
|
+
projectId: "my-gcp-project",
|
|
29
|
+
defaultTopic: "saga-events",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const bus = createBus({
|
|
33
|
+
transport,
|
|
34
|
+
// ... other config
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await bus.start();
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Configuration
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
interface GcpPubSubTransportOptions {
|
|
44
|
+
/** Existing PubSub client instance */
|
|
45
|
+
pubsub?: PubSub;
|
|
46
|
+
|
|
47
|
+
/** Client config for creating new PubSub instance */
|
|
48
|
+
clientConfig?: ClientConfig;
|
|
49
|
+
|
|
50
|
+
/** Project ID (required if not in clientConfig) */
|
|
51
|
+
projectId?: string;
|
|
52
|
+
|
|
53
|
+
/** Default topic for publishing */
|
|
54
|
+
defaultTopic?: string;
|
|
55
|
+
|
|
56
|
+
/** Subscription name prefix (default: "saga-bus-") */
|
|
57
|
+
subscriptionPrefix?: string;
|
|
58
|
+
|
|
59
|
+
/** Whether to use ordering keys for message ordering (default: false) */
|
|
60
|
+
enableOrdering?: boolean;
|
|
61
|
+
|
|
62
|
+
/** Max messages to pull at once (default: 10) */
|
|
63
|
+
maxMessages?: number;
|
|
64
|
+
|
|
65
|
+
/** Ack deadline in seconds (default: 60) */
|
|
66
|
+
ackDeadlineSeconds?: number;
|
|
67
|
+
|
|
68
|
+
/** Whether to auto-create topics/subscriptions (default: true) */
|
|
69
|
+
autoCreate?: boolean;
|
|
70
|
+
|
|
71
|
+
/** Dead-letter topic for failed messages */
|
|
72
|
+
deadLetterTopic?: string;
|
|
73
|
+
|
|
74
|
+
/** Max delivery attempts before dead-letter (default: 5) */
|
|
75
|
+
maxDeliveryAttempts?: number;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
### Basic Usage
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { GcpPubSubTransport } from "@saga-bus/transport-gcp-pubsub";
|
|
85
|
+
|
|
86
|
+
const transport = new GcpPubSubTransport({
|
|
87
|
+
projectId: "my-project",
|
|
88
|
+
defaultTopic: "saga-events",
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await transport.start();
|
|
92
|
+
|
|
93
|
+
// Publish a message
|
|
94
|
+
await transport.publish(
|
|
95
|
+
{ type: "OrderCreated", orderId: "123" },
|
|
96
|
+
{ endpoint: "orders" }
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// Subscribe to messages
|
|
100
|
+
await transport.subscribe(
|
|
101
|
+
{ endpoint: "orders", group: "order-processor" },
|
|
102
|
+
async (envelope) => {
|
|
103
|
+
console.log("Received:", envelope.payload);
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### With Message Ordering
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
const transport = new GcpPubSubTransport({
|
|
112
|
+
projectId: "my-project",
|
|
113
|
+
enableOrdering: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await transport.start();
|
|
117
|
+
|
|
118
|
+
// Messages with the same key will be delivered in order
|
|
119
|
+
await transport.publish(
|
|
120
|
+
{ type: "OrderCreated", orderId: "123" },
|
|
121
|
+
{ endpoint: "orders", key: "order-123" }
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await transport.publish(
|
|
125
|
+
{ type: "OrderShipped", orderId: "123" },
|
|
126
|
+
{ endpoint: "orders", key: "order-123" }
|
|
127
|
+
);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Using Existing PubSub Client
|
|
131
|
+
|
|
132
|
+
```typescript
|
|
133
|
+
import { PubSub } from "@google-cloud/pubsub";
|
|
134
|
+
|
|
135
|
+
const pubsub = new PubSub({
|
|
136
|
+
projectId: "my-project",
|
|
137
|
+
keyFilename: "/path/to/service-account.json",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const transport = new GcpPubSubTransport({
|
|
141
|
+
pubsub,
|
|
142
|
+
defaultTopic: "saga-events",
|
|
143
|
+
});
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### With Dead-Letter Topic
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
const transport = new GcpPubSubTransport({
|
|
150
|
+
projectId: "my-project",
|
|
151
|
+
deadLetterTopic: "saga-dlq",
|
|
152
|
+
maxDeliveryAttempts: 5,
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Message Format
|
|
157
|
+
|
|
158
|
+
Messages are published as JSON with attributes:
|
|
159
|
+
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"data": {
|
|
163
|
+
"id": "msg-uuid",
|
|
164
|
+
"type": "OrderCreated",
|
|
165
|
+
"payload": { "type": "OrderCreated", "orderId": "123" },
|
|
166
|
+
"headers": {},
|
|
167
|
+
"timestamp": "2024-01-01T00:00:00.000Z",
|
|
168
|
+
"partitionKey": "order-123"
|
|
169
|
+
},
|
|
170
|
+
"attributes": {
|
|
171
|
+
"messageType": "OrderCreated",
|
|
172
|
+
"messageId": "msg-uuid",
|
|
173
|
+
"correlationId": "order-123"
|
|
174
|
+
},
|
|
175
|
+
"orderingKey": "order-123"
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Limitations
|
|
180
|
+
|
|
181
|
+
### No Delayed Messages
|
|
182
|
+
|
|
183
|
+
GCP Pub/Sub does not support native delayed message delivery. Attempting to publish with `delayMs` will throw an error:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
// This will throw an error
|
|
187
|
+
await transport.publish(message, { delayMs: 5000 });
|
|
188
|
+
// Error: GCP Pub/Sub does not support delayed messages.
|
|
189
|
+
// Use Cloud Scheduler or Cloud Tasks for delayed delivery.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Alternatives for delayed delivery:**
|
|
193
|
+
|
|
194
|
+
1. **Cloud Scheduler**: Schedule messages at specific times
|
|
195
|
+
2. **Cloud Tasks**: Queue tasks with delay
|
|
196
|
+
3. **Cloud Functions with Pub/Sub**: Implement custom delay logic
|
|
197
|
+
|
|
198
|
+
## Authentication
|
|
199
|
+
|
|
200
|
+
The transport uses Google Cloud's Application Default Credentials (ADC):
|
|
201
|
+
|
|
202
|
+
1. **Local Development**: Use `gcloud auth application-default login`
|
|
203
|
+
2. **Service Account**: Set `GOOGLE_APPLICATION_CREDENTIALS` environment variable
|
|
204
|
+
3. **GCP Services**: Automatically uses metadata service
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
# Local development
|
|
208
|
+
gcloud auth application-default login
|
|
209
|
+
|
|
210
|
+
# Or use service account
|
|
211
|
+
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
## Testing
|
|
215
|
+
|
|
216
|
+
For testing, you can use the GCP Pub/Sub emulator:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Start emulator
|
|
220
|
+
gcloud beta emulators pubsub start --project=test-project
|
|
221
|
+
|
|
222
|
+
# Set environment variable
|
|
223
|
+
export PUBSUB_EMULATOR_HOST=localhost:8085
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## License
|
|
227
|
+
|
|
228
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
GcpPubSubTransport: () => GcpPubSubTransport
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/GcpPubSubTransport.ts
|
|
28
|
+
var import_pubsub = require("@google-cloud/pubsub");
|
|
29
|
+
var import_crypto = require("crypto");
|
|
30
|
+
var GcpPubSubTransport = class {
|
|
31
|
+
pubsub = null;
|
|
32
|
+
options;
|
|
33
|
+
topics = /* @__PURE__ */ new Map();
|
|
34
|
+
subscriptions = [];
|
|
35
|
+
started = false;
|
|
36
|
+
constructor(options) {
|
|
37
|
+
if (!options.pubsub && !options.clientConfig && !options.projectId) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
"Either pubsub, clientConfig, or projectId must be provided"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
this.options = {
|
|
43
|
+
subscriptionPrefix: "saga-bus-",
|
|
44
|
+
enableOrdering: false,
|
|
45
|
+
maxMessages: 10,
|
|
46
|
+
ackDeadlineSeconds: 60,
|
|
47
|
+
autoCreate: true,
|
|
48
|
+
maxDeliveryAttempts: 5,
|
|
49
|
+
...options
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async start() {
|
|
53
|
+
if (this.started) return;
|
|
54
|
+
if (this.options.pubsub) {
|
|
55
|
+
this.pubsub = this.options.pubsub;
|
|
56
|
+
} else {
|
|
57
|
+
this.pubsub = new import_pubsub.PubSub({
|
|
58
|
+
projectId: this.options.projectId,
|
|
59
|
+
...this.options.clientConfig
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
this.started = true;
|
|
63
|
+
}
|
|
64
|
+
async stop() {
|
|
65
|
+
if (!this.started) return;
|
|
66
|
+
for (const sub of this.subscriptions) {
|
|
67
|
+
await sub.close();
|
|
68
|
+
}
|
|
69
|
+
this.subscriptions.length = 0;
|
|
70
|
+
this.topics.clear();
|
|
71
|
+
if (!this.options.pubsub && this.pubsub) {
|
|
72
|
+
await this.pubsub.close();
|
|
73
|
+
}
|
|
74
|
+
this.pubsub = null;
|
|
75
|
+
this.started = false;
|
|
76
|
+
}
|
|
77
|
+
async subscribe(options, handler) {
|
|
78
|
+
if (!this.pubsub) throw new Error("Transport not started");
|
|
79
|
+
const { endpoint, group } = options;
|
|
80
|
+
const topicName = endpoint;
|
|
81
|
+
const subscriptionName = group ?? `${this.options.subscriptionPrefix}${endpoint}`;
|
|
82
|
+
let topic = this.topics.get(topicName);
|
|
83
|
+
if (!topic) {
|
|
84
|
+
topic = this.pubsub.topic(topicName);
|
|
85
|
+
if (this.options.autoCreate) {
|
|
86
|
+
const [exists] = await topic.exists();
|
|
87
|
+
if (!exists) await topic.create();
|
|
88
|
+
}
|
|
89
|
+
this.topics.set(topicName, topic);
|
|
90
|
+
}
|
|
91
|
+
const subscription = topic.subscription(subscriptionName);
|
|
92
|
+
if (this.options.autoCreate) {
|
|
93
|
+
const [exists] = await subscription.exists();
|
|
94
|
+
if (!exists) {
|
|
95
|
+
await subscription.create({
|
|
96
|
+
enableMessageOrdering: this.options.enableOrdering,
|
|
97
|
+
ackDeadlineSeconds: this.options.ackDeadlineSeconds
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
subscription.on("message", async (message) => {
|
|
102
|
+
try {
|
|
103
|
+
const rawEnvelope = JSON.parse(message.data.toString());
|
|
104
|
+
const envelope = {
|
|
105
|
+
id: rawEnvelope.id,
|
|
106
|
+
type: rawEnvelope.type,
|
|
107
|
+
payload: rawEnvelope.payload,
|
|
108
|
+
headers: rawEnvelope.headers,
|
|
109
|
+
timestamp: new Date(rawEnvelope.timestamp),
|
|
110
|
+
partitionKey: rawEnvelope.partitionKey
|
|
111
|
+
};
|
|
112
|
+
await handler(envelope);
|
|
113
|
+
message.ack();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
console.error("[GcpPubSub] Message handler error:", error);
|
|
116
|
+
message.nack();
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
subscription.on("error", (error) => {
|
|
120
|
+
console.error("[GcpPubSub] Subscription error:", error);
|
|
121
|
+
});
|
|
122
|
+
this.subscriptions.push(subscription);
|
|
123
|
+
}
|
|
124
|
+
async publish(message, options) {
|
|
125
|
+
if (!this.pubsub) throw new Error("Transport not started");
|
|
126
|
+
const { endpoint, key, headers = {}, delayMs } = options;
|
|
127
|
+
if (delayMs && delayMs > 0) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
"GCP Pub/Sub does not support delayed messages. Use Cloud Scheduler or Cloud Tasks for delayed delivery."
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
const topicName = endpoint ?? this.options.defaultTopic ?? message.type;
|
|
133
|
+
let topic = this.topics.get(topicName);
|
|
134
|
+
if (!topic) {
|
|
135
|
+
topic = this.pubsub.topic(topicName);
|
|
136
|
+
if (this.options.autoCreate) {
|
|
137
|
+
const [exists] = await topic.exists();
|
|
138
|
+
if (!exists) await topic.create();
|
|
139
|
+
}
|
|
140
|
+
this.topics.set(topicName, topic);
|
|
141
|
+
}
|
|
142
|
+
const envelope = {
|
|
143
|
+
id: (0, import_crypto.randomUUID)(),
|
|
144
|
+
type: message.type,
|
|
145
|
+
payload: message,
|
|
146
|
+
headers,
|
|
147
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
148
|
+
partitionKey: key
|
|
149
|
+
};
|
|
150
|
+
const attributes = {
|
|
151
|
+
messageType: message.type,
|
|
152
|
+
messageId: envelope.id
|
|
153
|
+
};
|
|
154
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
155
|
+
if (typeof v === "string") {
|
|
156
|
+
attributes[k] = v;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (key) {
|
|
160
|
+
attributes.correlationId = key;
|
|
161
|
+
}
|
|
162
|
+
const publishOptions = {};
|
|
163
|
+
if (this.options.enableOrdering && key) {
|
|
164
|
+
publishOptions.orderingKey = key;
|
|
165
|
+
}
|
|
166
|
+
await topic.publishMessage({
|
|
167
|
+
data: Buffer.from(JSON.stringify(envelope)),
|
|
168
|
+
attributes,
|
|
169
|
+
...publishOptions
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
};
|
|
173
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
174
|
+
0 && (module.exports = {
|
|
175
|
+
GcpPubSubTransport
|
|
176
|
+
});
|
|
177
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/GcpPubSubTransport.ts"],"sourcesContent":["export { GcpPubSubTransport } from \"./GcpPubSubTransport.js\";\nexport type { GcpPubSubTransportOptions } from \"./types.js\";\n","import { PubSub, Topic, Subscription, Message } from \"@google-cloud/pubsub\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n MessageEnvelope,\n BaseMessage,\n} from \"@saga-bus/core\";\nimport type { GcpPubSubTransportOptions } from \"./types.js\";\nimport { randomUUID } from \"crypto\";\n\n/**\n * GCP Pub/Sub transport for saga-bus.\n *\n * @example\n * ```typescript\n * import { GcpPubSubTransport } from \"@saga-bus/transport-gcp-pubsub\";\n *\n * const transport = new GcpPubSubTransport({\n * projectId: \"my-project\",\n * defaultTopic: \"saga-events\",\n * enableOrdering: true,\n * });\n *\n * await transport.start();\n * ```\n */\nexport class GcpPubSubTransport implements Transport {\n private pubsub: PubSub | null = null;\n private readonly options: Required<\n Pick<\n GcpPubSubTransportOptions,\n | \"subscriptionPrefix\"\n | \"enableOrdering\"\n | \"maxMessages\"\n | \"ackDeadlineSeconds\"\n | \"autoCreate\"\n | \"maxDeliveryAttempts\"\n >\n > &\n GcpPubSubTransportOptions;\n\n private readonly topics = new Map<string, Topic>();\n private readonly subscriptions: Subscription[] = [];\n private started = false;\n\n constructor(options: GcpPubSubTransportOptions) {\n if (!options.pubsub && !options.clientConfig && !options.projectId) {\n throw new Error(\n \"Either pubsub, clientConfig, or projectId must be provided\"\n );\n }\n\n this.options = {\n subscriptionPrefix: \"saga-bus-\",\n enableOrdering: false,\n maxMessages: 10,\n ackDeadlineSeconds: 60,\n autoCreate: true,\n maxDeliveryAttempts: 5,\n ...options,\n };\n }\n\n async start(): Promise<void> {\n if (this.started) return;\n\n if (this.options.pubsub) {\n this.pubsub = this.options.pubsub;\n } else {\n this.pubsub = new PubSub({\n projectId: this.options.projectId,\n ...this.options.clientConfig,\n });\n }\n\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) return;\n\n // Close all subscriptions\n for (const sub of this.subscriptions) {\n await sub.close();\n }\n this.subscriptions.length = 0;\n this.topics.clear();\n\n // Close client if we created it\n if (!this.options.pubsub && this.pubsub) {\n await this.pubsub.close();\n }\n this.pubsub = null;\n this.started = false;\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n if (!this.pubsub) throw new Error(\"Transport not started\");\n\n const { endpoint, group } = options;\n const topicName = endpoint;\n const subscriptionName =\n group ?? `${this.options.subscriptionPrefix}${endpoint}`;\n\n // Get or create topic\n let topic = this.topics.get(topicName);\n if (!topic) {\n topic = this.pubsub.topic(topicName);\n if (this.options.autoCreate) {\n const [exists] = await topic.exists();\n if (!exists) await topic.create();\n }\n this.topics.set(topicName, topic);\n }\n\n // Get or create subscription\n const subscription = topic.subscription(subscriptionName);\n if (this.options.autoCreate) {\n const [exists] = await subscription.exists();\n if (!exists) {\n await subscription.create({\n enableMessageOrdering: this.options.enableOrdering,\n ackDeadlineSeconds: this.options.ackDeadlineSeconds,\n });\n }\n }\n\n // Start listening\n subscription.on(\"message\", async (message: Message) => {\n try {\n const rawEnvelope = JSON.parse(message.data.toString());\n const envelope: MessageEnvelope<TMessage> = {\n id: rawEnvelope.id,\n type: rawEnvelope.type,\n payload: rawEnvelope.payload as TMessage,\n headers: rawEnvelope.headers,\n timestamp: new Date(rawEnvelope.timestamp),\n partitionKey: rawEnvelope.partitionKey,\n };\n await handler(envelope);\n message.ack();\n } catch (error) {\n console.error(\"[GcpPubSub] Message handler error:\", error);\n message.nack();\n }\n });\n\n subscription.on(\"error\", (error) => {\n console.error(\"[GcpPubSub] Subscription error:\", error);\n });\n\n this.subscriptions.push(subscription);\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n if (!this.pubsub) throw new Error(\"Transport not started\");\n\n const { endpoint, key, headers = {}, delayMs } = options;\n\n // GCP Pub/Sub doesn't support delayed messages natively\n if (delayMs && delayMs > 0) {\n throw new Error(\n \"GCP Pub/Sub does not support delayed messages. \" +\n \"Use Cloud Scheduler or Cloud Tasks for delayed delivery.\"\n );\n }\n\n const topicName = endpoint ?? this.options.defaultTopic ?? message.type;\n\n // Get or create topic\n let topic = this.topics.get(topicName);\n if (!topic) {\n topic = this.pubsub.topic(topicName);\n if (this.options.autoCreate) {\n const [exists] = await topic.exists();\n if (!exists) await topic.create();\n }\n this.topics.set(topicName, topic);\n }\n\n // Create envelope\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: headers as Record<string, string>,\n timestamp: new Date(),\n partitionKey: key,\n };\n\n // Publish\n const attributes: Record<string, string> = {\n messageType: message.type,\n messageId: envelope.id,\n };\n\n // Add custom headers as attributes\n for (const [k, v] of Object.entries(headers)) {\n if (typeof v === \"string\") {\n attributes[k] = v;\n }\n }\n\n if (key) {\n attributes.correlationId = key;\n }\n\n const publishOptions: { orderingKey?: string } = {};\n if (this.options.enableOrdering && key) {\n publishOptions.orderingKey = key;\n }\n\n await topic.publishMessage({\n data: Buffer.from(JSON.stringify(envelope)),\n attributes,\n ...publishOptions,\n });\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
|
|
2
|
+
import { PubSub, ClientConfig } from '@google-cloud/pubsub';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the GCP Pub/Sub transport.
|
|
6
|
+
*/
|
|
7
|
+
interface GcpPubSubTransportOptions {
|
|
8
|
+
/** Existing PubSub client instance */
|
|
9
|
+
pubsub?: PubSub;
|
|
10
|
+
/** Client config for creating new PubSub instance */
|
|
11
|
+
clientConfig?: ClientConfig;
|
|
12
|
+
/** Project ID (required if not in clientConfig) */
|
|
13
|
+
projectId?: string;
|
|
14
|
+
/** Default topic for publishing */
|
|
15
|
+
defaultTopic?: string;
|
|
16
|
+
/** Subscription name prefix */
|
|
17
|
+
subscriptionPrefix?: string;
|
|
18
|
+
/** Whether to use ordering keys for message ordering */
|
|
19
|
+
enableOrdering?: boolean;
|
|
20
|
+
/** Max messages to pull at once */
|
|
21
|
+
maxMessages?: number;
|
|
22
|
+
/** Ack deadline in seconds */
|
|
23
|
+
ackDeadlineSeconds?: number;
|
|
24
|
+
/** Whether to auto-create topics/subscriptions */
|
|
25
|
+
autoCreate?: boolean;
|
|
26
|
+
/** Dead-letter topic for failed messages */
|
|
27
|
+
deadLetterTopic?: string;
|
|
28
|
+
/** Max delivery attempts before dead-letter */
|
|
29
|
+
maxDeliveryAttempts?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GCP Pub/Sub transport for saga-bus.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import { GcpPubSubTransport } from "@saga-bus/transport-gcp-pubsub";
|
|
38
|
+
*
|
|
39
|
+
* const transport = new GcpPubSubTransport({
|
|
40
|
+
* projectId: "my-project",
|
|
41
|
+
* defaultTopic: "saga-events",
|
|
42
|
+
* enableOrdering: true,
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* await transport.start();
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare class GcpPubSubTransport implements Transport {
|
|
49
|
+
private pubsub;
|
|
50
|
+
private readonly options;
|
|
51
|
+
private readonly topics;
|
|
52
|
+
private readonly subscriptions;
|
|
53
|
+
private started;
|
|
54
|
+
constructor(options: GcpPubSubTransportOptions);
|
|
55
|
+
start(): Promise<void>;
|
|
56
|
+
stop(): Promise<void>;
|
|
57
|
+
subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
|
|
58
|
+
publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { GcpPubSubTransport, type GcpPubSubTransportOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Transport, BaseMessage, TransportSubscribeOptions, MessageEnvelope, TransportPublishOptions } from '@saga-bus/core';
|
|
2
|
+
import { PubSub, ClientConfig } from '@google-cloud/pubsub';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for the GCP Pub/Sub transport.
|
|
6
|
+
*/
|
|
7
|
+
interface GcpPubSubTransportOptions {
|
|
8
|
+
/** Existing PubSub client instance */
|
|
9
|
+
pubsub?: PubSub;
|
|
10
|
+
/** Client config for creating new PubSub instance */
|
|
11
|
+
clientConfig?: ClientConfig;
|
|
12
|
+
/** Project ID (required if not in clientConfig) */
|
|
13
|
+
projectId?: string;
|
|
14
|
+
/** Default topic for publishing */
|
|
15
|
+
defaultTopic?: string;
|
|
16
|
+
/** Subscription name prefix */
|
|
17
|
+
subscriptionPrefix?: string;
|
|
18
|
+
/** Whether to use ordering keys for message ordering */
|
|
19
|
+
enableOrdering?: boolean;
|
|
20
|
+
/** Max messages to pull at once */
|
|
21
|
+
maxMessages?: number;
|
|
22
|
+
/** Ack deadline in seconds */
|
|
23
|
+
ackDeadlineSeconds?: number;
|
|
24
|
+
/** Whether to auto-create topics/subscriptions */
|
|
25
|
+
autoCreate?: boolean;
|
|
26
|
+
/** Dead-letter topic for failed messages */
|
|
27
|
+
deadLetterTopic?: string;
|
|
28
|
+
/** Max delivery attempts before dead-letter */
|
|
29
|
+
maxDeliveryAttempts?: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* GCP Pub/Sub transport for saga-bus.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* import { GcpPubSubTransport } from "@saga-bus/transport-gcp-pubsub";
|
|
38
|
+
*
|
|
39
|
+
* const transport = new GcpPubSubTransport({
|
|
40
|
+
* projectId: "my-project",
|
|
41
|
+
* defaultTopic: "saga-events",
|
|
42
|
+
* enableOrdering: true,
|
|
43
|
+
* });
|
|
44
|
+
*
|
|
45
|
+
* await transport.start();
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
declare class GcpPubSubTransport implements Transport {
|
|
49
|
+
private pubsub;
|
|
50
|
+
private readonly options;
|
|
51
|
+
private readonly topics;
|
|
52
|
+
private readonly subscriptions;
|
|
53
|
+
private started;
|
|
54
|
+
constructor(options: GcpPubSubTransportOptions);
|
|
55
|
+
start(): Promise<void>;
|
|
56
|
+
stop(): Promise<void>;
|
|
57
|
+
subscribe<TMessage extends BaseMessage>(options: TransportSubscribeOptions, handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>): Promise<void>;
|
|
58
|
+
publish<TMessage extends BaseMessage>(message: TMessage, options: TransportPublishOptions): Promise<void>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { GcpPubSubTransport, type GcpPubSubTransportOptions };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/GcpPubSubTransport.ts
|
|
2
|
+
import { PubSub } from "@google-cloud/pubsub";
|
|
3
|
+
import { randomUUID } from "crypto";
|
|
4
|
+
var GcpPubSubTransport = class {
|
|
5
|
+
pubsub = null;
|
|
6
|
+
options;
|
|
7
|
+
topics = /* @__PURE__ */ new Map();
|
|
8
|
+
subscriptions = [];
|
|
9
|
+
started = false;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
if (!options.pubsub && !options.clientConfig && !options.projectId) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
"Either pubsub, clientConfig, or projectId must be provided"
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
this.options = {
|
|
17
|
+
subscriptionPrefix: "saga-bus-",
|
|
18
|
+
enableOrdering: false,
|
|
19
|
+
maxMessages: 10,
|
|
20
|
+
ackDeadlineSeconds: 60,
|
|
21
|
+
autoCreate: true,
|
|
22
|
+
maxDeliveryAttempts: 5,
|
|
23
|
+
...options
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async start() {
|
|
27
|
+
if (this.started) return;
|
|
28
|
+
if (this.options.pubsub) {
|
|
29
|
+
this.pubsub = this.options.pubsub;
|
|
30
|
+
} else {
|
|
31
|
+
this.pubsub = new PubSub({
|
|
32
|
+
projectId: this.options.projectId,
|
|
33
|
+
...this.options.clientConfig
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
this.started = true;
|
|
37
|
+
}
|
|
38
|
+
async stop() {
|
|
39
|
+
if (!this.started) return;
|
|
40
|
+
for (const sub of this.subscriptions) {
|
|
41
|
+
await sub.close();
|
|
42
|
+
}
|
|
43
|
+
this.subscriptions.length = 0;
|
|
44
|
+
this.topics.clear();
|
|
45
|
+
if (!this.options.pubsub && this.pubsub) {
|
|
46
|
+
await this.pubsub.close();
|
|
47
|
+
}
|
|
48
|
+
this.pubsub = null;
|
|
49
|
+
this.started = false;
|
|
50
|
+
}
|
|
51
|
+
async subscribe(options, handler) {
|
|
52
|
+
if (!this.pubsub) throw new Error("Transport not started");
|
|
53
|
+
const { endpoint, group } = options;
|
|
54
|
+
const topicName = endpoint;
|
|
55
|
+
const subscriptionName = group ?? `${this.options.subscriptionPrefix}${endpoint}`;
|
|
56
|
+
let topic = this.topics.get(topicName);
|
|
57
|
+
if (!topic) {
|
|
58
|
+
topic = this.pubsub.topic(topicName);
|
|
59
|
+
if (this.options.autoCreate) {
|
|
60
|
+
const [exists] = await topic.exists();
|
|
61
|
+
if (!exists) await topic.create();
|
|
62
|
+
}
|
|
63
|
+
this.topics.set(topicName, topic);
|
|
64
|
+
}
|
|
65
|
+
const subscription = topic.subscription(subscriptionName);
|
|
66
|
+
if (this.options.autoCreate) {
|
|
67
|
+
const [exists] = await subscription.exists();
|
|
68
|
+
if (!exists) {
|
|
69
|
+
await subscription.create({
|
|
70
|
+
enableMessageOrdering: this.options.enableOrdering,
|
|
71
|
+
ackDeadlineSeconds: this.options.ackDeadlineSeconds
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
subscription.on("message", async (message) => {
|
|
76
|
+
try {
|
|
77
|
+
const rawEnvelope = JSON.parse(message.data.toString());
|
|
78
|
+
const envelope = {
|
|
79
|
+
id: rawEnvelope.id,
|
|
80
|
+
type: rawEnvelope.type,
|
|
81
|
+
payload: rawEnvelope.payload,
|
|
82
|
+
headers: rawEnvelope.headers,
|
|
83
|
+
timestamp: new Date(rawEnvelope.timestamp),
|
|
84
|
+
partitionKey: rawEnvelope.partitionKey
|
|
85
|
+
};
|
|
86
|
+
await handler(envelope);
|
|
87
|
+
message.ack();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error("[GcpPubSub] Message handler error:", error);
|
|
90
|
+
message.nack();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
subscription.on("error", (error) => {
|
|
94
|
+
console.error("[GcpPubSub] Subscription error:", error);
|
|
95
|
+
});
|
|
96
|
+
this.subscriptions.push(subscription);
|
|
97
|
+
}
|
|
98
|
+
async publish(message, options) {
|
|
99
|
+
if (!this.pubsub) throw new Error("Transport not started");
|
|
100
|
+
const { endpoint, key, headers = {}, delayMs } = options;
|
|
101
|
+
if (delayMs && delayMs > 0) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
"GCP Pub/Sub does not support delayed messages. Use Cloud Scheduler or Cloud Tasks for delayed delivery."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
const topicName = endpoint ?? this.options.defaultTopic ?? message.type;
|
|
107
|
+
let topic = this.topics.get(topicName);
|
|
108
|
+
if (!topic) {
|
|
109
|
+
topic = this.pubsub.topic(topicName);
|
|
110
|
+
if (this.options.autoCreate) {
|
|
111
|
+
const [exists] = await topic.exists();
|
|
112
|
+
if (!exists) await topic.create();
|
|
113
|
+
}
|
|
114
|
+
this.topics.set(topicName, topic);
|
|
115
|
+
}
|
|
116
|
+
const envelope = {
|
|
117
|
+
id: randomUUID(),
|
|
118
|
+
type: message.type,
|
|
119
|
+
payload: message,
|
|
120
|
+
headers,
|
|
121
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
122
|
+
partitionKey: key
|
|
123
|
+
};
|
|
124
|
+
const attributes = {
|
|
125
|
+
messageType: message.type,
|
|
126
|
+
messageId: envelope.id
|
|
127
|
+
};
|
|
128
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
129
|
+
if (typeof v === "string") {
|
|
130
|
+
attributes[k] = v;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (key) {
|
|
134
|
+
attributes.correlationId = key;
|
|
135
|
+
}
|
|
136
|
+
const publishOptions = {};
|
|
137
|
+
if (this.options.enableOrdering && key) {
|
|
138
|
+
publishOptions.orderingKey = key;
|
|
139
|
+
}
|
|
140
|
+
await topic.publishMessage({
|
|
141
|
+
data: Buffer.from(JSON.stringify(envelope)),
|
|
142
|
+
attributes,
|
|
143
|
+
...publishOptions
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
export {
|
|
148
|
+
GcpPubSubTransport
|
|
149
|
+
};
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/GcpPubSubTransport.ts"],"sourcesContent":["import { PubSub, Topic, Subscription, Message } from \"@google-cloud/pubsub\";\nimport type {\n Transport,\n TransportSubscribeOptions,\n TransportPublishOptions,\n MessageEnvelope,\n BaseMessage,\n} from \"@saga-bus/core\";\nimport type { GcpPubSubTransportOptions } from \"./types.js\";\nimport { randomUUID } from \"crypto\";\n\n/**\n * GCP Pub/Sub transport for saga-bus.\n *\n * @example\n * ```typescript\n * import { GcpPubSubTransport } from \"@saga-bus/transport-gcp-pubsub\";\n *\n * const transport = new GcpPubSubTransport({\n * projectId: \"my-project\",\n * defaultTopic: \"saga-events\",\n * enableOrdering: true,\n * });\n *\n * await transport.start();\n * ```\n */\nexport class GcpPubSubTransport implements Transport {\n private pubsub: PubSub | null = null;\n private readonly options: Required<\n Pick<\n GcpPubSubTransportOptions,\n | \"subscriptionPrefix\"\n | \"enableOrdering\"\n | \"maxMessages\"\n | \"ackDeadlineSeconds\"\n | \"autoCreate\"\n | \"maxDeliveryAttempts\"\n >\n > &\n GcpPubSubTransportOptions;\n\n private readonly topics = new Map<string, Topic>();\n private readonly subscriptions: Subscription[] = [];\n private started = false;\n\n constructor(options: GcpPubSubTransportOptions) {\n if (!options.pubsub && !options.clientConfig && !options.projectId) {\n throw new Error(\n \"Either pubsub, clientConfig, or projectId must be provided\"\n );\n }\n\n this.options = {\n subscriptionPrefix: \"saga-bus-\",\n enableOrdering: false,\n maxMessages: 10,\n ackDeadlineSeconds: 60,\n autoCreate: true,\n maxDeliveryAttempts: 5,\n ...options,\n };\n }\n\n async start(): Promise<void> {\n if (this.started) return;\n\n if (this.options.pubsub) {\n this.pubsub = this.options.pubsub;\n } else {\n this.pubsub = new PubSub({\n projectId: this.options.projectId,\n ...this.options.clientConfig,\n });\n }\n\n this.started = true;\n }\n\n async stop(): Promise<void> {\n if (!this.started) return;\n\n // Close all subscriptions\n for (const sub of this.subscriptions) {\n await sub.close();\n }\n this.subscriptions.length = 0;\n this.topics.clear();\n\n // Close client if we created it\n if (!this.options.pubsub && this.pubsub) {\n await this.pubsub.close();\n }\n this.pubsub = null;\n this.started = false;\n }\n\n async subscribe<TMessage extends BaseMessage>(\n options: TransportSubscribeOptions,\n handler: (envelope: MessageEnvelope<TMessage>) => Promise<void>\n ): Promise<void> {\n if (!this.pubsub) throw new Error(\"Transport not started\");\n\n const { endpoint, group } = options;\n const topicName = endpoint;\n const subscriptionName =\n group ?? `${this.options.subscriptionPrefix}${endpoint}`;\n\n // Get or create topic\n let topic = this.topics.get(topicName);\n if (!topic) {\n topic = this.pubsub.topic(topicName);\n if (this.options.autoCreate) {\n const [exists] = await topic.exists();\n if (!exists) await topic.create();\n }\n this.topics.set(topicName, topic);\n }\n\n // Get or create subscription\n const subscription = topic.subscription(subscriptionName);\n if (this.options.autoCreate) {\n const [exists] = await subscription.exists();\n if (!exists) {\n await subscription.create({\n enableMessageOrdering: this.options.enableOrdering,\n ackDeadlineSeconds: this.options.ackDeadlineSeconds,\n });\n }\n }\n\n // Start listening\n subscription.on(\"message\", async (message: Message) => {\n try {\n const rawEnvelope = JSON.parse(message.data.toString());\n const envelope: MessageEnvelope<TMessage> = {\n id: rawEnvelope.id,\n type: rawEnvelope.type,\n payload: rawEnvelope.payload as TMessage,\n headers: rawEnvelope.headers,\n timestamp: new Date(rawEnvelope.timestamp),\n partitionKey: rawEnvelope.partitionKey,\n };\n await handler(envelope);\n message.ack();\n } catch (error) {\n console.error(\"[GcpPubSub] Message handler error:\", error);\n message.nack();\n }\n });\n\n subscription.on(\"error\", (error) => {\n console.error(\"[GcpPubSub] Subscription error:\", error);\n });\n\n this.subscriptions.push(subscription);\n }\n\n async publish<TMessage extends BaseMessage>(\n message: TMessage,\n options: TransportPublishOptions\n ): Promise<void> {\n if (!this.pubsub) throw new Error(\"Transport not started\");\n\n const { endpoint, key, headers = {}, delayMs } = options;\n\n // GCP Pub/Sub doesn't support delayed messages natively\n if (delayMs && delayMs > 0) {\n throw new Error(\n \"GCP Pub/Sub does not support delayed messages. \" +\n \"Use Cloud Scheduler or Cloud Tasks for delayed delivery.\"\n );\n }\n\n const topicName = endpoint ?? this.options.defaultTopic ?? message.type;\n\n // Get or create topic\n let topic = this.topics.get(topicName);\n if (!topic) {\n topic = this.pubsub.topic(topicName);\n if (this.options.autoCreate) {\n const [exists] = await topic.exists();\n if (!exists) await topic.create();\n }\n this.topics.set(topicName, topic);\n }\n\n // Create envelope\n const envelope: MessageEnvelope<TMessage> = {\n id: randomUUID(),\n type: message.type,\n payload: message,\n headers: headers as Record<string, string>,\n timestamp: new Date(),\n partitionKey: key,\n };\n\n // Publish\n const attributes: Record<string, string> = {\n messageType: message.type,\n messageId: envelope.id,\n };\n\n // Add custom headers as attributes\n for (const [k, v] of Object.entries(headers)) {\n if (typeof v === \"string\") {\n attributes[k] = v;\n }\n }\n\n if (key) {\n attributes.correlationId = key;\n }\n\n const publishOptions: { orderingKey?: string } = {};\n if (this.options.enableOrdering && key) {\n publishOptions.orderingKey = key;\n }\n\n await topic.publishMessage({\n data: Buffer.from(JSON.stringify(envelope)),\n attributes,\n ...publishOptions,\n });\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@saga-bus/transport-gcp-pubsub",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Google Cloud Pub/Sub transport for saga-bus",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@saga-bus/core": "0.1.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@google-cloud/pubsub": ">=4.0.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^22.10.1",
|
|
28
|
+
"eslint": "^9.16.0",
|
|
29
|
+
"tsup": "^8.3.5",
|
|
30
|
+
"typescript": "^5.7.2",
|
|
31
|
+
"vitest": "^2.1.8",
|
|
32
|
+
"@repo/eslint-config": "0.0.0",
|
|
33
|
+
"@repo/typescript-config": "0.0.0"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"build": "tsup",
|
|
37
|
+
"dev": "tsup --watch",
|
|
38
|
+
"lint": "eslint src/",
|
|
39
|
+
"check-types": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
42
|
+
}
|
|
43
|
+
}
|