@hotmeshio/hotmesh 0.0.10 → 0.0.12

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 (50) hide show
  1. package/README.md +8 -7
  2. package/build/modules/errors.d.ts +17 -1
  3. package/build/modules/errors.js +29 -1
  4. package/build/package.json +3 -2
  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 +8 -1
  8. package/build/services/activities/trigger.js +4 -5
  9. package/build/services/collator/index.d.ts +8 -0
  10. package/build/services/collator/index.js +11 -1
  11. package/build/services/connector/index.js +3 -3
  12. package/build/services/durable/client.js +2 -2
  13. package/build/services/durable/connection.js +2 -2
  14. package/build/services/durable/factory.d.ts +18 -1
  15. package/build/services/durable/factory.js +46 -4
  16. package/build/services/durable/handle.js +25 -7
  17. package/build/services/durable/worker.d.ts +3 -3
  18. package/build/services/durable/worker.js +16 -10
  19. package/build/services/durable/workflow.js +1 -1
  20. package/build/services/hotmesh/index.js +2 -2
  21. package/build/services/pipe/functions/math.d.ts +4 -0
  22. package/build/services/pipe/functions/math.js +73 -0
  23. package/build/services/pipe/functions/number.d.ts +0 -4
  24. package/build/services/pipe/functions/number.js +0 -73
  25. package/build/services/signaler/stream.js +6 -3
  26. package/build/types/durable.d.ts +7 -2
  27. package/build/types/index.d.ts +1 -0
  28. package/modules/errors.ts +42 -1
  29. package/package.json +3 -2
  30. package/services/activities/activity.ts +14 -2
  31. package/services/activities/cycle.ts +8 -1
  32. package/services/activities/trigger.ts +4 -5
  33. package/services/collator/index.ts +12 -1
  34. package/services/connector/index.ts +3 -3
  35. package/services/durable/client.ts +2 -2
  36. package/services/durable/connection.ts +2 -2
  37. package/services/durable/factory.ts +46 -4
  38. package/services/durable/handle.ts +23 -8
  39. package/services/durable/worker.ts +27 -12
  40. package/services/durable/workflow.ts +1 -1
  41. package/services/hotmesh/index.ts +2 -2
  42. package/services/pipe/functions/math.ts +74 -0
  43. package/services/pipe/functions/number.ts +0 -75
  44. package/services/signaler/stream.ts +6 -3
  45. package/types/durable.ts +11 -4
  46. package/types/index.ts +15 -0
  47. package/build/services/dimension/index.d.ts +0 -29
  48. package/build/services/dimension/index.js +0 -35
  49. package/services/dimension/README.md +0 -73
  50. package/services/dimension/index.ts +0 -39
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HotMeshService = void 0;
4
- const uuid_1 = require("uuid");
4
+ const nanoid_1 = require("nanoid");
5
5
  const key_1 = require("../../modules/key");
6
6
  const engine_1 = require("../engine");
7
7
  const logger_1 = require("../logger");
@@ -39,7 +39,7 @@ class HotMeshService {
39
39
  }
40
40
  static async init(config) {
41
41
  const instance = new HotMeshService();
42
- instance.guid = (0, uuid_1.v4)();
42
+ instance.guid = (0, nanoid_1.nanoid)();
43
43
  instance.verifyAndSetNamespace(config.namespace);
44
44
  instance.verifyAndSetAppId(config.appId);
45
45
  instance.logger = new logger_1.LoggerService(config.appId, instance.guid, config.name || '', config.logLevel);
@@ -1,4 +1,8 @@
1
1
  declare class MathHandler {
2
+ add(...operands: (number | number[])[]): number;
3
+ subtract(...operands: (number | number[])[]): number;
4
+ multiply(...operands: (number | number[])[]): number;
5
+ divide(...operands: (number | number[])[]): number;
2
6
  abs(x: number): number;
3
7
  acos(x: number): number;
4
8
  acosh(x: number): number;
@@ -2,6 +2,79 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.MathHandler = void 0;
4
4
  class MathHandler {
5
+ add(...operands) {
6
+ // @ts-ignore
7
+ return operands.reduce((a, b) => {
8
+ if (Array.isArray(b)) {
9
+ return a + this.add(...b);
10
+ }
11
+ else {
12
+ return a + b;
13
+ }
14
+ }, 0);
15
+ }
16
+ subtract(...operands) {
17
+ if (operands.length === 0) {
18
+ throw new Error('At least one operand is required.');
19
+ }
20
+ let flatOperands = [];
21
+ operands.forEach((op) => {
22
+ if (Array.isArray(op)) {
23
+ flatOperands = [...flatOperands, ...op];
24
+ }
25
+ else {
26
+ flatOperands.push(op);
27
+ }
28
+ });
29
+ if (flatOperands.length === 0) {
30
+ throw new Error('At least one operand is required after flattening.');
31
+ }
32
+ const result = flatOperands.reduce((a, b, i) => {
33
+ return i === 0 ? a : a - b;
34
+ });
35
+ return result;
36
+ }
37
+ multiply(...operands) {
38
+ if (operands.length === 0) {
39
+ throw new Error('At least one operand is required.');
40
+ }
41
+ // @ts-ignore
42
+ return operands.reduce((a, b) => {
43
+ if (Array.isArray(b)) {
44
+ return a * this.multiply(...b);
45
+ }
46
+ else {
47
+ return a * b;
48
+ }
49
+ }, 1);
50
+ }
51
+ divide(...operands) {
52
+ if (operands.length === 0) {
53
+ throw new Error('At least one operand is required.');
54
+ }
55
+ let flatOperands = [];
56
+ operands.forEach((op) => {
57
+ if (Array.isArray(op)) {
58
+ flatOperands = [...flatOperands, ...op];
59
+ }
60
+ else {
61
+ flatOperands.push(op);
62
+ }
63
+ });
64
+ if (flatOperands.length === 0) {
65
+ throw new Error('At least one operand is required after flattening.');
66
+ }
67
+ const result = flatOperands.reduce((a, b, i) => {
68
+ if (b === 0) {
69
+ return NaN;
70
+ }
71
+ return i === 0 ? a : a / b;
72
+ });
73
+ if (isNaN(result)) {
74
+ return NaN;
75
+ }
76
+ return result;
77
+ }
5
78
  abs(x) {
6
79
  return Math.abs(x);
7
80
  }
@@ -17,9 +17,5 @@ declare class NumberHandler {
17
17
  min(...values: number[]): number;
18
18
  pow(base: number, exponent: number): number;
19
19
  round(input: number): number;
20
- add(...operands: (number | number[])[]): number;
21
- subtract(...operands: (number | number[])[]): number;
22
- multiply(...operands: (number | number[])[]): number;
23
- divide(...operands: (number | number[])[]): number;
24
20
  }
25
21
  export { NumberHandler };
@@ -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.12",
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
  },
@@ -54,7 +55,7 @@
54
55
  "@opentelemetry/api": "^1.4.1",
55
56
  "js-yaml": "^4.1.0",
56
57
  "ms": "^2.1.3",
57
- "uuid": "^9.0.1",
58
+ "nanoid": "^3.3.6",
58
59
  "winston": "^3.8.2"
59
60
  },
60
61
  "devDependencies": {
@@ -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,20 @@ 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,
94
+ spn: this.context['$self'].output.metadata?.l1s,
95
+ trc: this.context.metadata.trc,
89
96
  },
90
- data: {} //todo: verify immutability, before enabling: `this.context.data`
97
+ data: this.context.data
91
98
  };
92
99
  return (await this.engine.streamSignaler?.publishMessage(null, streamData, multi)) as string;
93
100
  }
@@ -1,9 +1,8 @@
1
- import { v4 as uuidv4 } from 'uuid';
1
+ import { nanoid } from 'nanoid';
2
2
  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,
@@ -161,7 +160,7 @@ class Trigger extends Activity {
161
160
 
162
161
  resolveJobId(context: Partial<JobState>): string {
163
162
  const jobId = this.config.stats?.id;
164
- return jobId ? Pipe.resolve(jobId, context) : uuidv4();
163
+ return jobId ? Pipe.resolve(jobId, context) : nanoid();
165
164
  }
166
165
 
167
166
  resolveJobKey(context: Partial<JobState>): string {
@@ -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,4 @@
1
- import { v4 as uuidv4 } from 'uuid';
1
+ import { nanoid } from 'nanoid';
2
2
 
3
3
  import { identifyRedisTypeFromClass } from '../../modules/utils';
4
4
  import { RedisConnection as IORedisConnection } from '../connector/clients/ioredis';
@@ -23,14 +23,14 @@ export class ConnectorService {
23
23
  if (identifyRedisTypeFromClass(Redis) === 'redis') {
24
24
  for (let i = 1; i <= 3; i++) {
25
25
  instances.push(RedisConnection.connect(
26
- uuidv4(),
26
+ nanoid(),
27
27
  Redis as RedisClassType,
28
28
  options as RedisClientOptions));
29
29
  }
30
30
  } else {
31
31
  for (let i = 1; i <= 3; i++) {
32
32
  instances.push(IORedisConnection.connect(
33
- uuidv4(),
33
+ nanoid(),
34
34
  Redis as IORedisClassType,
35
35
  options as IORedisClientOptions));
36
36
  }
@@ -12,7 +12,7 @@ Here is an example of how the methods in this file are used:
12
12
  import { Durable } from '@hotmeshio/hotmesh';
13
13
  import Redis from 'ioredis';
14
14
  import { example } from './workflows';
15
- import { v4 as uuidv4 } from 'uuid';
15
+ import { nanoid } from 'nanoid';
16
16
 
17
17
  async function run() {
18
18
  const connection = await Durable.Connection.connect({
@@ -31,7 +31,7 @@ async function run() {
31
31
  args: ['HotMesh'],
32
32
  taskQueue: 'hello-world',
33
33
  workflowName: 'example',
34
- workflowId: 'workflow-' + uuidv4(),
34
+ workflowId: 'workflow-' + nanoid(),
35
35
  });
36
36
 
37
37
  console.log(`Started workflow ${handle.workflowId}`);
@@ -7,7 +7,7 @@ Here is an example of how the methods in this file are used:
7
7
 
8
8
  import { Durable } from '@hotmeshio/hotmesh';
9
9
  import Redis from 'ioredis';
10
- import { v4 as uuidv4 } from 'uuid';
10
+ import { nanoid } from 'nanoid';
11
11
 
12
12
  async function run() {
13
13
  const connection = await Durable.Connection.connect({
@@ -26,7 +26,7 @@ async function run() {
26
26
  taskQueue: 'hello-world',
27
27
  args: ['HotMesh'],
28
28
  workflowName: 'example',
29
- workflowId: uuidv4(),
29
+ workflowId: nanoid(),
30
30
  });
31
31
 
32
32
  console.log(`Started workflow ${handle.workflowId}`);
@@ -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) {