ts2workflows 0.4.0 → 0.6.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.
@@ -1,7 +1,7 @@
1
- import { AssignStepAST, CallStepAST, ForStepAST, RaiseStepAST, ReturnStepAST, SwitchStepAST, TryStepAST, } from '../ast/steps.js';
1
+ import { AssignStepAST, CallStepAST, ForStepAST, RaiseStepAST, ReturnStepAST, SwitchStepAST, } from '../ast/steps.js';
2
2
  import { InternalTranspilingError } from '../errors.js';
3
- import { isRecord } from '../utils.js';
4
- import { BinaryExpression, FunctionInvocationExpression, MemberExpression, PrimitiveExpression, UnaryExpression, VariableReferenceExpression, expressionToLiteralValueOrLiteralExpression, isExpression, isFullyQualifiedName, isLiteral, } from '../ast/expressions.js';
3
+ import { isRecord, mapRecordValues } from '../utils.js';
4
+ import { BinaryExpression, FunctionInvocationExpression, MemberExpression, PrimitiveExpression, UnaryExpression, VariableReferenceExpression, isExpression, isLiteral, } from '../ast/expressions.js';
5
5
  import { blockingFunctions } from './generated/functionMetadata.js';
6
6
  const Unmodified = Symbol();
7
7
  /**
@@ -11,7 +11,7 @@ const Unmodified = Symbol();
11
11
  * called on each nesting level separately.
12
12
  */
13
13
  export function transformAST(steps) {
14
- return blockingCallsAsCallSteps(runtimeFunctionImplementation(flattenPlainNextConditions(combineRetryBlocksToTry(mergeAssignSteps(mapLiteralsAsAssignSteps(steps))))));
14
+ return blockingCallsAsCallSteps(runtimeFunctionImplementation(flattenPlainNextConditions(mergeAssignSteps(mapLiteralsAsAssignSteps(steps)))));
15
15
  }
16
16
  /**
17
17
  * Merge consecutive assign steps into one assign step
@@ -25,7 +25,7 @@ function mergeAssignSteps(steps) {
25
25
  prev?.tag === 'assign' &&
26
26
  prev.assignments.length < 50 &&
27
27
  !prev.next) {
28
- const merged = new AssignStepAST(prev.assignments.concat(current.assignments), current.next);
28
+ const merged = new AssignStepAST(prev.assignments.concat(current.assignments), current.next, prev.label ?? current.label);
29
29
  acc.pop();
30
30
  acc.push(merged);
31
31
  }
@@ -35,95 +35,6 @@ function mergeAssignSteps(steps) {
35
35
  return acc;
36
36
  }, []);
37
37
  }
38
- /**
39
- * Transform a retry_policy call step to a retry block in a preceeding try step
40
- */
41
- function combineRetryBlocksToTry(steps) {
42
- return steps.reduce((acc, current) => {
43
- const prev = acc.length > 0 ? acc[acc.length - 1] : null;
44
- if (current.tag === 'call' && current.call === 'retry_policy') {
45
- if (prev?.tag === 'try') {
46
- if (prev.retryPolicy) {
47
- throw new InternalTranspilingError('Retry policy already assigned!');
48
- }
49
- let retryPolicy = undefined;
50
- const retryParameters = current.args;
51
- if (retryParameters) {
52
- const retryPolicyEx = retryParameters.policy;
53
- if (retryPolicyEx && isFullyQualifiedName(retryPolicyEx)) {
54
- retryPolicy = retryPolicyEx.toString();
55
- }
56
- if (!retryPolicy) {
57
- let predicate = '';
58
- const predicateEx = retryParameters.predicate;
59
- if (predicateEx) {
60
- if (isFullyQualifiedName(predicateEx)) {
61
- predicate = predicateEx.toString();
62
- }
63
- else {
64
- throw new InternalTranspilingError('"predicate" must be a function name');
65
- }
66
- }
67
- const maxRetries = parseRetryPolicyNumber(retryParameters, 'max_retries');
68
- let initialDelay = 1;
69
- let maxDelay = 1;
70
- let multiplier = 1;
71
- const backoffEx = retryParameters.backoff;
72
- if (backoffEx &&
73
- isLiteral(backoffEx) &&
74
- backoffEx.expressionType === 'primitive') {
75
- const backoffLit = backoffEx.value;
76
- if (isRecord(backoffLit)) {
77
- initialDelay = parseRetryPolicyNumber(backoffLit, 'initial_delay');
78
- maxDelay = parseRetryPolicyNumber(backoffLit, 'max_delay');
79
- multiplier = parseRetryPolicyNumber(backoffLit, 'multiplier');
80
- }
81
- }
82
- retryPolicy = {
83
- predicate,
84
- maxRetries,
85
- backoff: {
86
- initialDelay,
87
- maxDelay,
88
- multiplier,
89
- },
90
- };
91
- }
92
- }
93
- const tryWithRetry = new TryStepAST(prev.trySteps, prev.exceptSteps, retryPolicy, prev.errorMap);
94
- acc.pop();
95
- acc.push(tryWithRetry);
96
- }
97
- // If prev is not a try step, "retry_policy" is ignored. Should print a warning.
98
- }
99
- else {
100
- acc.push(current);
101
- }
102
- return acc;
103
- }, []);
104
- }
105
- function parseRetryPolicyNumber(record, keyName) {
106
- let primitiveValue;
107
- const primitiveOrExpression = record[keyName];
108
- if (primitiveOrExpression && isExpression(primitiveOrExpression)) {
109
- if (isLiteral(primitiveOrExpression)) {
110
- primitiveValue = expressionToLiteralValueOrLiteralExpression(primitiveOrExpression);
111
- }
112
- else {
113
- throw new InternalTranspilingError(`Support for non-literal "${keyName}" values not yet implemented`);
114
- }
115
- }
116
- else if (primitiveOrExpression) {
117
- primitiveValue = primitiveOrExpression;
118
- }
119
- else {
120
- throw new InternalTranspilingError(`"${keyName}" expected`);
121
- }
122
- if (typeof primitiveValue !== 'number') {
123
- throw new InternalTranspilingError(`"${keyName}" must be a number`);
124
- }
125
- return primitiveValue;
126
- }
127
38
  /**
128
39
  * Merge a next step to the previous step.
129
40
  *
@@ -301,11 +212,11 @@ function transformExpressionsAssign(step, transform) {
301
212
  function transformExpressionsCall(step, transform) {
302
213
  if (step.args) {
303
214
  const newSteps = [];
304
- const newArgs = Object.fromEntries(Object.entries(step.args).map(([name, ex]) => {
215
+ const newArgs = mapRecordValues(step.args, (ex) => {
305
216
  const [steps2, ex2] = transform(ex);
306
217
  newSteps.push(...steps2);
307
- return [name, ex2];
308
- }));
218
+ return ex2;
219
+ });
309
220
  newSteps.push(new CallStepAST(step.call, newArgs, step.result, step.label));
310
221
  return newSteps;
311
222
  }
@@ -357,8 +268,8 @@ function transformExpressionsSwitch(step, transform) {
357
268
  newSteps.push(new SwitchStepAST(newBranches, step.label));
358
269
  return newSteps;
359
270
  }
360
- function transformExpression(ex, transformer) {
361
- const transformed = transformer(ex);
271
+ function transformExpression(ex, transform) {
272
+ const transformed = transform(ex);
362
273
  if (transformed !== Unmodified) {
363
274
  // Use the transformed version of this term
364
275
  return transformed;
@@ -367,28 +278,76 @@ function transformExpression(ex, transformer) {
367
278
  // Otherwise, recurse into the nested expression
368
279
  switch (ex.expressionType) {
369
280
  case 'primitive':
370
- case 'variableReference':
371
- return ex;
281
+ if (isLiteral(ex)) {
282
+ return ex;
283
+ }
284
+ else {
285
+ const newPrimitive = transformPrimitive(ex.value, transform);
286
+ return newPrimitive === ex.value
287
+ ? ex
288
+ : new PrimitiveExpression(newPrimitive);
289
+ }
372
290
  case 'binary':
373
- return transformBinaryExpression(ex, transformer);
291
+ return transformBinaryExpression(ex, transform);
374
292
  case 'functionInvocation':
375
- return transformFunctionInvocationExpression(ex, transformer);
293
+ return transformFunctionInvocationExpression(ex, transform);
376
294
  case 'member':
377
- return new MemberExpression(transformExpression(ex.object, transformer), transformExpression(ex.property, transformer), ex.computed);
295
+ return transformMemberExpression(ex, transform);
378
296
  case 'unary':
379
- return new UnaryExpression(ex.operator, transformExpression(ex.value, transformer));
297
+ return transformUnaryExpression(ex, transform);
298
+ case 'variableReference':
299
+ return ex;
380
300
  }
381
301
  }
382
302
  }
383
- function transformBinaryExpression(ex, transformer) {
303
+ function transformPrimitive(val, transform) {
304
+ if (Array.isArray(val)) {
305
+ return val.map((x) => isExpression(x)
306
+ ? transformExpression(x, transform)
307
+ : transformPrimitive(x, transform));
308
+ }
309
+ else if (isRecord(val)) {
310
+ return mapRecordValues(val, (x) => isExpression(x)
311
+ ? transformExpression(x, transform)
312
+ : transformPrimitive(x, transform));
313
+ }
314
+ else {
315
+ return val;
316
+ }
317
+ }
318
+ function transformBinaryExpression(ex, transform) {
384
319
  // Transform left first to keep the correct order of execution of sub-expressions
385
- const newLeft = transformExpression(ex.left, transformer);
386
- const newRight = transformExpression(ex.right, transformer);
387
- return new BinaryExpression(newLeft, ex.binaryOperator, newRight);
320
+ const newLeft = transformExpression(ex.left, transform);
321
+ const newRight = transformExpression(ex.right, transform);
322
+ if (newLeft === ex.left && newRight === ex.right) {
323
+ return ex;
324
+ }
325
+ else {
326
+ return new BinaryExpression(newLeft, ex.binaryOperator, newRight);
327
+ }
388
328
  }
389
- function transformFunctionInvocationExpression(ex, transformer) {
390
- const newArguments = ex.arguments.map((x) => transformExpression(x, transformer));
391
- return new FunctionInvocationExpression(ex.functionName, newArguments);
329
+ function transformFunctionInvocationExpression(ex, transform) {
330
+ const newArguments = ex.arguments.map((x) => transformExpression(x, transform));
331
+ if (newArguments.every((x, i) => x === ex.arguments[i])) {
332
+ return ex;
333
+ }
334
+ else {
335
+ return new FunctionInvocationExpression(ex.functionName, newArguments);
336
+ }
337
+ }
338
+ function transformMemberExpression(ex, transform) {
339
+ const newObject = transformExpression(ex.object, transform);
340
+ const newProperty = transformExpression(ex.property, transform);
341
+ if (newObject === ex.object && newProperty === ex.property) {
342
+ return ex;
343
+ }
344
+ else {
345
+ return new MemberExpression(newObject, newProperty, ex.computed);
346
+ }
347
+ }
348
+ function transformUnaryExpression(ex, transform) {
349
+ const newValue = transformExpression(ex.value, transform);
350
+ return newValue === ex.value ? ex : new UnaryExpression(ex.operator, newValue);
392
351
  }
393
352
  /**
394
353
  * Search for map literals in expressions and replace them with assign step + variable.
@@ -410,89 +369,143 @@ function transformFunctionInvocationExpression(ex, transformer) {
410
369
  * return: ${__temp0.value}
411
370
  */
412
371
  function mapLiteralsAsAssignSteps(steps) {
413
- function transformer(ex) {
414
- const generateTemporaryVariableName = createTempVariableGenerator();
415
- const { transformedExpression, assignSteps } = replaceMapLiterals(ex, generateTemporaryVariableName);
416
- return [assignSteps, transformedExpression];
417
- }
418
- return steps.reduce((acc, current) => {
419
- let needsTransformation = true;
420
- // These steps are allowed to contain map literals if the map is the
421
- // main expression
422
- if (current.tag === 'assign') {
423
- // This does the transformation a bit too eagerly: If any of the
424
- // assignments need the map literal extraction, it is done on all of
425
- // the variables.
426
- needsTransformation = !current.assignments.every(([, value]) => {
427
- return value.expressionType === 'primitive';
428
- });
429
- }
430
- else if (current.tag === 'raise' || current.tag === 'return') {
431
- needsTransformation =
432
- current.value !== undefined &&
433
- includesExtractableMapLiteral(current.value, true);
434
- }
435
- else if (current.tag === 'call') {
436
- if (current.args) {
437
- needsTransformation = Object.values(current.args).some((ex) => includesExtractableMapLiteral(ex, true));
438
- }
439
- }
440
- if (needsTransformation) {
441
- const transformedSteps = transformStepExpressions(current, transformer);
442
- acc.push(...transformedSteps);
443
- }
444
- else {
445
- acc.push(current);
446
- }
447
- return acc;
448
- }, []);
372
+ return steps.flatMap((step) => transformStepExpressions(step, transformNestedMaps));
373
+ }
374
+ function transformNestedMaps(ex) {
375
+ const generateTemporaryVariableName = createTempVariableGenerator();
376
+ const { transformedExpression, tempVariables } = extractNestedMaps(ex, generateTemporaryVariableName, 0);
377
+ const assignments = tempVariables.length > 0 ? [new AssignStepAST(tempVariables)] : [];
378
+ return [assignments, transformedExpression];
449
379
  }
450
- // Return true if the string representation of ex would include {}
451
- function includesExtractableMapLiteral(ex, parentAllowsMaps) {
380
+ function extractNestedMaps(ex, generateName, nestingLevel) {
452
381
  switch (ex.expressionType) {
453
382
  case 'primitive':
454
- if (isRecord(ex.value)) {
455
- return (!parentAllowsMaps ||
456
- Object.values(ex.value).some((x) => isExpression(x) &&
457
- includesExtractableMapLiteral(x, parentAllowsMaps)));
458
- }
459
- else if (Array.isArray(ex.value)) {
460
- return ex.value.some((x) => isExpression(x) &&
461
- includesExtractableMapLiteral(x, parentAllowsMaps));
462
- }
463
- else {
464
- return false;
465
- }
383
+ return extractNestedMapPrimitive(ex.value, generateName, nestingLevel);
466
384
  case 'binary':
467
- return (includesExtractableMapLiteral(ex.left, parentAllowsMaps) ||
468
- includesExtractableMapLiteral(ex.right, parentAllowsMaps));
385
+ return extractNestedMapBinary(ex, generateName, nestingLevel);
469
386
  case 'variableReference':
470
- return false;
471
- case 'unary':
472
- return includesExtractableMapLiteral(ex.value, parentAllowsMaps);
387
+ return { transformedExpression: ex, tempVariables: [] };
473
388
  case 'functionInvocation':
474
- return ex.arguments.some((x) => includesExtractableMapLiteral(x, false));
389
+ return extractNestedMapFunctionInvocation(ex, generateName, nestingLevel);
475
390
  case 'member':
476
- return (includesExtractableMapLiteral(ex.object, false) ||
477
- includesExtractableMapLiteral(ex.property, false));
391
+ return extractNestedMapMember(ex, generateName, nestingLevel);
392
+ case 'unary':
393
+ return extractNestedMapUnary(ex, generateName, nestingLevel);
394
+ }
395
+ }
396
+ function extractNestedMapPrimitive(primitiveEx, generateName, nestingLevel) {
397
+ const { transformed, tempVariables } = extractNestedMapPrimitiveRecursive(primitiveEx, generateName, nestingLevel);
398
+ const ex = isExpression(transformed)
399
+ ? transformed
400
+ : new PrimitiveExpression(transformed);
401
+ return {
402
+ transformedExpression: ex,
403
+ tempVariables,
404
+ };
405
+ }
406
+ function extractNestedMapPrimitiveRecursive(primitiveEx, generateName, nestingLevel) {
407
+ if (typeof primitiveEx === 'string' ||
408
+ typeof primitiveEx === 'number' ||
409
+ typeof primitiveEx === 'boolean' ||
410
+ primitiveEx === null) {
411
+ return {
412
+ transformed: primitiveEx,
413
+ tempVariables: [],
414
+ };
415
+ }
416
+ else if (Array.isArray(primitiveEx)) {
417
+ return extractMapsInList(primitiveEx, generateName, nestingLevel);
418
+ }
419
+ else {
420
+ return extractMapsInMap(primitiveEx, generateName, nestingLevel);
478
421
  }
479
422
  }
480
- function replaceMapLiterals(expression, generateName) {
481
- function replace(ex) {
482
- if (ex.expressionType === 'primitive' && isRecord(ex.value)) {
483
- const tempVariable = generateName();
484
- assignSteps.push(new AssignStepAST([[tempVariable, new PrimitiveExpression(ex.value)]]));
485
- // replace the map literal with a reference to the temporary variable
486
- return new VariableReferenceExpression(tempVariable);
423
+ function extractMapsInList(primitiveEx, generateName, nestingLevel) {
424
+ const { tempVariables, elements } = primitiveEx.reduce((acc, val) => {
425
+ if (isExpression(val)) {
426
+ const { transformedExpression, tempVariables: temps } = extractNestedMaps(val, generateName, nestingLevel);
427
+ acc.tempVariables.push(...temps);
428
+ acc.elements.push(transformedExpression);
487
429
  }
488
430
  else {
489
- return Unmodified;
431
+ const { transformed, tempVariables: temps } = extractNestedMapPrimitiveRecursive(val, generateName, nestingLevel);
432
+ acc.tempVariables.push(...temps);
433
+ acc.elements.push(transformed);
434
+ }
435
+ return acc;
436
+ }, {
437
+ tempVariables: [],
438
+ elements: [],
439
+ });
440
+ return {
441
+ tempVariables,
442
+ transformed: elements,
443
+ };
444
+ }
445
+ function extractMapsInMap(primitiveEx, generateName, nestingLevel) {
446
+ const { tempVariables, properties } = Object.entries(primitiveEx).reduce((acc, [key, val]) => {
447
+ if (isExpression(val)) {
448
+ const { transformedExpression, tempVariables: temps } = extractNestedMaps(val, generateName, 0);
449
+ acc.tempVariables.push(...temps);
450
+ acc.properties[key] = transformedExpression;
490
451
  }
452
+ else {
453
+ const { transformed, tempVariables: temps } = extractNestedMapPrimitiveRecursive(val, generateName, 0);
454
+ acc.tempVariables.push(...temps);
455
+ acc.properties[key] = transformed;
456
+ }
457
+ return acc;
458
+ }, {
459
+ tempVariables: [],
460
+ properties: {},
461
+ });
462
+ let newValue;
463
+ if (nestingLevel === 0) {
464
+ newValue = properties;
465
+ }
466
+ else {
467
+ const name = generateName();
468
+ tempVariables.push([name, new PrimitiveExpression(properties)]);
469
+ newValue = new VariableReferenceExpression(name);
491
470
  }
492
- const assignSteps = [];
493
471
  return {
494
- transformedExpression: transformExpression(expression, replace),
495
- assignSteps,
472
+ transformed: newValue,
473
+ tempVariables,
474
+ };
475
+ }
476
+ function extractNestedMapFunctionInvocation(ex, generateName, nestingLevel) {
477
+ const { expressions, temps } = ex.arguments.reduce((acc, arg) => {
478
+ const { transformedExpression, tempVariables } = extractNestedMaps(arg, generateName, nestingLevel + 1);
479
+ acc.expressions.push(transformedExpression);
480
+ acc.temps.push(...tempVariables);
481
+ return acc;
482
+ }, { expressions: [], temps: [] });
483
+ return {
484
+ transformedExpression: new FunctionInvocationExpression(ex.functionName, expressions),
485
+ tempVariables: temps,
486
+ };
487
+ }
488
+ function extractNestedMapBinary(ex, generateName, nestingLevel) {
489
+ const left = extractNestedMaps(ex.left, generateName, nestingLevel + 1);
490
+ const right = extractNestedMaps(ex.right, generateName, nestingLevel + 1);
491
+ return {
492
+ transformedExpression: new BinaryExpression(left.transformedExpression, ex.binaryOperator, right.transformedExpression),
493
+ tempVariables: left.tempVariables.concat(right.tempVariables),
494
+ };
495
+ }
496
+ function extractNestedMapMember(ex, generateName, nestingLevel) {
497
+ const obj = extractNestedMaps(ex.object, generateName, nestingLevel + 1);
498
+ const pr = extractNestedMaps(ex.property, generateName, nestingLevel + 1);
499
+ return {
500
+ transformedExpression: new MemberExpression(obj.transformedExpression, pr.transformedExpression, ex.computed),
501
+ tempVariables: obj.tempVariables.concat(pr.tempVariables),
502
+ };
503
+ }
504
+ function extractNestedMapUnary(ex, generateName, nestingLevel) {
505
+ const { transformedExpression, tempVariables } = extractNestedMaps(ex.value, generateName, nestingLevel);
506
+ return {
507
+ transformedExpression: new UnaryExpression(ex.operator, transformedExpression),
508
+ tempVariables,
496
509
  };
497
510
  }
498
511
  /**
package/dist/utils.d.ts CHANGED
@@ -1,2 +1,13 @@
1
1
  export declare function isRecord(object: unknown): object is Record<keyof never, unknown>;
2
+ /**
3
+ * Apply f to values of obj and return the result
4
+ */
5
+ export declare function mapRecordValues<T, U>(obj: Record<string, T>, f: (t: T) => U): Record<string, U>;
6
+ /**
7
+ * Like arr.flatMap() but the callback takes two consecutive array elements.
8
+ *
9
+ * During the last execution of the callback, the second argument (which would
10
+ * be element after the last array element) will be undefined.
11
+ */
12
+ export declare function flatMapPair<T, U>(arr: T[], callback: (val: T, next: T | undefined) => U[]): U[];
2
13
  //# sourceMappingURL=utils.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CACtB,MAAM,EAAE,OAAO,GACd,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,OAAO,CAAC,CAExC"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,QAAQ,CACtB,MAAM,EAAE,OAAO,GACd,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,EAAE,OAAO,CAAC,CAExC;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,CAAC,EAClC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EACtB,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GACb,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAEnB;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,CAAC,EAC9B,GAAG,EAAE,CAAC,EAAE,EACR,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,GAAG,SAAS,KAAK,CAAC,EAAE,GAC7C,CAAC,EAAE,CAaL"}
package/dist/utils.js CHANGED
@@ -1,3 +1,26 @@
1
1
  export function isRecord(object) {
2
2
  return object instanceof Object && object.constructor === Object;
3
3
  }
4
+ /**
5
+ * Apply f to values of obj and return the result
6
+ */
7
+ export function mapRecordValues(obj, f) {
8
+ return Object.fromEntries(Object.entries(obj).map(([k, v]) => [k, f(v)]));
9
+ }
10
+ /**
11
+ * Like arr.flatMap() but the callback takes two consecutive array elements.
12
+ *
13
+ * During the last execution of the callback, the second argument (which would
14
+ * be element after the last array element) will be undefined.
15
+ */
16
+ export function flatMapPair(arr, callback) {
17
+ if (arr.length <= 0) {
18
+ return [];
19
+ }
20
+ const mapped = [];
21
+ for (let i = 0; i < arr.length - 1; i++) {
22
+ mapped.push(...callback(arr[i], arr[i + 1]));
23
+ }
24
+ mapped.push(...callback(arr[arr.length - 1], undefined));
25
+ return mapped;
26
+ }
@@ -561,7 +561,7 @@ parallel(
561
561
  )
562
562
  ```
563
563
 
564
- ## Try/catch statements
564
+ ## Try/catch/finally statements
565
565
 
566
566
  The statement
567
567
 
@@ -592,13 +592,48 @@ is compiled to the following [try/except structure](https://cloud.google.com/wor
592
592
 
593
593
  The error variable and other variables created inside the catch block are accessible only in that block's scope (similar to [the variable scoping in Workflows](https://cloud.google.com/workflows/docs/reference/syntax/catching-errors#variable-scope)).
594
594
 
595
+ Finally block is also supported:
596
+
597
+ ```javascript
598
+ try {
599
+ return readFile()
600
+ } catch (err) {
601
+ return 'Error!'
602
+ } finally {
603
+ closeFile()
604
+ }
605
+ ```
606
+
607
+ If an exception gets thrown inside a try block, the stack trace in Workflows logs will misleadingly show the exception originating from inside the finally block. This happens because the implementation of the finally block catches the original exception and later throws an identical exception. The original source location of the exception is lost.
608
+
609
+ ⚠️ At the moment, break and continue are not supported in a try or a catch block if there is a related finally block.
610
+
595
611
  ## Retrying on errors
596
612
 
597
613
  It is possible to set a retry policy for a try-catch statement. Because Typescript does not have `retry` keyword, the retry is implemented by a special `retry_policy` function. It must be called immediately after a try-catch block. A call to the `retry_policy` is ignored elsewhere.
598
614
 
599
- The `retry_policy` function must be called with parameters defining a retry policy. It can be either a policy provided by GCP Workflows or a custom retry policy. See the GCP documentation for the [required parameters for the two policy types](https://cloud.google.com/workflows/docs/reference/syntax/retrying#try-retry).
615
+ Finally and catch blocks are run after possible retry attempts. The following sample retries `http.get()` if it throws an exception and executes `sys.log('Error!')` and `closeConnection()` after retry attempts.
600
616
 
601
- A sample with a GCP-provided retry policy:
617
+ ```javascript
618
+ import { http, retry_policy, sys } from 'ts2workflows/types/workflowslib'
619
+
620
+ function main() {
621
+ try {
622
+ http.get('https://visit.dreamland.test/')
623
+ } catch (err) {
624
+ sys.log('Error!')
625
+ } finally {
626
+ closeConnection()
627
+ }
628
+ retry_policy(http.default_retry)
629
+ }
630
+ ```
631
+
632
+ The `retry_policy` function must be called with a parameter that defines the retry policy. It can be either a policy provided by GCP Workflows or a custom retry policy.
633
+
634
+ ### GCP-provided retry policy
635
+
636
+ GCP retry policy must be either `http.default_retry` or `http.default_retry_non_idempotent`. Their effects are described by the [GCP documentation](https://cloud.google.com/workflows/docs/reference/syntax/retrying#default-retry-policy).
602
637
 
603
638
  ```javascript
604
639
  import { http, retry_policy } from 'ts2workflows/types/workflowslib'
@@ -609,11 +644,15 @@ function main() {
609
644
  } catch (err) {
610
645
  return 'Error!'
611
646
  }
612
- retry_policy({ policy: http.default_retry })
647
+ retry_policy(http.default_retry)
613
648
  }
614
649
  ```
615
650
 
616
- A sample with a custom retry policy:
651
+ ### Custom retry policy
652
+
653
+ A custom retry policy is an object with the properties shown in the following example. See the GCP documentation for the [explanation of the properties](https://cloud.google.com/workflows/docs/reference/syntax/retrying#try-retry).
654
+
655
+ The parameter must be a literal map object (not a variable). The values may be literals or expressions.
617
656
 
618
657
  ```javascript
619
658
  import { http, retry_policy } from 'ts2workflows/types/workflowslib'
@@ -766,11 +805,9 @@ The `parallel` function executes code blocks in parallel (using [parallel step](
766
805
  ```typescript
767
806
  function retry_policy(
768
807
  params:
808
+ | ((errormap: Record<string, any>) => void)
769
809
  | {
770
- policy: (exception: unknown) => void
771
- }
772
- | {
773
- predicate: (exception: unknown) => boolean
810
+ predicate: (errormap: Record<string, any>) => boolean
774
811
  max_retries: number
775
812
  backoff: {
776
813
  initial_delay: number
@@ -781,7 +818,7 @@ function retry_policy(
781
818
  ): void
782
819
  ```
783
820
 
784
- The `retry_policy` function can called right after a `try`-`catch` block to specify a retry policy. See the section on retrying.
821
+ The `retry_policy` function can called right after a `try`-`catch` block to specify a retry policy. See the section on [retrying errors](#retrying-on-errors).
785
822
 
786
823
  ## Source code comments
787
824
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts2workflows",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Transpile Typescript code to GCP Workflows programs",
5
5
  "homepage": "https://github.com/aajanki/ts2workflows",
6
6
  "repository": {
@@ -57,7 +57,7 @@
57
57
  ],
58
58
  "devDependencies": {
59
59
  "@eslint/js": "^9.10.0",
60
- "@types/chai": "^4.3.16",
60
+ "@types/chai": "^5.0.1",
61
61
  "@types/mocha": "^10.0.6",
62
62
  "@types/node": "^18",
63
63
  "@typescript-eslint/eslint-plugin": "^8.0.0",
package/types/global.d.ts CHANGED
@@ -10,7 +10,15 @@ declare global {
10
10
 
11
11
  var Symbol: SymbolConstructor
12
12
 
13
- interface Iterator<T> {}
13
+ /**
14
+ * Iterator types copied from lib.es2015.iterable.d.ts
15
+ */
16
+ interface Iterator<T, TReturn = any, TNext = undefined> {
17
+ // NOTE: 'next' is defined using a tuple to ensure we report the correct assignability errors in all places.
18
+ next(...args: [] | [TNext]): IteratorResult<T, TReturn>
19
+ return?(value?: TReturn): IteratorResult<T, TReturn>
20
+ throw?(e?: any): IteratorResult<T, TReturn>
21
+ }
14
22
 
15
23
  interface Iterable<T> {
16
24
  [Symbol.iterator](): Iterator<T>