@qlever-llc/trellis 0.10.10 → 0.10.11

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/src/trellis.ts CHANGED
@@ -732,6 +732,55 @@ export function isResultLike(
732
732
  ): value is Result<unknown, BaseError> {
733
733
  return value instanceof Result;
734
734
  }
735
+
736
+ type SerializableRuntimeError = {
737
+ id?: string;
738
+ type: string;
739
+ message: string;
740
+ context?: Record<string, unknown>;
741
+ traceId?: string;
742
+ } & Record<string, unknown>;
743
+
744
+ export type HandlerErrorAnnotationContext = {
745
+ method?: string;
746
+ event?: string;
747
+ feed?: string;
748
+ operation?: string;
749
+ jobType?: string;
750
+ requestId?: string;
751
+ service?: string;
752
+ contractId?: string;
753
+ contractDigest?: string;
754
+ traceId?: string;
755
+ };
756
+
757
+ function compactHandlerErrorContext(
758
+ context: HandlerErrorAnnotationContext,
759
+ ): Record<string, unknown> {
760
+ return Object.fromEntries(
761
+ Object.entries(context).filter(([key, value]) =>
762
+ key !== "traceId" && value !== undefined
763
+ ),
764
+ );
765
+ }
766
+
767
+ function sanitizeHandlerErrorContext(error: BaseError): void {
768
+ delete error.getContext().subject;
769
+ }
770
+
771
+ export function annotateHandlerBoundaryError(
772
+ cause: unknown,
773
+ context: HandlerErrorAnnotationContext,
774
+ ): BaseError {
775
+ const error = cause instanceof BaseError && !(cause instanceof RemoteError)
776
+ ? cause
777
+ : new UnexpectedError({ cause });
778
+ sanitizeHandlerErrorContext(error);
779
+ error.withContext(compactHandlerErrorContext(context));
780
+ error.withTraceId(context.traceId);
781
+ return error;
782
+ }
783
+
735
784
  export type RuntimeOperationDesc = {
736
785
  subject: string;
737
786
  input: unknown;
@@ -788,10 +837,7 @@ export type RuntimeOperationSnapshot = {
788
837
  progress?: unknown;
789
838
  transfer?: RuntimeOperationTransferProgress;
790
839
  output?: unknown;
791
- error?: {
792
- type: string;
793
- message: string;
794
- };
840
+ error?: SerializableRuntimeError;
795
841
  };
796
842
 
797
843
  export type RuntimeOperationRecord = {
@@ -848,8 +894,11 @@ const DurableOperationSnapshotSchema = Type.Object({
848
894
  })),
849
895
  output: Type.Optional(Type.Any()),
850
896
  error: Type.Optional(Type.Object({
897
+ id: Type.Optional(Type.String()),
851
898
  type: Type.String(),
852
899
  message: Type.String(),
900
+ context: Type.Optional(Type.Record(Type.String(), Type.Unknown())),
901
+ traceId: Type.Optional(Type.String()),
853
902
  })),
854
903
  });
855
904
 
@@ -990,6 +1039,8 @@ export type TrellisOpts<TA extends AnyTrellisAPI> = {
990
1039
  state?: RuntimeStateStores;
991
1040
  connection?: TrellisConnection;
992
1041
  onSessionNotFound?: () => MaybePromise<void>;
1042
+ contractId?: string;
1043
+ contractDigest?: string;
993
1044
  };
994
1045
 
995
1046
  export type RequestOpts = {
@@ -1960,6 +2011,8 @@ export class Trellis<
1960
2011
  readonly handle: { readonly rpc: ActiveRpcHandleFacade<TA, TRequests> };
1961
2012
  /** Framework-neutral lifecycle handle for this Trellis runtime connection. */
1962
2013
  readonly connection: TrellisConnection;
2014
+ readonly contractId?: string;
2015
+ readonly contractDigest?: string;
1963
2016
 
1964
2017
  protected nats: NatsConnection;
1965
2018
  protected js: JetStreamClient;
@@ -1991,6 +2044,8 @@ export class Trellis<
1991
2044
  this.#log = (opts?.log ?? logger).child({ lib: "trellis" });
1992
2045
  this.timeout = opts?.timeout ?? 3000;
1993
2046
  this.stream = opts?.stream ?? "trellis";
2047
+ this.contractId = opts?.contractId;
2048
+ this.contractDigest = opts?.contractDigest;
1994
2049
  this.#hasExplicitApi = api !== undefined;
1995
2050
  this.#noResponderMaxRetries = opts?.noResponderRetry?.maxAttempts ??
1996
2051
  DEFAULT_NO_RESPONDER_MAX_RETRIES;
@@ -2860,9 +2915,14 @@ export class Trellis<
2860
2915
  this.#respondWithError(msg, value.error);
2861
2916
  }
2862
2917
  } catch (cause) {
2863
- const error = cause instanceof BaseError
2864
- ? cause
2865
- : new UnexpectedError({ cause });
2918
+ const error = annotateHandlerBoundaryError(cause, {
2919
+ feed,
2920
+ requestId: msg.headers?.get("request-id"),
2921
+ service: this.name,
2922
+ contractId: this.contractId,
2923
+ contractDigest: this.contractDigest,
2924
+ traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
2925
+ });
2866
2926
  this.#respondWithError(msg, error);
2867
2927
  }
2868
2928
  })();
@@ -2907,7 +2967,7 @@ export class Trellis<
2907
2967
 
2908
2968
  const controller = new AbortController();
2909
2969
  try {
2910
- await handler({
2970
+ const handlerResult = await handler({
2911
2971
  input: parsed as TInput,
2912
2972
  caller: callerValue,
2913
2973
  signal: controller.signal,
@@ -2927,6 +2987,19 @@ export class Trellis<
2927
2987
  return ok(undefined);
2928
2988
  })()),
2929
2989
  });
2990
+ const handlerOutcome = isResultLike(handlerResult)
2991
+ ? handlerResult.take()
2992
+ : handlerResult;
2993
+ if (isErr(handlerOutcome)) {
2994
+ return err(annotateHandlerBoundaryError(handlerOutcome.error, {
2995
+ feed,
2996
+ requestId: msg.headers?.get("request-id"),
2997
+ service: this.name,
2998
+ contractId: this.contractId,
2999
+ contractDigest: this.contractDigest,
3000
+ traceId: traceIdFromTraceparent(msg.headers?.get("traceparent")),
3001
+ }));
3002
+ }
2930
3003
  return ok(undefined);
2931
3004
  } finally {
2932
3005
  controller.abort();
@@ -3350,7 +3423,17 @@ export class Trellis<
3350
3423
  );
3351
3424
 
3352
3425
  if (handlerResultWrapped.isErr()) {
3353
- const error = handlerResultWrapped.error.withContext({ method });
3426
+ const error = annotateHandlerBoundaryError(
3427
+ handlerResultWrapped.error,
3428
+ {
3429
+ method: String(method),
3430
+ requestId: msg.headers?.get("request-id"),
3431
+ service: this.name,
3432
+ contractId: this.contractId,
3433
+ contractDigest: this.contractDigest,
3434
+ traceId: activeTraceId(span) ?? incomingTraceId,
3435
+ },
3436
+ );
3354
3437
  this.#log.error(
3355
3438
  {
3356
3439
  method,
@@ -3374,12 +3457,14 @@ export class Trellis<
3374
3457
  };
3375
3458
  const handlerOutcome = handlerResult.take();
3376
3459
  if (isErr(handlerOutcome)) {
3377
- const handlerError = handlerOutcome.error;
3378
-
3379
- const error = handlerError instanceof BaseError &&
3380
- !(handlerError instanceof RemoteError)
3381
- ? handlerError
3382
- : new UnexpectedError({ cause: handlerError });
3460
+ const error = annotateHandlerBoundaryError(handlerOutcome.error, {
3461
+ method: String(method),
3462
+ requestId: msg.headers?.get("request-id"),
3463
+ service: this.name,
3464
+ contractId: this.contractId,
3465
+ contractDigest: this.contractDigest,
3466
+ traceId: activeTraceId(span) ?? incomingTraceId,
3467
+ });
3383
3468
 
3384
3469
  this.#log.error(
3385
3470
  {
@@ -3713,21 +3798,18 @@ export class Trellis<
3713
3798
  continue;
3714
3799
  }
3715
3800
 
3716
- const handlerResult = await AsyncResult.lift(
3717
- fn(
3718
- m as EventOf<TA, EventsOf<TA>>,
3719
- createEventListenerContext({
3720
- payload: m,
3721
- subject: msg.subject,
3722
- mode: "ephemeral",
3723
- message: msg,
3724
- }),
3725
- ),
3726
- );
3727
- if (handlerResult.isErr()) {
3801
+ const handlerResult = await this.#invokeEventHandler({
3802
+ event,
3803
+ payload: m,
3804
+ mode: "ephemeral",
3805
+ message: msg,
3806
+ fn,
3807
+ });
3808
+ const handlerValue = handlerResult.take();
3809
+ if (isErr(handlerValue)) {
3728
3810
  this.#log.error(
3729
3811
  {
3730
- error: handlerResult.error.toSerializable(),
3812
+ error: handlerValue.error.toSerializable(),
3731
3813
  event,
3732
3814
  subject: msg.subject,
3733
3815
  },
@@ -3741,6 +3823,42 @@ export class Trellis<
3741
3823
  return ok(undefined);
3742
3824
  }
3743
3825
 
3826
+ async #invokeEventHandler(args: {
3827
+ event: EventsOf<TA>;
3828
+ payload: unknown;
3829
+ mode: "durable" | "ephemeral";
3830
+ group?: string;
3831
+ message: Pick<Msg, "headers" | "subject"> & object;
3832
+ fn: EventCallback<EventOf<TA, EventsOf<TA>>>;
3833
+ }): Promise<Result<void, BaseError>> {
3834
+ const annotation = {
3835
+ event: String(args.event),
3836
+ service: this.name,
3837
+ contractId: this.contractId,
3838
+ contractDigest: this.contractDigest,
3839
+ traceId: traceIdFromTraceparent(args.message.headers?.get("traceparent")),
3840
+ };
3841
+ try {
3842
+ const result = await Promise.resolve(args.fn(
3843
+ args.payload as EventOf<TA, EventsOf<TA>>,
3844
+ createEventListenerContext({
3845
+ payload: args.payload,
3846
+ subject: args.message.subject,
3847
+ mode: args.mode,
3848
+ ...(args.group ? { group: args.group } : {}),
3849
+ message: args.message,
3850
+ }),
3851
+ ));
3852
+ const outcome = isResultLike(result) ? result.take() : result;
3853
+ if (isErr(outcome)) {
3854
+ return err(annotateHandlerBoundaryError(outcome.error, annotation));
3855
+ }
3856
+ return ok(undefined);
3857
+ } catch (cause) {
3858
+ return err(annotateHandlerBoundaryError(cause, annotation));
3859
+ }
3860
+ }
3861
+
3744
3862
  #resolveEventConsumerGroup(
3745
3863
  event: EventsOf<TA>,
3746
3864
  opts: EventOpts | undefined,
@@ -3944,21 +4062,18 @@ export class Trellis<
3944
4062
  continue;
3945
4063
  }
3946
4064
 
3947
- const handlerResult = await AsyncResult.lift(
3948
- fn(
3949
- m as EventOf<TA, EventsOf<TA>>,
3950
- createEventListenerContext({
3951
- payload: m,
3952
- subject: msg.subject,
3953
- mode: "durable",
3954
- message: msg,
3955
- }),
3956
- ),
3957
- );
3958
- if (handlerResult.isErr()) {
4065
+ const handlerResult = await this.#invokeEventHandler({
4066
+ event,
4067
+ payload: m,
4068
+ mode: "durable",
4069
+ message: msg,
4070
+ fn,
4071
+ });
4072
+ const handlerValue = handlerResult.take();
4073
+ if (isErr(handlerValue)) {
3959
4074
  this.#log.error(
3960
4075
  {
3961
- error: handlerResult.error.toSerializable(),
4076
+ error: handlerValue.error.toSerializable(),
3962
4077
  event,
3963
4078
  subject: msg.subject,
3964
4079
  },
@@ -4014,22 +4129,19 @@ export class Trellis<
4014
4129
  break;
4015
4130
  }
4016
4131
 
4017
- const handlerResult = await AsyncResult.lift(
4018
- registration.fn(
4019
- eventPayload as EventOf<TA, EventsOf<TA>>,
4020
- createEventListenerContext({
4021
- payload: eventPayload,
4022
- subject: msg.subject,
4023
- mode: "durable",
4024
- group,
4025
- message: msg,
4026
- }),
4027
- ),
4028
- );
4029
- if (handlerResult.isErr()) {
4132
+ const handlerResult = await this.#invokeEventHandler({
4133
+ event: registration.event,
4134
+ payload: eventPayload,
4135
+ mode: "durable",
4136
+ group,
4137
+ message: msg,
4138
+ fn: registration.fn,
4139
+ });
4140
+ const handlerValue = handlerResult.take();
4141
+ if (isErr(handlerValue)) {
4030
4142
  this.#log.error(
4031
4143
  {
4032
- error: handlerResult.error.toSerializable(),
4144
+ error: handlerValue.error.toSerializable(),
4033
4145
  event: registration.event,
4034
4146
  subject: msg.subject,
4035
4147
  },