@hotmeshio/hotmesh 0.0.10 → 0.0.11

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.
Files changed (42) hide show
  1. package/README.md +5 -4
  2. package/build/modules/errors.d.ts +17 -1
  3. package/build/modules/errors.js +29 -1
  4. package/build/package.json +2 -1
  5. package/build/services/activities/activity.d.ts +1 -0
  6. package/build/services/activities/activity.js +13 -2
  7. package/build/services/activities/cycle.js +6 -1
  8. package/build/services/activities/trigger.js +2 -3
  9. package/build/services/collator/index.d.ts +8 -0
  10. package/build/services/collator/index.js +11 -1
  11. package/build/services/durable/factory.d.ts +18 -1
  12. package/build/services/durable/factory.js +46 -4
  13. package/build/services/durable/handle.js +25 -7
  14. package/build/services/durable/worker.d.ts +3 -3
  15. package/build/services/durable/worker.js +16 -10
  16. package/build/services/durable/workflow.js +1 -1
  17. package/build/services/pipe/functions/math.d.ts +4 -0
  18. package/build/services/pipe/functions/math.js +73 -0
  19. package/build/services/pipe/functions/number.d.ts +0 -4
  20. package/build/services/pipe/functions/number.js +0 -73
  21. package/build/services/signaler/stream.js +6 -3
  22. package/build/types/durable.d.ts +7 -2
  23. package/build/types/index.d.ts +1 -0
  24. package/modules/errors.ts +42 -1
  25. package/package.json +2 -1
  26. package/services/activities/activity.ts +14 -2
  27. package/services/activities/cycle.ts +6 -1
  28. package/services/activities/trigger.ts +2 -3
  29. package/services/collator/index.ts +12 -1
  30. package/services/durable/factory.ts +46 -4
  31. package/services/durable/handle.ts +23 -8
  32. package/services/durable/worker.ts +27 -12
  33. package/services/durable/workflow.ts +1 -1
  34. package/services/pipe/functions/math.ts +74 -0
  35. package/services/pipe/functions/number.ts +0 -75
  36. package/services/signaler/stream.ts +6 -3
  37. package/types/durable.ts +11 -4
  38. package/types/index.ts +15 -0
  39. package/build/services/dimension/index.d.ts +0 -29
  40. package/build/services/dimension/index.js +0 -35
  41. package/services/dimension/README.md +0 -73
  42. package/services/dimension/index.ts +0 -39
@@ -56,78 +56,5 @@ class NumberHandler {
56
56
  round(input) {
57
57
  return Math.round(input);
58
58
  }
59
- add(...operands) {
60
- // @ts-ignore
61
- return operands.reduce((a, b) => {
62
- if (Array.isArray(b)) {
63
- return a + this.add(...b);
64
- }
65
- else {
66
- return a + b;
67
- }
68
- }, 0);
69
- }
70
- subtract(...operands) {
71
- if (operands.length === 0) {
72
- throw new Error('At least one operand is required.');
73
- }
74
- let flatOperands = [];
75
- operands.forEach((op) => {
76
- if (Array.isArray(op)) {
77
- flatOperands = [...flatOperands, ...op];
78
- }
79
- else {
80
- flatOperands.push(op);
81
- }
82
- });
83
- if (flatOperands.length === 0) {
84
- throw new Error('At least one operand is required after flattening.');
85
- }
86
- const result = flatOperands.reduce((a, b, i) => {
87
- return i === 0 ? a : a - b;
88
- });
89
- return result;
90
- }
91
- multiply(...operands) {
92
- if (operands.length === 0) {
93
- throw new Error('At least one operand is required.');
94
- }
95
- // @ts-ignore
96
- return operands.reduce((a, b) => {
97
- if (Array.isArray(b)) {
98
- return a * this.multiply(...b);
99
- }
100
- else {
101
- return a * b;
102
- }
103
- }, 1);
104
- }
105
- divide(...operands) {
106
- if (operands.length === 0) {
107
- throw new Error('At least one operand is required.');
108
- }
109
- let flatOperands = [];
110
- operands.forEach((op) => {
111
- if (Array.isArray(op)) {
112
- flatOperands = [...flatOperands, ...op];
113
- }
114
- else {
115
- flatOperands.push(op);
116
- }
117
- });
118
- if (flatOperands.length === 0) {
119
- throw new Error('At least one operand is required after flattening.');
120
- }
121
- const result = flatOperands.reduce((a, b, i) => {
122
- if (b === 0) {
123
- return NaN;
124
- }
125
- return i === 0 ? a : a / b;
126
- });
127
- if (isNaN(result)) {
128
- return NaN;
129
- }
130
- return result;
131
- }
132
59
  }
133
60
  exports.NumberHandler = NumberHandler;
@@ -5,7 +5,7 @@ const key_1 = require("../../modules/key");
5
5
  const utils_1 = require("../../modules/utils");
6
6
  const telemetry_1 = require("../telemetry");
7
7
  const stream_1 = require("../../types/stream");
8
- const MAX_RETRIES = 4; //max delay (10s using exponential backoff);
8
+ const MAX_RETRIES = 3; //local retry; 10, 100, 1000ms
9
9
  const MAX_TIMEOUT_MS = 60000;
10
10
  const GRADUATED_INTERVAL_MS = 5000;
11
11
  const BLOCK_DURATION = 15000; //Set to `15` so SIGINT/SIGTERM can interrupt; set to `0` to BLOCK indefinitely
@@ -158,8 +158,11 @@ class StreamSignaler {
158
158
  const policy = policies?.[errorCode];
159
159
  const maxRetries = policy?.[0];
160
160
  const tryCount = Math.min(input.metadata.try || 0, MAX_RETRIES);
161
- if (maxRetries >= tryCount) {
162
- return [true, Math.pow(10, tryCount)];
161
+ //only possible values for maxRetries are 1, 2, 3
162
+ //only possible values for tryCount are 0, 1, 2
163
+ if (maxRetries > tryCount) {
164
+ // 10ms, 100ms, or 1000ms delays between system retries
165
+ return [true, Math.pow(10, tryCount + 1)];
163
166
  }
164
167
  return [false, 0];
165
168
  }
@@ -7,7 +7,7 @@ type WorkflowOptions = {
7
7
  workflowTrace?: string;
8
8
  workflowSpan?: string;
9
9
  };
10
- type ActivityDataType = {
10
+ type ActivityWorkflowDataType = {
11
11
  activityName: string;
12
12
  arguments: any[];
13
13
  workflowId: string;
@@ -35,6 +35,11 @@ type WorkerConfig = {
35
35
  namespace?: string;
36
36
  taskQueue: string;
37
37
  workflow: Function;
38
+ options?: WorkerOptions;
39
+ };
40
+ type WorkerOptions = {
41
+ maxSystemRetries?: number;
42
+ backoffExponent?: number;
38
43
  };
39
44
  type ContextType = {
40
45
  workflowId: string;
@@ -54,4 +59,4 @@ type ActivityConfig = {
54
59
  maximumInterval: string;
55
60
  };
56
61
  };
57
- export { ActivityConfig, ActivityDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkflowDataType, WorkflowOptions, };
62
+ export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, };
@@ -3,6 +3,7 @@ export { App, AppVID, AppTransitions, AppSubscriptions } from './app';
3
3
  export { AsyncSignal } from './async';
4
4
  export { CacheMode } from './cache';
5
5
  export { CollationFaultType, CollationStage } from './collator';
6
+ export { ActivityConfig, ActivityWorkflowDataType, ClientConfig, ContextType, ConnectionConfig, Connection, NativeConnection, ProxyType, Registry, WorkerConfig, WorkerOptions, WorkflowDataType, WorkflowOptions, } from './durable';
6
7
  export { HookCondition, HookConditions, HookGate, HookInterface, HookRule, HookRules, HookSignal } from './hook';
7
8
  export { RedisClientType as IORedisClientType, RedisMultiType as IORedisMultiType } from './ioredisclient';
8
9
  export { ILogger } from './logger';
package/modules/errors.ts CHANGED
@@ -12,6 +12,35 @@ class SetStateError extends Error {
12
12
  }
13
13
  }
14
14
 
15
+ class DurableTimeoutError extends Error {
16
+ code: number;
17
+ constructor(message: string) {
18
+ super(message);
19
+ this.code = 596;
20
+ }
21
+ }
22
+ class DurableMaxedError extends Error {
23
+ code: number;
24
+ constructor(message: string) {
25
+ super(message);
26
+ this.code = 597;
27
+ }
28
+ }
29
+ class DurableFatalError extends Error {
30
+ code: number;
31
+ constructor(message: string) {
32
+ super(message);
33
+ this.code = 598;
34
+ }
35
+ }
36
+ class DurableRetryError extends Error {
37
+ code: number;
38
+ constructor(message: string) {
39
+ super(message);
40
+ this.code = 599;
41
+ }
42
+ }
43
+
15
44
  class MapDataError extends Error {
16
45
  constructor() {
17
46
  super("Error occurred while mapping data");
@@ -52,4 +81,16 @@ class CollationError extends Error {
52
81
  }
53
82
  }
54
83
 
55
- export { CollationError, DuplicateJobError, GetStateError, SetStateError, MapDataError, RegisterTimeoutError, ExecActivityError };
84
+ export {
85
+ CollationError,
86
+ DurableTimeoutError,
87
+ DurableMaxedError,
88
+ DurableFatalError,
89
+ DurableRetryError,
90
+ DuplicateJobError,
91
+ GetStateError,
92
+ SetStateError,
93
+ MapDataError,
94
+ RegisterTimeoutError,
95
+ ExecActivityError
96
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hotmeshio/hotmesh",
3
- "version": "0.0.10",
3
+ "version": "0.0.11",
4
4
  "description": "Durable Workflows",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -43,6 +43,7 @@
43
43
  "test:durable:hello": "NODE_ENV=test jest ./tests/durable/helloworld/index.test.ts --detectOpenHandles --forceExit --verbose",
44
44
  "test:durable:goodbye": "NODE_ENV=test jest ./tests/durable/goodbye/index.test.ts --detectOpenHandles --forceExit --verbose",
45
45
  "test:durable:retry": "NODE_ENV=test jest ./tests/durable/retry/index.test.ts --detectOpenHandles --forceExit --verbose",
46
+ "test:durable:fatal": "NODE_ENV=test jest ./tests/durable/fatal/index.test.ts --detectOpenHandles --forceExit --verbose",
46
47
  "test:durable:loopactivity": "NODE_ENV=test jest ./tests/durable/loopactivity/index.test.ts --detectOpenHandles --forceExit --verbose",
47
48
  "test:durable:nested": "NODE_ENV=test jest ./tests/durable/nested/index.test.ts --detectOpenHandles --forceExit --verbose"
48
49
  },
@@ -4,7 +4,6 @@ import {
4
4
  getValueByPath,
5
5
  restoreHierarchy } from '../../modules/utils';
6
6
  import { CollatorService } from '../collator';
7
- import { DimensionService } from '../dimension';
8
7
  import { EngineService } from '../engine';
9
8
  import { ILogger } from '../logger';
10
9
  import { MapperService } from '../mapper';
@@ -84,6 +83,7 @@ class Activity {
84
83
  if (this.doesHook()) {
85
84
  //sleep and wait to awaken upon a signal
86
85
  await this.registerHook(multi);
86
+ this.mapOutputData();
87
87
  this.mapJobData();
88
88
  await this.setState(multi);
89
89
  await CollatorService.authorizeReentry(this, multi);
@@ -94,6 +94,7 @@ class Activity {
94
94
  } else {
95
95
  //end the activity and transition to its children
96
96
  this.adjacencyList = await this.filterAdjacent();
97
+ this.mapOutputData();
97
98
  this.mapJobData();
98
99
  await this.setState(multi);
99
100
  await CollatorService.notarizeEarlyCompletion(this, multi);
@@ -322,6 +323,17 @@ class Activity {
322
323
  }
323
324
  }
324
325
 
326
+ mapOutputData(): void {
327
+ //activity YAML may include output map data that produces/extends activity output data.
328
+ if(this.config.output?.maps) {
329
+ const mapper = new MapperService(this.config.output.maps, this.context);
330
+ const actOutData = mapper.mapRules();
331
+ const activityId = this.metadata.aid;
332
+ const data = { ...this.context[activityId].output, ...actOutData };
333
+ this.context[activityId].output.data = data;
334
+ }
335
+ }
336
+
325
337
  async registerTimeout(): Promise<void> {
326
338
  //set timeout in support of hook and/or duplex
327
339
  }
@@ -526,7 +538,7 @@ class Activity {
526
538
 
527
539
  resolveAdjacentDad(): string {
528
540
  //concat self and child dimension (all children (leg 1) begin life at 0)
529
- return `${this.resolveDad()}${DimensionService.getSeed(0)}`;
541
+ return `${this.resolveDad()}${CollatorService.getDimensionalSeed(0)}`;
530
542
  };
531
543
 
532
544
  async filterAdjacent(): Promise<StreamData[]> {
@@ -81,13 +81,18 @@ class Cycle extends Activity {
81
81
  * pattern allows for retries without violating the DAG.
82
82
  */
83
83
  async cycleAncestorActivity(multi: RedisMulti): Promise<string> {
84
+ //Cycle activity L1 is a standin for the target ancestor L1.
85
+ //Input data mapping (mapInputData) allows for the
86
+ //next dimensonal thread to execute with different
87
+ //input data than the current dimensional thread
88
+ this.mapInputData();
84
89
  const streamData: StreamData = {
85
90
  metadata: {
86
91
  dad: CollatorService.resolveReentryDimension(this),
87
92
  jid: this.context.metadata.jid,
88
93
  aid: this.config.ancestor,
89
94
  },
90
- data: {} //todo: verify immutability, before enabling: `this.context.data`
95
+ data: this.context.data
91
96
  };
92
97
  return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
93
98
  }
@@ -3,7 +3,6 @@ import { DuplicateJobError } from '../../modules/errors';
3
3
  import { formatISODate, getTimeSeries } from '../../modules/utils';
4
4
  import { Activity } from './activity';
5
5
  import { CollatorService } from '../collator';
6
- import { DimensionService } from '../dimension';
7
6
  import { EngineService } from '../engine';
8
7
  import { Pipe } from '../pipe';
9
8
  import { ReporterService } from '../reporter';
@@ -99,7 +98,7 @@ class Trigger extends Activity {
99
98
 
100
99
  const utc = formatISODate(new Date());
101
100
  const { id, version } = await this.engine.getVID();
102
- this.initDimensionalAddress(DimensionService.getSeed());
101
+ this.initDimensionalAddress(CollatorService.getDimensionalSeed());
103
102
  const activityMetadata = {
104
103
  ...this.metadata,
105
104
  jid: jobId,
@@ -119,7 +118,7 @@ class Trigger extends Activity {
119
118
  trc: this.context.metadata.trc,
120
119
  spn: this.context.metadata.spn,
121
120
  jid: jobId,
122
- dad: DimensionService.getSeed(), //top-level job implicitly uses `,0`
121
+ dad: CollatorService.getDimensionalSeed(), //top-level job implicitly uses `,0`
123
122
  key: jobKey,
124
123
  jc: utc,
125
124
  ju: utc,
@@ -85,7 +85,7 @@ class CollatorService {
85
85
  static async notarizeCompletion(activity: Activity, multi?: RedisMulti): Promise<number> {
86
86
  //1) ALWAYS actualize leg2 dimension (+1)
87
87
  //2) IF the activity is used in a cycle, don't close leg 2!
88
- const decrement = activity.config.cycle ? 0 : -1_000_000_000_000;
88
+ const decrement = activity.config.cycle ? 0 : 1_000_000_000_000;
89
89
  return await activity.store.collate(activity.context.metadata.jid, activity.metadata.aid, 1 - decrement, this.getDimensionalAddress(activity), multi);
90
90
  };
91
91
 
@@ -234,6 +234,17 @@ class CollatorService {
234
234
  static isActivityComplete(status: number): boolean {
235
235
  return (status - 0) <= 0;
236
236
  }
237
+
238
+ /**
239
+ * All activities exist on a dimensional plane. Zero
240
+ * is the default. A value of
241
+ * `AxY,0,0,0,0,1,0,0` would reflect that
242
+ * an ancestor activity was dimensionalized beyond
243
+ * the default.
244
+ */
245
+ static getDimensionalSeed(index = 0): string {
246
+ return `,${index}`;
247
+ }
237
248
  }
238
249
 
239
250
  export { CollatorService };
@@ -1,4 +1,21 @@
1
- const getWorkflowYAML = (topic: string, version = '1') => {
1
+ /**
2
+ * 1) `maxSystemRetries` | can be 0 to 3 and represents milliseconds;
3
+ * if there is an error, the workflow will retry up to `maxSystemRetries` times
4
+ * delaying by 10, 100, and 1000ms; this is a system level retry
5
+ * and is not configurable. It exists to handle intermittent network
6
+ * errors. (NOTE: each retry spawns a new transition stream)
7
+ *
8
+ * 2) `backoffExponent` | can be any number and represents `seconds` when applied;
9
+ * retries will happen indefinitely and adhere to the
10
+ * exponential backoff algorithm by multiplying by `backoffExponent`.
11
+ * For example, if `backoffExponent` is 10, the workflow will retry
12
+ * in 10s, 100s, 1000s, 10000s, etc.
13
+ *
14
+ * EXAMPLE | Using `maxSystemRetries = 3` and `backoffExponent = 10`, errant
15
+ * workflows will be retried on the following schedule (8 times in 27 hours):
16
+ * => 10ms, 100ms, 1000ms, 10s, 100s, 1_000s, 10_000s, 100_000s
17
+ */
18
+ const getWorkflowYAML = (topic: string, version = '1', maxSystemRetries = 2, backoffExponent = 10) => {
2
19
  return `app:
3
20
  id: ${topic}
4
21
  version: '${version}'
@@ -30,10 +47,20 @@ const getWorkflowYAML = (topic: string, version = '1') => {
30
47
  a1:
31
48
  type: activity
32
49
  cycle: true
33
-
50
+ output:
51
+ schema:
52
+ type: object
53
+ properties:
54
+ duration:
55
+ type: number
56
+ maps:
57
+ duration: ${backoffExponent}
58
+
34
59
  w1:
35
60
  type: worker
36
61
  topic: ${topic}
62
+ retry:
63
+ '599': [${maxSystemRetries}]
37
64
  input:
38
65
  schema:
39
66
  type: object
@@ -55,18 +82,33 @@ const getWorkflowYAML = (topic: string, version = '1') => {
55
82
  maps:
56
83
  response: '{$self.output.data.response}'
57
84
 
85
+ a599:
86
+ title: Sleep before trying again
87
+ type: activity
88
+ sleep: "{a1.output.data.duration}"
89
+
58
90
  c1:
91
+ title: Goto Activity a1
59
92
  type: cycle
60
93
  ancestor: a1
94
+ input:
95
+ maps:
96
+ duration:
97
+ '@pipe':
98
+ - ['{a1.output.data.duration}', ${backoffExponent}]
99
+ - ['{@math.multiply}']
100
+
61
101
  transitions:
62
102
  t1:
63
103
  - to: a1
64
104
  a1:
65
105
  - to: w1
66
106
  w1:
67
- - to: c1
107
+ - to: a599
68
108
  conditions:
69
- code: 500
109
+ code: 599
110
+ a599:
111
+ - to: c1
70
112
  `;
71
113
  }
72
114
 
@@ -16,22 +16,37 @@ export class WorkflowHandleService {
16
16
  let status = await this.hotMesh.getStatus(this.workflowId);
17
17
  const topic = `${this.workflowTopic}.${this.workflowId}`;
18
18
 
19
- if (status == 0) {
20
- return (await this.hotMesh.getState(this.workflowTopic, this.workflowId)).data?.response;
21
- }
22
-
23
19
  return new Promise((resolve, reject) => {
24
20
  let isResolved = false;
25
21
  //common fulfill/unsubscribe
26
- const complete = async (response?: any) => {
22
+ const complete = async (response?: any, err?: string) => {
27
23
  if (isResolved) return;
28
24
  isResolved = true;
29
25
  this.hotMesh.unsub(topic);
30
- resolve(response || (await this.hotMesh.getState(this.workflowTopic, this.workflowId)).data?.response);
26
+ if (err) {
27
+ return reject(JSON.parse(err));
28
+ } else if (!response) {
29
+ const state = await this.hotMesh.getState(this.workflowTopic, this.workflowId);
30
+ if (!state.data && state.metadata.err) {
31
+ return reject(JSON.parse(state.metadata.err));
32
+ }
33
+ response = state.data?.response;
34
+ }
35
+ resolve(response);
31
36
  };
32
- this.hotMesh.sub(topic, async (topic: string, message: JobOutput) => {
33
- await complete(message.data?.response);
37
+ //check for done
38
+ if (status == 0) {
39
+ return complete();
40
+ }
41
+ //subscribe to topic
42
+ this.hotMesh.sub(topic, async (topic: string, state: JobOutput) => {
43
+ if (!state.data && state.metadata.err) {
44
+ await complete(null, state.metadata.err);
45
+ } else {
46
+ await complete(state.data?.response);
47
+ }
34
48
  });
49
+ //resolve for race condition
35
50
  setTimeout(async () => {
36
51
  status = await this.hotMesh.getStatus(this.workflowId);
37
52
  if (status == 0) {
@@ -2,8 +2,18 @@ import { asyncLocalStorage } from './asyncLocalStorage';
2
2
  import { HotMeshService as HotMesh } from '../hotmesh';
3
3
  import { RedisClass, RedisOptions } from '../../types/redis';
4
4
  import { StreamData, StreamDataResponse, StreamStatus } from '../../types/stream';
5
- import { ActivityDataType, Connection, Registry, WorkerConfig, WorkflowDataType } from "../../types/durable";
5
+ import { ActivityWorkflowDataType,
6
+ Connection,
7
+ Registry,
8
+ WorkerConfig,
9
+ WorkerOptions,
10
+ WorkflowDataType } from "../../types/durable";
6
11
  import { getWorkflowYAML, getActivityYAML } from './factory';
12
+ import {
13
+ DurableFatalError,
14
+ DurableMaxedError,
15
+ DurableRetryError,
16
+ DurableTimeoutError } from '../../modules/errors';
7
17
 
8
18
  /*
9
19
  Here is an example of how the methods in this file are used:
@@ -45,7 +55,7 @@ export class WorkerService {
45
55
  workflowRunner: HotMesh;
46
56
  activityRunner: HotMesh;
47
57
 
48
- static getHotMesh = async (worflowTopic: string) => {
58
+ static getHotMesh = async (worflowTopic: string, options?: WorkerOptions) => {
49
59
  if (WorkerService.instances.has(worflowTopic)) {
50
60
  return await WorkerService.instances.get(worflowTopic);
51
61
  }
@@ -54,17 +64,17 @@ export class WorkerService {
54
64
  engine: { redis: { ...WorkerService.connection } }
55
65
  });
56
66
  WorkerService.instances.set(worflowTopic, hotMesh);
57
- await WorkerService.activateWorkflow(await hotMesh, worflowTopic, getWorkflowYAML);
67
+ await WorkerService.activateWorkflow(await hotMesh, worflowTopic, getWorkflowYAML, options);
58
68
  return hotMesh;
59
69
  }
60
70
 
61
- static async activateWorkflow(hotMesh: HotMesh, topic: string, factory: Function) {
71
+ static async activateWorkflow(hotMesh: HotMesh, topic: string, dagFactory: Function, options: WorkerOptions = {}) {
62
72
  const version = '1';
63
73
  const app = await hotMesh.engine.store.getApp(topic);
64
74
  const appVersion = app?.version;
65
75
  if(!appVersion) {
66
76
  try {
67
- await hotMesh.deploy(factory(topic, version));
77
+ await hotMesh.deploy(dagFactory(topic, version, options.maxSystemRetries, options.backoffExponent));
68
78
  await hotMesh.activate(version);
69
79
  } catch (err) {
70
80
  hotMesh.engine.logger.error('durable-worker-deploy-activate-err', err);
@@ -117,7 +127,7 @@ export class WorkerService {
117
127
  worker.activityRunner = await worker.initActivityWorkflow(config, activityTopic);
118
128
  await WorkerService.activateWorkflow(worker.activityRunner, activityTopic, getActivityYAML);
119
129
  worker.workflowRunner = await worker.initWorkerWorkflow(config, workflowTopic, workflowFunction);
120
- await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML);
130
+ await WorkerService.activateWorkflow(worker.workflowRunner, workflowTopic, getWorkflowYAML, config.options);
121
131
  return worker;
122
132
  }
123
133
 
@@ -160,7 +170,7 @@ export class WorkerService {
160
170
  return async (data: StreamData): Promise<StreamDataResponse> => {
161
171
  try {
162
172
  //always run the activity function when instructed; return the response
163
- const activityInput = data.data as unknown as ActivityDataType;
173
+ const activityInput = data.data as unknown as ActivityWorkflowDataType;
164
174
  const activityName = activityInput.activityName;
165
175
  const activityFunction = WorkerService.activityRegistry[activityName];
166
176
  const pojoResponse = await activityFunction.apply(this, activityInput.arguments);
@@ -172,12 +182,16 @@ export class WorkerService {
172
182
  };
173
183
  } catch (err) {
174
184
  this.activityRunner.engine.logger.error('durable-worker-activity-err', err);
185
+ if (!(err instanceof DurableTimeoutError) &&
186
+ !(err instanceof DurableMaxedError) &&
187
+ !(err instanceof DurableFatalError)) {
188
+ err = new DurableRetryError(err.message);
189
+ }
175
190
  return {
176
191
  status: StreamStatus.ERROR,
177
- code: 500,
178
- message: err.message,
192
+ code: err.code,
179
193
  metadata: { ...data.metadata },
180
- data: { error: err }
194
+ data: { message: err.message }
181
195
  } as StreamDataResponse;
182
196
  }
183
197
  }
@@ -257,11 +271,12 @@ export class WorkerService {
257
271
  data: { response: workflowResponse }
258
272
  };
259
273
  } catch (err) {
274
+ // 59* - Durable*Error
260
275
  return {
261
276
  status: StreamStatus.ERROR,
262
- code: 500,
277
+ code: err.code || new DurableRetryError(err.message).code,
263
278
  metadata: { ...data.metadata },
264
- data: { error: err }
279
+ data: { message: err.message, type: err.name }
265
280
  } as StreamDataResponse;
266
281
  }
267
282
  }
@@ -62,7 +62,7 @@ export class WorkflowService {
62
62
  const client = new Client({
63
63
  connection: await Connection.connect(WorkerService.connection),
64
64
  });
65
- //todo: should I allow-cross/app callback (pj:'@DURABLE@hello-world@<pjid>'/pa: <paid>/pd: <pdad>)
65
+ //todo: allow cross/app callback (pj:'@DURABLE@hello-world@<pjid>'/pa: <paid>/pd: <pdad>)
66
66
  const handle = await client.workflow.start({
67
67
  ...options,
68
68
  workflowId: `${workflowId}${options.workflowId}`, //concat
@@ -1,4 +1,78 @@
1
1
  class MathHandler {
2
+ add(...operands: (number | number[])[]): number {
3
+ // @ts-ignore
4
+ return operands.reduce((a: number, b: number | number[]) => {
5
+ if (Array.isArray(b)) {
6
+ return a + this.add(...b);
7
+ } else {
8
+ return a + b;
9
+ }
10
+ }, 0);
11
+ }
12
+
13
+ subtract(...operands: (number | number[])[]): number {
14
+ if (operands.length === 0) {
15
+ throw new Error('At least one operand is required.');
16
+ }
17
+ let flatOperands: number[] = [];
18
+ operands.forEach((op: number | number[]) => {
19
+ if (Array.isArray(op)) {
20
+ flatOperands = [...flatOperands, ...op];
21
+ } else {
22
+ flatOperands.push(op);
23
+ }
24
+ });
25
+ if (flatOperands.length === 0) {
26
+ throw new Error('At least one operand is required after flattening.');
27
+ }
28
+ const result = flatOperands.reduce((a: number, b: number, i: number) => {
29
+ return i === 0 ? a : a - b;
30
+ });
31
+ return result;
32
+ }
33
+
34
+ multiply(...operands: (number | number[])[]): number {
35
+ if (operands.length === 0) {
36
+ throw new Error('At least one operand is required.');
37
+ }
38
+
39
+ // @ts-ignore
40
+ return operands.reduce((a: number, b: number | number[]) => {
41
+ if (Array.isArray(b)) {
42
+ return a * this.multiply(...b);
43
+ } else {
44
+ return a * b;
45
+ }
46
+ }, 1);
47
+ }
48
+
49
+ divide(...operands: (number | number[])[]): number {
50
+ if (operands.length === 0) {
51
+ throw new Error('At least one operand is required.');
52
+ }
53
+ let flatOperands: number[] = [];
54
+ operands.forEach((op: number | number[]) => {
55
+ if (Array.isArray(op)) {
56
+ flatOperands = [...flatOperands, ...op];
57
+ } else {
58
+ flatOperands.push(op);
59
+ }
60
+ });
61
+ if (flatOperands.length === 0) {
62
+ throw new Error('At least one operand is required after flattening.');
63
+ }
64
+ const result = flatOperands.reduce((a: number, b: number, i: number) => {
65
+ if (b === 0) {
66
+ return NaN;
67
+ }
68
+ return i === 0 ? a : a / b;
69
+ });
70
+ if (isNaN(result)) {
71
+ return NaN;
72
+ }
73
+ return result;
74
+ }
75
+
2
76
  abs(x: number): number {
3
77
  return Math.abs(x);
4
78
  }