@emmett-community/emmett-google-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 +438 -0
- package/dist/index.d.mts +413 -0
- package/dist/index.d.ts +413 -0
- package/dist/index.js +802 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +778 -0
- package/dist/index.mjs.map +1 -0
- package/dist/testing/index.d.mts +2 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/index.mjs +3 -0
- package/dist/testing/index.mjs.map +1 -0
- package/package.json +94 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Emmett Community
|
|
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,438 @@
|
|
|
1
|
+
# @emmett-community/emmett-google-pubsub
|
|
2
|
+
|
|
3
|
+
Google Cloud Pub/Sub message bus implementation for [Emmett](https://event-driven-io.github.io/emmett/), the Node.js event sourcing framework.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@emmett-community/emmett-google-pubsub)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **Distributed Message Bus** - Scale command/event handling across multiple instances
|
|
11
|
+
- **Type-Safe** - Full TypeScript support with comprehensive types
|
|
12
|
+
- **Automatic Topic Management** - Auto-creates topics and subscriptions
|
|
13
|
+
- **Message Scheduling** - Schedule commands/events for future execution
|
|
14
|
+
- **Error Handling** - Built-in retry logic and dead letter queue support
|
|
15
|
+
- **Emulator Support** - Local development with PubSub emulator
|
|
16
|
+
- **Testing Utilities** - Helper functions for easy testing
|
|
17
|
+
- **Emmett Compatible** - Drop-in replacement for in-memory message bus
|
|
18
|
+
- **Producer-Only Mode** - Use without starting consumers
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @emmett-community/emmett-google-pubsub @google-cloud/pubsub
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Quick Start
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { PubSub } from '@google-cloud/pubsub';
|
|
30
|
+
import { getPubSubMessageBus } from '@emmett-community/emmett-google-pubsub';
|
|
31
|
+
|
|
32
|
+
// Initialize PubSub client
|
|
33
|
+
const pubsub = new PubSub({ projectId: 'your-project-id' });
|
|
34
|
+
|
|
35
|
+
// Create message bus
|
|
36
|
+
const messageBus = getPubSubMessageBus({ pubsub });
|
|
37
|
+
|
|
38
|
+
// Register command handler
|
|
39
|
+
messageBus.handle(async (command) => {
|
|
40
|
+
console.log('Processing:', command.type, command.data);
|
|
41
|
+
}, 'AddProductItem');
|
|
42
|
+
|
|
43
|
+
// Subscribe to events
|
|
44
|
+
messageBus.subscribe(async (event) => {
|
|
45
|
+
console.log('Received:', event.type, event.data);
|
|
46
|
+
}, 'ProductItemAdded');
|
|
47
|
+
|
|
48
|
+
// Start listening
|
|
49
|
+
await messageBus.start();
|
|
50
|
+
|
|
51
|
+
// Send commands and publish events
|
|
52
|
+
await messageBus.send({
|
|
53
|
+
type: 'AddProductItem',
|
|
54
|
+
data: { productId: '123', quantity: 2 },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await messageBus.publish({
|
|
58
|
+
type: 'ProductItemAdded',
|
|
59
|
+
data: { productId: '123', quantity: 2 },
|
|
60
|
+
});
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## How It Works
|
|
64
|
+
|
|
65
|
+
### Topic/Subscription Strategy
|
|
66
|
+
|
|
67
|
+
The message bus uses a **topic-per-type** strategy:
|
|
68
|
+
|
|
69
|
+
```
|
|
70
|
+
Commands (1-to-1):
|
|
71
|
+
Topic: {prefix}-cmd-{CommandType}
|
|
72
|
+
Subscription: {prefix}-cmd-{CommandType}-{instanceId}
|
|
73
|
+
→ Only ONE handler processes each command
|
|
74
|
+
|
|
75
|
+
Events (1-to-many):
|
|
76
|
+
Topic: {prefix}-evt-{EventType}
|
|
77
|
+
Subscription: {prefix}-evt-{EventType}-{subscriberId}
|
|
78
|
+
→ ALL subscribers receive each event
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Example topic names:**
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
emmett-cmd-AddProductItem
|
|
85
|
+
emmett-cmd-AddProductItem-instance-abc123
|
|
86
|
+
|
|
87
|
+
emmett-evt-ProductItemAdded
|
|
88
|
+
emmett-evt-ProductItemAdded-subscriber-xyz789
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Message Lifecycle
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
1. REGISTRATION 2. STARTUP 3. RUNTIME 4. SHUTDOWN
|
|
95
|
+
handle() start() send/publish close()
|
|
96
|
+
subscribe() → Create topics → Route messages → Stop listeners
|
|
97
|
+
→ Create subs → Execute handlers → Cleanup
|
|
98
|
+
→ Attach listeners → Ack/Nack
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Producer-Only Mode
|
|
102
|
+
|
|
103
|
+
You can use the message bus to only produce messages without consuming:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
const messageBus = getPubSubMessageBus({ pubsub });
|
|
107
|
+
|
|
108
|
+
// No handlers, no start() needed
|
|
109
|
+
await messageBus.send({ type: 'MyCommand', data: {} });
|
|
110
|
+
await messageBus.publish({ type: 'MyEvent', data: {} });
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
114
|
+
|
|
115
|
+
### `getPubSubMessageBus(config)`
|
|
116
|
+
|
|
117
|
+
Creates a message bus instance.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const messageBus = getPubSubMessageBus({
|
|
121
|
+
pubsub, // Required: PubSub client
|
|
122
|
+
topicPrefix: 'myapp', // Topic name prefix (default: "emmett")
|
|
123
|
+
instanceId: 'worker-1', // Instance ID (default: auto-generated)
|
|
124
|
+
useEmulator: true, // Emulator mode (default: false)
|
|
125
|
+
autoCreateResources: true, // Auto-create topics/subs (default: true)
|
|
126
|
+
cleanupOnClose: false, // Delete subs on close (default: false)
|
|
127
|
+
closePubSubClient: true, // Close PubSub on close (default: true)
|
|
128
|
+
subscriptionOptions: { // Subscription config
|
|
129
|
+
ackDeadlineSeconds: 60,
|
|
130
|
+
retryPolicy: {
|
|
131
|
+
minimumBackoff: { seconds: 10 },
|
|
132
|
+
maximumBackoff: { seconds: 600 },
|
|
133
|
+
},
|
|
134
|
+
deadLetterPolicy: {
|
|
135
|
+
deadLetterTopic: 'projects/.../topics/dead-letters',
|
|
136
|
+
maxDeliveryAttempts: 5,
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Methods
|
|
143
|
+
|
|
144
|
+
| Method | Description |
|
|
145
|
+
|--------|-------------|
|
|
146
|
+
| `send(command)` | Send a command (1-to-1) |
|
|
147
|
+
| `publish(event)` | Publish an event (1-to-many) |
|
|
148
|
+
| `handle(handler, ...types)` | Register command handler |
|
|
149
|
+
| `subscribe(handler, ...types)` | Subscribe to events |
|
|
150
|
+
| `schedule(message, options)` | Schedule for future delivery |
|
|
151
|
+
| `dequeue()` | Get scheduled messages (emulator only) |
|
|
152
|
+
| `start()` | Start listening for messages |
|
|
153
|
+
| `close()` | Graceful shutdown |
|
|
154
|
+
| `isStarted()` | Check if running |
|
|
155
|
+
|
|
156
|
+
See [docs/API.md](./docs/API.md) for complete API documentation.
|
|
157
|
+
|
|
158
|
+
## Configuration
|
|
159
|
+
|
|
160
|
+
### Basic Configuration
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
const messageBus = getPubSubMessageBus({
|
|
164
|
+
pubsub: new PubSub({ projectId: 'my-project' }),
|
|
165
|
+
topicPrefix: 'orders',
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Emulator Configuration
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// Set environment variable
|
|
173
|
+
process.env.PUBSUB_EMULATOR_HOST = 'localhost:8085';
|
|
174
|
+
|
|
175
|
+
const pubsub = new PubSub({ projectId: 'demo-project' });
|
|
176
|
+
const messageBus = getPubSubMessageBus({
|
|
177
|
+
pubsub,
|
|
178
|
+
useEmulator: true, // Enables in-memory scheduling
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Production Configuration
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
const pubsub = new PubSub({
|
|
186
|
+
projectId: process.env.GCP_PROJECT_ID,
|
|
187
|
+
// Uses Application Default Credentials or Workload Identity
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
const messageBus = getPubSubMessageBus({
|
|
191
|
+
pubsub,
|
|
192
|
+
topicPrefix: 'prod-myapp',
|
|
193
|
+
subscriptionOptions: {
|
|
194
|
+
ackDeadlineSeconds: 120,
|
|
195
|
+
retryPolicy: {
|
|
196
|
+
minimumBackoff: { seconds: 5 },
|
|
197
|
+
maximumBackoff: { seconds: 300 },
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Testing
|
|
204
|
+
|
|
205
|
+
### Testing Utilities
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { PubSub } from '@google-cloud/pubsub';
|
|
209
|
+
import { getPubSubMessageBus } from '@emmett-community/emmett-google-pubsub';
|
|
210
|
+
|
|
211
|
+
describe('My Tests', () => {
|
|
212
|
+
let pubsub: PubSub;
|
|
213
|
+
let messageBus: ReturnType<typeof getPubSubMessageBus>;
|
|
214
|
+
|
|
215
|
+
beforeAll(() => {
|
|
216
|
+
pubsub = new PubSub({ projectId: 'test-project' });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
messageBus = getPubSubMessageBus({
|
|
221
|
+
pubsub,
|
|
222
|
+
useEmulator: true,
|
|
223
|
+
topicPrefix: `test-${Date.now()}`,
|
|
224
|
+
cleanupOnClose: true,
|
|
225
|
+
closePubSubClient: false,
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
afterEach(async () => {
|
|
230
|
+
await messageBus.close();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
afterAll(async () => {
|
|
234
|
+
await pubsub.close();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle commands', async () => {
|
|
238
|
+
const received: unknown[] = [];
|
|
239
|
+
|
|
240
|
+
messageBus.handle(async (cmd) => {
|
|
241
|
+
received.push(cmd);
|
|
242
|
+
}, 'TestCommand');
|
|
243
|
+
|
|
244
|
+
await messageBus.start();
|
|
245
|
+
|
|
246
|
+
await messageBus.send({
|
|
247
|
+
type: 'TestCommand',
|
|
248
|
+
data: { value: 42 },
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// Wait for async delivery
|
|
252
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
253
|
+
|
|
254
|
+
expect(received).toHaveLength(1);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Running Tests
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
# Start PubSub emulator
|
|
263
|
+
gcloud beta emulators pubsub start --project=test-project
|
|
264
|
+
|
|
265
|
+
# Or use Docker
|
|
266
|
+
docker run -p 8085:8085 gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators \
|
|
267
|
+
gcloud beta emulators pubsub start --host-port=0.0.0.0:8085
|
|
268
|
+
|
|
269
|
+
# Set environment variable
|
|
270
|
+
export PUBSUB_EMULATOR_HOST=localhost:8085
|
|
271
|
+
|
|
272
|
+
# Run tests
|
|
273
|
+
npm test
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Examples
|
|
277
|
+
|
|
278
|
+
### Complete Shopping Cart Example
|
|
279
|
+
|
|
280
|
+
See [examples/shopping-cart](./examples/shopping-cart) for a full application including:
|
|
281
|
+
|
|
282
|
+
- Event-sourced shopping cart with Firestore
|
|
283
|
+
- Express.js API with OpenAPI spec
|
|
284
|
+
- Docker Compose setup with all emulators
|
|
285
|
+
- Unit, integration, and E2E tests
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
cd examples/shopping-cart
|
|
289
|
+
docker-compose up
|
|
290
|
+
|
|
291
|
+
# API: http://localhost:3000
|
|
292
|
+
# Firebase UI: http://localhost:4000
|
|
293
|
+
# PubSub UI: http://localhost:4001
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
### Multiple Event Subscribers
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// Analytics service
|
|
300
|
+
messageBus.subscribe(async (event) => {
|
|
301
|
+
await analytics.track(event);
|
|
302
|
+
}, 'OrderCreated');
|
|
303
|
+
|
|
304
|
+
// Notification service
|
|
305
|
+
messageBus.subscribe(async (event) => {
|
|
306
|
+
await email.sendConfirmation(event.data.customerId);
|
|
307
|
+
}, 'OrderCreated');
|
|
308
|
+
|
|
309
|
+
// Inventory service
|
|
310
|
+
messageBus.subscribe(async (event) => {
|
|
311
|
+
await inventory.reserve(event.data.items);
|
|
312
|
+
}, 'OrderCreated');
|
|
313
|
+
|
|
314
|
+
// All three receive every OrderCreated event
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Scheduled Messages
|
|
318
|
+
|
|
319
|
+
```typescript
|
|
320
|
+
// Schedule for future
|
|
321
|
+
messageBus.schedule(
|
|
322
|
+
{ type: 'SendReminder', data: { userId: '123' } },
|
|
323
|
+
{ afterInMs: 24 * 60 * 60 * 1000 } // 24 hours
|
|
324
|
+
);
|
|
325
|
+
|
|
326
|
+
// Schedule for specific time
|
|
327
|
+
messageBus.schedule(
|
|
328
|
+
{ type: 'SendReminder', data: { userId: '123' } },
|
|
329
|
+
{ at: new Date('2024-12-25T10:00:00Z') }
|
|
330
|
+
);
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
See [docs/EXAMPLES.md](./docs/EXAMPLES.md) for more examples.
|
|
334
|
+
|
|
335
|
+
## Architecture
|
|
336
|
+
|
|
337
|
+
### Message Format
|
|
338
|
+
|
|
339
|
+
Messages are wrapped in an envelope for transport:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
interface PubSubMessageEnvelope {
|
|
343
|
+
type: string; // Message type name
|
|
344
|
+
kind: 'command' | 'event';
|
|
345
|
+
data: unknown; // Serialized data
|
|
346
|
+
metadata?: unknown; // Optional metadata
|
|
347
|
+
timestamp: string; // ISO 8601
|
|
348
|
+
messageId: string; // UUID for idempotency
|
|
349
|
+
}
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Date Serialization
|
|
353
|
+
|
|
354
|
+
JavaScript `Date` objects are preserved through serialization:
|
|
355
|
+
|
|
356
|
+
```typescript
|
|
357
|
+
// Original
|
|
358
|
+
{ createdAt: new Date('2024-01-15T10:00:00Z') }
|
|
359
|
+
|
|
360
|
+
// Serialized
|
|
361
|
+
{ createdAt: { __type: 'Date', value: '2024-01-15T10:00:00.000Z' } }
|
|
362
|
+
|
|
363
|
+
// Deserialized (restored as Date object)
|
|
364
|
+
{ createdAt: Date('2024-01-15T10:00:00Z') }
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Error Handling
|
|
368
|
+
|
|
369
|
+
| Scenario | Behavior |
|
|
370
|
+
|----------|----------|
|
|
371
|
+
| Handler succeeds | Message acknowledged |
|
|
372
|
+
| Transient error | Message nack'd, retried with backoff |
|
|
373
|
+
| Permanent error | Message ack'd, logged |
|
|
374
|
+
| No handler | Message nack'd for retry |
|
|
375
|
+
|
|
376
|
+
See [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) for design decisions.
|
|
377
|
+
|
|
378
|
+
## Compatibility
|
|
379
|
+
|
|
380
|
+
- **Node.js**: >= 18.0.0
|
|
381
|
+
- **Emmett**: ^0.39.0
|
|
382
|
+
- **@google-cloud/pubsub**: ^4.8.0
|
|
383
|
+
|
|
384
|
+
## Contributing
|
|
385
|
+
|
|
386
|
+
Contributions are welcome! Please:
|
|
387
|
+
|
|
388
|
+
1. Fork the repository
|
|
389
|
+
2. Create a feature branch
|
|
390
|
+
3. Add tests for new functionality
|
|
391
|
+
4. Ensure all tests pass
|
|
392
|
+
5. Submit a pull request
|
|
393
|
+
|
|
394
|
+
## Development
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
# Install dependencies
|
|
398
|
+
npm install
|
|
399
|
+
|
|
400
|
+
# Build
|
|
401
|
+
npm run build
|
|
402
|
+
|
|
403
|
+
# Run tests
|
|
404
|
+
npm test
|
|
405
|
+
|
|
406
|
+
# Run unit tests only
|
|
407
|
+
npm run test:unit
|
|
408
|
+
|
|
409
|
+
# Run integration tests (requires emulator)
|
|
410
|
+
npm run test:int
|
|
411
|
+
|
|
412
|
+
# Lint
|
|
413
|
+
npm run lint
|
|
414
|
+
|
|
415
|
+
# Format
|
|
416
|
+
npm run format
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## License
|
|
420
|
+
|
|
421
|
+
MIT
|
|
422
|
+
|
|
423
|
+
## Resources
|
|
424
|
+
|
|
425
|
+
- [Emmett Documentation](https://event-driven-io.github.io/emmett/)
|
|
426
|
+
- [Google Cloud Pub/Sub Docs](https://cloud.google.com/pubsub/docs)
|
|
427
|
+
- [GitHub Repository](https://github.com/emmett-community/emmett-google-pubsub)
|
|
428
|
+
|
|
429
|
+
## Support
|
|
430
|
+
|
|
431
|
+
- **Issues**: [GitHub Issues](https://github.com/emmett-community/emmett-google-pubsub/issues)
|
|
432
|
+
- **Discussions**: [GitHub Discussions](https://github.com/emmett-community/emmett-google-pubsub/discussions)
|
|
433
|
+
- **Emmett Discord**: [Join Discord](https://discord.gg/fTpqUTMmVa)
|
|
434
|
+
|
|
435
|
+
## Acknowledgments
|
|
436
|
+
|
|
437
|
+
- Built for the [Emmett](https://event-driven-io.github.io/emmett/) framework by [Oskar Dudycz](https://github.com/oskardudycz)
|
|
438
|
+
- Part of the [Emmett Community](https://github.com/emmett-community)
|