@znight/messaging 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -4
- package/dist/compiler.d.mts +19 -1
- package/dist/compiler.mjs +99 -10
- package/dist/compiler.mjs.map +1 -1
- package/dist/{driver-ZxLqy_Ap.d.mts → driver-Dv38b74F.d.mts} +7 -1
- package/dist/index.d.mts +9 -3
- package/dist/index.mjs +12 -3
- package/dist/index.mjs.map +1 -1
- package/dist/kafka.d.mts +1 -1
- package/dist/memory.d.mts +1 -1
- package/dist/rabbitmq.d.mts +1 -1
- package/dist/sqs.d.mts +1 -1
- package/package.json +4 -4
- package/src/compiler/analyze.ts +35 -4
- package/src/compiler/data.ts +22 -1
- package/src/compiler/generate.ts +116 -12
- package/src/consumers.ts +31 -2
- package/src/message.ts +7 -0
- package/src/metadata.ts +9 -3
- package/src/runtime.ts +1 -1
package/src/compiler/generate.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* }
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
|
-
import type { GenerateContext, GenInput, GenNode } from '@znight/compiler'
|
|
21
|
+
import type { ClassRef, GenerateContext, GenInput, GenNode, StaticValue, ValueRef } from '@znight/compiler'
|
|
22
22
|
import type { ConsumerData, HandlerData, HandlerParamData, MessagingData } from './data.ts'
|
|
23
23
|
|
|
24
24
|
import { compareStrings, RUNTIME_MODULE } from './constants.ts'
|
|
@@ -33,6 +33,13 @@ interface Helpers {
|
|
|
33
33
|
inboundMessage(): GenNode
|
|
34
34
|
validateMessage(): GenNode
|
|
35
35
|
messageHeader(): GenNode
|
|
36
|
+
paramFunctionOf(): GenNode
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The class resolver instances of the handler being generated (`x`), with the position of each token. */
|
|
40
|
+
interface Resolvers {
|
|
41
|
+
readonly x: GenNode | undefined
|
|
42
|
+
readonly index: ReadonlyMap<string, number>
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/** Builds the messaging section; `undefined` (no section) when the application has no consumer. */
|
|
@@ -68,36 +75,77 @@ function handlerNode(ctx: Context, handler: HandlerData, consumerType: () => Gen
|
|
|
68
75
|
const entries: [string, GenInput][] = [['method', handler.method]]
|
|
69
76
|
if (handler.type !== undefined) entries.push(['type', handler.type])
|
|
70
77
|
if (handler.retry !== undefined) entries.push(['retry', ctx.lit({ ...handler.retry })])
|
|
71
|
-
|
|
78
|
+
const resolvers = paramResolvers(handler)
|
|
79
|
+
if (resolvers.length > 0) {
|
|
80
|
+
entries.push(['paramResolvers', ctx.lit(resolvers.map(ref => ctx.tokenOf(ref) ?? ref.token))])
|
|
81
|
+
}
|
|
82
|
+
entries.push(['invoke', invokeNode(ctx, handler, consumerType, helpers, resolvers)])
|
|
72
83
|
return ctx.obj(entries)
|
|
73
84
|
}
|
|
74
85
|
|
|
86
|
+
/** The provider classes resolving custom parameters, in order of first use (`x[i]`). */
|
|
87
|
+
function paramResolvers(handler: HandlerData): ClassRef[] {
|
|
88
|
+
const found = new Map<string, ClassRef>()
|
|
89
|
+
for (const param of handler.params) {
|
|
90
|
+
const resolver = param.kind === 'custom' ? param.resolver : undefined
|
|
91
|
+
if (resolver !== undefined && !found.has(resolver.token)) found.set(resolver.token, resolver)
|
|
92
|
+
}
|
|
93
|
+
return [...found.values()]
|
|
94
|
+
}
|
|
95
|
+
|
|
75
96
|
/**
|
|
76
97
|
* `(c: Consumer, m: InboundMessage) => c.method(...args)`, `async` only when it awaits a schema
|
|
77
|
-
* validation. `m` is omitted when no argument uses it (`noUnusedParameters`)
|
|
98
|
+
* validation or a custom parameter. `m` is omitted when no argument uses it (`noUnusedParameters`);
|
|
99
|
+
* `x` (the class resolver instances) is declared only when the handler has class resolvers.
|
|
78
100
|
*/
|
|
79
|
-
function invokeNode(
|
|
101
|
+
function invokeNode(
|
|
102
|
+
ctx: Context,
|
|
103
|
+
handler: HandlerData,
|
|
104
|
+
consumerType: () => GenNode,
|
|
105
|
+
helpers: Helpers,
|
|
106
|
+
resolvers: readonly ClassRef[],
|
|
107
|
+
): GenNode {
|
|
80
108
|
const params = [...handler.params]
|
|
81
109
|
while (params.length > 0 && params[params.length - 1]?.kind === 'undefined') params.pop()
|
|
82
110
|
if (params.length === 0) {
|
|
83
111
|
return ctx.arrow([['c', consumerType()]], c => ctx.call(ctx.member(c, handler.method)))
|
|
84
112
|
}
|
|
113
|
+
// `_m` when only class resolvers whose `resolve()` takes no parameter need arguments (`noUnusedParameters`)
|
|
114
|
+
const readsMessage = params.some(
|
|
115
|
+
param =>
|
|
116
|
+
param.kind !== 'undefined' && (param.kind !== 'custom' || param.resolver === undefined || param.arity !== 0),
|
|
117
|
+
)
|
|
118
|
+
const arrowParams: [string, GenNode][] = [
|
|
119
|
+
['c', consumerType()],
|
|
120
|
+
[readsMessage ? 'm' : '_m', helpers.inboundMessage()],
|
|
121
|
+
]
|
|
122
|
+
if (resolvers.length > 0) {
|
|
123
|
+
const types = resolvers.map(ref => ctx.importType(ref))
|
|
124
|
+
const joined = types.slice(1).reduce((all, node) => ctx.code`${all}, ${node}`, types[0] as GenNode)
|
|
125
|
+
arrowParams.push(['x', ctx.code`readonly [${joined}]`])
|
|
126
|
+
}
|
|
127
|
+
const index = new Map(resolvers.map((ref, position) => [ref.token, position]))
|
|
85
128
|
return ctx.arrow(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
['m', helpers.inboundMessage()],
|
|
89
|
-
],
|
|
90
|
-
(c, m) =>
|
|
129
|
+
arrowParams,
|
|
130
|
+
(c, m, x) =>
|
|
91
131
|
ctx.call(
|
|
92
132
|
ctx.member(c, handler.method),
|
|
93
|
-
params.map(param => argumentNode(ctx, param, m as GenNode, helpers)),
|
|
133
|
+
params.map(param => argumentNode(ctx, param, m as GenNode, helpers, { x, index })),
|
|
94
134
|
),
|
|
95
|
-
{ async: params.some(param => param.kind === 'validated') },
|
|
135
|
+
{ async: params.some(param => param.kind === 'validated' || param.kind === 'custom') },
|
|
96
136
|
)
|
|
97
137
|
}
|
|
98
138
|
|
|
99
|
-
function argumentNode(
|
|
139
|
+
function argumentNode(
|
|
140
|
+
ctx: Context,
|
|
141
|
+
param: HandlerParamData,
|
|
142
|
+
m: GenNode,
|
|
143
|
+
helpers: Helpers,
|
|
144
|
+
resolvers: Resolvers,
|
|
145
|
+
): GenNode {
|
|
100
146
|
switch (param.kind) {
|
|
147
|
+
case 'custom':
|
|
148
|
+
return customArgument(ctx, param, m, helpers, resolvers)
|
|
101
149
|
case 'payload':
|
|
102
150
|
return ctx.code`${ctx.member(m, 'payload')} as never`
|
|
103
151
|
case 'validated':
|
|
@@ -113,6 +161,54 @@ function argumentNode(ctx: Context, param: HandlerParamData, m: GenNode, helpers
|
|
|
113
161
|
}
|
|
114
162
|
}
|
|
115
163
|
|
|
164
|
+
/**
|
|
165
|
+
* A custom parameter (RFC 0010), awaited (the invoke is `async`):
|
|
166
|
+
* `await (D.resolvers.messaging as ParamFunctionOf<typeof D, 'messaging'>)(m, data)` or
|
|
167
|
+
* `await x[i].resolve(m, data)`, cast with `as never` when the value type could not be checked.
|
|
168
|
+
*/
|
|
169
|
+
function customArgument(
|
|
170
|
+
ctx: Context,
|
|
171
|
+
param: Extract<HandlerParamData, { kind: 'custom' }>,
|
|
172
|
+
m: GenNode,
|
|
173
|
+
helpers: Helpers,
|
|
174
|
+
resolvers: Resolvers,
|
|
175
|
+
): GenNode {
|
|
176
|
+
const data = param.data === undefined ? ctx.lit(undefined) : dataNode(ctx, param.data.value)
|
|
177
|
+
let node: GenNode
|
|
178
|
+
if (param.resolver === undefined) {
|
|
179
|
+
const decorator = ctx.importValue(param.identity)
|
|
180
|
+
const type = ctx.code`${helpers.paramFunctionOf()}<typeof ${decorator}, 'messaging'>`
|
|
181
|
+
node = ctx.call(ctx.code`${ctx.member(decorator, 'resolvers', 'messaging')} as ${type}`, [m, data])
|
|
182
|
+
} else {
|
|
183
|
+
const position = resolvers.index.get(param.resolver.token) as number
|
|
184
|
+
if (param.arity === undefined) {
|
|
185
|
+
// a `resolve` the compiler cannot read (inherited from a mixin...): the instance is cast to a
|
|
186
|
+
// method taking both arguments (method parameters are bivariant), the value with `as never`
|
|
187
|
+
const instance = ctx.code`${resolvers.x as GenNode}[${position}] as { resolve(message: unknown, data: unknown): unknown }`
|
|
188
|
+
return ctx.code`(await (${instance}).resolve(${m}, ${data})) as never`
|
|
189
|
+
}
|
|
190
|
+
// a `resolve(message)` that ignores the data gets no data argument (TypeScript checks the arity)
|
|
191
|
+
const arity = Math.min(param.arity, 2)
|
|
192
|
+
let args = ctx.code``
|
|
193
|
+
if (arity === 1) args = m
|
|
194
|
+
else if (arity === 2) args = ctx.code`${m}, ${data}`
|
|
195
|
+
node = ctx.code`${resolvers.x as GenNode}[${position}].resolve(${args})`
|
|
196
|
+
}
|
|
197
|
+
return param.cast ? ctx.code`(await ${node}) as never` : ctx.code`await ${node}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The data argument of a custom parameter: a literal (references imported) or a reference. */
|
|
201
|
+
function dataNode(ctx: Context, value: StaticValue): GenNode {
|
|
202
|
+
return isValueRef(value) ? ctx.ref(value as ValueRef) : ctx.lit(value as never)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** `true` for a {@link ValueRef} (the only non-plain object a static value can hold). */
|
|
206
|
+
function isValueRef(value: unknown): boolean {
|
|
207
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) return false
|
|
208
|
+
const prototype = Object.getPrototypeOf(value)
|
|
209
|
+
return prototype !== Object.prototype && prototype !== null && (value as { kind?: unknown }).kind === 'ref'
|
|
210
|
+
}
|
|
211
|
+
|
|
116
212
|
function lazyHelpers(ctx: Context): Helpers {
|
|
117
213
|
const cache = new Map<string, GenNode>()
|
|
118
214
|
const get = (name: string, type: boolean): GenNode => {
|
|
@@ -127,5 +223,13 @@ function lazyHelpers(ctx: Context): Helpers {
|
|
|
127
223
|
inboundMessage: () => get('InboundMessage', true),
|
|
128
224
|
validateMessage: () => get('validateMessage', false),
|
|
129
225
|
messageHeader: () => get('messageHeader', false),
|
|
226
|
+
paramFunctionOf: () => {
|
|
227
|
+
let node = cache.get('@znight/core#ParamFunctionOf')
|
|
228
|
+
if (node === undefined) {
|
|
229
|
+
node = ctx.importFrom('@znight/core', 'ParamFunctionOf', { type: true })
|
|
230
|
+
cache.set('@znight/core#ParamFunctionOf', node)
|
|
231
|
+
}
|
|
232
|
+
return node
|
|
233
|
+
},
|
|
130
234
|
}
|
|
131
235
|
}
|
package/src/consumers.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { Container, Token } from '@znight/core'
|
|
5
5
|
import type { MessagingDriver } from './driver.ts'
|
|
6
|
+
import type { InboundMessage } from './message.ts'
|
|
6
7
|
import type { ConsumerDefinition, MessageHandlerDefinition, MessagingMetadata } from './metadata.ts'
|
|
7
8
|
import type { RetryOptions, RetryPolicy } from './retry.ts'
|
|
8
9
|
|
|
@@ -22,6 +23,11 @@ import {
|
|
|
22
23
|
/** A handler ready to be dispatched to. */
|
|
23
24
|
export interface PreparedHandler {
|
|
24
25
|
readonly definition: MessageHandlerDefinition
|
|
26
|
+
/**
|
|
27
|
+
* Calls the handler: `definition.invoke`, or for a handler with parameter resolvers (RFC 0010) a
|
|
28
|
+
* function passing their instances, resolved in the delivery scope.
|
|
29
|
+
*/
|
|
30
|
+
readonly invoke: (consumer: unknown, message: InboundMessage) => unknown
|
|
25
31
|
readonly method: string
|
|
26
32
|
readonly policy: RetryPolicy
|
|
27
33
|
}
|
|
@@ -140,7 +146,7 @@ function prepareConsumer(
|
|
|
140
146
|
let maxAttempts = 1
|
|
141
147
|
const nativeDelays = new Set<number>()
|
|
142
148
|
for (const handlerDefinition of handlers) {
|
|
143
|
-
const handler = prepareHandler(handlerDefinition, name, driver, defaults.retry)
|
|
149
|
+
const handler = prepareHandler(handlerDefinition, name, driver, defaults.retry, container)
|
|
144
150
|
maxAttempts = Math.max(maxAttempts, handler.policy.attempts)
|
|
145
151
|
for (const delay of retryDelays(handler.policy)) {
|
|
146
152
|
if (!isInProcessDelay(delay, driver.capabilities)) nativeDelays.add(delay)
|
|
@@ -204,6 +210,7 @@ function prepareHandler(
|
|
|
204
210
|
consumer: string,
|
|
205
211
|
driver: MessagingDriver,
|
|
206
212
|
defaultPolicy: RetryPolicy,
|
|
213
|
+
container: Container,
|
|
207
214
|
): PreparedHandler {
|
|
208
215
|
if (
|
|
209
216
|
definition === null ||
|
|
@@ -227,7 +234,29 @@ function prepareHandler(
|
|
|
227
234
|
driver.name,
|
|
228
235
|
definition.retry === undefined ? `${where} (default retry policy)` : `@Retry on ${where}`,
|
|
229
236
|
)
|
|
230
|
-
return { definition, method: definition.method, policy }
|
|
237
|
+
return { definition, invoke: handlerInvoke(definition, container, where), method: definition.method, policy }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** The generated invoke, or a function passing the parameter resolver instances (resolved per delivery). */
|
|
241
|
+
function handlerInvoke(
|
|
242
|
+
definition: MessageHandlerDefinition,
|
|
243
|
+
container: Container,
|
|
244
|
+
where: string,
|
|
245
|
+
): PreparedHandler['invoke'] {
|
|
246
|
+
const invoke = definition.invoke
|
|
247
|
+
const tokens = definition.paramResolvers
|
|
248
|
+
if (tokens === undefined || tokens.length === 0) return invoke
|
|
249
|
+
for (const token of tokens) {
|
|
250
|
+
if (!container.has(token)) {
|
|
251
|
+
throw consumerError(`${where}: the parameter resolver ${token} is not a provider`, ['Regenerate the metadata'])
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return (consumer, message) =>
|
|
255
|
+
invoke(
|
|
256
|
+
consumer,
|
|
257
|
+
message,
|
|
258
|
+
tokens.map(token => container.get(token)),
|
|
259
|
+
)
|
|
231
260
|
}
|
|
232
261
|
|
|
233
262
|
function duplicateType(consumer: string, type: string): Error {
|
package/src/message.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
declare module '@znight/core' {
|
|
2
|
+
/** Custom parameter decorators (`createParamDecorator`) resolve handler parameters from the {@link InboundMessage}. */
|
|
3
|
+
interface ParamContexts {
|
|
4
|
+
messaging: InboundMessage
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
|
|
1
8
|
/**
|
|
2
9
|
* A message being handled by a consumer. It is the value of the request-scoped external
|
|
3
10
|
* `@znight/messaging#InboundMessage` during a delivery (inject it into request-scoped providers, or
|
package/src/metadata.ts
CHANGED
|
@@ -49,13 +49,19 @@ export interface MessageHandlerDefinition {
|
|
|
49
49
|
readonly type?: string
|
|
50
50
|
/** `@Retry` policy, normalized. Default: the plugin's `defaults.retry`. */
|
|
51
51
|
readonly retry?: RetryPolicy
|
|
52
|
+
/**
|
|
53
|
+
* Provider classes resolving custom parameters (`ParamResolver`, RFC 0010), in order of first use:
|
|
54
|
+
* the runtime resolves them in the delivery scope and passes the instances to `invoke` (`x`).
|
|
55
|
+
* Absent when there is none.
|
|
56
|
+
*/
|
|
57
|
+
readonly paramResolvers?: readonly Token[]
|
|
52
58
|
/**
|
|
53
59
|
* Calls the handler: extracts and validates the parameters from the message
|
|
54
60
|
* (`@Message(schema)` → `validateMessage(schema, m.payload)`, `@Headers(name)` →
|
|
55
|
-
* `messageHeader(m, name)`, `@Ctx()` → `m
|
|
61
|
+
* `messageHeader(m, name)`, `@Ctx()` → `m`, a custom parameter → its resolver, awaited).
|
|
56
62
|
*/
|
|
57
|
-
// biome-ignore lint/suspicious/noExplicitAny: generated invokers receive the concrete consumer class
|
|
58
|
-
readonly invoke: (consumer: any, message: InboundMessage) => unknown
|
|
63
|
+
// biome-ignore lint/suspicious/noExplicitAny: generated invokers receive the concrete consumer class and resolver tuple
|
|
64
|
+
readonly invoke: (consumer: any, message: InboundMessage, resolvers?: any) => unknown
|
|
59
65
|
}
|
|
60
66
|
|
|
61
67
|
declare module '@znight/core' {
|
package/src/runtime.ts
CHANGED
|
@@ -251,7 +251,7 @@ export class ConsumerRuntime {
|
|
|
251
251
|
}
|
|
252
252
|
try {
|
|
253
253
|
const instance = this.#services.container.get(this.consumer.token)
|
|
254
|
-
await handler.
|
|
254
|
+
await handler.invoke(instance, message)
|
|
255
255
|
} catch (error) {
|
|
256
256
|
return this.#failure(context, error, true)
|
|
257
257
|
}
|