@temporalio/workflow 1.10.3 → 1.11.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.
@@ -4,15 +4,14 @@
4
4
  * @module
5
5
  */
6
6
  import { IllegalStateError } from '@temporalio/common';
7
- import { tsToMs } from '@temporalio/common/lib/time';
8
7
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
9
8
  import { coresdk } from '@temporalio/proto';
10
9
  import { disableStorage } from './cancellation-scope';
10
+ import { disableUpdateStorage } from './update-scope';
11
11
  import { WorkflowInterceptorsFactory } from './interceptors';
12
12
  import { WorkflowCreateOptionsInternal } from './interfaces';
13
13
  import { Activator } from './internals';
14
14
  import { setActivatorUntyped, getActivator } from './global-attributes';
15
- import { type SinkCall } from './sinks';
16
15
 
17
16
  // Export the type for use on the "worker" side
18
17
  export { PromiseStackStore } from './internals';
@@ -33,7 +32,7 @@ export function initRuntime(options: WorkflowCreateOptionsInternal): void {
33
32
  unsafe: { ...options.info.unsafe, now: OriginalDate.now },
34
33
  }),
35
34
  });
36
- // There's on activator per workflow instance, set it globally on the context.
35
+ // There's one activator per workflow instance, set it globally on the context.
37
36
  // We do this before importing any user code so user code can statically reference @temporalio/workflow functions
38
37
  // as well as Date and Math.random.
39
38
  setActivatorUntyped(activator);
@@ -110,66 +109,26 @@ function fixPrototypes<X>(obj: X): X {
110
109
 
111
110
  /**
112
111
  * Run a chunk of activation jobs
113
- * @returns a boolean indicating whether job was processed or ignored
114
112
  */
115
- export function activate(activation: coresdk.workflow_activation.WorkflowActivation, batchIndex: number): void {
113
+ export function activate(activation: coresdk.workflow_activation.IWorkflowActivation, batchIndex: number): void {
116
114
  const activator = getActivator();
117
- const intercept = composeInterceptors(activator.interceptors.internals, 'activate', ({ activation, batchIndex }) => {
118
- if (batchIndex === 0) {
119
- if (!activation.jobs) {
120
- throw new TypeError('Got activation with no jobs');
121
- }
122
- if (activation.timestamp != null) {
123
- // timestamp will not be updated for activation that contain only queries
124
- activator.now = tsToMs(activation.timestamp);
125
- }
126
- activator.addKnownFlags(activation.availableInternalFlags ?? []);
127
-
128
- // The Rust Core ensures that these activation fields are not null
129
- activator.mutateWorkflowInfo((info) => ({
130
- ...info,
131
- historyLength: activation.historyLength as number,
132
- // Exact truncation for multi-petabyte histories
133
- // historySize === 0 means WFT was generated by pre-1.20.0 server, and the history size is unknown
134
- historySize: activation.historySizeBytes?.toNumber() || 0,
135
- continueAsNewSuggested: activation.continueAsNewSuggested ?? false,
136
- currentBuildId: activation.buildIdForCurrentTask ?? undefined,
137
- unsafe: {
138
- ...info.unsafe,
139
- isReplaying: activation.isReplaying ?? false,
140
- },
141
- }));
142
- }
143
-
115
+ const intercept = composeInterceptors(activator.interceptors.internals, 'activate', ({ activation }) => {
144
116
  // Cast from the interface to the class which has the `variant` attribute.
145
117
  // This is safe because we know that activation is a proto class.
146
118
  const jobs = activation.jobs as coresdk.workflow_activation.WorkflowActivationJob[];
147
119
 
148
120
  for (const job of jobs) {
149
- if (job.variant === undefined) {
150
- throw new TypeError('Expected job.variant to be defined');
151
- }
121
+ if (job.variant === undefined) throw new TypeError('Expected job.variant to be defined');
152
122
 
153
123
  const variant = job[job.variant];
154
- if (!variant) {
155
- throw new TypeError(`Expected job.${job.variant} to be set`);
156
- }
157
- // The only job that can be executed on a completed workflow is a query.
158
- // We might get other jobs after completion for instance when a single
159
- // activation contains multiple jobs and the first one completes the workflow.
160
- if (activator.completed && job.variant !== 'queryWorkflow') {
161
- return;
162
- }
124
+ if (!variant) throw new TypeError(`Expected job.${job.variant} to be set`);
125
+
163
126
  activator[job.variant](variant as any /* TS can't infer this type */);
164
- if (shouldUnblockConditions(job)) {
165
- tryUnblockConditions();
166
- }
127
+
128
+ if (job.variant !== 'queryWorkflow') tryUnblockConditions();
167
129
  }
168
130
  });
169
- intercept({
170
- activation,
171
- batchIndex,
172
- });
131
+ intercept({ activation, batchIndex });
173
132
  }
174
133
 
175
134
  /**
@@ -182,20 +141,18 @@ export function concludeActivation(): coresdk.workflow_completion.IWorkflowActiv
182
141
  const activator = getActivator();
183
142
  activator.rejectBufferedUpdates();
184
143
  const intercept = composeInterceptors(activator.interceptors.internals, 'concludeActivation', (input) => input);
185
- const { info } = activator;
186
144
  const activationCompletion = activator.concludeActivation();
187
145
  const { commands } = intercept({ commands: activationCompletion.commands });
146
+ if (activator.completed) {
147
+ activator.warnIfUnfinishedHandlers();
148
+ }
188
149
 
189
150
  return {
190
- runId: info.runId,
151
+ runId: activator.info.runId,
191
152
  successful: { ...activationCompletion, commands },
192
153
  };
193
154
  }
194
155
 
195
- export function getAndResetSinkCalls(): SinkCall[] {
196
- return getActivator().getAndResetSinkCalls();
197
- }
198
-
199
156
  /**
200
157
  * Loop through all blocked conditions, evaluate and unblock if possible.
201
158
  *
@@ -220,16 +177,10 @@ export function tryUnblockConditions(): number {
220
177
  return numUnblocked;
221
178
  }
222
179
 
223
- /**
224
- * Predicate used to prevent triggering conditions for non-query and non-patch jobs.
225
- */
226
- export function shouldUnblockConditions(job: coresdk.workflow_activation.IWorkflowActivationJob): boolean {
227
- return !job.queryWorkflow && !job.notifyHasPatch;
228
- }
229
-
230
180
  export function dispose(): void {
231
181
  const dispose = composeInterceptors(getActivator().interceptors.internals, 'dispose', async () => {
232
182
  disableStorage();
183
+ disableUpdateStorage();
233
184
  });
234
185
  dispose({});
235
186
  }
package/src/workflow.ts CHANGED
@@ -3,6 +3,7 @@ import {
3
3
  ActivityOptions,
4
4
  compileRetryPolicy,
5
5
  extractWorkflowType,
6
+ HandlerUnfinishedPolicy,
6
7
  LocalActivityOptions,
7
8
  mapToPayloads,
8
9
  QueryDefinition,
@@ -23,6 +24,7 @@ import { Duration, msOptionalToTs, msToNumber, msToTs, requiredTsToMs } from '@t
23
24
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
24
25
  import { temporal } from '@temporalio/proto';
25
26
  import { CancellationScope, registerSleepImplementation } from './cancellation-scope';
27
+ import { UpdateScope } from './update-scope';
26
28
  import {
27
29
  ActivityInput,
28
30
  LocalActivityInput,
@@ -43,6 +45,7 @@ import {
43
45
  SignalHandlerOptions,
44
46
  UpdateHandlerOptions,
45
47
  WorkflowInfo,
48
+ UpdateInfo,
46
49
  } from './interfaces';
47
50
  import { LocalActivityDoBackoff } from './errors';
48
51
  import { assertInWorkflowContext, getActivator, maybeGetActivator } from './global-attributes';
@@ -53,7 +56,7 @@ import { ChildWorkflowHandle, ExternalWorkflowHandle } from './workflow-handle';
53
56
  registerSleepImplementation(sleep);
54
57
 
55
58
  /**
56
- * Adds default values to `workflowId` and `workflowIdReusePolicy` to given workflow options.
59
+ * Adds default values of `workflowId` and `cancellationType` to given workflow options.
57
60
  */
58
61
  export function addDefaultWorkflowOptions<T extends Workflow>(
59
62
  opts: WithWorkflowArgs<T, ChildWorkflowOptions>
@@ -867,6 +870,19 @@ export function workflowInfo(): WorkflowInfo {
867
870
  return activator.info;
868
871
  }
869
872
 
873
+ /**
874
+ * Get information about the current update if any.
875
+ *
876
+ * @return Info for the current update handler the code calling this is executing
877
+ * within if any.
878
+ *
879
+ * @experimental
880
+ */
881
+ export function currentUpdateInfo(): UpdateInfo | undefined {
882
+ assertInWorkflowContext('Workflow.currentUpdateInfo(...) may only be used from a Workflow Execution.');
883
+ return UpdateScope.current();
884
+ }
885
+
870
886
  /**
871
887
  * Returns whether or not code is executing in workflow context
872
888
  */
@@ -1080,8 +1096,8 @@ function conditionInner(fn: () => boolean): Promise<void> {
1080
1096
  /**
1081
1097
  * Define an update method for a Workflow.
1082
1098
  *
1083
- * 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}.
1084
- * Definitions can be reused in multiple Workflows.
1099
+ * A definition is used to register a handler in the Workflow via {@link setHandler} and to update a Workflow using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
1100
+ * A definition can be reused in multiple Workflows.
1085
1101
  */
1086
1102
  export function defineUpdate<Ret, Args extends any[] = [], Name extends string = string>(
1087
1103
  name: Name
@@ -1095,8 +1111,8 @@ export function defineUpdate<Ret, Args extends any[] = [], Name extends string =
1095
1111
  /**
1096
1112
  * Define a signal method for a Workflow.
1097
1113
  *
1098
- * Definitions are used to register handler in the Workflow via {@link setHandler} and to signal Workflows using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
1099
- * Definitions can be reused in multiple Workflows.
1114
+ * A definition is used to register a handler in the Workflow via {@link setHandler} and to signal a Workflow using a {@link WorkflowHandle}, {@link ChildWorkflowHandle} or {@link ExternalWorkflowHandle}.
1115
+ * A definition can be reused in multiple Workflows.
1100
1116
  */
1101
1117
  export function defineSignal<Args extends any[] = [], Name extends string = string>(
1102
1118
  name: Name
@@ -1110,8 +1126,8 @@ export function defineSignal<Args extends any[] = [], Name extends string = stri
1110
1126
  /**
1111
1127
  * Define a query method for a Workflow.
1112
1128
  *
1113
- * Definitions are used to register handler in the Workflow via {@link setHandler} and to query Workflows using a {@link WorkflowHandle}.
1114
- * Definitions can be reused in multiple Workflows.
1129
+ * A definition is used to register a handler in the Workflow via {@link setHandler} and to query a Workflow using a {@link WorkflowHandle}.
1130
+ * A definition can be reused in multiple Workflows.
1115
1131
  */
1116
1132
  export function defineQuery<Ret, Args extends any[] = [], Name extends string = string>(
1117
1133
  name: Name
@@ -1178,7 +1194,7 @@ export function setHandler<Ret, Args extends any[], T extends UpdateDefinition<R
1178
1194
  // 1. sdk-core sorts Signal and Update jobs (and Patches) ahead of all other
1179
1195
  // jobs. Thus if the handler is available at the start of the Activation then
1180
1196
  // the Signal/Update will be executed before Workflow code is executed. If it
1181
- // is not, then the Signal/Update calls is pushed to a buffer.
1197
+ // is not, then the Signal/Update calls are pushed to a buffer.
1182
1198
  //
1183
1199
  // 2. On each call to setHandler for a given Signal/Update, we make a pass
1184
1200
  // through the buffer list. If a buffered job is associated with the just-set
@@ -1226,7 +1242,7 @@ export function setHandler<Ret, Args extends any[], T extends UpdateDefinition<R
1226
1242
  // Worker polling until after they have verified that both requests have
1227
1243
  // succeeded.)
1228
1244
  //
1229
- // 5. If an Update has a validation function then it is executed immediately
1245
+ // 4. If an Update has a validation function then it is executed immediately
1230
1246
  // prior to the handler. (Note that the validation function is required to be
1231
1247
  // synchronous).
1232
1248
  export function setHandler<
@@ -1243,8 +1259,10 @@ export function setHandler<
1243
1259
  if (def.type === 'update') {
1244
1260
  if (typeof handler === 'function') {
1245
1261
  const updateOptions = options as UpdateHandlerOptions<Args> | undefined;
1262
+
1246
1263
  const validator = updateOptions?.validator as WorkflowUpdateValidatorType | undefined;
1247
- activator.updateHandlers.set(def.name, { handler, validator, description });
1264
+ const unfinishedPolicy = updateOptions?.unfinishedPolicy ?? HandlerUnfinishedPolicy.WARN_AND_ABANDON;
1265
+ activator.updateHandlers.set(def.name, { handler, validator, description, unfinishedPolicy });
1248
1266
  activator.dispatchBufferedUpdates();
1249
1267
  } else if (handler == null) {
1250
1268
  activator.updateHandlers.delete(def.name);
@@ -1253,7 +1271,9 @@ export function setHandler<
1253
1271
  }
1254
1272
  } else if (def.type === 'signal') {
1255
1273
  if (typeof handler === 'function') {
1256
- activator.signalHandlers.set(def.name, { handler: handler as any, description });
1274
+ const signalOptions = options as SignalHandlerOptions | undefined;
1275
+ const unfinishedPolicy = signalOptions?.unfinishedPolicy ?? HandlerUnfinishedPolicy.WARN_AND_ABANDON;
1276
+ activator.signalHandlers.set(def.name, { handler: handler as any, description, unfinishedPolicy });
1257
1277
  activator.dispatchBufferedSignals();
1258
1278
  } else if (handler == null) {
1259
1279
  activator.signalHandlers.delete(def.name);
@@ -1351,6 +1371,91 @@ export function upsertSearchAttributes(searchAttributes: SearchAttributes): void
1351
1371
  });
1352
1372
  }
1353
1373
 
1374
+ /**
1375
+ * Updates this Workflow's Memos by merging the provided `memo` with existing
1376
+ * Memos (as returned by `workflowInfo().memo`).
1377
+ *
1378
+ * New memo is merged by replacing properties of the same name _at the first
1379
+ * level only_. Setting a property to value `undefined` or `null` clears that
1380
+ * key from the Memo.
1381
+ *
1382
+ * For example:
1383
+ *
1384
+ * ```ts
1385
+ * upsertMemo({
1386
+ * key1: value,
1387
+ * key3: { subkey1: value }
1388
+ * key4: value,
1389
+ * });
1390
+ * upsertMemo({
1391
+ * key2: value
1392
+ * key3: { subkey2: value }
1393
+ * key4: undefined,
1394
+ * });
1395
+ * ```
1396
+ *
1397
+ * would result in the Workflow having these Memo:
1398
+ *
1399
+ * ```ts
1400
+ * {
1401
+ * key1: value,
1402
+ * key2: value,
1403
+ * key3: { subkey2: value } // Note this object was completely replaced
1404
+ * // Note that key4 was completely removed
1405
+ * }
1406
+ * ```
1407
+ *
1408
+ * @param memo The Record to merge.
1409
+ */
1410
+ export function upsertMemo(memo: Record<string, unknown>): void {
1411
+ const activator = assertInWorkflowContext('Workflow.upsertMemo(...) may only be used from a Workflow Execution.');
1412
+
1413
+ if (memo == null) {
1414
+ throw new Error('memo must be a non-null Record');
1415
+ }
1416
+
1417
+ activator.pushCommand({
1418
+ modifyWorkflowProperties: {
1419
+ upsertedMemo: {
1420
+ fields: mapToPayloads(
1421
+ activator.payloadConverter,
1422
+ // Convert null to undefined
1423
+ Object.fromEntries(Object.entries(memo).map(([k, v]) => [k, v ?? undefined]))
1424
+ ),
1425
+ },
1426
+ },
1427
+ });
1428
+
1429
+ activator.mutateWorkflowInfo((info: WorkflowInfo): WorkflowInfo => {
1430
+ return {
1431
+ ...info,
1432
+ memo: Object.fromEntries(
1433
+ Object.entries({
1434
+ ...info.memo,
1435
+ ...memo,
1436
+ }).filter(([_, v]) => v != null)
1437
+ ),
1438
+ };
1439
+ });
1440
+ }
1441
+
1442
+ /**
1443
+ * Whether update and signal handlers have finished executing.
1444
+ *
1445
+ * Consider waiting on this condition before workflow return or continue-as-new, to prevent
1446
+ * interruption of in-progress handlers by workflow exit:
1447
+ *
1448
+ * ```ts
1449
+ * await workflow.condition(workflow.allHandlersFinished)
1450
+ * ```
1451
+ *
1452
+ * @returns true if there are no in-progress update or signal handler executions.
1453
+ */
1454
+ export function allHandlersFinished(): boolean {
1455
+ const activator = assertInWorkflowContext('allHandlersFinished() may only be used from a Workflow Execution.');
1456
+ return activator.inProgressSignals.size === 0 && activator.inProgressUpdates.size === 0;
1457
+ }
1458
+
1354
1459
  export const stackTraceQuery = defineQuery<string>('__stack_trace');
1355
1460
  export const enhancedStackTraceQuery = defineQuery<EnhancedStackTrace>('__enhanced_stack_trace');
1356
1461
  export const workflowMetadataQuery = defineQuery<temporal.api.sdk.v1.IWorkflowMetadata>('__temporal_workflow_metadata');