@temporalio/workflow 1.11.8 → 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/workflow.ts CHANGED
@@ -9,10 +9,11 @@ import {
9
9
  LocalActivityOptions,
10
10
  mapToPayloads,
11
11
  QueryDefinition,
12
- searchAttributePayloadConverter,
13
12
  SearchAttributes,
13
+ SearchAttributeValue,
14
14
  SignalDefinition,
15
15
  toPayloads,
16
+ TypedSearchAttributes,
16
17
  UntypedActivities,
17
18
  UpdateDefinition,
18
19
  WithWorkflowArgs,
@@ -20,7 +21,14 @@ import {
20
21
  WorkflowResultType,
21
22
  WorkflowReturnType,
22
23
  WorkflowUpdateValidatorType,
24
+ SearchAttributeUpdatePair,
25
+ compilePriority,
26
+ WorkflowDefinitionOptionsOrGetter,
23
27
  } from '@temporalio/common';
28
+ import {
29
+ encodeUnifiedSearchAttributes,
30
+ searchAttributePayloadConverter,
31
+ } from '@temporalio/common/lib/converter/payload-search-attributes';
24
32
  import { versioningIntentToProto } from '@temporalio/common/lib/versioning-intent-enum';
25
33
  import { Duration, msOptionalToTs, msToNumber, msToTs, requiredTsToMs } from '@temporalio/common/lib/time';
26
34
  import { composeInterceptors } from '@temporalio/common/lib/interceptors';
@@ -50,6 +58,8 @@ import {
50
58
  UpdateInfo,
51
59
  encodeChildWorkflowCancellationType,
52
60
  encodeParentClosePolicy,
61
+ DefaultUpdateHandler,
62
+ DefaultQueryHandler,
53
63
  } from './interfaces';
54
64
  import { LocalActivityDoBackoff } from './errors';
55
65
  import { assertInWorkflowContext, getActivator, maybeGetActivator } from './global-attributes';
@@ -186,6 +196,7 @@ function scheduleActivityNextHandler({ options, args, headers, seq, activityType
186
196
  cancellationType: encodeActivityCancellationType(options.cancellationType),
187
197
  doNotEagerlyExecute: !(options.allowEagerDispatch ?? true),
188
198
  versioningIntent: versioningIntentToProto(options.versioningIntent),
199
+ priority: options.priority ? compilePriority(options.priority) : undefined,
189
200
  },
190
201
  });
191
202
  activator.completions.activity.set(seq, {
@@ -380,11 +391,13 @@ function startChildWorkflowExecutionNextHandler({
380
391
  workflowIdReusePolicy: encodeWorkflowIdReusePolicy(options.workflowIdReusePolicy),
381
392
  parentClosePolicy: encodeParentClosePolicy(options.parentClosePolicy),
382
393
  cronSchedule: options.cronSchedule,
383
- searchAttributes: options.searchAttributes
384
- ? mapToPayloads(searchAttributePayloadConverter, options.searchAttributes)
385
- : undefined,
394
+ searchAttributes:
395
+ options.searchAttributes || options.typedSearchAttributes // eslint-disable-line deprecation/deprecation
396
+ ? encodeUnifiedSearchAttributes(options.searchAttributes, options.typedSearchAttributes) // eslint-disable-line deprecation/deprecation
397
+ : undefined,
386
398
  memo: options.memo && mapToPayloads(activator.payloadConverter, options.memo),
387
399
  versioningIntent: versioningIntentToProto(options.versioningIntent),
400
+ priority: options.priority ? compilePriority(options.priority) : undefined,
388
401
  },
389
402
  });
390
403
  activator.completions.childWorkflowStart.set(seq, {
@@ -922,9 +935,10 @@ export function makeContinueAsNewFunc<F extends Workflow>(
922
935
  headers,
923
936
  taskQueue: options.taskQueue,
924
937
  memo: options.memo && mapToPayloads(activator.payloadConverter, options.memo),
925
- searchAttributes: options.searchAttributes
926
- ? mapToPayloads(searchAttributePayloadConverter, options.searchAttributes)
927
- : undefined,
938
+ searchAttributes:
939
+ options.searchAttributes || options.typedSearchAttributes // eslint-disable-line deprecation/deprecation
940
+ ? encodeUnifiedSearchAttributes(options.searchAttributes, options.typedSearchAttributes) // eslint-disable-line deprecation/deprecation
941
+ : undefined,
928
942
  workflowRunTimeout: msOptionalToTs(options.workflowRunTimeout),
929
943
  workflowTaskTimeout: msOptionalToTs(options.workflowTaskTimeout),
930
944
  versioningIntent: versioningIntentToProto(options.versioningIntent),
@@ -946,14 +960,15 @@ export function makeContinueAsNewFunc<F extends Workflow>(
946
960
  *
947
961
  * @example
948
962
  *
949
- *```ts
963
+ * ```ts
950
964
  *import { continueAsNew } from '@temporalio/workflow';
965
+ import { SearchAttributeType } from '@temporalio/common';
951
966
  *
952
967
  *export async function myWorkflow(n: number): Promise<void> {
953
968
  * // ... Workflow logic
954
969
  * await continueAsNew<typeof myWorkflow>(n + 1);
955
970
  *}
956
- *```
971
+ * ```
957
972
  */
958
973
  export function continueAsNew<F extends Workflow>(...args: Parameters<F>): Promise<never> {
959
974
  return makeContinueAsNewFunc()(...args);
@@ -1300,7 +1315,7 @@ export function setHandler<
1300
1315
  *
1301
1316
  * Signals are dispatched to the default signal handler in the order that they were accepted by the server.
1302
1317
  *
1303
- * If this function is called multiple times for a given signal or query name the last handler will overwrite any previous calls.
1318
+ * If this function is called multiple times for a given signal name the last handler will overwrite any previous calls.
1304
1319
  *
1305
1320
  * @param handler a function that will handle signals for non-registered signal names, or `undefined` to unset the handler.
1306
1321
  */
@@ -1318,24 +1333,73 @@ export function setDefaultSignalHandler(handler: DefaultSignalHandler | undefine
1318
1333
  }
1319
1334
  }
1320
1335
 
1336
+ /**
1337
+ * Set a update handler function that will handle updates calls for non-registered update names.
1338
+ *
1339
+ * Updates are dispatched to the default update handler in the order that they were accepted by the server.
1340
+ *
1341
+ * If this function is called multiple times for a given update name the last handler will overwrite any previous calls.
1342
+ *
1343
+ * @param handler a function that will handle updates for non-registered update names, or `undefined` to unset the handler.
1344
+ */
1345
+ export function setDefaultUpdateHandler(handler: DefaultUpdateHandler | undefined): void {
1346
+ const activator = assertInWorkflowContext(
1347
+ 'Workflow.setDefaultUpdateHandler(...) may only be used from a Workflow Execution.'
1348
+ );
1349
+ if (typeof handler === 'function') {
1350
+ activator.defaultUpdateHandler = handler;
1351
+ activator.dispatchBufferedUpdates();
1352
+ } else if (handler == null) {
1353
+ activator.defaultUpdateHandler = undefined;
1354
+ } else {
1355
+ throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
1356
+ }
1357
+ }
1358
+
1359
+ /**
1360
+ * Set a query handler function that will handle query calls for non-registered query names.
1361
+ *
1362
+ * Queries are dispatched to the default query handler in the order that they were accepted by the server.
1363
+ *
1364
+ * If this function is called multiple times for a given query name the last handler will overwrite any previous calls.
1365
+ *
1366
+ * @param handler a function that will handle queries for non-registered query names, or `undefined` to unset the handler.
1367
+ */
1368
+ export function setDefaultQueryHandler(handler: DefaultQueryHandler | undefined): void {
1369
+ const activator = assertInWorkflowContext(
1370
+ 'Workflow.setDefaultQueryHandler(...) may only be used from a Workflow Execution.'
1371
+ );
1372
+ if (typeof handler === 'function' || handler === undefined) {
1373
+ activator.defaultQueryHandler = handler;
1374
+ } else {
1375
+ throw new TypeError(`Expected handler to be either a function or 'undefined'. Got: '${typeof handler}'`);
1376
+ }
1377
+ }
1378
+
1321
1379
  /**
1322
1380
  * Updates this Workflow's Search Attributes by merging the provided `searchAttributes` with the existing Search
1323
1381
  * Attributes, `workflowInfo().searchAttributes`.
1324
1382
  *
1325
- * For example, this Workflow code:
1383
+ * Search attributes can be upserted using either SearchAttributes (deprecated) or SearchAttributeUpdatePair[] (preferred)
1384
+ *
1385
+ * Upserting a workflow's search attributes using SearchAttributeUpdatePair[]:
1326
1386
  *
1327
1387
  * ```ts
1328
- * upsertSearchAttributes({
1329
- * CustomIntField: [1],
1330
- * CustomBoolField: [true]
1331
- * });
1332
- * upsertSearchAttributes({
1333
- * CustomIntField: [42],
1334
- * CustomKeywordField: ['durable code', 'is great']
1335
- * });
1388
+ * const intKey = defineSearchKey('CustomIntField', 'INT');
1389
+ * const boolKey = defineSearchKey('CustomBoolField', 'BOOL');
1390
+ * const keywordListKey = defineSearchKey('CustomKeywordField', 'KEYWORD_LIST');
1391
+ *
1392
+ * upsertSearchAttributes([
1393
+ * defineSearchAttribute(intKey, 1),
1394
+ * defineSearchAttribute(boolKey, true)
1395
+ * ]);
1396
+ * upsertSearchAttributes([
1397
+ * defineSearchAttribute(intKey, 42),
1398
+ * defineSearchAttribute(keywordListKey, ['durable code', 'is great'])
1399
+ * ]);
1336
1400
  * ```
1337
1401
  *
1338
- * would result in the Workflow having these Search Attributes:
1402
+ * Would result in the Workflow having these Search Attributes:
1339
1403
  *
1340
1404
  * ```ts
1341
1405
  * {
@@ -1345,9 +1409,12 @@ export function setDefaultSignalHandler(handler: DefaultSignalHandler | undefine
1345
1409
  * }
1346
1410
  * ```
1347
1411
  *
1348
- * @param searchAttributes The Record to merge. Use a value of `[]` to clear a Search Attribute.
1412
+ * @param searchAttributes The Record to merge.
1413
+ * If using SearchAttributeUpdatePair[] (preferred), set a value to null to remove the search attribute.
1414
+ * If using SearchAttributes (deprecated), set a value to undefined or an empty list to remove the search attribute.
1349
1415
  */
1350
- export function upsertSearchAttributes(searchAttributes: SearchAttributes): void {
1416
+ // eslint-disable-next-line deprecation/deprecation
1417
+ export function upsertSearchAttributes(searchAttributes: SearchAttributes | SearchAttributeUpdatePair[]): void {
1351
1418
  const activator = assertInWorkflowContext(
1352
1419
  'Workflow.upsertSearchAttributes(...) may only be used from a Workflow Execution.'
1353
1420
  );
@@ -1356,21 +1423,111 @@ export function upsertSearchAttributes(searchAttributes: SearchAttributes): void
1356
1423
  throw new Error('searchAttributes must be a non-null SearchAttributes');
1357
1424
  }
1358
1425
 
1359
- activator.pushCommand({
1360
- upsertWorkflowSearchAttributes: {
1361
- searchAttributes: mapToPayloads(searchAttributePayloadConverter, searchAttributes),
1362
- },
1363
- });
1426
+ if (Array.isArray(searchAttributes)) {
1427
+ // Typed search attributes
1428
+ activator.pushCommand({
1429
+ upsertWorkflowSearchAttributes: {
1430
+ searchAttributes: encodeUnifiedSearchAttributes(undefined, searchAttributes),
1431
+ },
1432
+ });
1364
1433
 
1365
- activator.mutateWorkflowInfo((info: WorkflowInfo): WorkflowInfo => {
1366
- return {
1367
- ...info,
1368
- searchAttributes: {
1369
- ...info.searchAttributes,
1370
- ...searchAttributes,
1434
+ activator.mutateWorkflowInfo((info: WorkflowInfo): WorkflowInfo => {
1435
+ // Create a copy of the current state.
1436
+ const newSearchAttributes: SearchAttributes = { ...info.searchAttributes }; // eslint-disable-line deprecation/deprecation
1437
+ for (const pair of searchAttributes) {
1438
+ if (pair.value == null) {
1439
+ // If the value is null, remove the search attribute.
1440
+ // We don't mutate the existing state (just the new map) so this is safe.
1441
+ delete newSearchAttributes[pair.key.name];
1442
+ } else {
1443
+ newSearchAttributes[pair.key.name] = Array.isArray(pair.value)
1444
+ ? pair.value
1445
+ : ([pair.value] as SearchAttributeValue); // eslint-disable-line deprecation/deprecation
1446
+ }
1447
+ }
1448
+ return {
1449
+ ...info,
1450
+ searchAttributes: newSearchAttributes,
1451
+ // Create an empty copy and apply existing and new updates. Keep in mind the order matters here (existing first, new second - to possibly overwrite existing).
1452
+ typedSearchAttributes: info.typedSearchAttributes.updateCopy([...searchAttributes]),
1453
+ };
1454
+ });
1455
+ } else {
1456
+ // Legacy search attributes
1457
+ activator.pushCommand({
1458
+ upsertWorkflowSearchAttributes: {
1459
+ searchAttributes: mapToPayloads(searchAttributePayloadConverter, searchAttributes),
1371
1460
  },
1372
- };
1373
- });
1461
+ });
1462
+
1463
+ activator.mutateWorkflowInfo((info: WorkflowInfo): WorkflowInfo => {
1464
+ // Create a new copy of the current state.
1465
+ let typedSearchAttributes = info.typedSearchAttributes.updateCopy([]);
1466
+ const newSearchAttributes: SearchAttributes = { ...info.searchAttributes }; // eslint-disable-line deprecation/deprecation
1467
+
1468
+ // Upsert legacy search attributes into typedSearchAttributes.
1469
+ for (const [k, v] of Object.entries(searchAttributes)) {
1470
+ if (v !== undefined && !Array.isArray(v)) {
1471
+ throw new Error(`Search attribute value must be an array or undefined, got ${v}`);
1472
+ }
1473
+
1474
+ // The value is undefined or an empty list, this signifies deletion.
1475
+ // Remove from both untyped & typed search attributes.
1476
+ if (v == null || (Array.isArray(v) && v.length === 0)) {
1477
+ // We cannot discern a valid key typing from these values.
1478
+ // Instead, we do a "best effort" deletion from typed search attributes:
1479
+ // - check if a matching key name exists, if so, remove it.
1480
+ const matchingPair = typedSearchAttributes.getAll().find((pair) => pair.key.name === k);
1481
+ if (matchingPair) {
1482
+ typedSearchAttributes = typedSearchAttributes.updateCopy([
1483
+ { key: matchingPair.key, value: null } as SearchAttributeUpdatePair,
1484
+ ]);
1485
+ }
1486
+ delete newSearchAttributes[k];
1487
+ continue;
1488
+ }
1489
+
1490
+ // Attempt to discern a valid key typing for the update.
1491
+ const typedKey = TypedSearchAttributes.getKeyFromUntyped(k, v);
1492
+
1493
+ // Unable to discern a valid key typing (no valid type for defined value).
1494
+ // Skip applying this update (no-op).
1495
+ if (typedKey === undefined) {
1496
+ continue;
1497
+ }
1498
+
1499
+ // TEXT type is inferred from a string value, but it could also be KEYWORD.
1500
+ // If a matching pair exists with KEYWORD type, use that instead.
1501
+ if (typedKey.type === 'TEXT') {
1502
+ const matchingPair = typedSearchAttributes.getAll().find((pair) => pair.key.name === typedKey.name);
1503
+ if (matchingPair) {
1504
+ typedKey.type = matchingPair.key.type;
1505
+ }
1506
+ }
1507
+
1508
+ let newValue: unknown = v;
1509
+ // Unpack value if it is a single-element array.
1510
+ if (v.length === 1) {
1511
+ newValue = v[0];
1512
+ // Convert value back to Date.
1513
+ if (typedKey.type === 'DATETIME') {
1514
+ newValue = new Date(newValue as string);
1515
+ }
1516
+ }
1517
+
1518
+ // We have a defined value with valid type. Apply the update.
1519
+ typedSearchAttributes = typedSearchAttributes.updateCopy([
1520
+ { key: typedKey, value: newValue } as SearchAttributeUpdatePair,
1521
+ ]);
1522
+ newSearchAttributes[k] = v;
1523
+ }
1524
+ return {
1525
+ ...info,
1526
+ searchAttributes: newSearchAttributes,
1527
+ typedSearchAttributes,
1528
+ };
1529
+ });
1530
+ }
1374
1531
  }
1375
1532
 
1376
1533
  /**
@@ -1458,6 +1615,43 @@ export function allHandlersFinished(): boolean {
1458
1615
  return activator.inProgressSignals.size === 0 && activator.inProgressUpdates.size === 0;
1459
1616
  }
1460
1617
 
1618
+ /**
1619
+ * Can be used to alter workflow functions with certain options specified at definition time.
1620
+ *
1621
+ * @example
1622
+ * For example:
1623
+ * ```ts
1624
+ * setWorkflowOptions({ versioningBehavior: 'PINNED' }, myWorkflow);
1625
+ * export async function myWorkflow(): Promise<string> {
1626
+ * // Workflow code here
1627
+ * return "hi";
1628
+ * }
1629
+ * ```
1630
+ *
1631
+ * @example
1632
+ * To annotate a default or dynamic workflow:
1633
+ * ```ts
1634
+ * export default async function (): Promise<string> {
1635
+ * // Workflow code here
1636
+ * return "hi";
1637
+ * }
1638
+ * setWorkflowOptions({ versioningBehavior: 'PINNED' }, module.exports.default);
1639
+ * ```
1640
+ *
1641
+ * @param options Options for the workflow defintion, or a function that returns options. If a
1642
+ * function is provided, it will be called once just before the workflow function is called for the
1643
+ * first time. It is safe to call {@link workflowInfo} inside such a function.
1644
+ * @param fn The workflow function.
1645
+ */
1646
+ export function setWorkflowOptions<A extends any[], RT>(
1647
+ options: WorkflowDefinitionOptionsOrGetter,
1648
+ fn: (...args: A) => Promise<RT>
1649
+ ): void {
1650
+ Object.assign(fn, {
1651
+ workflowDefinitionOptions: options,
1652
+ });
1653
+ }
1654
+
1461
1655
  export const stackTraceQuery = defineQuery<string>('__stack_trace');
1462
1656
  export const enhancedStackTraceQuery = defineQuery<EnhancedStackTrace>('__enhanced_stack_trace');
1463
1657
  export const workflowMetadataQuery = defineQuery<temporal.api.sdk.v1.IWorkflowMetadata>('__temporal_workflow_metadata');