@temporalio/workflow 1.7.4 → 1.8.1

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.
@@ -10,7 +10,7 @@ import type { coresdk } from '@temporalio/proto';
10
10
  import { disableStorage } from './cancellation-scope';
11
11
  import { DeterminismViolationError } from './errors';
12
12
  import { WorkflowInterceptorsFactory } from './interceptors';
13
- import { WorkflowCreateOptionsWithSourceMap, WorkflowInfo } from './interfaces';
13
+ import { WorkflowCreateOptionsInternal, WorkflowInfo } from './interfaces';
14
14
  import { Activator, getActivator } from './internals';
15
15
  import { SinkCall } from './sinks';
16
16
  import { setActivatorUntyped } from './global-attributes';
@@ -92,7 +92,7 @@ export function overrideGlobals(): void {
92
92
  *
93
93
  * Sets required internal state and instantiates the workflow and interceptors.
94
94
  */
95
- export function initRuntime(options: WorkflowCreateOptionsWithSourceMap): void {
95
+ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
96
96
  const info: WorkflowInfo = fixPrototypes(options.info);
97
97
  info.unsafe.now = OriginalDate.now;
98
98
  const activator = new Activator({ ...options, info });
@@ -137,12 +137,12 @@ export function initRuntime(options: WorkflowCreateOptionsWithSourceMap): void {
137
137
 
138
138
  const mod = importWorkflows();
139
139
  const workflowFn = mod[info.workflowType];
140
- const defaultWorfklowFn = mod['default'];
140
+ const defaultWorkflowFn = mod['default'];
141
141
 
142
142
  if (typeof workflowFn === 'function') {
143
143
  activator.workflow = workflowFn;
144
- } else if (typeof defaultWorfklowFn === 'function') {
145
- activator.workflow = defaultWorfklowFn;
144
+ } else if (typeof defaultWorkflowFn === 'function') {
145
+ activator.workflow = defaultWorkflowFn;
146
146
  } else {
147
147
  const details =
148
148
  workflowFn === undefined
package/src/workflow.ts CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  ActivityFunction,
3
3
  ActivityOptions,
4
4
  compileRetryPolicy,
5
+ extractWorkflowType,
5
6
  IllegalStateError,
6
7
  LocalActivityOptions,
7
8
  mapToPayloads,
@@ -11,13 +12,16 @@ import {
11
12
  SignalDefinition,
12
13
  toPayloads,
13
14
  UntypedActivities,
15
+ VersioningIntent,
14
16
  WithWorkflowArgs,
15
17
  Workflow,
16
18
  WorkflowResultType,
17
19
  WorkflowReturnType,
18
20
  } from '@temporalio/common';
19
- import { msOptionalToTs, msToNumber, msToTs, tsToMs } from '@temporalio/common/lib/time';
21
+ import { assertNever } from '@temporalio/common/lib/type-helpers';
22
+ import { Duration, msOptionalToTs, msToNumber, msToTs, tsToMs } from '@temporalio/common/lib/time';
20
23
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
24
+ import { coresdk } from '@temporalio/proto';
21
25
  import { CancellationScope, registerSleepImplementation } from './cancellation-scope';
22
26
  import {
23
27
  ActivityInput,
@@ -37,8 +41,8 @@ import {
37
41
  Handler,
38
42
  WorkflowInfo,
39
43
  } from './interfaces';
40
- import { LocalActivityDoBackoff, getActivator, maybeGetActivator } from './internals';
41
- import { Sinks } from './sinks';
44
+ import { Activator, LocalActivityDoBackoff, getActivator, maybeGetActivator } from './internals';
45
+ import { LoggerSinks, Sinks } from './sinks';
42
46
  import { untrackPromise } from './stack-helpers';
43
47
  import { ChildWorkflowHandle, ExternalWorkflowHandle } from './workflow-handle';
44
48
 
@@ -107,8 +111,8 @@ function timerNextHandler(input: TimerInput) {
107
111
  * @param ms sleep duration - number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}.
108
112
  * If given a negative number or 0, value will be set to 1.
109
113
  */
110
- export function sleep(ms: number | string): Promise<void> {
111
- const activator = getActivator();
114
+ export function sleep(ms: Duration): Promise<void> {
115
+ const activator = assertInWorkflowContext('Workflow.sleep(...) may only be used from a Workflow Execution');
112
116
  const seq = activator.nextSeqs.timer++;
113
117
 
114
118
  const durationMs = Math.max(1, msToNumber(ms));
@@ -130,11 +134,6 @@ function validateActivityOptions(options: ActivityOptions): void {
130
134
  // Use same validation we use for normal activities
131
135
  const validateLocalActivityOptions = validateActivityOptions;
132
136
 
133
- /**
134
- * Hooks up activity promise to current cancellation scope and completion callbacks.
135
- *
136
- * Returns `false` if the current scope is already cancelled.
137
- */
138
137
  /**
139
138
  * Push a scheduleActivity command into activator accumulator and register completion
140
139
  */
@@ -176,6 +175,7 @@ function scheduleActivityNextHandler({ options, args, headers, seq, activityType
176
175
  headers,
177
176
  cancellationType: options.cancellationType,
178
177
  doNotEagerlyExecute: !(options.allowEagerDispatch ?? true),
178
+ versioningIntent: versioningIntentToProto(options.versioningIntent),
179
179
  },
180
180
  });
181
181
  activator.completions.activity.set(seq, {
@@ -198,6 +198,11 @@ async function scheduleLocalActivityNextHandler({
198
198
  originalScheduleTime,
199
199
  }: LocalActivityInput): Promise<unknown> {
200
200
  const activator = getActivator();
201
+ // Eagerly fail the local activity (which will in turn fail the workflow task.
202
+ // Do not fail on replay where the local activities may not be registered on the replay worker.
203
+ if (!workflowInfo().unsafe.isReplaying && !activator.registeredActivityNames.has(activityType)) {
204
+ throw new ReferenceError(`Local activity of type '${activityType}' not registered on worker`);
205
+ }
201
206
  validateLocalActivityOptions(options);
202
207
 
203
208
  return new Promise((resolve, reject) => {
@@ -250,7 +255,9 @@ async function scheduleLocalActivityNextHandler({
250
255
  * @hidden
251
256
  */
252
257
  export function scheduleActivity<R>(activityType: string, args: any[], options: ActivityOptions): Promise<R> {
253
- const activator = getActivator();
258
+ const activator = assertInWorkflowContext(
259
+ 'Workflow.scheduleActivity(...) may only be used from a Workflow Execution'
260
+ );
254
261
  if (options === undefined) {
255
262
  throw new TypeError('Got empty activity options');
256
263
  }
@@ -275,7 +282,9 @@ export async function scheduleLocalActivity<R>(
275
282
  args: any[],
276
283
  options: LocalActivityOptions
277
284
  ): Promise<R> {
278
- const activator = getActivator();
285
+ const activator = assertInWorkflowContext(
286
+ 'Workflow.scheduleLocalActivity(...) may only be used from a Workflow Execution'
287
+ );
279
288
  if (options === undefined) {
280
289
  throw new TypeError('Got empty activity options');
281
290
  }
@@ -365,6 +374,7 @@ function startChildWorkflowExecutionNextHandler({
365
374
  ? mapToPayloads(searchAttributePayloadConverter, options.searchAttributes)
366
375
  : undefined,
367
376
  memo: options.memo && mapToPayloads(activator.payloadConverter, options.memo),
377
+ versioningIntent: versioningIntentToProto(options.versioningIntent),
368
378
  },
369
379
  });
370
380
  activator.completions.childWorkflowStart.set(seq, {
@@ -504,7 +514,7 @@ export type ActivityInterfaceFor<T> = {
504
514
  * });
505
515
  *
506
516
  * export function execute(): Promise<void> {
507
- * const response = await httpGet('http://example.com');
517
+ * const response = await httpGet("http://example.com");
508
518
  * // ...
509
519
  * }
510
520
  * ```
@@ -574,7 +584,9 @@ const CONDITION_0_PATCH = '__sdk_internal_patch_number:1';
574
584
  * It takes a Workflow ID and optional run ID.
575
585
  */
576
586
  export function getExternalWorkflowHandle(workflowId: string, runId?: string): ExternalWorkflowHandle {
577
- const activator = getActivator();
587
+ const activator = assertInWorkflowContext(
588
+ 'Workflow.getExternalWorkflowHandle(...) may only be used from a Workflow Execution. Consider using Client.workflow.getHandle(...) instead.)'
589
+ );
578
590
  return {
579
591
  workflowId,
580
592
  runId,
@@ -696,9 +708,11 @@ export async function startChild<T extends Workflow>(
696
708
  workflowTypeOrFunc: string | T,
697
709
  options?: WithWorkflowArgs<T, ChildWorkflowOptions>
698
710
  ): Promise<ChildWorkflowHandle<T>> {
699
- const activator = getActivator();
711
+ const activator = assertInWorkflowContext(
712
+ 'Workflow.startChild(...) may only be used from a Workflow Execution. Consider using Client.workflow.start(...) instead.)'
713
+ );
700
714
  const optionsWithDefaults = addDefaultWorkflowOptions(options ?? ({} as any));
701
- const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
715
+ const workflowType = extractWorkflowType(workflowTypeOrFunc);
702
716
  const execute = composeInterceptors(
703
717
  activator.interceptors.outbound,
704
718
  'startChildWorkflowExecution',
@@ -795,9 +809,11 @@ export async function executeChild<T extends Workflow>(
795
809
  workflowTypeOrFunc: string | T,
796
810
  options?: WithWorkflowArgs<T, ChildWorkflowOptions>
797
811
  ): Promise<WorkflowResultType<T>> {
798
- const activator = getActivator();
812
+ const activator = assertInWorkflowContext(
813
+ 'Workflow.executeChild(...) may only be used from a Workflow Execution. Consider using Client.workflow.execute(...) instead.'
814
+ );
799
815
  const optionsWithDefaults = addDefaultWorkflowOptions(options ?? ({} as any));
800
- const workflowType = typeof workflowTypeOrFunc === 'string' ? workflowTypeOrFunc : workflowTypeOrFunc.name;
816
+ const workflowType = extractWorkflowType(workflowTypeOrFunc);
801
817
  const execute = composeInterceptors(
802
818
  activator.interceptors.outbound,
803
819
  'startChildWorkflowExecution',
@@ -841,7 +857,8 @@ export async function executeChild<T extends Workflow>(
841
857
  * }
842
858
  */
843
859
  export function workflowInfo(): WorkflowInfo {
844
- return getActivator().info;
860
+ const activator = assertInWorkflowContext('Workflow.workflowInfo(...) may only be used from a Workflow Execution.');
861
+ return activator.info;
845
862
  }
846
863
 
847
864
  /**
@@ -874,8 +891,8 @@ export function inWorkflowContext(): boolean {
874
891
  * export function myWorkflow() {
875
892
  * return {
876
893
  * async execute() {
877
- * logger.info('hey ho');
878
- * logger.error('lets go');
894
+ * logger.info("hey ho");
895
+ * logger.error("lets go");
879
896
  * }
880
897
  * };
881
898
  * }
@@ -891,11 +908,14 @@ export function proxySinks<T extends Sinks>(): T {
891
908
  {
892
909
  get(_, fnName) {
893
910
  return (...args: any[]) => {
894
- const activator = getActivator();
911
+ const activator = assertInWorkflowContext(
912
+ 'Proxied sinks functions may only be used from a Workflow Execution.'
913
+ );
895
914
  activator.sinkCalls.push({
896
915
  ifaceName: ifaceName as string,
897
916
  fnName: fnName as string,
898
- args,
917
+ // Only available from node 17.
918
+ args: (globalThis as any).structuredClone ? (globalThis as any).structuredClone(args) : args,
899
919
  });
900
920
  };
901
921
  },
@@ -916,8 +936,10 @@ export function proxySinks<T extends Sinks>(): T {
916
936
  export function makeContinueAsNewFunc<F extends Workflow>(
917
937
  options?: ContinueAsNewOptions
918
938
  ): (...args: Parameters<F>) => Promise<never> {
919
- const activator = getActivator();
920
- const info = workflowInfo();
939
+ const activator = assertInWorkflowContext(
940
+ 'Workflow.continueAsNew(...) and Workflow.makeContinueAsNewFunc(...) may only be used from a Workflow Execution.'
941
+ );
942
+ const info = activator.info;
921
943
  const { workflowType, taskQueue, ...rest } = options ?? {};
922
944
  const requiredOptions = {
923
945
  workflowType: workflowType ?? info.workflowType,
@@ -939,6 +961,7 @@ export function makeContinueAsNewFunc<F extends Workflow>(
939
961
  : undefined,
940
962
  workflowRunTimeout: msOptionalToTs(options.workflowRunTimeout),
941
963
  workflowTaskTimeout: msOptionalToTs(options.workflowTaskTimeout),
964
+ versioningIntent: versioningIntentToProto(options.versioningIntent),
942
965
  });
943
966
  });
944
967
  return fn({
@@ -1040,7 +1063,9 @@ export function deprecatePatch(patchId: string): void {
1040
1063
  }
1041
1064
 
1042
1065
  function patchInternal(patchId: string, deprecated: boolean): boolean {
1043
- const activator = getActivator();
1066
+ const activator = assertInWorkflowContext(
1067
+ 'Workflow.patch(...) and Workflow.deprecatePatch may only be used from a Workflow Execution.'
1068
+ );
1044
1069
  // Patch operation does not support interception at the moment, if it did,
1045
1070
  // this would be the place to start the interception chain
1046
1071
 
@@ -1066,14 +1091,15 @@ function patchInternal(patchId: string, deprecated: boolean): boolean {
1066
1091
  *
1067
1092
  * @returns a boolean indicating whether the condition was true before the timeout expires
1068
1093
  */
1069
- export function condition(fn: () => boolean, timeout: number | string): Promise<boolean>;
1094
+ export function condition(fn: () => boolean, timeout: Duration): Promise<boolean>;
1070
1095
 
1071
1096
  /**
1072
1097
  * Returns a Promise that resolves when `fn` evaluates to `true`.
1073
1098
  */
1074
1099
  export function condition(fn: () => boolean): Promise<void>;
1075
1100
 
1076
- export async function condition(fn: () => boolean, timeout?: number | string): Promise<void | boolean> {
1101
+ export async function condition(fn: () => boolean, timeout?: Duration): Promise<void | boolean> {
1102
+ assertInWorkflowContext('Workflow.condition(...) may only be used from a Workflow Execution.');
1077
1103
  // Prior to 1.5.0, `condition(fn, 0)` was treated as equivalent to `condition(fn, undefined)`
1078
1104
  if (timeout === 0 && !patched(CONDITION_0_PATCH)) {
1079
1105
  return conditionInner(fn);
@@ -1161,7 +1187,7 @@ export function setHandler<Ret, Args extends any[], T extends SignalDefinition<A
1161
1187
  def: T,
1162
1188
  handler: Handler<Ret, Args, T> | undefined
1163
1189
  ): void {
1164
- const activator = getActivator();
1190
+ const activator = assertInWorkflowContext('Workflow.setHandler(...) may only be used from a Workflow Execution.');
1165
1191
  if (def.type === 'signal') {
1166
1192
  if (typeof handler === 'function') {
1167
1193
  activator.signalHandlers.set(def.name, handler as any);
@@ -1194,7 +1220,9 @@ export function setHandler<Ret, Args extends any[], T extends SignalDefinition<A
1194
1220
  * @param handler a function that will handle signals for non-registered signal names, or `undefined` to unset the handler.
1195
1221
  */
1196
1222
  export function setDefaultSignalHandler(handler: DefaultSignalHandler | undefined): void {
1197
- const activator = getActivator();
1223
+ const activator = assertInWorkflowContext(
1224
+ 'Workflow.setDefaultSignalHandler(...) may only be used from a Workflow Execution.'
1225
+ );
1198
1226
  if (typeof handler === 'function') {
1199
1227
  activator.defaultSignalHandler = handler;
1200
1228
  activator.dispatchBufferedSignals();
@@ -1235,7 +1263,9 @@ export function setDefaultSignalHandler(handler: DefaultSignalHandler | undefine
1235
1263
  * @param searchAttributes The Record to merge. Use a value of `[]` to clear a Search Attribute.
1236
1264
  */
1237
1265
  export function upsertSearchAttributes(searchAttributes: SearchAttributes): void {
1238
- const activator = getActivator();
1266
+ const activator = assertInWorkflowContext(
1267
+ 'Workflow.upsertSearchAttributes(...) may only be used from a Workflow Execution.'
1268
+ );
1239
1269
 
1240
1270
  const mergedSearchAttributes = { ...activator.info.searchAttributes, ...searchAttributes };
1241
1271
  if (!mergedSearchAttributes) {
@@ -1253,3 +1283,61 @@ export function upsertSearchAttributes(searchAttributes: SearchAttributes): void
1253
1283
 
1254
1284
  export const stackTraceQuery = defineQuery<string>('__stack_trace');
1255
1285
  export const enhancedStackTraceQuery = defineQuery<EnhancedStackTrace>('__enhanced_stack_trace');
1286
+
1287
+ const loggerSinks = proxySinks<LoggerSinks>();
1288
+
1289
+ /**
1290
+ * Symbol used by the SDK logger to extract a timestamp from log attributes.
1291
+ * Also defined in `worker/logger.ts` - intentionally not shared.
1292
+ */
1293
+ const LogTimestamp = Symbol.for('log_timestamp');
1294
+
1295
+ /**
1296
+ * Default workflow logger.
1297
+ * This logger is replay-aware and will omit log messages on workflow replay.
1298
+ * The messages emitted by this logger are funnelled to the worker's `defaultSinks`, which are installed by default.
1299
+ *
1300
+ * Note that since sinks are used to power this logger, any log attributes must be transferable via the
1301
+ * {@link https://nodejs.org/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist | postMessage}
1302
+ * API.
1303
+ *
1304
+ * `defaultSinks` accepts a user logger and defaults to the `Runtime`'s logger.
1305
+ *
1306
+ * See the documentation for `WorkerOptions`, `defaultSinks`, and `Runtime` for more information.
1307
+ */
1308
+ export const log: LoggerSinks['defaultWorkerLogger'] = Object.fromEntries(
1309
+ (['trace', 'debug', 'info', 'warn', 'error'] as Array<keyof LoggerSinks['defaultWorkerLogger']>).map((level) => {
1310
+ return [
1311
+ level,
1312
+ (message: string, attrs?: Record<string, unknown>) => {
1313
+ const activator = assertInWorkflowContext('Workflow.log(...) may only be used from a Workflow Execution.');
1314
+ const getLogAttributes = composeInterceptors(activator.interceptors.outbound, 'getLogAttributes', (a) => a);
1315
+ return loggerSinks.defaultWorkerLogger[level](message, {
1316
+ // Inject the call time in nanosecond resolution as expected by the worker logger.
1317
+ [LogTimestamp]: activator.getTimeOfDay(),
1318
+ ...getLogAttributes({}),
1319
+ ...attrs,
1320
+ });
1321
+ },
1322
+ ];
1323
+ })
1324
+ ) as any;
1325
+
1326
+ function assertInWorkflowContext(message: string): Activator {
1327
+ const activator = maybeGetActivator();
1328
+ if (activator == null) throw new IllegalStateError(message);
1329
+ return activator;
1330
+ }
1331
+
1332
+ function versioningIntentToProto(intent: VersioningIntent | undefined): coresdk.common.VersioningIntent {
1333
+ switch (intent) {
1334
+ case 'DEFAULT':
1335
+ return coresdk.common.VersioningIntent.DEFAULT;
1336
+ case 'COMPATIBLE':
1337
+ return coresdk.common.VersioningIntent.COMPATIBLE;
1338
+ case undefined:
1339
+ return coresdk.common.VersioningIntent.UNSPECIFIED;
1340
+ default:
1341
+ assertNever('Unexpected VersioningIntent', intent);
1342
+ }
1343
+ }