@temporalio/workflow 1.8.5 → 1.9.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/workflow.ts CHANGED
@@ -12,10 +12,12 @@ import {
12
12
  SignalDefinition,
13
13
  toPayloads,
14
14
  UntypedActivities,
15
+ UpdateDefinition,
15
16
  WithWorkflowArgs,
16
17
  Workflow,
17
18
  WorkflowResultType,
18
19
  WorkflowReturnType,
20
+ WorkflowUpdateValidatorType,
19
21
  } from '@temporalio/common';
20
22
  import { versioningIntentToProto } from '@temporalio/common/lib/versioning-intent-enum';
21
23
  import { Duration, msOptionalToTs, msToNumber, msToTs, tsToMs } from '@temporalio/common/lib/time';
@@ -37,10 +39,11 @@ import {
37
39
  DefaultSignalHandler,
38
40
  EnhancedStackTrace,
39
41
  Handler,
42
+ UpdateValidator,
40
43
  WorkflowInfo,
41
44
  } from './interfaces';
42
- import { Activator, LocalActivityDoBackoff, getActivator, maybeGetActivator } from './internals';
43
- import { LoggerSinks, Sinks } from './sinks';
45
+ import { LocalActivityDoBackoff } from './errors';
46
+ import { assertInWorkflowContext, getActivator, maybeGetActivator } from './global-attributes';
44
47
  import { untrackPromise } from './stack-helpers';
45
48
  import { ChildWorkflowHandle, ExternalWorkflowHandle } from './workflow-handle';
46
49
 
@@ -165,7 +168,7 @@ function scheduleActivityNextHandler({ options, args, headers, seq, activityType
165
168
  activityType,
166
169
  arguments: toPayloads(activator.payloadConverter, ...args),
167
170
  retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
168
- taskQueue: options.taskQueue || activator.info?.taskQueue,
171
+ taskQueue: options.taskQueue || activator.info.taskQueue,
169
172
  heartbeatTimeout: msOptionalToTs(options.heartbeatTimeout),
170
173
  scheduleToCloseTimeout: msOptionalToTs(options.scheduleToCloseTimeout),
171
174
  startToCloseTimeout: msOptionalToTs(options.startToCloseTimeout),
@@ -198,7 +201,7 @@ async function scheduleLocalActivityNextHandler({
198
201
  const activator = getActivator();
199
202
  // Eagerly fail the local activity (which will in turn fail the workflow task.
200
203
  // Do not fail on replay where the local activities may not be registered on the replay worker.
201
- if (!workflowInfo().unsafe.isReplaying && !activator.registeredActivityNames.has(activityType)) {
204
+ if (!activator.info.unsafe.isReplaying && !activator.registeredActivityNames.has(activityType)) {
202
205
  throw new ReferenceError(`Local activity of type '${activityType}' not registered on worker`);
203
206
  }
204
207
  validateLocalActivityOptions(options);
@@ -358,11 +361,11 @@ function startChildWorkflowExecutionNextHandler({
358
361
  workflowType,
359
362
  input: toPayloads(activator.payloadConverter, ...options.args),
360
363
  retryPolicy: options.retry ? compileRetryPolicy(options.retry) : undefined,
361
- taskQueue: options.taskQueue || activator.info?.taskQueue,
364
+ taskQueue: options.taskQueue || activator.info.taskQueue,
362
365
  workflowExecutionTimeout: msOptionalToTs(options.workflowExecutionTimeout),
363
366
  workflowRunTimeout: msOptionalToTs(options.workflowRunTimeout),
364
367
  workflowTaskTimeout: msOptionalToTs(options.workflowTaskTimeout),
365
- namespace: workflowInfo().namespace, // Not configurable
368
+ namespace: activator.info.namespace, // Not configurable
366
369
  headers,
367
370
  cancellationType: options.cancellationType,
368
371
  workflowIdReusePolicy: options.workflowIdReusePolicy,
@@ -830,9 +833,11 @@ export async function executeChild<T extends Workflow>(
830
833
  /**
831
834
  * Get information about the current Workflow.
832
835
  *
833
- * ⚠️ We recommend calling `workflowInfo()` whenever accessing {@link WorkflowInfo} fields. Some WorkflowInfo fields
834
- * change during the lifetime of an Execution—like {@link WorkflowInfo.historyLength} and
835
- * {@link WorkflowInfo.searchAttributes}—and some may be changeable in the future—like {@link WorkflowInfo.taskQueue}.
836
+ * WARNING: This function returns a frozen copy of WorkflowInfo, at the point where this method has been called.
837
+ * Changes happening at later point in workflow execution will not be reflected in the returned object.
838
+ *
839
+ * For this reason, we recommend calling `workflowInfo()` on every access to {@link WorkflowInfo}'s fields,
840
+ * rather than caching the `WorkflowInfo` object (or part of it) in a local variable. For example:
836
841
  *
837
842
  * ```ts
838
843
  * // GOOD
@@ -843,6 +848,8 @@ export async function executeChild<T extends Workflow>(
843
848
  * }
844
849
  * ```
845
850
  *
851
+ * vs
852
+ *
846
853
  * ```ts
847
854
  * // BAD
848
855
  * function myWorkflow() {
@@ -851,6 +858,7 @@ export async function executeChild<T extends Workflow>(
851
858
  * ...
852
859
  * doSomethingElse(attributes)
853
860
  * }
861
+ * ```
854
862
  */
855
863
  export function workflowInfo(): WorkflowInfo {
856
864
  const activator = assertInWorkflowContext('Workflow.workflowInfo(...) may only be used from a Workflow Execution.');
@@ -864,72 +872,6 @@ export function inWorkflowContext(): boolean {
864
872
  return maybeGetActivator() !== undefined;
865
873
  }
866
874
 
867
- /**
868
- * Get a reference to Sinks for exporting data out of the Workflow.
869
- *
870
- * These Sinks **must** be registered with the Worker in order for this
871
- * mechanism to work.
872
- *
873
- * @example
874
- * ```ts
875
- * import { proxySinks, Sinks } from '@temporalio/workflow';
876
- *
877
- * interface MySinks extends Sinks {
878
- * logger: {
879
- * info(message: string): void;
880
- * error(message: string): void;
881
- * };
882
- * }
883
- *
884
- * const { logger } = proxySinks<MyDependencies>();
885
- * logger.info('setting up');
886
- *
887
- * export function myWorkflow() {
888
- * return {
889
- * async execute() {
890
- * logger.info("hey ho");
891
- * logger.error("lets go");
892
- * }
893
- * };
894
- * }
895
- * ```
896
- */
897
- export function proxySinks<T extends Sinks>(): T {
898
- return new Proxy(
899
- {},
900
- {
901
- get(_, ifaceName) {
902
- return new Proxy(
903
- {},
904
- {
905
- get(_, fnName) {
906
- return (...args: any[]) => {
907
- const activator = assertInWorkflowContext(
908
- 'Proxied sinks functions may only be used from a Workflow Execution.'
909
- );
910
- const info = workflowInfo();
911
- activator.sinkCalls.push({
912
- ifaceName: ifaceName as string,
913
- fnName: fnName as string,
914
- // Only available from node 17.
915
- args: (globalThis as any).structuredClone ? (globalThis as any).structuredClone(args) : args,
916
- // Clone the workflowInfo object so that any further mutations to it does not get reflected in sink
917
- workflowInfo: {
918
- ...info,
919
- // Make sure to clone any sub-property that may get mutated during the lifespan of an activation
920
- searchAttributes: { ...info.searchAttributes },
921
- memo: info.memo ? { ...info.memo } : undefined,
922
- },
923
- });
924
- };
925
- },
926
- }
927
- );
928
- },
929
- }
930
- ) as any;
931
- }
932
-
933
875
  /**
934
876
  * Returns a function `f` that will cause the current Workflow to ContinueAsNew when called.
935
877
  *
@@ -1149,6 +1091,21 @@ function conditionInner(fn: () => boolean): Promise<void> {
1149
1091
  });
1150
1092
  }
1151
1093
 
1094
+ /**
1095
+ * Define an update method for a Workflow.
1096
+ *
1097
+ * Definitions are used to register handler in the Workflow via {@link setHandler} and to update Workflows using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
1098
+ * Definitions can be reused in multiple Workflows.
1099
+ */
1100
+ export function defineUpdate<Ret, Args extends any[] = [], Name extends string = string>(
1101
+ name: Name
1102
+ ): UpdateDefinition<Ret, Args, Name> {
1103
+ return {
1104
+ type: 'update',
1105
+ name,
1106
+ } as UpdateDefinition<Ret, Args, Name>;
1107
+ }
1108
+
1152
1109
  /**
1153
1110
  * Define a signal method for a Workflow.
1154
1111
  *
@@ -1180,19 +1137,122 @@ export function defineQuery<Ret, Args extends any[] = [], Name extends string =
1180
1137
  }
1181
1138
 
1182
1139
  /**
1183
- * Set a handler function for a Workflow query or signal.
1140
+ * Set a handler function for a Workflow update, signal, or query.
1184
1141
  *
1185
- * If this function is called multiple times for a given signal or query name the last handler will overwrite any previous calls.
1142
+ * If this function is called multiple times for a given update, signal, or query name the last handler will overwrite any previous calls.
1186
1143
  *
1187
- * @param def a {@link SignalDefinition} or {@link QueryDefinition} as returned by {@link defineSignal} or {@link defineQuery} respectively.
1144
+ * @param def an {@link UpdateDefinition}, {@link SignalDefinition}, or {@link QueryDefinition} as returned by {@link defineUpdate}, {@link defineSignal}, or {@link defineQuery} respectively.
1188
1145
  * @param handler a compatible handler function for the given definition or `undefined` to unset the handler.
1189
1146
  */
1190
1147
  export function setHandler<Ret, Args extends any[], T extends SignalDefinition<Args> | QueryDefinition<Ret, Args>>(
1191
1148
  def: T,
1192
1149
  handler: Handler<Ret, Args, T> | undefined
1193
- ): void {
1150
+ ): void;
1151
+ export function setHandler<Ret, Args extends any[], T extends UpdateDefinition<Ret, Args>>(
1152
+ def: T,
1153
+ handler: Handler<Ret, Args, T> | undefined,
1154
+ options?: { validator: UpdateValidator<Args> }
1155
+ ): void;
1156
+
1157
+ // For Updates and Signals we want to make a public guarantee something like the
1158
+ // following:
1159
+ //
1160
+ // "If a WFT contains a Signal/Update, and if a handler is available for that
1161
+ // Signal/Update, then the handler will be executed.""
1162
+ //
1163
+ // However, that statement is not well-defined, leaving several questions open:
1164
+ //
1165
+ // 1. What does it mean for a handler to be "available"? What happens if the
1166
+ // handler is not present initially but is set at some point during the
1167
+ // Workflow code that is executed in that WFT? What happens if the handler is
1168
+ // set and then deleted, or replaced with a different handler?
1169
+ //
1170
+ // 2. When is the handler executed? (When it first becomes available? At the end
1171
+ // of the activation?) What are the execution semantics of Workflow and
1172
+ // Signal/Update handler code given that they are concurrent? Can the user
1173
+ // rely on Signal/Update side effects being reflected in the Workflow return
1174
+ // value, or in the value passed to Continue-As-New? If the handler is an
1175
+ // async function / coroutine, how much of it is executed and when is the
1176
+ // rest executed?
1177
+ //
1178
+ // 3. What happens if the handler is not executed? (i.e. because it wasn't
1179
+ // available in the sense defined by (1))
1180
+ //
1181
+ // 4. In the case of Update, when is the validation function executed?
1182
+ //
1183
+ // The implementation for Typescript is as follows:
1184
+ //
1185
+ // 1. sdk-core sorts Signal and Update jobs (and Patches) ahead of all other
1186
+ // jobs. Thus if the handler is available at the start of the Activation then
1187
+ // the Signal/Update will be executed before Workflow code is executed. If it
1188
+ // is not, then the Signal/Update calls is pushed to a buffer.
1189
+ //
1190
+ // 2. On each call to setHandler for a given Signal/Update, we make a pass
1191
+ // through the buffer list. If a buffered job is associated with the just-set
1192
+ // handler, then the job is removed from the buffer and the initial
1193
+ // synchronous portion of the handler is invoked on that input (i.e.
1194
+ // preempting workflow code).
1195
+ //
1196
+ // Thus in the case of Typescript the questions above are answered as follows:
1197
+ //
1198
+ // 1. A handler is "available" if it is set at the start of the Activation or
1199
+ // becomes set at any point during the Activation. If the handler is not set
1200
+ // initially then it is executed as soon as it is set. Subsequent deletion or
1201
+ // replacement by a different handler has no impact because the jobs it was
1202
+ // handling have already been handled and are no longer in the buffer.
1203
+ //
1204
+ // 2. The handler is executed as soon as it becomes available. I.e. if the
1205
+ // handler is set at the start of the Activation then it is executed when
1206
+ // first attempting to process the Signal/Update job; alternatively, if it is
1207
+ // set by a setHandler call made by Workflow code, then it is executed as
1208
+ // part of that call (preempting Workflow code). Therefore, a user can rely
1209
+ // on Signal/Update side effects being reflected in e.g. the Workflow return
1210
+ // value, and in the value passed to Continue-As-New. Activation jobs are
1211
+ // processed in the order supplied by sdk-core, i.e. Signals, then Updates,
1212
+ // then other jobs. Within each group, the order sent by the server is
1213
+ // preserved. If the handler is async, it is executed up to its first yield
1214
+ // point.
1215
+ //
1216
+ // 3. Signal case: If a handler does not become available for a Signal job then
1217
+ // the job remains in the buffer. If a handler for the Signal becomes
1218
+ // available in a subsequent Activation (of the same or a subsequent WFT)
1219
+ // then the handler will be executed. If not, then the Signal will never be
1220
+ // responded to and this causes no error.
1221
+ //
1222
+ // Update case: If a handler does not become available for an Update job then
1223
+ // the Update is rejected at the end of the Activation. Thus, if a user does
1224
+ // not want an Update to be rejected for this reason, then it is their
1225
+ // responsibility to ensure that their application and workflow code interact
1226
+ // such that a handler is available for the Update during any Activation
1227
+ // which might contain their Update job. (Note that the user often has
1228
+ // uncertainty about which WFT their Signal/Update will appear in. For
1229
+ // example, if they call startWorkflow() followed by startUpdate(), then they
1230
+ // will typically not know whether these will be delivered in one or two
1231
+ // WFTs. On the other hand there are situations where they would have reason
1232
+ // to believe they are in the same WFT, for example if they do not start
1233
+ // Worker polling until after they have verified that both requests have
1234
+ // succeeded.)
1235
+ //
1236
+ // 5. If an Update has a validation function then it is executed immediately
1237
+ // prior to the handler. (Note that the validation function is required to be
1238
+ // synchronous).
1239
+ export function setHandler<
1240
+ Ret,
1241
+ Args extends any[],
1242
+ T extends UpdateDefinition<Ret, Args> | SignalDefinition<Args> | QueryDefinition<Ret, Args>
1243
+ >(def: T, handler: Handler<Ret, Args, T> | undefined, options?: { validator: UpdateValidator<Args> }): void {
1194
1244
  const activator = assertInWorkflowContext('Workflow.setHandler(...) may only be used from a Workflow Execution.');
1195
- if (def.type === 'signal') {
1245
+ if (def.type === 'update') {
1246
+ if (typeof handler === 'function') {
1247
+ const validator = options?.validator as WorkflowUpdateValidatorType | undefined;
1248
+ activator.updateHandlers.set(def.name, { handler, validator });
1249
+ activator.dispatchBufferedUpdates();
1250
+ } else if (handler == null) {
1251
+ activator.updateHandlers.delete(def.name);
1252
+ } else {
1253
+ throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
1254
+ }
1255
+ } else if (def.type === 'signal') {
1196
1256
  if (typeof handler === 'function') {
1197
1257
  activator.signalHandlers.set(def.name, handler as any);
1198
1258
  activator.dispatchBufferedSignals();
@@ -1271,8 +1331,7 @@ export function upsertSearchAttributes(searchAttributes: SearchAttributes): void
1271
1331
  'Workflow.upsertSearchAttributes(...) may only be used from a Workflow Execution.'
1272
1332
  );
1273
1333
 
1274
- const mergedSearchAttributes = { ...activator.info.searchAttributes, ...searchAttributes };
1275
- if (!mergedSearchAttributes) {
1334
+ if (searchAttributes == null) {
1276
1335
  throw new Error('searchAttributes must be a non-null SearchAttributes');
1277
1336
  }
1278
1337
 
@@ -1282,53 +1341,16 @@ export function upsertSearchAttributes(searchAttributes: SearchAttributes): void
1282
1341
  },
1283
1342
  });
1284
1343
 
1285
- activator.info.searchAttributes = mergedSearchAttributes;
1344
+ activator.mutateWorkflowInfo((info: WorkflowInfo): WorkflowInfo => {
1345
+ return {
1346
+ ...info,
1347
+ searchAttributes: {
1348
+ ...info.searchAttributes,
1349
+ ...searchAttributes,
1350
+ },
1351
+ };
1352
+ });
1286
1353
  }
1287
1354
 
1288
1355
  export const stackTraceQuery = defineQuery<string>('__stack_trace');
1289
1356
  export const enhancedStackTraceQuery = defineQuery<EnhancedStackTrace>('__enhanced_stack_trace');
1290
-
1291
- const loggerSinks = proxySinks<LoggerSinks>();
1292
-
1293
- /**
1294
- * Symbol used by the SDK logger to extract a timestamp from log attributes.
1295
- * Also defined in `worker/logger.ts` - intentionally not shared.
1296
- */
1297
- const LogTimestamp = Symbol.for('log_timestamp');
1298
-
1299
- /**
1300
- * Default workflow logger.
1301
- * This logger is replay-aware and will omit log messages on workflow replay.
1302
- * The messages emitted by this logger are funnelled to the worker's `defaultSinks`, which are installed by default.
1303
- *
1304
- * Note that since sinks are used to power this logger, any log attributes must be transferable via the
1305
- * {@link https://nodejs.org/api/worker_threads.html#worker_threads_port_postmessage_value_transferlist | postMessage}
1306
- * API.
1307
- *
1308
- * `defaultSinks` accepts a user logger and defaults to the `Runtime`'s logger.
1309
- *
1310
- * See the documentation for `WorkerOptions`, `defaultSinks`, and `Runtime` for more information.
1311
- */
1312
- export const log: LoggerSinks['defaultWorkerLogger'] = Object.fromEntries(
1313
- (['trace', 'debug', 'info', 'warn', 'error'] as Array<keyof LoggerSinks['defaultWorkerLogger']>).map((level) => {
1314
- return [
1315
- level,
1316
- (message: string, attrs?: Record<string, unknown>) => {
1317
- const activator = assertInWorkflowContext('Workflow.log(...) may only be used from a Workflow Execution.');
1318
- const getLogAttributes = composeInterceptors(activator.interceptors.outbound, 'getLogAttributes', (a) => a);
1319
- return loggerSinks.defaultWorkerLogger[level](message, {
1320
- // Inject the call time in nanosecond resolution as expected by the worker logger.
1321
- [LogTimestamp]: activator.getTimeOfDay(),
1322
- ...getLogAttributes({}),
1323
- ...attrs,
1324
- });
1325
- },
1326
- ];
1327
- })
1328
- ) as any;
1329
-
1330
- function assertInWorkflowContext(message: string): Activator {
1331
- const activator = maybeGetActivator();
1332
- if (activator == null) throw new IllegalStateError(message);
1333
- return activator;
1334
- }