@temporalio/workflow 1.11.7 → 1.12.0-rc.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/src/interfaces.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import type { RawSourceMap } from 'source-map';
2
2
  import {
3
3
  RetryPolicy,
4
- TemporalFailure,
5
4
  CommonWorkflowOptions,
6
5
  HandlerUnfinishedPolicy,
7
6
  SearchAttributes,
@@ -10,6 +9,11 @@ import {
10
9
  QueryDefinition,
11
10
  Duration,
12
11
  VersioningIntent,
12
+ TypedSearchAttributes,
13
+ SearchAttributePair,
14
+ Priority,
15
+ WorkerDeploymentVersion,
16
+ VersioningBehavior,
13
17
  } from '@temporalio/common';
14
18
  import { SymbolBasedInstanceOfError } from '@temporalio/common/lib/type-helpers';
15
19
  import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow/enums-helpers';
@@ -39,8 +43,16 @@ export interface WorkflowInfo {
39
43
  * Indexed information attached to the Workflow Execution
40
44
  *
41
45
  * This value may change during the lifetime of an Execution.
46
+ * @deprecated Use {@link typedSearchAttributes} instead.
42
47
  */
43
- readonly searchAttributes: SearchAttributes;
48
+ readonly searchAttributes: SearchAttributes; // eslint-disable-line deprecation/deprecation
49
+
50
+ /**
51
+ * Indexed information attached to the Workflow Execution, exposed through an interface.
52
+ *
53
+ * This value may change during the lifetime of an Execution.
54
+ */
55
+ readonly typedSearchAttributes: TypedSearchAttributes;
44
56
 
45
57
  /**
46
58
  * Non-indexed information attached to the Workflow Execution
@@ -52,6 +64,22 @@ export interface WorkflowInfo {
52
64
  */
53
65
  readonly parent?: ParentWorkflowInfo;
54
66
 
67
+ /**
68
+ * The root workflow execution, defined as follows:
69
+ * 1. A workflow without a parent workflow is its own root workflow.
70
+ * 2. A workflow with a parent workflow has the same root workflow as
71
+ * its parent.
72
+ *
73
+ * When there is no parent workflow, i.e., the workflow is its own root workflow,
74
+ * this field is `undefined`.
75
+ *
76
+ * Note that Continue-as-New (or reset) propagates the workflow parentage relationship,
77
+ * and therefore, whether the new workflow has the same root workflow as the original one
78
+ * depends on whether it had a parent.
79
+ *
80
+ */
81
+ readonly root?: RootWorkflowInfo;
82
+
55
83
  /**
56
84
  * Result from the previous Run (present if this is a Cron Workflow or was Continued As New).
57
85
  *
@@ -62,7 +90,7 @@ export interface WorkflowInfo {
62
90
  /**
63
91
  * Failure from the previous Run (present when this Run is a retry, or the last Run of a Cron Workflow failed)
64
92
  */
65
- readonly lastFailure?: TemporalFailure;
93
+ readonly lastFailure?: Error;
66
94
 
67
95
  /**
68
96
  * Length of Workflow history up until the current Workflow Task.
@@ -169,10 +197,28 @@ export interface WorkflowInfo {
169
197
  * task was completed by a worker without a Build ID. If this worker is the one executing this
170
198
  * task for the first time and has a Build ID set, then its ID will be used. This value may change
171
199
  * over the lifetime of the workflow run, but is deterministic and safe to use for branching.
200
+ *
201
+ * @deprecated Use `currentDeploymentVersion` instead
172
202
  */
173
203
  readonly currentBuildId?: string;
174
204
 
205
+ /**
206
+ * The Deployment Version of the worker which executed the current Workflow Task. May be undefined
207
+ * if the task was completed by a worker without a Deployment Version. If this worker is the one
208
+ * executing this task for the first time and has a Deployment Version set, then its ID will be
209
+ * used. This value may change over the lifetime of the workflow run, but is deterministic and
210
+ * safe to use for branching.
211
+ *
212
+ * @experimental Deployment based versioning is experimental and may change in the future.
213
+ */
214
+ readonly currentDeploymentVersion?: WorkerDeploymentVersion;
215
+
175
216
  readonly unsafe: UnsafeWorkflowInfo;
217
+
218
+ /**
219
+ * Priority of this workflow
220
+ */
221
+ readonly priority?: Priority;
176
222
  }
177
223
 
178
224
  /**
@@ -213,6 +259,11 @@ export interface ParentWorkflowInfo {
213
259
  namespace: string;
214
260
  }
215
261
 
262
+ export interface RootWorkflowInfo {
263
+ workflowId: string;
264
+ runId: string;
265
+ }
266
+
216
267
  /**
217
268
  * Not an actual error, used by the Workflow runtime to abort execution when {@link continueAsNew} is called
218
269
  */
@@ -251,15 +302,27 @@ export interface ContinueAsNewOptions {
251
302
  memo?: Record<string, unknown>;
252
303
  /**
253
304
  * Searchable attributes to attach to next Workflow run
305
+ * @deprecated Use {@link typedSearchAttributes} instead.
306
+ */
307
+ searchAttributes?: SearchAttributes; // eslint-disable-line deprecation/deprecation
308
+ /**
309
+ * Specifies additional indexed information to attach to the Workflow Execution. More info:
310
+ * https://docs.temporal.io/docs/typescript/search-attributes
311
+ *
312
+ * Values are always converted using {@link JsonPayloadConverter}, even when a custom data converter is provided.
313
+ * Note that search attributes are not encoded, as such, do not include any sensitive information.
314
+ *
315
+ * If both {@link searchAttributes} and {@link typedSearchAttributes} are provided, conflicting keys will be overwritten
316
+ * by {@link typedSearchAttributes}.
254
317
  */
255
- searchAttributes?: SearchAttributes;
318
+ typedSearchAttributes?: SearchAttributePair[] | TypedSearchAttributes;
256
319
  /**
257
320
  * When using the Worker Versioning feature, specifies whether this Workflow should
258
321
  * Continue-as-New onto a worker with a compatible Build Id or not. See {@link VersioningIntent}.
259
322
  *
260
323
  * @default 'COMPATIBLE'
261
324
  *
262
- * @experimental
325
+ * @experimental The Worker Versioning API is still being designed. Major changes are expected.
263
326
  */
264
327
  versioningIntent?: VersioningIntent;
265
328
  }
@@ -390,7 +453,7 @@ export const [encodeParentClosePolicy, decodeParentClosePolicy] = makeProtoEnumC
390
453
  'PARENT_CLOSE_POLICY_'
391
454
  );
392
455
 
393
- export interface ChildWorkflowOptions extends CommonWorkflowOptions {
456
+ export interface ChildWorkflowOptions extends Omit<CommonWorkflowOptions, 'workflowIdConflictPolicy'> {
394
457
  /**
395
458
  * Workflow id to use when starting. If not specified a UUID is generated. Note that it is
396
459
  * dangerous as in case of client side retries no deduplication will happen based on the
@@ -429,7 +492,7 @@ export interface ChildWorkflowOptions extends CommonWorkflowOptions {
429
492
  *
430
493
  * @default 'COMPATIBLE'
431
494
  *
432
- * @experimental
495
+ * @experimental The Worker Versioning API is still being designed. Major changes are expected.
433
496
  */
434
497
  versioningIntent?: VersioningIntent;
435
498
  }
@@ -538,6 +601,16 @@ export type Handler<
538
601
  */
539
602
  export type DefaultSignalHandler = (signalName: string, ...args: unknown[]) => void | Promise<void>;
540
603
 
604
+ /**
605
+ * A handler function accepting update calls for non-registered update names.
606
+ */
607
+ export type DefaultUpdateHandler = (updateName: string, ...args: unknown[]) => Promise<unknown> | unknown;
608
+
609
+ /**
610
+ * A handler function accepting query calls for non-registered query names.
611
+ */
612
+ export type DefaultQueryHandler = (queryName: string, ...args: unknown[]) => unknown;
613
+
541
614
  /**
542
615
  * A validation function capable of accepting the arguments for a given UpdateDefinition.
543
616
  */
@@ -565,4 +638,5 @@ export type UpdateHandlerOptions<Args extends any[]> = {
565
638
  export interface ActivationCompletion {
566
639
  commands: coresdk.workflow_commands.IWorkflowCommand[];
567
640
  usedInternalFlags: number[];
641
+ versioningBehavior?: VersioningBehavior;
568
642
  }
package/src/internals.ts CHANGED
@@ -19,10 +19,15 @@ import {
19
19
  WorkflowUpdateType,
20
20
  WorkflowUpdateValidatorType,
21
21
  mapFromPayloads,
22
- searchAttributePayloadConverter,
23
22
  fromPayloadsAtIndex,
24
- SearchAttributes,
23
+ WorkflowFunctionWithOptions,
24
+ VersioningBehavior,
25
+ WorkflowDefinitionOptions,
25
26
  } from '@temporalio/common';
27
+ import {
28
+ decodeSearchAttributes,
29
+ decodeTypedSearchAttributes,
30
+ } from '@temporalio/common/lib/converter/payload-search-attributes';
26
31
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
27
32
  import { makeProtoEnumConverters } from '@temporalio/common/lib/internal-workflow';
28
33
  import type { coresdk, temporal } from '@temporalio/proto';
@@ -41,6 +46,8 @@ import {
41
46
  WorkflowInfo,
42
47
  WorkflowCreateOptionsInternal,
43
48
  ActivationCompletion,
49
+ DefaultUpdateHandler,
50
+ DefaultQueryHandler,
44
51
  } from './interfaces';
45
52
  import { type SinkCall } from './sinks';
46
53
  import { untrackPromise } from './stack-helpers';
@@ -189,6 +196,16 @@ export class Activator implements ActivationHandler {
189
196
  */
190
197
  defaultSignalHandler?: DefaultSignalHandler;
191
198
 
199
+ /**
200
+ * A update handler that catches calls for non-registered update names.
201
+ */
202
+ defaultUpdateHandler?: DefaultUpdateHandler;
203
+
204
+ /**
205
+ * A query handler that catches calls for non-registered query names.
206
+ */
207
+ defaultQueryHandler?: DefaultQueryHandler;
208
+
192
209
  /**
193
210
  * Source map file for looking up the source files in response to __enhanced_stack_trace
194
211
  */
@@ -204,6 +221,38 @@ export class Activator implements ActivationHandler {
204
221
  childToParent: new Map(),
205
222
  };
206
223
 
224
+ /**
225
+ * The error that caused the current Workflow Task to fail. Sets if a non-`TemporalFailure`
226
+ * error bubbles up out of the Workflow function, or out of a Signal or Update handler. We
227
+ * capture errors this way because those functions are not technically awaited when started,
228
+ * but left to run asynchronously. There is therefore no real "parent" function that can
229
+ * directly handle those errors, and not capturing it would result in an Unhandled Promise
230
+ * Rejection. So instead, we buffer the error here, to then be processed in the context
231
+ * of our own synchronous Activation handling event loop.
232
+ *
233
+ * Our code does a best effort to stop processing the current activation as soon as possible
234
+ * after this field is set:
235
+ * - If an error is thrown while executing code synchronously (e.g. anything before the
236
+ * first `await` statement in a Workflow function or a signal/update handler), the error
237
+ * will be _immediately_ rethrown, which will prevent execution of further jobs in the
238
+ * current activation. We know we're currently running code synchronously thanks to the
239
+ * `rethrowSynchronously` flag below.
240
+ * - It an error is thrown while executing microtasks, then the error will be rethrown on
241
+ * the next call to `tryUnblockConditions()`.
242
+ *
243
+ * Unfortunately, there's no way for us to prevent further execution of microtasks that have
244
+ * already been scheduled, nor those that will be recursively scheduled from those microtasks.
245
+ * Should more errors get thrown while settling microtasks, those will be ignored (i.e. only
246
+ * the first captured error is preserved).
247
+ */
248
+ public workflowTaskError: unknown;
249
+
250
+ /**
251
+ * Set to true when running synchronous code (e.g. while processing activation jobs and when calling
252
+ * `tryUnblockConditions()`). While this flag is set, it is safe to let errors bubble up.
253
+ */
254
+ public rethrowSynchronously = false;
255
+
207
256
  public readonly rootScope = new RootCancellationScope();
208
257
 
209
258
  /**
@@ -336,7 +385,7 @@ export class Activator implements ActivationHandler {
336
385
  /**
337
386
  * Reference to the current Workflow, initialized when a Workflow is started
338
387
  */
339
- public workflow?: Workflow;
388
+ public workflow?: Workflow | WorkflowFunctionWithOptions<any[], any>;
340
389
 
341
390
  /**
342
391
  * Information about the current Workflow
@@ -370,12 +419,17 @@ export class Activator implements ActivationHandler {
370
419
  sinkCalls = Array<SinkCall>();
371
420
 
372
421
  /**
373
- * A nanosecond resolution time function, externally injected
422
+ * A nanosecond resolution time function, externally injected. This is used to
423
+ * precisely sort logs entries emitted from the Workflow Context vs those emitted
424
+ * from other sources (e.g. main thread, Core, etc).
374
425
  */
375
426
  public readonly getTimeOfDay: () => bigint;
376
427
 
377
428
  public readonly registeredActivityNames: Set<string>;
378
429
 
430
+ public versioningBehavior?: VersioningBehavior;
431
+ public workflowDefinitionOptionsGetter?: () => WorkflowDefinitionOptions;
432
+
379
433
  constructor({
380
434
  info,
381
435
  now,
@@ -448,6 +502,7 @@ export class Activator implements ActivationHandler {
448
502
  return {
449
503
  commands: this.commands.splice(0),
450
504
  usedInternalFlags: [...this.knownFlags],
505
+ versioningBehavior: this.versioningBehavior,
451
506
  };
452
507
  }
453
508
 
@@ -478,8 +533,10 @@ export class Activator implements ActivationHandler {
478
533
  // Most things related to initialization have already been handled in the constructor
479
534
  this.mutateWorkflowInfo((info) => ({
480
535
  ...info,
481
- searchAttributes:
482
- (mapFromPayloads(searchAttributePayloadConverter, searchAttributes?.indexedFields) as SearchAttributes) ?? {},
536
+
537
+ searchAttributes: decodeSearchAttributes(searchAttributes?.indexedFields),
538
+ typedSearchAttributes: decodeTypedSearchAttributes(searchAttributes?.indexedFields),
539
+
483
540
  memo: mapFromPayloads(this.payloadConverter, memo?.fields),
484
541
  lastResult: fromPayloadsAtIndex(this.payloadConverter, 0, lastCompletionResult?.payloads),
485
542
  lastFailure:
@@ -487,6 +544,9 @@ export class Activator implements ActivationHandler {
487
544
  ? this.failureConverter.failureToError(continuedFailure, this.payloadConverter)
488
545
  : undefined,
489
546
  }));
547
+ if (this.workflowDefinitionOptionsGetter) {
548
+ this.versioningBehavior = this.workflowDefinitionOptionsGetter().versioningBehavior;
549
+ }
490
550
  }
491
551
 
492
552
  public cancelWorkflow(_activation: coresdk.workflow_activation.ICancelWorkflow): void {
@@ -577,9 +637,21 @@ export class Activator implements ActivationHandler {
577
637
  }
578
638
  }
579
639
 
640
+ public resolveNexusOperationStart(_: coresdk.workflow_activation.IResolveNexusOperationStart): void {
641
+ throw new Error('TODO');
642
+ }
643
+
644
+ public resolveNexusOperation(_: coresdk.workflow_activation.IResolveNexusOperation): void {
645
+ throw new Error('TODO');
646
+ }
647
+
580
648
  // Intentionally non-async function so this handler doesn't show up in the stack trace
581
649
  protected queryWorkflowNextHandler({ queryName, args }: QueryInput): Promise<unknown> {
582
- const fn = this.queryHandlers.get(queryName)?.handler;
650
+ let fn = this.queryHandlers.get(queryName)?.handler;
651
+ if (fn === undefined && this.defaultQueryHandler !== undefined) {
652
+ fn = this.defaultQueryHandler.bind(undefined, queryName);
653
+ }
654
+ // No handler or default registered, fail.
583
655
  if (fn === undefined) {
584
656
  const knownQueryTypes = [...this.queryHandlers.keys()].join(' ');
585
657
  // Fail the query
@@ -589,6 +661,7 @@ export class Activator implements ActivationHandler {
589
661
  )
590
662
  );
591
663
  }
664
+ // Execute handler.
592
665
  try {
593
666
  const ret = fn(...args);
594
667
  if (ret instanceof Promise) {
@@ -633,8 +706,20 @@ export class Activator implements ActivationHandler {
633
706
  if (!protocolInstanceId) {
634
707
  throw new TypeError('Missing activation update protocolInstanceId');
635
708
  }
636
- const entry = this.updateHandlers.get(name);
637
- if (!entry) {
709
+
710
+ const entry =
711
+ this.updateHandlers.get(name) ??
712
+ (this.defaultUpdateHandler
713
+ ? {
714
+ handler: this.defaultUpdateHandler.bind(undefined, name),
715
+ validator: undefined,
716
+ // Default to a warning policy.
717
+ unfinishedPolicy: HandlerUnfinishedPolicy.WARN_AND_ABANDON,
718
+ }
719
+ : null);
720
+
721
+ // If we don't have an entry from either source, buffer and return
722
+ if (entry === null) {
638
723
  this.bufferedUpdates.push(activation);
639
724
  return;
640
725
  }
@@ -703,7 +788,7 @@ export class Activator implements ActivationHandler {
703
788
  if (error instanceof TemporalFailure) {
704
789
  this.rejectUpdate(protocolInstanceId, error);
705
790
  } else {
706
- throw error;
791
+ this.handleWorkflowFailure(error);
707
792
  }
708
793
  })
709
794
  .finally(() => this.inProgressUpdates.delete(updateId));
@@ -726,13 +811,21 @@ export class Activator implements ActivationHandler {
726
811
  public dispatchBufferedUpdates(): void {
727
812
  const bufferedUpdates = this.bufferedUpdates;
728
813
  while (bufferedUpdates.length) {
729
- const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name as string));
730
- if (foundIndex === -1) {
731
- // No buffered Updates have a handler yet.
732
- break;
814
+ // We have a default update handler, so all updates are dispatchable.
815
+ if (this.defaultUpdateHandler) {
816
+ const update = bufferedUpdates.shift();
817
+ // Logically, this must be defined as we're in the loop.
818
+ // But Typescript doesn't know that so we use a non-null assertion (!).
819
+ this.doUpdate(update!);
820
+ } else {
821
+ const foundIndex = bufferedUpdates.findIndex((update) => this.updateHandlers.has(update.name as string));
822
+ if (foundIndex === -1) {
823
+ // No buffered Updates have a handler yet.
824
+ break;
825
+ }
826
+ const [update] = bufferedUpdates.splice(foundIndex, 1);
827
+ this.doUpdate(update);
733
828
  }
734
- const [update] = bufferedUpdates.splice(foundIndex, 1);
735
- this.doUpdate(update);
736
829
  }
737
830
  }
738
831
 
@@ -830,6 +923,8 @@ export class Activator implements ActivationHandler {
830
923
  }
831
924
 
832
925
  public warnIfUnfinishedHandlers(): void {
926
+ if (this.workflowTaskError) return;
927
+
833
928
  const getWarnable = (handlerExecutions: Iterable<MessageHandlerExecution>): MessageHandlerExecution[] => {
834
929
  return Array.from(handlerExecutions).filter(
835
930
  (ex) => ex.unfinishedPolicy === HandlerUnfinishedPolicy.WARN_AND_ABANDON
@@ -941,17 +1036,12 @@ export class Activator implements ActivationHandler {
941
1036
  * Transforms failures into a command to be sent to the server.
942
1037
  * Used to handle any failure emitted by the Workflow.
943
1038
  */
944
- async handleWorkflowFailure(error: unknown): Promise<void> {
1039
+ handleWorkflowFailure(error: unknown): void {
945
1040
  if (this.cancelled && isCancellation(error)) {
946
1041
  this.pushCommand({ cancelWorkflowExecution: {} }, true);
947
1042
  } else if (error instanceof ContinueAsNew) {
948
1043
  this.pushCommand({ continueAsNewWorkflowExecution: error.command }, true);
949
- } else {
950
- if (!(error instanceof TemporalFailure)) {
951
- // This results in an unhandled rejection which will fail the activation
952
- // preventing it from completing.
953
- throw error;
954
- }
1044
+ } else if (error instanceof TemporalFailure) {
955
1045
  // Fail the workflow. We do not want to issue unfinishedHandlers warnings. To achieve that, we
956
1046
  // mark all handlers as completed now.
957
1047
  this.inProgressSignals.clear();
@@ -964,9 +1054,29 @@ export class Activator implements ActivationHandler {
964
1054
  },
965
1055
  true
966
1056
  );
1057
+ } else {
1058
+ this.recordWorkflowTaskError(error);
967
1059
  }
968
1060
  }
969
1061
 
1062
+ recordWorkflowTaskError(error: unknown): void {
1063
+ // Only keep the first error that bubbles up; subsequent errors will be ignored.
1064
+ if (this.workflowTaskError === undefined) this.workflowTaskError = error;
1065
+
1066
+ // Immediately rethrow the error if we know it is safe to do so (i.e. we are not running async
1067
+ // microtasks). Otherwise, the error will be rethrown whenever we get an opportunity to do so,
1068
+ // e.g. the next time `tryUnblockConditions()` is called.
1069
+ if (this.rethrowSynchronously) this.maybeRethrowWorkflowTaskError();
1070
+ }
1071
+
1072
+ /**
1073
+ * If a Workflow Task error was captured, and we are running in synchronous mode,
1074
+ * then bubble it up now. This is safe to call even if there is no error to rethrow.
1075
+ */
1076
+ maybeRethrowWorkflowTaskError(): void {
1077
+ if (this.workflowTaskError) throw this.workflowTaskError;
1078
+ }
1079
+
970
1080
  private completeQuery(queryId: string, result: unknown): void {
971
1081
  this.pushCommand({
972
1082
  respondToQuery: { queryId, succeeded: { response: this.payloadConverter.toPayload(result) } },
package/src/logs.ts CHANGED
@@ -5,6 +5,7 @@ import { type Sink, type Sinks, proxySinks } from './sinks';
5
5
  import { isCancellation } from './errors';
6
6
  import { WorkflowInfo, ContinueAsNew } from './interfaces';
7
7
  import { assertInWorkflowContext } from './global-attributes';
8
+ import { currentUpdateInfo, inWorkflowContext } from './workflow';
8
9
 
9
10
  export interface WorkflowLogger extends Sink {
10
11
  trace(message: string, attrs?: Record<string, unknown>): void;
@@ -117,11 +118,20 @@ export function executeWithLifecycleLogging(fn: () => Promise<unknown>): Promise
117
118
  * Note that this function may be called from outside of the Workflow context (eg. by the worker itself).
118
119
  */
119
120
  export function workflowLogAttributes(info: WorkflowInfo): Record<string, unknown> {
120
- return {
121
+ const attributes: { [key: string]: string } = {
121
122
  namespace: info.namespace,
122
123
  taskQueue: info.taskQueue,
123
124
  workflowId: info.workflowId,
124
125
  runId: info.runId,
125
126
  workflowType: info.workflowType,
126
127
  };
128
+ if (inWorkflowContext()) {
129
+ const updateInfo = currentUpdateInfo();
130
+ if (updateInfo) {
131
+ // Add update info if it exists
132
+ attributes['updateId'] = updateInfo.id;
133
+ attributes['updateName'] = updateInfo.name;
134
+ }
135
+ }
136
+ return attributes;
127
137
  }
package/src/metrics.ts ADDED
@@ -0,0 +1,191 @@
1
+ import {
2
+ MetricCounter,
3
+ MetricGauge,
4
+ MetricHistogram,
5
+ MetricMeter,
6
+ MetricMeterWithComposedTags,
7
+ MetricTags,
8
+ NumericMetricValueType,
9
+ } from '@temporalio/common';
10
+ import { composeInterceptors } from '@temporalio/common/lib/interceptors';
11
+ import { proxySinks, Sink, Sinks } from './sinks';
12
+ import { workflowInfo } from './workflow';
13
+ import { assertInWorkflowContext } from './global-attributes';
14
+
15
+ class WorkflowMetricMeterImpl implements MetricMeter {
16
+ constructor() {}
17
+
18
+ createCounter(name: string, unit?: string, description?: string): MetricCounter {
19
+ assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
20
+ return new WorkflowMetricCounter(name, unit, description);
21
+ }
22
+
23
+ createHistogram(
24
+ name: string,
25
+ valueType: NumericMetricValueType = 'int',
26
+ unit?: string,
27
+ description?: string
28
+ ): MetricHistogram {
29
+ assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
30
+ return new WorkflowMetricHistogram(name, valueType, unit, description);
31
+ }
32
+
33
+ createGauge(
34
+ name: string,
35
+ valueType: NumericMetricValueType = 'int',
36
+ unit?: string,
37
+ description?: string
38
+ ): MetricGauge {
39
+ assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
40
+ return new WorkflowMetricGauge(name, valueType, unit, description);
41
+ }
42
+
43
+ withTags(_tags: MetricTags): MetricMeter {
44
+ assertInWorkflowContext("Workflow's `metricMeter` can only be used while in Workflow Context");
45
+ // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
46
+ throw new Error(`withTags is not supported directly on WorkflowMetricMeter`);
47
+ }
48
+ }
49
+
50
+ class WorkflowMetricCounter implements MetricCounter {
51
+ constructor(
52
+ public readonly name: string,
53
+ public readonly unit: string | undefined,
54
+ public readonly description: string | undefined
55
+ ) {}
56
+
57
+ add(value: number, extraTags: MetricTags = {}): void {
58
+ if (value < 0) {
59
+ throw new Error(`MetricCounter value must be non-negative (got ${value})`);
60
+ }
61
+ if (!workflowInfo().unsafe.isReplaying) {
62
+ metricSink.addMetricCounterValue(this.name, this.unit, this.description, value, extraTags);
63
+ }
64
+ }
65
+
66
+ withTags(_tags: MetricTags): MetricCounter {
67
+ // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
68
+ throw new Error(`withTags is not supported directly on WorkflowMetricCounter`);
69
+ }
70
+ }
71
+
72
+ class WorkflowMetricHistogram implements MetricHistogram {
73
+ constructor(
74
+ public readonly name: string,
75
+ public readonly valueType: NumericMetricValueType,
76
+ public readonly unit: string | undefined,
77
+ public readonly description: string | undefined
78
+ ) {}
79
+
80
+ record(value: number, extraTags: MetricTags = {}): void {
81
+ if (value < 0) {
82
+ throw new Error(`MetricHistogram value must be non-negative (got ${value})`);
83
+ }
84
+ if (!workflowInfo().unsafe.isReplaying) {
85
+ metricSink.recordMetricHistogramValue(this.name, this.valueType, this.unit, this.description, value, extraTags);
86
+ }
87
+ }
88
+
89
+ withTags(_tags: MetricTags): MetricHistogram {
90
+ // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
91
+ throw new Error(`withTags is not supported directly on WorkflowMetricHistogram`);
92
+ }
93
+ }
94
+
95
+ class WorkflowMetricGauge implements MetricGauge {
96
+ constructor(
97
+ public readonly name: string,
98
+ public readonly valueType: NumericMetricValueType,
99
+ public readonly unit: string | undefined,
100
+ public readonly description: string | undefined
101
+ ) {}
102
+
103
+ set(value: number, tags?: MetricTags): void {
104
+ if (value < 0) {
105
+ throw new Error(`MetricGauge value must be non-negative (got ${value})`);
106
+ }
107
+ if (!workflowInfo().unsafe.isReplaying) {
108
+ metricSink.setMetricGaugeValue(this.name, this.valueType, this.unit, this.description, value, tags ?? {});
109
+ }
110
+ }
111
+
112
+ withTags(_tags: MetricTags): MetricGauge {
113
+ // Tags composition is handled by a MetricMeterWithComposedTags wrapper over this one
114
+ throw new Error(`withTags is not supported directly on WorkflowMetricGauge`);
115
+ }
116
+ }
117
+
118
+ ////////////////////////////////////////////////////////////////////////////////////////////////////
119
+
120
+ // Note: given that forwarding metrics outside of the sanbox can be quite chatty and add non
121
+ // negligeable overhead, we eagerly check for `isReplaying` and completely skip doing sink
122
+ // calls if we are replaying.
123
+ const metricSink = proxySinks<MetricSinks>().__temporal_metrics;
124
+
125
+ /**
126
+ * Sink interface for forwarding metrics from the Workflow sandbox to the Worker.
127
+ *
128
+ * These sink functions are not intended to be called directly from workflow code; instead,
129
+ * developers should use the `metricMeter` object exposed to workflow code by the SDK, which
130
+ * provides an API that is easier to work with.
131
+ *
132
+ * This sink interface is also not meant to be implemented by user.
133
+ *
134
+ * @hidden
135
+ * @internal Users should not implement this interface, nor use it directly. Use `metricMeter` instead.
136
+ */
137
+ export interface MetricSinks extends Sinks {
138
+ __temporal_metrics: WorkflowMetricMeter;
139
+ }
140
+
141
+ /**
142
+ * @hidden
143
+ * @internal Users should not implement this interface, nor use it directly. Use `metricMeter` instead.
144
+ */
145
+ export interface WorkflowMetricMeter extends Sink {
146
+ addMetricCounterValue(
147
+ metricName: string,
148
+ unit: string | undefined,
149
+ description: string | undefined,
150
+ value: number,
151
+ attrs: MetricTags
152
+ ): void;
153
+
154
+ recordMetricHistogramValue(
155
+ metricName: string,
156
+ valueType: NumericMetricValueType,
157
+ unit: string | undefined,
158
+ description: string | undefined,
159
+ value: number,
160
+ attrs: MetricTags
161
+ ): void;
162
+
163
+ setMetricGaugeValue(
164
+ metricName: string,
165
+ valueType: NumericMetricValueType,
166
+ unit: string | undefined,
167
+ description: string | undefined,
168
+ value: number,
169
+ attrs: MetricTags
170
+ ): void;
171
+ }
172
+
173
+ /**
174
+ * A MetricMeter that can be used to emit metrics from within a Workflow.
175
+ *
176
+ * @experimental The Metric API is an experimental feature and may be subject to change.
177
+ */
178
+ export const metricMeter: MetricMeter = MetricMeterWithComposedTags.compose(
179
+ new WorkflowMetricMeterImpl(),
180
+ () => {
181
+ const activator = assertInWorkflowContext('Workflow.metricMeter may only be used from workflow context.');
182
+ const getMetricTags = composeInterceptors(activator.interceptors.outbound, 'getMetricTags', (a) => a);
183
+
184
+ const info = activator.info;
185
+ return getMetricTags({
186
+ // namespace and taskQueue will be added by the Worker
187
+ workflowType: info.workflowType,
188
+ });
189
+ },
190
+ true
191
+ );