moost 0.6.5 → 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.
- package/dist/index.cjs +10 -4
- package/dist/index.d.ts +6 -1
- package/dist/index.mjs +10 -4
- package/package.json +8 -14
- package/scripts/setup-skills.js +0 -76
- package/skills/moost/SKILL.md +0 -34
- package/skills/moost/core.md +0 -174
- package/skills/moost/custom-adapters.md +0 -744
- package/skills/moost/decorators.md +0 -185
- package/skills/moost/di.md +0 -161
- package/skills/moost/interceptors.md +0 -199
- package/skills/moost/pipes.md +0 -213
|
@@ -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
|