@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/dist/index.js ADDED
@@ -0,0 +1,802 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('crypto');
4
+ var emmett = require('@event-driven-io/emmett');
5
+
6
+ // src/messageBus/serialization.ts
7
+ function transformDatesToMarkers(obj) {
8
+ if (obj instanceof Date) {
9
+ return {
10
+ __type: "Date",
11
+ value: obj.toISOString()
12
+ };
13
+ }
14
+ if (Array.isArray(obj)) {
15
+ return obj.map(transformDatesToMarkers);
16
+ }
17
+ if (obj !== null && typeof obj === "object") {
18
+ const result = {};
19
+ for (const [key, value] of Object.entries(obj)) {
20
+ result[key] = transformDatesToMarkers(value);
21
+ }
22
+ return result;
23
+ }
24
+ return obj;
25
+ }
26
+ function isDateMarker(value) {
27
+ return typeof value === "object" && value !== null && "__type" in value && value.__type === "Date" && "value" in value && typeof value.value === "string";
28
+ }
29
+ function dateReviver(_key, value) {
30
+ if (isDateMarker(value)) {
31
+ return new Date(value.value);
32
+ }
33
+ return value;
34
+ }
35
+ function getMessageKind(message) {
36
+ const typeStr = message.type.toLowerCase();
37
+ return typeStr.includes("command") ? "command" : "event";
38
+ }
39
+ function serialize(message) {
40
+ const envelope = {
41
+ type: message.type,
42
+ kind: getMessageKind(message),
43
+ data: transformDatesToMarkers(message.data),
44
+ metadata: "metadata" in message ? transformDatesToMarkers(message.metadata) : void 0,
45
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
46
+ messageId: crypto.randomUUID()
47
+ };
48
+ const json = JSON.stringify(envelope);
49
+ return Buffer.from(json);
50
+ }
51
+ function deserialize(buffer) {
52
+ try {
53
+ const json = buffer.toString("utf-8");
54
+ const envelope = JSON.parse(json, dateReviver);
55
+ const message = {
56
+ type: envelope.type,
57
+ data: envelope.data,
58
+ ...envelope.metadata ? { metadata: envelope.metadata } : {}
59
+ };
60
+ return message;
61
+ } catch (error) {
62
+ throw new Error(
63
+ `Failed to deserialize message: ${error instanceof Error ? error.message : String(error)}`
64
+ );
65
+ }
66
+ }
67
+ function attachMessageId(message, messageId) {
68
+ return {
69
+ ...message,
70
+ __messageId: messageId
71
+ };
72
+ }
73
+ function extractMessageId(message) {
74
+ return "__messageId" in message ? message.__messageId : void 0;
75
+ }
76
+
77
+ // src/messageBus/topicManager.ts
78
+ function getCommandTopicName(commandType, prefix = "emmett") {
79
+ return `${prefix}-cmd-${commandType}`;
80
+ }
81
+ function getEventTopicName(eventType, prefix = "emmett") {
82
+ return `${prefix}-evt-${eventType}`;
83
+ }
84
+ function getCommandSubscriptionName(commandType, instanceId, prefix = "emmett") {
85
+ return `${prefix}-cmd-${commandType}-${instanceId}`;
86
+ }
87
+ function getEventSubscriptionName(eventType, subscriptionId, prefix = "emmett") {
88
+ return `${prefix}-evt-${eventType}-${subscriptionId}`;
89
+ }
90
+ async function getOrCreateTopic(pubsub, topicName) {
91
+ const topic = pubsub.topic(topicName);
92
+ try {
93
+ const [exists] = await topic.exists();
94
+ if (!exists) {
95
+ try {
96
+ await topic.create();
97
+ } catch (createError) {
98
+ if (createError.code !== 6) {
99
+ throw createError;
100
+ }
101
+ }
102
+ }
103
+ return topic;
104
+ } catch (error) {
105
+ throw new Error(
106
+ `Failed to get or create topic ${topicName}: ${error instanceof Error ? error.message : String(error)}`
107
+ );
108
+ }
109
+ }
110
+ async function getOrCreateSubscription(topic, subscriptionName, options) {
111
+ const subscription = topic.subscription(subscriptionName);
112
+ try {
113
+ const [exists] = await subscription.exists();
114
+ if (!exists) {
115
+ const config = {
116
+ ...options?.ackDeadlineSeconds && {
117
+ ackDeadlineSeconds: options.ackDeadlineSeconds
118
+ },
119
+ ...options?.retryPolicy && {
120
+ retryPolicy: {
121
+ ...options.retryPolicy.minimumBackoff && {
122
+ minimumBackoff: options.retryPolicy.minimumBackoff
123
+ },
124
+ ...options.retryPolicy.maximumBackoff && {
125
+ maximumBackoff: options.retryPolicy.maximumBackoff
126
+ }
127
+ }
128
+ },
129
+ ...options?.deadLetterPolicy && {
130
+ deadLetterPolicy: {
131
+ ...options.deadLetterPolicy.deadLetterTopic && {
132
+ deadLetterTopic: options.deadLetterPolicy.deadLetterTopic
133
+ },
134
+ ...options.deadLetterPolicy.maxDeliveryAttempts && {
135
+ maxDeliveryAttempts: options.deadLetterPolicy.maxDeliveryAttempts
136
+ }
137
+ }
138
+ }
139
+ };
140
+ try {
141
+ await subscription.create(config);
142
+ } catch (createError) {
143
+ if (createError.code !== 6) {
144
+ throw createError;
145
+ }
146
+ }
147
+ }
148
+ return subscription;
149
+ } catch (error) {
150
+ throw new Error(
151
+ `Failed to get or create subscription ${subscriptionName}: ${error instanceof Error ? error.message : String(error)}`
152
+ );
153
+ }
154
+ }
155
+ async function deleteSubscription(subscription) {
156
+ try {
157
+ const [exists] = await subscription.exists();
158
+ if (exists) {
159
+ await subscription.delete();
160
+ }
161
+ } catch (error) {
162
+ console.warn(
163
+ `Failed to delete subscription: ${error instanceof Error ? error.message : String(error)}`
164
+ );
165
+ }
166
+ }
167
+ async function deleteSubscriptions(subscriptions) {
168
+ await Promise.all(subscriptions.map((sub) => deleteSubscription(sub)));
169
+ }
170
+ function generateUUID() {
171
+ return crypto.randomUUID();
172
+ }
173
+ function assertNotEmptyString(value, name) {
174
+ if (typeof value !== "string" || value.trim() === "") {
175
+ throw new Error(`${name} must be a non-empty string`);
176
+ }
177
+ }
178
+ function assertPositiveNumber(value, name) {
179
+ if (typeof value !== "number" || value <= 0 || Number.isNaN(value)) {
180
+ throw new Error(`${name} must be a positive number`);
181
+ }
182
+ }
183
+
184
+ // src/messageBus/scheduler.ts
185
+ function calculateScheduledTime(options) {
186
+ if (!options) {
187
+ return /* @__PURE__ */ new Date();
188
+ }
189
+ if ("afterInMs" in options) {
190
+ const now = /* @__PURE__ */ new Date();
191
+ return new Date(now.getTime() + options.afterInMs);
192
+ }
193
+ if ("at" in options) {
194
+ return options.at;
195
+ }
196
+ return /* @__PURE__ */ new Date();
197
+ }
198
+ function filterReadyMessages(pending, now) {
199
+ return pending.filter((msg) => msg.scheduledAt <= now);
200
+ }
201
+ var MessageScheduler = class {
202
+ pendingMessages = [];
203
+ useEmulator;
204
+ pubsub;
205
+ topicPrefix;
206
+ scheduledTopic;
207
+ constructor(config) {
208
+ this.useEmulator = config.useEmulator;
209
+ this.pubsub = config.pubsub;
210
+ this.scheduledTopic = config.scheduledTopic;
211
+ this.topicPrefix = config.topicPrefix ?? "emmett";
212
+ }
213
+ /**
214
+ * Schedule a message for future delivery
215
+ *
216
+ * In production mode: Publishes to PubSub with publishTime attribute
217
+ * In emulator mode: Stores in memory for later dequeue (emulator doesn't support scheduling)
218
+ *
219
+ * @param message - The message to schedule
220
+ * @param options - When to deliver the message
221
+ */
222
+ async schedule(message, options) {
223
+ const scheduledAt = calculateScheduledTime(options);
224
+ if (this.useEmulator) {
225
+ this.pendingMessages.push({
226
+ message,
227
+ options,
228
+ scheduledAt
229
+ });
230
+ } else {
231
+ await this.publishScheduledMessage(message, scheduledAt);
232
+ }
233
+ }
234
+ /**
235
+ * Dequeue ready scheduled messages (emulator mode only)
236
+ *
237
+ * Returns messages whose scheduled time has passed and removes them from pending queue
238
+ *
239
+ * @returns Array of scheduled messages ready for delivery
240
+ */
241
+ dequeue() {
242
+ if (!this.useEmulator) {
243
+ return [];
244
+ }
245
+ const now = /* @__PURE__ */ new Date();
246
+ const ready = filterReadyMessages(this.pendingMessages, now);
247
+ this.pendingMessages = this.pendingMessages.filter(
248
+ (msg) => msg.scheduledAt > now
249
+ );
250
+ return ready.map((info) => ({
251
+ message: info.message,
252
+ options: info.options
253
+ }));
254
+ }
255
+ /**
256
+ * Publish a scheduled message to PubSub (production mode)
257
+ *
258
+ * @param message - The message to publish
259
+ * @param scheduledAt - When the message should be delivered
260
+ */
261
+ async publishScheduledMessage(message, scheduledAt) {
262
+ try {
263
+ if (!this.scheduledTopic) {
264
+ const topicName = `${this.topicPrefix}-scheduled-messages`;
265
+ this.scheduledTopic = this.pubsub.topic(topicName);
266
+ const [exists] = await this.scheduledTopic.exists();
267
+ if (!exists) {
268
+ await this.scheduledTopic.create();
269
+ }
270
+ }
271
+ const buffer = serialize(message);
272
+ await this.scheduledTopic.publishMessage({
273
+ data: buffer,
274
+ attributes: {
275
+ messageType: message.type,
276
+ publishTime: scheduledAt.toISOString()
277
+ }
278
+ });
279
+ } catch (error) {
280
+ throw new Error(
281
+ `Failed to publish scheduled message ${message.type}: ${error instanceof Error ? error.message : String(error)}`
282
+ );
283
+ }
284
+ }
285
+ /**
286
+ * Get count of pending scheduled messages (emulator mode only)
287
+ *
288
+ * @returns Number of pending scheduled messages
289
+ */
290
+ getPendingCount() {
291
+ return this.useEmulator ? this.pendingMessages.length : 0;
292
+ }
293
+ /**
294
+ * Clear all pending scheduled messages (emulator mode only, useful for testing)
295
+ */
296
+ clearPending() {
297
+ if (this.useEmulator) {
298
+ this.pendingMessages = [];
299
+ }
300
+ }
301
+ };
302
+ function shouldRetry(error) {
303
+ if (!(error instanceof Error)) {
304
+ return true;
305
+ }
306
+ const errorMessage = error.message.toLowerCase();
307
+ if (errorMessage.includes("network") || errorMessage.includes("timeout") || errorMessage.includes("econnrefused") || errorMessage.includes("enotfound") || errorMessage.includes("unavailable")) {
308
+ return true;
309
+ }
310
+ if (error instanceof emmett.EmmettError) {
311
+ return false;
312
+ }
313
+ if (errorMessage.includes("validation") || errorMessage.includes("invalid") || errorMessage.includes("not found") || errorMessage.includes("already exists")) {
314
+ return false;
315
+ }
316
+ return true;
317
+ }
318
+ async function handleCommandMessage(message, handlers, commandType) {
319
+ try {
320
+ const commandHandlers = handlers.get(commandType);
321
+ if (!commandHandlers || commandHandlers.length === 0) {
322
+ throw new emmett.EmmettError(
323
+ `No handler registered for command ${commandType}!`
324
+ );
325
+ }
326
+ if (commandHandlers.length > 1) {
327
+ throw new emmett.EmmettError(
328
+ `Multiple handlers registered for command ${commandType}. Commands must have exactly one handler.`
329
+ );
330
+ }
331
+ const command = deserialize(message.data);
332
+ const handler = commandHandlers[0];
333
+ await handler(command);
334
+ return "ack";
335
+ } catch (error) {
336
+ console.error(
337
+ `Error handling command ${commandType}:`,
338
+ error instanceof Error ? error.message : String(error)
339
+ );
340
+ if (shouldRetry(error)) {
341
+ console.info(
342
+ `Nacking command ${commandType} for retry (delivery attempt: ${message.deliveryAttempt})`
343
+ );
344
+ return "nack";
345
+ } else {
346
+ console.warn(
347
+ `Acking command ${commandType} despite error (permanent failure)`
348
+ );
349
+ return "ack";
350
+ }
351
+ }
352
+ }
353
+ async function handleEventMessage(message, handlers, eventType) {
354
+ try {
355
+ const eventHandlers = handlers.get(eventType);
356
+ if (!eventHandlers || eventHandlers.length === 0) {
357
+ console.debug(`No handlers registered for event ${eventType}, skipping`);
358
+ return "ack";
359
+ }
360
+ const event = deserialize(message.data);
361
+ for (const handler of eventHandlers) {
362
+ try {
363
+ await handler(event);
364
+ } catch (error) {
365
+ console.error(
366
+ `Error in event handler for ${eventType}:`,
367
+ error instanceof Error ? error.message : String(error)
368
+ );
369
+ if (shouldRetry(error)) {
370
+ console.info(
371
+ `Nacking event ${eventType} for retry due to handler failure (delivery attempt: ${message.deliveryAttempt})`
372
+ );
373
+ return "nack";
374
+ }
375
+ console.warn(
376
+ `Continuing event ${eventType} processing despite handler error (permanent failure)`
377
+ );
378
+ }
379
+ }
380
+ return "ack";
381
+ } catch (error) {
382
+ console.error(
383
+ `Error handling event ${eventType}:`,
384
+ error instanceof Error ? error.message : String(error)
385
+ );
386
+ if (shouldRetry(error)) {
387
+ return "nack";
388
+ } else {
389
+ return "ack";
390
+ }
391
+ }
392
+ }
393
+ function createMessageListener(subscription, messageType, kind, handlers) {
394
+ subscription.on("message", async (message) => {
395
+ try {
396
+ const result = kind === "command" ? await handleCommandMessage(message, handlers, messageType) : await handleEventMessage(message, handlers, messageType);
397
+ if (result === "ack") {
398
+ message.ack();
399
+ } else {
400
+ message.nack();
401
+ }
402
+ } catch (error) {
403
+ console.error(
404
+ `Unexpected error in message listener for ${messageType}:`,
405
+ error instanceof Error ? error.message : String(error)
406
+ );
407
+ message.nack();
408
+ }
409
+ });
410
+ subscription.on("error", (error) => {
411
+ console.error(
412
+ `Subscription error for ${messageType}:`,
413
+ error instanceof Error ? error.message : String(error)
414
+ );
415
+ });
416
+ }
417
+ function determineMessageKindFallback(messageType) {
418
+ const typeStr = messageType.toLowerCase();
419
+ return typeStr.includes("command") ? "command" : "event";
420
+ }
421
+ function getPubSubMessageBus(config) {
422
+ const instanceId = config.instanceId ?? generateUUID();
423
+ const topicPrefix = config.topicPrefix ?? "emmett";
424
+ const autoCreateResources = config.autoCreateResources ?? true;
425
+ const cleanupOnClose = config.cleanupOnClose ?? false;
426
+ const closePubSubClient = config.closePubSubClient;
427
+ const handlers = /* @__PURE__ */ new Map();
428
+ const subscriptionHandlers = /* @__PURE__ */ new Map();
429
+ const eventSubscriptionIds = /* @__PURE__ */ new Map();
430
+ const commandTypes = /* @__PURE__ */ new Set();
431
+ const eventTypes = /* @__PURE__ */ new Set();
432
+ const subscriptions = [];
433
+ const scheduler = new MessageScheduler({
434
+ useEmulator: config.useEmulator ?? false,
435
+ pubsub: config.pubsub,
436
+ topicPrefix
437
+ });
438
+ let started = false;
439
+ function determineMessageKind(messageType) {
440
+ if (commandTypes.has(messageType)) {
441
+ return "command";
442
+ }
443
+ if (eventTypes.has(messageType)) {
444
+ return "event";
445
+ }
446
+ return determineMessageKindFallback(messageType);
447
+ }
448
+ async function createSubscriptionForType(messageType, kind, subscriptionId) {
449
+ const topicName = kind === "command" ? getCommandTopicName(messageType, topicPrefix) : getEventTopicName(messageType, topicPrefix);
450
+ const topic = await getOrCreateTopic(config.pubsub, topicName);
451
+ const subName = kind === "command" ? getCommandSubscriptionName(messageType, instanceId, topicPrefix) : getEventSubscriptionName(
452
+ messageType,
453
+ subscriptionId ?? instanceId,
454
+ topicPrefix
455
+ );
456
+ const subscription = await getOrCreateSubscription(
457
+ topic,
458
+ subName,
459
+ config.subscriptionOptions
460
+ );
461
+ if (kind === "event" && subscriptionId) {
462
+ const handler = subscriptionHandlers.get(subscriptionId);
463
+ if (handler) {
464
+ const singleHandlerMap = /* @__PURE__ */ new Map();
465
+ singleHandlerMap.set(messageType, [handler]);
466
+ createMessageListener(subscription, messageType, kind, singleHandlerMap);
467
+ }
468
+ } else {
469
+ createMessageListener(subscription, messageType, kind, handlers);
470
+ }
471
+ subscriptions.push({
472
+ topic,
473
+ subscription,
474
+ messageType,
475
+ kind
476
+ });
477
+ }
478
+ async function publishMessage(message, kind) {
479
+ const topicName = kind === "command" ? getCommandTopicName(message.type, topicPrefix) : getEventTopicName(message.type, topicPrefix);
480
+ try {
481
+ const topic = config.pubsub.topic(topicName);
482
+ if (!autoCreateResources) {
483
+ const [exists] = await topic.exists();
484
+ if (!exists) {
485
+ throw new Error(
486
+ `Topic ${topicName} does not exist and autoCreateResources is disabled`
487
+ );
488
+ }
489
+ } else {
490
+ const [exists] = await topic.exists();
491
+ if (!exists) {
492
+ await topic.create();
493
+ }
494
+ }
495
+ const buffer = serialize(message);
496
+ await topic.publishMessage({
497
+ data: buffer,
498
+ attributes: {
499
+ messageType: message.type,
500
+ messageKind: kind
501
+ }
502
+ });
503
+ } catch (error) {
504
+ throw new Error(
505
+ `Failed to publish ${kind} ${message.type} to topic ${topicName}: ${error instanceof Error ? error.message : String(error)}`
506
+ );
507
+ }
508
+ }
509
+ return {
510
+ // ===== MessageBus Interface =====
511
+ /**
512
+ * Send a command to the message bus
513
+ *
514
+ * Commands are routed to exactly one handler via PubSub topics
515
+ *
516
+ * @param command - The command to send
517
+ */
518
+ async send(command) {
519
+ await publishMessage(command, "command");
520
+ },
521
+ /**
522
+ * Publish an event to the message bus
523
+ *
524
+ * Events are delivered to all registered subscribers via PubSub topics
525
+ *
526
+ * @param event - The event to publish
527
+ */
528
+ async publish(event) {
529
+ await publishMessage(event, "event");
530
+ },
531
+ /**
532
+ * Schedule a message for future delivery
533
+ *
534
+ * In production mode: Uses PubSub native scheduling
535
+ * In emulator mode: Stores in memory (emulator doesn't support scheduling)
536
+ *
537
+ * @param message - The message to schedule
538
+ * @param when - When to deliver the message (afterInMs or at)
539
+ */
540
+ schedule(message, when) {
541
+ scheduler.schedule(message, when);
542
+ },
543
+ // ===== CommandProcessor Interface =====
544
+ /**
545
+ * Register a command handler
546
+ *
547
+ * Commands must have exactly one handler. Attempting to register multiple
548
+ * handlers for the same command will throw an EmmettError.
549
+ *
550
+ * @param commandHandler - The handler function
551
+ * @param commandTypes - Command types this handler processes
552
+ * @throws EmmettError if a handler is already registered for any command type
553
+ *
554
+ * @example
555
+ * ```typescript
556
+ * messageBus.handle(
557
+ * async (command: AddProductItemCommand) => {
558
+ * // Handle command
559
+ * },
560
+ * 'AddProductItem'
561
+ * );
562
+ * ```
563
+ */
564
+ handle(commandHandler, ...commandTypeNames) {
565
+ for (const commandType of commandTypeNames) {
566
+ if (handlers.has(commandType)) {
567
+ throw new emmett.EmmettError(
568
+ `Handler already registered for command ${commandType}. Commands must have exactly one handler.`
569
+ );
570
+ }
571
+ commandTypes.add(commandType);
572
+ handlers.set(commandType, [
573
+ commandHandler
574
+ ]);
575
+ if (started) {
576
+ createSubscriptionForType(commandType, "command").catch((error) => {
577
+ console.error(
578
+ `Failed to create subscription for command ${commandType}:`,
579
+ error instanceof Error ? error.message : String(error)
580
+ );
581
+ });
582
+ }
583
+ }
584
+ },
585
+ // ===== EventSubscription Interface =====
586
+ /**
587
+ * Subscribe to events
588
+ *
589
+ * Events can have multiple subscribers. Each subscription gets its own
590
+ * PubSub subscription to ensure all handlers receive all events.
591
+ *
592
+ * @param eventHandler - The handler function
593
+ * @param eventTypes - Event types to subscribe to
594
+ *
595
+ * @example
596
+ * ```typescript
597
+ * messageBus.subscribe(
598
+ * async (event: ProductItemAddedEvent) => {
599
+ * // Handle event
600
+ * },
601
+ * 'ProductItemAdded'
602
+ * );
603
+ * ```
604
+ */
605
+ subscribe(eventHandler, ...eventTypeNames) {
606
+ for (const eventType of eventTypeNames) {
607
+ eventTypes.add(eventType);
608
+ const subscriptionId = generateUUID();
609
+ subscriptionHandlers.set(
610
+ subscriptionId,
611
+ eventHandler
612
+ );
613
+ const existing = handlers.get(eventType) ?? [];
614
+ handlers.set(eventType, [
615
+ ...existing,
616
+ eventHandler
617
+ ]);
618
+ const existingIds = eventSubscriptionIds.get(eventType) ?? [];
619
+ eventSubscriptionIds.set(eventType, [...existingIds, subscriptionId]);
620
+ if (started) {
621
+ createSubscriptionForType(eventType, "event", subscriptionId).catch(
622
+ (error) => {
623
+ console.error(
624
+ `Failed to create subscription for event ${eventType}:`,
625
+ error instanceof Error ? error.message : String(error)
626
+ );
627
+ }
628
+ );
629
+ }
630
+ }
631
+ },
632
+ // ===== ScheduledMessageProcessor Interface =====
633
+ /**
634
+ * Dequeue scheduled messages that are ready for delivery
635
+ *
636
+ * Only used in emulator mode. In production, PubSub handles scheduling.
637
+ *
638
+ * @returns Array of scheduled messages ready for delivery
639
+ *
640
+ * @example
641
+ * ```typescript
642
+ * // In emulator mode, periodically call dequeue
643
+ * setInterval(() => {
644
+ * const ready = messageBus.dequeue();
645
+ * for (const { message } of ready) {
646
+ * // Process message
647
+ * }
648
+ * }, 1000);
649
+ * ```
650
+ */
651
+ dequeue() {
652
+ return scheduler.dequeue();
653
+ },
654
+ // ===== PubSubMessageBusLifecycle Interface =====
655
+ /**
656
+ * Start the message bus
657
+ *
658
+ * Creates topics and subscriptions for all registered handlers and begins
659
+ * listening for messages.
660
+ *
661
+ * This method is idempotent - calling it multiple times is safe.
662
+ *
663
+ * @throws Error if topic/subscription creation fails
664
+ *
665
+ * @example
666
+ * ```typescript
667
+ * // Register all handlers first
668
+ * messageBus.handle(commandHandler, 'MyCommand');
669
+ * messageBus.subscribe(eventHandler, 'MyEvent');
670
+ *
671
+ * // Then start
672
+ * await messageBus.start();
673
+ * ```
674
+ */
675
+ async start() {
676
+ if (started) {
677
+ console.debug("Message bus already started, skipping");
678
+ return;
679
+ }
680
+ console.info("Starting PubSub message bus...");
681
+ try {
682
+ const subscriptionPromises = [];
683
+ for (const [messageType] of handlers.entries()) {
684
+ const kind = determineMessageKind(messageType);
685
+ if (kind === "command") {
686
+ subscriptionPromises.push(
687
+ createSubscriptionForType(messageType, "command")
688
+ );
689
+ } else {
690
+ const subIds = eventSubscriptionIds.get(messageType) ?? [
691
+ instanceId
692
+ ];
693
+ for (const subId of subIds) {
694
+ subscriptionPromises.push(
695
+ createSubscriptionForType(messageType, "event", subId)
696
+ );
697
+ }
698
+ }
699
+ }
700
+ await Promise.all(subscriptionPromises);
701
+ started = true;
702
+ console.info(
703
+ `PubSub message bus started with ${subscriptions.length} subscription(s)`
704
+ );
705
+ } catch (error) {
706
+ throw new Error(
707
+ `Failed to start message bus: ${error instanceof Error ? error.message : String(error)}`
708
+ );
709
+ }
710
+ },
711
+ /**
712
+ * Close the message bus gracefully
713
+ *
714
+ * Stops accepting new messages, waits for in-flight messages to complete,
715
+ * optionally cleans up subscriptions, and closes the PubSub client.
716
+ *
717
+ * @throws Error if cleanup fails
718
+ *
719
+ * @example
720
+ * ```typescript
721
+ * // Graceful shutdown
722
+ * process.on('SIGTERM', async () => {
723
+ * await messageBus.close();
724
+ * process.exit(0);
725
+ * });
726
+ * ```
727
+ */
728
+ async close() {
729
+ console.info("Closing PubSub message bus...");
730
+ try {
731
+ if (started) {
732
+ for (const { subscription } of subscriptions) {
733
+ subscription.removeAllListeners("message");
734
+ subscription.removeAllListeners("error");
735
+ }
736
+ const timeout = 3e4;
737
+ const waitStart = Date.now();
738
+ const closePromises = subscriptions.map(
739
+ ({ subscription }) => subscription.close()
740
+ );
741
+ await Promise.race([
742
+ Promise.all(closePromises),
743
+ new Promise((resolve) => setTimeout(resolve, timeout))
744
+ ]);
745
+ const waitTime = Date.now() - waitStart;
746
+ if (waitTime >= timeout) {
747
+ console.warn(
748
+ `Timeout waiting for in-flight messages after ${timeout}ms`
749
+ );
750
+ }
751
+ if (cleanupOnClose) {
752
+ console.info("Cleaning up subscriptions...");
753
+ await deleteSubscriptions(subscriptions.map((s) => s.subscription));
754
+ }
755
+ started = false;
756
+ }
757
+ if (closePubSubClient !== false) {
758
+ await config.pubsub.close();
759
+ }
760
+ console.info("PubSub message bus closed");
761
+ } catch (error) {
762
+ throw new Error(
763
+ `Failed to close message bus: ${error instanceof Error ? error.message : String(error)}`
764
+ );
765
+ }
766
+ },
767
+ /**
768
+ * Check if the message bus is started
769
+ *
770
+ * @returns true if the message bus is started and ready to process messages
771
+ */
772
+ isStarted() {
773
+ return started;
774
+ }
775
+ };
776
+ }
777
+
778
+ exports.MessageScheduler = MessageScheduler;
779
+ exports.assertNotEmptyString = assertNotEmptyString;
780
+ exports.assertPositiveNumber = assertPositiveNumber;
781
+ exports.attachMessageId = attachMessageId;
782
+ exports.calculateScheduledTime = calculateScheduledTime;
783
+ exports.createMessageListener = createMessageListener;
784
+ exports.deleteSubscription = deleteSubscription;
785
+ exports.deleteSubscriptions = deleteSubscriptions;
786
+ exports.deserialize = deserialize;
787
+ exports.extractMessageId = extractMessageId;
788
+ exports.filterReadyMessages = filterReadyMessages;
789
+ exports.generateUUID = generateUUID;
790
+ exports.getCommandSubscriptionName = getCommandSubscriptionName;
791
+ exports.getCommandTopicName = getCommandTopicName;
792
+ exports.getEventSubscriptionName = getEventSubscriptionName;
793
+ exports.getEventTopicName = getEventTopicName;
794
+ exports.getOrCreateSubscription = getOrCreateSubscription;
795
+ exports.getOrCreateTopic = getOrCreateTopic;
796
+ exports.getPubSubMessageBus = getPubSubMessageBus;
797
+ exports.handleCommandMessage = handleCommandMessage;
798
+ exports.handleEventMessage = handleEventMessage;
799
+ exports.serialize = serialize;
800
+ exports.shouldRetry = shouldRetry;
801
+ //# sourceMappingURL=index.js.map
802
+ //# sourceMappingURL=index.js.map