@ebarahona/loopback-transport-core 1.0.0 → 1.2.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.
Files changed (105) hide show
  1. package/README.md +472 -35
  2. package/dist/client/client-proxy.d.ts +73 -42
  3. package/dist/client/client-proxy.js +71 -42
  4. package/dist/client/client-proxy.js.map +1 -1
  5. package/dist/client/index.d.ts +1 -1
  6. package/dist/client/index.js +3 -15
  7. package/dist/client/index.js.map +1 -1
  8. package/dist/context/execution-context.d.ts +58 -11
  9. package/dist/context/execution-context.js +62 -17
  10. package/dist/context/execution-context.js.map +1 -1
  11. package/dist/context/index.d.ts +2 -1
  12. package/dist/context/index.js +3 -15
  13. package/dist/context/index.js.map +1 -1
  14. package/dist/decorators/constants.d.ts +20 -10
  15. package/dist/decorators/constants.js +6 -2
  16. package/dist/decorators/constants.js.map +1 -1
  17. package/dist/decorators/event-handler.decorator.d.ts +10 -7
  18. package/dist/decorators/event-handler.decorator.js +18 -13
  19. package/dist/decorators/event-handler.decorator.js.map +1 -1
  20. package/dist/decorators/index.d.ts +5 -4
  21. package/dist/decorators/index.js +11 -18
  22. package/dist/decorators/index.js.map +1 -1
  23. package/dist/decorators/message-handler.decorator.d.ts +8 -5
  24. package/dist/decorators/message-handler.decorator.js +16 -11
  25. package/dist/decorators/message-handler.decorator.js.map +1 -1
  26. package/dist/decorators/payload.decorator.d.ts +16 -7
  27. package/dist/decorators/payload.decorator.js +16 -8
  28. package/dist/decorators/payload.decorator.js.map +1 -1
  29. package/dist/discovery/discovery.service.d.ts +143 -0
  30. package/dist/discovery/discovery.service.js +165 -0
  31. package/dist/discovery/discovery.service.js.map +1 -0
  32. package/dist/discovery/event-handler-discoverer.d.ts +19 -0
  33. package/dist/discovery/event-handler-discoverer.js +50 -0
  34. package/dist/discovery/event-handler-discoverer.js.map +1 -0
  35. package/dist/discovery/handler-discoverer.d.ts +48 -0
  36. package/dist/discovery/handler-discoverer.js +3 -0
  37. package/dist/discovery/handler-discoverer.js.map +1 -0
  38. package/dist/discovery/handler-kind.d.ts +23 -0
  39. package/dist/discovery/handler-kind.js +14 -0
  40. package/dist/discovery/handler-kind.js.map +1 -0
  41. package/dist/discovery/index.d.ts +7 -0
  42. package/dist/discovery/index.js +13 -0
  43. package/dist/discovery/index.js.map +1 -0
  44. package/dist/discovery/message-handler-discoverer.d.ts +19 -0
  45. package/dist/discovery/message-handler-discoverer.js +50 -0
  46. package/dist/discovery/message-handler-discoverer.js.map +1 -0
  47. package/dist/discovery/registered-handler.d.ts +35 -0
  48. package/dist/discovery/registered-handler.js +3 -0
  49. package/dist/discovery/registered-handler.js.map +1 -0
  50. package/dist/helpers/errors.d.ts +61 -0
  51. package/dist/helpers/errors.js +80 -0
  52. package/dist/helpers/errors.js.map +1 -0
  53. package/dist/helpers/index.d.ts +2 -0
  54. package/dist/helpers/index.js +14 -0
  55. package/dist/helpers/index.js.map +1 -0
  56. package/dist/helpers/register-server.d.ts +44 -0
  57. package/dist/helpers/register-server.js +72 -0
  58. package/dist/helpers/register-server.js.map +1 -0
  59. package/dist/index.d.ts +15 -7
  60. package/dist/index.js +33 -9
  61. package/dist/index.js.map +1 -1
  62. package/dist/interfaces/index.d.ts +4 -4
  63. package/dist/interfaces/index.js +0 -18
  64. package/dist/interfaces/index.js.map +1 -1
  65. package/dist/interfaces/message-handler.interface.d.ts +12 -6
  66. package/dist/interfaces/packet.interface.d.ts +20 -2
  67. package/dist/interfaces/transport-client.interface.d.ts +25 -13
  68. package/dist/interfaces/transport-server.interface.d.ts +36 -10
  69. package/dist/keys.d.ts +143 -46
  70. package/dist/keys.js +142 -68
  71. package/dist/keys.js.map +1 -1
  72. package/dist/registry/handler-registry.d.ts +94 -24
  73. package/dist/registry/handler-registry.js +279 -89
  74. package/dist/registry/handler-registry.js.map +1 -1
  75. package/dist/registry/index.d.ts +1 -1
  76. package/dist/registry/index.js +3 -15
  77. package/dist/registry/index.js.map +1 -1
  78. package/dist/serializers/cloudevents-serializer.d.ts +107 -0
  79. package/dist/serializers/cloudevents-serializer.js +130 -0
  80. package/dist/serializers/cloudevents-serializer.js.map +1 -0
  81. package/dist/serializers/index.d.ts +4 -1
  82. package/dist/serializers/index.js +7 -15
  83. package/dist/serializers/index.js.map +1 -1
  84. package/dist/serializers/serializer.interface.d.ts +49 -16
  85. package/dist/serializers/serializer.interface.js +41 -15
  86. package/dist/serializers/serializer.interface.js.map +1 -1
  87. package/dist/server/index.d.ts +2 -1
  88. package/dist/server/index.js +3 -15
  89. package/dist/server/index.js.map +1 -1
  90. package/dist/server/server-base.d.ts +125 -52
  91. package/dist/server/server-base.js +168 -57
  92. package/dist/server/server-base.js.map +1 -1
  93. package/dist/transport.component.d.ts +21 -8
  94. package/dist/transport.component.js +41 -18
  95. package/dist/transport.component.js.map +1 -1
  96. package/dist/transport.observer.d.ts +91 -0
  97. package/dist/transport.observer.js +297 -0
  98. package/dist/transport.observer.js.map +1 -0
  99. package/dist/utils/normalize-pattern.d.ts +11 -6
  100. package/dist/utils/normalize-pattern.js +18 -13
  101. package/dist/utils/normalize-pattern.js.map +1 -1
  102. package/package.json +58 -16
  103. package/dist/transport-booter.d.ts +0 -31
  104. package/dist/transport-booter.js +0 -117
  105. package/dist/transport-booter.js.map +0 -1
package/README.md CHANGED
@@ -8,25 +8,29 @@ This package is **transport core only** -- it provides the framework abstraction
8
8
  npm install @ebarahona/loopback-transport-core
9
9
  ```
10
10
 
11
+ > Part of the [`@ebarahona/loopback-*` plugin portfolio](https://github.com/ebarahona/loopback-plugins). See the [portfolio roadmap](https://github.com/ebarahona/loopback-plugins/blob/main/ROADMAP.md) for sibling plugins (`loopback-connector-mongodb`, planned `loopback-graphql`) and the shared infrastructure they use.
12
+
11
13
  ## What This Provides
12
14
 
13
- | Export | Purpose |
14
- |---|---|
15
- | `TransportComponent` | LoopBack 4 component (registers registry + booter) |
16
- | `@messageHandler(pattern)` | Request/response handler decorator |
17
- | `@eventHandler(pattern)` | Fire-and-forget event handler decorator |
18
- | `@payload()` | Injects the message data (transport invocation context, not HTTP) |
19
- | `@transportCtx()` | Injects the broker-specific context (transport invocation context, not HTTP) |
20
- | `ClientProxy` | Abstract client with `send()` (cold Observable) and `emit()` (Promise) |
21
- | `ServerBase` | Abstract server with handler registry, dispatch, and structured results |
22
- | `ExecutionContext` | Unified context across HTTP, RPC, and event transports |
23
- | `HandlerResult` | Structured settlement result for adapter ack/nack decisions |
24
- | `TransportBindings` | Typed binding keys and server registration helpers |
25
- | `normalizePattern()` | Deterministic pattern key generation |
26
- | `Serializer` / `Deserializer` | Pluggable serialization (JSON default) |
27
- | `TransportServer` / `TransportClient` | Adapter interfaces |
28
- | `TransportStatus` | Connection status type (`connected`, `disconnected`, `reconnecting`, `error`) |
29
- | `ReadPacket` / `WritePacket` / `PacketId` | Request/response correlation types |
15
+ | Export | Purpose |
16
+ | ----------------------------------------- | ----------------------------------------------------------------------------- |
17
+ | `TransportComponent` | LoopBack 4 component (registers registry + booter) |
18
+ | `@messageHandler(pattern)` | Request/response handler decorator |
19
+ | `@eventHandler(pattern)` | Fire-and-forget event handler decorator |
20
+ | `@payload()` | Injects the message data (transport invocation context, not HTTP) |
21
+ | `@transportCtx()` | Injects the broker-specific context (transport invocation context, not HTTP) |
22
+ | `ClientProxy` | Abstract client with `send()` (cold Observable) and `emit()` (Promise) |
23
+ | `ServerBase` | Abstract server with handler registry, dispatch, and structured results |
24
+ | `ExecutionContext` | Unified context across HTTP, RPC, and event transports |
25
+ | `HandlerResult` | Structured settlement result for adapter ack/nack decisions |
26
+ | `TransportBindings` | Typed binding keys and server registration helpers |
27
+ | `DiscoveryService` | Read-only enumeration of every discovered handler (NestJS-style) |
28
+ | `HandlerDiscoverer` | Plugin extension point for custom decorator vocabularies |
29
+ | `normalizePattern()` | Deterministic pattern key generation |
30
+ | `Serializer` / `Deserializer` | Pluggable serialization (JSON default) |
31
+ | `TransportServer` / `TransportClient` | Adapter interfaces |
32
+ | `TransportStatus` | Connection status type (`connected`, `disconnected`, `reconnecting`, `error`) |
33
+ | `ReadPacket` / `WritePacket` / `PacketId` | Request/response correlation types |
30
34
 
31
35
  ## Usage
32
36
 
@@ -34,7 +38,10 @@ npm install @ebarahona/loopback-transport-core
34
38
 
35
39
  ```typescript
36
40
  import {Application} from '@loopback/core';
37
- import {TransportComponent, TransportBindings} from '@ebarahona/loopback-transport-core';
41
+ import {
42
+ TransportComponent,
43
+ TransportBindings,
44
+ } from '@ebarahona/loopback-transport-core';
38
45
 
39
46
  const app = new Application();
40
47
  app.component(TransportComponent);
@@ -46,8 +53,16 @@ Works with `RestApplication` for hybrid HTTP + transport apps, or plain `Applica
46
53
 
47
54
  Patterns can be strings or objects. Object patterns are normalized (deep key sort) so `{cmd: 'get', service: 'order'}` and `{service: 'order', cmd: 'get'}` match the same handler.
48
55
 
56
+ <details>
57
+ <summary><b>Show example: controller with message and event handlers</b></summary>
58
+
49
59
  ```typescript
50
- import {messageHandler, eventHandler, payload, transportCtx} from '@ebarahona/loopback-transport-core';
60
+ import {
61
+ messageHandler,
62
+ eventHandler,
63
+ payload,
64
+ transportCtx,
65
+ } from '@ebarahona/loopback-transport-core';
51
66
 
52
67
  class OrderController {
53
68
  // String pattern -- request/response
@@ -80,14 +95,22 @@ class OrderController {
80
95
  }
81
96
  ```
82
97
 
98
+ </details>
99
+
83
100
  Non-JSON values in object patterns (undefined, functions, symbols, NaN, Infinity, BigInt, Date, RegExp, Map, Set, class instances) throw at decoration time.
84
101
 
85
102
  ### Client (Producer)
86
103
 
104
+ <details>
105
+ <summary><b>Show example: injecting a TransportClient and using send/emit</b></summary>
106
+
87
107
  ```typescript
88
108
  import {inject} from '@loopback/core';
89
109
  import {lastValueFrom} from 'rxjs';
90
- import {TransportBindings, TransportClient} from '@ebarahona/loopback-transport-core';
110
+ import {
111
+ TransportBindings,
112
+ TransportClient,
113
+ } from '@ebarahona/loopback-transport-core';
91
114
 
92
115
  class NotificationService {
93
116
  constructor(
@@ -109,10 +132,15 @@ class NotificationService {
109
132
  }
110
133
  ```
111
134
 
135
+ </details>
136
+
112
137
  ### Registering Transport Servers
113
138
 
114
139
  Transport adapters register with typed helpers. Class and provider registrations use singleton scope so the same instance receives handlers and gets started.
115
140
 
141
+ <details>
142
+ <summary><b>Show example: three server-registration shapes</b></summary>
143
+
116
144
  ```typescript
117
145
  import {TransportBindings} from '@ebarahona/loopback-transport-core';
118
146
 
@@ -126,10 +154,15 @@ TransportBindings.registerServerClass(app, 'kafka', KafkaServer);
126
154
  TransportBindings.registerServerProvider(app, 'kafka', KafkaServerProvider);
127
155
  ```
128
156
 
157
+ </details>
158
+
129
159
  ### Unified Execution Context
130
160
 
131
161
  `ExecutionContext` provides a NestJS-style context model that adapters or higher-level integrations can use across HTTP, RPC, and event transports:
132
162
 
163
+ <details>
164
+ <summary><b>Show example: building an event ExecutionContext</b></summary>
165
+
133
166
  ```typescript
134
167
  const ctx = ExecutionContext.forEvent(args, handler, controllerClass, {
135
168
  getData: () => eventData,
@@ -142,20 +175,21 @@ const event = ctx.switchToEvent();
142
175
  const pattern = event.getPattern();
143
176
  ```
144
177
 
178
+ </details>
179
+
145
180
  Constructed via immutable factory methods (`forHttp`, `forRpc`, `forEvent`). `switchToX()` validates the context type before returning.
146
181
 
147
182
  ## Adapter Authors
148
183
 
149
184
  ### Extending ServerBase
150
185
 
186
+ <details>
187
+ <summary><b>Show example: KafkaServer extending ServerBase</b></summary>
188
+
151
189
  ```typescript
152
190
  import {ServerBase} from '@ebarahona/loopback-transport-core';
153
191
 
154
192
  class KafkaServer extends ServerBase {
155
- constructor() {
156
- super({handlerTimeoutMs: 10_000}); // default 30s
157
- }
158
-
159
193
  async listen(): Promise<void> {
160
194
  await this.consumer.connect();
161
195
  await this.consumer.subscribe({topics: this.getTopics()});
@@ -178,11 +212,16 @@ class KafkaServer extends ServerBase {
178
212
  this.setStatus('disconnected');
179
213
  }
180
214
 
181
- unwrap<T>(): T { return this.consumer as T; }
215
+ unwrap<T>(): T {
216
+ return this.consumer as T;
217
+ }
182
218
  }
183
219
  ```
184
220
 
221
+ </details>
222
+
185
223
  **What the base class handles:**
224
+
186
225
  - Handler registration and lookup via `addHandler()` / `getHandlersByPattern()`
187
226
  - `clearHandlers()` for restart/rebind safety (called by the booter)
188
227
  - Message dispatch with `handleMessage()` returning structured `HandlerResult`
@@ -192,12 +231,16 @@ class KafkaServer extends ServerBase {
192
231
  - `dispose()` for permanent shutdown (completes the status stream)
193
232
 
194
233
  **What adapters implement:**
234
+
195
235
  - `listen()` -- connect to broker, start consuming, call `setStatus('connected')`
196
236
  - `close()` -- disconnect from broker, call `setStatus('disconnected')`
197
237
  - `unwrap<T>()` -- expose the native client
198
238
 
199
239
  ### Extending ClientProxy
200
240
 
241
+ <details>
242
+ <summary><b>Show example: KafkaClient extending ClientProxy</b></summary>
243
+
201
244
  ```typescript
202
245
  import {ClientProxy} from '@ebarahona/loopback-transport-core';
203
246
 
@@ -211,7 +254,9 @@ class KafkaClient extends ClientProxy {
211
254
  await this.producer.disconnect();
212
255
  }
213
256
 
214
- unwrap<T>(): T { return this.producer as T; }
257
+ unwrap<T>(): T {
258
+ return this.producer as T;
259
+ }
215
260
 
216
261
  protected publish(packet, callback): () => void {
217
262
  // Send request, register correlation callback
@@ -223,7 +268,10 @@ class KafkaClient extends ClientProxy {
223
268
  }
224
269
  ```
225
270
 
271
+ </details>
272
+
226
273
  **What the base class handles:**
274
+
227
275
  - Lazy connection on first `send()` / `emit()`
228
276
  - Concurrent connect deduplication
229
277
  - Epoch-based stale connection detection (close during connect)
@@ -233,6 +281,7 @@ class KafkaClient extends ClientProxy {
233
281
  - Status stream (`status$`)
234
282
 
235
283
  **What adapters implement:**
284
+
236
285
  - `connect()` -- establish broker connection (idempotent)
237
286
  - `doClose()` -- tear down native connection (may be called with partial connect)
238
287
  - `unwrap<T>()` -- expose the native client
@@ -243,6 +292,9 @@ class KafkaClient extends ClientProxy {
243
292
 
244
293
  `handleMessage()` returns a structured `HandlerResult` for broker settlement:
245
294
 
295
+ <details>
296
+ <summary><b>Show example: dispatching on HandlerResult.outcome</b></summary>
297
+
246
298
  ```typescript
247
299
  const result = await this.handleMessage(request, respond, context);
248
300
 
@@ -262,6 +314,220 @@ switch (result.outcome) {
262
314
  }
263
315
  ```
264
316
 
317
+ </details>
318
+
319
+ ## Custom decorator vocabularies
320
+
321
+ Transport-core ships with two decorator vocabularies (`@messageHandler` and `@eventHandler`), but the discovery layer is open. Plugins (gRPC routes, cron schedules, WebSocket subscriptions, etc.) can contribute their own decorators by implementing `HandlerDiscoverer` and binding it under `HANDLER_DISCOVERER_TAG`. The handler registry consults every bound discoverer for every controller at boot, so the built-in decorators and any plugin decorators coexist without modifications to transport-core.
322
+
323
+ ```typescript
324
+ import type {Constructor} from '@loopback/core';
325
+ import type {
326
+ HandlerKind,
327
+ HandlerOptions,
328
+ } from '@ebarahona/loopback-transport-core';
329
+
330
+ interface DiscoveredHandler {
331
+ pattern: string | Record<string, unknown>;
332
+ transport: string; // '*' for wildcard
333
+ kind: HandlerKind; // open string type; see below
334
+ methodName: string;
335
+ options?: HandlerOptions;
336
+ }
337
+
338
+ interface HandlerDiscoverer {
339
+ readonly id: string;
340
+ discover(
341
+ controllerClass: Constructor<unknown>,
342
+ ): DiscoveredHandler[] | Promise<DiscoveredHandler[]>;
343
+ }
344
+ ```
345
+
346
+ The `kind` field is an open string type (`HandlerKind = 'request' | 'event' | (string & {})`). The well-known constants `HANDLER_KIND_REQUEST` and `HANDLER_KIND_EVENT` are exported for the default decorators; plugins are free to declare additional kinds such as `'cron'`, `'subscription'`, or `'stream'` without waiting for a transport-core release.
347
+
348
+ A hypothetical `loopback-transport-grpc` plugin defining `@grpcRoute` would look like:
349
+
350
+ <details>
351
+ <summary><b>Show example: a plugin contributing its own decorator vocabulary</b></summary>
352
+
353
+ ```typescript
354
+ import {
355
+ Binding,
356
+ BindingScope,
357
+ Component,
358
+ Constructor,
359
+ MetadataInspector,
360
+ } from '@loopback/core';
361
+ import {MetadataAccessor, MethodDecoratorFactory} from '@loopback/metadata';
362
+ import {
363
+ HANDLER_DISCOVERER_TAG,
364
+ type DiscoveredHandler,
365
+ type HandlerDiscoverer,
366
+ } from '@ebarahona/loopback-transport-core';
367
+
368
+ interface GrpcRouteMetadata {
369
+ service: string;
370
+ method: string;
371
+ }
372
+
373
+ const GRPC_ROUTE_METADATA = MetadataAccessor.create<
374
+ GrpcRouteMetadata,
375
+ MethodDecorator
376
+ >('grpc:route');
377
+
378
+ export function grpcRoute(service: string, method: string): MethodDecorator {
379
+ return MethodDecoratorFactory.createDecorator<GrpcRouteMetadata>(
380
+ GRPC_ROUTE_METADATA,
381
+ {service, method},
382
+ );
383
+ }
384
+
385
+ export class GrpcRouteDiscoverer implements HandlerDiscoverer {
386
+ readonly id = 'grpc';
387
+
388
+ discover(controllerClass: Constructor<unknown>): DiscoveredHandler[] {
389
+ const methods = MetadataInspector.getAllMethodMetadata<GrpcRouteMetadata>(
390
+ GRPC_ROUTE_METADATA.key,
391
+ controllerClass.prototype,
392
+ );
393
+ if (!methods) return [];
394
+ return Object.entries(methods).map(([methodName, m]) => ({
395
+ pattern: `${m.service}/${m.method}`,
396
+ transport: 'grpc',
397
+ kind: 'request' as const,
398
+ methodName,
399
+ }));
400
+ }
401
+ }
402
+
403
+ export class GrpcComponent implements Component {
404
+ readonly bindings = [
405
+ Binding.bind('grpc.discoverer')
406
+ .toClass(GrpcRouteDiscoverer)
407
+ .tag(HANDLER_DISCOVERER_TAG)
408
+ .inScope(BindingScope.SINGLETON),
409
+ ];
410
+ }
411
+ ```
412
+
413
+ </details>
414
+
415
+ The built-in `@messageHandler` and `@eventHandler` are themselves implemented as default `HandlerDiscoverer` instances (`MessageHandlerDiscoverer`, `EventHandlerDiscoverer`) bound by `TransportComponent`, so plugin-supplied vocabularies operate on equal footing with the framework.
416
+
417
+ ## Universal discovery
418
+
419
+ `DiscoveryService` is a read-only enumeration of every handler the application contains, regardless of which `HandlerDiscoverer` produced it or which transport server serves it. This mirrors the `DiscoveryService` exposed by NestJS's `@nestjs/core` package. Inject it from `TransportBindings.DISCOVERY_SERVICE` after boot to build cross-cutting integrations: schema export, audit registries, OpenAPI generation, observability boots.
420
+
421
+ Available methods:
422
+
423
+ - `getHandlers()`: every `RegisteredHandler` across every discoverer.
424
+ - `getHandlersForTransport(transport)`: handlers bound to a specific transport id.
425
+ - `getHandlersByKind(kind)`: filter by `'request'` or `'event'`.
426
+ - `getHandlersByDiscoverer(discovererId)`: handlers produced by one `HandlerDiscoverer`.
427
+ - `getDiscoverers()`: the live list of bound discoverers.
428
+ - `getTransportServers()`: every registered `TransportServer`.
429
+ - `getSerializers()`: every binding tagged with `SERIALIZER_TAG`.
430
+ - `getDeserializers()`: every binding tagged with `DESERIALIZER_TAG`.
431
+ - `getSerializerForTransport(transport)`: resolves the serializer for a transport using the same precedence as `ServerBase.resolveSerializer` (transport-scoped tag, then generic tag).
432
+ - `getDeserializerForTransport(transport)`: same precedence, applied to the inbound side.
433
+
434
+ <details>
435
+ <summary><b>Show example: a boot-time audit pass that catalogues every handler</b></summary>
436
+
437
+ ```typescript
438
+ import {inject, lifeCycleObserver, LifeCycleObserver} from '@loopback/core';
439
+ import {
440
+ DiscoveryService,
441
+ TransportBindings,
442
+ } from '@ebarahona/loopback-transport-core';
443
+
444
+ @lifeCycleObserver()
445
+ export class HandlerAuditObserver implements LifeCycleObserver {
446
+ constructor(
447
+ @inject(TransportBindings.DISCOVERY_SERVICE)
448
+ private readonly discovery: DiscoveryService,
449
+ ) {}
450
+
451
+ async start(): Promise<void> {
452
+ for (const h of this.discovery.getHandlers()) {
453
+ console.log(
454
+ `[audit] ${h.discovererId} ${h.transport} ${h.kind} ` +
455
+ `${h.controllerClass.name}.${h.methodName} pattern=${JSON.stringify(h.pattern)}`,
456
+ );
457
+ }
458
+ }
459
+ }
460
+ ```
461
+
462
+ </details>
463
+
464
+ ## Boot-time validation
465
+
466
+ At application start, `HandlerRegistry.bindToServers` validates the
467
+ registered handlers and transport servers against four common
468
+ misconfigurations. The intent is to fail loud at boot rather than
469
+ silently drop events at runtime.
470
+
471
+ | Misconfiguration | Default behavior | Configurable |
472
+ | ----------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------------- |
473
+ | Handler references a transport name no `TransportServer` provides | Throws `TransportConfigError` | Yes, via `TransportBindings.STRICT_BINDING` |
474
+ | Two `TransportServer` bindings share the same `NAME` tag | Throws `TransportConfigError` | No |
475
+ | `TransportServer` is bound without a `NAME` tag | Debug log; server is skipped (no `listen()`, no handlers) | No |
476
+ | Handlers exist but no `TransportServer` bindings are registered | Debug log | No |
477
+
478
+ The first guard is the loudest, because misnamed transports silently
479
+ drop events at runtime if not caught at boot. The error lists every
480
+ orphaned handler with its controller, method, pattern, and discoverer
481
+ id, plus the set of known transport names so misspellings are easy to
482
+ spot.
483
+
484
+ To suppress the throw (for example to register handlers before their
485
+ transport server is ready), set:
486
+
487
+ ```typescript
488
+ app.bind(TransportBindings.STRICT_BINDING).to(false);
489
+ ```
490
+
491
+ before calling `app.start()`. Orphaned handlers will then be logged
492
+ via `debug` instead of throwing.
493
+
494
+ ## Cross-cutting concerns
495
+
496
+ Cross-cutting work (metrics, tracing, audit, authorization) goes through LoopBack 4's standard `@globalInterceptor` mechanism. Every transport handler runs through the same `invokeMethod` pipeline as HTTP controllers, so a single global interceptor instruments the whole application without per-decorator wiring or a separate wrapper extension point.
497
+
498
+ <details>
499
+ <summary><b>Show example: a global metrics interceptor timing every method invocation</b></summary>
500
+
501
+ ```typescript
502
+ import {
503
+ globalInterceptor,
504
+ Interceptor,
505
+ InvocationContext,
506
+ Provider,
507
+ ValueOrPromise,
508
+ } from '@loopback/core';
509
+
510
+ @globalInterceptor('metrics', {tags: {name: 'metrics'}})
511
+ export class MetricsInterceptor implements Provider<Interceptor> {
512
+ value(): Interceptor {
513
+ return async (invocationCtx: InvocationContext, next) => {
514
+ const label = `${invocationCtx.targetClass.name}.${invocationCtx.methodName}`;
515
+ const start = process.hrtime.bigint();
516
+ try {
517
+ return await next();
518
+ } finally {
519
+ const ns = Number(process.hrtime.bigint() - start);
520
+ metrics.observe(label, ns / 1_000_000);
521
+ }
522
+ };
523
+ }
524
+ }
525
+ ```
526
+
527
+ </details>
528
+
529
+ If an interceptor needs handler metadata (pattern, transport, discovererId) at invocation time, inject `DiscoveryService` into the provider and look up the entry by `invocationCtx.targetClass` and `invocationCtx.methodName`.
530
+
265
531
  ## Lifecycle Behavior
266
532
 
267
533
  - **Startup**: The `TransportBooter` lifecycle observer discovers decorated handlers from controllers, binds them to registered servers, and starts servers sequentially. If any server fails to start, all previously started servers are rolled back.
@@ -273,25 +539,196 @@ switch (result.outcome) {
273
539
 
274
540
  Companion adapter packages, each implementing `TransportServer` and `TransportClient`:
275
541
 
276
- | Package | Transport | Native Client |
277
- |---|---|---|
278
- | `@ebarahona/loopback-transport-kafka` | Apache Kafka | `kafkajs` |
279
- | `@ebarahona/loopback-transport-rabbitmq` | RabbitMQ | `amqplib` |
280
- | `@ebarahona/loopback-transport-grpc` | gRPC | `@grpc/grpc-js` |
281
- | `@ebarahona/loopback-transport-mqtt` | MQTT | `mqtt` |
282
- | `@ebarahona/loopback-transport-nats` | NATS | `nats` |
542
+ | Package | Transport | Native Client |
543
+ | ---------------------------------------- | ------------ | --------------- |
544
+ | `@ebarahona/loopback-transport-kafka` | Apache Kafka | `kafkajs` |
545
+ | `@ebarahona/loopback-transport-rabbitmq` | RabbitMQ | `amqplib` |
546
+ | `@ebarahona/loopback-transport-grpc` | gRPC | `@grpc/grpc-js` |
547
+ | `@ebarahona/loopback-transport-mqtt` | MQTT | `mqtt` |
548
+ | `@ebarahona/loopback-transport-nats` | NATS | `nats` |
549
+
550
+ ## Event envelopes
551
+
552
+ [CloudEvents 1.0](https://cloudevents.io) is a CNCF standard envelope format for event data. Knative, Dapr, Azure Event Grid, AWS EventBridge, NATS JetStream, and GCP Eventarc all speak CloudEvents natively. Binding `CloudEventsSerializer` / `CloudEventsDeserializer` into a transport adapter makes the adapter interoperable with that broader cloud-native event ecosystem.
553
+
554
+ The serializer is marked `@experimental` until validated by a real adapter. It plugs into the existing `Serializer` / `Deserializer` interface -- no architectural changes are required on the adapter side. Structured mode (the default) embeds attributes in the JSON payload and is broker-agnostic. Binary mode places attributes in transport headers and keeps the payload clean, suitable for Kafka, AMQP, and HTTP binary content mode.
555
+
556
+ The `cloudevents` SDK is a peer dependency; install it alongside transport-core when using these helpers.
557
+
558
+ <details>
559
+ <summary><b>Show example: binding CloudEventsSerializer into a transport adapter</b></summary>
560
+
561
+ ```typescript
562
+ import {
563
+ CloudEventsSerializer,
564
+ CloudEventsDeserializer,
565
+ } from '@ebarahona/loopback-transport-core';
566
+
567
+ const serializer = new CloudEventsSerializer({
568
+ source: '/orders',
569
+ typePrefix: 'com.example.order',
570
+ mode: 'structured', // or 'binary' for Kafka/AMQP/HTTP
571
+ });
572
+ const deserializer = new CloudEventsDeserializer();
573
+
574
+ // Adapter wires these into its consume/produce loop in place of the
575
+ // default JsonSerializer / JsonDeserializer.
576
+ class KafkaServerCE extends ServerBase {
577
+ protected override serializer = serializer;
578
+ protected override deserializer = deserializer;
579
+ // ...
580
+ }
581
+ ```
582
+
583
+ </details>
584
+
585
+ <details>
586
+ <summary><b>Show example: handler receiving a deserialized CloudEvent payload</b></summary>
587
+
588
+ ```typescript
589
+ import {eventHandler, payload} from '@ebarahona/loopback-transport-core';
590
+
591
+ class OrderController {
592
+ // CloudEvents `type` arrives as the handler's pattern, prefixed by
593
+ // CloudEventsSerializerOptions.typePrefix. The packet's `data`
594
+ // attribute is the CloudEvent `data` payload; `correlationId` is the
595
+ // CloudEvent `id`.
596
+ @eventHandler('com.example.order.placed')
597
+ async onPlaced(@payload() data: OrderDto): Promise<void> {
598
+ await this.notificationService.send(data);
599
+ }
600
+ }
601
+ ```
602
+
603
+ </details>
604
+
605
+ See https://cloudevents.io and the [CNCF JavaScript SDK](https://github.com/cloudevents/sdk-javascript) for protocol details.
606
+
607
+ Serializers and deserializers are tag-based extension points. Bind your implementation under `TransportBindings.tags.SERIALIZER` (or `DESERIALIZER` for the inbound side) to make it discoverable by every `ServerBase` instance at boot. Add `TransportBindings.tags.NAME` set to a transport tag (for example `'kafka'`) to scope a serializer to a single transport; without that tag the serializer is treated as generic and applies wherever no transport-scoped match exists. Subclass-level defaults set via the `protected serializer` field on a `ServerBase` subclass still work as the final fallback.
608
+
609
+ <details>
610
+ <summary><b>Show example: registering a MsgPack serializer via tag</b></summary>
611
+
612
+ ```typescript
613
+ import {Binding, BindingScope, Component} from '@loopback/core';
614
+ import {
615
+ DESERIALIZER_TAG,
616
+ SERIALIZER_TAG,
617
+ type Deserializer,
618
+ type Serializer,
619
+ } from '@ebarahona/loopback-transport-core';
620
+ import {decode, encode} from '@msgpack/msgpack';
621
+
622
+ class MsgPackSerializer implements Serializer {
623
+ serialize(value: unknown): Uint8Array {
624
+ return encode(value);
625
+ }
626
+ }
627
+
628
+ class MsgPackDeserializer implements Deserializer {
629
+ deserialize(bytes: Uint8Array): unknown {
630
+ return decode(bytes);
631
+ }
632
+ }
633
+
634
+ export class MsgPackComponent implements Component {
635
+ readonly bindings = [
636
+ Binding.bind('serializers.msgpack')
637
+ .toClass(MsgPackSerializer)
638
+ .tag(SERIALIZER_TAG)
639
+ .inScope(BindingScope.SINGLETON),
640
+ Binding.bind('deserializers.msgpack')
641
+ .toClass(MsgPackDeserializer)
642
+ .tag(DESERIALIZER_TAG)
643
+ .inScope(BindingScope.SINGLETON),
644
+ ];
645
+ }
646
+ ```
647
+
648
+ `app.component(MsgPackComponent)` and every `ServerBase` instance resolves to MsgPack at boot; no per-transport wiring required.
649
+
650
+ #### Scoping to a specific transport
651
+
652
+ If you need different codecs per transport (for example Avro on Kafka via a schema registry, JSON on Redis Pub/Sub for mixed consumers), add `TransportBindings.tags.NAME` to the binding to scope it to one transport. Resolution at boot is transport-scoped first, then generic, then the subclass-default `protected serializer` field.
653
+
654
+ ```typescript
655
+ Binding.bind('serializers.avro-kafka')
656
+ .toClass(AvroSerializer)
657
+ .tag(SERIALIZER_TAG)
658
+ .tag({[TransportBindings.tags.NAME]: 'kafka'})
659
+ .inScope(BindingScope.SINGLETON);
660
+ ```
661
+
662
+ </details>
663
+
664
+ #### Implementing TransportServer directly
665
+
666
+ `resolveSerializer(app: Application): Promise<void>` is a required member of the `TransportServer` interface. Subclasses of `ServerBase` inherit a default implementation that performs the tag-based resolution described above. Servers that implement `TransportServer` directly (without extending `ServerBase`) must define `resolveSerializer` themselves; a no-op is acceptable for servers that hardcode their codec.
667
+
668
+ ## Reaching the native driver
669
+
670
+ This package exposes the transport surface via typed helpers, and the native broker client is one method call away through documented escape hatches. The goal is that users never need to leave LoopBack 4's DI surface to use a broker-specific feature -- everything the core does not abstract is reachable through `unwrap<T>()` on the registered `TransportServer` or `TransportClient`.
671
+
672
+ ### Reaching the native server
673
+
674
+ `ServerBase` exposes the underlying broker consumer via `unwrap<T>()`. Adapter authors implement it; consumers call it after acquiring the server from the container.
675
+
676
+ ```typescript
677
+ import {TransportBindings} from '@ebarahona/loopback-transport-core';
678
+
679
+ const server = await app.get(TransportBindings.server('kafka'));
680
+ const consumer = server.unwrap<Consumer>(); // kafkajs Consumer
681
+ consumer.on('consumer.crash', err => log.error(err));
682
+ ```
683
+
684
+ ### Reaching the native client
685
+
686
+ `ClientProxy` exposes the underlying broker producer via `unwrap<T>()`. The same pattern applies to every transport.
687
+
688
+ ```typescript
689
+ const client = await app.get(TransportBindings.client('kafka'));
690
+ const producer = client.unwrap<Producer>(); // kafkajs Producer
691
+ await producer.send({topic: 'audit', messages: [{value: payload}]});
692
+ ```
693
+
694
+ ### Reaching the execution arguments
695
+
696
+ `ExecutionContext.getArgs<T>()` returns a copy of the handler arguments tuple, and `getArgByIndex<T>(i)` returns a single argument typed as `T`. Both methods cast through the supplied generic so adapters can recover the original tuple shape at the call site.
697
+
698
+ ```typescript
699
+ const [data, ctx] = exec.getArgs<readonly [OrderDto, KafkaContext]>();
700
+ const handlerData = exec.getArgByIndex<OrderDto>(0);
701
+ ```
702
+
703
+ These generic casts are marked `@experimental`; see [Known limitations](#known-limitations).
704
+
705
+ ## Known limitations
706
+
707
+ These APIs are marked `@experimental` for the first release. They work but have documented edge cases pending follow-up work.
708
+
709
+ ### Generic casts on `ExecutionContext` accessors
710
+
711
+ `ExecutionContext.getClass<T>()`, `getArgs<T>()`, and `getArgByIndex<T>()` accept a generic and return the underlying value cast to that generic without runtime validation. Misalignment between the caller's `T` and the actual handler shape produces a silent type-system lie, not a runtime error. Validate inputs at the boundary if the handler shape is not fixed by adapter convention. A future release will replace these with overloaded signatures keyed off the recorded handler metadata.
712
+
713
+ ### `ClientProxy.assignPacketId` correlation IDs
714
+
715
+ The default `assignPacketId` uses `crypto.randomUUID()` for correlation IDs. The format is hardcoded; adapters that need a transport-specific ID shape (Kafka headers with binary keys, gRPC `request-id` semantics, MQTT 5 `Correlation Data` byte arrays) currently override the entire method to substitute their own generator. A future release will expose an injectable `IdGenerator` interface so adapters can swap the generator without overriding the protected method.
283
716
 
284
717
  ## Requirements
285
718
 
286
- - Node.js >= 18
719
+ - Node.js >= 20.19.0
287
720
  - LoopBack 4 application
288
721
 
289
- Peer dependencies (`@loopback/core`, `@loopback/metadata`) are satisfied by any LoopBack 4 project. Runtime dependencies (`rxjs`, `debug`) are installed automatically.
722
+ Peer dependencies: `@loopback/core` (>=7.0.0 <8.0.0), `@loopback/metadata` (>=8.0.0 <9.0.0). Runtime dependencies: `rxjs` 7.x, `debug`.
290
723
 
291
724
  ## Contributing
292
725
 
293
726
  See [CONTRIBUTING.md](CONTRIBUTING.md).
294
727
 
728
+ ## Documentation
729
+
730
+ The full API reference is published at https://ebarahona.github.io/loopback-transport-core (generated by TypeDoc on every release).
731
+
295
732
  ## License
296
733
 
297
734
  MIT