@znight/messaging 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +622 -2
- package/dist/async-CFh_bvex.mjs +216 -0
- package/dist/async-CFh_bvex.mjs.map +1 -0
- package/dist/codec-Dku-NXXU.mjs +83 -0
- package/dist/codec-Dku-NXXU.mjs.map +1 -0
- package/dist/compiler.d.mts +75 -0
- package/dist/compiler.mjs +1023 -0
- package/dist/compiler.mjs.map +1 -0
- package/dist/driver-ZxLqy_Ap.d.mts +216 -0
- package/dist/index.d.mts +459 -0
- package/dist/index.mjs +870 -0
- package/dist/index.mjs.map +1 -0
- package/dist/kafka.d.mts +51 -0
- package/dist/kafka.mjs +517 -0
- package/dist/kafka.mjs.map +1 -0
- package/dist/memory.d.mts +124 -0
- package/dist/memory.mjs +429 -0
- package/dist/memory.mjs.map +1 -0
- package/dist/rabbitmq.d.mts +65 -0
- package/dist/rabbitmq.mjs +686 -0
- package/dist/rabbitmq.mjs.map +1 -0
- package/dist/shared-BJBpLS8V.mjs +52 -0
- package/dist/shared-BJBpLS8V.mjs.map +1 -0
- package/dist/sqs.d.mts +38 -0
- package/dist/sqs.mjs +421 -0
- package/dist/sqs.mjs.map +1 -0
- package/package.json +123 -4
- package/src/broker.ts +156 -0
- package/src/codec.ts +98 -0
- package/src/compiler/analyze.ts +800 -0
- package/src/compiler/constants.ts +73 -0
- package/src/compiler/data.ts +75 -0
- package/src/compiler/generate.ts +131 -0
- package/src/compiler/index.ts +52 -0
- package/src/compiler/patterns.ts +54 -0
- package/src/compiler/retry.ts +136 -0
- package/src/compiler/validate.ts +57 -0
- package/src/compiler/values.ts +76 -0
- package/src/consumers.ts +260 -0
- package/src/decorators.ts +131 -0
- package/src/driver.ts +142 -0
- package/src/drivers/kafka.ts +765 -0
- package/src/drivers/memory.ts +609 -0
- package/src/drivers/rabbitmq.ts +944 -0
- package/src/drivers/shared.ts +81 -0
- package/src/drivers/sqs.ts +583 -0
- package/src/errors.ts +95 -0
- package/src/headers.ts +70 -0
- package/src/hooks.ts +2 -0
- package/src/index.ts +89 -0
- package/src/internal/async.ts +115 -0
- package/src/message.ts +96 -0
- package/src/metadata.ts +65 -0
- package/src/plugin.ts +236 -0
- package/src/retry.ts +129 -0
- package/src/runtime.ts +424 -0
- package/src/standard-schema.ts +68 -0
- package/src/tokens.ts +14 -0
- package/src/validation.ts +61 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `analyzeClass` of the messaging extension: reads `@Consumer` classes, their `@OnMessage` handlers
|
|
3
|
+
* (`@Retry`, parameters), their `@OnError` method, registers the consumer provider and reports every
|
|
4
|
+
* problem that can be detected on one class (ZNIGHT005, ZNIGHT011, ZNIGHT017).
|
|
5
|
+
*
|
|
6
|
+
* Handlers may be declared on base classes: the consumer inherits the messaging methods of its
|
|
7
|
+
* analysable base classes (the nearest declaration of a method carrying messaging decorators wins, an
|
|
8
|
+
* override without decorators keeps the inherited handler).
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
AnalyzeContext,
|
|
12
|
+
ClassInfo,
|
|
13
|
+
DecoratorInfo,
|
|
14
|
+
Location,
|
|
15
|
+
MethodInfo,
|
|
16
|
+
ParameterInfo,
|
|
17
|
+
StaticValue,
|
|
18
|
+
TypeHelpers,
|
|
19
|
+
TypeInfo,
|
|
20
|
+
ValueRef,
|
|
21
|
+
} from '@znight/compiler'
|
|
22
|
+
import type { Scope } from '@znight/core'
|
|
23
|
+
import type { HandlerData, HandlerParamData, MessagingData, RetryPolicyData } from './data.ts'
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
CONSUMER_SCOPES,
|
|
27
|
+
EXTENSION_NAME,
|
|
28
|
+
INBOUND_MESSAGE_IDENTITY,
|
|
29
|
+
INVALID_CONSUMER,
|
|
30
|
+
INVALID_DECORATOR,
|
|
31
|
+
METHOD_DECORATORS,
|
|
32
|
+
MessagingDecorator,
|
|
33
|
+
PARAMETER_DECORATORS,
|
|
34
|
+
RUNTIME_MODULE,
|
|
35
|
+
UNVALIDATED_INPUT,
|
|
36
|
+
} from './constants.ts'
|
|
37
|
+
import { isPattern, patternCovers } from './patterns.ts'
|
|
38
|
+
import { normalizeRetryDecorator } from './retry.ts'
|
|
39
|
+
import { describeValue, isRecord, propertyLocation } from './values.ts'
|
|
40
|
+
|
|
41
|
+
type Context = AnalyzeContext<MessagingData>
|
|
42
|
+
|
|
43
|
+
/** The messaging declarations of one method. */
|
|
44
|
+
interface MemberAnalysis {
|
|
45
|
+
readonly method: string
|
|
46
|
+
readonly declaredBy: string
|
|
47
|
+
/** Number of `@OnMessage` decorators (valid or not). */
|
|
48
|
+
readonly onMessage: number
|
|
49
|
+
/** One entry per valid `@OnMessage` decorator. */
|
|
50
|
+
readonly handlers: readonly HandlerData[]
|
|
51
|
+
/** Location of the `@OnError` decorator when the method is the error handler. */
|
|
52
|
+
readonly errorHandler: Location | undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Types injected as is by `@Message()` without being "unvalidated input" (binary payloads). */
|
|
56
|
+
const BINARY_TYPES: ReadonlySet<string> = new Set([
|
|
57
|
+
'globalThis#Uint8Array',
|
|
58
|
+
'globalThis#ArrayBuffer',
|
|
59
|
+
'globalThis#Buffer',
|
|
60
|
+
'node:buffer#Buffer',
|
|
61
|
+
'buffer#Buffer',
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
/** Maximum depth of the base class chain walked for inherited handlers. */
|
|
65
|
+
const MAX_CHAIN = 32
|
|
66
|
+
|
|
67
|
+
/** Analyses a class carrying at least one decorator of the extension. */
|
|
68
|
+
export function analyzeMessagingClass(cls: ClassInfo, ctx: Context): void {
|
|
69
|
+
const own = analyzeMembers(cls, ctx)
|
|
70
|
+
const consumers = cls.decorators.filter(decorator => decorator.identity === MessagingDecorator.Consumer)
|
|
71
|
+
const consumer = consumers[0]
|
|
72
|
+
if (consumer === undefined) {
|
|
73
|
+
const decorators = cls.methods.flatMap(method =>
|
|
74
|
+
method.decorators
|
|
75
|
+
.filter(decorator => decorator.identity !== undefined && METHOD_DECORATORS.has(decorator.identity))
|
|
76
|
+
.map(decorator => ({ name: decorator.name, location: decorator.location })),
|
|
77
|
+
)
|
|
78
|
+
if (decorators.length > 0) {
|
|
79
|
+
ctx.store({
|
|
80
|
+
kind: 'members',
|
|
81
|
+
identity: cls.identity,
|
|
82
|
+
displayName: cls.displayName,
|
|
83
|
+
abstract: cls.abstract,
|
|
84
|
+
decorators,
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
for (const duplicate of consumers.slice(1)) {
|
|
90
|
+
ctx.report({
|
|
91
|
+
code: INVALID_DECORATOR,
|
|
92
|
+
severity: 'error',
|
|
93
|
+
message: `@${duplicate.name} is applied several times on ${cls.displayName}`,
|
|
94
|
+
location: duplicate.location,
|
|
95
|
+
hints: [`Keep a single @${duplicate.name}: a class consumes one queue`],
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const options = readConsumerOptions(consumer, cls, ctx)
|
|
100
|
+
const token = ctx.registerProvider(cls, { scope: options.scope })
|
|
101
|
+
const chain = classChain(cls, ctx)
|
|
102
|
+
|
|
103
|
+
// messaging methods of the class and its bases: the nearest declaration wins, at the position of
|
|
104
|
+
// the first declaration (base classes first)
|
|
105
|
+
const merged = new Map<string, MemberAnalysis>()
|
|
106
|
+
for (let index = chain.length - 1; index >= 0; index--) {
|
|
107
|
+
const owner = chain[index] as ClassInfo
|
|
108
|
+
for (const [name, member] of index === 0 ? own : analyzeMembers(owner, ctx)) merged.set(name, member)
|
|
109
|
+
}
|
|
110
|
+
const members = [...merged.values()]
|
|
111
|
+
const handlers = members.flatMap(member => member.handlers)
|
|
112
|
+
|
|
113
|
+
if (!members.some(member => member.onMessage > 0)) {
|
|
114
|
+
ctx.report({
|
|
115
|
+
code: INVALID_CONSUMER,
|
|
116
|
+
severity: 'error',
|
|
117
|
+
message: `@Consumer ${cls.displayName} has no @OnMessage handler`,
|
|
118
|
+
location: consumer.location,
|
|
119
|
+
hints: ['Decorate at least one public method with @OnMessage() (default handler) or @OnMessage("type")'],
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
checkHandlerTypes(cls.displayName, handlers, ctx)
|
|
123
|
+
|
|
124
|
+
const errorHandlers = members.filter(member => member.errorHandler !== undefined)
|
|
125
|
+
const errorHandler = errorHandlers[0]
|
|
126
|
+
for (const extra of errorHandlers.slice(1)) {
|
|
127
|
+
ctx.report({
|
|
128
|
+
code: INVALID_CONSUMER,
|
|
129
|
+
severity: 'error',
|
|
130
|
+
message: `@Consumer ${cls.displayName} has several @OnError methods: ${errorHandlers.map(member => `${member.declaredBy}.${member.method}()`).join(', ')}`,
|
|
131
|
+
location: extra.errorHandler as Location,
|
|
132
|
+
hints: ['Keep a single @OnError method per consumer'],
|
|
133
|
+
related: [{ message: 'First @OnError method', location: errorHandler?.errorHandler as Location }],
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
ctx.store({
|
|
138
|
+
kind: 'consumer',
|
|
139
|
+
token,
|
|
140
|
+
identity: cls.identity,
|
|
141
|
+
displayName: cls.displayName,
|
|
142
|
+
queue: options.queue,
|
|
143
|
+
options: options.options,
|
|
144
|
+
handlers,
|
|
145
|
+
errorHandler: errorHandler?.method,
|
|
146
|
+
chain: chain.map(owner => owner.identity),
|
|
147
|
+
location: consumer.location,
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ------------------------------------------------------------------------------------------ consumer
|
|
152
|
+
|
|
153
|
+
interface ConsumerOptions {
|
|
154
|
+
/** Empty when invalid (reported). */
|
|
155
|
+
readonly queue: string
|
|
156
|
+
readonly scope: Scope
|
|
157
|
+
readonly options: Readonly<Record<string, StaticValue>> | undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** `@Consumer(queue)` / `@Consumer({ queue, scope?, concurrency?, deadLetter?, ...driver options })`. */
|
|
161
|
+
function readConsumerOptions(decorator: DecoratorInfo, cls: ClassInfo, ctx: Context): ConsumerOptions {
|
|
162
|
+
const invalid: ConsumerOptions = { queue: '', scope: 'singleton', options: undefined }
|
|
163
|
+
const prefix = `@Consumer ${cls.displayName}`
|
|
164
|
+
if (!decorator.called) return invalid // `@Consumer` without call: already reported
|
|
165
|
+
const arg = decorator.args[0]
|
|
166
|
+
if (arg === undefined) {
|
|
167
|
+
ctx.report({
|
|
168
|
+
code: INVALID_DECORATOR,
|
|
169
|
+
severity: 'error',
|
|
170
|
+
message: `${prefix}: the queue to consume is required`,
|
|
171
|
+
location: decorator.location,
|
|
172
|
+
hints: ["Write @Consumer('orders') or @Consumer({ queue: 'orders', concurrency: 5 })"],
|
|
173
|
+
})
|
|
174
|
+
return invalid
|
|
175
|
+
}
|
|
176
|
+
if (!arg.evaluated) return invalid // not statically evaluable: already reported
|
|
177
|
+
const value = arg.value
|
|
178
|
+
if (typeof value === 'string') {
|
|
179
|
+
if (value === '') {
|
|
180
|
+
ctx.report({
|
|
181
|
+
code: INVALID_CONSUMER,
|
|
182
|
+
severity: 'error',
|
|
183
|
+
message: `${prefix}: the queue must be a non-empty string`,
|
|
184
|
+
location: arg.location,
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
return { queue: value, scope: 'singleton', options: undefined }
|
|
188
|
+
}
|
|
189
|
+
if (!isRecord(value)) {
|
|
190
|
+
ctx.report({
|
|
191
|
+
code: INVALID_DECORATOR,
|
|
192
|
+
severity: 'error',
|
|
193
|
+
message: `${prefix}: expected a queue name or an options object ({ queue, scope?, concurrency?, deadLetter? }), got ${describeValue(value)}`,
|
|
194
|
+
location: arg.location,
|
|
195
|
+
})
|
|
196
|
+
return invalid
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const fail = (key: string, message: string, hints?: readonly string[]): void => {
|
|
200
|
+
ctx.report({
|
|
201
|
+
code: INVALID_CONSUMER,
|
|
202
|
+
severity: 'error',
|
|
203
|
+
message: `${prefix}: ${message}`,
|
|
204
|
+
location: propertyLocation(ctx.project, arg, key),
|
|
205
|
+
...(hints === undefined ? {} : { hints }),
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let queue = ''
|
|
210
|
+
const rawQueue = value['queue']
|
|
211
|
+
if (rawQueue === undefined) {
|
|
212
|
+
fail('queue', 'the "queue" option is required', ["Write @Consumer({ queue: 'orders', ... })"])
|
|
213
|
+
} else if (typeof rawQueue !== 'string' || rawQueue === '') {
|
|
214
|
+
fail('queue', `"queue" must be a non-empty string, got ${describeValue(rawQueue)}`)
|
|
215
|
+
} else {
|
|
216
|
+
queue = rawQueue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let scope: Scope = 'singleton'
|
|
220
|
+
const rawScope = value['scope']
|
|
221
|
+
if (rawScope !== undefined) {
|
|
222
|
+
if (typeof rawScope === 'string' && CONSUMER_SCOPES.includes(rawScope)) {
|
|
223
|
+
scope = rawScope as Scope
|
|
224
|
+
} else {
|
|
225
|
+
fail('scope', `"scope" must be 'singleton', 'request' or 'transient', got ${describeValue(rawScope)}`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const concurrency = value['concurrency']
|
|
230
|
+
if (
|
|
231
|
+
concurrency !== undefined &&
|
|
232
|
+
!(typeof concurrency === 'number' && Number.isSafeInteger(concurrency) && concurrency > 0)
|
|
233
|
+
) {
|
|
234
|
+
fail('concurrency', `"concurrency" must be a positive integer, got ${describeValue(concurrency)}`)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const deadLetter = value['deadLetter']
|
|
238
|
+
if (deadLetter !== undefined && deadLetter !== false && (typeof deadLetter !== 'string' || deadLetter === '')) {
|
|
239
|
+
fail('deadLetter', `"deadLetter" must be a queue name or false, got ${describeValue(deadLetter)}`)
|
|
240
|
+
} else if (deadLetter === queue && queue !== '') {
|
|
241
|
+
fail('deadLetter', `the dead-letter queue cannot be the consumed queue "${queue}"`, [
|
|
242
|
+
'Use another queue (default: the driver naming, e.g. "orders.dlq"), or false to disable dead-lettering',
|
|
243
|
+
])
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const options: Record<string, StaticValue> = {}
|
|
247
|
+
for (const [key, option] of Object.entries(value)) {
|
|
248
|
+
if (key === 'queue' || key === 'scope' || option === undefined) continue
|
|
249
|
+
options[key] = option
|
|
250
|
+
}
|
|
251
|
+
return { queue, scope, options: Object.keys(options).length === 0 ? undefined : Object.freeze(options) }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The class and its analysable base classes (nearest first). */
|
|
255
|
+
function classChain(cls: ClassInfo, ctx: Context): ClassInfo[] {
|
|
256
|
+
const chain = [cls]
|
|
257
|
+
const seen = new Set([cls.identity])
|
|
258
|
+
let current = cls
|
|
259
|
+
while (current.superClass !== undefined && chain.length < MAX_CHAIN) {
|
|
260
|
+
const base = ctx.classInfo(current.superClass.identity)
|
|
261
|
+
if (base === undefined || seen.has(base.identity)) break
|
|
262
|
+
chain.push(base)
|
|
263
|
+
seen.add(base.identity)
|
|
264
|
+
current = base
|
|
265
|
+
}
|
|
266
|
+
return chain
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Handler selection checks, mirroring the runtime (`consumers.ts`): exact types first, then patterns
|
|
271
|
+
* in declaration order, then the single default handler.
|
|
272
|
+
*/
|
|
273
|
+
function checkHandlerTypes(consumer: string, handlers: readonly HandlerData[], ctx: Context): void {
|
|
274
|
+
let fallback: HandlerData | undefined
|
|
275
|
+
const exact = new Map<string, HandlerData>()
|
|
276
|
+
const patterns: (HandlerData & { readonly type: string })[] = []
|
|
277
|
+
const label = (handler: HandlerData): string => `${handler.declaredBy}.${handler.method}()`
|
|
278
|
+
for (const handler of handlers) {
|
|
279
|
+
const type = handler.type
|
|
280
|
+
if (type === undefined) {
|
|
281
|
+
if (fallback === undefined) {
|
|
282
|
+
fallback = handler
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
ctx.report({
|
|
286
|
+
code: INVALID_CONSUMER,
|
|
287
|
+
severity: 'error',
|
|
288
|
+
message: `@Consumer ${consumer}: ${label(fallback)} and ${label(handler)} are both default @OnMessage() handlers`,
|
|
289
|
+
location: handler.location,
|
|
290
|
+
hints: ['Give one of them a message type: @OnMessage("order.created")'],
|
|
291
|
+
related: [{ message: 'First default handler', location: fallback.location }],
|
|
292
|
+
})
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
const previous = isPattern(type) ? patterns.find(pattern => pattern.type === type) : exact.get(type)
|
|
296
|
+
if (previous !== undefined) {
|
|
297
|
+
ctx.report({
|
|
298
|
+
code: INVALID_CONSUMER,
|
|
299
|
+
severity: 'error',
|
|
300
|
+
message: `@Consumer ${consumer}: several @OnMessage handlers for type "${type}" (${label(previous)} and ${label(handler)})`,
|
|
301
|
+
location: handler.location,
|
|
302
|
+
hints: ['A message type is handled by a single method: merge the handlers'],
|
|
303
|
+
related: [{ message: `First handler of "${type}"`, location: previous.location }],
|
|
304
|
+
})
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
if (!isPattern(type)) {
|
|
308
|
+
exact.set(type, handler)
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
const covering = patterns.find(pattern => patternCovers(pattern.type, type))
|
|
312
|
+
if (covering !== undefined) {
|
|
313
|
+
ctx.report({
|
|
314
|
+
code: INVALID_CONSUMER,
|
|
315
|
+
severity: 'error',
|
|
316
|
+
message: `@Consumer ${consumer}: the pattern "${type}" of ${label(handler)} never matches: every type it matches is matched first by "${covering.type}" (${label(covering)})`,
|
|
317
|
+
location: handler.location,
|
|
318
|
+
hints: ['Patterns are tried in declaration order (after exact types): declare the more specific pattern first'],
|
|
319
|
+
related: [{ message: `Pattern "${covering.type}"`, location: covering.location }],
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
patterns.push({ ...handler, type })
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// ------------------------------------------------------------------------------------------- members
|
|
327
|
+
|
|
328
|
+
/** Messaging methods declared by `cls` itself, by name, in declaration order. */
|
|
329
|
+
function analyzeMembers(cls: ClassInfo, ctx: Context): Map<string, MemberAnalysis> {
|
|
330
|
+
const members = new Map<string, MemberAnalysis>()
|
|
331
|
+
for (const method of cls.methods) {
|
|
332
|
+
const member = analyzeMethod(cls, method, ctx)
|
|
333
|
+
if (member !== undefined) members.set(method.name, member)
|
|
334
|
+
}
|
|
335
|
+
return members
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function analyzeMethod(cls: ClassInfo, method: MethodInfo, ctx: Context): MemberAnalysis | undefined {
|
|
339
|
+
const onMessage = decoratorsOf(method.decorators, MessagingDecorator.OnMessage)
|
|
340
|
+
const retry = decoratorsOf(method.decorators, MessagingDecorator.Retry)
|
|
341
|
+
const onError = decoratorsOf(method.decorators, MessagingDecorator.OnError)
|
|
342
|
+
const decoratedParams = method.params.some(param => param.decorators.some(isParameterDecorator))
|
|
343
|
+
if (onMessage.length === 0 && retry.length === 0 && onError.length === 0 && !decoratedParams) return undefined
|
|
344
|
+
|
|
345
|
+
const where = `${cls.displayName}.${method.name}()`
|
|
346
|
+
const primary = onMessage[0] ?? onError[0]
|
|
347
|
+
if (primary !== undefined) checkMethodShape(method, primary, where, ctx)
|
|
348
|
+
for (const duplicate of [...retry.slice(1), ...onError.slice(1)]) {
|
|
349
|
+
ctx.report({
|
|
350
|
+
code: INVALID_DECORATOR,
|
|
351
|
+
severity: 'error',
|
|
352
|
+
message: `@${duplicate.name} is applied several times on ${where}`,
|
|
353
|
+
location: duplicate.location,
|
|
354
|
+
hints: [`Keep a single @${duplicate.name}`],
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
if (onMessage.length > 0 && onError[0] !== undefined) {
|
|
358
|
+
ctx.report({
|
|
359
|
+
code: INVALID_DECORATOR,
|
|
360
|
+
severity: 'error',
|
|
361
|
+
message: `${where} cannot be both an @OnMessage handler and the @OnError method`,
|
|
362
|
+
location: onError[0].location,
|
|
363
|
+
hints: ['Declare the error handler in a separate method'],
|
|
364
|
+
})
|
|
365
|
+
}
|
|
366
|
+
if (onMessage.length === 0 && retry[0] !== undefined) {
|
|
367
|
+
ctx.report({
|
|
368
|
+
code: INVALID_DECORATOR,
|
|
369
|
+
severity: 'error',
|
|
370
|
+
message: `@Retry has no effect on ${where}: it is not an @OnMessage handler`,
|
|
371
|
+
location: retry[0].location,
|
|
372
|
+
hints: ['Add @OnMessage() to the method, or remove @Retry'],
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
let handlers: HandlerData[] = []
|
|
377
|
+
if (onMessage.length > 0) {
|
|
378
|
+
const policy: RetryPolicyData | undefined =
|
|
379
|
+
retry[0] === undefined ? undefined : normalizeRetryDecorator(retry[0], where, ctx)
|
|
380
|
+
const params = analyzeHandlerParams(method, where, ctx)
|
|
381
|
+
handlers = onMessage.flatMap(decorator => {
|
|
382
|
+
const type = messageType(decorator, where, ctx)
|
|
383
|
+
if (type === false) return []
|
|
384
|
+
return [
|
|
385
|
+
Object.freeze({
|
|
386
|
+
method: method.name,
|
|
387
|
+
type,
|
|
388
|
+
retry: policy,
|
|
389
|
+
params,
|
|
390
|
+
declaredBy: cls.displayName,
|
|
391
|
+
location: decorator.location,
|
|
392
|
+
}),
|
|
393
|
+
]
|
|
394
|
+
})
|
|
395
|
+
} else {
|
|
396
|
+
reportStrayParameterDecorators(method, where, onError.length > 0, ctx)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const errorHandler = onMessage.length === 0 ? onError[0] : undefined
|
|
400
|
+
if (errorHandler !== undefined) checkErrorHandler(method, errorHandler, where, ctx)
|
|
401
|
+
return {
|
|
402
|
+
method: method.name,
|
|
403
|
+
declaredBy: cls.displayName,
|
|
404
|
+
onMessage: onMessage.length,
|
|
405
|
+
handlers,
|
|
406
|
+
errorHandler: errorHandler?.location,
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Handlers are called by the generated code: `consumer.method(...)`. */
|
|
411
|
+
function checkMethodShape(method: MethodInfo, decorator: DecoratorInfo, where: string, ctx: Context): void {
|
|
412
|
+
const name = `@${decorator.name}`
|
|
413
|
+
let problem: { message: string; hint: string } | undefined
|
|
414
|
+
if (method.kind !== 'method') {
|
|
415
|
+
problem = { message: `${name} cannot be applied to the accessor ${where}`, hint: 'Use a regular method' }
|
|
416
|
+
} else if (method.static) {
|
|
417
|
+
problem = {
|
|
418
|
+
message: `${name} cannot be applied to the static method ${where}`,
|
|
419
|
+
hint: 'Handlers are called on the consumer instance: remove `static`',
|
|
420
|
+
}
|
|
421
|
+
} else if (method.privateName) {
|
|
422
|
+
problem = {
|
|
423
|
+
message: `${name} cannot be applied to the #private method ${where}`,
|
|
424
|
+
hint: 'Use a public method: the generated code calls it',
|
|
425
|
+
}
|
|
426
|
+
} else if (method.visibility !== 'public') {
|
|
427
|
+
problem = {
|
|
428
|
+
message: `${where} must be public: ${name} methods are called by the generated code`,
|
|
429
|
+
hint: `Remove the '${method.visibility}' modifier`,
|
|
430
|
+
}
|
|
431
|
+
} else if (method.computed) {
|
|
432
|
+
problem = {
|
|
433
|
+
message: `${name} cannot be applied to ${where}: computed method names are not supported`,
|
|
434
|
+
hint: 'Use a plain method name',
|
|
435
|
+
}
|
|
436
|
+
} else if (method.generator) {
|
|
437
|
+
problem = {
|
|
438
|
+
message: `${name} cannot be applied to the generator method ${where}`,
|
|
439
|
+
hint: 'Use a regular (or async) method',
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if (problem !== undefined) {
|
|
443
|
+
ctx.report({
|
|
444
|
+
code: INVALID_DECORATOR,
|
|
445
|
+
severity: 'error',
|
|
446
|
+
message: problem.message,
|
|
447
|
+
location: decorator.location,
|
|
448
|
+
hints: [problem.hint],
|
|
449
|
+
})
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/** Type of an `@OnMessage` decorator: a string, `undefined` (default handler) or `false` (invalid). */
|
|
454
|
+
function messageType(decorator: DecoratorInfo, where: string, ctx: Context): string | undefined | false {
|
|
455
|
+
const arg = decorator.args[0]
|
|
456
|
+
if (arg === undefined || (arg.evaluated && arg.value === undefined)) return undefined
|
|
457
|
+
if (!arg.evaluated) return false // not statically evaluable: already reported
|
|
458
|
+
if (typeof arg.value !== 'string') {
|
|
459
|
+
ctx.report({
|
|
460
|
+
code: INVALID_DECORATOR,
|
|
461
|
+
severity: 'error',
|
|
462
|
+
message: `@OnMessage on ${where} expects a message type (a string), got ${describeValue(arg.value)}`,
|
|
463
|
+
location: arg.location,
|
|
464
|
+
hints: ['Use @OnMessage() for the default handler'],
|
|
465
|
+
})
|
|
466
|
+
return false
|
|
467
|
+
}
|
|
468
|
+
if (arg.value === '') {
|
|
469
|
+
ctx.report({
|
|
470
|
+
code: INVALID_CONSUMER,
|
|
471
|
+
severity: 'error',
|
|
472
|
+
message: `The @OnMessage type of ${where} must be a non-empty string`,
|
|
473
|
+
location: arg.location,
|
|
474
|
+
hints: ['Use @OnMessage() for the default handler'],
|
|
475
|
+
})
|
|
476
|
+
return false
|
|
477
|
+
}
|
|
478
|
+
return arg.value
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** The `@OnError` method is called as `consumer[method](error, message)`. */
|
|
482
|
+
function checkErrorHandler(method: MethodInfo, decorator: DecoratorInfo, where: string, ctx: Context): void {
|
|
483
|
+
const required = method.params.filter(param => !param.optional && !param.hasDefault && !param.rest).length
|
|
484
|
+
if (required > 2) {
|
|
485
|
+
ctx.report({
|
|
486
|
+
code: INVALID_CONSUMER,
|
|
487
|
+
severity: 'error',
|
|
488
|
+
message: `The @OnError method ${where} has ${required} required parameters: it is called with (error, message)`,
|
|
489
|
+
location: method.location,
|
|
490
|
+
hints: ['Declare onError(error: unknown, message: InboundMessage)'],
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
if (method.returnType !== undefined && !isErrorHandlerResult(ctx.types.awaited(method.returnType))) {
|
|
494
|
+
ctx.report({
|
|
495
|
+
code: INVALID_CONSUMER,
|
|
496
|
+
severity: 'error',
|
|
497
|
+
message: `The @OnError method ${where} returns '${ctx.types.format(method.returnType)}': return 'ack' to acknowledge the message, or nothing to dead-letter it`,
|
|
498
|
+
location: decorator.location,
|
|
499
|
+
hints: ["Declare the return type `'ack' | undefined` (or `void`)"],
|
|
500
|
+
})
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// ---------------------------------------------------------------------------------------- parameters
|
|
505
|
+
|
|
506
|
+
function analyzeHandlerParams(method: MethodInfo, where: string, ctx: Context): readonly HandlerParamData[] {
|
|
507
|
+
return Object.freeze(method.params.map(param => analyzeHandlerParam(param, where, ctx)))
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function analyzeHandlerParam(param: ParameterInfo, where: string, ctx: Context): HandlerParamData {
|
|
511
|
+
const decorators = parameterDecorators(param)
|
|
512
|
+
const decorator = decorators[0]
|
|
513
|
+
if (decorator === undefined) {
|
|
514
|
+
const foreign = foreignParameterDecorator(param)
|
|
515
|
+
if (foreign !== undefined) {
|
|
516
|
+
// e.g. `Headers` auto-imported from '@znight/http': one precise error instead of "no decorator"
|
|
517
|
+
ctx.report({
|
|
518
|
+
code: INVALID_DECORATOR,
|
|
519
|
+
severity: 'error',
|
|
520
|
+
message: `@${foreign.decorator.name} on parameter '${param.name}' of ${where} is imported from '${foreign.module}': @OnMessage handler parameters use ${foreign.name} from '${RUNTIME_MODULE}'`,
|
|
521
|
+
location: foreign.decorator.location,
|
|
522
|
+
hints: [`Import it from '${RUNTIME_MODULE}': import { ${foreign.name} } from '${RUNTIME_MODULE}'`],
|
|
523
|
+
})
|
|
524
|
+
return { kind: 'undefined' }
|
|
525
|
+
}
|
|
526
|
+
if (param.optional || param.hasDefault || param.rest) return { kind: 'undefined' }
|
|
527
|
+
ctx.report({
|
|
528
|
+
code: INVALID_CONSUMER,
|
|
529
|
+
severity: 'error',
|
|
530
|
+
message: `Parameter '${param.name}' of ${where} has no @Message(), @Headers() or @Ctx() decorator`,
|
|
531
|
+
location: param.location,
|
|
532
|
+
hints: [
|
|
533
|
+
'Every handler parameter is extracted from the message: @Message(schema?) for the payload, @Headers(name?) for the headers, @Ctx() for the InboundMessage',
|
|
534
|
+
],
|
|
535
|
+
})
|
|
536
|
+
return { kind: 'undefined' }
|
|
537
|
+
}
|
|
538
|
+
if (decorators.length > 1) {
|
|
539
|
+
ctx.report({
|
|
540
|
+
code: INVALID_DECORATOR,
|
|
541
|
+
severity: 'error',
|
|
542
|
+
message: `Parameter '${param.name}' of ${where} has several messaging decorators (${decorators.map(found => `@${found.name}`).join(', ')})`,
|
|
543
|
+
location: (decorators[1] as DecoratorInfo).location,
|
|
544
|
+
hints: ['Keep a single one'],
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
if (param.rest) {
|
|
548
|
+
ctx.report({
|
|
549
|
+
code: INVALID_DECORATOR,
|
|
550
|
+
severity: 'error',
|
|
551
|
+
message: `@${decorator.name} cannot be applied to the rest parameter '...${param.name}' of ${where}`,
|
|
552
|
+
location: decorator.location,
|
|
553
|
+
})
|
|
554
|
+
return { kind: 'undefined' }
|
|
555
|
+
}
|
|
556
|
+
switch (decorator.identity) {
|
|
557
|
+
case MessagingDecorator.Message:
|
|
558
|
+
return messageParam(param, decorator, where, ctx)
|
|
559
|
+
case MessagingDecorator.Headers:
|
|
560
|
+
return headersParam(param, decorator, where, ctx)
|
|
561
|
+
default:
|
|
562
|
+
return { kind: 'message', narrow: narrowsPayload(param.type, ctx.types) }
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/** `@Message()` / `@Message(schema)`. */
|
|
567
|
+
function messageParam(param: ParameterInfo, decorator: DecoratorInfo, where: string, ctx: Context): HandlerParamData {
|
|
568
|
+
const arg = decorator.args[0]
|
|
569
|
+
if (arg === undefined || (arg.evaluated && arg.value === undefined)) {
|
|
570
|
+
checkUnvalidated(param, decorator, where, ctx)
|
|
571
|
+
return { kind: 'payload' }
|
|
572
|
+
}
|
|
573
|
+
if (arg.ref !== undefined) {
|
|
574
|
+
if (isClassReference(arg.ref)) {
|
|
575
|
+
ctx.report({
|
|
576
|
+
code: INVALID_DECORATOR,
|
|
577
|
+
severity: 'error',
|
|
578
|
+
message: `@Message(${arg.text}) on ${where}: '${arg.ref.name}' is a class, not a Standard Schema`,
|
|
579
|
+
location: arg.location,
|
|
580
|
+
hints: [
|
|
581
|
+
'Pass a Standard Schema (Zod, Valibot, ArkType...): export const OrderPlaced = z.object({ ... })',
|
|
582
|
+
'Or inject the raw payload with @Message() and validate it in the handler',
|
|
583
|
+
],
|
|
584
|
+
})
|
|
585
|
+
return { kind: 'undefined' }
|
|
586
|
+
}
|
|
587
|
+
return { kind: 'validated', schema: arg.ref }
|
|
588
|
+
}
|
|
589
|
+
if (!arg.evaluated) return { kind: 'undefined' } // unexported or invalid reference: already reported
|
|
590
|
+
ctx.report({
|
|
591
|
+
code: INVALID_DECORATOR,
|
|
592
|
+
severity: 'error',
|
|
593
|
+
message: `@Message on ${where} expects a Standard Schema (Zod, Valibot, ArkType...), got ${describeValue(arg.value)}`,
|
|
594
|
+
location: arg.location,
|
|
595
|
+
hints: [
|
|
596
|
+
'Declare the schema with `export const OrderPlaced = z.object({ ... })` and pass it: @Message(OrderPlaced)',
|
|
597
|
+
],
|
|
598
|
+
})
|
|
599
|
+
return { kind: 'undefined' }
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** `@Headers()` / `@Headers(name)`. */
|
|
603
|
+
function headersParam(param: ParameterInfo, decorator: DecoratorInfo, where: string, ctx: Context): HandlerParamData {
|
|
604
|
+
const arg = decorator.args[0]
|
|
605
|
+
if (arg === undefined || (arg.evaluated && arg.value === undefined)) return { kind: 'headers' }
|
|
606
|
+
if (!arg.evaluated) return { kind: 'undefined' } // not statically evaluable: already reported
|
|
607
|
+
const name = arg.value
|
|
608
|
+
if (typeof name !== 'string' || name === '') {
|
|
609
|
+
ctx.report({
|
|
610
|
+
code: INVALID_DECORATOR,
|
|
611
|
+
severity: 'error',
|
|
612
|
+
message: `@Headers on ${where} expects a header name (a non-empty string), got ${describeValue(name)}`,
|
|
613
|
+
location: arg.location,
|
|
614
|
+
hints: ['Use @Headers() to inject every header'],
|
|
615
|
+
})
|
|
616
|
+
return { kind: 'undefined' }
|
|
617
|
+
}
|
|
618
|
+
if (!param.optional && !param.hasDefault && !acceptsUndefined(param.type)) {
|
|
619
|
+
ctx.report({
|
|
620
|
+
code: INVALID_DECORATOR,
|
|
621
|
+
severity: 'warning',
|
|
622
|
+
message: `@Headers(${JSON.stringify(name)}) injects 'string | undefined' (the header may be missing) but parameter '${param.name}' of ${where} is typed '${ctx.types.format(param.type)}'`,
|
|
623
|
+
location: param.typeLocation,
|
|
624
|
+
hints: [`Declare it '${param.name}?: string' or '${param.name}: string | undefined', or give it a default value`],
|
|
625
|
+
})
|
|
626
|
+
}
|
|
627
|
+
return { kind: 'header', name }
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/** ZNIGHT017: a structured payload injected without schema is trusted blindly. */
|
|
631
|
+
function checkUnvalidated(param: ParameterInfo, decorator: DecoratorInfo, where: string, ctx: Context): void {
|
|
632
|
+
const { type } = ctx.types.stripNullish(param.type)
|
|
633
|
+
if (isSafePayloadType(type)) return
|
|
634
|
+
const typed =
|
|
635
|
+
type.kind === 'opaque' && type.reason === 'missing-annotation'
|
|
636
|
+
? 'has no type annotation'
|
|
637
|
+
: `is typed '${ctx.types.format(param.type)}'`
|
|
638
|
+
ctx.report({
|
|
639
|
+
code: UNVALIDATED_INPUT,
|
|
640
|
+
severity: ctx.options.strictValidation ? 'error' : 'warning',
|
|
641
|
+
message: `Unvalidated message payload: parameter '${param.name}' of ${where} ${typed} but @Message() has no schema`,
|
|
642
|
+
location: decorator.location,
|
|
643
|
+
hints: [
|
|
644
|
+
'Validate it with a Standard Schema (Zod, Valibot, ArkType...): @Message(OrderPlaced)',
|
|
645
|
+
"Or declare the parameter as 'unknown' and check it in the handler",
|
|
646
|
+
],
|
|
647
|
+
})
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Methods whose parameters carry messaging decorators without being `@OnMessage` handlers. */
|
|
651
|
+
function reportStrayParameterDecorators(method: MethodInfo, where: string, errorHandler: boolean, ctx: Context): void {
|
|
652
|
+
// a method handled by another extension (an HTTP route...): the decorator comes from the wrong package
|
|
653
|
+
const owner = errorHandler ? undefined : foreignHandler(method)
|
|
654
|
+
for (const param of method.params) {
|
|
655
|
+
for (const decorator of parameterDecorators(param)) {
|
|
656
|
+
let hint = 'Add @OnMessage() to the method, or remove the decorator'
|
|
657
|
+
if (errorHandler) hint = '@OnError methods are called with (error, message): remove the decorator'
|
|
658
|
+
else if (owner !== undefined) {
|
|
659
|
+
hint = `${where} is a @${owner.decorator.name} handler: use the parameter decorators of '${owner.module}'`
|
|
660
|
+
}
|
|
661
|
+
ctx.report({
|
|
662
|
+
code: INVALID_DECORATOR,
|
|
663
|
+
severity: 'error',
|
|
664
|
+
message:
|
|
665
|
+
owner === undefined
|
|
666
|
+
? `@${decorator.name} has no effect on parameter '${param.name}' of ${where}: it is not an @OnMessage handler`
|
|
667
|
+
: `@${decorator.name} (from '${RUNTIME_MODULE}') has no effect on parameter '${param.name}' of ${where}: it is not an @OnMessage handler`,
|
|
668
|
+
location: decorator.location,
|
|
669
|
+
hints: [hint],
|
|
670
|
+
})
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** A decorator, with the module and the export name of its identity (`'@znight/http#Get'`). */
|
|
676
|
+
interface ForeignDecorator {
|
|
677
|
+
readonly decorator: DecoratorInfo
|
|
678
|
+
readonly module: string
|
|
679
|
+
readonly name: string
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function splitIdentity(decorator: DecoratorInfo): ForeignDecorator | undefined {
|
|
683
|
+
const identity = decorator.identity
|
|
684
|
+
const separator = identity?.lastIndexOf('#') ?? -1
|
|
685
|
+
if (identity === undefined || separator <= 0) return undefined
|
|
686
|
+
return { decorator, module: identity.slice(0, separator), name: identity.slice(separator + 1) }
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* A parameter decorator named like a messaging one (`Headers`, `Ctx`, `Message`) but imported from
|
|
691
|
+
* another package (`@znight/http` has `Headers` and `Ctx` too).
|
|
692
|
+
*/
|
|
693
|
+
function foreignParameterDecorator(param: ParameterInfo): ForeignDecorator | undefined {
|
|
694
|
+
for (const decorator of param.decorators) {
|
|
695
|
+
const split = splitIdentity(decorator)
|
|
696
|
+
if (split === undefined || split.module === RUNTIME_MODULE) continue
|
|
697
|
+
if (PARAMETER_DECORATORS.has(`${RUNTIME_MODULE}#${split.name}`)) return split
|
|
698
|
+
}
|
|
699
|
+
return undefined
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/** The decorator through which another extension handles the method (`@Get` of `@znight/http`...). */
|
|
703
|
+
function foreignHandler(method: MethodInfo): ForeignDecorator | undefined {
|
|
704
|
+
for (const decorator of method.decorators) {
|
|
705
|
+
const owner = decorator.owner
|
|
706
|
+
if (owner === undefined || owner === EXTENSION_NAME || owner === 'core' || owner === 'manifest') continue
|
|
707
|
+
const split = splitIdentity(decorator)
|
|
708
|
+
if (split !== undefined) return split
|
|
709
|
+
}
|
|
710
|
+
return undefined
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// ------------------------------------------------------------------------------------------- helpers
|
|
714
|
+
|
|
715
|
+
function decoratorsOf(decorators: readonly DecoratorInfo[], identity: string): DecoratorInfo[] {
|
|
716
|
+
return decorators.filter(decorator => decorator.identity === identity)
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Messaging decorators of a parameter. De-duplicated by position: the compiler (0.1) reports the
|
|
721
|
+
* decorators of a parameter with a default value (`@Headers('x') x = 'a'`) twice.
|
|
722
|
+
*/
|
|
723
|
+
function parameterDecorators(param: ParameterInfo): DecoratorInfo[] {
|
|
724
|
+
const seen = new Set<number>()
|
|
725
|
+
return param.decorators.filter(decorator => {
|
|
726
|
+
if (!isParameterDecorator(decorator) || seen.has(decorator.location.start)) return false
|
|
727
|
+
seen.add(decorator.location.start)
|
|
728
|
+
return true
|
|
729
|
+
})
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
function isParameterDecorator(decorator: DecoratorInfo): boolean {
|
|
733
|
+
return decorator.identity !== undefined && PARAMETER_DECORATORS.has(decorator.identity)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/** A reference to a class declaration (`@Message(OrderDto)`). */
|
|
737
|
+
function isClassReference(ref: ValueRef): boolean {
|
|
738
|
+
const symbol = ref.symbol
|
|
739
|
+
return symbol.kind === 'declaration' && symbol.declaration.kind === 'class'
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Primitive and binary payload types need no schema (`unknown` is honest, `any` is not). */
|
|
743
|
+
function isSafePayloadType(type: TypeInfo): boolean {
|
|
744
|
+
switch (type.kind) {
|
|
745
|
+
case 'primitive':
|
|
746
|
+
return type.name !== 'any' && type.name !== 'object'
|
|
747
|
+
case 'literal':
|
|
748
|
+
return true
|
|
749
|
+
case 'union':
|
|
750
|
+
return type.types.every(isSafePayloadType)
|
|
751
|
+
case 'reference':
|
|
752
|
+
return BINARY_TYPES.has(type.identity)
|
|
753
|
+
default:
|
|
754
|
+
return false
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Whether `undefined` is assignable to the type (unknown types are given the benefit of the doubt). */
|
|
759
|
+
function acceptsUndefined(type: TypeInfo): boolean {
|
|
760
|
+
switch (type.kind) {
|
|
761
|
+
case 'primitive':
|
|
762
|
+
return type.name === 'any' || type.name === 'unknown' || type.name === 'undefined' || type.name === 'void'
|
|
763
|
+
case 'union':
|
|
764
|
+
return type.types.some(acceptsUndefined)
|
|
765
|
+
case 'opaque':
|
|
766
|
+
case 'typeParameter':
|
|
767
|
+
return true
|
|
768
|
+
default:
|
|
769
|
+
return false
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** Valid results of an `@OnError` method: `'ack'`, nothing (`void`, `undefined`), or unknown types. */
|
|
774
|
+
function isErrorHandlerResult(type: TypeInfo): boolean {
|
|
775
|
+
switch (type.kind) {
|
|
776
|
+
case 'primitive':
|
|
777
|
+
return ['void', 'undefined', 'any', 'unknown', 'never'].includes(type.name)
|
|
778
|
+
case 'literal':
|
|
779
|
+
return type.value === 'ack'
|
|
780
|
+
case 'union':
|
|
781
|
+
return type.types.every(isErrorHandlerResult)
|
|
782
|
+
case 'promise':
|
|
783
|
+
return isErrorHandlerResult(type.inner)
|
|
784
|
+
case 'opaque':
|
|
785
|
+
case 'typeParameter':
|
|
786
|
+
return true
|
|
787
|
+
default:
|
|
788
|
+
return false
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/** `@Ctx() message: InboundMessage<Order>` narrows the payload: the invoker casts `m`. */
|
|
793
|
+
function narrowsPayload(type: TypeInfo, types: TypeHelpers): boolean {
|
|
794
|
+
const { type: inner } = types.stripNullish(type)
|
|
795
|
+
if (inner.kind !== 'reference' || inner.identity !== INBOUND_MESSAGE_IDENTITY) return false
|
|
796
|
+
const payload = inner.typeArgs[0]
|
|
797
|
+
return (
|
|
798
|
+
payload !== undefined && !(payload.kind === 'primitive' && (payload.name === 'unknown' || payload.name === 'any'))
|
|
799
|
+
)
|
|
800
|
+
}
|