@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 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
- invoke: (consumer: any, message: InboundMessage) => unknown
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); an optional parameter
588
- without decorator → `undefined` (trailing ones are omitted). The invoker is `async` only when it
589
- awaits a validation, and its `m` parameter is omitted when no argument uses it (`noUnusedParameters`).
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
 
@@ -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
- entries.push(["invoke", invokeNode(ctx, handler, consumerType, helpers)]);
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
- return ctx.arrow([["c", consumerType()], ["m", helpers.inboundMessage()]], (c, m) => ctx.call(ctx.member(c, handler.method), params.map((param) => argumentNode(ctx, param, m, helpers))), { async: params.some((param) => param.kind === "validated") });
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