moost 0.6.6 → 0.6.7

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.
@@ -1,744 +0,0 @@
1
- # Custom Adapters — moost
2
-
3
- > How to build a custom event adapter implementing `TMoostAdapter`, using `defineMoostEventHandler` to bridge any event source into Moost's DI, interceptor, and pipe systems. Includes real-world examples from HTTP, WebSocket, and Workflow adapters.
4
-
5
- ## Concepts
6
-
7
- An **adapter** connects an external event source to Moost. When an event arrives (HTTP request, WebSocket message, CLI command, cron tick, queue message, etc.), the adapter creates an event context and delegates to Moost's unified handler pipeline:
8
-
9
- ```
10
- Event Source → Adapter.bindHandler() → defineMoostEventHandler() → [scope → DI → interceptors → pipes → handler → cleanup]
11
- ```
12
-
13
- ### The Adapter Contract
14
-
15
- Every adapter implements `TMoostAdapter<H>` where `H` is the **handler metadata type** — a type describing what extra metadata your decorators attach to handler methods (e.g., HTTP method + path, WS event name, cron schedule).
16
-
17
- ### How Moost Calls Your Adapter
18
-
19
- During `app.init()`, Moost iterates every registered controller and every decorated method. For each method that has `handlers` metadata (set by decorators via `getMoostMate().decorate('handlers', {...}, true)`), Moost calls `adapter.bindHandler()` on **every** attached adapter. Your adapter must:
20
-
21
- 1. **Filter** — Only process handlers matching your adapter's type string
22
- 2. **Wrap** — Call `defineMoostEventHandler()` to create a composable handler function
23
- 3. **Register** — Register that function with your underlying event engine
24
- 4. **Report** — Call `opts.logHandler()` and `opts.register()` for logging and overview tracking
25
-
26
- ## API Reference
27
-
28
- ### `TMoostAdapter<H>` (interface)
29
-
30
- ```ts
31
- import type { TMoostAdapter } from 'moost'
32
-
33
- interface TMoostAdapter<H> {
34
- name: string
35
- bindHandler: <T extends object = object>(
36
- options: TMoostAdapterOptions<H, T>,
37
- ) => void | Promise<void>
38
- onInit?: (moost: Moost) => void | Promise<void>
39
- getProvideRegistry?: () => TProvideRegistry
40
- }
41
- ```
42
-
43
- | Member | Required | Description |
44
- |--------|----------|-------------|
45
- | `name` | Yes | Unique string identifying the adapter (e.g., `'http'`, `'ws'`, `'workflow'`, `'cron'`) |
46
- | `bindHandler` | Yes | Called once per controller method per controller. Filter by handler type, register routes |
47
- | `onInit` | No | Called after all controllers are bound. Use to finalize setup, store Moost reference |
48
- | `getProvideRegistry` | No | Return DI providers so controllers can inject adapter-specific services |
49
-
50
- ### `TMoostAdapterOptions<H, T>` (interface)
51
-
52
- The options object passed to `bindHandler()`:
53
-
54
- ```ts
55
- interface TMoostAdapterOptions<H, T> {
56
- prefix: string // Combined controller prefix
57
- fakeInstance: T // Prototype instance for reading metadata
58
- getInstance: () => Promise<T> | T // Factory for real controller instance
59
- method: keyof T // Handler method name
60
- handlers: TMoostHandler<H>[] // All handler decorators on this method
61
- getIterceptorHandler: () => InterceptorHandler | undefined // Factory for interceptor chain
62
- resolveArgs?: () => Promise<unknown[]> | unknown[] // Factory for argument resolution
63
- controllerName?: string // Class name (for logging)
64
- logHandler: (eventName: string) => void // Call to log route registration
65
- register: (handler: TMoostHandler<TEmpty>, path: string, args: string[]) => void // Track registration
66
- }
67
- ```
68
-
69
- **Key points:**
70
- - `handlers` is an **array** — a single method can have multiple handler decorators (e.g., both `@Get()` and `@Post()`)
71
- - Each handler has a `type` field (string) set by the decorator — use this to filter
72
- - `fakeInstance` is created via `Object.create(constructor.prototype)` — used only for metadata reads, never for handler execution
73
- - `getInstance` returns the real DI-resolved controller instance (may be async for `FOR_EVENT` scope)
74
-
75
- ### `TMoostHandler<H>` (type)
76
-
77
- ```ts
78
- type TMoostHandler<H> = {
79
- type: string // Your adapter's handler type identifier (e.g., 'HTTP', 'WS_MESSAGE', 'CRON')
80
- path?: string // Optional path/route set by the decorator
81
- } & H // Merged with your custom handler metadata
82
- ```
83
-
84
- ### `defineMoostEventHandler<T>(options)` (function)
85
-
86
- Creates a **composable handler function** that runs inside a Wooks event context. This is the core bridge between your event source and Moost's handler pipeline.
87
-
88
- ```ts
89
- import { defineMoostEventHandler } from 'moost'
90
-
91
- const handler = defineMoostEventHandler({
92
- contextType: 'MY_EVENT', // Event type identifier for filtering
93
- loggerTitle: 'my-adapter', // Logger topic prefix
94
- getIterceptorHandler, // From TMoostAdapterOptions
95
- getControllerInstance: getInstance, // From TMoostAdapterOptions
96
- controllerMethod: method, // From TMoostAdapterOptions
97
- controllerName, // From TMoostAdapterOptions
98
- resolveArgs, // From TMoostAdapterOptions
99
- targetPath: '/my/route', // Full resolved path (for logging/tracing)
100
- handlerType: 'MY_TYPE', // Handler type string (for context injection)
101
- // Optional:
102
- manualUnscope: false, // If true, YOU must call unscope() manually
103
- logErrors: false, // If true, logs errors to context logger
104
- hooks: { // Lifecycle hooks around the handler
105
- init: ({ unscope, scopeId, logger }) => { /* runs first */ },
106
- end: ({ getResponse }) => { /* runs after cleanup */ },
107
- },
108
- })
109
-
110
- // handler is a function: () => unknown
111
- // Call it inside a Wooks event context
112
- ```
113
-
114
- #### Options Reference
115
-
116
- ```ts
117
- interface TMoostEventHandlerOptions<T> {
118
- contextType?: string | string[] // Marks event context type (used by interceptors to filter)
119
- loggerTitle: string // Topic name for the scoped logger
120
- getIterceptorHandler: () => InterceptorHandler | undefined
121
- getControllerInstance: () => Promise<T> | T | undefined
122
- controllerMethod?: keyof T
123
- controllerName?: string
124
- callControllerMethod?: (args: unknown[]) => unknown // Override: custom invocation logic
125
- resolveArgs?: () => Promise<unknown[]> | unknown[]
126
- logErrors?: boolean // Log errors to event logger (default: false)
127
- manualUnscope?: boolean // If true, adapter handles DI scope cleanup (default: false)
128
- hooks?: {
129
- init?: (opts: TMoostEventHandlerHookOptions<T>) => unknown
130
- end?: (opts: TMoostEventHandlerHookOptions<T>) => unknown
131
- }
132
- targetPath: string // Resolved path (for tracing spans)
133
- handlerType: string // Type identifier (for context injection hooks)
134
- }
135
- ```
136
-
137
- #### Handler Lifecycle (what happens inside the returned function)
138
-
139
- ```
140
- 1. Get current event context → current()
141
- 2. Generate scope ID → useScopeId()
142
- 3. Create scoped logger → useLogger().topic(loggerTitle)
143
- 4. Register DI scope → registerEventScope(scopeId)
144
- 5. Run init hook → hooks.init?.()
145
- 6. Resolve controller instance → getControllerInstance()
146
- 7. Set controller context → setControllerContext()
147
- 8. Run interceptor before phase → interceptorHandler.before()
148
- └─ If returns value → skip to cleanup (short-circuit)
149
- 9. Resolve arguments → resolveArgs()
150
- 10. Call handler method → instance[method](...args)
151
- 11. Run interceptor after phase → interceptorHandler.fireAfter(response)
152
- 12. Unregister DI scope → unscope() (unless manualUnscope)
153
- 13. Run end hook → hooks.end?.()
154
- 14. Return response or throw error
155
- ```
156
-
157
- ### `createProvideRegistry(...entries)` (function)
158
-
159
- Creates a DI provide registry for `getProvideRegistry()`. Each entry maps a token to a factory.
160
-
161
- ```ts
162
- import { createProvideRegistry } from 'moost'
163
-
164
- // Entries: [Token, () => instance]
165
- createProvideRegistry(
166
- [MyService, () => this.getService()],
167
- ['MyService', () => this.getService()], // String token alternative
168
- )
169
- ```
170
-
171
- ### `getMoostMate()` (function)
172
-
173
- Returns the singleton Mate instance for reading/writing decorator metadata. Used by decorators and by `bindHandler` to read handler-specific metadata.
174
-
175
- ```ts
176
- import { getMoostMate } from 'moost'
177
-
178
- const mate = getMoostMate()
179
- const methodMeta = mate.read(fakeInstance, methodName)
180
- // methodMeta.handlers → TMoostHandler[] array
181
- ```
182
-
183
- ## Common Patterns
184
-
185
- ### Pattern: Minimal Custom Adapter
186
-
187
- The simplest possible adapter — handles a single event type with no extra features.
188
-
189
- ```ts
190
- import type { Moost, TMoostAdapter, TMoostAdapterOptions, TObject } from 'moost'
191
- import { defineMoostEventHandler } from 'moost'
192
-
193
- // 1. Define your handler metadata type
194
- interface TCronHandlerMeta {
195
- schedule: string // cron expression
196
- }
197
-
198
- // 2. Define your context type identifier
199
- const HANDLER_TYPE = 'CRON'
200
-
201
- // 3. Implement the adapter
202
- class MoostCron implements TMoostAdapter<TCronHandlerMeta> {
203
- public readonly name = 'cron'
204
- protected moost?: Moost
205
- private jobs: Array<{ schedule: string; handler: () => unknown }> = []
206
-
207
- onInit(moost: Moost) {
208
- this.moost = moost
209
- // Start all registered cron jobs
210
- for (const job of this.jobs) {
211
- scheduleCron(job.schedule, job.handler)
212
- }
213
- }
214
-
215
- bindHandler<T extends TObject>(opts: TMoostAdapterOptions<TCronHandlerMeta, T>): void {
216
- for (const handler of opts.handlers) {
217
- // CRITICAL: Filter — only process handlers meant for this adapter
218
- if (handler.type !== HANDLER_TYPE) {
219
- continue
220
- }
221
-
222
- // Build the target path for logging/tracing
223
- const path = handler.path || (opts.method as string)
224
- const targetPath = `${opts.prefix || ''}/${path}`.replaceAll(/\/\/+/g, '/')
225
-
226
- // Create the Moost event handler (composable)
227
- const fn = defineMoostEventHandler({
228
- contextType: HANDLER_TYPE,
229
- loggerTitle: 'moost-cron',
230
- getIterceptorHandler: opts.getIterceptorHandler,
231
- getControllerInstance: opts.getInstance,
232
- controllerMethod: opts.method,
233
- controllerName: opts.controllerName,
234
- resolveArgs: opts.resolveArgs,
235
- targetPath,
236
- handlerType: HANDLER_TYPE,
237
- })
238
-
239
- // Store for registration during onInit
240
- this.jobs.push({
241
- schedule: handler.schedule,
242
- handler: fn, // fn is a composable — call it inside an event context
243
- })
244
-
245
- // Log the registration
246
- opts.logHandler(`(cron:${handler.schedule})${targetPath}`)
247
- opts.register(handler, targetPath, [])
248
- }
249
- }
250
- }
251
- ```
252
-
253
- ### Pattern: Decorator for Your Custom Adapter
254
-
255
- Every adapter needs at least one method decorator that stores handler metadata.
256
-
257
- ```ts
258
- import type { TEmpty, TMoostMetadata } from 'moost'
259
- import { getMoostMate } from 'moost'
260
-
261
- // Decorator that marks a method as a cron handler
262
- function Cron(schedule: string, path?: string): MethodDecorator {
263
- return getMoostMate<TEmpty, TMoostMetadata<{ schedule: string }>>().decorate(
264
- 'handlers', // metadata key (always 'handlers')
265
- { schedule, path, type: 'CRON' }, // merged into TMoostHandler<TCronHandlerMeta>
266
- true, // array mode — accumulates, doesn't overwrite
267
- )
268
- }
269
-
270
- // Usage:
271
- @Controller('tasks')
272
- class TaskController {
273
- @Cron('0 * * * *', 'cleanup') // every hour
274
- runCleanup() {
275
- // handler logic
276
- }
277
- }
278
- ```
279
-
280
- **Key rules for decorators:**
281
- - Always use `getMoostMate().decorate('handlers', {...}, true)` — the `true` enables array accumulation
282
- - Always include a `type` field — this is how your adapter filters its handlers in `bindHandler()`
283
- - The `path` field is optional but conventional — it becomes the handler identifier
284
-
285
- ### Pattern: Adapter with Event Context (Wooks Integration)
286
-
287
- If your event source uses Wooks (like HTTP, CLI, WS do), the event context is already set up. If not, you need to create one via `createEventContext()`.
288
-
289
- ```ts
290
- import { createEventContext } from 'moost'
291
-
292
- class MoostCron implements TMoostAdapter<TCronHandlerMeta> {
293
- // ...
294
-
295
- onInit(moost: Moost) {
296
- this.moost = moost
297
- for (const job of this.jobs) {
298
- scheduleCron(job.schedule, () => {
299
- // Create a Wooks event context for each cron tick
300
- return createEventContext(
301
- { logger: moost.getLogger('cron') },
302
- () => job.handler(),
303
- )
304
- })
305
- }
306
- }
307
- }
308
- ```
309
-
310
- If your adapter wraps a Wooks engine (like `WooksHttp`, `WooksWs`), the engine creates the event context automatically — you just register `fn` directly.
311
-
312
- ### Pattern: Manual Scope Cleanup (Long-lived Events)
313
-
314
- For events that outlive a single function call (HTTP streaming, WebSocket connections, workflows), use `manualUnscope: true` and clean up the DI scope explicitly.
315
-
316
- ```ts
317
- // HTTP adapter: scope lives until request ends
318
- const fn = defineMoostEventHandler({
319
- // ...
320
- manualUnscope: true,
321
- hooks: {
322
- init: ({ unscope }) => {
323
- const { raw } = useRequest()
324
- raw.on('end', unscope) // Clean up when request stream ends
325
- },
326
- },
327
- })
328
-
329
- // Workflow adapter: scope cleaned up by the workflow engine
330
- const fn = defineMoostEventHandler({
331
- // ...
332
- manualUnscope: true,
333
- })
334
- // Then in start()/resume():
335
- wfApp.start(schemaId, ctx, {
336
- cleanup: () => {
337
- getMoostInfact().unregisterScope(useScopeId())
338
- },
339
- })
340
- ```
341
-
342
- **Rule:** If `manualUnscope` is `false` (default), the scope is cleaned up automatically after the handler returns. Use `true` only when the event's lifetime extends beyond the handler function.
343
-
344
- ### Pattern: DI Provider Registry
345
-
346
- Expose adapter-specific services for controller injection.
347
-
348
- ```ts
349
- import { createProvideRegistry } from 'moost'
350
-
351
- class MoostCron implements TMoostAdapter<TCronHandlerMeta> {
352
- // ...
353
-
354
- getProvideRegistry() {
355
- return createProvideRegistry(
356
- [MoostCron, () => this], // Inject by class
357
- ['MoostCron', () => this], // Inject by string token
358
- [CronEngine, () => this.engine], // Expose underlying engine
359
- )
360
- }
361
- }
362
-
363
- // Now controllers can inject it:
364
- @Controller()
365
- class TaskController {
366
- constructor(private cron: MoostCron) {}
367
- // or
368
- constructor(@Inject('MoostCron') private cron: MoostCron) {}
369
- }
370
- ```
371
-
372
- ### Pattern: Not-Found Handler
373
-
374
- Adapters that support "catch-all" or "not found" scenarios can use `defineMoostEventHandler` with `callControllerMethod` to run the global interceptor chain (CORS, logging, etc.) even for unmatched routes.
375
-
376
- ```ts
377
- async onNotFound() {
378
- return defineMoostEventHandler({
379
- loggerTitle: 'my-adapter',
380
- getIterceptorHandler: () => this.moost?.getGlobalInterceptorHandler(),
381
- getControllerInstance: () => this.moost,
382
- callControllerMethod: () => new MyError(404, 'Not Found'),
383
- targetPath: '',
384
- handlerType: '__SYSTEM__',
385
- })()
386
- }
387
- ```
388
-
389
- ### Pattern: Parameter Resolver Decorators
390
-
391
- Create parameter decorators that inject event-specific data using `@Resolve()`.
392
-
393
- ```ts
394
- import { Resolve } from 'moost'
395
-
396
- // Resolve using composables from your event engine
397
- function CronSchedule() {
398
- return Resolve(() => useCronContext().schedule, 'cron-schedule')
399
- }
400
-
401
- function CronTick() {
402
- return Resolve(() => useCronContext().tickNumber, 'cron-tick')
403
- }
404
-
405
- // Usage:
406
- @Cron('*/5 * * * *')
407
- handle(@CronSchedule() schedule: string, @CronTick() tick: number) {
408
- console.log(`Tick ${tick} for schedule ${schedule}`)
409
- }
410
- ```
411
-
412
- ## Real-World Adapter Examples
413
-
414
- ### HTTP Adapter (MoostHttp)
415
-
416
- Source: `@moostjs/event-http` — wraps `@wooksjs/event-http` (WooksHttp).
417
-
418
- **Handler metadata type:**
419
- ```ts
420
- interface THttpHandlerMeta {
421
- method: string // 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | '*' | 'UPGRADE'
422
- path: string
423
- }
424
- ```
425
-
426
- **Key `bindHandler` implementation:**
427
- ```ts
428
- bindHandler<T extends object>(opts: TMoostAdapterOptions<THttpHandlerMeta, T>): void {
429
- for (const handler of opts.handlers) {
430
- if (handler.type !== 'HTTP') continue // Filter
431
-
432
- const path = typeof handler.path === 'string' ? handler.path : (opts.method as string)
433
- const targetPath = `${opts.prefix || ''}/${path}`.replaceAll(/\/\/+/g, '/')
434
-
435
- const fn = defineMoostEventHandler({
436
- contextType: 'HTTP',
437
- loggerTitle: 'moost-http',
438
- getIterceptorHandler: opts.getIterceptorHandler,
439
- getControllerInstance: opts.getInstance,
440
- controllerMethod: opts.method,
441
- controllerName: opts.controllerName,
442
- resolveArgs: opts.resolveArgs,
443
- manualUnscope: true, // Scope lives until request ends
444
- hooks: {
445
- init: ({ unscope }) => {
446
- useRequest().raw.on('end', unscope) // Cleanup on request end
447
- },
448
- },
449
- targetPath,
450
- handlerType: handler.type,
451
- })
452
-
453
- // Register with WooksHttp router
454
- if (handler.method === 'UPGRADE') {
455
- this.httpApp.upgrade(targetPath, fn)
456
- } else {
457
- this.httpApp.on(handler.method, targetPath, fn)
458
- }
459
-
460
- opts.logHandler(`(${handler.method})${targetPath}`)
461
- opts.register(handler, targetPath, routerBinding.getArgs())
462
- }
463
- }
464
- ```
465
-
466
- **Decorators:**
467
- ```ts
468
- // HttpMethod stores { method, path, type: 'HTTP' } in handlers metadata
469
- const Get = (path?: string) => HttpMethod('GET', path)
470
- const Post = (path?: string) => HttpMethod('POST', path)
471
- ```
472
-
473
- **Why `manualUnscope: true`:** HTTP requests can stream — the response may be sent long after the handler returns. The DI scope must survive until the Node.js request stream emits `'end'`.
474
-
475
- ---
476
-
477
- ### WebSocket Adapter (MoostWs)
478
-
479
- Source: `@moostjs/event-ws` — wraps `@wooksjs/event-ws` (WooksWs).
480
-
481
- **Handler metadata type (union):**
482
- ```ts
483
- interface TWsMessageHandlerMeta { event: string; path: string }
484
- interface TWsConnectHandlerMeta { /* no extra fields */ }
485
- type TWsHandlerMeta = TWsMessageHandlerMeta | TWsConnectHandlerMeta
486
- ```
487
-
488
- **Multiple handler types in one adapter:**
489
- ```ts
490
- bindHandler<T extends object>(opts: TMoostAdapterOptions<TWsHandlerMeta, T>): void {
491
- for (const handler of opts.handlers) {
492
- if (handler.type === 'WS_MESSAGE') {
493
- this.bindMessageHandler(opts, handler)
494
- } else if (handler.type === 'WS_CONNECT') {
495
- this.bindConnectHandler(opts)
496
- } else if (handler.type === 'WS_DISCONNECT') {
497
- this.bindDisconnectHandler(opts)
498
- }
499
- }
500
- }
501
-
502
- // Message handler — routed by event + path
503
- protected bindMessageHandler(opts, handler) {
504
- const fn = defineMoostEventHandler({
505
- contextType: 'WS_MESSAGE',
506
- loggerTitle: 'moost-ws',
507
- // ...standard opts pass-through...
508
- targetPath,
509
- handlerType: 'WS_MESSAGE',
510
- })
511
- this.wsApp.onMessage(handler.event, targetPath, fn)
512
- }
513
-
514
- // Connect handler — no routing, fires for all connections
515
- protected bindConnectHandler(opts) {
516
- const fn = defineMoostEventHandler({
517
- contextType: 'WS_CONNECT',
518
- // ...
519
- targetPath: '__ws_connect__',
520
- handlerType: 'WS_CONNECT',
521
- })
522
- this.wsApp.onConnect(fn)
523
- }
524
- ```
525
-
526
- **Decorators:**
527
- ```ts
528
- const Message = (event: string, path?: string) =>
529
- getMoostMate().decorate('handlers', { event, path, type: 'WS_MESSAGE' }, true)
530
-
531
- const Connect = () =>
532
- getMoostMate().decorate('handlers', { type: 'WS_CONNECT' }, true)
533
-
534
- const Disconnect = () =>
535
- getMoostMate().decorate('handlers', { type: 'WS_DISCONNECT' }, true)
536
- ```
537
-
538
- **Key insight:** A single adapter can handle multiple handler types. Use different `type` strings for each and dispatch in `bindHandler`.
539
-
540
- ---
541
-
542
- ### Workflow Adapter (MoostWf)
543
-
544
- Source: `@moostjs/event-wf` — wraps `@wooksjs/event-wf` (WooksWf).
545
-
546
- **Handler metadata type:**
547
- ```ts
548
- interface TWfHandlerMeta { path: string }
549
- ```
550
-
551
- **Deferred registration pattern:**
552
- ```ts
553
- class MoostWf implements TMoostAdapter<TWfHandlerMeta> {
554
- protected toInit: (() => void)[] = []
555
-
556
- bindHandler<T extends object>(opts: TMoostAdapterOptions<TWfHandlerMeta, T>): void {
557
- for (const handler of opts.handlers) {
558
- if (!['WF_STEP', 'WF_FLOW'].includes(handler.type)) continue
559
-
560
- const fn = defineMoostEventHandler({
561
- contextType: 'WF',
562
- manualUnscope: true, // Workflows span multiple steps
563
- // ...
564
- })
565
-
566
- if (handler.type === 'WF_STEP') {
567
- // Steps register immediately
568
- this.wfApp.step(targetPath, { handler: fn })
569
- } else {
570
- // Flows defer to onInit — they need schema metadata
571
- const wfSchema = getWfMate().read(opts.fakeInstance, opts.method)?.wfSchema
572
- this.toInit.push(() => {
573
- this.wfApp.flow(targetPath, wfSchema || [], opts.prefix, fn)
574
- })
575
- }
576
- }
577
- }
578
-
579
- onInit(moost: Moost) {
580
- this.moost = moost
581
- this.toInit.forEach((fn) => fn()) // Execute deferred registrations
582
- }
583
- }
584
- ```
585
-
586
- **Key insight:** When registration requires data that's only available after all controllers are processed, use a deferred queue (`toInit`) and execute it in `onInit()`.
587
-
588
- **Decorators:**
589
- ```ts
590
- const Step = (path?: string) =>
591
- getWfMate().decorate('handlers', { path, type: 'WF_STEP' }, true)
592
-
593
- const Workflow = (path?: string) =>
594
- getWfMate().decorate('handlers', { path, type: 'WF_FLOW' }, true)
595
-
596
- const WorkflowSchema = (schema) =>
597
- getWfMate().decorate('wfSchema', schema) // Non-array metadata (overwrites)
598
- ```
599
-
600
- ## Step-by-Step: Building a Custom Adapter
601
-
602
- ### Step 1: Define Handler Metadata Type
603
-
604
- ```ts
605
- // What extra data your decorator stores per handler
606
- interface TMyHandlerMeta {
607
- channel: string // e.g., queue name, topic, schedule
608
- }
609
- ```
610
-
611
- ### Step 2: Create Method Decorator(s)
612
-
613
- ```ts
614
- import { getMoostMate } from 'moost'
615
-
616
- function OnEvent(channel: string, path?: string): MethodDecorator {
617
- return getMoostMate().decorate(
618
- 'handlers',
619
- { channel, path, type: 'MY_EVENT' },
620
- true,
621
- )
622
- }
623
- ```
624
-
625
- ### Step 3: Implement TMoostAdapter
626
-
627
- ```ts
628
- import type { Moost, TMoostAdapter, TMoostAdapterOptions, TObject } from 'moost'
629
- import { createProvideRegistry, defineMoostEventHandler } from 'moost'
630
-
631
- class MyAdapter implements TMoostAdapter<TMyHandlerMeta> {
632
- public readonly name = 'my-adapter'
633
- protected moost?: Moost
634
-
635
- constructor(private engine: MyEventEngine) {}
636
-
637
- onInit(moost: Moost) {
638
- this.moost = moost
639
- }
640
-
641
- getProvideRegistry() {
642
- return createProvideRegistry(
643
- [MyAdapter, () => this],
644
- [MyEventEngine, () => this.engine],
645
- )
646
- }
647
-
648
- bindHandler<T extends TObject>(opts: TMoostAdapterOptions<TMyHandlerMeta, T>) {
649
- for (const handler of opts.handlers) {
650
- if (handler.type !== 'MY_EVENT') continue
651
-
652
- const path = handler.path || (opts.method as string)
653
- const targetPath = `${opts.prefix || ''}/${path}`.replaceAll(/\/\/+/g, '/')
654
-
655
- const fn = defineMoostEventHandler({
656
- contextType: 'MY_EVENT',
657
- loggerTitle: 'my-adapter',
658
- getIterceptorHandler: opts.getIterceptorHandler,
659
- getControllerInstance: opts.getInstance,
660
- controllerMethod: opts.method,
661
- controllerName: opts.controllerName,
662
- resolveArgs: opts.resolveArgs,
663
- targetPath,
664
- handlerType: handler.type,
665
- })
666
-
667
- this.engine.subscribe(handler.channel, targetPath, fn)
668
-
669
- opts.logHandler(`(${handler.channel})${targetPath}`)
670
- opts.register(handler, targetPath, [])
671
- }
672
- }
673
- }
674
- ```
675
-
676
- ### Step 4: Create Parameter Resolvers (Optional)
677
-
678
- ```ts
679
- import { Resolve } from 'moost'
680
-
681
- function EventPayload() {
682
- return Resolve(() => useMyEventContext().payload, 'event-payload')
683
- }
684
-
685
- function EventChannel() {
686
- return Resolve(() => useMyEventContext().channel, 'event-channel')
687
- }
688
- ```
689
-
690
- ### Step 5: Wire It Up
691
-
692
- ```ts
693
- const app = new Moost()
694
- const myAdapter = app.adapter(new MyAdapter(new MyEventEngine()))
695
-
696
- app.registerControllers(MyController)
697
- await app.init()
698
- myAdapter.engine.connect()
699
- ```
700
-
701
- ## Integration
702
-
703
- ### With Wooks Event Engines
704
-
705
- If your event source has a Wooks engine (like `WooksHttp`, `WooksCli`, `WooksWs`), the engine creates event contexts via `createEventContext()` automatically. Your adapter just registers handler functions and the engine calls them in the right context.
706
-
707
- If you're building without a Wooks engine, wrap each event dispatch in `createEventContext()`:
708
-
709
- ```ts
710
- import { createEventContext } from 'moost'
711
-
712
- myEngine.on('event', (data) => {
713
- return createEventContext(
714
- { logger: this.moost.getLogger('my-adapter') },
715
- () => registeredHandler(),
716
- )
717
- })
718
- ```
719
-
720
- ### With Interceptors
721
-
722
- `defineMoostEventHandler` automatically runs the interceptor chain from `opts.getIterceptorHandler()`. No extra work needed. Global interceptors (registered via `app.interceptor()`) are included.
723
-
724
- ### With Pipes
725
-
726
- Argument resolution via `opts.resolveArgs` runs the full pipe pipeline (RESOLVE → TRANSFORM → VALIDATE). Your `@Resolve()` decorators for parameter injection integrate automatically.
727
-
728
- ## Best Practices
729
-
730
- - Always filter handlers by `type` in `bindHandler` — other adapters' handlers will be passed to you
731
- - Use `manualUnscope: true` only for long-lived events (streams, connections, multi-step processes)
732
- - Provide both class and string tokens in `getProvideRegistry()` for flexible injection
733
- - Use `opts.logHandler()` with DYE color codes for consistent terminal output: `` opts.logHandler(`${__DYE_CYAN__}(type)${__DYE_GREEN__}${path}`) ``
734
- - Call `opts.register()` for every registered handler — this populates `app.controllersOverview`
735
- - Store a reference to the Moost instance in `onInit()` — you'll need it for `getGlobalInterceptorHandler()` in not-found handlers
736
-
737
- ## Gotchas
738
-
739
- - `bindHandler` is called during `app.init()` **before** `onInit()` — don't rely on `this.moost` being set in `bindHandler`. Use the deferred queue pattern (like MoostWf's `toInit`) if you need the Moost reference during registration
740
- - `defineMoostEventHandler` returns a **function**, not the result — call `fn()` to execute the handler pipeline. When registering with a Wooks engine, pass `fn` directly (the engine calls it)
741
- - The `fakeInstance` in adapter options is **not** a real instance — it's `Object.create(prototype)` for metadata reads only. Never call methods on it
742
- - The `handlers` array may contain entries from **other** adapters — always check `handler.type`
743
- - `contextType` in `defineMoostEventHandler` is used by interceptors to filter which events they apply to — choose a distinctive string
744
- - If your adapter wraps a Wooks engine, the engine handles `createEventContext()` — don't double-wrap