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