@relaybus/nestjs 0.1.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
@@ -1,9 +1,9 @@
1
1
  'use strict';
2
2
 
3
+ require('reflect-metadata');
4
+ var core = require('@relaybus/core');
3
5
  var common = require('@nestjs/common');
4
6
  var core$1 = require('@nestjs/core');
5
- var core = require('@relaybus/core');
6
- require('reflect-metadata');
7
7
 
8
8
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
9
  var __decorateClass = (decorators, target, key, kind) => {
@@ -14,8 +14,106 @@ var __decorateClass = (decorators, target, key, kind) => {
14
14
  return result;
15
15
  };
16
16
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
17
+ var EVENT_CONSUMER_METADATA_KEY = /* @__PURE__ */ Symbol.for("relaybus.nestjs.event-consumer");
18
+ function RelayBusCatalogConsumer(catalog, {
19
+ events,
20
+ ...options
21
+ } = {}) {
22
+ const { name } = core.describeCatalog(catalog);
23
+ const selectedEntries = events === void 0 ? void 0 : core.consume(catalog, { events }).entries;
24
+ return (target) => {
25
+ const existing = Reflect.getOwnMetadata(EVENT_CONSUMER_METADATA_KEY, target) ?? [];
26
+ if (existing.some((metadata) => metadata.catalog === catalog)) {
27
+ throw new core.ConfigurationError(
28
+ `@RelayBusCatalogConsumer(${name ?? "catalog"}) is declared twice on ${target.name}; merge the declarations into one`
29
+ );
30
+ }
31
+ Reflect.defineMetadata(
32
+ EVENT_CONSUMER_METADATA_KEY,
33
+ [...existing, { catalog, options, selectedEntries }],
34
+ target
35
+ );
36
+ };
37
+ }
38
+ var EventConsumer = RelayBusCatalogConsumer;
39
+ function readCatalogConsumerMetadata(target) {
40
+ const lineage = [];
41
+ for (let current = target; current !== Function.prototype; ) {
42
+ lineage.unshift(current);
43
+ current = Object.getPrototypeOf(current);
44
+ }
45
+ const byCatalog = /* @__PURE__ */ new Map();
46
+ for (const type of lineage) {
47
+ const own = Reflect.getOwnMetadata(EVENT_CONSUMER_METADATA_KEY, type) ?? [];
48
+ for (const metadata of own) {
49
+ byCatalog.set(metadata.catalog, metadata);
50
+ }
51
+ }
52
+ return [...byCatalog.values()];
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
+ }
17
115
  var SUBSCRIBE_METADATA_KEY = /* @__PURE__ */ Symbol.for("relaybus.nestjs.subscribe");
18
- function Subscribe(event, options = {}) {
116
+ function RelayBusConsumer(event, options = {}) {
19
117
  return (target, propertyKey) => {
20
118
  const existing = Reflect.getOwnMetadata(SUBSCRIBE_METADATA_KEY, target, propertyKey) ?? [];
21
119
  Reflect.defineMetadata(
@@ -26,68 +124,182 @@ function Subscribe(event, options = {}) {
26
124
  );
27
125
  };
28
126
  }
29
- function readSubscribeMetadata(prototype, propertyKey) {
127
+ var Subscribe = RelayBusConsumer;
128
+ function readConsumerMetadata(prototype, propertyKey) {
30
129
  return Reflect.getMetadata(SUBSCRIBE_METADATA_KEY, prototype, propertyKey) ?? [];
31
130
  }
32
131
 
33
132
  // src/handler-explorer.ts
34
133
  exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
35
- constructor(discovery, metadataScanner, bus) {
134
+ constructor(discovery, metadataScanner, bus, modules) {
36
135
  this.discovery = discovery;
37
136
  this.metadataScanner = metadataScanner;
38
137
  this.bus = bus;
138
+ this.modules = modules;
39
139
  }
40
140
  discovery;
41
141
  metadataScanner;
42
142
  bus;
143
+ modules;
43
144
  onModuleInit() {
44
- const registeredBy = /* @__PURE__ */ new Map();
45
- const visited = /* @__PURE__ */ new Map();
145
+ const discovery = {
146
+ registeredBy: /* @__PURE__ */ new Map(),
147
+ visited: /* @__PURE__ */ new Map(),
148
+ consumers: /* @__PURE__ */ new WeakSet(),
149
+ consumedByModule: /* @__PURE__ */ new Map()
150
+ };
151
+ for (const wrapper of this.discovery.getProviders()) {
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 = [];
46
162
  for (const wrapper of this.discovery.getProviders()) {
47
- if (wrapper.isTransient || !wrapper.isDependencyTreeStatic()) {
48
- this.assertNoHandlers(wrapper);
163
+ const contract = wrapper.instance;
164
+ if (!(contract instanceof RelayBusFeatureContract)) {
49
165
  continue;
50
166
  }
51
- const instance = wrapper.instance;
52
- if (typeof instance !== "object" || instance === null) {
53
- 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`);
171
+ }
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`
175
+ );
176
+ }
54
177
  }
55
- const prototype = Object.getPrototypeOf(instance);
56
- if (prototype === null) {
57
- continue;
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);
58
198
  }
59
- const className = instance.constructor.name;
60
- for (const { methodName, declarations } of this.subscribedMethods(prototype)) {
61
- const seen = visited.get(instance) ?? /* @__PURE__ */ new Set();
62
- visited.set(instance, seen);
63
- if (seen.has(methodName)) {
64
- continue;
65
- }
66
- seen.add(methodName);
67
- const method = instance[methodName];
68
- const location = `${className}.${methodName}`;
69
- for (const { event, options } of declarations) {
70
- const id = options.id ?? location;
71
- const previous = registeredBy.get(id);
72
- if (previous !== void 0) {
73
- throw new core.ConfigurationError(
74
- `Handler id '${id}' is used by both ${previous} and ${location}; pass a unique { id } to @Subscribe`
75
- );
76
- }
77
- registeredBy.set(id, location);
78
- this.bus.subscribe(
79
- event,
80
- (envelope, context) => method.call(instance, envelope, context),
81
- { ...options, id }
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);
204
+ }
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) {
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
+ }) {
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 = selectedEntries ?? entries.filter((entry) => isMethod(lookup, entry));
270
+ const missing = handlers.filter((entry) => !isMethod(lookup, entry));
271
+ if (missing.length > 0) {
272
+ throw new core.ConfigurationError(
273
+ `${className} uses @RelayBusCatalogConsumer(${label}) but lacks the handler methods of events it consumes: ${missing.map((entry) => entry.handlerName).join(", ")}`
274
+ );
275
+ }
276
+ if (handlers.length === 0) {
277
+ throw new core.ConfigurationError(
278
+ `${className} uses @RelayBusCatalogConsumer(${label}) but defines none of its handler methods (${entries.map((entry) => entry.handlerName).join(", ")})`
279
+ );
280
+ }
281
+ for (const entry of handlers) {
282
+ const id = `${className}.${entry.handlerName}`;
283
+ const previous = registeredBy.get(id);
284
+ if (previous !== void 0) {
285
+ throw new core.ConfigurationError(
286
+ `Handler id '${id}' of @RelayBusCatalogConsumer(${label}) is already used by ${previous}; give the @RelayBusConsumer a unique { id } or drop it`
82
287
  );
83
288
  }
289
+ registeredBy.set(id, `${className}.${entry.handlerName}`);
290
+ const method = lookup[entry.handlerName];
291
+ this.bus.subscribe(
292
+ entry.eventClass,
293
+ (envelope, context) => method.call(instance, envelope, context),
294
+ { ...options, id }
295
+ );
84
296
  }
85
297
  }
86
298
  }
87
299
  subscribedMethods(prototype) {
88
300
  const methods = [];
89
301
  for (const methodName of this.metadataScanner.getAllMethodNames(prototype)) {
90
- const declarations = readSubscribeMetadata(prototype, methodName);
302
+ const declarations = readConsumerMetadata(prototype, methodName);
91
303
  if (declarations.length > 0) {
92
304
  methods.push({ methodName, declarations });
93
305
  }
@@ -101,11 +313,16 @@ exports.RelayBusHandlerExplorer = class RelayBusHandlerExplorer {
101
313
  */
102
314
  assertNoHandlers(wrapper) {
103
315
  const type = wrapper.isFactory ? wrapper.token : wrapper.metatype;
316
+ if (typeof type === "function" && readCatalogConsumerMetadata(type).length > 0) {
317
+ throw new core.ConfigurationError(
318
+ `${type.name} uses @RelayBusCatalogConsumer but its provider is request-scoped or transient (directly or through a dependency); handlers must be singleton providers`
319
+ );
320
+ }
104
321
  const prototype = typeof type === "function" ? type.prototype : null;
105
322
  const [first] = prototype === null ? [] : this.subscribedMethods(prototype);
106
323
  if (first !== void 0) {
107
324
  throw new core.ConfigurationError(
108
- `${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`
109
326
  );
110
327
  }
111
328
  }
@@ -114,8 +331,12 @@ exports.RelayBusHandlerExplorer = __decorateClass([
114
331
  common.Injectable(),
115
332
  __decorateParam(0, common.Inject(core$1.DiscoveryService)),
116
333
  __decorateParam(1, common.Inject(core$1.MetadataScanner)),
117
- __decorateParam(2, common.Inject(core.RelayBus))
334
+ __decorateParam(2, common.Inject(core.RelayBus)),
335
+ __decorateParam(3, common.Inject(core$1.ModulesContainer))
118
336
  ], exports.RelayBusHandlerExplorer);
337
+ function isMethod(instance, entry) {
338
+ return typeof instance[entry.handlerName] === "function";
339
+ }
119
340
  var {
120
341
  ConfigurableModuleClass: RelayBusConfigurableModule,
121
342
  MODULE_OPTIONS_TOKEN: RELAYBUS_MODULE_OPTIONS
@@ -172,18 +393,86 @@ exports.RelayBusLifecycle = class RelayBusLifecycle {
172
393
  this.bus = bus;
173
394
  }
174
395
  bus;
396
+ onModuleInit() {
397
+ registerBus(this.bus);
398
+ }
175
399
  async onApplicationBootstrap() {
176
400
  await this.bus.start();
177
401
  }
178
402
  async beforeApplicationShutdown() {
179
403
  await this.bus.stop();
180
404
  }
405
+ /** After the bus stopped, so handlers still draining at shutdown can open transactions. */
406
+ onApplicationShutdown() {
407
+ unregisterBus(this.bus);
408
+ }
181
409
  };
182
410
  exports.RelayBusLifecycle = __decorateClass([
183
411
  common.Injectable(),
184
412
  __decorateParam(0, common.Inject(core.RelayBus))
185
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
186
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
+ }
187
476
  };
188
477
  exports.RelayBusModule = __decorateClass([
189
478
  common.Module({
@@ -196,16 +485,40 @@ exports.RelayBusModule = __decorateClass([
196
485
  },
197
486
  exports.RelayBusHandlerExplorer,
198
487
  exports.RelayBusLifecycle,
199
- exports.RelayBusHealth
488
+ exports.RelayBusHealth,
489
+ exports.RelayBusTransactionHost
200
490
  ],
201
- exports: [core.RelayBus, exports.RelayBusHealth]
491
+ exports: [core.RelayBus, exports.RelayBusHealth, exports.RelayBusTransactionHost]
202
492
  })
203
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
+ }
204
512
 
205
513
  Object.defineProperty(exports, "RelayBus", {
206
514
  enumerable: true,
207
515
  get: function () { return core.RelayBus; }
208
516
  });
517
+ exports.EventConsumer = EventConsumer;
518
+ exports.RelayBusCatalogConsumer = RelayBusCatalogConsumer;
519
+ exports.RelayBusConsumer = RelayBusConsumer;
520
+ exports.RelayBusFeatureContract = RelayBusFeatureContract;
521
+ exports.RelayBusTransaction = RelayBusTransaction;
209
522
  exports.Subscribe = Subscribe;
210
523
  //# sourceMappingURL=index.cjs.map
211
524
  //# sourceMappingURL=index.cjs.map