@relaybus/nestjs 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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,72 +124,144 @@ 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);
178
+ }
179
+ if (violations.length > 0) {
180
+ throw new core.ConfigurationError(
181
+ `[RelayBusHandlerExplorer](assertModuleContracts) ${violations.join("; ")}`
182
+ );
183
+ }
184
+ }
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);
192
+ const lookup = instance;
193
+ for (const { declarations } of this.subscribedMethods(
194
+ Object.getPrototypeOf(instance)
195
+ )) {
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);
125
204
  }
126
205
  }
127
206
  }
128
- registerEventConsumers(instance, className, registeredBy) {
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) {
247
+ throw new core.ConfigurationError(
248
+ `Handler id '${id}' is used by both ${previous} and ${location}; pass a unique { id } to @RelayBusConsumer`
249
+ );
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
+ }) {
129
263
  const lookup = instance;
130
- for (const { catalog, options, selectedEntries } of readEventConsumerMetadata(
264
+ for (const { catalog, options, selectedEntries } of readCatalogConsumerMetadata(
131
265
  instance.constructor
132
266
  )) {
133
267
  const { name, entries } = core.describeCatalog(catalog);
@@ -136,12 +270,12 @@ exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
136
270
  const missing = handlers.filter((entry) => !isMethod(lookup, entry));
137
271
  if (missing.length > 0) {
138
272
  throw new core.ConfigurationError(
139
- `${className} uses @EventConsumer(${label}) but lacks the handler methods of events it consumes: ${missing.map((entry) => entry.handlerName).join(", ")}`
273
+ `${className} uses @RelayBusCatalogConsumer(${label}) but lacks the handler methods of events it consumes: ${missing.map((entry) => entry.handlerName).join(", ")}`
140
274
  );
141
275
  }
142
276
  if (handlers.length === 0) {
143
277
  throw new core.ConfigurationError(
144
- `${className} uses @EventConsumer(${label}) but defines none of its handler methods (${entries.map((entry) => entry.handlerName).join(", ")})`
278
+ `${className} uses @RelayBusCatalogConsumer(${label}) but defines none of its handler methods (${entries.map((entry) => entry.handlerName).join(", ")})`
145
279
  );
146
280
  }
147
281
  for (const entry of handlers) {
@@ -149,7 +283,7 @@ exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
149
283
  const previous = registeredBy.get(id);
150
284
  if (previous !== void 0) {
151
285
  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`
286
+ `Handler id '${id}' of @RelayBusCatalogConsumer(${label}) is already used by ${previous}; give the @RelayBusConsumer a unique { id } or drop it`
153
287
  );
154
288
  }
155
289
  registeredBy.set(id, `${className}.${entry.handlerName}`);
@@ -165,7 +299,7 @@ exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
165
299
  subscribedMethods(prototype) {
166
300
  const methods = [];
167
301
  for (const methodName of this.metadataScanner.getAllMethodNames(prototype)) {
168
- const declarations = readSubscribeMetadata(prototype, methodName);
302
+ const declarations = readConsumerMetadata(prototype, methodName);
169
303
  if (declarations.length > 0) {
170
304
  methods.push({ methodName, declarations });
171
305
  }
@@ -179,16 +313,16 @@ exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
179
313
  */
180
314
  assertNoHandlers(wrapper) {
181
315
  const type = wrapper.isFactory ? wrapper.token : wrapper.metatype;
182
- if (typeof type === "function" && readEventConsumerMetadata(type).length > 0) {
316
+ if (typeof type === "function" && readCatalogConsumerMetadata(type).length > 0) {
183
317
  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`
318
+ `${type.name} uses @RelayBusCatalogConsumer but its provider is request-scoped or transient (directly or through a dependency); handlers must be singleton providers`
185
319
  );
186
320
  }
187
321
  const prototype = typeof type === "function" ? type.prototype : null;
188
322
  const [first] = prototype === null ? [] : this.subscribedMethods(prototype);
189
323
  if (first !== void 0) {
190
324
  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`
325
+ `${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
326
  );
193
327
  }
194
328
  }
@@ -197,7 +331,8 @@ exports.RelayBusHandlerExplorer = __decorateClass([
197
331
  common.Injectable(),
198
332
  __decorateParam(0, common.Inject(core$1.DiscoveryService)),
199
333
  __decorateParam(1, common.Inject(core$1.MetadataScanner)),
200
- __decorateParam(2, common.Inject(core.RelayBus))
334
+ __decorateParam(2, common.Inject(core.RelayBus)),
335
+ __decorateParam(3, common.Inject(core$1.ModulesContainer))
201
336
  ], exports.RelayBusHandlerExplorer);
202
337
  function isMethod(instance, entry) {
203
338
  return typeof instance[entry.handlerName] === "function";
@@ -258,18 +393,86 @@ exports.RelayBusLifecycle = class RelayBusLifecycle {
258
393
  this.bus = bus;
259
394
  }
260
395
  bus;
396
+ onModuleInit() {
397
+ registerBus(this.bus);
398
+ }
261
399
  async onApplicationBootstrap() {
262
400
  await this.bus.start();
263
401
  }
264
402
  async beforeApplicationShutdown() {
265
403
  await this.bus.stop();
266
404
  }
405
+ /** After the bus stopped, so handlers still draining at shutdown can open transactions. */
406
+ onApplicationShutdown() {
407
+ unregisterBus(this.bus);
408
+ }
267
409
  };
268
410
  exports.RelayBusLifecycle = __decorateClass([
269
411
  common.Injectable(),
270
412
  __decorateParam(0, common.Inject(core.RelayBus))
271
413
  ], exports.RelayBusLifecycle);
414
+ exports.RelayBusFeatureModule = class RelayBusFeatureModule {
415
+ };
416
+ exports.RelayBusFeatureModule = __decorateClass([
417
+ common.Module({})
418
+ ], exports.RelayBusFeatureModule);
419
+ exports.RelayBusTransactionHost = class RelayBusTransactionHost {
420
+ constructor(bus) {
421
+ this.bus = bus;
422
+ }
423
+ bus;
424
+ /**
425
+ * The active transaction.
426
+ *
427
+ * @throws TransactionRequiredError when the caller runs outside every transaction.
428
+ */
429
+ current() {
430
+ const transaction = this.bus.currentTransaction();
431
+ if (transaction === void 0) {
432
+ throw new core.TransactionRequiredError(
433
+ "No relaybus transaction is active: call the repository from a @RelayBusTransaction() method or inside bus.transaction()"
434
+ );
435
+ }
436
+ return transaction;
437
+ }
438
+ /** The active transaction, or undefined outside every transaction. */
439
+ find() {
440
+ return this.bus.currentTransaction();
441
+ }
442
+ };
443
+ exports.RelayBusTransactionHost = __decorateClass([
444
+ common.Injectable(),
445
+ __decorateParam(0, common.Inject(core.RelayBus))
446
+ ], exports.RelayBusTransactionHost);
447
+
448
+ // src/relay-bus.module.ts
272
449
  exports.RelayBusModule = class RelayBusModule extends RelayBusConfigurableModule {
450
+ /**
451
+ * The relaybus contract of a feature module: import it in the module whose providers consume
452
+ * and produce the events. When the application initialises, every consumed event needs a
453
+ * `@RelayBusConsumer` method (or a `@RelayBusCatalogConsumer` class) among the providers of
454
+ * that module, spread over any classes and method names, and every produced event needs a
455
+ * route to a transport; otherwise the application fails to start naming the module and each
456
+ * missing event. Needs `RelayBusModule.forRoot` in the application.
457
+ *
458
+ * ```ts
459
+ * @Module({
460
+ * imports: [RelayBusModule.forFeature({ consumes: [OrderEvents], produces: [produce(BillingEvents)] })],
461
+ * providers: [OrderPlacedHandlers, OrderCancelledHandlers],
462
+ * })
463
+ * export class BillingModule {}
464
+ * ```
465
+ *
466
+ * @throws ConfigurationError for a `produce(...)` in `consumes` or a `consume(...)` in `produces`.
467
+ */
468
+ static forFeature(options) {
469
+ return {
470
+ module: exports.RelayBusFeatureModule,
471
+ providers: [
472
+ { provide: RelayBusFeatureContract, useValue: new RelayBusFeatureContract(options) }
473
+ ]
474
+ };
475
+ }
273
476
  };
274
477
  exports.RelayBusModule = __decorateClass([
275
478
  common.Module({
@@ -282,17 +485,40 @@ exports.RelayBusModule = __decorateClass([
282
485
  },
283
486
  exports.RelayBusHandlerExplorer,
284
487
  exports.RelayBusLifecycle,
285
- exports.RelayBusHealth
488
+ exports.RelayBusHealth,
489
+ exports.RelayBusTransactionHost
286
490
  ],
287
- exports: [core.RelayBus, exports.RelayBusHealth]
491
+ exports: [core.RelayBus, exports.RelayBusHealth, exports.RelayBusTransactionHost]
288
492
  })
289
493
  ], exports.RelayBusModule);
494
+ function RelayBusTransaction() {
495
+ return (target, propertyKey, descriptor) => {
496
+ const original = descriptor.value;
497
+ const label = `${target.constructor.name}.${String(propertyKey)}`;
498
+ const transactional = async function(...args) {
499
+ const bus = resolveBus(this, label);
500
+ if (bus.currentTransaction() !== void 0) {
501
+ return original.apply(this, args);
502
+ }
503
+ return bus.transaction(() => original.apply(this, args));
504
+ };
505
+ Object.defineProperty(transactional, "name", { value: original.name });
506
+ for (const key of Reflect.getOwnMetadataKeys(original)) {
507
+ Reflect.defineMetadata(key, Reflect.getOwnMetadata(key, original), transactional);
508
+ }
509
+ descriptor.value = transactional;
510
+ };
511
+ }
290
512
 
291
513
  Object.defineProperty(exports, "RelayBus", {
292
514
  enumerable: true,
293
515
  get: function () { return core.RelayBus; }
294
516
  });
295
517
  exports.EventConsumer = EventConsumer;
518
+ exports.RelayBusCatalogConsumer = RelayBusCatalogConsumer;
519
+ exports.RelayBusConsumer = RelayBusConsumer;
520
+ exports.RelayBusFeatureContract = RelayBusFeatureContract;
521
+ exports.RelayBusTransaction = RelayBusTransaction;
296
522
  exports.Subscribe = Subscribe;
297
523
  //# sourceMappingURL=index.cjs.map
298
524
  //# sourceMappingURL=index.cjs.map