@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 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
+ [![npm version](https://img.shields.io/npm/v/@emmett-community/emmett-google-pubsub.svg)](https://www.npmjs.com/package/@emmett-community/emmett-google-pubsub)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)