@znight/messaging 0.3.0 → 0.5.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/README.md
CHANGED
|
@@ -112,6 +112,21 @@ Handler selection: exact type, then patterns (declaration order), then the defau
|
|
|
112
112
|
message no handler matches is dead-lettered with reason `no-handler` (or acknowledged by `@OnError`).
|
|
113
113
|
A queue is consumed by a single `@Consumer` class per application.
|
|
114
114
|
|
|
115
|
+
Handler parameters can also use a custom parameter decorator defined with `createParamDecorator`
|
|
116
|
+
(`@znight/core`, RFC 0010) when it has a `messaging` resolver, which receives the `InboundMessage`:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { createParamDecorator } from '@znight/core'
|
|
120
|
+
|
|
121
|
+
export const Tenant = createParamDecorator<string>({
|
|
122
|
+
messaging: message => message.headers['x-tenant'] ?? 'default',
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// @OnMessage('order.placed') placed(@Message(Order) order: Order, @Tenant() tenant: string) { ... }
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
A resolver error fails the delivery like a handler error (retries, `@OnError`, dead letters).
|
|
129
|
+
|
|
115
130
|
## Delivery semantics
|
|
116
131
|
|
|
117
132
|
Delivery is **at-least-once**. A message can be handled more than once: redelivery after a crash, a
|
|
@@ -566,7 +581,8 @@ interface MessagingMetadata {
|
|
|
566
581
|
method: string
|
|
567
582
|
type?: string // @OnMessage(type)
|
|
568
583
|
retry?: { attempts: number; backoff: 'fixed' | 'exponential'; delay: number; maxDelay?: number } // @Retry, defaults filled
|
|
569
|
-
|
|
584
|
+
paramResolvers?: Token[] // class resolvers of custom parameters (RFC 0010), passed to invoke as `x`
|
|
585
|
+
invoke: (consumer: any, message: InboundMessage, x?: readonly object[]) => unknown
|
|
570
586
|
}[]
|
|
571
587
|
errorHandler?: string // @OnError method name, called as consumer[errorHandler](error, message)
|
|
572
588
|
}[]
|
|
@@ -584,9 +600,14 @@ Helpers generated code may call (exported from `@znight/messaging`, stable):
|
|
|
584
600
|
Parameter extraction in `invoke`: `@Message()` → `m.payload as never` (no schema, ZNIGHT017 when the
|
|
585
601
|
type is not primitive); `@Message(S)` → `await validateMessage(S, m.payload)` (the invoker is then
|
|
586
602
|
`async`); `@Headers()` → `m.headers`; `@Headers('x')` → `messageHeader(m, 'x')`; `@Ctx()` → `m`
|
|
587
|
-
(`m as InboundMessage<never>` when the parameter narrows the payload type);
|
|
588
|
-
|
|
589
|
-
|
|
603
|
+
(`m as InboundMessage<never>` when the parameter narrows the payload type); a custom parameter
|
|
604
|
+
decorator (`createParamDecorator`, RFC 0010) → `await (D.resolvers.messaging as ParamFunctionOf<typeof
|
|
605
|
+
D, 'messaging'>)(m, data)` or `await x[i].resolve(m, data)` for a provider class (`x`: the
|
|
606
|
+
instances of `paramResolvers`, resolved in the delivery scope); an optional parameter without
|
|
607
|
+
decorator → `undefined` (trailing ones are omitted). The invoker is `async` only when it awaits a
|
|
608
|
+
validation or a custom parameter, and its `m` parameter is omitted when the handler has no
|
|
609
|
+
argument, `_m` when no argument reads it (only class resolvers whose `resolve()` takes no
|
|
610
|
+
parameter), so that it type-checks with `noUnusedParameters`.
|
|
590
611
|
Consumers are sorted by token, handlers keep their declaration order (inherited handlers first), and
|
|
591
612
|
`options` / `errorHandler` / `type` / `retry` are omitted when absent. Example:
|
|
592
613
|
|
package/dist/compiler.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Token } from "@znight/core";
|
|
2
|
-
import { CompilerExtension, Location, StaticValue, ValueRef } from "@znight/compiler";
|
|
2
|
+
import { ClassRef, CompilerExtension, Location, StaticValue, ValueRef } from "@znight/compiler";
|
|
3
3
|
/** A normalized `@Retry` policy (`MessageHandlerDefinition.retry`). */
|
|
4
4
|
interface RetryPolicyData {
|
|
5
5
|
readonly attempts: number;
|
|
@@ -23,6 +23,24 @@ type HandlerParamData = {
|
|
|
23
23
|
readonly narrow: boolean;
|
|
24
24
|
} | {
|
|
25
25
|
readonly kind: "undefined";
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "custom";
|
|
28
|
+
/** Canonical identity of the decorator `const` (imported by the generated module). */
|
|
29
|
+
readonly identity: string;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
/** The data argument (a value, possibly holding references, or a reference); `undefined` without argument. */
|
|
32
|
+
readonly data: {
|
|
33
|
+
readonly value: StaticValue;
|
|
34
|
+
} | undefined;
|
|
35
|
+
/** The class resolver; `undefined` for a function resolver. */
|
|
36
|
+
readonly resolver: ClassRef | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* Parameters of the class resolver's `resolve` (own, inherited or an arrow function property);
|
|
39
|
+
* `undefined` for a function, or when the compiler cannot read it (the call is cast).
|
|
40
|
+
*/
|
|
41
|
+
readonly arity?: number;
|
|
42
|
+
/** The value type could not be checked against the parameter. */
|
|
43
|
+
readonly cast: boolean;
|
|
26
44
|
};
|
|
27
45
|
/** One `@OnMessage` decorator of a method. */
|
|
28
46
|
interface HandlerData {
|
package/dist/compiler.mjs
CHANGED
|
@@ -526,7 +526,7 @@ function analyzeMethod(cls, method, ctx) {
|
|
|
526
526
|
let handlers = [];
|
|
527
527
|
if (onMessage.length > 0) {
|
|
528
528
|
const policy = retry[0] === void 0 ? void 0 : normalizeRetryDecorator(retry[0], where, ctx);
|
|
529
|
-
const params = analyzeHandlerParams(method, where, ctx);
|
|
529
|
+
const params = analyzeHandlerParams(cls, method, where, ctx);
|
|
530
530
|
handlers = onMessage.flatMap((decorator) => {
|
|
531
531
|
const type = messageType(decorator, where, ctx);
|
|
532
532
|
if (type === false) return [];
|
|
@@ -631,11 +631,32 @@ function checkErrorHandler(method, decorator, where, ctx) {
|
|
|
631
631
|
hints: ["Declare the return type `'ack' | undefined` (or `void`)"]
|
|
632
632
|
});
|
|
633
633
|
}
|
|
634
|
-
function analyzeHandlerParams(method, where, ctx) {
|
|
635
|
-
return Object.freeze(method.params.map((param) => analyzeHandlerParam(param, where, ctx)));
|
|
634
|
+
function analyzeHandlerParams(cls, method, where, ctx) {
|
|
635
|
+
return Object.freeze(method.params.map((param) => analyzeHandlerParam(cls, param, where, ctx)));
|
|
636
636
|
}
|
|
637
|
-
function analyzeHandlerParam(param, where, ctx) {
|
|
637
|
+
function analyzeHandlerParam(cls, param, where, ctx) {
|
|
638
638
|
const decorators = parameterDecorators(param);
|
|
639
|
+
const custom = typeof ctx.paramDecorator === "function" ? ctx.paramDecorator(param, "messaging", cls) : void 0;
|
|
640
|
+
if (custom !== void 0) {
|
|
641
|
+
for (const other of decorators) ctx.report({
|
|
642
|
+
code: INVALID_DECORATOR,
|
|
643
|
+
severity: "error",
|
|
644
|
+
message: `Parameter '${param.name}' of ${where} has several parameter decorators (@${custom.decorator.name} and @${other.name})`,
|
|
645
|
+
location: other.location,
|
|
646
|
+
hints: ["Keep a single parameter decorator"]
|
|
647
|
+
});
|
|
648
|
+
if (!custom.ok || decorators.length > 0) return { kind: "undefined" };
|
|
649
|
+
const data = custom.data;
|
|
650
|
+
return {
|
|
651
|
+
kind: "custom",
|
|
652
|
+
identity: custom.identity,
|
|
653
|
+
name: custom.name,
|
|
654
|
+
data: data === void 0 ? void 0 : { value: data.ref ?? data.value },
|
|
655
|
+
resolver: custom.resolver.kind === "class" ? custom.resolver.ref : void 0,
|
|
656
|
+
...custom.resolver.kind === "class" && custom.resolver.arity !== void 0 ? { arity: custom.resolver.arity } : {},
|
|
657
|
+
cast: custom.cast
|
|
658
|
+
};
|
|
659
|
+
}
|
|
639
660
|
const decorator = decorators[0];
|
|
640
661
|
if (decorator === void 0) {
|
|
641
662
|
const foreign = foreignParameterDecorator(param);
|
|
@@ -896,21 +917,45 @@ function handlerNode(ctx, handler, consumerType, helpers) {
|
|
|
896
917
|
const entries = [["method", handler.method]];
|
|
897
918
|
if (handler.type !== void 0) entries.push(["type", handler.type]);
|
|
898
919
|
if (handler.retry !== void 0) entries.push(["retry", ctx.lit({ ...handler.retry })]);
|
|
899
|
-
|
|
920
|
+
const resolvers = paramResolvers(handler);
|
|
921
|
+
if (resolvers.length > 0) entries.push(["paramResolvers", ctx.lit(resolvers.map((ref) => ctx.tokenOf(ref) ?? ref.token))]);
|
|
922
|
+
entries.push(["invoke", invokeNode(ctx, handler, consumerType, helpers, resolvers)]);
|
|
900
923
|
return ctx.obj(entries);
|
|
901
924
|
}
|
|
925
|
+
/** The provider classes resolving custom parameters, in order of first use (`x[i]`). */
|
|
926
|
+
function paramResolvers(handler) {
|
|
927
|
+
const found = /* @__PURE__ */ new Map();
|
|
928
|
+
for (const param of handler.params) {
|
|
929
|
+
const resolver = param.kind === "custom" ? param.resolver : void 0;
|
|
930
|
+
if (resolver !== void 0 && !found.has(resolver.token)) found.set(resolver.token, resolver);
|
|
931
|
+
}
|
|
932
|
+
return [...found.values()];
|
|
933
|
+
}
|
|
902
934
|
/**
|
|
903
935
|
* `(c: Consumer, m: InboundMessage) => c.method(...args)`, `async` only when it awaits a schema
|
|
904
|
-
* validation. `m` is omitted when no argument uses it (`noUnusedParameters`)
|
|
936
|
+
* validation or a custom parameter. `m` is omitted when no argument uses it (`noUnusedParameters`);
|
|
937
|
+
* `x` (the class resolver instances) is declared only when the handler has class resolvers.
|
|
905
938
|
*/
|
|
906
|
-
function invokeNode(ctx, handler, consumerType, helpers) {
|
|
939
|
+
function invokeNode(ctx, handler, consumerType, helpers, resolvers) {
|
|
907
940
|
const params = [...handler.params];
|
|
908
941
|
while (params.length > 0 && params[params.length - 1]?.kind === "undefined") params.pop();
|
|
909
942
|
if (params.length === 0) return ctx.arrow([["c", consumerType()]], (c) => ctx.call(ctx.member(c, handler.method)));
|
|
910
|
-
|
|
943
|
+
const readsMessage = params.some((param) => param.kind !== "undefined" && (param.kind !== "custom" || param.resolver === void 0 || param.arity !== 0));
|
|
944
|
+
const arrowParams = [["c", consumerType()], [readsMessage ? "m" : "_m", helpers.inboundMessage()]];
|
|
945
|
+
if (resolvers.length > 0) {
|
|
946
|
+
const types = resolvers.map((ref) => ctx.importType(ref));
|
|
947
|
+
const joined = types.slice(1).reduce((all, node) => ctx.code`${all}, ${node}`, types[0]);
|
|
948
|
+
arrowParams.push(["x", ctx.code`readonly [${joined}]`]);
|
|
949
|
+
}
|
|
950
|
+
const index = new Map(resolvers.map((ref, position) => [ref.token, position]));
|
|
951
|
+
return ctx.arrow(arrowParams, (c, m, x) => ctx.call(ctx.member(c, handler.method), params.map((param) => argumentNode(ctx, param, m, helpers, {
|
|
952
|
+
x,
|
|
953
|
+
index
|
|
954
|
+
}))), { async: params.some((param) => param.kind === "validated" || param.kind === "custom") });
|
|
911
955
|
}
|
|
912
|
-
function argumentNode(ctx, param, m, helpers) {
|
|
956
|
+
function argumentNode(ctx, param, m, helpers, resolvers) {
|
|
913
957
|
switch (param.kind) {
|
|
958
|
+
case "custom": return customArgument(ctx, param, m, helpers, resolvers);
|
|
914
959
|
case "payload": return ctx.code`${ctx.member(m, "payload")} as never`;
|
|
915
960
|
case "validated": return ctx.code`await ${ctx.call(helpers.validateMessage(), [ctx.importValue(param.schema), ctx.member(m, "payload")])}`;
|
|
916
961
|
case "headers": return ctx.member(m, "headers");
|
|
@@ -919,6 +964,42 @@ function argumentNode(ctx, param, m, helpers) {
|
|
|
919
964
|
case "undefined": return ctx.lit(void 0);
|
|
920
965
|
}
|
|
921
966
|
}
|
|
967
|
+
/**
|
|
968
|
+
* A custom parameter (RFC 0010), awaited (the invoke is `async`):
|
|
969
|
+
* `await (D.resolvers.messaging as ParamFunctionOf<typeof D, 'messaging'>)(m, data)` or
|
|
970
|
+
* `await x[i].resolve(m, data)`, cast with `as never` when the value type could not be checked.
|
|
971
|
+
*/
|
|
972
|
+
function customArgument(ctx, param, m, helpers, resolvers) {
|
|
973
|
+
const data = param.data === void 0 ? ctx.lit(void 0) : dataNode(ctx, param.data.value);
|
|
974
|
+
let node;
|
|
975
|
+
if (param.resolver === void 0) {
|
|
976
|
+
const decorator = ctx.importValue(param.identity);
|
|
977
|
+
const type = ctx.code`${helpers.paramFunctionOf()}<typeof ${decorator}, 'messaging'>`;
|
|
978
|
+
node = ctx.call(ctx.code`${ctx.member(decorator, "resolvers", "messaging")} as ${type}`, [m, data]);
|
|
979
|
+
} else {
|
|
980
|
+
const position = resolvers.index.get(param.resolver.token);
|
|
981
|
+
if (param.arity === void 0) {
|
|
982
|
+
const instance = ctx.code`${resolvers.x}[${position}] as { resolve(message: unknown, data: unknown): unknown }`;
|
|
983
|
+
return ctx.code`(await (${instance}).resolve(${m}, ${data})) as never`;
|
|
984
|
+
}
|
|
985
|
+
const arity = Math.min(param.arity, 2);
|
|
986
|
+
let args = ctx.code``;
|
|
987
|
+
if (arity === 1) args = m;
|
|
988
|
+
else if (arity === 2) args = ctx.code`${m}, ${data}`;
|
|
989
|
+
node = ctx.code`${resolvers.x}[${position}].resolve(${args})`;
|
|
990
|
+
}
|
|
991
|
+
return param.cast ? ctx.code`(await ${node}) as never` : ctx.code`await ${node}`;
|
|
992
|
+
}
|
|
993
|
+
/** The data argument of a custom parameter: a literal (references imported) or a reference. */
|
|
994
|
+
function dataNode(ctx, value) {
|
|
995
|
+
return isValueRef(value) ? ctx.ref(value) : ctx.lit(value);
|
|
996
|
+
}
|
|
997
|
+
/** `true` for a {@link ValueRef} (the only non-plain object a static value can hold). */
|
|
998
|
+
function isValueRef(value) {
|
|
999
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
1000
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1001
|
+
return prototype !== Object.prototype && prototype !== null && value.kind === "ref";
|
|
1002
|
+
}
|
|
922
1003
|
function lazyHelpers(ctx) {
|
|
923
1004
|
const cache = /* @__PURE__ */ new Map();
|
|
924
1005
|
const get = (name, type) => {
|
|
@@ -932,7 +1013,15 @@ function lazyHelpers(ctx) {
|
|
|
932
1013
|
return {
|
|
933
1014
|
inboundMessage: () => get("InboundMessage", true),
|
|
934
1015
|
validateMessage: () => get("validateMessage", false),
|
|
935
|
-
messageHeader: () => get("messageHeader", false)
|
|
1016
|
+
messageHeader: () => get("messageHeader", false),
|
|
1017
|
+
paramFunctionOf: () => {
|
|
1018
|
+
let node = cache.get("@znight/core#ParamFunctionOf");
|
|
1019
|
+
if (node === void 0) {
|
|
1020
|
+
node = ctx.importFrom("@znight/core", "ParamFunctionOf", { type: true });
|
|
1021
|
+
cache.set("@znight/core#ParamFunctionOf", node);
|
|
1022
|
+
}
|
|
1023
|
+
return node;
|
|
1024
|
+
}
|
|
936
1025
|
};
|
|
937
1026
|
}
|
|
938
1027
|
// src/compiler/validate.ts
|