@l-etabli/events 0.4.2 → 0.5.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.
@@ -22,6 +22,7 @@ __export(InMemoryEventBus_exports, {
22
22
  });
23
23
  module.exports = __toCommonJS(InMemoryEventBus_exports);
24
24
  var import_createNewEvent = require("../../createNewEvent.ts");
25
+ var import_subscriptions = require("../../subscriptions.ts");
25
26
  const createInMemoryEventBus = (withUow, options = {}) => {
26
27
  const maxRetries = options.maxRetries ?? 3;
27
28
  const createNewEvent = (0, import_createNewEvent.makeCreateNewEvent)({
@@ -48,7 +49,7 @@ const createInMemoryEventBus = (withUow, options = {}) => {
48
49
  const lastPublication = event.publications.reduce(
49
50
  (latest, current) => current.publishedAt > latest.publishedAt ? current : latest
50
51
  );
51
- const failedSubscriptionIds = lastPublication.failures.map(
52
+ const failedSubscriptionIds = (lastPublication.failures ?? []).map(
52
53
  (failure) => failure.subscriptionId
53
54
  );
54
55
  return allSubscriptionIds.filter(
@@ -63,8 +64,7 @@ const createInMemoryEventBus = (withUow, options = {}) => {
63
64
  if (!callbacksBySubscriptionSlug) {
64
65
  event.publications.push({
65
66
  publishedAt,
66
- publishedSubscribers: [],
67
- failures: []
67
+ publishedSubscribers: []
68
68
  });
69
69
  event.status = "published";
70
70
  await withUow(async (uow) => {
@@ -95,7 +95,7 @@ const createInMemoryEventBus = (withUow, options = {}) => {
95
95
  publishedSubscribers: subscriptionIdsToPublish.map(
96
96
  (id) => id
97
97
  ),
98
- failures
98
+ ...failures.length > 0 && { failures }
99
99
  }
100
100
  ];
101
101
  if (failures.length === 0) {
@@ -119,7 +119,20 @@ const createInMemoryEventBus = (withUow, options = {}) => {
119
119
  }
120
120
  }
121
121
  };
122
- return { eventBus, createNewEvent };
122
+ const defineSubscriptions = (subscriptions2) => subscriptions2;
123
+ const subscribeAll = (subscriptions2) => {
124
+ (0, import_subscriptions.subscribeByTopic)(eventBus, subscriptions2);
125
+ };
126
+ const subscribeGlobal = (subscriptions2, config) => {
127
+ (0, import_subscriptions.subscribeGlobalToTopics)(eventBus, subscriptions2, config);
128
+ };
129
+ return {
130
+ eventBus,
131
+ createNewEvent,
132
+ defineSubscriptions,
133
+ subscribeAll,
134
+ subscribeGlobal
135
+ };
123
136
  };
124
137
  // Annotate the CommonJS export names for ESM import in node:
125
138
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventBus.ts"],"sourcesContent":["import { makeCreateNewEvent } from \"../../createNewEvent.ts\";\nimport type { EventBus } from \"../../ports/EventBus.ts\";\nimport type { WithEventsUow } from \"../../ports/EventRepository.ts\";\nimport type {\n DefaultContext,\n EventId,\n EventPublication,\n GenericEvent,\n SubscriptionId,\n} from \"../../types.ts\";\n\ntype SubscriptionsForTopic = Record<\n string,\n (event: GenericEvent<string, unknown, DefaultContext>) => Promise<void>\n>;\n\ntype CreateInMemoryEventBusOptions = {\n maxRetries?: number;\n getNow?: () => Date;\n generateId?: () => EventId;\n};\n\nexport const createInMemoryEventBus = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n withUow: WithEventsUow<Event>,\n options: CreateInMemoryEventBusOptions = {},\n) => {\n const maxRetries = options.maxRetries ?? 3;\n const createNewEvent = makeCreateNewEvent<Event>({\n getNow: options.getNow,\n generateId: options.generateId,\n });\n const subscriptions: Partial<Record<string, SubscriptionsForTopic>> = {};\n\n const executeCallback = async (\n event: Event,\n subscriptionId: string,\n callback: (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>,\n ): Promise<\n { subscriptionId: string; errorMessage: string; stack?: string } | undefined\n > => {\n try {\n await callback(event);\n } catch (error) {\n return {\n subscriptionId,\n errorMessage: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n };\n }\n };\n\n const getSubscriptionIdsToPublish = (\n event: Event,\n callbacksBySubscriptionId: SubscriptionsForTopic,\n ): string[] => {\n const allSubscriptionIds = Object.keys(callbacksBySubscriptionId);\n\n if (event.publications.length === 0 || event.status === \"to-republish\") {\n return allSubscriptionIds;\n }\n\n const lastPublication = event.publications.reduce((latest, current) =>\n current.publishedAt > latest.publishedAt ? current : latest,\n );\n const failedSubscriptionIds = lastPublication.failures.map(\n (failure) => failure.subscriptionId,\n );\n\n return allSubscriptionIds.filter((id) =>\n failedSubscriptionIds.includes(id),\n );\n };\n\n const eventBus: EventBus<Event> = {\n publish: async (event) => {\n const publishedAt = new Date();\n const topic = event.topic;\n\n const callbacksBySubscriptionSlug = subscriptions[topic];\n\n if (!callbacksBySubscriptionSlug) {\n event.publications.push({\n publishedAt,\n publishedSubscribers: [],\n failures: [],\n });\n event.status = \"published\";\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n return;\n }\n\n const subscriptionIdsToPublish = getSubscriptionIdsToPublish(\n event,\n callbacksBySubscriptionSlug,\n );\n\n const failuresOrUndefined = await Promise.all(\n subscriptionIdsToPublish.map((subscriptionId) =>\n executeCallback(\n event,\n subscriptionId,\n callbacksBySubscriptionSlug[subscriptionId],\n ),\n ),\n );\n\n const failures = failuresOrUndefined.filter(\n (\n f,\n ): f is {\n subscriptionId: string;\n errorMessage: string;\n stack?: string;\n } => f !== undefined,\n );\n\n const publications: EventPublication[] = [\n ...event.publications,\n {\n publishedAt,\n publishedSubscribers: subscriptionIdsToPublish.map(\n (id) => id as SubscriptionId,\n ),\n failures,\n },\n ];\n\n if (failures.length === 0) {\n event.status = \"published\";\n } else {\n const wasMaxNumberOfErrorsReached = publications.length >= maxRetries;\n event.status = wasMaxNumberOfErrorsReached\n ? \"quarantined\"\n : \"failed-but-will-retry\";\n }\n\n event.publications = publications;\n\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n },\n\n subscribe: ({ topic, subscriptionId, callBack }) => {\n if (!subscriptions[topic]) {\n subscriptions[topic] = {};\n }\n\n const subscriptionsForTopic = subscriptions[topic];\n if (subscriptionsForTopic) {\n subscriptionsForTopic[subscriptionId] = callBack as (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>;\n }\n },\n };\n\n return { eventBus, createNewEvent };\n};\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventBus.ts"],"sourcesContent":["import { makeCreateNewEvent } from \"../../createNewEvent.ts\";\nimport type { EventBus } from \"../../ports/EventBus.ts\";\nimport type { WithEventsUow } from \"../../ports/EventRepository.ts\";\nimport {\n type GlobalSubscriberConfig,\n subscribeByTopic,\n subscribeGlobalToTopics,\n type TopicSubscriptions,\n} from \"../../subscriptions.ts\";\nimport type {\n DefaultContext,\n EventId,\n EventPublication,\n GenericEvent,\n SubscriptionId,\n} from \"../../types.ts\";\n\ntype SubscriptionsForTopic = Record<\n string,\n (event: GenericEvent<string, unknown, DefaultContext>) => Promise<void>\n>;\n\ntype CreateInMemoryEventBusOptions = {\n maxRetries?: number;\n getNow?: () => Date;\n generateId?: () => EventId;\n};\n\nexport const createInMemoryEventBus = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n withUow: WithEventsUow<Event>,\n options: CreateInMemoryEventBusOptions = {},\n) => {\n const maxRetries = options.maxRetries ?? 3;\n const createNewEvent = makeCreateNewEvent<Event>({\n getNow: options.getNow,\n generateId: options.generateId,\n });\n const subscriptions: Partial<Record<string, SubscriptionsForTopic>> = {};\n\n const executeCallback = async (\n event: Event,\n subscriptionId: string,\n callback: (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>,\n ): Promise<\n { subscriptionId: string; errorMessage: string; stack?: string } | undefined\n > => {\n try {\n await callback(event);\n } catch (error) {\n return {\n subscriptionId,\n errorMessage: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n };\n }\n };\n\n const getSubscriptionIdsToPublish = (\n event: Event,\n callbacksBySubscriptionId: SubscriptionsForTopic,\n ): string[] => {\n const allSubscriptionIds = Object.keys(callbacksBySubscriptionId);\n\n if (event.publications.length === 0 || event.status === \"to-republish\") {\n return allSubscriptionIds;\n }\n\n const lastPublication = event.publications.reduce((latest, current) =>\n current.publishedAt > latest.publishedAt ? current : latest,\n );\n const failedSubscriptionIds = (lastPublication.failures ?? []).map(\n (failure) => failure.subscriptionId,\n );\n\n return allSubscriptionIds.filter((id) =>\n failedSubscriptionIds.includes(id),\n );\n };\n\n const eventBus: EventBus<Event> = {\n publish: async (event) => {\n const publishedAt = new Date();\n const topic = event.topic;\n\n const callbacksBySubscriptionSlug = subscriptions[topic];\n\n if (!callbacksBySubscriptionSlug) {\n event.publications.push({\n publishedAt,\n publishedSubscribers: [],\n });\n event.status = \"published\";\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n return;\n }\n\n const subscriptionIdsToPublish = getSubscriptionIdsToPublish(\n event,\n callbacksBySubscriptionSlug,\n );\n\n const failuresOrUndefined = await Promise.all(\n subscriptionIdsToPublish.map((subscriptionId) =>\n executeCallback(\n event,\n subscriptionId,\n callbacksBySubscriptionSlug[subscriptionId],\n ),\n ),\n );\n\n const failures = failuresOrUndefined.filter(\n (\n f,\n ): f is {\n subscriptionId: string;\n errorMessage: string;\n stack?: string;\n } => f !== undefined,\n );\n\n const publications: EventPublication[] = [\n ...event.publications,\n {\n publishedAt,\n publishedSubscribers: subscriptionIdsToPublish.map(\n (id) => id as SubscriptionId,\n ),\n ...(failures.length > 0 && { failures }),\n },\n ];\n\n if (failures.length === 0) {\n event.status = \"published\";\n } else {\n const wasMaxNumberOfErrorsReached = publications.length >= maxRetries;\n event.status = wasMaxNumberOfErrorsReached\n ? \"quarantined\"\n : \"failed-but-will-retry\";\n }\n\n event.publications = publications;\n\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n },\n\n subscribe: ({ topic, subscriptionId, callBack }) => {\n if (!subscriptions[topic]) {\n subscriptions[topic] = {};\n }\n\n const subscriptionsForTopic = subscriptions[topic];\n if (subscriptionsForTopic) {\n subscriptionsForTopic[subscriptionId] = callBack as (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>;\n }\n },\n };\n\n /**\n * Identity function for type inference when defining subscription maps.\n * Ensures all topics are covered and handlers have correct payload types.\n *\n * @example\n * ```typescript\n * const subscriptions = defineSubscriptions({\n * OrderCreated: [{ subscriptionId: \"notify\", handler: async (e) => {...} }],\n * OrderShipped: [], // Required even if empty\n * });\n * ```\n */\n const defineSubscriptions = (\n subscriptions: TopicSubscriptions<Event>,\n ): TopicSubscriptions<Event> => subscriptions;\n\n /**\n * Subscribe all handlers from a topic subscription map to this event bus.\n *\n * @example\n * ```typescript\n * const subscriptions = defineSubscriptions({...});\n * subscribeAll(subscriptions);\n * ```\n */\n const subscribeAll = (subscriptions: TopicSubscriptions<Event>): void => {\n subscribeByTopic(eventBus, subscriptions);\n };\n\n /**\n * Subscribe a global handler to multiple topics with optional filtering.\n *\n * @example\n * ```typescript\n * subscribeGlobal(subscriptions, {\n * subscriptionId: \"audit-log\",\n * handler: async (event) => auditLog.record(event),\n * filter: { exclude: [\"NotificationAdded\"] },\n * });\n * ```\n */\n const subscribeGlobal = (\n subscriptions: TopicSubscriptions<Event>,\n config: GlobalSubscriberConfig<Event>,\n ): void => {\n subscribeGlobalToTopics(eventBus, subscriptions, config);\n };\n\n return {\n eventBus,\n createNewEvent,\n defineSubscriptions,\n subscribeAll,\n subscribeGlobal,\n };\n};\n"],"mappings":"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","names":["subscriptions"]}
@@ -1,6 +1,7 @@
1
1
  import { CreateNewEvent } from '../../createNewEvent.cjs';
2
2
  import { EventBus } from '../../ports/EventBus.cjs';
3
3
  import { WithEventsUow } from '../../ports/EventRepository.cjs';
4
+ import { TopicSubscriptions, GlobalSubscriberConfig } from '../../subscriptions.cjs';
4
5
  import { GenericEvent, DefaultContext, EventId } from '../../types.cjs';
5
6
 
6
7
  type CreateInMemoryEventBusOptions = {
@@ -11,6 +12,9 @@ type CreateInMemoryEventBusOptions = {
11
12
  declare const createInMemoryEventBus: <Event extends GenericEvent<string, unknown, DefaultContext>>(withUow: WithEventsUow<Event>, options?: CreateInMemoryEventBusOptions) => {
12
13
  eventBus: EventBus<Event>;
13
14
  createNewEvent: CreateNewEvent<Event>;
15
+ defineSubscriptions: (subscriptions: TopicSubscriptions<Event>) => TopicSubscriptions<Event>;
16
+ subscribeAll: (subscriptions: TopicSubscriptions<Event>) => void;
17
+ subscribeGlobal: (subscriptions: TopicSubscriptions<Event>, config: GlobalSubscriberConfig<Event>) => void;
14
18
  };
15
19
 
16
20
  export { createInMemoryEventBus };
@@ -1,6 +1,7 @@
1
1
  import { CreateNewEvent } from '../../createNewEvent.js';
2
2
  import { EventBus } from '../../ports/EventBus.js';
3
3
  import { WithEventsUow } from '../../ports/EventRepository.js';
4
+ import { TopicSubscriptions, GlobalSubscriberConfig } from '../../subscriptions.js';
4
5
  import { GenericEvent, DefaultContext, EventId } from '../../types.js';
5
6
 
6
7
  type CreateInMemoryEventBusOptions = {
@@ -11,6 +12,9 @@ type CreateInMemoryEventBusOptions = {
11
12
  declare const createInMemoryEventBus: <Event extends GenericEvent<string, unknown, DefaultContext>>(withUow: WithEventsUow<Event>, options?: CreateInMemoryEventBusOptions) => {
12
13
  eventBus: EventBus<Event>;
13
14
  createNewEvent: CreateNewEvent<Event>;
15
+ defineSubscriptions: (subscriptions: TopicSubscriptions<Event>) => TopicSubscriptions<Event>;
16
+ subscribeAll: (subscriptions: TopicSubscriptions<Event>) => void;
17
+ subscribeGlobal: (subscriptions: TopicSubscriptions<Event>, config: GlobalSubscriberConfig<Event>) => void;
14
18
  };
15
19
 
16
20
  export { createInMemoryEventBus };
@@ -1,4 +1,8 @@
1
1
  import { makeCreateNewEvent } from "../../createNewEvent.mjs";
2
+ import {
3
+ subscribeByTopic,
4
+ subscribeGlobalToTopics
5
+ } from "../../subscriptions.mjs";
2
6
  const createInMemoryEventBus = (withUow, options = {}) => {
3
7
  const maxRetries = options.maxRetries ?? 3;
4
8
  const createNewEvent = makeCreateNewEvent({
@@ -25,7 +29,7 @@ const createInMemoryEventBus = (withUow, options = {}) => {
25
29
  const lastPublication = event.publications.reduce(
26
30
  (latest, current) => current.publishedAt > latest.publishedAt ? current : latest
27
31
  );
28
- const failedSubscriptionIds = lastPublication.failures.map(
32
+ const failedSubscriptionIds = (lastPublication.failures ?? []).map(
29
33
  (failure) => failure.subscriptionId
30
34
  );
31
35
  return allSubscriptionIds.filter(
@@ -40,8 +44,7 @@ const createInMemoryEventBus = (withUow, options = {}) => {
40
44
  if (!callbacksBySubscriptionSlug) {
41
45
  event.publications.push({
42
46
  publishedAt,
43
- publishedSubscribers: [],
44
- failures: []
47
+ publishedSubscribers: []
45
48
  });
46
49
  event.status = "published";
47
50
  await withUow(async (uow) => {
@@ -72,7 +75,7 @@ const createInMemoryEventBus = (withUow, options = {}) => {
72
75
  publishedSubscribers: subscriptionIdsToPublish.map(
73
76
  (id) => id
74
77
  ),
75
- failures
78
+ ...failures.length > 0 && { failures }
76
79
  }
77
80
  ];
78
81
  if (failures.length === 0) {
@@ -96,7 +99,20 @@ const createInMemoryEventBus = (withUow, options = {}) => {
96
99
  }
97
100
  }
98
101
  };
99
- return { eventBus, createNewEvent };
102
+ const defineSubscriptions = (subscriptions2) => subscriptions2;
103
+ const subscribeAll = (subscriptions2) => {
104
+ subscribeByTopic(eventBus, subscriptions2);
105
+ };
106
+ const subscribeGlobal = (subscriptions2, config) => {
107
+ subscribeGlobalToTopics(eventBus, subscriptions2, config);
108
+ };
109
+ return {
110
+ eventBus,
111
+ createNewEvent,
112
+ defineSubscriptions,
113
+ subscribeAll,
114
+ subscribeGlobal
115
+ };
100
116
  };
101
117
  export {
102
118
  createInMemoryEventBus
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventBus.ts"],"sourcesContent":["import { makeCreateNewEvent } from '../../createNewEvent.mjs';\nimport type { EventBus } from '../../ports/EventBus.mjs';\nimport type { WithEventsUow } from '../../ports/EventRepository.mjs';\nimport type {\n DefaultContext,\n EventId,\n EventPublication,\n GenericEvent,\n SubscriptionId,\n} from '../../types.mjs';\n\ntype SubscriptionsForTopic = Record<\n string,\n (event: GenericEvent<string, unknown, DefaultContext>) => Promise<void>\n>;\n\ntype CreateInMemoryEventBusOptions = {\n maxRetries?: number;\n getNow?: () => Date;\n generateId?: () => EventId;\n};\n\nexport const createInMemoryEventBus = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n withUow: WithEventsUow<Event>,\n options: CreateInMemoryEventBusOptions = {},\n) => {\n const maxRetries = options.maxRetries ?? 3;\n const createNewEvent = makeCreateNewEvent<Event>({\n getNow: options.getNow,\n generateId: options.generateId,\n });\n const subscriptions: Partial<Record<string, SubscriptionsForTopic>> = {};\n\n const executeCallback = async (\n event: Event,\n subscriptionId: string,\n callback: (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>,\n ): Promise<\n { subscriptionId: string; errorMessage: string; stack?: string } | undefined\n > => {\n try {\n await callback(event);\n } catch (error) {\n return {\n subscriptionId,\n errorMessage: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n };\n }\n };\n\n const getSubscriptionIdsToPublish = (\n event: Event,\n callbacksBySubscriptionId: SubscriptionsForTopic,\n ): string[] => {\n const allSubscriptionIds = Object.keys(callbacksBySubscriptionId);\n\n if (event.publications.length === 0 || event.status === \"to-republish\") {\n return allSubscriptionIds;\n }\n\n const lastPublication = event.publications.reduce((latest, current) =>\n current.publishedAt > latest.publishedAt ? current : latest,\n );\n const failedSubscriptionIds = lastPublication.failures.map(\n (failure) => failure.subscriptionId,\n );\n\n return allSubscriptionIds.filter((id) =>\n failedSubscriptionIds.includes(id),\n );\n };\n\n const eventBus: EventBus<Event> = {\n publish: async (event) => {\n const publishedAt = new Date();\n const topic = event.topic;\n\n const callbacksBySubscriptionSlug = subscriptions[topic];\n\n if (!callbacksBySubscriptionSlug) {\n event.publications.push({\n publishedAt,\n publishedSubscribers: [],\n failures: [],\n });\n event.status = \"published\";\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n return;\n }\n\n const subscriptionIdsToPublish = getSubscriptionIdsToPublish(\n event,\n callbacksBySubscriptionSlug,\n );\n\n const failuresOrUndefined = await Promise.all(\n subscriptionIdsToPublish.map((subscriptionId) =>\n executeCallback(\n event,\n subscriptionId,\n callbacksBySubscriptionSlug[subscriptionId],\n ),\n ),\n );\n\n const failures = failuresOrUndefined.filter(\n (\n f,\n ): f is {\n subscriptionId: string;\n errorMessage: string;\n stack?: string;\n } => f !== undefined,\n );\n\n const publications: EventPublication[] = [\n ...event.publications,\n {\n publishedAt,\n publishedSubscribers: subscriptionIdsToPublish.map(\n (id) => id as SubscriptionId,\n ),\n failures,\n },\n ];\n\n if (failures.length === 0) {\n event.status = \"published\";\n } else {\n const wasMaxNumberOfErrorsReached = publications.length >= maxRetries;\n event.status = wasMaxNumberOfErrorsReached\n ? \"quarantined\"\n : \"failed-but-will-retry\";\n }\n\n event.publications = publications;\n\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n },\n\n subscribe: ({ topic, subscriptionId, callBack }) => {\n if (!subscriptions[topic]) {\n subscriptions[topic] = {};\n }\n\n const subscriptionsForTopic = subscriptions[topic];\n if (subscriptionsForTopic) {\n subscriptionsForTopic[subscriptionId] = callBack as (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>;\n }\n },\n };\n\n return { eventBus, createNewEvent };\n};\n"],"mappings":"AAAA,SAAS,0BAA0B;AAsB5B,MAAM,yBAAyB,CAGpC,SACA,UAAyC,CAAC,MACvC;AACH,QAAM,aAAa,QAAQ,cAAc;AACzC,QAAM,iBAAiB,mBAA0B;AAAA,IAC/C,QAAQ,QAAQ;AAAA,IAChB,YAAY,QAAQ;AAAA,EACtB,CAAC;AACD,QAAM,gBAAgE,CAAC;AAEvE,QAAM,kBAAkB,OACtB,OACA,gBACA,aAKG;AACH,QAAI;AACF,YAAM,SAAS,KAAK;AAAA,IACtB,SAAS,OAAO;AACd,aAAO;AAAA,QACL;AAAA,QACA,cAAc,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QACnE,OAAO,iBAAiB,QAAQ,MAAM,QAAQ;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,8BAA8B,CAClC,OACA,8BACa;AACb,UAAM,qBAAqB,OAAO,KAAK,yBAAyB;AAEhE,QAAI,MAAM,aAAa,WAAW,KAAK,MAAM,WAAW,gBAAgB;AACtE,aAAO;AAAA,IACT;AAEA,UAAM,kBAAkB,MAAM,aAAa;AAAA,MAAO,CAAC,QAAQ,YACzD,QAAQ,cAAc,OAAO,cAAc,UAAU;AAAA,IACvD;AACA,UAAM,wBAAwB,gBAAgB,SAAS;AAAA,MACrD,CAAC,YAAY,QAAQ;AAAA,IACvB;AAEA,WAAO,mBAAmB;AAAA,MAAO,CAAC,OAChC,sBAAsB,SAAS,EAAE;AAAA,IACnC;AAAA,EACF;AAEA,QAAM,WAA4B;AAAA,IAChC,SAAS,OAAO,UAAU;AACxB,YAAM,cAAc,oBAAI,KAAK;AAC7B,YAAM,QAAQ,MAAM;AAEpB,YAAM,8BAA8B,cAAc,KAAK;AAEvD,UAAI,CAAC,6BAA6B;AAChC,cAAM,aAAa,KAAK;AAAA,UACtB;AAAA,UACA,sBAAsB,CAAC;AAAA,UACvB,UAAU,CAAC;AAAA,QACb,CAAC;AACD,cAAM,SAAS;AACf,cAAM,QAAQ,OAAO,QAAQ;AAC3B,gBAAM,IAAI,gBAAgB,KAAK,KAAK;AAAA,QACtC,CAAC;AACD;AAAA,MACF;AAEA,YAAM,2BAA2B;AAAA,QAC/B;AAAA,QACA;AAAA,MACF;AAEA,YAAM,sBAAsB,MAAM,QAAQ;AAAA,QACxC,yBAAyB;AAAA,UAAI,CAAC,mBAC5B;AAAA,YACE;AAAA,YACA;AAAA,YACA,4BAA4B,cAAc;AAAA,UAC5C;AAAA,QACF;AAAA,MACF;AAEA,YAAM,WAAW,oBAAoB;AAAA,QACnC,CACE,MAKG,MAAM;AAAA,MACb;AAEA,YAAM,eAAmC;AAAA,QACvC,GAAG,MAAM;AAAA,QACT;AAAA,UACE;AAAA,UACA,sBAAsB,yBAAyB;AAAA,YAC7C,CAAC,OAAO;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAEA,UAAI,SAAS,WAAW,GAAG;AACzB,cAAM,SAAS;AAAA,MACjB,OAAO;AACL,cAAM,8BAA8B,aAAa,UAAU;AAC3D,cAAM,SAAS,8BACX,gBACA;AAAA,MACN;AAEA,YAAM,eAAe;AAErB,YAAM,QAAQ,OAAO,QAAQ;AAC3B,cAAM,IAAI,gBAAgB,KAAK,KAAK;AAAA,MACtC,CAAC;AAAA,IACH;AAAA,IAEA,WAAW,CAAC,EAAE,OAAO,gBAAgB,SAAS,MAAM;AAClD,UAAI,CAAC,cAAc,KAAK,GAAG;AACzB,sBAAc,KAAK,IAAI,CAAC;AAAA,MAC1B;AAEA,YAAM,wBAAwB,cAAc,KAAK;AACjD,UAAI,uBAAuB;AACzB,8BAAsB,cAAc,IAAI;AAAA,MAG1C;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,UAAU,eAAe;AACpC;","names":[]}
1
+ {"version":3,"sources":["../../../src/adapters/in-memory/InMemoryEventBus.ts"],"sourcesContent":["import { makeCreateNewEvent } from '../../createNewEvent.mjs';\nimport type { EventBus } from '../../ports/EventBus.mjs';\nimport type { WithEventsUow } from '../../ports/EventRepository.mjs';\nimport {\n type GlobalSubscriberConfig,\n subscribeByTopic,\n subscribeGlobalToTopics,\n type TopicSubscriptions,\n} from '../../subscriptions.mjs';\nimport type {\n DefaultContext,\n EventId,\n EventPublication,\n GenericEvent,\n SubscriptionId,\n} from '../../types.mjs';\n\ntype SubscriptionsForTopic = Record<\n string,\n (event: GenericEvent<string, unknown, DefaultContext>) => Promise<void>\n>;\n\ntype CreateInMemoryEventBusOptions = {\n maxRetries?: number;\n getNow?: () => Date;\n generateId?: () => EventId;\n};\n\nexport const createInMemoryEventBus = <\n Event extends GenericEvent<string, unknown, DefaultContext>,\n>(\n withUow: WithEventsUow<Event>,\n options: CreateInMemoryEventBusOptions = {},\n) => {\n const maxRetries = options.maxRetries ?? 3;\n const createNewEvent = makeCreateNewEvent<Event>({\n getNow: options.getNow,\n generateId: options.generateId,\n });\n const subscriptions: Partial<Record<string, SubscriptionsForTopic>> = {};\n\n const executeCallback = async (\n event: Event,\n subscriptionId: string,\n callback: (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>,\n ): Promise<\n { subscriptionId: string; errorMessage: string; stack?: string } | undefined\n > => {\n try {\n await callback(event);\n } catch (error) {\n return {\n subscriptionId,\n errorMessage: error instanceof Error ? error.message : String(error),\n stack: error instanceof Error ? error.stack : undefined,\n };\n }\n };\n\n const getSubscriptionIdsToPublish = (\n event: Event,\n callbacksBySubscriptionId: SubscriptionsForTopic,\n ): string[] => {\n const allSubscriptionIds = Object.keys(callbacksBySubscriptionId);\n\n if (event.publications.length === 0 || event.status === \"to-republish\") {\n return allSubscriptionIds;\n }\n\n const lastPublication = event.publications.reduce((latest, current) =>\n current.publishedAt > latest.publishedAt ? current : latest,\n );\n const failedSubscriptionIds = (lastPublication.failures ?? []).map(\n (failure) => failure.subscriptionId,\n );\n\n return allSubscriptionIds.filter((id) =>\n failedSubscriptionIds.includes(id),\n );\n };\n\n const eventBus: EventBus<Event> = {\n publish: async (event) => {\n const publishedAt = new Date();\n const topic = event.topic;\n\n const callbacksBySubscriptionSlug = subscriptions[topic];\n\n if (!callbacksBySubscriptionSlug) {\n event.publications.push({\n publishedAt,\n publishedSubscribers: [],\n });\n event.status = \"published\";\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n return;\n }\n\n const subscriptionIdsToPublish = getSubscriptionIdsToPublish(\n event,\n callbacksBySubscriptionSlug,\n );\n\n const failuresOrUndefined = await Promise.all(\n subscriptionIdsToPublish.map((subscriptionId) =>\n executeCallback(\n event,\n subscriptionId,\n callbacksBySubscriptionSlug[subscriptionId],\n ),\n ),\n );\n\n const failures = failuresOrUndefined.filter(\n (\n f,\n ): f is {\n subscriptionId: string;\n errorMessage: string;\n stack?: string;\n } => f !== undefined,\n );\n\n const publications: EventPublication[] = [\n ...event.publications,\n {\n publishedAt,\n publishedSubscribers: subscriptionIdsToPublish.map(\n (id) => id as SubscriptionId,\n ),\n ...(failures.length > 0 && { failures }),\n },\n ];\n\n if (failures.length === 0) {\n event.status = \"published\";\n } else {\n const wasMaxNumberOfErrorsReached = publications.length >= maxRetries;\n event.status = wasMaxNumberOfErrorsReached\n ? \"quarantined\"\n : \"failed-but-will-retry\";\n }\n\n event.publications = publications;\n\n await withUow(async (uow) => {\n await uow.eventRepository.save(event);\n });\n },\n\n subscribe: ({ topic, subscriptionId, callBack }) => {\n if (!subscriptions[topic]) {\n subscriptions[topic] = {};\n }\n\n const subscriptionsForTopic = subscriptions[topic];\n if (subscriptionsForTopic) {\n subscriptionsForTopic[subscriptionId] = callBack as (\n event: GenericEvent<string, unknown, DefaultContext>,\n ) => Promise<void>;\n }\n },\n };\n\n /**\n * Identity function for type inference when defining subscription maps.\n * Ensures all topics are covered and handlers have correct payload types.\n *\n * @example\n * ```typescript\n * const subscriptions = defineSubscriptions({\n * OrderCreated: [{ subscriptionId: \"notify\", handler: async (e) => {...} }],\n * OrderShipped: [], // Required even if empty\n * });\n * ```\n */\n const defineSubscriptions = (\n subscriptions: TopicSubscriptions<Event>,\n ): TopicSubscriptions<Event> => subscriptions;\n\n /**\n * Subscribe all handlers from a topic subscription map to this event bus.\n *\n * @example\n * ```typescript\n * const subscriptions = defineSubscriptions({...});\n * subscribeAll(subscriptions);\n * ```\n */\n const subscribeAll = (subscriptions: TopicSubscriptions<Event>): void => {\n subscribeByTopic(eventBus, subscriptions);\n };\n\n /**\n * Subscribe a global handler to multiple topics with optional filtering.\n *\n * @example\n * ```typescript\n * subscribeGlobal(subscriptions, {\n * subscriptionId: \"audit-log\",\n * handler: async (event) => auditLog.record(event),\n * filter: { exclude: [\"NotificationAdded\"] },\n * });\n * ```\n */\n const subscribeGlobal = (\n subscriptions: TopicSubscriptions<Event>,\n config: GlobalSubscriberConfig<Event>,\n ): void => {\n subscribeGlobalToTopics(eventBus, subscriptions, config);\n };\n\n return {\n eventBus,\n createNewEvent,\n defineSubscriptions,\n subscribeAll,\n subscribeGlobal,\n };\n};\n"],"mappings":"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","names":["subscriptions"]}
@@ -7,6 +7,7 @@ export { createInMemoryEventBus } from './InMemoryEventBus.cjs';
7
7
  export { createInMemoryEventQueries } from './InMemoryEventQueries.cjs';
8
8
  import '../../createNewEvent.cjs';
9
9
  import '../../ports/EventBus.cjs';
10
+ import '../../subscriptions.cjs';
10
11
 
11
12
  declare const createInMemoryEventRepositoryAndQueries: <Event extends GenericEvent<string, unknown, DefaultContext>>() => {
12
13
  eventRepository: EventRepository<Event>;
@@ -7,6 +7,7 @@ export { createInMemoryEventBus } from './InMemoryEventBus.js';
7
7
  export { createInMemoryEventQueries } from './InMemoryEventQueries.js';
8
8
  import '../../createNewEvent.js';
9
9
  import '../../ports/EventBus.js';
10
+ import '../../subscriptions.js';
10
11
 
11
12
  declare const createInMemoryEventRepositoryAndQueries: <Event extends GenericEvent<string, unknown, DefaultContext>>() => {
12
13
  eventRepository: EventRepository<Event>;
package/dist/index.cjs CHANGED
@@ -18,10 +18,12 @@ module.exports = __toCommonJS(index_exports);
18
18
  __reExport(index_exports, require("./adapters/in-memory/index.ts"), module.exports);
19
19
  __reExport(index_exports, require("./createEventCrawler.ts"), module.exports);
20
20
  __reExport(index_exports, require("./createNewEvent.ts"), module.exports);
21
+ __reExport(index_exports, require("./subscriptions.ts"), module.exports);
21
22
  // Annotate the CommonJS export names for ESM import in node:
22
23
  0 && (module.exports = {
23
24
  ...require("./adapters/in-memory/index.ts"),
24
25
  ...require("./createEventCrawler.ts"),
25
- ...require("./createNewEvent.ts")
26
+ ...require("./createNewEvent.ts"),
27
+ ...require("./subscriptions.ts")
26
28
  });
27
29
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./adapters/in-memory/index.ts\";\nexport * from \"./createEventCrawler.ts\";\nexport * from \"./createNewEvent.ts\";\nexport type * from \"./ports/EventBus.ts\";\nexport type * from \"./ports/EventQueries.ts\";\nexport type * from \"./ports/EventRepository.ts\";\nexport type * from \"./types.ts\";\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0CAAd;AACA,0BAAc,oCADd;AAEA,0BAAc,gCAFd;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from \"./adapters/in-memory/index.ts\";\nexport * from \"./createEventCrawler.ts\";\nexport * from \"./createNewEvent.ts\";\nexport type * from \"./ports/EventBus.ts\";\nexport type * from \"./ports/EventQueries.ts\";\nexport type * from \"./ports/EventRepository.ts\";\nexport * from \"./subscriptions.ts\";\nexport type * from \"./types.ts\";\n"],"mappings":";;;;;;;;;;;;;;;AAAA;AAAA;AAAA,0BAAc,0CAAd;AACA,0BAAc,oCADd;AAEA,0BAAc,gCAFd;AAMA,0BAAc,+BANd;","names":[]}
package/dist/index.d.cts CHANGED
@@ -4,6 +4,7 @@ export { CreateNewEvent, makeCreateNewEvent } from './createNewEvent.cjs';
4
4
  export { EventBus } from './ports/EventBus.cjs';
5
5
  export { EventQueries } from './ports/EventQueries.cjs';
6
6
  export { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions } from './ports/EventRepository.cjs';
7
+ export { GlobalSubscriberConfig, NarrowEvent, TopicFilter, TopicSubscriber, TopicSubscriptions, subscribeByTopic, subscribeGlobalToTopics } from './subscriptions.cjs';
7
8
  export { DefaultContext, EventFailure, EventId, EventPublication, EventStatus, Flavor, GenericEvent, SubscriptionId, UserId } from './types.cjs';
8
9
  export { createInMemoryEventBus } from './adapters/in-memory/InMemoryEventBus.cjs';
9
10
  export { createInMemoryEventQueries } from './adapters/in-memory/InMemoryEventQueries.cjs';
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export { CreateNewEvent, makeCreateNewEvent } from './createNewEvent.js';
4
4
  export { EventBus } from './ports/EventBus.js';
5
5
  export { EventQueries } from './ports/EventQueries.js';
6
6
  export { EventRepository, EventsUnitOfWork, WithEventsUow, WithEventsUowOptions } from './ports/EventRepository.js';
7
+ export { GlobalSubscriberConfig, NarrowEvent, TopicFilter, TopicSubscriber, TopicSubscriptions, subscribeByTopic, subscribeGlobalToTopics } from './subscriptions.js';
7
8
  export { DefaultContext, EventFailure, EventId, EventPublication, EventStatus, Flavor, GenericEvent, SubscriptionId, UserId } from './types.js';
8
9
  export { createInMemoryEventBus } from './adapters/in-memory/InMemoryEventBus.js';
9
10
  export { createInMemoryEventQueries } from './adapters/in-memory/InMemoryEventQueries.js';
package/dist/index.mjs CHANGED
@@ -1,4 +1,5 @@
1
1
  export * from "./adapters/in-memory/index.mjs";
2
2
  export * from "./createEventCrawler.mjs";
3
3
  export * from "./createNewEvent.mjs";
4
+ export * from "./subscriptions.mjs";
4
5
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './adapters/in-memory/index.mjs';\nexport * from './createEventCrawler.mjs';\nexport * from './createNewEvent.mjs';\nexport type * from './ports/EventBus.mjs';\nexport type * from './ports/EventQueries.mjs';\nexport type * from './ports/EventRepository.mjs';\nexport type * from './types.mjs';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export * from './adapters/in-memory/index.mjs';\nexport * from './createEventCrawler.mjs';\nexport * from './createNewEvent.mjs';\nexport type * from './ports/EventBus.mjs';\nexport type * from './ports/EventQueries.mjs';\nexport type * from './ports/EventRepository.mjs';\nexport * from './subscriptions.mjs';\nexport type * from './types.mjs';\n"],"mappings":"AAAA,cAAc;AACd,cAAc;AACd,cAAc;AAId,cAAc;","names":[]}
@@ -0,0 +1,61 @@
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
+ var subscriptions_exports = {};
20
+ __export(subscriptions_exports, {
21
+ subscribeByTopic: () => subscribeByTopic,
22
+ subscribeGlobalToTopics: () => subscribeGlobalToTopics
23
+ });
24
+ module.exports = __toCommonJS(subscriptions_exports);
25
+ function subscribeByTopic(eventBus, subscriptions) {
26
+ for (const topic of Object.keys(subscriptions)) {
27
+ const handlers = subscriptions[topic];
28
+ for (const { subscriptionId, handler } of handlers) {
29
+ eventBus.subscribe({
30
+ topic,
31
+ subscriptionId,
32
+ callBack: handler
33
+ });
34
+ }
35
+ }
36
+ }
37
+ function subscribeGlobalToTopics(eventBus, subscriptions, config) {
38
+ const allTopics = Object.keys(subscriptions);
39
+ let topicsToSubscribe;
40
+ const filter = config.filter;
41
+ if (!filter) {
42
+ topicsToSubscribe = allTopics;
43
+ } else if ("include" in filter) {
44
+ topicsToSubscribe = filter.include;
45
+ } else {
46
+ topicsToSubscribe = allTopics.filter((t) => !filter.exclude.includes(t));
47
+ }
48
+ for (const topic of topicsToSubscribe) {
49
+ eventBus.subscribe({
50
+ topic,
51
+ subscriptionId: config.subscriptionId,
52
+ callBack: config.handler
53
+ });
54
+ }
55
+ }
56
+ // Annotate the CommonJS export names for ESM import in node:
57
+ 0 && (module.exports = {
58
+ subscribeByTopic,
59
+ subscribeGlobalToTopics
60
+ });
61
+ //# sourceMappingURL=subscriptions.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/subscriptions.ts"],"sourcesContent":["import type { EventBus } from \"./ports/EventBus.ts\";\nimport type { DefaultContext, GenericEvent, SubscriptionId } from \"./types.ts\";\n\n/**\n * Extracts a specific event type from a union of events by topic.\n *\n * @example\n * ```typescript\n * type MyEvents =\n * | GenericEvent<\"OrderCreated\", { orderId: string }>\n * | GenericEvent<\"OrderShipped\", { trackingNumber: string }>;\n *\n * type OrderCreatedEvent = NarrowEvent<MyEvents, \"OrderCreated\">;\n * // Result: GenericEvent<\"OrderCreated\", { orderId: string }>\n * ```\n */\nexport type NarrowEvent<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n Topic extends AllEvents[\"topic\"],\n> = Extract<AllEvents, { topic: Topic }>;\n\n/**\n * A subscriber for a specific event topic.\n * The handler receives the full event (not just payload) for access to metadata.\n */\nexport type TopicSubscriber<\n E extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Unique identifier for retry tracking. */\n subscriptionId: SubscriptionId;\n /** Async handler receiving the full event with metadata (id, occurredAt, etc). */\n handler: (event: E) => Promise<void>;\n};\n\n/**\n * Complete subscription map requiring ALL event topics.\n * Forces explicit decision about each topic, documenting intent.\n *\n * @example\n * ```typescript\n * const subscriptions: TopicSubscriptions<MyEvents> = {\n * OrderCreated: [{ subscriptionId: \"notify\", handler: async (e) => {...} }],\n * OrderShipped: [], // Required even if empty\n * };\n * ```\n */\nexport type TopicSubscriptions<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n [K in AllEvents[\"topic\"]]: Array<TopicSubscriber<NarrowEvent<AllEvents, K>>>;\n};\n\n/**\n * Filter for global handlers. Use `include` OR `exclude` (mutually exclusive).\n *\n * @example\n * ```typescript\n * { include: [\"OrderCreated\", \"OrderShipped\"] } // Only these topics\n * { exclude: [\"NotificationAdded\"] } // All except these\n * ```\n */\nexport type TopicFilter<Topic extends string> =\n | { include: Topic[] }\n | { exclude: Topic[] };\n\n/**\n * Configuration for a global handler that listens to multiple topics.\n */\nexport type GlobalSubscriberConfig<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Unique identifier for retry tracking. */\n subscriptionId: SubscriptionId;\n /** Async handler receiving any event matching the filter. */\n handler: (event: AllEvents) => Promise<void>;\n /** Optional topic filter. Without filter, receives all topics. */\n filter?: TopicFilter<AllEvents[\"topic\"]>;\n};\n\n/**\n * Subscribe all handlers from a topic subscription map to an event bus.\n * Standalone function for use with any EventBus implementation.\n *\n * @example\n * ```typescript\n * const subscriptions: TopicSubscriptions<MyEvents> = {\n * OrderCreated: [{ subscriptionId: \"notify\", handler: sendNotification }],\n * OrderShipped: [{ subscriptionId: \"track\", handler: updateTracking }],\n * };\n * subscribeByTopic(eventBus, subscriptions);\n * ```\n */\nexport function subscribeByTopic<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventBus: EventBus<AllEvents>,\n subscriptions: TopicSubscriptions<AllEvents>,\n): void {\n for (const topic of Object.keys(subscriptions) as AllEvents[\"topic\"][]) {\n const handlers = subscriptions[topic];\n for (const { subscriptionId, handler } of handlers) {\n eventBus.subscribe({\n topic,\n subscriptionId,\n callBack: handler,\n });\n }\n }\n}\n\n/**\n * Subscribe a global handler to multiple topics with optional filtering.\n * Standalone function for use with any EventBus implementation.\n *\n * @example\n * ```typescript\n * // Audit log for all events except noisy ones\n * subscribeGlobalToTopics(eventBus, subscriptions, {\n * subscriptionId: \"audit-log\",\n * handler: async (event) => auditLog.record(event),\n * filter: { exclude: [\"NotificationAdded\"] },\n * });\n *\n * // Analytics for specific topics only\n * subscribeGlobalToTopics(eventBus, subscriptions, {\n * subscriptionId: \"order-analytics\",\n * handler: async (event) => analytics.track(event),\n * filter: { include: [\"OrderCreated\", \"OrderShipped\"] },\n * });\n * ```\n */\nexport function subscribeGlobalToTopics<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventBus: EventBus<AllEvents>,\n subscriptions: TopicSubscriptions<AllEvents>,\n config: GlobalSubscriberConfig<AllEvents>,\n): void {\n const allTopics = Object.keys(subscriptions) as AllEvents[\"topic\"][];\n\n let topicsToSubscribe: AllEvents[\"topic\"][];\n\n const filter = config.filter;\n if (!filter) {\n topicsToSubscribe = allTopics;\n } else if (\"include\" in filter) {\n topicsToSubscribe = filter.include;\n } else {\n topicsToSubscribe = allTopics.filter((t) => !filter.exclude.includes(t));\n }\n\n for (const topic of topicsToSubscribe) {\n eventBus.subscribe({\n topic,\n subscriptionId: config.subscriptionId,\n callBack: config.handler,\n });\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA4FO,SAAS,iBAGd,UACA,eACM;AACN,aAAW,SAAS,OAAO,KAAK,aAAa,GAA2B;AACtE,UAAM,WAAW,cAAc,KAAK;AACpC,eAAW,EAAE,gBAAgB,QAAQ,KAAK,UAAU;AAClD,eAAS,UAAU;AAAA,QACjB;AAAA,QACA;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAuBO,SAAS,wBAGd,UACA,eACA,QACM;AACN,QAAM,YAAY,OAAO,KAAK,aAAa;AAE3C,MAAI;AAEJ,QAAM,SAAS,OAAO;AACtB,MAAI,CAAC,QAAQ;AACX,wBAAoB;AAAA,EACtB,WAAW,aAAa,QAAQ;AAC9B,wBAAoB,OAAO;AAAA,EAC7B,OAAO;AACL,wBAAoB,UAAU,OAAO,CAAC,MAAM,CAAC,OAAO,QAAQ,SAAS,CAAC,CAAC;AAAA,EACzE;AAEA,aAAW,SAAS,mBAAmB;AACrC,aAAS,UAAU;AAAA,MACjB;AAAA,MACA,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,CAAC;AAAA,EACH;AACF;","names":[]}
@@ -0,0 +1,107 @@
1
+ import { EventBus } from './ports/EventBus.cjs';
2
+ import { GenericEvent, DefaultContext, SubscriptionId } from './types.cjs';
3
+
4
+ /**
5
+ * Extracts a specific event type from a union of events by topic.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * type MyEvents =
10
+ * | GenericEvent<"OrderCreated", { orderId: string }>
11
+ * | GenericEvent<"OrderShipped", { trackingNumber: string }>;
12
+ *
13
+ * type OrderCreatedEvent = NarrowEvent<MyEvents, "OrderCreated">;
14
+ * // Result: GenericEvent<"OrderCreated", { orderId: string }>
15
+ * ```
16
+ */
17
+ type NarrowEvent<AllEvents extends GenericEvent<string, unknown, DefaultContext>, Topic extends AllEvents["topic"]> = Extract<AllEvents, {
18
+ topic: Topic;
19
+ }>;
20
+ /**
21
+ * A subscriber for a specific event topic.
22
+ * The handler receives the full event (not just payload) for access to metadata.
23
+ */
24
+ type TopicSubscriber<E extends GenericEvent<string, unknown, DefaultContext>> = {
25
+ /** Unique identifier for retry tracking. */
26
+ subscriptionId: SubscriptionId;
27
+ /** Async handler receiving the full event with metadata (id, occurredAt, etc). */
28
+ handler: (event: E) => Promise<void>;
29
+ };
30
+ /**
31
+ * Complete subscription map requiring ALL event topics.
32
+ * Forces explicit decision about each topic, documenting intent.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const subscriptions: TopicSubscriptions<MyEvents> = {
37
+ * OrderCreated: [{ subscriptionId: "notify", handler: async (e) => {...} }],
38
+ * OrderShipped: [], // Required even if empty
39
+ * };
40
+ * ```
41
+ */
42
+ type TopicSubscriptions<AllEvents extends GenericEvent<string, unknown, DefaultContext>> = {
43
+ [K in AllEvents["topic"]]: Array<TopicSubscriber<NarrowEvent<AllEvents, K>>>;
44
+ };
45
+ /**
46
+ * Filter for global handlers. Use `include` OR `exclude` (mutually exclusive).
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * { include: ["OrderCreated", "OrderShipped"] } // Only these topics
51
+ * { exclude: ["NotificationAdded"] } // All except these
52
+ * ```
53
+ */
54
+ type TopicFilter<Topic extends string> = {
55
+ include: Topic[];
56
+ } | {
57
+ exclude: Topic[];
58
+ };
59
+ /**
60
+ * Configuration for a global handler that listens to multiple topics.
61
+ */
62
+ type GlobalSubscriberConfig<AllEvents extends GenericEvent<string, unknown, DefaultContext>> = {
63
+ /** Unique identifier for retry tracking. */
64
+ subscriptionId: SubscriptionId;
65
+ /** Async handler receiving any event matching the filter. */
66
+ handler: (event: AllEvents) => Promise<void>;
67
+ /** Optional topic filter. Without filter, receives all topics. */
68
+ filter?: TopicFilter<AllEvents["topic"]>;
69
+ };
70
+ /**
71
+ * Subscribe all handlers from a topic subscription map to an event bus.
72
+ * Standalone function for use with any EventBus implementation.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const subscriptions: TopicSubscriptions<MyEvents> = {
77
+ * OrderCreated: [{ subscriptionId: "notify", handler: sendNotification }],
78
+ * OrderShipped: [{ subscriptionId: "track", handler: updateTracking }],
79
+ * };
80
+ * subscribeByTopic(eventBus, subscriptions);
81
+ * ```
82
+ */
83
+ declare function subscribeByTopic<AllEvents extends GenericEvent<string, unknown, DefaultContext>>(eventBus: EventBus<AllEvents>, subscriptions: TopicSubscriptions<AllEvents>): void;
84
+ /**
85
+ * Subscribe a global handler to multiple topics with optional filtering.
86
+ * Standalone function for use with any EventBus implementation.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Audit log for all events except noisy ones
91
+ * subscribeGlobalToTopics(eventBus, subscriptions, {
92
+ * subscriptionId: "audit-log",
93
+ * handler: async (event) => auditLog.record(event),
94
+ * filter: { exclude: ["NotificationAdded"] },
95
+ * });
96
+ *
97
+ * // Analytics for specific topics only
98
+ * subscribeGlobalToTopics(eventBus, subscriptions, {
99
+ * subscriptionId: "order-analytics",
100
+ * handler: async (event) => analytics.track(event),
101
+ * filter: { include: ["OrderCreated", "OrderShipped"] },
102
+ * });
103
+ * ```
104
+ */
105
+ declare function subscribeGlobalToTopics<AllEvents extends GenericEvent<string, unknown, DefaultContext>>(eventBus: EventBus<AllEvents>, subscriptions: TopicSubscriptions<AllEvents>, config: GlobalSubscriberConfig<AllEvents>): void;
106
+
107
+ export { type GlobalSubscriberConfig, type NarrowEvent, type TopicFilter, type TopicSubscriber, type TopicSubscriptions, subscribeByTopic, subscribeGlobalToTopics };
@@ -0,0 +1,107 @@
1
+ import { EventBus } from './ports/EventBus.js';
2
+ import { GenericEvent, DefaultContext, SubscriptionId } from './types.js';
3
+
4
+ /**
5
+ * Extracts a specific event type from a union of events by topic.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * type MyEvents =
10
+ * | GenericEvent<"OrderCreated", { orderId: string }>
11
+ * | GenericEvent<"OrderShipped", { trackingNumber: string }>;
12
+ *
13
+ * type OrderCreatedEvent = NarrowEvent<MyEvents, "OrderCreated">;
14
+ * // Result: GenericEvent<"OrderCreated", { orderId: string }>
15
+ * ```
16
+ */
17
+ type NarrowEvent<AllEvents extends GenericEvent<string, unknown, DefaultContext>, Topic extends AllEvents["topic"]> = Extract<AllEvents, {
18
+ topic: Topic;
19
+ }>;
20
+ /**
21
+ * A subscriber for a specific event topic.
22
+ * The handler receives the full event (not just payload) for access to metadata.
23
+ */
24
+ type TopicSubscriber<E extends GenericEvent<string, unknown, DefaultContext>> = {
25
+ /** Unique identifier for retry tracking. */
26
+ subscriptionId: SubscriptionId;
27
+ /** Async handler receiving the full event with metadata (id, occurredAt, etc). */
28
+ handler: (event: E) => Promise<void>;
29
+ };
30
+ /**
31
+ * Complete subscription map requiring ALL event topics.
32
+ * Forces explicit decision about each topic, documenting intent.
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * const subscriptions: TopicSubscriptions<MyEvents> = {
37
+ * OrderCreated: [{ subscriptionId: "notify", handler: async (e) => {...} }],
38
+ * OrderShipped: [], // Required even if empty
39
+ * };
40
+ * ```
41
+ */
42
+ type TopicSubscriptions<AllEvents extends GenericEvent<string, unknown, DefaultContext>> = {
43
+ [K in AllEvents["topic"]]: Array<TopicSubscriber<NarrowEvent<AllEvents, K>>>;
44
+ };
45
+ /**
46
+ * Filter for global handlers. Use `include` OR `exclude` (mutually exclusive).
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * { include: ["OrderCreated", "OrderShipped"] } // Only these topics
51
+ * { exclude: ["NotificationAdded"] } // All except these
52
+ * ```
53
+ */
54
+ type TopicFilter<Topic extends string> = {
55
+ include: Topic[];
56
+ } | {
57
+ exclude: Topic[];
58
+ };
59
+ /**
60
+ * Configuration for a global handler that listens to multiple topics.
61
+ */
62
+ type GlobalSubscriberConfig<AllEvents extends GenericEvent<string, unknown, DefaultContext>> = {
63
+ /** Unique identifier for retry tracking. */
64
+ subscriptionId: SubscriptionId;
65
+ /** Async handler receiving any event matching the filter. */
66
+ handler: (event: AllEvents) => Promise<void>;
67
+ /** Optional topic filter. Without filter, receives all topics. */
68
+ filter?: TopicFilter<AllEvents["topic"]>;
69
+ };
70
+ /**
71
+ * Subscribe all handlers from a topic subscription map to an event bus.
72
+ * Standalone function for use with any EventBus implementation.
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * const subscriptions: TopicSubscriptions<MyEvents> = {
77
+ * OrderCreated: [{ subscriptionId: "notify", handler: sendNotification }],
78
+ * OrderShipped: [{ subscriptionId: "track", handler: updateTracking }],
79
+ * };
80
+ * subscribeByTopic(eventBus, subscriptions);
81
+ * ```
82
+ */
83
+ declare function subscribeByTopic<AllEvents extends GenericEvent<string, unknown, DefaultContext>>(eventBus: EventBus<AllEvents>, subscriptions: TopicSubscriptions<AllEvents>): void;
84
+ /**
85
+ * Subscribe a global handler to multiple topics with optional filtering.
86
+ * Standalone function for use with any EventBus implementation.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * // Audit log for all events except noisy ones
91
+ * subscribeGlobalToTopics(eventBus, subscriptions, {
92
+ * subscriptionId: "audit-log",
93
+ * handler: async (event) => auditLog.record(event),
94
+ * filter: { exclude: ["NotificationAdded"] },
95
+ * });
96
+ *
97
+ * // Analytics for specific topics only
98
+ * subscribeGlobalToTopics(eventBus, subscriptions, {
99
+ * subscriptionId: "order-analytics",
100
+ * handler: async (event) => analytics.track(event),
101
+ * filter: { include: ["OrderCreated", "OrderShipped"] },
102
+ * });
103
+ * ```
104
+ */
105
+ declare function subscribeGlobalToTopics<AllEvents extends GenericEvent<string, unknown, DefaultContext>>(eventBus: EventBus<AllEvents>, subscriptions: TopicSubscriptions<AllEvents>, config: GlobalSubscriberConfig<AllEvents>): void;
106
+
107
+ export { type GlobalSubscriberConfig, type NarrowEvent, type TopicFilter, type TopicSubscriber, type TopicSubscriptions, subscribeByTopic, subscribeGlobalToTopics };
@@ -0,0 +1,36 @@
1
+ function subscribeByTopic(eventBus, subscriptions) {
2
+ for (const topic of Object.keys(subscriptions)) {
3
+ const handlers = subscriptions[topic];
4
+ for (const { subscriptionId, handler } of handlers) {
5
+ eventBus.subscribe({
6
+ topic,
7
+ subscriptionId,
8
+ callBack: handler
9
+ });
10
+ }
11
+ }
12
+ }
13
+ function subscribeGlobalToTopics(eventBus, subscriptions, config) {
14
+ const allTopics = Object.keys(subscriptions);
15
+ let topicsToSubscribe;
16
+ const filter = config.filter;
17
+ if (!filter) {
18
+ topicsToSubscribe = allTopics;
19
+ } else if ("include" in filter) {
20
+ topicsToSubscribe = filter.include;
21
+ } else {
22
+ topicsToSubscribe = allTopics.filter((t) => !filter.exclude.includes(t));
23
+ }
24
+ for (const topic of topicsToSubscribe) {
25
+ eventBus.subscribe({
26
+ topic,
27
+ subscriptionId: config.subscriptionId,
28
+ callBack: config.handler
29
+ });
30
+ }
31
+ }
32
+ export {
33
+ subscribeByTopic,
34
+ subscribeGlobalToTopics
35
+ };
36
+ //# sourceMappingURL=subscriptions.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/subscriptions.ts"],"sourcesContent":["import type { EventBus } from './ports/EventBus.mjs';\nimport type { DefaultContext, GenericEvent, SubscriptionId } from './types.mjs';\n\n/**\n * Extracts a specific event type from a union of events by topic.\n *\n * @example\n * ```typescript\n * type MyEvents =\n * | GenericEvent<\"OrderCreated\", { orderId: string }>\n * | GenericEvent<\"OrderShipped\", { trackingNumber: string }>;\n *\n * type OrderCreatedEvent = NarrowEvent<MyEvents, \"OrderCreated\">;\n * // Result: GenericEvent<\"OrderCreated\", { orderId: string }>\n * ```\n */\nexport type NarrowEvent<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n Topic extends AllEvents[\"topic\"],\n> = Extract<AllEvents, { topic: Topic }>;\n\n/**\n * A subscriber for a specific event topic.\n * The handler receives the full event (not just payload) for access to metadata.\n */\nexport type TopicSubscriber<\n E extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Unique identifier for retry tracking. */\n subscriptionId: SubscriptionId;\n /** Async handler receiving the full event with metadata (id, occurredAt, etc). */\n handler: (event: E) => Promise<void>;\n};\n\n/**\n * Complete subscription map requiring ALL event topics.\n * Forces explicit decision about each topic, documenting intent.\n *\n * @example\n * ```typescript\n * const subscriptions: TopicSubscriptions<MyEvents> = {\n * OrderCreated: [{ subscriptionId: \"notify\", handler: async (e) => {...} }],\n * OrderShipped: [], // Required even if empty\n * };\n * ```\n */\nexport type TopicSubscriptions<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n [K in AllEvents[\"topic\"]]: Array<TopicSubscriber<NarrowEvent<AllEvents, K>>>;\n};\n\n/**\n * Filter for global handlers. Use `include` OR `exclude` (mutually exclusive).\n *\n * @example\n * ```typescript\n * { include: [\"OrderCreated\", \"OrderShipped\"] } // Only these topics\n * { exclude: [\"NotificationAdded\"] } // All except these\n * ```\n */\nexport type TopicFilter<Topic extends string> =\n | { include: Topic[] }\n | { exclude: Topic[] };\n\n/**\n * Configuration for a global handler that listens to multiple topics.\n */\nexport type GlobalSubscriberConfig<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n> = {\n /** Unique identifier for retry tracking. */\n subscriptionId: SubscriptionId;\n /** Async handler receiving any event matching the filter. */\n handler: (event: AllEvents) => Promise<void>;\n /** Optional topic filter. Without filter, receives all topics. */\n filter?: TopicFilter<AllEvents[\"topic\"]>;\n};\n\n/**\n * Subscribe all handlers from a topic subscription map to an event bus.\n * Standalone function for use with any EventBus implementation.\n *\n * @example\n * ```typescript\n * const subscriptions: TopicSubscriptions<MyEvents> = {\n * OrderCreated: [{ subscriptionId: \"notify\", handler: sendNotification }],\n * OrderShipped: [{ subscriptionId: \"track\", handler: updateTracking }],\n * };\n * subscribeByTopic(eventBus, subscriptions);\n * ```\n */\nexport function subscribeByTopic<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventBus: EventBus<AllEvents>,\n subscriptions: TopicSubscriptions<AllEvents>,\n): void {\n for (const topic of Object.keys(subscriptions) as AllEvents[\"topic\"][]) {\n const handlers = subscriptions[topic];\n for (const { subscriptionId, handler } of handlers) {\n eventBus.subscribe({\n topic,\n subscriptionId,\n callBack: handler,\n });\n }\n }\n}\n\n/**\n * Subscribe a global handler to multiple topics with optional filtering.\n * Standalone function for use with any EventBus implementation.\n *\n * @example\n * ```typescript\n * // Audit log for all events except noisy ones\n * subscribeGlobalToTopics(eventBus, subscriptions, {\n * subscriptionId: \"audit-log\",\n * handler: async (event) => auditLog.record(event),\n * filter: { exclude: [\"NotificationAdded\"] },\n * });\n *\n * // Analytics for specific topics only\n * subscribeGlobalToTopics(eventBus, subscriptions, {\n * subscriptionId: \"order-analytics\",\n * handler: async (event) => analytics.track(event),\n * filter: { include: [\"OrderCreated\", \"OrderShipped\"] },\n * });\n * ```\n */\nexport function subscribeGlobalToTopics<\n AllEvents extends GenericEvent<string, unknown, DefaultContext>,\n>(\n eventBus: EventBus<AllEvents>,\n subscriptions: TopicSubscriptions<AllEvents>,\n config: GlobalSubscriberConfig<AllEvents>,\n): void {\n const allTopics = Object.keys(subscriptions) as AllEvents[\"topic\"][];\n\n let topicsToSubscribe: AllEvents[\"topic\"][];\n\n const filter = config.filter;\n if (!filter) {\n topicsToSubscribe = allTopics;\n } else if (\"include\" in filter) {\n topicsToSubscribe = filter.include;\n } else {\n topicsToSubscribe = allTopics.filter((t) => !filter.exclude.includes(t));\n }\n\n for (const topic of topicsToSubscribe) {\n eventBus.subscribe({\n topic,\n subscriptionId: config.subscriptionId,\n callBack: config.handler,\n });\n }\n}\n"],"mappings":"AA4FO,SAAS,iBAGd,UACA,eACM;AACN,aAAW,SAAS,OAAO,KAAK,aAAa,GAA2B;AACtE,UAAM,WAAW,cAAc,KAAK;AACpC,eAAW,EAAE,gBAAgB,QAAQ,KAAK,UAAU;AAClD,eAAS,UAAU;AAAA,QACjB;AAAA,QACA;AAAA,QACA,UAAU;AAAA,MACZ,CAAC;AAAA,IACH;AAAA,EACF;AACF;AAuBO,SAAS,wBAGd,UACA,eACA,QACM;AACN,QAAM,YAAY,OAAO,KAAK,aAAa;AAE3C,MAAI;AAEJ,QAAM,SAAS,OAAO;AACtB,MAAI,CAAC,QAAQ;AACX,wBAAoB;AAAA,EACtB,WAAW,aAAa,QAAQ;AAC9B,wBAAoB,OAAO;AAAA,EAC7B,OAAO;AACL,wBAAoB,UAAU,OAAO,CAAC,MAAM,CAAC,OAAO,QAAQ,SAAS,CAAC,CAAC;AAAA,EACzE;AAEA,aAAW,SAAS,mBAAmB;AACrC,aAAS,UAAU;AAAA,MACjB;AAAA,MACA,gBAAgB,OAAO;AAAA,MACvB,UAAU,OAAO;AAAA,IACnB,CAAC;AAAA,EACH;AACF;","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Branded type helper for nominal typing.\n * Adds a phantom type property to distinguish between structurally identical types.\n */\nexport type Flavor<T, FlavorT> = T & {\n _type?: FlavorT;\n};\n\n/** Unique identifier for an event subscription. */\nexport type SubscriptionId = Flavor<string, \"SubscriptionId\">;\n\n/** Unique identifier for a user who triggered an event. */\nexport type UserId = Flavor<string, \"UserId\">;\n\n/** Unique identifier for an event. */\nexport type EventId = Flavor<string, \"EventId\">;\n\n/**\n * Records a subscription failure during event publication.\n * Contains the subscription that failed and error details for debugging.\n */\nexport type EventFailure = {\n subscriptionId: SubscriptionId;\n errorMessage: string;\n /** Stack trace captured when the subscription callback threw. */\n stack?: string;\n};\n\n/**\n * Records a single publication attempt for an event.\n * Tracks which subscribers were notified and any failures that occurred.\n */\nexport type EventPublication = {\n publishedAt: Date;\n /** All subscription IDs that were attempted in this publication. */\n publishedSubscribers: SubscriptionId[];\n /** Subscriptions that failed during this publication attempt. */\n failures: EventFailure[];\n};\n\n/**\n * Event lifecycle status.\n * - `never-published`: New event, not yet processed by crawler\n * - `in-process`: Currently being published by crawler\n * - `published`: Successfully delivered to all subscribers\n * - `failed-but-will-retry`: Some subscribers failed, will retry\n * - `quarantined`: Exceeded max retries, requires manual intervention\n * - `to-republish`: Force republish to all subscribers (manual trigger)\n */\nexport type EventStatus =\n | \"never-published\"\n | \"to-republish\"\n | \"in-process\"\n | \"published\"\n | \"failed-but-will-retry\"\n | \"quarantined\";\n\n/** Context type constraint - must be a string record or undefined. */\nexport type DefaultContext = Record<string, string> | undefined;\n\n/**\n * Generic event type for the outbox pattern.\n * Events are persisted in the same transaction as domain changes,\n * then asynchronously published to subscribers.\n *\n * @typeParam T - Event topic/type string literal\n * @typeParam P - Event payload type\n * @typeParam C - Optional context for filtering (e.g., tenant ID)\n *\n * @example\n * ```typescript\n * type MyEvents =\n * | GenericEvent<\"UserCreated\", { userId: string; email: string }>\n * | GenericEvent<\"OrderPlaced\", { orderId: string }, { tenantId: string }>;\n * ```\n */\nexport type GenericEvent<\n T extends string,\n P,\n C extends DefaultContext = undefined,\n> = {\n /** Unique event identifier. */\n id: EventId;\n /** When the event occurred in the domain. */\n occurredAt: Date;\n /** Event type/topic for routing to subscribers. */\n topic: T;\n /** Event-specific data. */\n payload: P;\n /** Current lifecycle status. */\n status: EventStatus;\n /** History of publication attempts. */\n publications: EventPublication[];\n /** User who triggered the action that created this event. */\n triggeredByUserId: UserId;\n /** Optional priority for processing order (not yet implemented in crawler). */\n priority?: number;\n /** Optional context for filtering events (e.g., by tenant). */\n context?: C;\n};\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
1
+ {"version":3,"sources":["../src/types.ts"],"sourcesContent":["/**\n * Branded type helper for nominal typing.\n * Adds a phantom type property to distinguish between structurally identical types.\n */\nexport type Flavor<T, FlavorT> = T & {\n _type?: FlavorT;\n};\n\n/** Unique identifier for an event subscription. */\nexport type SubscriptionId = Flavor<string, \"SubscriptionId\">;\n\n/** Unique identifier for a user who triggered an event. */\nexport type UserId = Flavor<string, \"UserId\">;\n\n/** Unique identifier for an event. */\nexport type EventId = Flavor<string, \"EventId\">;\n\n/**\n * Records a subscription failure during event publication.\n * Contains the subscription that failed and error details for debugging.\n */\nexport type EventFailure = {\n subscriptionId: SubscriptionId;\n errorMessage: string;\n /** Stack trace captured when the subscription callback threw. */\n stack?: string;\n};\n\n/**\n * Records a single publication attempt for an event.\n * Tracks which subscribers were notified and any failures that occurred.\n */\nexport type EventPublication = {\n publishedAt: Date;\n /** All subscription IDs that were attempted in this publication. */\n publishedSubscribers: SubscriptionId[];\n /** Subscriptions that failed during this publication attempt. */\n failures?: EventFailure[];\n};\n\n/**\n * Event lifecycle status.\n * - `never-published`: New event, not yet processed by crawler\n * - `in-process`: Currently being published by crawler\n * - `published`: Successfully delivered to all subscribers\n * - `failed-but-will-retry`: Some subscribers failed, will retry\n * - `quarantined`: Exceeded max retries, requires manual intervention\n * - `to-republish`: Force republish to all subscribers (manual trigger)\n */\nexport type EventStatus =\n | \"never-published\"\n | \"to-republish\"\n | \"in-process\"\n | \"published\"\n | \"failed-but-will-retry\"\n | \"quarantined\";\n\n/** Context type constraint - must be a string record or undefined. */\nexport type DefaultContext = Record<string, string> | undefined;\n\n/**\n * Generic event type for the outbox pattern.\n * Events are persisted in the same transaction as domain changes,\n * then asynchronously published to subscribers.\n *\n * @typeParam T - Event topic/type string literal\n * @typeParam P - Event payload type\n * @typeParam C - Optional context for filtering (e.g., tenant ID)\n *\n * @example\n * ```typescript\n * type MyEvents =\n * | GenericEvent<\"UserCreated\", { userId: string; email: string }>\n * | GenericEvent<\"OrderPlaced\", { orderId: string }, { tenantId: string }>;\n * ```\n */\nexport type GenericEvent<\n T extends string,\n P,\n C extends DefaultContext = undefined,\n> = {\n /** Unique event identifier. */\n id: EventId;\n /** When the event occurred in the domain. */\n occurredAt: Date;\n /** Event type/topic for routing to subscribers. */\n topic: T;\n /** Event-specific data. */\n payload: P;\n /** Current lifecycle status. */\n status: EventStatus;\n /** History of publication attempts. */\n publications: EventPublication[];\n /** User who triggered the action that created this event. */\n triggeredByUserId: UserId;\n /** Optional priority for processing order (not yet implemented in crawler). */\n priority?: number;\n /** Optional context for filtering events (e.g., by tenant). */\n context?: C;\n};\n"],"mappings":";;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
package/dist/types.d.cts CHANGED
@@ -30,7 +30,7 @@ type EventPublication = {
30
30
  /** All subscription IDs that were attempted in this publication. */
31
31
  publishedSubscribers: SubscriptionId[];
32
32
  /** Subscriptions that failed during this publication attempt. */
33
- failures: EventFailure[];
33
+ failures?: EventFailure[];
34
34
  };
35
35
  /**
36
36
  * Event lifecycle status.
package/dist/types.d.ts CHANGED
@@ -30,7 +30,7 @@ type EventPublication = {
30
30
  /** All subscription IDs that were attempted in this publication. */
31
31
  publishedSubscribers: SubscriptionId[];
32
32
  /** Subscriptions that failed during this publication attempt. */
33
- failures: EventFailure[];
33
+ failures?: EventFailure[];
34
34
  };
35
35
  /**
36
36
  * Event lifecycle status.
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "The purpose of this repository is to make it easy to setup event driven architecture using outbox pattern",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
- "version": "0.4.2",
6
+ "version": "0.5.0",
7
7
  "main": "./dist/index.mjs",
8
8
  "types": "./dist/index.d.ts",
9
9
  "files": [
@@ -1,6 +1,12 @@
1
1
  import { makeCreateNewEvent } from "../../createNewEvent.ts";
2
2
  import type { EventBus } from "../../ports/EventBus.ts";
3
3
  import type { WithEventsUow } from "../../ports/EventRepository.ts";
4
+ import {
5
+ type GlobalSubscriberConfig,
6
+ subscribeByTopic,
7
+ subscribeGlobalToTopics,
8
+ type TopicSubscriptions,
9
+ } from "../../subscriptions.ts";
4
10
  import type {
5
11
  DefaultContext,
6
12
  EventId,
@@ -66,7 +72,7 @@ export const createInMemoryEventBus = <
66
72
  const lastPublication = event.publications.reduce((latest, current) =>
67
73
  current.publishedAt > latest.publishedAt ? current : latest,
68
74
  );
69
- const failedSubscriptionIds = lastPublication.failures.map(
75
+ const failedSubscriptionIds = (lastPublication.failures ?? []).map(
70
76
  (failure) => failure.subscriptionId,
71
77
  );
72
78
 
@@ -86,7 +92,6 @@ export const createInMemoryEventBus = <
86
92
  event.publications.push({
87
93
  publishedAt,
88
94
  publishedSubscribers: [],
89
- failures: [],
90
95
  });
91
96
  event.status = "published";
92
97
  await withUow(async (uow) => {
@@ -127,7 +132,7 @@ export const createInMemoryEventBus = <
127
132
  publishedSubscribers: subscriptionIdsToPublish.map(
128
133
  (id) => id as SubscriptionId,
129
134
  ),
130
- failures,
135
+ ...(failures.length > 0 && { failures }),
131
136
  },
132
137
  ];
133
138
 
@@ -161,5 +166,59 @@ export const createInMemoryEventBus = <
161
166
  },
162
167
  };
163
168
 
164
- return { eventBus, createNewEvent };
169
+ /**
170
+ * Identity function for type inference when defining subscription maps.
171
+ * Ensures all topics are covered and handlers have correct payload types.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * const subscriptions = defineSubscriptions({
176
+ * OrderCreated: [{ subscriptionId: "notify", handler: async (e) => {...} }],
177
+ * OrderShipped: [], // Required even if empty
178
+ * });
179
+ * ```
180
+ */
181
+ const defineSubscriptions = (
182
+ subscriptions: TopicSubscriptions<Event>,
183
+ ): TopicSubscriptions<Event> => subscriptions;
184
+
185
+ /**
186
+ * Subscribe all handlers from a topic subscription map to this event bus.
187
+ *
188
+ * @example
189
+ * ```typescript
190
+ * const subscriptions = defineSubscriptions({...});
191
+ * subscribeAll(subscriptions);
192
+ * ```
193
+ */
194
+ const subscribeAll = (subscriptions: TopicSubscriptions<Event>): void => {
195
+ subscribeByTopic(eventBus, subscriptions);
196
+ };
197
+
198
+ /**
199
+ * Subscribe a global handler to multiple topics with optional filtering.
200
+ *
201
+ * @example
202
+ * ```typescript
203
+ * subscribeGlobal(subscriptions, {
204
+ * subscriptionId: "audit-log",
205
+ * handler: async (event) => auditLog.record(event),
206
+ * filter: { exclude: ["NotificationAdded"] },
207
+ * });
208
+ * ```
209
+ */
210
+ const subscribeGlobal = (
211
+ subscriptions: TopicSubscriptions<Event>,
212
+ config: GlobalSubscriberConfig<Event>,
213
+ ): void => {
214
+ subscribeGlobalToTopics(eventBus, subscriptions, config);
215
+ };
216
+
217
+ return {
218
+ eventBus,
219
+ createNewEvent,
220
+ defineSubscriptions,
221
+ subscribeAll,
222
+ subscribeGlobal,
223
+ };
165
224
  };
package/src/index.ts CHANGED
@@ -4,4 +4,5 @@ export * from "./createNewEvent.ts";
4
4
  export type * from "./ports/EventBus.ts";
5
5
  export type * from "./ports/EventQueries.ts";
6
6
  export type * from "./ports/EventRepository.ts";
7
+ export * from "./subscriptions.ts";
7
8
  export type * from "./types.ts";
@@ -0,0 +1,159 @@
1
+ import type { EventBus } from "./ports/EventBus.ts";
2
+ import type { DefaultContext, GenericEvent, SubscriptionId } from "./types.ts";
3
+
4
+ /**
5
+ * Extracts a specific event type from a union of events by topic.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * type MyEvents =
10
+ * | GenericEvent<"OrderCreated", { orderId: string }>
11
+ * | GenericEvent<"OrderShipped", { trackingNumber: string }>;
12
+ *
13
+ * type OrderCreatedEvent = NarrowEvent<MyEvents, "OrderCreated">;
14
+ * // Result: GenericEvent<"OrderCreated", { orderId: string }>
15
+ * ```
16
+ */
17
+ export type NarrowEvent<
18
+ AllEvents extends GenericEvent<string, unknown, DefaultContext>,
19
+ Topic extends AllEvents["topic"],
20
+ > = Extract<AllEvents, { topic: Topic }>;
21
+
22
+ /**
23
+ * A subscriber for a specific event topic.
24
+ * The handler receives the full event (not just payload) for access to metadata.
25
+ */
26
+ export type TopicSubscriber<
27
+ E extends GenericEvent<string, unknown, DefaultContext>,
28
+ > = {
29
+ /** Unique identifier for retry tracking. */
30
+ subscriptionId: SubscriptionId;
31
+ /** Async handler receiving the full event with metadata (id, occurredAt, etc). */
32
+ handler: (event: E) => Promise<void>;
33
+ };
34
+
35
+ /**
36
+ * Complete subscription map requiring ALL event topics.
37
+ * Forces explicit decision about each topic, documenting intent.
38
+ *
39
+ * @example
40
+ * ```typescript
41
+ * const subscriptions: TopicSubscriptions<MyEvents> = {
42
+ * OrderCreated: [{ subscriptionId: "notify", handler: async (e) => {...} }],
43
+ * OrderShipped: [], // Required even if empty
44
+ * };
45
+ * ```
46
+ */
47
+ export type TopicSubscriptions<
48
+ AllEvents extends GenericEvent<string, unknown, DefaultContext>,
49
+ > = {
50
+ [K in AllEvents["topic"]]: Array<TopicSubscriber<NarrowEvent<AllEvents, K>>>;
51
+ };
52
+
53
+ /**
54
+ * Filter for global handlers. Use `include` OR `exclude` (mutually exclusive).
55
+ *
56
+ * @example
57
+ * ```typescript
58
+ * { include: ["OrderCreated", "OrderShipped"] } // Only these topics
59
+ * { exclude: ["NotificationAdded"] } // All except these
60
+ * ```
61
+ */
62
+ export type TopicFilter<Topic extends string> =
63
+ | { include: Topic[] }
64
+ | { exclude: Topic[] };
65
+
66
+ /**
67
+ * Configuration for a global handler that listens to multiple topics.
68
+ */
69
+ export type GlobalSubscriberConfig<
70
+ AllEvents extends GenericEvent<string, unknown, DefaultContext>,
71
+ > = {
72
+ /** Unique identifier for retry tracking. */
73
+ subscriptionId: SubscriptionId;
74
+ /** Async handler receiving any event matching the filter. */
75
+ handler: (event: AllEvents) => Promise<void>;
76
+ /** Optional topic filter. Without filter, receives all topics. */
77
+ filter?: TopicFilter<AllEvents["topic"]>;
78
+ };
79
+
80
+ /**
81
+ * Subscribe all handlers from a topic subscription map to an event bus.
82
+ * Standalone function for use with any EventBus implementation.
83
+ *
84
+ * @example
85
+ * ```typescript
86
+ * const subscriptions: TopicSubscriptions<MyEvents> = {
87
+ * OrderCreated: [{ subscriptionId: "notify", handler: sendNotification }],
88
+ * OrderShipped: [{ subscriptionId: "track", handler: updateTracking }],
89
+ * };
90
+ * subscribeByTopic(eventBus, subscriptions);
91
+ * ```
92
+ */
93
+ export function subscribeByTopic<
94
+ AllEvents extends GenericEvent<string, unknown, DefaultContext>,
95
+ >(
96
+ eventBus: EventBus<AllEvents>,
97
+ subscriptions: TopicSubscriptions<AllEvents>,
98
+ ): void {
99
+ for (const topic of Object.keys(subscriptions) as AllEvents["topic"][]) {
100
+ const handlers = subscriptions[topic];
101
+ for (const { subscriptionId, handler } of handlers) {
102
+ eventBus.subscribe({
103
+ topic,
104
+ subscriptionId,
105
+ callBack: handler,
106
+ });
107
+ }
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Subscribe a global handler to multiple topics with optional filtering.
113
+ * Standalone function for use with any EventBus implementation.
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * // Audit log for all events except noisy ones
118
+ * subscribeGlobalToTopics(eventBus, subscriptions, {
119
+ * subscriptionId: "audit-log",
120
+ * handler: async (event) => auditLog.record(event),
121
+ * filter: { exclude: ["NotificationAdded"] },
122
+ * });
123
+ *
124
+ * // Analytics for specific topics only
125
+ * subscribeGlobalToTopics(eventBus, subscriptions, {
126
+ * subscriptionId: "order-analytics",
127
+ * handler: async (event) => analytics.track(event),
128
+ * filter: { include: ["OrderCreated", "OrderShipped"] },
129
+ * });
130
+ * ```
131
+ */
132
+ export function subscribeGlobalToTopics<
133
+ AllEvents extends GenericEvent<string, unknown, DefaultContext>,
134
+ >(
135
+ eventBus: EventBus<AllEvents>,
136
+ subscriptions: TopicSubscriptions<AllEvents>,
137
+ config: GlobalSubscriberConfig<AllEvents>,
138
+ ): void {
139
+ const allTopics = Object.keys(subscriptions) as AllEvents["topic"][];
140
+
141
+ let topicsToSubscribe: AllEvents["topic"][];
142
+
143
+ const filter = config.filter;
144
+ if (!filter) {
145
+ topicsToSubscribe = allTopics;
146
+ } else if ("include" in filter) {
147
+ topicsToSubscribe = filter.include;
148
+ } else {
149
+ topicsToSubscribe = allTopics.filter((t) => !filter.exclude.includes(t));
150
+ }
151
+
152
+ for (const topic of topicsToSubscribe) {
153
+ eventBus.subscribe({
154
+ topic,
155
+ subscriptionId: config.subscriptionId,
156
+ callBack: config.handler,
157
+ });
158
+ }
159
+ }
package/src/types.ts CHANGED
@@ -35,7 +35,7 @@ export type EventPublication = {
35
35
  /** All subscription IDs that were attempted in this publication. */
36
36
  publishedSubscribers: SubscriptionId[];
37
37
  /** Subscriptions that failed during this publication attempt. */
38
- failures: EventFailure[];
38
+ failures?: EventFailure[];
39
39
  };
40
40
 
41
41
  /**