@relaybus/nestjs 0.2.0 → 0.3.1

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.cjs CHANGED
@@ -15,7 +15,7 @@ var __decorateClass = (decorators, target, key, kind) => {
15
15
  };
16
16
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
17
17
  var EVENT_CONSUMER_METADATA_KEY = /* @__PURE__ */ Symbol.for("relaybus.nestjs.event-consumer");
18
- function EventConsumer(catalog, {
18
+ function RelayBusCatalogConsumer(catalog, {
19
19
  events,
20
20
  ...options
21
21
  } = {}) {
@@ -25,7 +25,7 @@ function EventConsumer(catalog, {
25
25
  const existing = Reflect.getOwnMetadata(EVENT_CONSUMER_METADATA_KEY, target) ?? [];
26
26
  if (existing.some((metadata) => metadata.catalog === catalog)) {
27
27
  throw new core.ConfigurationError(
28
- `@EventConsumer(${name ?? "catalog"}) is declared twice on ${target.name}; merge the declarations into one`
28
+ `@RelayBusCatalogConsumer(${name ?? "catalog"}) is declared twice on ${target.name}; merge the declarations into one`
29
29
  );
30
30
  }
31
31
  Reflect.defineMetadata(
@@ -35,7 +35,8 @@ function EventConsumer(catalog, {
35
35
  );
36
36
  };
37
37
  }
38
- function readEventConsumerMetadata(target) {
38
+ var EventConsumer = RelayBusCatalogConsumer;
39
+ function readCatalogConsumerMetadata(target) {
39
40
  const lineage = [];
40
41
  for (let current = target; current !== Function.prototype; ) {
41
42
  lineage.unshift(current);
@@ -50,8 +51,69 @@ function readEventConsumerMetadata(target) {
50
51
  }
51
52
  return [...byCatalog.values()];
52
53
  }
54
+ var RelayBusFeatureContract = class {
55
+ consumes;
56
+ produces;
57
+ /** @throws ConfigurationError for a `produce(...)` in `consumes`, a `consume(...)` in `produces`, or something that is not a catalog. */
58
+ constructor(options) {
59
+ this.consumes = contractEvents(options.consumes ?? [], "consume");
60
+ this.produces = contractEvents(options.produces ?? [], "produce");
61
+ }
62
+ };
63
+ function contractEvents(items, role) {
64
+ return items.flatMap((item) => {
65
+ const declaration = isDeclaration(item) ? item : role === "consume" ? core.consume(item) : core.produce(item);
66
+ const name = core.describeCatalog(declaration.catalog).name ?? "catalog";
67
+ if (declaration.role !== role) {
68
+ throw new core.ConfigurationError(
69
+ `RelayBusModule.forFeature ${role}s takes catalogs and ${role}(...) declarations, received ${declaration.role}(${name})`
70
+ );
71
+ }
72
+ return declaration.entries.map((entry) => ({
73
+ label: `${name}.${entry.name}`,
74
+ type: entry.eventClass.type
75
+ }));
76
+ });
77
+ }
78
+ function isDeclaration(item) {
79
+ return typeof item["role"] === "string" && Array.isArray(item["entries"]);
80
+ }
81
+ var runningBuses = /* @__PURE__ */ new Set();
82
+ var busOfInstance = /* @__PURE__ */ new WeakMap();
83
+ function registerBus(bus) {
84
+ runningBuses.add(bus);
85
+ }
86
+ function unregisterBus(bus) {
87
+ runningBuses.delete(bus);
88
+ }
89
+ function bindInstance(instance, bus) {
90
+ busOfInstance.set(instance, bus);
91
+ }
92
+ function resolveBus(instance, method) {
93
+ const bound = typeof instance === "object" && instance !== null ? busOfInstance.get(instance) : void 0;
94
+ if (bound !== void 0) {
95
+ if (!runningBuses.has(bound)) {
96
+ throw new core.ConfigurationError(
97
+ `@RelayBusTransaction() ${method} was called after its application shut down`
98
+ );
99
+ }
100
+ return bound;
101
+ }
102
+ const [only, ...others] = runningBuses;
103
+ if (only === void 0) {
104
+ throw new core.ConfigurationError(
105
+ `@RelayBusTransaction() ${method} found no running RelayBusModule: import RelayBusModule.forRoot(...) and call the method after the application initialised (app.init() or app.listen())`
106
+ );
107
+ }
108
+ if (others.length > 0) {
109
+ throw new core.ConfigurationError(
110
+ `@RelayBusTransaction() ${method} cannot tell which of ${runningBuses.size} running applications it belongs to; call it on a singleton provider of one of them`
111
+ );
112
+ }
113
+ return only;
114
+ }
53
115
  var SUBSCRIBE_METADATA_KEY = /* @__PURE__ */ Symbol.for("relaybus.nestjs.subscribe");
54
- function Subscribe(event, options = {}) {
116
+ function RelayBusConsumer(event, options = {}) {
55
117
  return (target, propertyKey) => {
56
118
  const existing = Reflect.getOwnMetadata(SUBSCRIBE_METADATA_KEY, target, propertyKey) ?? [];
57
119
  Reflect.defineMetadata(
@@ -62,94 +124,160 @@ function Subscribe(event, options = {}) {
62
124
  );
63
125
  };
64
126
  }
65
- function readSubscribeMetadata(prototype, propertyKey) {
127
+ var Subscribe = RelayBusConsumer;
128
+ function readConsumerMetadata(prototype, propertyKey) {
66
129
  return Reflect.getMetadata(SUBSCRIBE_METADATA_KEY, prototype, propertyKey) ?? [];
67
130
  }
68
131
 
69
132
  // src/handler-explorer.ts
70
133
  exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
71
- constructor(discovery, metadataScanner, bus) {
134
+ constructor(discovery, metadataScanner, bus, modules) {
72
135
  this.discovery = discovery;
73
136
  this.metadataScanner = metadataScanner;
74
137
  this.bus = bus;
138
+ this.modules = modules;
75
139
  }
76
140
  discovery;
77
141
  metadataScanner;
78
142
  bus;
143
+ modules;
79
144
  onModuleInit() {
80
- const registeredBy = /* @__PURE__ */ new Map();
81
- const visited = /* @__PURE__ */ new Map();
82
- const consumers = /* @__PURE__ */ new WeakSet();
145
+ const discovery = {
146
+ registeredBy: /* @__PURE__ */ new Map(),
147
+ visited: /* @__PURE__ */ new Map(),
148
+ consumers: /* @__PURE__ */ new WeakSet(),
149
+ consumedByModule: /* @__PURE__ */ new Map()
150
+ };
83
151
  for (const wrapper of this.discovery.getProviders()) {
84
- if (wrapper.isTransient || !wrapper.isDependencyTreeStatic()) {
85
- this.assertNoHandlers(wrapper);
86
- continue;
87
- }
88
- const instance = wrapper.instance;
89
- if (typeof instance !== "object" || instance === null) {
90
- continue;
91
- }
92
- const prototype = Object.getPrototypeOf(instance);
93
- if (prototype === null) {
152
+ this.registerProvider(wrapper, discovery);
153
+ }
154
+ this.assertModuleContracts(discovery.consumedByModule);
155
+ }
156
+ /** Checks the `RelayBusModule.forFeature` contract of every module that imports one. */
157
+ assertModuleContracts(consumedByModule) {
158
+ const routed = new Set(
159
+ this.bus.handlers().filter((handler) => handler.forwardsToTransport).map((handler) => handler.eventType)
160
+ );
161
+ const violations = [];
162
+ for (const wrapper of this.discovery.getProviders()) {
163
+ const contract = wrapper.instance;
164
+ if (!(contract instanceof RelayBusFeatureContract)) {
94
165
  continue;
95
166
  }
96
- const className = instance.constructor.name;
97
- for (const { methodName, declarations } of this.subscribedMethods(prototype)) {
98
- const seen = visited.get(instance) ?? /* @__PURE__ */ new Set();
99
- visited.set(instance, seen);
100
- if (seen.has(methodName)) {
101
- continue;
167
+ for (const module of this.importersOf(wrapper.host)) {
168
+ const consumed = consumedByModule.get(module);
169
+ for (const event of contract.consumes.filter(({ type }) => !consumed.has(type))) {
170
+ violations.push(`${module.name}: ${event.label} (${event.type}) has no consumer`);
102
171
  }
103
- seen.add(methodName);
104
- const method = instance[methodName];
105
- const location = `${className}.${methodName}`;
106
- for (const { event, options } of declarations) {
107
- const id = options.id ?? location;
108
- const previous = registeredBy.get(id);
109
- if (previous !== void 0) {
110
- throw new core.ConfigurationError(
111
- `Handler id '${id}' is used by both ${previous} and ${location}; pass a unique { id } to @Subscribe`
112
- );
113
- }
114
- registeredBy.set(id, location);
115
- this.bus.subscribe(
116
- event,
117
- (envelope, context) => method.call(instance, envelope, context),
118
- { ...options, id }
172
+ for (const event of contract.produces.filter(({ type }) => !routed.has(type))) {
173
+ violations.push(
174
+ `${module.name}: ${event.label} (${event.type}) has no route to a transport`
119
175
  );
120
176
  }
121
177
  }
122
- if (!consumers.has(instance)) {
123
- consumers.add(instance);
124
- this.registerEventConsumers(instance, className, registeredBy);
125
- }
178
+ }
179
+ if (violations.length > 0) {
180
+ throw new core.ConfigurationError(
181
+ `[RelayBusHandlerExplorer](assertModuleContracts) ${violations.join("; ")}`
182
+ );
126
183
  }
127
184
  }
128
- registerEventConsumers(instance, className, registeredBy) {
185
+ importersOf(contractModule) {
186
+ return [...this.modules.values()].filter((module) => module.imports.has(contractModule));
187
+ }
188
+ /** Records the event types the provider of `wrapper` consumes under the provider's module. */
189
+ recordConsumedTypes(wrapper, { instance, discovery }) {
190
+ const types = discovery.consumedByModule.get(wrapper.host) ?? /* @__PURE__ */ new Set();
191
+ discovery.consumedByModule.set(wrapper.host, types);
129
192
  const lookup = instance;
130
- for (const { catalog, options, selectedEntries } of readEventConsumerMetadata(
131
- instance.constructor
193
+ for (const { declarations } of this.subscribedMethods(
194
+ Object.getPrototypeOf(instance)
132
195
  )) {
133
- const { name, entries } = core.describeCatalog(catalog);
134
- const label = name ?? "catalog";
135
- const handlers = selectedEntries ?? entries.filter((entry) => isMethod(lookup, entry));
136
- const missing = handlers.filter((entry) => !isMethod(lookup, entry));
137
- if (missing.length > 0) {
138
- throw new core.ConfigurationError(
139
- `${className} uses @EventConsumer(${label}) but lacks the handler methods of events it consumes: ${missing.map((entry) => entry.handlerName).join(", ")}`
140
- );
196
+ for (const { event } of declarations) {
197
+ types.add(typeof event === "string" ? event : event.type);
198
+ }
199
+ }
200
+ for (const { catalog, selectedEntries } of readCatalogConsumerMetadata(instance.constructor)) {
201
+ const entries = selectedEntries ?? core.describeCatalog(catalog).entries.filter((entry) => isMethod(lookup, entry));
202
+ for (const entry of entries) {
203
+ types.add(entry.eventClass.type);
141
204
  }
142
- if (handlers.length === 0) {
205
+ }
206
+ }
207
+ registerProvider(wrapper, discovery) {
208
+ if (wrapper.isTransient || !wrapper.isDependencyTreeStatic()) {
209
+ this.assertNoHandlers(wrapper);
210
+ return;
211
+ }
212
+ const instance = wrapper.instance;
213
+ if (typeof instance !== "object" || instance === null) {
214
+ return;
215
+ }
216
+ const prototype = Object.getPrototypeOf(instance);
217
+ if (prototype === null) {
218
+ return;
219
+ }
220
+ bindInstance(instance, this.bus);
221
+ this.recordConsumedTypes(wrapper, { instance, discovery });
222
+ const className = instance.constructor.name;
223
+ for (const subscribed of this.subscribedMethods(prototype)) {
224
+ this.registerSubscribedMethod(instance, { subscribedMethod: subscribed, discovery });
225
+ }
226
+ if (!discovery.consumers.has(instance)) {
227
+ discovery.consumers.add(instance);
228
+ this.registerEventConsumers(instance, { className, registeredBy: discovery.registeredBy });
229
+ }
230
+ }
231
+ registerSubscribedMethod(instance, {
232
+ subscribedMethod: { methodName, declarations },
233
+ discovery: { registeredBy, visited }
234
+ }) {
235
+ const seen = visited.get(instance) ?? /* @__PURE__ */ new Set();
236
+ visited.set(instance, seen);
237
+ if (seen.has(methodName)) {
238
+ return;
239
+ }
240
+ seen.add(methodName);
241
+ const method = instance[methodName];
242
+ const location = `${instance.constructor.name}.${methodName}`;
243
+ for (const { event, options } of declarations) {
244
+ const id = options.id ?? location;
245
+ const previous = registeredBy.get(id);
246
+ if (previous !== void 0) {
143
247
  throw new core.ConfigurationError(
144
- `${className} uses @EventConsumer(${label}) but defines none of its handler methods (${entries.map((entry) => entry.handlerName).join(", ")})`
248
+ `Handler id '${id}' is used by both ${previous} and ${location}; pass a unique { id } to @RelayBusConsumer`
145
249
  );
146
250
  }
251
+ registeredBy.set(id, location);
252
+ this.bus.subscribe(
253
+ event,
254
+ (envelope, context) => method.call(instance, envelope, context),
255
+ { ...options, id }
256
+ );
257
+ }
258
+ }
259
+ registerEventConsumers(instance, {
260
+ className,
261
+ registeredBy
262
+ }) {
263
+ const lookup = instance;
264
+ for (const { catalog, options, selectedEntries } of readCatalogConsumerMetadata(
265
+ instance.constructor
266
+ )) {
267
+ const { name, entries } = core.describeCatalog(catalog);
268
+ const label = name ?? "catalog";
269
+ const handlers = this.catalogHandlers(lookup, {
270
+ className,
271
+ label,
272
+ entries,
273
+ selected: selectedEntries
274
+ });
147
275
  for (const entry of handlers) {
148
276
  const id = `${className}.${entry.handlerName}`;
149
277
  const previous = registeredBy.get(id);
150
278
  if (previous !== void 0) {
151
279
  throw new core.ConfigurationError(
152
- `Handler id '${id}' of @EventConsumer(${label}) is already used by ${previous}; give the @Subscribe a unique { id } or drop it`
280
+ `Handler id '${id}' of @RelayBusCatalogConsumer(${label}) is already used by ${previous}; give the @RelayBusConsumer a unique { id } or drop it`
153
281
  );
154
282
  }
155
283
  registeredBy.set(id, `${className}.${entry.handlerName}`);
@@ -162,10 +290,31 @@ exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
162
290
  }
163
291
  }
164
292
  }
293
+ /** The catalog entries `lookup` handles: the selected ones, else every entry it has a method for. */
294
+ catalogHandlers(lookup, {
295
+ className,
296
+ label,
297
+ entries,
298
+ selected
299
+ }) {
300
+ const handlers = selected ?? entries.filter((entry) => isMethod(lookup, entry));
301
+ const missing = handlers.filter((entry) => !isMethod(lookup, entry));
302
+ if (missing.length > 0) {
303
+ throw new core.ConfigurationError(
304
+ `${className} uses @RelayBusCatalogConsumer(${label}) but lacks the handler methods of events it consumes: ${missing.map((entry) => entry.handlerName).join(", ")}`
305
+ );
306
+ }
307
+ if (handlers.length === 0) {
308
+ throw new core.ConfigurationError(
309
+ `${className} uses @RelayBusCatalogConsumer(${label}) but defines none of its handler methods (${entries.map((entry) => entry.handlerName).join(", ")})`
310
+ );
311
+ }
312
+ return handlers;
313
+ }
165
314
  subscribedMethods(prototype) {
166
315
  const methods = [];
167
316
  for (const methodName of this.metadataScanner.getAllMethodNames(prototype)) {
168
- const declarations = readSubscribeMetadata(prototype, methodName);
317
+ const declarations = readConsumerMetadata(prototype, methodName);
169
318
  if (declarations.length > 0) {
170
319
  methods.push({ methodName, declarations });
171
320
  }
@@ -179,16 +328,16 @@ exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
179
328
  */
180
329
  assertNoHandlers(wrapper) {
181
330
  const type = wrapper.isFactory ? wrapper.token : wrapper.metatype;
182
- if (typeof type === "function" && readEventConsumerMetadata(type).length > 0) {
331
+ if (typeof type === "function" && readCatalogConsumerMetadata(type).length > 0) {
183
332
  throw new core.ConfigurationError(
184
- `${type.name} uses @EventConsumer but its provider is request-scoped or transient (directly or through a dependency); handlers must be singleton providers`
333
+ `${type.name} uses @RelayBusCatalogConsumer but its provider is request-scoped or transient (directly or through a dependency); handlers must be singleton providers`
185
334
  );
186
335
  }
187
336
  const prototype = typeof type === "function" ? type.prototype : null;
188
337
  const [first] = prototype === null ? [] : this.subscribedMethods(prototype);
189
338
  if (first !== void 0) {
190
339
  throw new core.ConfigurationError(
191
- `${prototype.constructor.name}.${first.methodName} uses @Subscribe but its provider is request-scoped or transient (directly or through a dependency); handlers must be singleton providers`
340
+ `${prototype.constructor.name}.${first.methodName} uses @RelayBusConsumer but its provider is request-scoped or transient (directly or through a dependency); handlers must be singleton providers`
192
341
  );
193
342
  }
194
343
  }
@@ -197,7 +346,8 @@ exports.RelayBusHandlerExplorer = __decorateClass([
197
346
  common.Injectable(),
198
347
  __decorateParam(0, common.Inject(core$1.DiscoveryService)),
199
348
  __decorateParam(1, common.Inject(core$1.MetadataScanner)),
200
- __decorateParam(2, common.Inject(core.RelayBus))
349
+ __decorateParam(2, common.Inject(core.RelayBus)),
350
+ __decorateParam(3, common.Inject(core$1.ModulesContainer))
201
351
  ], exports.RelayBusHandlerExplorer);
202
352
  function isMethod(instance, entry) {
203
353
  return typeof instance[entry.handlerName] === "function";
@@ -258,18 +408,86 @@ exports.RelayBusLifecycle = class RelayBusLifecycle {
258
408
  this.bus = bus;
259
409
  }
260
410
  bus;
411
+ onModuleInit() {
412
+ registerBus(this.bus);
413
+ }
261
414
  async onApplicationBootstrap() {
262
415
  await this.bus.start();
263
416
  }
264
417
  async beforeApplicationShutdown() {
265
418
  await this.bus.stop();
266
419
  }
420
+ /** After the bus stopped, so handlers still draining at shutdown can open transactions. */
421
+ onApplicationShutdown() {
422
+ unregisterBus(this.bus);
423
+ }
267
424
  };
268
425
  exports.RelayBusLifecycle = __decorateClass([
269
426
  common.Injectable(),
270
427
  __decorateParam(0, common.Inject(core.RelayBus))
271
428
  ], exports.RelayBusLifecycle);
429
+ exports.RelayBusFeatureModule = class RelayBusFeatureModule {
430
+ };
431
+ exports.RelayBusFeatureModule = __decorateClass([
432
+ common.Module({})
433
+ ], exports.RelayBusFeatureModule);
434
+ exports.RelayBusTransactionHost = class RelayBusTransactionHost {
435
+ constructor(bus) {
436
+ this.bus = bus;
437
+ }
438
+ bus;
439
+ /**
440
+ * The active transaction.
441
+ *
442
+ * @throws TransactionRequiredError when the caller runs outside every transaction.
443
+ */
444
+ current() {
445
+ const transaction = this.bus.currentTransaction();
446
+ if (transaction === void 0) {
447
+ throw new core.TransactionRequiredError(
448
+ "No relaybus transaction is active: call the repository from a @RelayBusTransaction() method or inside bus.transaction()"
449
+ );
450
+ }
451
+ return transaction;
452
+ }
453
+ /** The active transaction, or undefined outside every transaction. */
454
+ find() {
455
+ return this.bus.currentTransaction();
456
+ }
457
+ };
458
+ exports.RelayBusTransactionHost = __decorateClass([
459
+ common.Injectable(),
460
+ __decorateParam(0, common.Inject(core.RelayBus))
461
+ ], exports.RelayBusTransactionHost);
462
+
463
+ // src/relay-bus.module.ts
272
464
  exports.RelayBusModule = class RelayBusModule extends RelayBusConfigurableModule {
465
+ /**
466
+ * The relaybus contract of a feature module: import it in the module whose providers consume
467
+ * and produce the events. When the application initialises, every consumed event needs a
468
+ * `@RelayBusConsumer` method (or a `@RelayBusCatalogConsumer` class) among the providers of
469
+ * that module, spread over any classes and method names, and every produced event needs a
470
+ * route to a transport; otherwise the application fails to start naming the module and each
471
+ * missing event. Needs `RelayBusModule.forRoot` in the application.
472
+ *
473
+ * ```ts
474
+ * @Module({
475
+ * imports: [RelayBusModule.forFeature({ consumes: [OrderEvents], produces: [produce(BillingEvents)] })],
476
+ * providers: [OrderPlacedHandlers, OrderCancelledHandlers],
477
+ * })
478
+ * export class BillingModule {}
479
+ * ```
480
+ *
481
+ * @throws ConfigurationError for a `produce(...)` in `consumes` or a `consume(...)` in `produces`.
482
+ */
483
+ static forFeature(options) {
484
+ return {
485
+ module: exports.RelayBusFeatureModule,
486
+ providers: [
487
+ { provide: RelayBusFeatureContract, useValue: new RelayBusFeatureContract(options) }
488
+ ]
489
+ };
490
+ }
273
491
  };
274
492
  exports.RelayBusModule = __decorateClass([
275
493
  common.Module({
@@ -282,17 +500,40 @@ exports.RelayBusModule = __decorateClass([
282
500
  },
283
501
  exports.RelayBusHandlerExplorer,
284
502
  exports.RelayBusLifecycle,
285
- exports.RelayBusHealth
503
+ exports.RelayBusHealth,
504
+ exports.RelayBusTransactionHost
286
505
  ],
287
- exports: [core.RelayBus, exports.RelayBusHealth]
506
+ exports: [core.RelayBus, exports.RelayBusHealth, exports.RelayBusTransactionHost]
288
507
  })
289
508
  ], exports.RelayBusModule);
509
+ function RelayBusTransaction() {
510
+ return (target, propertyKey, descriptor) => {
511
+ const original = descriptor.value;
512
+ const label = `${target.constructor.name}.${String(propertyKey)}`;
513
+ const transactional = async function(...args) {
514
+ const bus = resolveBus(this, label);
515
+ if (bus.currentTransaction() !== void 0) {
516
+ return original.apply(this, args);
517
+ }
518
+ return bus.transaction(() => original.apply(this, args));
519
+ };
520
+ Object.defineProperty(transactional, "name", { value: original.name });
521
+ for (const key of Reflect.getOwnMetadataKeys(original)) {
522
+ Reflect.defineMetadata(key, Reflect.getOwnMetadata(key, original), transactional);
523
+ }
524
+ descriptor.value = transactional;
525
+ };
526
+ }
290
527
 
291
528
  Object.defineProperty(exports, "RelayBus", {
292
529
  enumerable: true,
293
530
  get: function () { return core.RelayBus; }
294
531
  });
295
532
  exports.EventConsumer = EventConsumer;
533
+ exports.RelayBusCatalogConsumer = RelayBusCatalogConsumer;
534
+ exports.RelayBusConsumer = RelayBusConsumer;
535
+ exports.RelayBusFeatureContract = RelayBusFeatureContract;
536
+ exports.RelayBusTransaction = RelayBusTransaction;
296
537
  exports.Subscribe = Subscribe;
297
538
  //# sourceMappingURL=index.cjs.map
298
539
  //# sourceMappingURL=index.cjs.map